ui(timeline): drop redundant tints in discarded + active-heap views

When the grid is already filtered to discarded photos or to the active
heap, every cell would carry the same tint — the grayscale wash or the
green overlay stopped signalling anything and just made thumbnails
harder to read. Timeline now suppresses both when the corresponding
filter is active; the BR icon badges stay for colorblind readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 21:56:07 +02:00
parent 96624bf853
commit b7f2eb7832
2 changed files with 27 additions and 5 deletions

View File

@@ -74,6 +74,14 @@ interface PhotoThumbnailProps {
/** True when the photo belongs to the currently active heap — drives /** True when the photo belongs to the currently active heap — drives
* the green tint overlay. */ * the green tint overlay. */
isInActiveHeap?: boolean isInActiveHeap?: boolean
/** Suppress the desaturate/opacity treatment applied to discarded
* photos. Set by the Timeline when the view is already filtered to
* discarded only — every cell would otherwise carry the same tint. */
hideDiscardedTint?: boolean
/** Suppress the green "in active heap" tint overlay. Set when the
* view is filtered to the active heap, where every cell would
* otherwise be wash-green. */
hideActiveHeapTint?: boolean
/** Active text-search query, passed in from Timeline rather than /** Active text-search query, passed in from Timeline rather than
* subscribed-to here so we don't have N thumbnails each running a * subscribed-to here so we don't have N thumbnails each running a
* per-keystroke selector. Empty string disables the highlight. */ * per-keystroke selector. Empty string disables the highlight. */
@@ -91,6 +99,8 @@ function PhotoThumbnailImpl({
fill = false, fill = false,
isSelected, isSelected,
isInActiveHeap = false, isInActiveHeap = false,
hideDiscardedTint = false,
hideActiveHeapTint = false,
searchQuery = '', searchQuery = '',
onClick, onClick,
onDoubleClick, onDoubleClick,
@@ -267,9 +277,11 @@ function PhotoThumbnailImpl({
className={cn( className={cn(
'h-full w-full object-cover transition-opacity duration-200', 'h-full w-full object-cover transition-opacity duration-200',
imageLoaded ? 'opacity-100' : 'opacity-0', imageLoaded ? 'opacity-100' : 'opacity-0',
// Discarded photos fade out + desaturate so the trash section // Discarded photos fade out + desaturate so they read as
// reads as a trash section, not just another grid view. // trash when mixed into a normal timeline. Suppressed when
photo.is_discarded && 'opacity-50 grayscale' // the view is already filtered to discarded only (every
// cell would get the same tint — pure noise).
photo.is_discarded && !hideDiscardedTint && 'opacity-50 grayscale'
)} )}
onLoad={handleImageLoad} onLoad={handleImageLoad}
onError={handleImageError} onError={handleImageError}
@@ -310,7 +322,7 @@ function PhotoThumbnailImpl({
* both selected AND in the active heap shows both tints. Never * both selected AND in the active heap shows both tints. Never
* tinted on hover so the keyboard-driven selection stays * tinted on hover so the keyboard-driven selection stays
* distinguishable from the mouse-driven hover. */} * distinguishable from the mouse-driven hover. */}
{isInActiveHeap && ( {isInActiveHeap && !hideActiveHeapTint && (
<div className="pointer-events-none absolute inset-0 bg-emerald-500/40" /> <div className="pointer-events-none absolute inset-0 bg-emerald-500/40" />
)} )}
{isSelected && ( {isSelected && (

View File

@@ -173,6 +173,8 @@ export function Timeline() {
const sortBy = useFilterStore((s) => s.sortBy) const sortBy = useFilterStore((s) => s.sortBy)
const groupBy = useFilterStore((s) => s.groupBy) const groupBy = useFilterStore((s) => s.groupBy)
const currentSection = useFilterStore((s) => s.currentSection) const currentSection = useFilterStore((s) => s.currentSection)
const flag = useFilterStore((s) => s.flag)
const filterHeapId = useFilterStore((s) => s.heapId)
// Subscribed once at this level and passed down to each // Subscribed once at this level and passed down to each
// PhotoThumbnail as a prop. Previously every thumbnail had its own // PhotoThumbnail as a prop. Previously every thumbnail had its own
// subscription, multiplying keystroke renders by the row count. // subscription, multiplying keystroke renders by the row count.
@@ -231,7 +233,13 @@ export function Timeline() {
// Membership in the active heap (drives the green tint on each // Membership in the active heap (drives the green tint on each
// thumbnail). Subscribed once at this level so we don't have hundreds // thumbnail). Subscribed once at this level so we don't have hundreds
// of thumbnails each subscribing to the same query. // of thumbnails each subscribing to the same query.
const { memberIds: activeHeapMembers } = useActiveHeapMembers() const { activeHeap, memberIds: activeHeapMembers } = useActiveHeapMembers()
// When the view is already filtered to a specific attribute, every
// thumbnail would carry the same indicator — redundant visual noise.
// Pass these down so PhotoThumbnail can skip the tint in those cases.
const hideDiscardedTint = flag === 'discarded'
const hideActiveHeapTint =
filterHeapId != null && activeHeap != null && filterHeapId === activeHeap.id
// Stable cell handlers. PhotoThumbnail is wrapped in React.memo so // Stable cell handlers. PhotoThumbnail is wrapped in React.memo so
// identity-stable callbacks let it skip re-render on unrelated store // identity-stable callbacks let it skip re-render on unrelated store
@@ -812,6 +820,8 @@ export function Timeline() {
fill fill
isSelected={selectedPhotos.includes(photo.id)} isSelected={selectedPhotos.includes(photo.id)}
isInActiveHeap={activeHeapMembers.has(photo.id)} isInActiveHeap={activeHeapMembers.has(photo.id)}
hideDiscardedTint={hideDiscardedTint}
hideActiveHeapTint={hideActiveHeapTint}
searchQuery={searchQuery} searchQuery={searchQuery}
onClick={handleCellClick} onClick={handleCellClick}
onDoubleClick={handleCellDoubleClick} onDoubleClick={handleCellDoubleClick}