feat: colors view and color label thumbnail ornament
Adds a Colors entry under Views that buckets photos in canonical R-O-Y-G-B-P order (plus an Uncolored tail), mirroring the rated/tags grouping pattern. Surfaces the color label on each thumbnail as a small swatch chip preceding the rating badge in the BL corner so labels are visible everywhere, not just inside the new view. Also types color_label on the Photo interface — the backend already returned it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { useFilterStore } from '../../store/filterStore'
|
||||
import { PhotoThumbnail } from './PhotoThumbnail'
|
||||
import { usePhotosQuery } from '../../hooks/usePhotosQuery'
|
||||
import { useActiveHeapMembers } from '../../hooks/useActiveHeapMembersQuery'
|
||||
import { COLOR_LABEL_OPTIONS } from '../../constants/colorLabels'
|
||||
import type { Photo } from '../../types/photo'
|
||||
|
||||
// Layout constants for the grid + grouped headers.
|
||||
@@ -26,11 +27,13 @@ type TimelineItem =
|
||||
/**
|
||||
* Build the flat header|row item array the virtualizer renders.
|
||||
*
|
||||
* Four modes:
|
||||
* Five modes:
|
||||
* - groupBy='tag': one bucket per unique tag (plus an "Untagged" bucket
|
||||
* for photos with no tags). A photo with N tags appears in N buckets.
|
||||
* - groupBy='rating': one bucket per star rating 5..1 (plus "Unrated"
|
||||
* for rating 0). Each photo lands in exactly one bucket.
|
||||
* - groupBy='color': one bucket per color label, in canonical order
|
||||
* (plus an "Uncolored" bucket for photos with no label).
|
||||
* - groupBy='date' AND sortBy is a date field: month buckets (existing).
|
||||
* - otherwise: one un-headered stream.
|
||||
*/
|
||||
@@ -39,7 +42,7 @@ function buildItems(
|
||||
columns: number,
|
||||
rowHeight: number,
|
||||
sortBy: string,
|
||||
groupBy: 'date' | 'tag' | 'rating'
|
||||
groupBy: 'date' | 'tag' | 'rating' | 'color'
|
||||
): TimelineItem[] {
|
||||
if (photos.length === 0) return []
|
||||
|
||||
@@ -154,6 +157,55 @@ function buildItems(
|
||||
return items
|
||||
}
|
||||
|
||||
// ── Color label grouping ──────────────────────────────────────────────
|
||||
if (groupBy === 'color') {
|
||||
// Bucket by color_label. Each photo lands in exactly one bucket;
|
||||
// photos with no label go into "Uncolored".
|
||||
const colorBuckets = new Map<string, PhotoCell[]>()
|
||||
const uncolored: PhotoCell[] = []
|
||||
|
||||
photos.forEach((photo, globalIndex) => {
|
||||
const cell: PhotoCell = { photo, globalIndex }
|
||||
const label = photo.color_label
|
||||
if (label) {
|
||||
const arr = colorBuckets.get(label) ?? []
|
||||
arr.push(cell)
|
||||
colorBuckets.set(label, arr)
|
||||
} else {
|
||||
uncolored.push(cell)
|
||||
}
|
||||
})
|
||||
|
||||
// Walk the canonical color order so headers always read R-O-Y-G-B-P,
|
||||
// matching every other color UI in the app. Skip empty buckets and
|
||||
// ignore any unexpected label values that aren't in the canonical
|
||||
// list (they'd be invalid backend state).
|
||||
let bucketIndex = 0
|
||||
for (const { value } of COLOR_LABEL_OPTIONS) {
|
||||
const cells = colorBuckets.get(value)
|
||||
if (!cells || cells.length === 0) continue
|
||||
const label = value.charAt(0).toUpperCase() + value.slice(1)
|
||||
items.push({
|
||||
type: 'header',
|
||||
key: `color::${bucketIndex}::${value}`,
|
||||
label,
|
||||
height: HEADER_HEIGHT,
|
||||
})
|
||||
pushRowsForGroup(`color::${bucketIndex}::${value}`, cells)
|
||||
bucketIndex++
|
||||
}
|
||||
if (uncolored.length > 0) {
|
||||
items.push({
|
||||
type: 'header',
|
||||
key: `color::${bucketIndex}::__uncolored`,
|
||||
label: 'Uncolored',
|
||||
height: HEADER_HEIGHT,
|
||||
})
|
||||
pushRowsForGroup(`color::${bucketIndex}::uncolored`, uncolored)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// ── Date grouping (existing) ──────────────────────────────────────────
|
||||
const isDateSort = sortBy === 'taken_at' || sortBy === 'added_at'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user