diff --git a/BLOCKED.md b/BLOCKED.md index 4dcfbf7c..a59322cb 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,5 +1,12 @@ # BLOCKED +## TASK-site-button-contrast(BUG-1079):真机、登录态与两条待决发现(2026-09-28) + +- 无受控登录账号与真机:各页实心按钮只在 harness(新 build CSS + 无头 Chrome,样本按源码类名渲染)上测,页面级走查按 `docs/testing/site-button-contrast-20260928.md`。 +- 本机无 Docker:`npm test` 的 24 条 DB / 部署套件失败与开工基线逐条同名(PROGRESS 附 diff)。 +- 待产品决定(未修,不在任务书范围):① 后台管理路由(`src/app/admin/layout.tsx`)只加载 `antd/dist/reset.css` 与 `admin.css`、不加载 `globals.css`,其中 52 个 shadcn ``; const samples = [ - ["Button 默认(实心)", ``], - ["Button 默认 · 禁用", ``], - ["Button 描边", ``], - ["Button 描边 · 禁用", ``], + ["Button 默认(实心)", B("default")], + ["Button 默认 · 禁用", B("default", "default", "", "disabled")], + ["Button 描边", B("outline")], + ["Button 描边 · 禁用", B("outline", "default", "", "disabled")], + ["Button 幽灵", B("ghost")], + ["Button 幽灵 · 禁用", B("ghost", "default", "", "disabled")], [".button-primary", ``], [".button-primary · 禁用", ``], [".danger-primary", ``], [".danger-primary · 禁用", ``], [".button-secondary", ``], - ["输入框发送键", `
`], - ["输入框发送键 · 禁用", `
`], - ["报告页 Button 默认(BUG-1078 后)", `
`], - ["报告页 Button 默认 · 禁用", `
`], + [".button-secondary · 禁用", ``], + ["输入框发送键", `
${B("default", "icon")}
`], + ["输入框发送键 · 禁用", `
${B("default", "icon", "", "disabled")}
`], + ["输入框停止键", `
${B("default", "icon", "composer-stop")}
`], + ["日历选中日", `
${B("ghost", "icon", dayClass, 'data-selected-single="true"')}
`], + ["日期选择器(未选,占位)", B("outline", "default", triggerClass, 'data-empty="true"')], + ["报告页 Button 默认", `
${B("default")}
`], + ["报告页 Button 默认 · 禁用", `
${B("default", "default", "", "disabled")}
`], + ["报告页 描边 · 禁用", `
${B("outline", "default", "", "disabled")}
`], + ["报告页页眉「生成报告」· 禁用", B("outline", "sm", "report-center-generate shrink-0 whitespace-nowrap", "disabled")], + // Hover samples: the third element marks the sample to be hovered first. + ["Button 默认 · 悬停", B("default"), true], + ["Button 描边 · 悬停", B("outline"), true], + [".button-primary · 悬停", ``, true], + [".danger-primary · 悬停", ``, true], + ["输入框发送键 · 悬停", `
${B("default", "icon")}
`, true], + ["日历选中日 · 悬停", `
${B("ghost", "icon", dayClass, 'data-selected-single="true"')}
`, true], ]; -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(""); +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 oklabToRgb(L, a, b) { + const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3; + const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3; + const s = (L - 0.0894841775 * a - 1.2914855480 * b) ** 3; + const lin = [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s]; + return lin.map(v => { v = Math.min(1, Math.max(0, v)); return 255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055); }); +} +function parse(c) { + // Tailwind's `/80` opacity computes to `oklab(L a b / alpha)`. + if (c.startsWith("oklab(")) { + const m = c.slice(6).match(/-?[\d.]+/g).map(Number); + return { rgb: oklabToRgb(m[0], m[1], m[2]), a: m.length > 3 ? m[3] : 1 }; + } + // color-mix() computes to `color(srgb r g b / a)` with 0–1 channels; rgb()/rgba() use 0–255. + const srgb = c.startsWith("color(srgb"); + const m = c.replace(/^color\(srgb/, "").match(/[\d.]+/g).map(Number); + const rgb = m.slice(0, 3).map(v => (srgb ? v * 255 : v)); + return { rgb, 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 = `${cssFiles.map(f=>``).join("")}${samples.map(([n,h],i)=>`
${h}
`).join("")}`; + const html = `${cssFiles.map(f => ``).join("")}${samples.map(([, h], i) => `
${h}
`).join("")}`; fs.writeFileSync(path.join(dir, `h-${theme}.html`), html); await page.goto("file://" + path.join(dir, `h-${theme}.html`)); - for (let i=0;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 container = b.parentElement; const pageBg = getComputedStyle(document.body).backgroundColor; + return { color: s.color, bg: s.backgroundColor, op: s.opacity, pageBg, fontSize: s.fontSize, cursor: s.cursor, border: s.borderTopColor, cBg: getComputedStyle(container).backgroundColor }; }, i); + const page0 = parse(r.pageBg).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) }); + rows.push({ theme, name: samples[i][0], text: hex(fgFinal), bg: hex(bgFinal), opacity: op, contrast: ratio(fgFinal, bgFinal).toFixed(2), fontSize: r.fontSize }); } } await browser.close(); console.table(rows); - fs.writeFileSync(path.join(dir,"contrast.json"), JSON.stringify(rows,null,1)); + fs.writeFileSync(outFile, JSON.stringify(rows, null, 1)); })(); diff --git a/docs/testing/site-button-contrast-20260928.md b/docs/testing/site-button-contrast-20260928.md new file mode 100644 index 00000000..fd7a22c4 --- /dev/null +++ b/docs/testing/site-button-contrast-20260928.md @@ -0,0 +1,21 @@ +# 真机清单 · 全站实心按钮配色与禁用态(BUG-1079,2026-09-28) + +每一项在**浅色**和**深色**各走一遍(手机「设置 → 显示与亮度」切换,或浏览器外观设置)。看两件事:①实心按钮是白字配陶土色、字清楚;②按钮不可点时是灰底灰字,不是半透明的淡红。 + +| # | 页面 / 位置 | 操作 | 期望 | +| --- | --- | --- | --- | +| 1 | 登录页 | 输入邮箱前后看「发送验证码」 | 不可点时灰;可点时白字陶土底 | +| 2 | 首次引导 | 资料没填完 / 填完的「下一步」「保存」 | 同上 | +| 3 | 点数与订阅面板 | 打开购买点数、订阅 | 购买类按钮白字陶土底 | +| 4 | 兑换码付费墙 | 输入兑换码前后 | 空时灰,有码后白字陶土底 | +| 5 | 人物档案 | 「重试」;删除人物的确认按钮 | 重试是陶土底;删除确认仍是深红底白字(浅色)/ 浅红底深字(深色),不变 | +| 6 | 设置 → 星盘资料 | 修改后「保存」 | 白字陶土底;保存中灰 | +| 7 | 星历 | 未登录时「去登录」、出错时「重试」 | 白字陶土底 | +| 8 | 生时校正 | 各卡片的主按钮(提交、继续、再次校正) | 白字陶土底;提交中灰 | +| 9 | 对话输入框 | 空输入时的发送键、输入后发送键、生成中的停止键、排队区 | 空时灰;可发时陶土底白箭头;停止键同色 | +| 10 | 出生日期选择器 | 打开日历选一天;未选时的占位文字 | 选中日白字陶土底;占位字是浅灰(以前是正文黑) | +| 11 | 我的报告 → 分块导出 | 不勾选 / 勾选后「导出选中」 | 与上次一样:未选灰、选后白字陶土底 | +| 12 | 我的报告列表页眉「生成报告」 | 点一次,看「正在创建…」 | 描边按钮变淡(与其它描边按钮一致;上次报告单里是灰底,现已统一) | +| 13 | 悬停(电脑) | 鼠标移到上述实心按钮 | 浅色变更深、深色略提亮,字始终清楚 | + +对比度数值与测量方法:`node docs/testing/button-contrast-harness.cjs `,见脚本头部说明。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 93331679..6501ae04 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -82,7 +82,8 @@ Jyotisha feels like a private reading room: warm, editorial, grounded, and quiet | Secondary text | `--color-ink-secondary` | `#5f5f59` | Supporting copy and labels, section headings | | Tertiary text | `--color-ink-tertiary` | `#6a6963` | Hints, metadata, empty-state copy | | Primary action (text rank) | `--color-action` | `#a9583e` | Links, tinted labels, icons — anything the user reads | -| Primary action (fill rank) | `--color-action-strong` | `#cc785c` | Button and avatar fills, the active session rail, rings, carets | +| Primary action (fill rank) | `--color-action-strong` | `#cc785c` | Avatar fills, the active session rail, rings, carets, checked radios — never a button with a label | +| Solid button fill | `--color-action-fill` / `--color-on-action-fill` | `#a9583e` / `#fbfaf7` (dark `#8f4a33` / `#f2f0ea`) | Every solid primary button: `