feat(filters): move date picker to sidebar, track visible photo order

Pulls the date range picker out of the filter-bar pill into a
dedicated always-visible section at the top of the left sidebar, and
teaches the timeline to publish its visible photo sequence so "open
first photo" shortcuts respect the on-screen order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-15 17:12:34 +02:00
parent 45f1649979
commit eac005109c
8 changed files with 764 additions and 184 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import {
ChevronRight,
ChevronDown,
@@ -25,7 +25,9 @@ import {
Clock,
Upload as UploadIcon,
Download as DownloadIcon,
CalendarRange,
} from 'lucide-react'
import { DateRangePicker } from '../filter/DateRangePicker'
import { cn } from '@/lib/utils'
import { sourceFolders, photos as photosApi, downloads, type FolderTreeNode } from '../../services/api'
import { useMutation, useQueryClient } from '@tanstack/react-query'
@@ -851,6 +853,11 @@ export function LeftSidebar() {
</button>
</div>
</div>
{/* 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. */}
<DateFilterSection />
{/* Active heap card — pinned just below the Library header so
* toasts (bottom-left fixed) can't cover it. Returns null when
* no heap is active, so the layout collapses cleanly. */}
@@ -972,3 +979,71 @@ 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'
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 border-b border-border">
<button
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs text-text-muted hover:bg-surface-2"
aria-expanded={open}
>
<CalendarRange className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{summary}</span>
<span
className={cn(
'ml-auto shrink-0 text-[10px] uppercase tracking-wider',
active && 'text-primary',
)}
>
{open ? 'Hide' : active ? 'Filtering' : 'Filter'}
</span>
</button>
{open && (
<div className="px-3 pb-3">
<DateRangePicker
from={dateFrom}
to={dateTo}
onFromChange={setDateFrom}
onToChange={setDateTo}
/>
</div>
)}
</div>
)
}

View File

@@ -213,6 +213,7 @@ export function RightSidebar() {
</div>
)
if (selectedPhotos.length === 0) {
return (
<div