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:
@@ -26,7 +26,7 @@ import {
|
||||
Upload as UploadIcon,
|
||||
Download as DownloadIcon,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { sourceFolders, photos as photosApi, downloads, type FolderTreeNode } from '../../services/api'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from '../ToastContainer'
|
||||
@@ -487,11 +487,11 @@ export function LeftSidebar() {
|
||||
return (
|
||||
<div key={item.id}>
|
||||
<div
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'group flex cursor-pointer items-center gap-1',
|
||||
isSectionHeader
|
||||
? 'mt-2 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:text-text'
|
||||
: clsx(
|
||||
: cn(
|
||||
// Fixed h-[24px] (not min-h) locks the row height so the
|
||||
// hover-only kebab button can't grow the row vertically.
|
||||
'h-[24px] rounded px-2 text-[12px] leading-none',
|
||||
@@ -577,7 +577,7 @@ export function LeftSidebar() {
|
||||
* without hunting through the kebab menu. */}
|
||||
{item.icon && !isSectionHeader && (
|
||||
<span
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'flex-shrink-0',
|
||||
isSelected
|
||||
? 'text-primary'
|
||||
@@ -619,7 +619,7 @@ export function LeftSidebar() {
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'flex-1 truncate',
|
||||
item.isHidden && !isSelected && 'italic text-text-muted/80'
|
||||
)}
|
||||
@@ -677,7 +677,7 @@ export function LeftSidebar() {
|
||||
onOpenChange={(o) => setOpenMenuId(o ? item.id : null)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'relative flex-shrink-0',
|
||||
isMenuOpen ? 'block' : 'hidden group-hover:block'
|
||||
)}
|
||||
@@ -871,7 +871,7 @@ export function LeftSidebar() {
|
||||
return (
|
||||
<div
|
||||
key={sf.id}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'flex h-[24px] cursor-pointer items-center gap-1 rounded px-2 text-[12px] leading-none',
|
||||
isSelected ? 'bg-primary/20 text-primary' : 'text-text hover:bg-surface-2',
|
||||
)}
|
||||
@@ -881,7 +881,7 @@ export function LeftSidebar() {
|
||||
}
|
||||
>
|
||||
<Users
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 flex-shrink-0',
|
||||
isSelected ? 'text-primary' : 'text-text-muted'
|
||||
)}
|
||||
@@ -922,7 +922,7 @@ export function LeftSidebar() {
|
||||
)}
|
||||
<button
|
||||
onClick={logout}
|
||||
className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-red-400 flex-shrink-0"
|
||||
className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-reject flex-shrink-0"
|
||||
title="Sign out"
|
||||
>
|
||||
<LogOut className="h-3 w-3" />
|
||||
@@ -933,7 +933,7 @@ export function LeftSidebar() {
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => navigateToSection('settings', {})}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded px-2 py-1 text-[12px] hover:bg-surface-2 hover:text-text',
|
||||
currentSection === 'settings' ? 'text-primary' : 'text-text-muted',
|
||||
)}
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user