From 4456deb146105f06799895ee7374dda914b91530 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 25 Jun 2026 20:08:16 +0200 Subject: [PATCH] fix(collections): make per-card remove buttons usable on touch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remove buttons on a collection's cards were revealed only on hover / focus-within (opacity 0 → 1), so on touch they were invisible and undiscoverable. Show them persistently at the 640px breakpoint and enlarge the hit area from 24px to 32px (kept below the full 44px floor because the button floats over a 4-up cover thumbnail). Test pins the CSS contract (jsdom can't evaluate @media): the mobile block makes `.remove` opaque. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/routes/collections/[id]/+page.svelte | 13 +++++ .../collections/[id]/remove-touch.test.ts | 49 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 frontend/src/routes/collections/[id]/remove-touch.test.ts diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 981e4d1..4c8585a 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -406,6 +406,19 @@ opacity: 1; } + /* Touch has no hover, so the reveal-on-hover treatment leaves the + remove buttons invisible and undiscoverable. Show them persistently + and enlarge the hit area on phones. (Capped well below 44px because + the button floats over a 4-up cover thumbnail — a full touch-floor + square would swallow the cover.) */ + @media (max-width: 640px) { + .remove { + opacity: 1; + width: 32px; + height: 32px; + } + } + .icon-btn { display: inline-flex; align-items: center; diff --git a/frontend/src/routes/collections/[id]/remove-touch.test.ts b/frontend/src/routes/collections/[id]/remove-touch.test.ts new file mode 100644 index 0000000..504344a --- /dev/null +++ b/frontend/src/routes/collections/[id]/remove-touch.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +// The per-card "remove" buttons on a collection are revealed on hover / +// focus-within (opacity 0 → 1), which leaves them invisible and +// undiscoverable on touch. They must be shown persistently at the mobile +// breakpoint. jsdom can't evaluate @media or layout, so this pins the CSS +// contract: the 640px block makes `.remove` opaque. (Same approach as the +// shared touch-target contract test.) + +const SRC = readFileSync( + resolve(process.cwd(), 'src/routes/collections/[id]/+page.svelte'), + 'utf8' +); + +// Brace-balanced extraction of every `@media (max-width: 640px) { ... }` body. +function mobileBlocks(css: string): string { + const marker = '@media (max-width: 640px)'; + let out = ''; + let from = 0; + for (;;) { + const start = css.indexOf(marker, from); + if (start === -1) break; + const open = css.indexOf('{', start); + if (open === -1) break; + let depth = 0; + let i = open; + for (; i < css.length; i++) { + if (css[i] === '{') depth++; + else if (css[i] === '}') { + depth--; + if (depth === 0) break; + } + } + out += css.slice(open + 1, i) + '\n'; + from = i + 1; + } + return out; +} + +const squish = (s: string) => s.replace(/\s+/g, ' '); + +describe('collection remove buttons on touch', () => { + it('reveals the remove buttons persistently at the mobile breakpoint', () => { + const mobile = squish(mobileBlocks(SRC)); + expect(mobile).toMatch(/\.remove[^}]*opacity:\s*1/); + }); +});