Files
MailUI4Agents/client/electron/test/manual
JianFeeeee 84c1d749cd feat(webui): 自定义背景 + 外观现代化;修正实心按钮白字在深色下的对比度
# 自定义背景(新功能)

三选一:不设 / 预设渐变 / 自定义图片,另加压暗与模糊两条滑杆。

**预设的色值全部复用现有调色板变量**,因此自动随主题变化 —— 那一组
(50–300)在深色下本来就是暗的(见 .dark 与 theme.test.mjs 第 19 条),
于是浅色得到柔和 pastel、深色得到低沉暗调,不需要维护两套渐变,也不会
出现「深色模式下原样落下浅色渐变」这类绕过主题变量的错误。

图片路径的关键取舍:
- **先压缩再存**。手机直出照片 4–8MB,而 localStorage 配额约 5MB,直接写会抛
  异常,用户看到的是「选了图片没反应」。等比缩到最长边 2560px、转 JPEG;
  仍超限则再缩一档;再不行就**明确拒绝并说明原因**(不是静默失败)。
- 失败一律返回 `{ok:false, reason}` 并渲染成 `role="alert"`。

# 背景层为什么不放进主题 store

主题(light/dark/system)是必须全局一致的语义;背景是纯装饰偏好,取值空间
与主题毫无关系。混在一起会让「跟随系统」的实现被背景字段淹没。

# 背景层实现在 CSS,不改 27 个组件

按 Tailwind 生成的实际类名统一接管:背景开启时让出不透明的页面底
(body / bg-gray-50 / bg-slate-100 → 透明),并把卡片(bg-white)与框架
(bg-chrome-800/900)变成半透明 + 背景模糊。

逐个组件加 class 必然漏 —— 漏掉的那块就是一张不透明卡片浮在背景上。
这段 CSS **刻意放在所有 @layer 之外**:它要覆盖的正是 utilities 生成的
`.bg-white`,写进 @layer components 会被 utilities 压过去(静默失效),
而分层 CSS 恒输给未分层 CSS,这是唯一稳定可靠的位置。

**chrome-600/700 刻意保持不透明**:它们不是大面板,而是导航项与 15px 的
计数徽标。真实渲染量得半透明会把徽标上的数字压到 4.46:1,低于 AA 4.5 ——
小控件的可读性优先于装饰效果(已用脚本量出,见下)。

# 「跟随系统」的可见性

三态本来就已实现(system 为默认值 + matchMedia 监听)。这次做的是让它可被
发现与信任:选择器改成分段控件(role=radiogroup + aria-checked),说明文案
写清「跟随系统会随系统的深色开关自动切换」,并保留单选按钮入口的
「当前跟随系统:深色/浅色」提示。

# 外观现代化

- **圆角整体调大一档**(默认 0.25→0.5rem)。原值是几年前的紧凑风格,
  在宽屏桌面应用上偏硬。只改比例尺,200 处圆角一次性刷新,不产生
  「新组件大圆角、旧组件小圆角」的断层。
- 语义化圆角令牌:`rounded-card` / `rounded-control`(数值档位答的是「多大」,
  这两个名字答的是「用在哪」)。
- 自定义滚动条(桌面应用里常驻可见,系统默认样式偏旧)。
- 键盘焦点环(`:focus-visible`,仅键盘导航时出现;可访问性硬要求)。
- 交互元素统一过渡;并尊重 `prefers-reduced-motion`。

# 顺带修正两处真实问题(都由真实渲染量出,不是估算)

1. **实心按钮白字在深色下 4.46:1,低于 AA**。
   深色 `--c-on-accent` 是「近白」244 246 250(为了不刺眼),而结构检查第 23
   条只拿**浅色**的纯白 255 去算 → 4.83 通过。**测试存在盲区**:
   同一个实心底,白字换暗一点点就越过 AA 线。导航未读徽标「12」正是这个组合。

   两处都修:把第 23 条改成**两种模式的 on-accent 都算**(闭合盲区),
   并把深色 on-accent 抬到 250 250 252(4.65:1,仍非纯白,保留原初衷)。

2. **theme.test.mjs 切颜色块的方式很脆**:它用 `indexOf('.dark')` 切片,于是在
   :root 的注释里写一句带点的选择器写法就会把浅色块提前截断(我加注释时
   真的踩到了,第 8 条假失败)。更危险的是反向情形:块被截短后变量集合变小,
   「覆盖齐全」这类断言可能**真空通过**。改为所有块切分都基于**剥注释后**的文本。

# 测试

- 新增 `test/background.test.mjs`(15 条结构检查):遮罩两主题各一份、
  背景层必须负 z-index(0 会盖住界面)、背景开启时必须让出页面底、
  玻璃化只在 data-bg=on 下、悬停态一并接管、预设复用调色板变量、
  图片上限与失败原因存在、尊重 reduced-motion 等。
