From a073ee7fb9c429d9645f1987437a0d2a5164098a Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 15 Apr 2026 10:30:16 +0200 Subject: [PATCH] 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) --- .../src/components/admin/UserManagement.tsx | 8 +- frontend/src/components/colors/ColorsView.tsx | 12 ++- .../components/dialogs/DeleteFolderDialog.tsx | 8 +- .../src/components/dialogs/SettingsDialog.tsx | 16 ++-- .../components/duplicates/DuplicatesView.tsx | 8 +- frontend/src/components/filter/FilterBar.tsx | 6 +- frontend/src/components/filter/FilterPill.tsx | 8 +- frontend/src/components/heaps/HeapsPanel.tsx | 14 ++-- .../src/components/layout/LeftSidebar.tsx | 20 ++--- .../src/components/layout/RightSidebar.tsx | 18 +++-- .../components/preview/PreviewFilmstrip.tsx | 6 +- frontend/src/components/rated/RatedView.tsx | 16 ++-- .../src/components/sidebar/PhotoInfoPanel.tsx | 18 ++--- .../src/components/sidebar/TakenAtEditor.tsx | 6 +- frontend/src/components/tags/TagsView.tsx | 12 ++- .../components/timeline/PhotoThumbnail.tsx | 60 +++++++++----- frontend/src/components/timeline/Timeline.tsx | 81 ++++++++++++++----- frontend/src/components/ui/badge.tsx | 39 +++++++++ frontend/src/components/ui/button.tsx | 1 + .../src/components/upload/UploadModal.tsx | 8 +- frontend/src/hooks/usePhotosQuery.ts | 63 ++++++--------- 21 files changed, 268 insertions(+), 160 deletions(-) create mode 100644 frontend/src/components/ui/badge.tsx diff --git a/frontend/src/components/admin/UserManagement.tsx b/frontend/src/components/admin/UserManagement.tsx index dbb4df6..a2690e9 100644 --- a/frontend/src/components/admin/UserManagement.tsx +++ b/frontend/src/components/admin/UserManagement.tsx @@ -105,11 +105,7 @@ export function UserManagement() { {u.is_active ? 'Active' : 'Inactive'} @@ -129,7 +125,7 @@ export function UserManagement() { -

{selectedGroup.label}

+

{selectedGroup.label}

@@ -137,7 +138,7 @@ export function RatedView() { {groups.map((group, i) => (
)} - + {group.count} - +
-

{group.label}

+

{group.label}

))} diff --git a/frontend/src/components/sidebar/PhotoInfoPanel.tsx b/frontend/src/components/sidebar/PhotoInfoPanel.tsx index 9afae96..57d4307 100644 --- a/frontend/src/components/sidebar/PhotoInfoPanel.tsx +++ b/frontend/src/components/sidebar/PhotoInfoPanel.tsx @@ -10,7 +10,7 @@ import { ShoppingBasket, Trash2, } from 'lucide-react' -import clsx from 'clsx' +import { cn } from '@/lib/utils' import { useQuery, useMutation, @@ -367,13 +367,13 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro // Single themable input class so the same component reads against either // the surface (grid sidebar) or a darker preview overlay. - const inputClass = clsx( + const inputClass = cn( 'w-full rounded border px-2 py-1 text-sm focus:outline-none', darkTheme ? 'border-white/15 bg-black/40 text-white placeholder-white/40 focus:border-primary' : 'border-border bg-bg text-text placeholder-text-faint focus:border-primary' ) - const monoInputClass = clsx( + const monoInputClass = cn( 'w-full rounded border px-2 py-1 font-mono text-xs focus:outline-none', darkTheme ? 'border-white/15 bg-black/40 text-white placeholder-white/40 focus:border-primary' @@ -382,7 +382,7 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro return (
@@ -454,7 +454,7 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro title={`Set rating to ${value}`} > updateMutation.mutate({ color_label: active ? null : value }) } - className={clsx( + className={cn( 'h-5 w-5 rounded-full ring-offset-2 ring-offset-surface transition-all', className, active ? 'ring-2 ring-primary' : 'opacity-60 hover:opacity-100' @@ -509,7 +509,7 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro heapMutation.mutate({ remove: isInActiveHeap }) }} disabled={!activeHeap || heapMutation.isPending} - className={clsx( + className={cn( 'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50', isInActiveHeap ? 'bg-pick/20 text-pick' @@ -528,7 +528,7 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro