From f91dd9df432acf28c32344cbcbcf0a67d79ebc0e Mon Sep 17 00:00:00 2001 From: leora Date: Thu, 8 Oct 2026 19:39:41 +0800 Subject: [PATCH] Keep page-level text readable on gradients/patterns; test fixes --- static/styles.css | 10 +++++----- static/theme.js | 7 +++++++ tests/browser_e2e.js | 1 + tests/theme_check.js | 11 ++++++++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/static/styles.css b/static/styles.css index aa53bfe..c279edd 100644 --- a/static/styles.css +++ b/static/styles.css @@ -6,19 +6,19 @@ :root{--bg:#0f1115;--page-bg:#0f1115;--panel:#171a21;--card:#1d212a;--ink:#e9ecf1;--mute:#9aa3b2;--acc:#7c9cff;--acc-ink:#0b0e16;--line:#2a2f3a;--ok:#38c172;--warn:#f6ad55; --font:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;--hfont:var(--font);--scale:1;--radius:14px;--space:1;--cols:auto-fill; --grid-cols:repeat(auto-fill,minmax(220px,1fr));--chat-bg:var(--panel);--chat-ink:var(--ink);--chat-acc:var(--acc);--chat-acc-ink:var(--acc-ink);--chat-w:370px;--chat-h:520px; - --p-bg:#171a21;--p-ink:#ffffff} + --p-bg:#171a21;--p-ink:#ffffff;--page-ink:var(--ink);--page-mute:var(--mute)} *{box-sizing:border-box} body{margin:0;font-family:var(--font);font-size:calc(15px * var(--scale));line-height:1.5;background:var(--page-bg);background-attachment:fixed;color:var(--ink);min-height:100vh} h1,h2,h3{font-family:var(--hfont)} a{color:var(--acc)}button{font:inherit;cursor:pointer} .top{display:flex;justify-content:space-between;align-items:center;padding:calc(14px * var(--space)) 28px;border-bottom:1px solid var(--line)} -.brand{font-weight:700;font-size:1.2em}.logo{color:var(--acc)} +.top,.top .link{color:var(--page-ink)}.brand{font-weight:700;font-size:1.2em}.logo{color:var(--acc)} nav{display:flex;gap:14px;align-items:center}.link{background:none;border:0;color:var(--ink);text-decoration:none;padding:4px} main{max-width:1100px;margin:0 auto;padding:calc(28px * var(--space)) 28px;display:flex;flex-direction:column;gap:calc(14px * var(--space))} -.hero-sec h1{font-size:2.65em;line-height:1.1;margin:calc(20px * var(--space)) 0 10px}.hero-sec p{color:var(--mute);max-width:660px;margin:0} +.hero-sec h1{font-size:2.65em;line-height:1.1;margin:calc(20px * var(--space)) 0 10px;color:var(--page-ink)}.hero-sec p{color:var(--page-mute);max-width:660px;margin:0} .chips{display:flex;flex-wrap:wrap;gap:10px}.chip{background:var(--card);border:1px solid var(--line);color:var(--ink);padding:8px 14px;border-radius:999px} -.chip:hover{border-color:var(--acc)}.facts{color:var(--mute);padding-left:18px;margin:0} -.foot{color:var(--mute);font-size:.85em;border-top:1px solid var(--line);padding-top:calc(14px * var(--space));margin-top:calc(20px * var(--space))} +.chip:hover{border-color:var(--acc)}.facts{color:var(--page-mute);padding-left:18px;margin:0} +.foot{color:var(--page-mute);font-size:.85em;border-top:1px solid var(--line);padding-top:calc(14px * var(--space));margin-top:calc(20px * var(--space))} .is-hidden{display:none !important} #stage{display:flex;flex-direction:column;gap:calc(18px * var(--space))} #stage:empty{display:none} diff --git a/static/theme.js b/static/theme.js index e7203c4..8ae9f01 100644 --- a/static/theme.js +++ b/static/theme.js @@ -99,6 +99,13 @@ set("--space", String({ compact: 0.65, normal: 1, comfy: 1.4 }[t.density] || 1)); const cols = Math.min(6, Math.max(0, parseInt(t.columns) || 0)); set("--grid-cols", cols ? `repeat(${cols}, minmax(0, 1fr))` : "repeat(auto-fill, minmax(220px, 1fr))"); + // text that sits directly on the page background (header, hero, bullets, footer) must stay readable on + // gradients/patterns/images too: keep the theme colour only if it clears 4.5:1 against every background stop + const stops = (isObj(t.background) && Array.isArray(t.background.colors) ? t.background.colors.filter(okColor) : []); + const bgStops = stops.length ? stops.map(solid) : [solid(pal.bg)]; + const minC = (ink) => Math.min(...bgStops.map((b) => contrast(ink, b))); + const pageInk = minC(pal.text) >= 4.5 ? pal.text : (minC("#111111") >= minC("#ffffff") ? "#111111" : "#ffffff"); + set("--page-ink", pageInk); set("--page-mute", minC(pal.muted) >= 4.5 ? pal.muted : pageInk); // protected panels: solid surface colour + whichever of black/white reads best (always >= 4.5:1) const pbg = solid(pal.surface); set("--p-bg", pbg); set("--p-ink", bestInk(pbg)); // chat popup diff --git a/tests/browser_e2e.js b/tests/browser_e2e.js index fc5c0a6..6c22591 100644 --- a/tests/browser_e2e.js +++ b/tests/browser_e2e.js @@ -70,6 +70,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); await page.click("nav [data-event=sell]"); await page.waitForSelector("[data-component=listing-form]", { timeout: 15000 }); await page.type("[name=title]", "Test-only bookshelf"); + await page.$eval("[name=price]", (n) => (n.value = "")); await page.type("[name=price]", "30"); await page.type("[name=description]", "Fake listing created by the browser test"); await page.click("[data-component=listing-form] button[type=submit]"); diff --git a/tests/theme_check.js b/tests/theme_check.js index eb972fb..e2db54b 100644 --- a/tests/theme_check.js +++ b/tests/theme_check.js @@ -65,8 +65,13 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); await ask("make the background a sunset gradient"); const bg1 = await cs("body", "backgroundImage"); check("sunset gradient background", /gradient/.test(bg1), bg1.slice(0, 90)); - if (SHOTS) await page.screenshot({ path: SHOTS + "/6-theme-sunset-gradient.png" }); + if (SHOTS) { await page.evaluate(() => window.scrollTo(0, 0)); await sleep(3800); await page.screenshot({ path: SHOTS + "/6-theme-sunset-gradient.png" }); } await guardrails("sunset gradient"); + const heroC = await page.evaluate(() => { + const t = JSON.parse(localStorage.getItem("am_theme_v1") || "{}"); const stops = ((t.background || {}).colors || []); + return ["headline", "subhead"].map((id) => Math.min(...stops.map((c) => window.__amContrast(getComputedStyle(document.getElementById(id)).color, c)))); + }); + check("hero text readable on the gradient (>= 4.5:1 vs every stop)", heroC.every((c) => c >= 4.5), heroC.map((c) => c.toFixed(1) + ":1").join(", ")); await ask("show me everything"); await ask("switch to light mode with a green accent"); @@ -74,7 +79,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const acc = await page.evaluate(() => { const s = document.createElement("i"); s.style.color = getComputedStyle(document.documentElement).getPropertyValue("--acc"); document.body.append(s); const c = getComputedStyle(s).color; s.remove(); return c; }); const [r, g, b] = acc.match(/\d+/g).map(Number); check("light mode with green accent", t2.mode === "light" && g > r && g > b, `mode=${t2.mode} accent=${acc}`); - if (SHOTS) await page.screenshot({ path: SHOTS + "/7-theme-light-green.png" }); + if (SHOTS) { await page.evaluate(() => window.scrollTo(0, 0)); await sleep(3800); await page.screenshot({ path: SHOTS + "/7-theme-light-green.png" }); } await guardrails("light + green"); await ask("use a serif font and make the text bigger"); @@ -93,7 +98,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); await ask("make it look retro"); const ff2 = await cs("body", "fontFamily"); const t4 = await theme(); check("retro preset applied", /VT323|Press Start|Space Mono|mono|Courier/i.test(ff2) || /retro/i.test(JSON.stringify(t4)), ff2.slice(0, 50)); - if (SHOTS) { await ask("show me everything"); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: SHOTS + "/8-theme-retro.png" }); } + if (SHOTS) { await ask("show me everything"); await page.evaluate(() => window.scrollTo(0, 0)); await sleep(3800); await page.screenshot({ path: SHOTS + "/8-theme-retro.png" }); } await guardrails("retro"); await ask("hide the bullet points");