diff --git a/tests/browser/workspace-images.spec.mjs b/tests/browser/workspace-images.spec.mjs index 65ad2d6..2ccd007 100644 --- a/tests/browser/workspace-images.spec.mjs +++ b/tests/browser/workspace-images.spec.mjs @@ -1226,3 +1226,21 @@ test('a debounced search that lands after a sort change does not reload the same await page.waitForTimeout(1000); expect(reads.filter(search => new URLSearchParams(search).get('q') === 'same')).toHaveLength(1); }); + +// #249 D18: the concept's hover zoom (1.03 over 0.2s); reduced motion makes it instant, and a touch device (hover: none) +// has no sticky zoom after a tap. +for (const motion of ['no-preference', 'reduce']) { + test(`card image zooms on hover per concept (${motion})`, async ({ page }) => { + await page.emulateMedia({ reducedMotion: motion }); + await page.goto(workspace); + await expect(cards(page)).toHaveCount(2); + const card = cards(page).first(); + const img = card.locator('.image-open img'); + const touch = await page.evaluate(() => matchMedia('(hover: none)').matches); + expect(await img.evaluate(n => getComputedStyle(n).transform)).toBe('none'); + await card.locator('.image-open').hover(); + // Touch: the emulated hover must not leave a zoom behind. + await expect.poll(() => img.evaluate(n => getComputedStyle(n).transform)).toBe(touch ? 'none' : 'matrix(1.03, 0, 0, 1.03, 0, 0)'); + expect(await img.evaluate(n => getComputedStyle(n).transitionDuration)).toBe(motion === 'reduce' ? '0s' : '0.2s'); + }); +} diff --git a/web/js/workspace/workspace.css b/web/js/workspace/workspace.css index 5b47105..6e4fe34 100644 --- a/web/js/workspace/workspace.css +++ b/web/js/workspace/workspace.css @@ -65,6 +65,10 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; } /* #249 D32: likewise the continuation's text and Load more wrap instead of pushing the button out of the column. */ .infinite-continuation { flex-wrap: wrap; } +/* #249 D18: the concept zooms a hovered card image (1.03 over 0.2s; its reduced-motion rule makes that instant). A touch + device has no real hover, so a tap must not leave the zoom stuck. */ +@media (hover: none) { .image-open:hover img { transform: none; } } + /* Images cards (#249 C3). #249 D3: bounded rendering mounts only the visible cards and places each one itself, in every layout (the concept places only masonry cards), so the grid reserves the collection's height and every card is absolute. Columns and gap still come from the concept's `.image-grid` tracks. */