ui(layout): move Date filter inputs to topbar, Active Heap to right sidebar
- FilterBar: new Date pill hosts from/to inputs; calendar stays in left sidebar (always visible, no collapse) with reduced padding and a taller MONTH_HEIGHT so 6-week months render fully. - LeftSidebar: drop Library collapse; Heaps regains its chevron toggle to match Views/Folders. - RightSidebar: render ActiveHeapCard above the Metadata header (with its own eyebrow); preview overlay reuses RightSidebar so the active heap stays visible there too. - Toaster: top-right, more compact (smaller padding, font, gap). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
@@ -33,7 +33,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from '../ToastContainer'
|
||||
import { useFilterStore } from '../../store/filterStore'
|
||||
import { HeapsPanel } from '../heaps/HeapsPanel'
|
||||
import { ActiveHeapCard } from '../heaps/ActiveHeapCard'
|
||||
import { PHOTO_DRAG_MIME } from '../timeline/PhotoThumbnail'
|
||||
import { useFolderTreeQuery } from '../../hooks/useFolderTreeQuery'
|
||||
import { useTagsQuery } from '../../hooks/useTagsQuery'
|
||||
@@ -84,7 +83,6 @@ export function LeftSidebar() {
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['library', 'folders', 'heaps']))
|
||||
// Library pane collapse. Its body holds Views, Folders, Shared, and
|
||||
// Heaps, so a single toggle hides the whole navigation area.
|
||||
const [libraryPaneOpen, setLibraryPaneOpen] = useState(true)
|
||||
// Inline rename state for source-root rows. Stores the id being edited
|
||||
// and the draft name. Double-click a folder row to start.
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null)
|
||||
@@ -838,40 +836,18 @@ export function LeftSidebar() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-surface">
|
||||
{/* Active heap card — pinned at the very top so it stays visible
|
||||
* even when the date filter is expanded into a tall calendar.
|
||||
* Returns null when no heap is active, so the layout collapses
|
||||
* cleanly. */}
|
||||
<ActiveHeapCard />
|
||||
|
||||
{/* Date range filter — always visible, collapsible. Drives the
|
||||
* global dateFrom/dateTo on the filter store, so it applies to
|
||||
* every section regardless of which tree item is selected. */}
|
||||
{/* Date range calendar — always visible, collapsible. The
|
||||
* from/to range inputs live on the top filter bar; this block
|
||||
* just hosts the calendar visualisation and click-to-jump. */}
|
||||
<DateFilterSection />
|
||||
|
||||
{/* Resizable two-pane region: Library (Views + Folders + Shared)
|
||||
* on top, Heaps on the bottom. Each pane collapses to its
|
||||
* header; when both are expanded a drag divider splits the
|
||||
* vertical space between them (persisted to localStorage). */}
|
||||
{/* Library pane — the sole navigation section. Contains Views,
|
||||
* Folders, Shared-with-me, and Heaps in a single scroll area;
|
||||
* collapses to its header when hidden. */}
|
||||
<div
|
||||
className="flex min-h-0 flex-col"
|
||||
style={{ flex: libraryPaneOpen ? '1 1 0' : '0 0 auto' }}
|
||||
>
|
||||
<div
|
||||
className="group flex h-9 flex-shrink-0 cursor-pointer items-center gap-2 border-b border-border px-3 text-[11px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:text-text"
|
||||
onClick={() => setLibraryPaneOpen((v) => !v)}
|
||||
aria-expanded={libraryPaneOpen}
|
||||
role="button"
|
||||
>
|
||||
{/* Library — the sole navigation section. Contains Views,
|
||||
* Folders, Shared-with-me, and Heaps in a single scroll area. */}
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="group flex h-9 flex-shrink-0 items-center gap-2 border-b border-border px-3 text-[11px] font-semibold uppercase tracking-[0.14em] text-text-muted">
|
||||
<span className="flex-1 truncate">Library</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setUploadTarget({ open: true, folderId: null })
|
||||
}}
|
||||
onClick={() => setUploadTarget({ open: true, folderId: null })}
|
||||
className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
title="Upload photos"
|
||||
aria-label="Upload photos"
|
||||
@@ -879,8 +855,7 @@ export function LeftSidebar() {
|
||||
<UploadIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{libraryPaneOpen && (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pb-2">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pb-2">
|
||||
{libraryTree.map((item) => renderTreeItem(item))}
|
||||
|
||||
{/* Shared with me — folders shared by other users */}
|
||||
@@ -929,11 +904,9 @@ export function LeftSidebar() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Heaps — nested inside the Library section; HeapsPanel owns
|
||||
* its own eyebrow header + collapse state. */}
|
||||
{/* Heaps — nested inside the Library section. */}
|
||||
<HeapsPanel />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom panel — user identity + settings, pinned below the tree. */}
|
||||
@@ -999,69 +972,22 @@ export function LeftSidebar() {
|
||||
)
|
||||
}
|
||||
|
||||
/** Collapsible date-range filter block. Lives at the top of the left
|
||||
* sidebar and drives the global dateFrom/dateTo filter-store fields,
|
||||
* so it applies across every section. The calendar component itself
|
||||
* supports range selection and decorates days that have photos.
|
||||
* Open state persists across sessions via localStorage. */
|
||||
const DATE_FILTER_OPEN_KEY = 'mulita:dateFilterOpen'
|
||||
/** Always-visible calendar block at the top of the left sidebar.
|
||||
* Drives the same global dateFrom/dateTo filter-store fields as the
|
||||
* topbar Date pill, and visualises photo density per day. */
|
||||
function DateFilterSection() {
|
||||
const dateFrom = useFilterStore((s) => s.dateFrom)
|
||||
const dateTo = useFilterStore((s) => s.dateTo)
|
||||
const setDateFrom = useFilterStore((s) => s.setDateFrom)
|
||||
const setDateTo = useFilterStore((s) => s.setDateTo)
|
||||
const active = dateFrom !== null || dateTo !== null
|
||||
// Default open; remembered across sessions. localStorage is read
|
||||
// lazily inside the initialiser so SSR / disabled-storage fall back
|
||||
// cleanly to the default.
|
||||
const [open, setOpen] = useState<boolean>(() => {
|
||||
try {
|
||||
const v = localStorage.getItem(DATE_FILTER_OPEN_KEY)
|
||||
if (v === null) return true
|
||||
return v === '1'
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
})
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(DATE_FILTER_OPEN_KEY, open ? '1' : '0')
|
||||
} catch {
|
||||
// Ignore — quota / disabled storage is non-fatal.
|
||||
}
|
||||
}, [open])
|
||||
const summary = active
|
||||
? dateFrom && dateTo && dateFrom === dateTo
|
||||
? dateFrom
|
||||
: `${dateFrom ?? '…'} → ${dateTo ?? '…'}`
|
||||
: 'Any date'
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
<div
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="group flex h-9 cursor-pointer items-center gap-2 border-b border-border px-3 text-[11px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:text-text"
|
||||
aria-expanded={open}
|
||||
role="button"
|
||||
>
|
||||
<span className="flex-1 truncate">
|
||||
{active ? summary : 'Date'}
|
||||
</span>
|
||||
{active && (
|
||||
<span className="rounded bg-primary/20 px-1 py-px text-[9px] uppercase tracking-wider text-primary">
|
||||
Filtering
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{open && (
|
||||
<div className="border-b border-border px-3 py-3">
|
||||
<DateRangePicker
|
||||
from={dateFrom}
|
||||
to={dateTo}
|
||||
onFromChange={setDateFrom}
|
||||
onToChange={setDateTo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-shrink-0 border-b border-border px-2 py-2">
|
||||
<DateRangePicker
|
||||
from={dateFrom}
|
||||
to={dateTo}
|
||||
onFromChange={setDateFrom}
|
||||
onToChange={setDateTo}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { HEAPS_QUERY_KEY } from '../../hooks/useHeapsQuery'
|
||||
import { useTagsQuery, TAGS_QUERY_KEY } from '../../hooks/useTagsQuery'
|
||||
import { stripPhotosFromCache } from '../../hooks/usePhotosQuery'
|
||||
import { toast } from '../ToastContainer'
|
||||
import { ActiveHeapCard } from '../heaps/ActiveHeapCard'
|
||||
import { PhotoInfoPanel } from '../sidebar/PhotoInfoPanel'
|
||||
import { BulkTakenAtEditor } from '../sidebar/BulkTakenAtEditor'
|
||||
import { BulkTagsEditor } from '../sidebar/BulkTagsEditor'
|
||||
@@ -221,6 +222,7 @@ export function RightSidebar() {
|
||||
role="region"
|
||||
aria-label="Photo metadata"
|
||||
>
|
||||
<ActiveHeapCard />
|
||||
<Header />
|
||||
<div className="flex flex-1 items-center justify-center p-4 text-center">
|
||||
<div className="text-text-muted">
|
||||
@@ -255,6 +257,7 @@ export function RightSidebar() {
|
||||
role="region"
|
||||
aria-label="Photo metadata"
|
||||
>
|
||||
<ActiveHeapCard />
|
||||
<Header />
|
||||
<PhotoInfoPanel photoId={id} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user