fix: assorted UI polish from review pass

- 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>
This commit is contained in:
2026-04-08 20:45:30 +02:00
parent 696477eefd
commit bd904aca36
13 changed files with 207 additions and 115 deletions

View File

@@ -8,6 +8,7 @@ import { discard as discardApi, photos as photosApi } from '../../services/api'
import { toast } from '../ToastContainer'
import { ConfirmDialog } from '../dialogs/ConfirmDialog'
import { registerUndoable } from '../../store/undoStore'
import { LIBRARY_STATS_QUERY_KEY } from '../../hooks/useLibraryStatsQuery'
/**
* Top-of-timeline bar visible only when the discarded filter is active.
@@ -32,10 +33,12 @@ export function DiscardActionBar() {
async () => {
await photosApi.bulkDiscard(ids)
queryClient.invalidateQueries({ queryKey: ['photos'] })
queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
}
)
clearSelection()
queryClient.invalidateQueries({ queryKey: ['photos'] })
queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
},
onError: (e: any) => toast.error('Restore failed', e.message || 'Unknown error'),
})
@@ -58,6 +61,7 @@ export function DiscardActionBar() {
}
clearSelection()
queryClient.invalidateQueries({ queryKey: ['photos'] })
queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
setDeleteSelectedOpen(false)
},
onError: (e: any) =>
@@ -79,6 +83,7 @@ export function DiscardActionBar() {
}
clearSelection()
queryClient.invalidateQueries({ queryKey: ['photos'] })
queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
setConfirmOpen(false)
},
onError: (e: any) => toast.error('Empty failed', e.message || 'Unknown error'),