From e6ca78881f2e74ffe5decb2a65d15a772306c5f4 Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 15 Apr 2026 21:21:57 +0200 Subject: [PATCH] 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) --- .../src/components/filter/DateRangePicker.tsx | 115 +++-------------- frontend/src/components/filter/FilterBar.tsx | 48 ++++++- .../src/components/heaps/ActiveHeapCard.tsx | 5 + frontend/src/components/heaps/HeapsPanel.tsx | 28 ++--- .../src/components/layout/LeftSidebar.tsx | 118 ++++-------------- .../src/components/layout/RightSidebar.tsx | 3 + .../src/components/preview/PreviewView.tsx | 4 +- frontend/src/components/ui/sonner.tsx | 16 +-- 8 files changed, 107 insertions(+), 230 deletions(-) diff --git a/frontend/src/components/filter/DateRangePicker.tsx b/frontend/src/components/filter/DateRangePicker.tsx index 90c32b3..97df32f 100644 --- a/frontend/src/components/filter/DateRangePicker.tsx +++ b/frontend/src/components/filter/DateRangePicker.tsx @@ -1,21 +1,16 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Filter, X } from 'lucide-react' +import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import { DayPicker } from 'react-day-picker' import { useVirtualizer } from '@tanstack/react-virtual' import { cn } from '@/lib/utils' import { usePhotosQuery } from '../../hooks/usePhotosQuery' import { usePhotoStore } from '../../store/photoStore' -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover' import { buttonVariants } from '@/components/ui/button' // Fixed month height so snap points are uniform regardless of whether // a given month lays out as 5 or 6 weeks. Sized to fit the tallest -// (6-week) case: caption ~18px + weekday head ~14px + 6 rows × 24px + -// padding ~12px. +// (6-week) case: caption ~20px + weekday head ~14px + 6 rows × 24px +// + 5 × 2px row gap = 154px. Plus a few px slack so the last row +// never gets clipped on months that span 6 weeks (e.g. Nov 2025). const MONTH_HEIGHT = 200 /** @@ -214,9 +209,9 @@ export function DateRangePicker({ const next = photosByDate[lo] const best = !prev ? next - : !next ? prev - : target - prev.t <= next.t - target ? prev - : next + : !next ? prev + : target - prev.t <= next.t - target ? prev + : next if (best) jumpToPhoto(best.id) }, [photosByDate, jumpToPhoto] @@ -224,14 +219,8 @@ export function DateRangePicker({ return (
-
- - {hasSelection && ( + {hasSelection && ( +
- )} -
+
+ )} {/* Single-month viewport — container height matches exactly one * month so the previous / next months stay off-screen. Uses * scroll-snap so a wheel tick or drag settles on a whole month @@ -256,7 +245,7 @@ export function DateRangePicker({ * sets scrollTop even when overflow is hidden. */}
void - onToChange: (v: string | null) => void -}) { - const [open, setOpen] = useState(false) - const active = !!(from || to) - return ( - - - - Filter - - -
-

- Date range -

- {active && ( - - )} -
- - -
-
- ) -} - function isInRange(d: Date, from?: Date, to?: Date): boolean { if (!from && !to) return false const t = d.getTime() diff --git a/frontend/src/components/filter/FilterBar.tsx b/frontend/src/components/filter/FilterBar.tsx index ff54b1e..e37c1c4 100644 --- a/frontend/src/components/filter/FilterBar.tsx +++ b/frontend/src/components/filter/FilterBar.tsx @@ -77,6 +77,10 @@ export function FilterBar({ const tagIds = useFilterStore((s) => s.tagIds) const needsReview = useFilterStore((s) => s.needsReview) const setNeedsReview = useFilterStore((s) => s.setNeedsReview) + 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 currentSection = useFilterStore((s) => s.currentSection) // Only the Flag pill is hidden inside the Discarded section. Flag has @@ -173,8 +177,48 @@ export function FilterBar({ {/* Pills — left side, scroll horizontally if they overflow. */}
- {/* Date filter is rendered inline at the top of the right - * sidebar (always visible) — no pill needed here. */} + {/* Date */} + { + setDateFrom(null) + setDateTo(null) + }} + contentClassName="w-56 space-y-2 p-3" + > + + + {/* Type */} +
+ Active Heap +
{/* Header — clickable, navigates to the heap section. */}
+ ) } diff --git a/frontend/src/components/heaps/HeapsPanel.tsx b/frontend/src/components/heaps/HeapsPanel.tsx index 32e70d2..de16b00 100644 --- a/frontend/src/components/heaps/HeapsPanel.tsx +++ b/frontend/src/components/heaps/HeapsPanel.tsx @@ -44,27 +44,13 @@ import { * - filter heapId: which heap is currently filtered to (visual) * - heap.is_active: which heap T adds to (server-side, single per row) */ -interface HeapsPanelProps { - /** Controlled expand state. When both props are supplied the panel - * defers to the parent for collapse/expand, so the sidebar's pane - * sizing layer can flex the Heaps pane only when it is open. */ - expanded?: boolean - onExpandedChange?: (expanded: boolean) => void -} - -export function HeapsPanel({ expanded: expandedProp, onExpandedChange }: HeapsPanelProps = {}) { +export function HeapsPanel() { const { data: heaps = [] } = useHeapsQuery() const navigateToSection = useFilterStore((s) => s.navigateToSection) const currentSection = useFilterStore((s) => s.currentSection) const queryClient = useQueryClient() - const [expandedInternal, setExpandedInternal] = useState(true) - const expanded = expandedProp ?? expandedInternal - const setExpanded = (next: boolean | ((prev: boolean) => boolean)) => { - const resolved = typeof next === 'function' ? (next as (p: boolean) => boolean)(expanded) : next - if (onExpandedChange) onExpandedChange(resolved) - else setExpandedInternal(resolved) - } + const [expanded, setExpanded] = useState(true) const [creating, setCreating] = useState(false) const [newName, setNewName] = useState('') // Which heap row is currently being hovered with a drag — used to render @@ -188,9 +174,9 @@ export function HeapsPanel({ expanded: expandedProp, onExpandedChange }: HeapsPa return (
- {/* Section header — mirrors the Folders section eyebrow in - * LeftSidebar so Heaps sits alongside it at the same visual - * tier, with heap rows indented the same as folder rows. */} + {/* Section header — mirrors the Views/Folders section eyebrows + * in LeftSidebar so Heaps sits alongside them at the same + * visual tier, including the click-to-collapse chevron. */}
setExpanded((v) => !v)} @@ -226,7 +212,7 @@ export function HeapsPanel({ expanded: expandedProp, onExpandedChange }: HeapsPa
{expanded && ( -
+
{/* Inline create form */} {creating && (
) })} -
+
)} {/* Shared with me */} diff --git a/frontend/src/components/layout/LeftSidebar.tsx b/frontend/src/components/layout/LeftSidebar.tsx index 17cf3d4..c5ecae7 100644 --- a/frontend/src/components/layout/LeftSidebar.tsx +++ b/frontend/src/components/layout/LeftSidebar.tsx @@ -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>(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(null) @@ -838,40 +836,18 @@ export function LeftSidebar() { return (
- {/* 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. */} - - - {/* 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. */} - {/* 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. */} -
-
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. */} +
+
Library
- {libraryPaneOpen && ( -
+
{libraryTree.map((item) => renderTreeItem(item))} {/* Shared with me — folders shared by other users */} @@ -929,11 +904,9 @@ export function LeftSidebar() {
)} - {/* Heaps — nested inside the Library section; HeapsPanel owns - * its own eyebrow header + collapse state. */} + {/* Heaps — nested inside the Library section. */} -
- )} +
{/* 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(() => { - 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 ( -
-
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" - > - - {active ? summary : 'Date'} - - {active && ( - - Filtering - - )} -
- {open && ( -
- -
- )} +
+
) } diff --git a/frontend/src/components/layout/RightSidebar.tsx b/frontend/src/components/layout/RightSidebar.tsx index 9f1eee6..8f69b15 100644 --- a/frontend/src/components/layout/RightSidebar.tsx +++ b/frontend/src/components/layout/RightSidebar.tsx @@ -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" > +
@@ -255,6 +257,7 @@ export function RightSidebar() { role="region" aria-label="Photo metadata" > +
diff --git a/frontend/src/components/preview/PreviewView.tsx b/frontend/src/components/preview/PreviewView.tsx index c26caf3..1a09dbe 100644 --- a/frontend/src/components/preview/PreviewView.tsx +++ b/frontend/src/components/preview/PreviewView.tsx @@ -11,7 +11,7 @@ import type { Photo } from '../../types/photo' import { PreviewImage } from './PreviewImage' import { PreviewFilmstrip } from './PreviewFilmstrip' import { getPreviewImageSrc, isVideo } from './previewSrc' -import { PhotoInfoPanel } from '../sidebar/PhotoInfoPanel' +import { RightSidebar } from '../layout/RightSidebar' import { KeyboardHints } from '../KeyboardHints' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' @@ -309,7 +309,7 @@ export function PreviewView() { * but lives inside the preview overlay so it isn't covered by it. */} {infoPanelOpen && ( )}
diff --git a/frontend/src/components/ui/sonner.tsx b/frontend/src/components/ui/sonner.tsx index 2cc3bb5..b4bba04 100644 --- a/frontend/src/components/ui/sonner.tsx +++ b/frontend/src/components/ui/sonner.tsx @@ -8,22 +8,22 @@ type ToasterProps = React.ComponentProps // existing call sites don't need edits. const Toaster = ({ ...props }: ToasterProps) => (