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,23 +1,59 @@
import { Settings } from 'lucide-react'
import { Settings, PanelLeftOpen, PanelRightOpen } from 'lucide-react'
import muliLogo from '../../assets/muli-logo.png'
interface TopBarProps {
onOpenSettings: () => void
leftSidebarOpen: boolean
rightSidebarOpen: boolean
onExpandLeft: () => void
onExpandRight: () => void
}
/**
* Slim top bar — logo on the left, settings gear on the right. The
* active heap badge moved into the Heaps panel in the left sidebar
* (where it actually relates to the heap rows the user navigates to).
*
* Also hosts the "expand sidebar" affordances: when a side panel is
* collapsed, a small panel-open icon appears on the corresponding edge
* so the user has a way to bring it back without hunting for the
* keyboard shortcut. When the panel is open, the button hides — its
* collapse twin lives in the panel's own header.
*/
export function TopBar({ onOpenSettings }: TopBarProps) {
export function TopBar({
onOpenSettings,
leftSidebarOpen,
rightSidebarOpen,
onExpandLeft,
onExpandRight,
}: TopBarProps) {
return (
<header className="flex h-12 items-center justify-between border-b border-border bg-surface px-4">
<div className="flex items-center gap-2">
{!leftSidebarOpen && (
<button
onClick={onExpandLeft}
className="rounded p-1.5 text-text-muted transition-colors hover:bg-surface-2 hover:text-text"
title="Expand panel (Tab)"
aria-label="Expand left panel"
>
<PanelLeftOpen className="h-4 w-4" />
</button>
)}
<img src={muliLogo} alt="Mulimago" className="h-7 w-7 object-contain" />
<h1 className="text-lg font-semibold text-text">Mulimago</h1>
</div>
<div className="flex items-center gap-2">
{!rightSidebarOpen && (
<button
onClick={onExpandRight}
className="rounded p-1.5 text-text-muted transition-colors hover:bg-surface-2 hover:text-text"
title="Expand panel (I)"
aria-label="Expand right panel"
>
<PanelRightOpen className="h-4 w-4" />
</button>
)}
<button
onClick={onOpenSettings}
className="rounded p-1.5 text-text-muted transition-colors hover:bg-surface-2 hover:text-text"