ui(sidebar): pin heap, single scroll area, collapsible metadata group

The right sidepanel had three stacked flex regions (heap card +
header + PhotoInfoPanel) with PhotoInfoPanel maintaining its own
internal scroll. That made the editable fields (filename, title,
notes, rating, color, flag) stick at the top — separate from the
readonly metadata that scrolled below. Effectively two scroll
boundaries on one sidebar.

Move the scroll boundary up so only ActiveHeapCard + Header stay
pinned; editable fields and readonly metadata now scroll together.
Wrap the four readonly sections (Tags / Basic Info / Camera /
Location) in a single outer 'Metadata' collapsible so the user can
hide the whole block with one click. Sub-sections inside stay
individually collapsible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claudio
2026-05-10 21:36:18 +02:00
parent 1408ec3fa3
commit 172f869e15
2 changed files with 31 additions and 7 deletions

View File

@@ -249,6 +249,9 @@ export function RightSidebar() {
} }
// ── Single-photo: full editor via PhotoInfoPanel ──────────────────── // ── Single-photo: full editor via PhotoInfoPanel ────────────────────
// Heap card + header stay pinned at the top; the edit fields and
// readonly metadata sections scroll together in a single overflow
// region below.
if (selectedPhotos.length === 1) { if (selectedPhotos.length === 1) {
const id = activePhotoId ?? selectedPhotos[0] const id = activePhotoId ?? selectedPhotos[0]
return ( return (
@@ -259,7 +262,9 @@ export function RightSidebar() {
> >
<ActiveHeapCard /> <ActiveHeapCard />
<Header /> <Header />
<PhotoInfoPanel photoId={id} /> <div className="flex-1 overflow-y-auto">
<PhotoInfoPanel photoId={id} />
</div>
</div> </div>
) )
} }

View File

@@ -143,7 +143,7 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [expandedSections, setExpandedSections] = useState<Set<string>>( const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set(['basic', 'camera', 'location', 'tags']) new Set(['metadata', 'basic', 'camera', 'location', 'tags'])
) )
const toggleSection = (section: string) => { const toggleSection = (section: string) => {
const next = new Set(expandedSections) const next = new Set(expandedSections)
@@ -380,10 +380,13 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro
: 'border-border bg-bg text-text placeholder-text-faint focus:border-primary' : 'border-border bg-bg text-text placeholder-text-faint focus:border-primary'
) )
// Note: no h-full / flex-1 here — the parent (RightSidebar) owns the
// scroll container so the edit fields and readonly metadata scroll
// together as one block beneath the pinned heap + header.
return ( return (
<div <div
className={cn( className={cn(
'flex h-full flex-col transition-opacity duration-150', 'flex flex-col transition-opacity duration-150',
isPlaceholderData && 'opacity-70' isPlaceholderData && 'opacity-70'
)} )}
> >
@@ -542,8 +545,24 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro
</div> </div>
</div> </div>
{/* Read-only metadata sections */} {/* Read-only metadata — collapsed/expanded as one block so the user
<div className="flex-1 overflow-y-auto"> * can hide everything below the editable form with a single click.
* Sub-sections inside stay individually collapsible for finer
* control once the outer group is open. */}
<Collapsible
open={expandedSections.has('metadata')}
onOpenChange={() => toggleSection('metadata')}
className="border-b border-border"
>
<CollapsibleTrigger className="flex w-full items-center justify-between border-b border-border bg-surface-2/40 px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:bg-surface-2 hover:text-text">
<span>Metadata</span>
{expandedSections.has('metadata') ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</CollapsibleTrigger>
<CollapsibleContent>
<Section <Section
title="Tags" title="Tags"
expanded={expandedSections.has('tags')} expanded={expandedSections.has('tags')}
@@ -667,8 +686,8 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro
<div className="text-xs text-text-muted">No GPS data</div> <div className="text-xs text-text-muted">No GPS data</div>
)} )}
</Section> </Section>
</CollapsibleContent>
</div> </Collapsible>
</div> </div>
) )
} }