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:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user