diff --git a/README.md b/README.md index 0aed9f5..72340bf 100644 --- a/README.md +++ b/README.md @@ -1411,6 +1411,15 @@ Things I chose while building, with the reasoning: pre-blurred wallpaper — the scene is clearly visible through the glass, and the pixel-contrast scan still passes. +73. **Touch devices blur the live backdrop; desktop keeps the bake.** Mobile engines repaint + `background-attachment: fixed` late (and iOS treats it as scroll), so the baked glass showed a + stale crop of the wallpaper until a tap forced a repaint — "some of the UI looks bugged until I + click it". On touch devices (`max-width: 639px` or `pointer: coarse`) the bake is dropped for a + real `backdrop-filter` over the live scene (what the floating chrome always did) — the bubbles + now blur *through* the glass as they pass, and the position is correct by construction. The + hand-rolled frost fills keep their own tints there; desktop keeps the baked layer (measured + cheaper there and verified correct). Pinned in `tests/mobile-test.mjs` with a touch context. + 72. **More, shinier bubbles.** Aero's rising bubbles went from ten to eighteen (the extra eight interleaved between the others, 18px to 96px, 10-23s runs) with a tighter specular hotspot, a second reflected-light glint low on the sphere, a 1px glass edge and a brighter inner rim on diff --git a/app/globals.css b/app/globals.css index d4056bd..67060f6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -597,6 +597,57 @@ background-repeat: no-repeat, no-repeat; } +/* Touch devices: the baked backdrop is `background-attachment: fixed`, which + mobile engines repaint late (and iOS treats as scroll) — panes showed a + stale crop of the wallpaper until a tap forced a repaint ("some UI looks + bugged until I click it"). Touch hardware has the GPU budget for the honest + version instead: a real backdrop blur over the live scene, so the bubbles + blur through the glass as they pass. Desktop keeps the bake (measured + cheaper there, and verified correct). */ +@media (max-width: 639px), (pointer: coarse) { + .aero .card, + .aero .glass-solid, + .aero .sidebar-surface, + .aero .gate-card, + .aero .list-group, + .aero .nav-action, + .aero .nav-row[aria-current="page"], + .aero .nav-row[data-active="true"], + .aero .glass-chip, + .aero .glass-pill, + .aero .btn-secondary, + .aero .btn-primary, + .aero .grad-ice, + .aero .segmented, + .aero .segmented > button[aria-pressed="true"], + .aero .segmented > a[aria-current="page"], + .aero .chat-bubble, + .aero .chat-answer { + background-image: none; + @apply backdrop-blur-md backdrop-saturate-[1.4]; + } + .aero .chat-bubble, + .aero .chat-answer { + background-color: rgb(255 255 255 / 0.55); + } + .aero .tabbar { + background-image: none; + } + /* The hand-rolled frost fills drop the bake too and keep their own tints. */ + .aero [class*="bg-white/35"], + .aero [class*="bg-white/40"], + .aero [class*="bg-white/45"], + .aero [class*="bg-white/50"], + .aero [class*="bg-white/55"], + .aero [class*="bg-white/60"], + .aero [class*="bg-white/70"], + .aero [class*="bg-[var(--surface)]"], + .aero [class*="bg-[var(--surface-strong)]"], + .aero [class*="bg-frost-50"] { + background-image: none; + } +} + /* Inside a sheet or dialog the backdrop is not the raw scene — a scrim and a more opaque sheet glass sit in between — so the viewport-aligned frosted layer would not match what is actually behind the element (it showed the diff --git a/package.json b/package.json index 9c90e6b..0b287c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freezmo", - "version": "0.3.7", + "version": "0.3.8", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/public/sw.js b/public/sw.js index c7dbefd..1ffe03c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,7 +9,7 @@ * * Bumping CACHE_VERSION is how a new deploy invalidates old assets. */ -const CACHE_VERSION = "freezmo-v66"; +const CACHE_VERSION = "freezmo-v67"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/tests/lib.mjs b/tests/lib.mjs index d09ab95..555af3c 100644 --- a/tests/lib.mjs +++ b/tests/lib.mjs @@ -121,8 +121,8 @@ export async function seed( ); } -export async function makePage(browser, { width = 390, height = 844, deviceScaleFactor = 2, reducedMotion, colorScheme } = {}) { - const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor, reducedMotion, colorScheme }); +export async function makePage(browser, { width = 390, height = 844, deviceScaleFactor = 2, reducedMotion, colorScheme, isMobile, hasTouch } = {}) { + const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor, reducedMotion, colorScheme, isMobile, hasTouch }); const page = await context.newPage(); return { context, page }; } diff --git a/tests/mobile-test.mjs b/tests/mobile-test.mjs index 1213db4..398b890 100644 --- a/tests/mobile-test.mjs +++ b/tests/mobile-test.mjs @@ -287,6 +287,26 @@ const AXE = axeSource(); await context.close(); } +/* 6d. touch devices blur the real backdrop (the baked layer mispaints) - */ +{ + const { context, page } = await makePage(browser, { width: 390, height: 844, isMobile: true, hasTouch: true }); + await seed(page, { decks: [DECK], theme: "aero" }); + await page.goto(BASE, { waitUntil: "networkidle" }); + await page.waitForTimeout(1000); + const glass = await page.evaluate(() => { + const coarsish = matchMedia("(pointer: coarse)").matches || matchMedia("(max-width: 639px)").matches; + const el = document.querySelector(".segmented"); + const cs = getComputedStyle(el); + return { coarsish, backdrop: cs.backdropFilter, image: cs.backgroundImage }; + }); + check( + "theme: touch glass blurs the live backdrop (no baked layer)", + glass.coarsish && glass.backdrop.includes("blur") && glass.image === "none", + JSON.stringify(glass), + ); + await context.close(); +} + /* 6c. a dark OS preference must not leak the dark palette into Aero ------ */ { const { context, page } = await makePage(browser, { width: 390, height: 844, colorScheme: "dark" });