Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
60 lines
5.3 KiB
JavaScript
60 lines
5.3 KiB
JavaScript
// Button contrast harness (TASK-site-button-contrast-20260928).
|
|
// Renders sample buttons against the real built CSS in headless Chrome, light
|
|
// and dark, and prints text / background / opacity / contrast per sample.
|
|
// Not a repo dependency: uses the machine's global playwright-core + Chrome.
|
|
//
|
|
// Usage:
|
|
// mkdir -p /tmp/btn && cp frontend/.next/static/chunks/*.css /tmp/btn/ # or download staging's CSS:
|
|
// for c in $(curl -s https://staging.jyotisha.chat/login | grep -o '/_next/static/chunks/[^"]*\.css' | sort -u); do curl -s "https://staging.jyotisha.chat$c" -o /tmp/btn/$(basename $c); done
|
|
// node docs/testing/button-contrast-harness.cjs /tmp/btn
|
|
//
|
|
// Only copy a *fresh* build's CSS: a stale .next once hid a live fix during
|
|
// the 2026-09-28 scan. Update `base` / `V` below if button.tsx classes change.
|
|
const { chromium } = require(process.env.PLAYWRIGHT_CORE || "/usr/local/lib/node_modules/playwright-core");
|
|
const fs = require("fs"), path = require("path");
|
|
const dir = process.argv[2];
|
|
const cssFiles = fs.readdirSync(dir).filter(f => f.endsWith(".css"));
|
|
const base = "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[background-color,border-color,color,box-shadow,transform] duration-150 ease-out outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:scale-[0.96] disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 h-11 gap-2 px-4";
|
|
const V = { default: "bg-primary text-primary-foreground hover:bg-primary/80", outline: "border-border bg-background hover:bg-muted hover:text-foreground", ghost: "hover:bg-muted hover:text-foreground" };
|
|
const samples = [
|
|
["Button 默认(实心)", `<button data-slot="button" class="${base} ${V.default}">导出</button>`],
|
|
["Button 默认 · 禁用", `<button data-slot="button" disabled class="${base} ${V.default}">导出</button>`],
|
|
["Button 描边", `<button data-slot="button" class="${base} ${V.outline}">取消</button>`],
|
|
["Button 描边 · 禁用", `<button data-slot="button" disabled class="${base} ${V.outline}">取消</button>`],
|
|
[".button-primary", `<button class="button-primary">保存</button>`],
|
|
[".button-primary · 禁用", `<button class="button-primary" disabled>保存</button>`],
|
|
[".danger-primary", `<button class="button-primary danger-primary">删除</button>`],
|
|
[".danger-primary · 禁用", `<button class="button-primary danger-primary" disabled>删除</button>`],
|
|
[".button-secondary", `<button class="button-secondary">返回</button>`],
|
|
["输入框发送键", `<div class="composer"><div class="composer-row-end"><button>↑</button></div></div>`],
|
|
["输入框发送键 · 禁用", `<div class="composer"><div class="composer-row-end"><button disabled>↑</button></div></div>`],
|
|
["报告页 Button 默认(BUG-1078 后)", `<div class="personal-report-reader"><button data-slot="button" class="${base} ${V.default}">导出选中</button></div>`],
|
|
["报告页 Button 默认 · 禁用", `<div class="personal-report-reader"><button data-slot="button" disabled class="${base} ${V.default}">导出选中</button></div>`],
|
|
];
|
|
function lum([r,g,b]) { const f = c => { c/=255; return c<=0.03928?c/12.92:((c+0.055)/1.055)**2.4; }; return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b); }
|
|
function parse(c) { const m = c.match(/[\d.]+/g).map(Number); return { rgb: m.slice(0,3), a: m.length>3?m[3]:1 }; }
|
|
function over(fg, bg, a) { return fg.map((v,i)=>v*a+bg[i]*(1-a)); }
|
|
function ratio(a,b){ const [x,y]=[lum(a),lum(b)].sort((p,q)=>q-p); return (x+0.05)/(y+0.05); }
|
|
const hex = c => "#" + c.map(v=>Math.round(v).toString(16).padStart(2,"0")).join("");
|
|
(async () => {
|
|
const browser = await chromium.launch({ executablePath: process.env.CHROME || "/usr/bin/google-chrome", args: ["--no-sandbox"] });
|
|
const page = await browser.newPage();
|
|
const rows = [];
|
|
for (const theme of ["light", "dark"]) {
|
|
const html = `<!doctype html><html data-theme="${theme}"><head>${cssFiles.map(f=>`<link rel="stylesheet" href="file://${path.join(dir,f)}">`).join("")}</head><body>${samples.map(([n,h],i)=>`<div id="s${i}" style="padding:8px">${h}</div>`).join("")}</body></html>`;
|
|
fs.writeFileSync(path.join(dir, `h-${theme}.html`), html);
|
|
await page.goto("file://" + path.join(dir, `h-${theme}.html`));
|
|
for (let i=0;i<samples.length;i++) {
|
|
const r = await page.evaluate((i) => { const b = document.querySelector(`#s${i} button`); const s = getComputedStyle(b);
|
|
const bodyBg = getComputedStyle(document.body).backgroundColor; return { color: s.color, bg: s.backgroundColor, op: s.opacity, bodyBg }; }, i);
|
|
const page0 = parse(r.bodyBg).rgb; const bgc = parse(r.bg); const fgc = parse(r.color); const op = Number(r.op);
|
|
const bgSolid = over(bgc.rgb, page0, bgc.a); const fgSolid = over(fgc.rgb, bgSolid, fgc.a);
|
|
const bgFinal = over(bgSolid, page0, op), fgFinal = over(fgSolid, page0, op);
|
|
rows.push({ theme, name: samples[i][0], text: hex(fgFinal), bg: hex(bgFinal), opacity: op, contrast: ratio(fgFinal,bgFinal).toFixed(2) });
|
|
}
|
|
}
|
|
await browser.close();
|
|
console.table(rows);
|
|
fs.writeFileSync(path.join(dir,"contrast.json"), JSON.stringify(rows,null,1));
|
|
})();
|