ui: tidy sidebar tree indent and consolidate sidebar toggles into filter bar
Folder tree now indents 20px per level (chevron width + gap) so a child's chevron column lines up under its parent's label, and depth-1 rows nest under the section eyebrow instead of starting flush with it. Spacer for leaf rows matches the chevron button footprint so rows align regardless of expandability. Sidebar open/close buttons (previously split between TopBar and each panel header) collapse into two toggles at the ends of the FilterBar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,12 +56,7 @@ function MainApp() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen bg-bg text-text">
|
<div className="flex flex-col h-screen bg-bg text-text">
|
||||||
<TopBar
|
<TopBar />
|
||||||
leftSidebarOpen={leftSidebarOpen}
|
|
||||||
rightSidebarOpen={showRightSidebar}
|
|
||||||
onExpandLeft={() => setLeftSidebarOpen(true)}
|
|
||||||
onExpandRight={() => setRightSidebarOpen(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Left Sidebar */}
|
{/* Left Sidebar */}
|
||||||
@@ -70,9 +65,7 @@ function MainApp() {
|
|||||||
leftSidebarOpen ? 'w-60' : 'w-0'
|
leftSidebarOpen ? 'w-60' : 'w-0'
|
||||||
} overflow-hidden border-r border-border bg-surface`}
|
} overflow-hidden border-r border-border bg-surface`}
|
||||||
>
|
>
|
||||||
<LeftSidebar
|
<LeftSidebar />
|
||||||
onCollapse={() => setLeftSidebarOpen(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main column — filter bar, discard bar, timeline. Lives to the
|
{/* Main column — filter bar, discard bar, timeline. Lives to the
|
||||||
@@ -80,7 +73,14 @@ function MainApp() {
|
|||||||
* across the sidebar. relative so the KeyboardHints overlay
|
* across the sidebar. relative so the KeyboardHints overlay
|
||||||
* centers against this column, not the viewport. */}
|
* centers against this column, not the viewport. */}
|
||||||
<div className="relative flex min-w-0 flex-1 flex-col">
|
<div className="relative flex min-w-0 flex-1 flex-col">
|
||||||
{!isSettings && <FilterBar />}
|
{!isSettings && (
|
||||||
|
<FilterBar
|
||||||
|
leftSidebarOpen={leftSidebarOpen}
|
||||||
|
rightSidebarOpen={showRightSidebar}
|
||||||
|
onToggleLeftSidebar={() => setLeftSidebarOpen(!leftSidebarOpen)}
|
||||||
|
onToggleRightSidebar={() => setRightSidebarOpen(!rightSidebarOpen)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!isSettings && <DiscardActionBar />}
|
{!isSettings && <DiscardActionBar />}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{currentSection === 'settings' ? (
|
{currentSection === 'settings' ? (
|
||||||
@@ -110,7 +110,7 @@ function MainApp() {
|
|||||||
showRightSidebar ? 'w-72' : 'w-0'
|
showRightSidebar ? 'w-72' : 'w-0'
|
||||||
} overflow-hidden border-l border-border bg-surface`}
|
} overflow-hidden border-l border-border bg-surface`}
|
||||||
>
|
>
|
||||||
<RightSidebar onCollapse={() => setRightSidebarOpen(false)} />
|
<RightSidebar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Star, X, ArrowDown, ArrowUp, Search, AlertTriangle, Check } from 'lucide-react'
|
import {
|
||||||
|
Star,
|
||||||
|
X,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
Search,
|
||||||
|
AlertTriangle,
|
||||||
|
Check,
|
||||||
|
PanelLeftOpen,
|
||||||
|
PanelLeftClose,
|
||||||
|
PanelRightOpen,
|
||||||
|
PanelRightClose,
|
||||||
|
} from 'lucide-react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
useFilterStore,
|
useFilterStore,
|
||||||
@@ -35,7 +47,19 @@ const SORT_OPTIONS: { value: SortField; label: string }[] = [
|
|||||||
* underlying control, and shows a short value summary inline when active.
|
* underlying control, and shows a short value summary inline when active.
|
||||||
* Replaces the old expandable FilterBar + ActiveFilterChips combo.
|
* Replaces the old expandable FilterBar + ActiveFilterChips combo.
|
||||||
*/
|
*/
|
||||||
export function FilterBar() {
|
interface FilterBarProps {
|
||||||
|
leftSidebarOpen: boolean
|
||||||
|
rightSidebarOpen: boolean
|
||||||
|
onToggleLeftSidebar: () => void
|
||||||
|
onToggleRightSidebar: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilterBar({
|
||||||
|
leftSidebarOpen,
|
||||||
|
rightSidebarOpen,
|
||||||
|
onToggleLeftSidebar,
|
||||||
|
onToggleRightSidebar,
|
||||||
|
}: FilterBarProps) {
|
||||||
const filterState = useFilterStore()
|
const filterState = useFilterStore()
|
||||||
const dateFrom = useFilterStore((s) => s.dateFrom)
|
const dateFrom = useFilterStore((s) => s.dateFrom)
|
||||||
const dateTo = useFilterStore((s) => s.dateTo)
|
const dateTo = useFilterStore((s) => s.dateTo)
|
||||||
@@ -132,6 +156,22 @@ export function FilterBar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-9 items-center gap-3 border-b border-border bg-surface px-3 py-0">
|
<div className="flex h-9 items-center gap-3 border-b border-border bg-surface px-3 py-0">
|
||||||
|
{/* Left sidebar toggle — pinned to the far-left edge of the bar so it
|
||||||
|
* sits flush against the panel it controls (or the viewport edge
|
||||||
|
* when collapsed). */}
|
||||||
|
<button
|
||||||
|
onClick={onToggleLeftSidebar}
|
||||||
|
className="flex-shrink-0 rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||||
|
title={leftSidebarOpen ? 'Collapse panel (Tab)' : 'Expand panel (Tab)'}
|
||||||
|
aria-label={leftSidebarOpen ? 'Collapse left panel' : 'Expand left panel'}
|
||||||
|
>
|
||||||
|
{leftSidebarOpen ? (
|
||||||
|
<PanelLeftClose className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<PanelLeftOpen className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Pills — left side, scroll horizontally if they overflow. */}
|
{/* Pills — left side, scroll horizontally if they overflow. */}
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto">
|
<div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto">
|
||||||
{/* Date */}
|
{/* Date */}
|
||||||
@@ -423,6 +463,20 @@ export function FilterBar() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right sidebar toggle — pinned to the far-right edge. */}
|
||||||
|
<button
|
||||||
|
onClick={onToggleRightSidebar}
|
||||||
|
className="flex-shrink-0 rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||||
|
title={rightSidebarOpen ? 'Collapse panel (I)' : 'Expand panel (I)'}
|
||||||
|
aria-label={rightSidebarOpen ? 'Collapse right panel' : 'Expand right panel'}
|
||||||
|
>
|
||||||
|
{rightSidebarOpen ? (
|
||||||
|
<PanelRightClose className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<PanelRightOpen className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,16 +125,14 @@ export function FilterPill({
|
|||||||
onClear()
|
onClear()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
// Same h-4 w-4 as the chevron slot below so swapping the
|
className="ml-0.5 inline-flex h-3.5 w-4 cursor-pointer items-center justify-center rounded-full hover:bg-primary/30"
|
||||||
// two doesn't change the pill's footprint.
|
|
||||||
className="ml-0.5 inline-flex h-4 w-4 cursor-pointer items-center justify-center rounded-full hover:bg-primary/30"
|
|
||||||
title={`Clear ${label}`}
|
title={`Clear ${label}`}
|
||||||
aria-label={`Clear ${label}`}
|
aria-label={`Clear ${label}`}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex h-4 w-4 items-center justify-center">
|
<span className="inline-flex h-3.5 w-4 items-center justify-center">
|
||||||
<ChevronDown className="h-3 w-3 opacity-60" />
|
<ChevronDown className="h-3 w-3 opacity-60" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
Layers2,
|
Layers2,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Pencil,
|
Pencil,
|
||||||
PanelLeftClose,
|
|
||||||
Settings,
|
Settings,
|
||||||
Users,
|
Users,
|
||||||
Eye,
|
Eye,
|
||||||
@@ -64,11 +63,7 @@ interface TreeItem {
|
|||||||
isHidden?: boolean
|
isHidden?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LeftSidebarProps {
|
export function LeftSidebar() {
|
||||||
onCollapse: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LeftSidebar({ onCollapse }: LeftSidebarProps) {
|
|
||||||
const { user, isAdmin, logout } = useAuth()
|
const { user, isAdmin, logout } = useAuth()
|
||||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['library', 'folders', 'heaps']))
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['library', 'folders', 'heaps']))
|
||||||
// Inline rename state for source-root rows. Stores the id being edited
|
// Inline rename state for source-root rows. Stores the id being edited
|
||||||
@@ -839,14 +834,6 @@ export function LeftSidebar({ onCollapse }: LeftSidebarProps) {
|
|||||||
>
|
>
|
||||||
<UploadIcon className="h-3.5 w-3.5" />
|
<UploadIcon className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={onCollapse}
|
|
||||||
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-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Active heap card — pinned just below the Library header so
|
{/* Active heap card — pinned just below the Library header so
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { X, Star, ShoppingBasket, Trash2, Plus, PanelRightClose } from 'lucide-react'
|
import { X, Star, ShoppingBasket, Trash2, Plus } from 'lucide-react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
@@ -29,11 +29,7 @@ import { COLOR_LABEL_OPTIONS } from '../../constants/colorLabels'
|
|||||||
* - 2+ photos selected → renders a slim bulk-action panel that fans out
|
* - 2+ photos selected → renders a slim bulk-action panel that fans out
|
||||||
* rating / color / discard / pick across the entire selection.
|
* rating / color / discard / pick across the entire selection.
|
||||||
*/
|
*/
|
||||||
interface RightSidebarProps {
|
export function RightSidebar() {
|
||||||
onCollapse: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RightSidebar({ onCollapse }: RightSidebarProps) {
|
|
||||||
const { selectedPhotos, activePhotoId, clearSelection } = usePhotoStore()
|
const { selectedPhotos, activePhotoId, clearSelection } = usePhotoStore()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
@@ -221,14 +217,6 @@ export function RightSidebar({ onCollapse }: RightSidebarProps) {
|
|||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
onClick={onCollapse}
|
|
||||||
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-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import { PanelLeftOpen, PanelRightOpen } from 'lucide-react'
|
|
||||||
import desertBg from '../../assets/desert.png'
|
import desertBg from '../../assets/desert.png'
|
||||||
import muleSprites from '../../assets/mule-sprites.png'
|
import muleSprites from '../../assets/mule-sprites.png'
|
||||||
|
|
||||||
interface TopBarProps {
|
|
||||||
leftSidebarOpen: boolean
|
|
||||||
rightSidebarOpen: boolean
|
|
||||||
onExpandLeft: () => void
|
|
||||||
onExpandRight: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block-character ASCII rendering of "Mulimago" — sits on a black plate
|
// Block-character ASCII rendering of "Mulimago" — sits on a black plate
|
||||||
// in place of the old text title.
|
// in place of the old text title.
|
||||||
const MULIMAGO_ASCII = `▖ ▖ ▜ ▘
|
const MULIMAGO_ASCII = `▖ ▖ ▜ ▘
|
||||||
@@ -34,22 +26,9 @@ function toRoman(n: number): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Slim top bar — animated walking mule on the left over a tiled desert
|
* Slim top bar — animated walking mule on the left over a tiled desert
|
||||||
* backdrop, settings gear on the right. The active heap badge moved into
|
* backdrop. Sidebar toggle buttons live on the FilterBar.
|
||||||
* 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({
|
export function TopBar() {
|
||||||
leftSidebarOpen,
|
|
||||||
rightSidebarOpen,
|
|
||||||
onExpandLeft,
|
|
||||||
onExpandRight,
|
|
||||||
}: TopBarProps) {
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className="relative flex h-16 items-center justify-between overflow-hidden border-b border-border px-4"
|
className="relative flex h-16 items-center justify-between overflow-hidden border-b border-border px-4"
|
||||||
@@ -67,16 +46,6 @@ export function TopBar({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative flex items-center gap-3">
|
<div className="relative flex items-center gap-3">
|
||||||
{!leftSidebarOpen && (
|
|
||||||
<button
|
|
||||||
onClick={onExpandLeft}
|
|
||||||
className="rounded bg-black/30 p-1.5 text-text-muted backdrop-blur-sm transition-colors hover:bg-black/50 hover:text-text"
|
|
||||||
title="Expand panel (Tab)"
|
|
||||||
aria-label="Expand left panel"
|
|
||||||
>
|
|
||||||
<PanelLeftOpen className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
aria-label="Mulimago"
|
aria-label="Mulimago"
|
||||||
className="h-12 w-14"
|
className="h-12 w-14"
|
||||||
@@ -100,16 +69,6 @@ export function TopBar({
|
|||||||
<span className="text-[10px] font-serif text-black/80">
|
<span className="text-[10px] font-serif text-black/80">
|
||||||
Built with hubris • {toRoman(new Date().getFullYear())}
|
Built with hubris • {toRoman(new Date().getFullYear())}
|
||||||
</span>
|
</span>
|
||||||
{!rightSidebarOpen && (
|
|
||||||
<button
|
|
||||||
onClick={onExpandRight}
|
|
||||||
className="rounded bg-black/30 p-1.5 text-text-muted backdrop-blur-sm transition-colors hover:bg-black/50 hover:text-text"
|
|
||||||
title="Expand panel (I)"
|
|
||||||
aria-label="Expand right panel"
|
|
||||||
>
|
|
||||||
<PanelRightOpen className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user