- FilterPill: drop the inline value text from the active state. Pills
now stay the same width whether or not a filter is set; the popover
is the canonical place to read the value, and the title attribute
surfaces it on hover.
- TopBar: remove the search input — search lives in the filter bar now.
- FilterBar: add a search input on the left, with the pill cluster
centered between it and a flex-shrink-0 Clear-all on the right.
- LeftSidebar / HeapsPanel: count badges use a fixed-width slot
(h-5 min-w-[24px], tabular-nums) so counts line up in the same
visual column across rows. Empty rows reserve the slot.
- LeftSidebar: pull section counts (All Photos, Rated, Duplicates,
Discarded) from a new useLibraryStatsQuery hook backed by the
expanded /library/stats endpoint. Tags count was already wired.
- backend/library: stats endpoint returns per-section counts that
match the filter the sidebar applies on click.
- Stats invalidation hooked into the standard photo-mutation paths.
- RightSidebar header: h-12 to match TopBar height.
- Timeline sticky date overlay: only show once the natural in-grid
header has scrolled OUT of the viewport. Avoids the duplicate-label
flash when both labels would be visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related sidebar UX bugs.
1. Parent folders weren't clickable
renderTreeItem's onClick called toggleExpanded(item.id) for any
row with children — so a parent folder only expanded/collapsed,
never applied its filter. Restructured: folder rows always call
applyLibraryNode (which the photos endpoint already expands to
include descendants), and the chevron remains a separate
stopPropagation button for expansion. Other group headers
(Library, Folders, Tags) still toggle expansion on row click
since they have no associated filter.
Result: clicking any folder at any depth filters the timeline
to that folder + every descendant, matching the Lightroom
model the user expects.
2. New files not appearing after Scan all folders
scanLibraryMutation.onSettled invalidated ['photos'] when the
trigger returned, but POST /library/scan just queues the celery
task and returns immediately. By the time the worker finishes
walking the directory and inserting new rows, the photos query
has already refetched (with no new data) and is sitting on a
30-second staleTime — so newly-indexed photos stayed invisible
until the next manual refetch.
Fix: ScanProgress already polls /library/scan/status. Track the
previous is_scanning value via a ref; when it transitions from
true → false, invalidate ['photos'], ['folders'], ['folders',
'tree'], ['heaps'], and ['tags']. That's the actual moment new
data is available, regardless of how the scan was triggered
(button, watcher, startup).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>