Files
mule-image/frontend/src/App.tsx
dtoro a6eb406052 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>
2026-04-14 22:31:10 +02:00

155 lines
5.6 KiB
TypeScript

import { useState } from 'react'
import { Timeline } from './components/timeline/Timeline'
import { DuplicatesView } from './components/duplicates/DuplicatesView'
import { MapView } from './components/map/MapView'
import { MemoriesView } from './components/memories/MemoriesView'
import { TagsView } from './components/tags/TagsView'
import { ColorsView } from './components/colors/ColorsView'
import { RatedView } from './components/rated/RatedView'
import { LeftSidebar } from './components/layout/LeftSidebar'
import { RightSidebar } from './components/layout/RightSidebar'
import { TopBar } from './components/layout/TopBar'
import { ScanProgress } from './components/ScanProgress'
import { ToastContainer } from './components/ToastContainer'
import { KeyboardHints } from './components/KeyboardHints'
import { PreviewView } from './components/preview/PreviewView'
import { FilterBar } from './components/filter/FilterBar'
import { DiscardActionBar } from './components/discard/DiscardActionBar'
import { SettingsPage } from './components/dialogs/SettingsDialog'
import { usePhotoStore } from './store/photoStore'
import { useFilterStore } from './store/filterStore'
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
import { useFilterUrlSync } from './hooks/useFilterUrlSync'
import { usePhotosQuery } from './hooks/usePhotosQuery'
import { AuthProvider, useAuth } from './contexts/AuthContext'
import { LoginPage } from './components/auth/LoginPage'
import { SetupPage } from './components/auth/SetupPage'
function MainApp() {
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true)
const [rightSidebarOpen, setRightSidebarOpen] = useState(true)
const viewMode = usePhotoStore((state) => state.viewMode)
const currentSection = useFilterStore((s) => s.currentSection)
// Bidirectional sync of filter store with URL query params.
useFilterUrlSync()
// Subscribe to the same photos query the Timeline uses, so the keyboard
// "open preview on first photo" path can read from the live cache regardless
// of what filter key it's stored under.
const { data: allPhotos } = usePhotosQuery()
// Set up global keyboard shortcuts
useKeyboardShortcuts({
onToggleLeftSidebar: () => setLeftSidebarOpen(!leftSidebarOpen),
onToggleRightSidebar: () => setRightSidebarOpen(!rightSidebarOpen),
getFirstPhotoId: () => allPhotos?.[0]?.id ?? null,
})
// Settings page is a full-page section — hide filter bar, right sidebar,
// and keyboard hints when it's active.
const isSettings = currentSection === 'settings'
// Right sidebar stays open by default and shows whatever's selected
// (or an empty state if nothing is). User can still toggle it manually.
const showRightSidebar = rightSidebarOpen && !isSettings
return (
<div className="flex flex-col h-screen bg-bg text-text">
<TopBar />
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar */}
<div
className={`transition-all duration-200 ${
leftSidebarOpen ? 'w-60' : 'w-0'
} overflow-hidden border-r border-border bg-surface`}
>
<LeftSidebar />
</div>
{/* Main column — filter bar, discard bar, timeline. Lives to the
* right of the left sidebar so the filter row doesn't bleed
* across the sidebar. relative so the KeyboardHints overlay
* centers against this column, not the viewport. */}
<div className="relative flex min-w-0 flex-1 flex-col">
{!isSettings && (
<FilterBar
leftSidebarOpen={leftSidebarOpen}
rightSidebarOpen={showRightSidebar}
onToggleLeftSidebar={() => setLeftSidebarOpen(!leftSidebarOpen)}
onToggleRightSidebar={() => setRightSidebarOpen(!rightSidebarOpen)}
/>
)}
{!isSettings && <DiscardActionBar />}
<div className="flex-1 overflow-auto">
{currentSection === 'settings' ? (
<SettingsPage />
) : currentSection === 'map' ? (
<MapView />
) : currentSection === 'memories' ? (
<MemoriesView />
) : currentSection === 'duplicates' ? (
<DuplicatesView />
) : currentSection === 'tags' ? (
<TagsView />
) : currentSection === 'colors' ? (
<ColorsView />
) : currentSection === 'rated' ? (
<RatedView />
) : (
<Timeline />
)}
</div>
{!isSettings && <KeyboardHints />}
</div>
{/* Right Sidebar */}
<div
className={`transition-all duration-200 ${
showRightSidebar ? 'w-72' : 'w-0'
} overflow-hidden border-l border-border bg-surface`}
>
<RightSidebar />
</div>
</div>
{/* Scan Progress Indicator */}
<ScanProgress />
{/* Toast Notifications */}
<ToastContainer />
{/* Preview overlay — covers TopBar when active */}
{viewMode === 'preview' && <PreviewView />}
</div>
)
}
/** Auth-gated shell: shows setup, login, or the main app. */
function App() {
return (
<AuthProvider>
<AuthGate />
</AuthProvider>
)
}
function AuthGate() {
const { user, isLoading, needsSetup } = useAuth()
if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-bg">
<div className="text-text-muted">Loading&hellip;</div>
</div>
)
}
if (needsSetup) return <SetupPage />
if (!user) return <LoginPage />
return <MainApp />
}
export default App