feat: persistent metadata panel, symmetric sidebar toggles, keyboard nav scroll

- Right sidebar stays open by default and shows an empty state when
  nothing is selected, instead of auto-hiding on deselect.
- Both sidebars now have a collapse button in their header and an
  expand button in the TopBar that only appears when collapsed, so
  each panel has a discoverable affordance in either state.
- Arrow-key navigation auto-scrolls the destination row into view
  with a ~35% peek margin, cueing the user that there's more content
  in the scroll direction.
- Fix: the width sentinel's measurement effect never installed its
  ResizeObserver when Timeline first rendered the loading state (ref
  was null, empty-dep effect didn't re-run), so containerWidth stuck
  at 0 and the grid fell back to 4 columns × 200px forever. Switched
  to a callback ref that attaches the observer the moment the
  sentinel actually mounts.
- KeyboardHints surface the Tab (library) and I (info) shortcuts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 16:20:22 +02:00
parent a4b1802657
commit d6c667ae78
6 changed files with 189 additions and 61 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { Timeline } from './components/timeline/Timeline'
import { LeftSidebar } from './components/layout/LeftSidebar'
import { RightSidebar } from './components/layout/RightSidebar'
@@ -18,9 +18,8 @@ import { usePhotosQuery } from './hooks/usePhotosQuery'
function App() {
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true)
const [rightSidebarOpen, setRightSidebarOpen] = useState(false)
const [rightSidebarOpen, setRightSidebarOpen] = useState(true)
const [settingsOpen, setSettingsOpen] = useState(false)
const selectedPhotos = usePhotoStore((state) => state.selectedPhotos)
const viewMode = usePhotoStore((state) => state.viewMode)
// Bidirectional sync of filter store with URL query params.
@@ -38,24 +37,19 @@ function App() {
getFirstPhotoId: () => allPhotos?.[0]?.id ?? null,
})
// Auto-show right sidebar when photos are selected — but only in grid mode,
// so leaving the preview doesn't fight the user's prior sidebar state.
// Lives in an effect (not the render body) to avoid setState-during-render
// and the cascading re-renders the audit caught.
useEffect(() => {
if (viewMode !== 'grid') return
if (selectedPhotos.length > 0 && !rightSidebarOpen) {
setRightSidebarOpen(true)
} else if (selectedPhotos.length === 0 && rightSidebarOpen) {
setRightSidebarOpen(false)
}
}, [viewMode, selectedPhotos.length, rightSidebarOpen])
// Right sidebar stays open by default and shows whatever's selected
// (or an empty state if nothing is). User can still toggle it manually.
const showRightSidebar = rightSidebarOpen && viewMode === 'grid'
return (
<div className="flex flex-col h-screen bg-bg text-text">
<TopBar onOpenSettings={() => setSettingsOpen(true)} />
<TopBar
onOpenSettings={() => setSettingsOpen(true)}
leftSidebarOpen={leftSidebarOpen}
rightSidebarOpen={showRightSidebar}
onExpandLeft={() => setLeftSidebarOpen(true)}
onExpandRight={() => setRightSidebarOpen(true)}
/>
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar */}
@@ -64,7 +58,7 @@ function App() {
leftSidebarOpen ? 'w-64' : 'w-0'
} overflow-hidden border-r border-border bg-surface`}
>
<LeftSidebar />
<LeftSidebar onCollapse={() => setLeftSidebarOpen(false)} />
</div>
{/* Main column — filter bar, discard bar, timeline. Lives to the
@@ -90,7 +84,7 @@ function App() {
showRightSidebar ? 'w-80' : 'w-0'
} overflow-hidden border-l border-border bg-surface`}
>
<RightSidebar />
<RightSidebar onCollapse={() => setRightSidebarOpen(false)} />
</div>
</div>