Agent controls the whole look: validated theme spec, presets, layout, copy, chat, scoped CSS, protected fee/banner #4

Merged
leora merged 6 commits from feat/agent-theming into main 2026-10-08 19:41:37 +08:00
4 changed files with 16 additions and 5 deletions
Showing only changes of commit 9853c39ef0 - Show all commits

View File

@ -62,7 +62,13 @@ body.chat-left .cmpbar{left:auto;right:22px}
#toasts{position:fixed;left:50%;top:50px;transform:translateX(-50%);display:flex;flex-direction:column;gap:6px;z-index:9}
.toast{background:var(--card);color:var(--ink);border:1px solid var(--line);padding:8px 14px;border-radius:8px}.toast.ok{border-color:var(--ok)}.toast.warn{border-color:var(--warn)}
.cmpbar{position:fixed;left:22px;bottom:22px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:8px 12px;display:none;gap:8px;align-items:center}
body.no-anim *,body.no-anim *::before,body.no-anim *::after{animation:none !important;transition:none !important}
body.scrim .hero-sec,body.scrim .facts,body.scrim .foot,body.scrim .top{background:var(--scrim);border-radius:var(--radius);padding:calc(10px * var(--space)) 16px}
body.scrim .facts{padding-left:34px}body.scrim .top{border-radius:0}
body.no-anim *,body.scrim .hero-sec,body.scrim .facts,body.scrim .foot,body.scrim .top{background:var(--scrim);border-radius:var(--radius);padding:calc(10px * var(--space)) 16px}
body.scrim .facts{padding-left:34px}body.scrim .top{border-radius:0}
body.no-anim *::before,body.scrim .hero-sec,body.scrim .facts,body.scrim .foot,body.scrim .top{background:var(--scrim);border-radius:var(--radius);padding:calc(10px * var(--space)) 16px}
body.scrim .facts{padding-left:34px}body.scrim .top{border-radius:0}
body.no-anim *::after{animation:none !important;transition:none !important}
@media(max-width:640px){.hero-sec h1{font-size:1.9em}.form{grid-template-columns:1fr}.top{padding:12px}main{padding:16px}}
}

View File

@ -104,8 +104,12 @@
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);
let pageInk = minC(pal.text) >= 4.5 ? pal.text : (minC("#111111") >= minC("#ffffff") ? "#111111" : "#ffffff");
// busy gradients (light and dark stops) can't be fixed by text colour alone: put a readability scrim behind page text
const scrim = minC(pageInk) < 4.5;
body.classList.toggle("scrim", scrim);
if (scrim) { set("--scrim", solid(pal.surface)); pageInk = bestInk(solid(pal.surface)); }
set("--page-ink", pageInk); set("--page-mute", !scrim && 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

View File

@ -55,7 +55,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
check("test payment confirms, receipt shows fee", rfee === money(feeC), rfee);
// cash meetup: first card offering cash
await ask("show me everything that I can pay for in cash");
await ask("show me everything");
const cashId = await page.evaluate(() => { const c = [...document.querySelectorAll("#stage .card")].find((c) => c.querySelector("button.cash")); return c && c.dataset.listing; });
await page.click(`.card[data-listing="${cashId}"] button.cash`);
await page.waitForSelector("[data-component=cash-meetup]", { timeout: 15000 });

View File

@ -69,7 +69,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
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))));
const eff = (n) => { while (n && n !== document.body) { const c = getComputedStyle(n).backgroundColor; if (c && !/rgba\(0, 0, 0, 0\)|transparent/.test(c)) return [c]; n = n.parentElement; } return stops; };
return ["headline", "subhead"].map((id) => { const n = document.getElementById(id); return Math.min(...eff(n).map((c) => window.__amContrast(getComputedStyle(n).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");