perf+style: grid subscription hygiene, a11y, shadcn-style consistency

Perf / a11y (high-impact review items)
- Timeline arrow-key handler binds once per (viewMode, currentSection)
  and reads fresh state via navStateRef instead of an 8-element dep
  array of new-each-render values.
- usePhotosQuery collapses 14 individual Zustand selectors into one
  useShallow selector returning the params object.
- PhotoThumbnail no longer subscribes to the search query directly;
  Timeline subscribes once and passes it down as a prop.
- PhotoThumbnail gains role="button", tabIndex, aria-label, aria-pressed,
  Enter/Space key handlers and a focus-visible ring. Timeline marked
  role="grid"; RightSidebar marked role="region".

Style consistency
- Swap clsx for cn (tailwind-merge aware) across 17 files so
  conflicting utility classes collapse correctly.
- New Badge primitive (ui/badge.tsx) with default/neutral/overlay/
  outline variants; adopted in ColorsView, RatedView, TagsView for
  the repeated count overlay pill.
- Fix palette drift: text-amber-400 -> text-star, text-green-*
  -> text-pick, text-red-* -> text-reject (5 files).
- Button gains an xs size (h-6 px-1.5 text-[11px]) for the repeated
  compact-button pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 10:30:16 +02:00
parent e65e798021
commit a073ee7fb9
21 changed files with 268 additions and 160 deletions

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'
import { X, Star, ShoppingBasket, Trash2 } from 'lucide-react'
import clsx from 'clsx'
import { cn } from '@/lib/utils'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { usePhotoStore } from '../../store/photoStore'
import {
@@ -215,7 +215,11 @@ export function RightSidebar() {
if (selectedPhotos.length === 0) {
return (
<div className="flex h-full flex-col bg-surface">
<div
className="flex h-full flex-col bg-surface"
role="region"
aria-label="Photo metadata"
>
<Header />
<div className="flex flex-1 items-center justify-center p-4 text-center">
<div className="text-text-muted">
@@ -245,7 +249,11 @@ export function RightSidebar() {
if (selectedPhotos.length === 1) {
const id = activePhotoId ?? selectedPhotos[0]
return (
<div className="flex h-full flex-col bg-surface">
<div
className="flex h-full flex-col bg-surface"
role="region"
aria-label="Photo metadata"
>
<Header />
<PhotoInfoPanel photoId={id} />
</div>
@@ -333,7 +341,7 @@ export function RightSidebar() {
onClick={() =>
bulkColorMutation.mutate({ ids: selectedPhotos, color: value })
}
className={clsx(
className={cn(
'h-5 w-5 rounded-full opacity-80 ring-offset-2 ring-offset-surface transition-all hover:opacity-100',
className
)}
@@ -363,7 +371,7 @@ export function RightSidebar() {
heapMutation.mutate({ ids: selectedPhotos, remove: allMembers })
}}
disabled={!activeHeap || heapMutation.isPending}
className={clsx(
className={cn(
allMembers
? 'bg-pick/20 text-pick hover:bg-pick/30'
: 'bg-surface-2 text-text-muted hover:bg-surface-offset'