fix: more audit findings — perf, types, and a11y polish

- backend/photos: collapse the per-tag subquery loop in the tag filter
  into a single GROUP BY ... HAVING COUNT(DISTINCT) = N subquery so the
  cost is independent of how many tags the user is filtering on.
- useFilterUrlSync: type the parseUrl return value as
  Partial<FilterState> & { currentSection?: string } so the section field
  doesn't need an (out as any) cast.
- Timeline sticky header: bump opacity, padding, and border so it reads
  more clearly against the underlying grid.
- FilterPill clear: convert the nested <button> (invalid HTML — buttons
  cannot nest) to a span with role=button + keyboard handler, with a
  larger hit area.
- RightSidebar: add aria-label to the close-X buttons so screen readers
  announce them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 20:22:23 +02:00
parent 749e836617
commit a55839d9a2
5 changed files with 33 additions and 14 deletions

View File

@@ -28,9 +28,14 @@ const ALLOWED_SORT_FIELDS: SortField[] = [
]
const ALLOWED_SORT_ORDERS: SortOrder[] = ['asc', 'desc']
function parseUrl(): Partial<FilterState> {
// What parseUrl returns: a partial filter state, plus the optional
// section id (which lives on the store but isn't part of FilterState
// itself). The hydrate action accepts this exact shape.
type HydratePayload = Partial<FilterState> & { currentSection?: string }
function parseUrl(): HydratePayload {
const sp = new URLSearchParams(window.location.search)
const out: Partial<FilterState> = {}
const out: HydratePayload = {}
const q = sp.get('q')
if (q) out.q = q
@@ -83,7 +88,7 @@ function parseUrl(): Partial<FilterState> {
if (groupBy === 'date' || groupBy === 'tag') out.groupBy = groupBy
const section = sp.get('section')
if (section) (out as any).currentSection = section
if (section) out.currentSection = section
const sortBy = sp.get('sort')
if (sortBy && ALLOWED_SORT_FIELDS.includes(sortBy as SortField)) {