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:
2026-04-09 20:41:06 +02:00
parent 870e7dd3d3
commit c973ca0443
5 changed files with 98 additions and 18 deletions

View File

@@ -4,6 +4,7 @@ import clsx from 'clsx'
import { photos as photosApi } from '../../services/api'
import type { Photo } from '../../types/photo'
import { usePhotoStore } from '../../store/photoStore'
import { COLOR_LABEL_OPTIONS } from '../../constants/colorLabels'
/** Custom MIME used by HeapsPanel to recognise our drag payload. */
export const PHOTO_DRAG_MIME = 'application/x-mulita-photos'
@@ -28,12 +29,21 @@ export const THUMB_BADGE_BASE =
export const THUMB_BADGE_SQUARE = 'w-5 justify-center !px-0'
/** Standard icon size for any lucide glyph inside a badge. */
export const THUMB_BADGE_ICON = 'h-3 w-3'
export const THUMB_BADGE_PRIMARY =
'bg-primary shadow-[0_0_0_1px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.28),0_1px_2px_rgba(0,0,0,0.5)]'
/** Chiseled-pixel frame: 1px outline + inset top highlight + drop shadow.
* Shared by every "user-affirmed" variant so any swatch (primary, pick,
* color label) reads as part of the same badge family. */
const THUMB_BADGE_FRAME =
'shadow-[0_0_0_1px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.28),0_1px_2px_rgba(0,0,0,0.5)]'
export const THUMB_BADGE_PRIMARY = `bg-primary ${THUMB_BADGE_FRAME}`
export const THUMB_BADGE_NEUTRAL =
'bg-black/75 backdrop-blur-sm shadow-[0_0_0_1px_rgba(255,255,255,0.12),inset_0_1px_0_rgba(255,255,255,0.08),0_1px_2px_rgba(0,0,0,0.55)]'
export const THUMB_BADGE_PICK =
'bg-pick shadow-[0_0_0_1px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.28),0_1px_2px_rgba(0,0,0,0.5)]'
export const THUMB_BADGE_PICK = `bg-pick ${THUMB_BADGE_FRAME}`
/** Tailwind bg-class for each color label, looked up at render time so the
* classnames are statically present in the source for the JIT to scan. */
const COLOR_LABEL_BG: Record<string, string> = Object.fromEntries(
COLOR_LABEL_OPTIONS.map((o) => [o.value, o.className])
)
// Auto-retry schedule (ms). Backend generates thumbs on-demand via Celery, so
// the first hit on a freshly-scanned library returns 404 "not ready" until
@@ -254,18 +264,30 @@ export function PhotoThumbnail({
</div>
)}
{/* BL — rating */}
{photo.rating > 0 && (
<div
className={clsx(
'absolute bottom-1 left-1 gap-0.5',
THUMB_BADGE_BASE,
THUMB_BADGE_PRIMARY
{/* BL — color label + rating. Color comes first (left of rating)
* so the swatch reads as a "category dot" prefixing the stars. */}
{(photo.color_label || photo.rating > 0) && (
<div className="absolute bottom-1 left-1 flex items-center gap-1">
{photo.color_label && COLOR_LABEL_BG[photo.color_label] && (
<div
className={clsx(
THUMB_BADGE_BASE,
THUMB_BADGE_SQUARE,
COLOR_LABEL_BG[photo.color_label],
THUMB_BADGE_FRAME
)}
title={`Color label: ${photo.color_label}`}
/>
)}
{photo.rating > 0 && (
<div
className={clsx('gap-0.5', THUMB_BADGE_BASE, THUMB_BADGE_PRIMARY)}
>
{Array.from({ length: photo.rating }).map((_, i) => (
<Star key={i} className={clsx(THUMB_BADGE_ICON, 'fill-white')} />
))}
</div>
)}
>
{Array.from({ length: photo.rating }).map((_, i) => (
<Star key={i} className={clsx(THUMB_BADGE_ICON, 'fill-white')} />
))}
</div>
)}