Thin frosted Aero glass, boot-theme fix, latest background revision
The white surfaces were reading as a milky white panel (a card top was ~71% white). The frosted recipes now use a 14% base veil plus the gloss over the pre-blurred wallpaper, small chrome (rows, chips, segments, buttons) keeps a touch more veil, and nothing nested inside a sheet or dialog uses the viewport-aligned layer (it showed the raw dark water under the settings theme labels at 1.5:1 — nested chrome falls back to the flat fills over the genuinely blurred sheet). The first-load text bug was the boot script: its dark test was `s && !a ? s === 'dark' : matchMedia(...)`, which for a stored aero fell through to the OS preference, so a dark-preferring machine loaded with both `dark` and `aero` classes and every `.dark` rule painted over the wallpaper until a theme toggle re-applied the classes. The stored value now decides alone; a dark-colorScheme check pins it in the mobile suite (123 checks). Background: the user's latest Gemini scene (1280x714) re-encoded to public/aero/background.webp (206 kB) with a fresh pre-blurred sibling. Contrast: 0 below-minimum findings, 0 axe violations, all three themes. Version 0.3.1, SW -> freezmo-v62.
This commit is contained in:
parent
857e6445ae
commit
c88ad2865a
9 changed files with 122 additions and 38 deletions
18
README.md
18
README.md
|
|
@ -1398,6 +1398,20 @@ Things I chose while building, with the reasoning:
|
||||||
phase-1 suite (43 checks, including the real route mapping against a stub Ollama returning a
|
phase-1 suite (43 checks, including the real route mapping against a stub Ollama returning a
|
||||||
monthly-limit body, a two-device sync round-trip, and the generation guards below).
|
monthly-limit body, a two-device sync round-trip, and the generation guards below).
|
||||||
|
|
||||||
|
66. **Aero's glass is thin, and the OS dark preference can't leak into it.** Two fixes from one
|
||||||
|
report ("the white UI should be blurry and transparent; some text is hard to read on first
|
||||||
|
load, until the theme is toggled"). The text bug was at boot: the inline script's dark test
|
||||||
|
was `s && !a ? s === 'dark' : matchMedia(...)`, which for a stored `aero` fell through to the
|
||||||
|
OS preference — so on a dark-preferring machine the page loaded with **both** `dark` and
|
||||||
|
`aero` classes and every `.dark` rule (starting with `body`'s colour and the segment labels)
|
||||||
|
painted the dark palette over the bright wallpaper, until a theme toggle re-applied the
|
||||||
|
classes. The stored value now decides alone and the class is toggled explicitly;
|
||||||
|
`tests/mobile-test.mjs` pins it with a dark-`colorScheme` browser context. And the surfaces
|
||||||
|
drop their thick white veils (a card top measured ~71% white) for a 14% base veil plus the
|
||||||
|
gloss over the pre-blurred wallpaper — the scene is clearly visible through the glass, small
|
||||||
|
chrome (rows, chips, segments, buttons) keeps slightly more veil, and the pixel-contrast scan
|
||||||
|
that re-runs in the deploy e2e gate still passes.
|
||||||
|
|
||||||
65. **Slide content never flex-shrinks, and the AI note appears once.** A long typed answer in a
|
65. **Slide content never flex-shrinks, and the AI note appears once.** A long typed answer in a
|
||||||
short-answer activity shrank the textarea inside the slide's flex column (58px instead of its
|
short-answer activity shrank the textarea inside the slide's flex column (58px instead of its
|
||||||
~82px), clipping the user's text mid-line on a phone; every direct child of a study/review/guide
|
~82px), clipping the user's text mid-line on a phone; every direct child of a study/review/guide
|
||||||
|
|
@ -1415,14 +1429,14 @@ Things I chose while building, with the reasoning:
|
||||||
|
|
||||||
63. **Frutiger Aero is the third theme.** Settings → Theme offers Light, Dark and Frutiger Aero.
|
63. **Frutiger Aero is the third theme.** Settings → Theme offers Light, Dark and Frutiger Aero.
|
||||||
Aero is not a dark/light variant — it replaces the palette and the scene: the wallpaper is a
|
Aero is not a dark/light variant — it replaces the palette and the scene: the wallpaper is a
|
||||||
Frutiger Aero scene made with Gemini (re-encoded to WebP in `public/aero/`), with ten drawn
|
Frutiger Aero scene made with Gemini (latest 1280x714 revision re-encoded to WebP in `public/aero/`), with ten drawn
|
||||||
bubbles rising over it (`.ice-bubble` — `display: none`
|
bubbles rising over it (`.ice-bubble` — `display: none`
|
||||||
outside aero so they cost nothing elsewhere, still under reduced motion). Buttons and segments
|
outside aero so they cost nothing elsewhere, still under reduced motion). Buttons and segments
|
||||||
are **liquid glass** — transparent, glossed, with a bright rim — and every surface shows a
|
are **liquid glass** — transparent, glossed, with a bright rim — and every surface shows a
|
||||||
**pre-blurred copy of the wallpaper** (`public/aero/background-blur.webp`, 3 kB, glued to the
|
**pre-blurred copy of the wallpaper** (`public/aero/background-blur.webp`, 3 kB, glued to the
|
||||||
viewport with `background-attachment: fixed`) instead of blurring the live scene: the bubbles
|
viewport with `background-attachment: fixed`) instead of blurring the live scene: the bubbles
|
||||||
move behind every surface, so a real `backdrop-filter` re-runs ~60 times a second (measured
|
move behind every surface, so a real `backdrop-filter` re-runs ~60 times a second (measured
|
||||||
66ms/frame at 2x throttle vs 17ms with nothing blurring). The tint and the gloss ride on top of
|
66ms/frame at 2x throttle vs 17ms with nothing blurring). A thin veil and the gloss ride on top of
|
||||||
that layer, so the look is identical at zero per-frame cost. The helper lines that sit straight
|
that layer, so the look is identical at zero per-frame cost. The helper lines that sit straight
|
||||||
on the wallpaper (the keyboard tip, the AI note) get a small glass chip — dark text on grass
|
on the wallpaper (the keyboard tip, the AI note) get a small glass chip — dark text on grass
|
||||||
measured 2.5:1. The light/dark
|
measured 2.5:1. The light/dark
|
||||||
|
|
|
||||||
116
app/globals.css
116
app/globals.css
|
|
@ -287,17 +287,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The Aero gloss for the floating surfaces (sheets, dropdowns and the sticky
|
/* The Aero gloss for the floating surfaces (sheets, dropdowns and the sticky
|
||||||
chrome keep a real backdrop-filter — they sit over scrolling content). */
|
chrome keep a real backdrop-filter — they sit over scrolling content).
|
||||||
|
Thin like the card glass: the old 75% top read as a white lid. */
|
||||||
.aero .glass-panel,
|
.aero .glass-panel,
|
||||||
.aero .study-chrome,
|
.aero .study-chrome,
|
||||||
.aero .tabbar,
|
.aero .tabbar,
|
||||||
.aero .glass {
|
.aero .glass {
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgb(255 255 255 / 0.75) 0%,
|
rgb(255 255 255 / 0.5) 0%,
|
||||||
rgb(255 255 255 / 0.3) 46%,
|
rgb(255 255 255 / 0.18) 46%,
|
||||||
rgb(255 255 255 / 0.06) 47%,
|
rgb(255 255 255 / 0.05) 47%,
|
||||||
rgb(255 255 255 / 0.24) 100%
|
rgb(255 255 255 / 0.14) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,15 +365,6 @@
|
||||||
sharp wallpaper with a bright rim — no pre-blurred layer, so it reads as
|
sharp wallpaper with a bright rim — no pre-blurred layer, so it reads as
|
||||||
glass rather than as a white chip. Rail rows, the active nav item and the
|
glass rather than as a white chip. Rail rows, the active nav item and the
|
||||||
active segment all match it. */
|
active segment all match it. */
|
||||||
.aero .nav-action,
|
|
||||||
.aero .nav-row[aria-current="page"],
|
|
||||||
.aero .nav-row[data-active="true"],
|
|
||||||
.aero .segmented > button[aria-pressed="true"],
|
|
||||||
.aero .segmented > a[aria-current="page"] {
|
|
||||||
background-image: none;
|
|
||||||
background-color: rgb(255 255 255 / 0.34);
|
|
||||||
border: 1px solid rgb(255 255 255 / 0.9);
|
|
||||||
}
|
|
||||||
.aero .segmented > button,
|
.aero .segmented > button,
|
||||||
.aero .segmented > a {
|
.aero .segmented > a {
|
||||||
border-color: rgb(255 255 255 / 0.5);
|
border-color: rgb(255 255 255 / 0.5);
|
||||||
|
|
@ -387,20 +379,21 @@
|
||||||
border: 1px solid rgb(255 255 255 / 0.6);
|
border: 1px solid rgb(255 255 255 / 0.6);
|
||||||
color: #04223c;
|
color: #04223c;
|
||||||
box-shadow: 0 12px 26px -16px rgb(2 62 110 / 0.55), inset 0 1px 0 rgb(255 255 255 / 0.9);
|
box-shadow: 0 12px 26px -16px rgb(2 62 110 / 0.55), inset 0 1px 0 rgb(255 255 255 / 0.9);
|
||||||
/* The glass itself comes from the pre-blurred wallpaper rule above. */
|
/* The glass itself comes from the frosted recipes below. */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Liquid glass: far more transparent than the other themes, with a strong
|
/* Liquid glass: the surfaces themselves are the frosted wallpaper recipes
|
||||||
backdrop blur doing the legibility work — the wallpaper stays visible
|
below. These tokens are the fills for the layers that keep a real backdrop
|
||||||
through every surface. Contrast is pixel-scanned in all three themes, so
|
blur (sheets, dropdowns, the sticky chrome) and for nested chips, so they
|
||||||
the tint cannot drop below what the text needs. */
|
stay thin too. Contrast is pixel-scanned in all three themes, so the tints
|
||||||
|
cannot drop below what the text needs. */
|
||||||
.aero {
|
.aero {
|
||||||
--glass-bg: rgb(228 246 255 / 0.34);
|
--glass-bg: rgb(228 246 255 / 0.3);
|
||||||
--glass-solid-bg: rgb(232 248 255 / 0.46);
|
--glass-solid-bg: rgb(232 248 255 / 0.42);
|
||||||
--glass-sheet-bg: rgb(232 248 255 / 0.8);
|
--glass-sheet-bg: rgb(232 248 255 / 0.74);
|
||||||
--surface-strong: rgb(255 255 255 / 0.34);
|
--surface-strong: rgb(255 255 255 / 0.32);
|
||||||
--surface-sunken: rgb(255 255 255 / 0.26);
|
--surface-sunken: rgb(255 255 255 / 0.24);
|
||||||
--nav-active-bg: rgb(255 255 255 / 0.4);
|
--nav-active-bg: rgb(255 255 255 / 0.38);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Liquid glass without the per-frame cost: every surface shows a pre-blurred
|
/* Liquid glass without the per-frame cost: every surface shows a pre-blurred
|
||||||
|
|
@ -413,18 +406,16 @@
|
||||||
.aero .glass-solid,
|
.aero .glass-solid,
|
||||||
.aero .sidebar-surface,
|
.aero .sidebar-surface,
|
||||||
.aero .gate-card,
|
.aero .gate-card,
|
||||||
.aero .btn-primary,
|
.aero .list-group {
|
||||||
.aero .grad-ice,
|
|
||||||
.aero .hint-line {
|
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgb(255 255 255 / 0.5) 0%,
|
rgb(255 255 255 / 0.3) 0%,
|
||||||
rgb(255 255 255 / 0.2) 47%,
|
rgb(255 255 255 / 0.1) 46%,
|
||||||
rgb(255 255 255 / 0.07) 53%,
|
rgb(255 255 255 / 0.04) 53%,
|
||||||
rgb(255 255 255 / 0.24) 100%
|
rgb(255 255 255 / 0.14) 100%
|
||||||
),
|
),
|
||||||
linear-gradient(rgb(230 247 255 / 0.42), rgb(230 247 255 / 0.42)),
|
linear-gradient(rgb(255 255 255 / 0.14), rgb(255 255 255 / 0.14)),
|
||||||
url("/aero/background-blur.webp");
|
url("/aero/background-blur.webp");
|
||||||
background-size: 100% 100%, 100% 100%, cover;
|
background-size: 100% 100%, 100% 100%, cover;
|
||||||
background-position: 0 0, 0 0, center 42%;
|
background-position: 0 0, 0 0, center 42%;
|
||||||
|
|
@ -432,6 +423,65 @@
|
||||||
background-repeat: no-repeat, no-repeat, no-repeat;
|
background-repeat: no-repeat, no-repeat, no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The small chrome — rail rows, the active nav item, chips, segments and the
|
||||||
|
buttons — gets the same frosted glass with a touch more veil, so its small
|
||||||
|
text pops over the busy wallpaper. */
|
||||||
|
.aero .nav-action,
|
||||||
|
.aero .nav-row[aria-current="page"],
|
||||||
|
.aero .nav-row[data-active="true"],
|
||||||
|
.aero .glass-chip,
|
||||||
|
.aero .btn-secondary,
|
||||||
|
.aero .btn-primary,
|
||||||
|
.aero .grad-ice,
|
||||||
|
.aero .segmented,
|
||||||
|
.aero .segmented > button[aria-pressed="true"],
|
||||||
|
.aero .segmented > a[aria-current="page"] {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgb(255 255 255 / 0.42) 0%,
|
||||||
|
rgb(255 255 255 / 0.16) 46%,
|
||||||
|
rgb(255 255 255 / 0.08) 53%,
|
||||||
|
rgb(255 255 255 / 0.2) 100%
|
||||||
|
),
|
||||||
|
linear-gradient(rgb(255 255 255 / 0.12), rgb(255 255 255 / 0.12)),
|
||||||
|
url("/aero/background-blur.webp");
|
||||||
|
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 phone tab bar carries its labels right at the bottom of the water, so
|
||||||
|
it keeps a little more veil than the cards. */
|
||||||
|
.aero .tabbar {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgb(255 255 255 / 0.48), rgb(255 255 255 / 0.48)),
|
||||||
|
url("/aero/background-blur.webp");
|
||||||
|
background-size: 100% 100%, cover;
|
||||||
|
background-position: 0 0, center 42%;
|
||||||
|
background-attachment: scroll, fixed;
|
||||||
|
background-repeat: no-repeat, no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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
|
||||||
|
dark water under the settings theme labels: 1.5:1). Nested chrome falls
|
||||||
|
back to the flat fills there; the sheet itself is genuinely blurred. */
|
||||||
|
.aero .glass-sheet .card,
|
||||||
|
.aero .glass-sheet .glass-solid,
|
||||||
|
.aero .glass-sheet .list-group,
|
||||||
|
.aero .glass-sheet .glass-chip,
|
||||||
|
.aero .glass-sheet .segmented,
|
||||||
|
.aero .glass-sheet .segmented > button[aria-pressed="true"],
|
||||||
|
.aero .glass-sheet .segmented > a[aria-current="page"],
|
||||||
|
.aero .glass-sheet .btn-secondary,
|
||||||
|
.aero .glass-sheet .btn-primary,
|
||||||
|
.aero .glass-sheet .grad-ice {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,12 @@ export const viewport: Viewport = {
|
||||||
* Applies the saved theme before paint to avoid a flash, and keeps the
|
* Applies the saved theme before paint to avoid a flash, and keeps the
|
||||||
* `theme-color` meta in step with the *app* theme (the SSR metas follow the
|
* `theme-color` meta in step with the *app* theme (the SSR metas follow the
|
||||||
* OS preference, which is wrong once the user has picked a theme here).
|
* OS preference, which is wrong once the user has picked a theme here).
|
||||||
|
* The stored theme decides alone: aero is not dark, and the old ternary added
|
||||||
|
* `dark` alongside `aero` whenever the OS preferred dark — the dark palette
|
||||||
|
* on the bright wallpaper is what made the first load look washed out until
|
||||||
|
* a theme toggle re-applied the classes.
|
||||||
*/
|
*/
|
||||||
const themeScript = `(function(){try{var DEV=${devInstanceJs};var s=localStorage.getItem('freezmo.theme');var a=s==='aero';var d=s&&!a?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark');if(a)document.documentElement.classList.add('aero');var l=localStorage.getItem('freezmo.locale.v1');if(l==='cs'||l==='en')document.documentElement.lang=l;var m=document.querySelector('meta[name="theme-color"][data-app]');if(!m){m=document.createElement('meta');m.setAttribute('name','theme-color');m.setAttribute('data-app','1');document.head.insertBefore(m,document.head.firstChild);}m.setAttribute('content',DEV?'#b45309':(a?'#2fa7e6':(d?'#0e1424':'#f4f7fd')));}catch(e){}})();`;
|
const themeScript = `(function(){try{var DEV=${devInstanceJs};var s=localStorage.getItem('freezmo.theme');var a=s==='aero';var d=a?false:(s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',!!d);if(a)document.documentElement.classList.add('aero');var l=localStorage.getItem('freezmo.locale.v1');if(l==='cs'||l==='en')document.documentElement.lang=l;var m=document.querySelector('meta[name="theme-color"][data-app]');if(!m){m=document.createElement('meta');m.setAttribute('name','theme-color');m.setAttribute('data-app','1');document.head.insertBefore(m,document.head.firstChild);}m.setAttribute('content',DEV?'#b45309':(a?'#2fa7e6':(d?'#0e1424':'#f4f7fd')));}catch(e){}})();`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Captures Chrome's `beforeinstallprompt` (it can fire before React hydrates)
|
* Captures Chrome's `beforeinstallprompt` (it can fire before React hydrates)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "freezmo",
|
"name": "freezmo",
|
||||||
"version": "0.2.9",
|
"version": "0.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 201 KiB |
|
|
@ -9,7 +9,7 @@
|
||||||
*
|
*
|
||||||
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
|
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
|
||||||
*/
|
*/
|
||||||
const CACHE_VERSION = "freezmo-v61";
|
const CACHE_VERSION = "freezmo-v62";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,8 @@ export async function seed(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makePage(browser, { width = 390, height = 844, deviceScaleFactor = 2, reducedMotion } = {}) {
|
export async function makePage(browser, { width = 390, height = 844, deviceScaleFactor = 2, reducedMotion, colorScheme } = {}) {
|
||||||
const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor, reducedMotion });
|
const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor, reducedMotion, colorScheme });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
return { context, page };
|
return { context, page };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,22 @@ const AXE = axeSource();
|
||||||
await context.close();
|
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" });
|
||||||
|
await seed(page, { decks: [DECK], theme: "aero" });
|
||||||
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
const state = await page.evaluate(() => ({
|
||||||
|
aero: document.documentElement.classList.contains("aero"),
|
||||||
|
dark: document.documentElement.classList.contains("dark"),
|
||||||
|
bodyColor: getComputedStyle(document.body).color,
|
||||||
|
}));
|
||||||
|
check("theme: aero on a dark OS keeps the dark class off", state.aero && !state.dark);
|
||||||
|
check("theme: boot paints the aero body text", state.bodyColor === "rgb(5, 32, 58)", state.bodyColor);
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
|
||||||
async function audit(label, { width = 390, locale, theme, openSettingsNow }) {
|
async function audit(label, { width = 390, locale, theme, openSettingsNow }) {
|
||||||
const { context, page } = await makePage(browser, { width, height: width >= 1280 ? 900 : 800, deviceScaleFactor: width >= 1280 ? 1 : 2 });
|
const { context, page } = await makePage(browser, { width, height: width >= 1280 ? 900 : 800, deviceScaleFactor: width >= 1280 ? 1 : 2 });
|
||||||
await seed(page, { locale, theme, decks: [DECK] });
|
await seed(page, { locale, theme, decks: [DECK] });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue