diff --git a/README.md b/README.md index 8059642..5992133 100644 --- a/README.md +++ b/README.md @@ -1434,6 +1434,20 @@ Things I chose while building, with the reasoning: (measured: 268..1280 at a 1280 window) — a flat pane floating 40px inside the scene read as "cut off next to the sidebar". +94. **Aero glass goes back to glass (the "even wash" read as a white pane).** The uniform wash + (`background-glass.webp`) from decisions 91/93 made the rail and the chat chrome nearly + opaque: "why is the sidebar so white and non-translucent on the frutiger theme now? and the + chat mode looks even worse... its supposed to be translucent and blurry!". The rail, the chat + header, the welcome card and the composer now use the *same* recipe as every other surface in + the app — `background-blur.webp` under the shared gloss + veil + `--glass-bg`, exactly what + the deck cards do — and the dedicated wash asset is deleted. Measured on the pixels: the rail + runs from `181,229,241` over the sky to `133,174,75` over the grass (Δ166, unmistakably + translucent), while the chat and library screens still scan at 0 below-minimum text-contrast + findings in all three themes (Aero's near-black text tokens carry the thin glass). The chat + suite pins both halves of that: the shared recipe on all four panels *and* a translucency + check that the rail's tint follows the scene, so a future "make it uniform" pass cannot + quietly reintroduce an opaque panel. Phone touch keeps the flat 32% Ask-pill glass from 93. + 93. **Phone Aero glass, and a timed run you can actually see.** Two follow-ups from a phone. - *The chat read as a white pane.* On touch devices the chat header, welcome card and composer carried a 55% flat white veil while every other surface in the app used the thin 32% diff --git a/app/globals.css b/app/globals.css index f7f6c22..74deede 100644 --- a/app/globals.css +++ b/app/globals.css @@ -452,6 +452,7 @@ .aero .btn-secondary, .aero .chat-header, .aero .chat-empty, +.aero .chat-composer-band, .aero .segmented, /* Hand-rolled frost fills in the study, exam, chat and homework flows: white/xx utilities and the surface tokens — without this they render as @@ -564,15 +565,14 @@ /* Chat: the longest text in the app sits straight on the wallpaper. The assistant answers are deliberately bubble-less in the flat themes, and the user bubble's thin tint washes out over the bright parts of the scene — in - Aero both get a real pane: the same even wash as the chrome, a touch more - opaque (this is where the reading happens), so an answer reads as its own - bubble and its colour does not drift with the photo behind it. */ + Aero both get a real pane: the shared blurred wallpaper under a veil thick + enough for reading, still translucent enough to read as glass. */ .aero .chat-bubble, .aero .chat-answer { border-color: rgb(255 255 255 / 0.85); background-image: - linear-gradient(rgb(255 255 255 / 0.6), rgb(255 255 255 / 0.6)), - url("/aero/background-glass.webp?v=4"); + linear-gradient(rgb(255 255 255 / 0.3), rgb(255 255 255 / 0.3)), + url("/aero/background-blur.webp?v=4"); background-size: 100% 100%, cover; background-position: 0 0, center 42%; background-attachment: scroll, fixed; @@ -604,31 +604,6 @@ background-repeat: no-repeat, no-repeat; } -/* Chat chrome and the rail: one even frosted recipe. The pre-blurred, - extra-soft wash of the wallpaper (background-glass) sits under a uniform - white veil, so the surface colour cannot drift with the photo behind it — - the rail used to run from clear blue into a muddy green smear, the composer - picked up the river's teal and the header a saturated stripe of sky. */ -.aero .sidebar-surface, -.aero .chat-header, -.aero .chat-empty, -.aero .chat-composer-band { - background-image: - linear-gradient( - 180deg, - rgb(255 255 255 / 0.3) 0%, - rgb(255 255 255 / 0.1) 46%, - rgb(255 255 255 / 0.05) 53%, - rgb(255 255 255 / 0.12) 100% - ), - linear-gradient(rgb(255 255 255 / 0.46), rgb(255 255 255 / 0.46)), - url("/aero/background-glass.webp?v=4"); - background-size: 100% 100%, 100% 100%, cover; - background-position: 0 0, 0 0, center 42%; - background-attachment: scroll, scroll, fixed; - background-repeat: no-repeat, no-repeat, no-repeat; -} - /* The chat welcome (title, subtitle, suggestion chips): a card like the library's, stretched across the conversation column so it lines up with the floating header above and composer below it. */ diff --git a/package.json b/package.json index a102cc5..663bc8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freezmo", - "version": "0.6.2", + "version": "0.6.3", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/public/aero/background-glass.webp b/public/aero/background-glass.webp deleted file mode 100644 index e7a0754..0000000 Binary files a/public/aero/background-glass.webp and /dev/null differ diff --git a/public/sw.js b/public/sw.js index 9296924..f8f9b0e 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-v84"; +const CACHE_VERSION = "freezmo-v85"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/tests/chat-test.mjs b/tests/chat-test.mjs index 38fa311..a146c75 100644 --- a/tests/chat-test.mjs +++ b/tests/chat-test.mjs @@ -320,7 +320,7 @@ try { bl: style.borderBottomLeftRadius, br: style.borderBottomRightRadius, border: style.borderTopWidth, - wash: style.backgroundImage.includes("background-glass"), + wash: style.backgroundImage.includes("background-blur"), }; }; return { @@ -354,8 +354,43 @@ try { ), JSON.stringify(chrome), ); + /* Translucency, measured on the pixels: an opaque (or near-opaque) panel + reads nearly the same colour over the bright sky and over the dark + grass; glass follows the scene behind it. */ + const railShot = await aero.page.screenshot({ type: "png", scale: "css" }); + const railData = `data:image/png;base64,${railShot.toString("base64")}`; + const railTint = await aero.page.evaluate(async ({ dataUrl }) => { + const image = await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = dataUrl; + }); + const canvas = document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0); + const strip = (y) => { + const data = ctx.getImageData(30, y, 200, 12).data; + let r = 0, g = 0, b = 0, n = 0; + for (let i = 0; i < data.length; i += 4) { + r += data[i]; g += data[i + 1]; b += data[i + 2]; n += 1; + } + return [Math.round(r / n), Math.round(g / n), Math.round(b / n)]; + }; + return { upper: strip(440), lower: strip(680) }; + }, { dataUrl: railData }); + const tintDistance = Math.max( + ...[0, 1, 2].map((index) => Math.abs(railTint.upper[index] - railTint.lower[index])), + ); check( - "desktop: the rail and the chat chrome share one even wash", + "desktop: the rail is still translucent (its tint follows the scene)", + tintDistance >= 30, + `${railTint.upper.join(",")} → ${railTint.lower.join(",")} (Δ${tintDistance})`, + ); + check( + "desktop: the rail and the chat chrome share the app's glass recipe", chrome !== null && [chrome.card, chrome.header, chrome.empty, chrome.band].every((panel) => panel.wash), chrome ? [chrome.card.wash, chrome.header.wash, chrome.empty.wash, chrome.band.wash].join(",") : "missing", );