refactor: tighten sidebars with eyebrow section headers
Compact the left/right sidebars and the photo info panel: shrink panel widths, drop header height, switch top-level tree groups and metadata sections to small uppercase eyebrow labels, and tighten row padding, icon sizes, and count badges throughout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ function App() {
|
||||
{/* Left Sidebar */}
|
||||
<div
|
||||
className={`transition-all duration-200 ${
|
||||
leftSidebarOpen ? 'w-64' : 'w-0'
|
||||
leftSidebarOpen ? 'w-60' : 'w-0'
|
||||
} overflow-hidden border-r border-border bg-surface`}
|
||||
>
|
||||
<LeftSidebar
|
||||
@@ -88,7 +88,7 @@ function App() {
|
||||
{/* Right Sidebar */}
|
||||
<div
|
||||
className={`transition-all duration-200 ${
|
||||
showRightSidebar ? 'w-80' : 'w-0'
|
||||
showRightSidebar ? 'w-72' : 'w-0'
|
||||
} overflow-hidden border-l border-border bg-surface`}
|
||||
>
|
||||
<RightSidebar onCollapse={() => setRightSidebarOpen(false)} />
|
||||
|
||||
@@ -56,20 +56,20 @@ export function ActiveHeapCard() {
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="m-2 rounded-lg border border-border bg-surface-2 shadow-sm">
|
||||
<div className="m-1.5 rounded-md border border-border bg-surface-2 shadow-sm">
|
||||
{/* Header — clickable, navigates to the heap section. */}
|
||||
<button
|
||||
onClick={() =>
|
||||
navigateToSection(`heap-${activeHeap.id}`, { heapId: activeHeap.id })
|
||||
}
|
||||
className="flex w-full items-center gap-2 rounded-t-lg px-3 py-2 text-left hover:bg-surface-offset"
|
||||
className="flex w-full items-center gap-1.5 rounded-t-md px-2 py-1.5 text-left hover:bg-surface-offset"
|
||||
title={`Open "${activeHeap.name}"`}
|
||||
>
|
||||
<ShoppingBasket className="h-4 w-4 flex-shrink-0 text-pick" />
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-text">
|
||||
<ShoppingBasket className="h-3.5 w-3.5 flex-shrink-0 text-pick" />
|
||||
<span className="min-w-0 flex-1 truncate text-[12px] font-semibold text-text">
|
||||
{activeHeap.name}
|
||||
</span>
|
||||
<span className="flex h-5 min-w-[24px] items-center justify-center rounded bg-surface px-1.5 text-[11px] font-medium text-text-muted">
|
||||
<span className="flex h-4 min-w-[20px] items-center justify-center rounded bg-surface px-1 text-[10px] font-medium text-text-muted">
|
||||
{orderedIds.length}
|
||||
</span>
|
||||
</button>
|
||||
@@ -81,7 +81,7 @@ export function ActiveHeapCard() {
|
||||
* so the cacti frame the photos rather than the (transparent) sky. */}
|
||||
<div
|
||||
key={activeHeap.id}
|
||||
className="relative h-24 overflow-hidden rounded-b-lg px-3 pb-3"
|
||||
className="relative h-20 overflow-hidden rounded-b-md px-2 pb-2"
|
||||
style={{
|
||||
backgroundImage: `url(${cardBg})`,
|
||||
backgroundSize: 'cover',
|
||||
@@ -117,10 +117,10 @@ export function ActiveHeapCard() {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
marginLeft: -32, // half of w-16
|
||||
marginTop: -32, // half of h-16
|
||||
marginLeft: -26, // half of w-13
|
||||
marginTop: -26, // half of h-13
|
||||
}}
|
||||
className="h-16 w-16 rounded object-cover shadow-md ring-1 ring-black/40"
|
||||
className="h-[52px] w-[52px] rounded object-cover shadow-md ring-1 ring-black/40"
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -176,9 +176,10 @@ export function HeapsPanel() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Section header */}
|
||||
{/* Section header — eyebrow style to match the LeftSidebar
|
||||
* library/folders headers. */}
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-1 rounded px-2 py-1 text-sm text-text hover:bg-surface-2"
|
||||
className="group mt-2 flex cursor-pointer items-center gap-1 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:text-text"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
>
|
||||
<button className="rounded p-0.5 hover:bg-surface-offset">
|
||||
@@ -188,7 +189,6 @@ export function HeapsPanel() {
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
<ShoppingBasket className="h-4 w-4 text-text-muted" />
|
||||
<span className="flex-1 truncate">Heaps</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -196,7 +196,7 @@ export function HeapsPanel() {
|
||||
setCreating(true)
|
||||
setExpanded(true)
|
||||
}}
|
||||
className="invisible rounded p-0.5 text-text-muted hover:bg-surface-offset hover:text-text group-hover:visible"
|
||||
className="invisible rounded p-0.5 hover:bg-surface-offset hover:text-text group-hover:visible"
|
||||
title="New heap"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
@@ -208,8 +208,8 @@ export function HeapsPanel() {
|
||||
{/* Inline create form */}
|
||||
{creating && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-1"
|
||||
style={{ paddingLeft: '32px' }}
|
||||
className="flex items-center gap-1 px-2 py-0.5"
|
||||
style={{ paddingLeft: '20px' }}
|
||||
>
|
||||
<input
|
||||
autoFocus
|
||||
@@ -224,12 +224,12 @@ export function HeapsPanel() {
|
||||
}
|
||||
}}
|
||||
placeholder="Heap name"
|
||||
className="flex-1 rounded border border-border bg-bg px-2 py-0.5 text-xs text-text focus:border-primary focus:outline-none"
|
||||
className="flex-1 rounded border border-border bg-bg px-2 py-0.5 text-[11px] text-text focus:border-primary focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
disabled={!newName.trim() || createMutation.isPending}
|
||||
className="rounded bg-primary px-2 py-0.5 text-xs text-white hover:bg-primary/80 disabled:opacity-50"
|
||||
className="rounded bg-primary px-2 py-0.5 text-[11px] text-white hover:bg-primary/80 disabled:opacity-50"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
@@ -238,8 +238,8 @@ export function HeapsPanel() {
|
||||
|
||||
{heaps.length === 0 && !creating && (
|
||||
<div
|
||||
className="px-2 py-1 text-xs text-text-faint"
|
||||
style={{ paddingLeft: '32px' }}
|
||||
className="px-2 py-0.5 text-[11px] text-text-faint"
|
||||
style={{ paddingLeft: '20px' }}
|
||||
>
|
||||
No heaps yet
|
||||
</div>
|
||||
@@ -264,7 +264,7 @@ export function HeapsPanel() {
|
||||
<div
|
||||
key={heap.id}
|
||||
className={clsx(
|
||||
'group relative flex cursor-pointer items-center gap-1 rounded px-2 py-1 text-[13px]',
|
||||
'group relative flex cursor-pointer items-center gap-1 rounded px-2 py-0.5 text-[12px]',
|
||||
isFiltered ? 'bg-primary/20 text-primary' : 'text-text hover:bg-surface-2',
|
||||
// Active-heap row gets a soft primary wash so the
|
||||
// user always knows where Pick / T will land, even
|
||||
@@ -272,7 +272,7 @@ export function HeapsPanel() {
|
||||
isActive && !isFiltered && 'bg-primary/8 text-text',
|
||||
isDropTarget && 'ring-2 ring-primary bg-primary/10'
|
||||
)}
|
||||
style={{ paddingLeft: '32px' }}
|
||||
style={{ paddingLeft: '20px' }}
|
||||
onClick={() => {
|
||||
if (isRenaming) return
|
||||
navigateToSection(`heap-${heap.id}`, { heapId: heap.id })
|
||||
@@ -311,7 +311,7 @@ export function HeapsPanel() {
|
||||
>
|
||||
<ShoppingBasket
|
||||
className={clsx(
|
||||
'h-4 w-4 flex-shrink-0',
|
||||
'h-3.5 w-3.5 flex-shrink-0',
|
||||
isActive || isFiltered ? 'text-primary' : 'text-text-muted'
|
||||
)}
|
||||
/>
|
||||
@@ -361,11 +361,11 @@ export function HeapsPanel() {
|
||||
* on the name above; the standalone Target indicator
|
||||
* was making heap counts sit left of the others. */}
|
||||
{heap.photo_count > 0 ? (
|
||||
<span className="flex h-5 min-w-[24px] flex-shrink-0 items-center justify-center rounded bg-surface-offset px-1.5 text-xs tabular-nums text-text-muted">
|
||||
<span className="flex h-4 min-w-[20px] flex-shrink-0 items-center justify-center rounded bg-surface-offset px-1 text-[10px] tabular-nums text-text-muted">
|
||||
{heap.photo_count}
|
||||
</span>
|
||||
) : (
|
||||
<span className="h-5 min-w-[24px] flex-shrink-0" aria-hidden="true" />
|
||||
<span className="h-4 min-w-[20px] flex-shrink-0" aria-hidden="true" />
|
||||
)}
|
||||
{!isActive && (
|
||||
<button
|
||||
|
||||
@@ -395,19 +395,31 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
|
||||
const isSelected = isItemActive(item.id)
|
||||
const acceptsDrop = isDropTarget(item.id)
|
||||
const isDropHover = dropTargetId === item.id
|
||||
// Top-level entries (Views, Folders) render as small uppercase
|
||||
// section eyebrows rather than another tree row, so the panel reads
|
||||
// as distinct sections with the actual items beneath them.
|
||||
const isSectionHeader = depth === 0
|
||||
|
||||
return (
|
||||
<div key={item.id}>
|
||||
<div
|
||||
className={clsx(
|
||||
'group flex cursor-pointer items-center gap-1 rounded px-2 py-1 text-sm',
|
||||
isSelected ? 'bg-primary/20 text-primary' : 'text-text hover:bg-surface-2',
|
||||
'group flex cursor-pointer items-center gap-1',
|
||||
isSectionHeader
|
||||
? 'mt-2 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:text-text'
|
||||
: clsx(
|
||||
'rounded px-2 py-0.5 text-[12px]',
|
||||
isSelected ? 'bg-primary/20 text-primary' : 'text-text hover:bg-surface-2'
|
||||
),
|
||||
isDropHover && (item.id === 'discarded'
|
||||
? 'ring-2 ring-reject bg-reject/10'
|
||||
: 'ring-2 ring-primary bg-primary/10'),
|
||||
depth > 0 && 'text-[13px]'
|
||||
: 'ring-2 ring-primary bg-primary/10')
|
||||
)}
|
||||
style={{ paddingLeft: `${8 + depth * 16}px` }}
|
||||
style={
|
||||
isSectionHeader
|
||||
? undefined
|
||||
: { paddingLeft: `${8 + (depth - 1) * 12}px` }
|
||||
}
|
||||
onClick={() => {
|
||||
if (renamingId === item.id) return
|
||||
// Folder rows are always filterable, parent or leaf — clicking
|
||||
@@ -470,13 +482,19 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<div className="w-4" />
|
||||
!isSectionHeader && <div className="w-3" />
|
||||
)}
|
||||
|
||||
{/* Item Icon */}
|
||||
{item.icon && (
|
||||
<span className={clsx('flex-shrink-0', isSelected ? 'text-primary' : 'text-text-muted')}>
|
||||
{item.icon}
|
||||
{/* Item Icon — section headers drop their icon in favor of the
|
||||
* uppercase eyebrow label. */}
|
||||
{item.icon && !isSectionHeader && (
|
||||
<span
|
||||
className={clsx(
|
||||
'flex-shrink-0',
|
||||
isSelected ? 'text-primary' : 'text-text-muted'
|
||||
)}
|
||||
>
|
||||
<span className="[&>svg]:h-3.5 [&>svg]:w-3.5">{item.icon}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -510,13 +528,16 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
|
||||
)}
|
||||
|
||||
{/* Count Badge — fixed-width slot so counts line up in a column
|
||||
* across rows regardless of digit count. */}
|
||||
{item.count !== undefined && item.count > 0 ? (
|
||||
<span className="flex h-5 min-w-[24px] flex-shrink-0 items-center justify-center rounded bg-surface-offset px-1.5 text-xs tabular-nums text-text-muted">
|
||||
{item.count}
|
||||
</span>
|
||||
) : (
|
||||
<span className="h-5 min-w-[24px] flex-shrink-0" aria-hidden="true" />
|
||||
* across rows regardless of digit count. Section headers skip
|
||||
* the badge entirely (they're labels, not navigable rows). */}
|
||||
{!isSectionHeader && (
|
||||
item.count !== undefined && item.count > 0 ? (
|
||||
<span className="flex h-4 min-w-[20px] flex-shrink-0 items-center justify-center rounded bg-surface-offset px-1 text-[10px] tabular-nums text-text-muted">
|
||||
{item.count}
|
||||
</span>
|
||||
) : (
|
||||
<span className="h-4 min-w-[20px] flex-shrink-0" aria-hidden="true" />
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Folder kebab menu — only on folder rows. Hidden (display:none)
|
||||
@@ -658,15 +679,17 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
|
||||
<div className="flex h-full flex-col bg-surface">
|
||||
{/* Header with collapse button. Matches the right sidebar header
|
||||
* so both panels have symmetric affordances. */}
|
||||
<div className="flex h-11 flex-shrink-0 items-center justify-between border-b border-border px-4">
|
||||
<h2 className="text-sm font-semibold text-text">Library</h2>
|
||||
<div className="flex h-9 flex-shrink-0 items-center justify-between border-b border-border px-3">
|
||||
<h2 className="text-[11px] font-semibold uppercase tracking-[0.14em] text-text-muted">
|
||||
Library
|
||||
</h2>
|
||||
<button
|
||||
onClick={onCollapse}
|
||||
className="rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
title="Collapse panel (Tab)"
|
||||
aria-label="Collapse panel"
|
||||
>
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
<PanelLeftClose className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Active heap card — pinned just below the Library header so
|
||||
@@ -675,20 +698,20 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
|
||||
<ActiveHeapCard />
|
||||
|
||||
{/* Tree View */}
|
||||
<div className="flex-1 overflow-y-auto py-2">
|
||||
<div className="flex-1 overflow-y-auto pb-2">
|
||||
{libraryTree.map((item) => renderTreeItem(item))}
|
||||
<HeapsPanel />
|
||||
</div>
|
||||
|
||||
{/* Settings entry point — pinned to the bottom of the panel so it
|
||||
* sits out of the way of the library tree but is always reachable. */}
|
||||
<div className="border-t border-border p-2">
|
||||
<div className="border-t border-border p-1.5">
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 text-[12px] text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
title="Settings"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -149,26 +149,28 @@ export function RightSidebar({ onCollapse }: RightSidebarProps) {
|
||||
: `${selectedPhotos.length} Photos Selected`
|
||||
|
||||
const Header = () => (
|
||||
<div className="flex h-11 flex-shrink-0 items-center justify-between border-b border-border px-4">
|
||||
<h2 className="text-sm font-semibold text-text">{headerTitle}</h2>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex h-9 flex-shrink-0 items-center justify-between border-b border-border px-3">
|
||||
<h2 className="text-[11px] font-semibold uppercase tracking-[0.14em] text-text-muted">
|
||||
{headerTitle}
|
||||
</h2>
|
||||
<div className="flex items-center gap-0.5">
|
||||
{selectedPhotos.length > 0 && (
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
title="Clear selection (Esc)"
|
||||
aria-label="Clear selection"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onCollapse}
|
||||
className="rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
title="Collapse panel (I)"
|
||||
aria-label="Collapse panel"
|
||||
>
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
<PanelRightClose className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,8 +208,8 @@ export function RightSidebar({ onCollapse }: RightSidebarProps) {
|
||||
<div className="flex h-full flex-col bg-surface">
|
||||
<Header />
|
||||
|
||||
<div className="space-y-3 border-b border-border p-4">
|
||||
<p className="text-xs text-text-muted">
|
||||
<div className="space-y-2.5 border-b border-border p-3">
|
||||
<p className="text-[11px] text-text-muted">
|
||||
Rating, color, and flag apply to all {selectedPhotos.length} selected.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Edit fields */}
|
||||
<div className="space-y-3 border-b border-border p-4">
|
||||
<div className="space-y-2.5 border-b border-border p-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-muted">Filename</label>
|
||||
<input
|
||||
@@ -594,16 +594,16 @@ function Section({
|
||||
<div className="border-b border-border">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="flex w-full items-center justify-between px-4 py-2 text-sm hover:bg-surface-2"
|
||||
className="flex w-full items-center justify-between px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
>
|
||||
<span className="font-medium text-text">{title}</span>
|
||||
<span>{title}</span>
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-text-muted" />
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-text-muted" />
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
{expanded && <div className="px-4 pb-3">{children}</div>}
|
||||
{expanded && <div className="px-3 pb-2.5">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user