- 新增 `test/stores/background.test.ts`(16 条):脏数据归一化(未知预设、
  kind=image 却无图、越界数值)、CSS 变量写入与清理成对(残留 --bg-image 会
  让「关掉背景」后仍显示旧图)、localStorage 抛异常不打断操作。
- 新增 `test/manual/background-verify.mjs`:连真实 Chromium 验收**渲染结果**
  (背景层是否真的可见、玻璃化的计算样式、正文在背景之上是否仍达 WCAG AA、
  自动模式在**不刷新**页面时跟随系统切换、显式选择不被系统覆盖)。
  它拦住了上面两个真问题,也拦住了我自己两次写错的判据。

# 验证

- typecheck 干净
- 主题 30/30、背景 15/15、vitest 216/216(新增 16)
- 真实渲染验收 23/23(AGENTMAIL_DIST 注入本地构建 + 活 Gateway,未部署即验收)
- 截图对照:浅色/深色 + 极光背景,面板玻璃化与层次均符合预期
2026-09-12 08:02:30 +08:00
..

手工浏览器实测脚本

不进 npm test —— 它们需要一个跑着的 Chromium 与一个活的 Gateway。 日常回归靠 ../narrow-layout.test.mjs(读源码验形态,无外部依赖)。

为什么两套都要

结构性断言守住「代码写成了什么形态」,量不出「按钮实际多大、点下去命中谁」。

窄屏那轮修复里最严重的一个 bug 是抽屉式侧栏(fixed ... z-50 铺满视口高度) 把底部导航最左那一项盖住 —— 按钮在那里、尺寸也够、md:hidden 之类的规则也 没写错,只有 elementFromPoint 才能发现它命中的是抽屉里的 SVG

用法

# 窄屏390pxiPhone 14 Pro+ 320pxiPhone SE
ADMIN_PW=<密码> npm run test:narrow

# 宽屏回归:窄屏修复不能把桌面改坏
ADMIN_PW=<密码> npm run test:wide

环境变量:

变量 默认 说明
ADMIN_PW 无(必填) 管理员密码
ADMIN_USER admin 登录用户名
AGENTMAIL_URL https://mail.jianfgit.xyz 目标地址
CDP_URL http://127.0.0.1:9222 浏览器 CDP 端点
PLAYWRIGHT /usr/lib/node_modules/playwright/index.mjs playwright 入口

浏览器用的是本机 systemd 托管的共享 Chromiumhomeagent-browser.service 通过 CDP 连上去开自己的标签页,用完关掉。没有它时先 systemctl start homeagent-browser

文件

文件 作用
narrow-probe-helper.mjs 连浏览器、登录、量盒子/溢出/命中区/命中测试
narrow-verify.mjs 窄屏 13 项验收
wide-regression.mjs 宽屏 5 项回归
inbox-group-verify.mjs 收件箱按会话分组
theme-verify.mjs 深浅两色的 WCAG 对比度
accent-verify.mjs 强调色(红/绿/橙/黄/蓝17 组配色,两模式各一遍

narrow-probe-helper.mjs 里两个函数值得单独知道:

  • tapTargets(page, labels) —— 量 .tap 按钮的真实命中区(::after 伪元素的尺寸)。.tap 刻意不改变视觉尺寸,所以只看 boundingBox 会误判成偏小
  • hitTest(page, selector) —— 每个元素点下去是否命中自己。遮挡类 bug 只能这样查

accent-verify.mjs 存在的理由是一次真实事故:tailwind.config.jscolors 里同时写了固定 hex 与 accent() 两份 red/green/amber/orange/yellowJS 对象 字面量重复键后者胜出(不报错),而 index.css 当时没有对应的 --c-red-* 变量。rgb(var(--c-red-600) / 1) 里变量未定义 → 整条 background-color 声明 失效 → bg-red-600 退回透明、text-white 的白字落在白卡片上: 按钮看不见但点得动。所有静态检查都过,只有肉眼能发现。

因此这个脚本量的是实际计算值:它把类名注入真页面、读 getComputedStyle 把「背景透明」单独判为失败(那正是上述 bug 的指纹),再算 WCAG 对比度。

只以 hover: 变体出现的档(bg-red-700 / bg-blue-700不能放进探针: Tailwind 不生成未被使用的基础类,探它必然得到透明背景 —— 那是假阳性。 它们由 ../theme.test.mjs 的档位断言覆盖。

已知限制

headless Chromium 报告 hover: none,因此 .reveal(只在支持悬停的设备上隐藏) 在这里永远是可见的 —— 脚本只能验「触摸设备上可见」这一半, 「鼠标设备上隐藏」那一半靠 ../narrow-layout.test.mjs 检查 CSS 规则存在。

没有像素级视觉比对:字体差异下极脆,维护成本高于收益。