|
|
5621cf97fa
|
fix(webui): 深色模式「只有通信页正常」—— 根因不是 alpha,是底没暗下来
用户原话:「webui 只有通信页面的深色模式正常了,剩下的三个页面深色模式
可读性都极差」。99a2d7a 只修了 `.glass-card` 那块(通信页走的就是它),
其余三页的面板走 `html[data-bg='on'] .bg-white` → `--bg-glass`,没跟进。
我第一版把这三档 alpha 从 0.9/0.84/0.55 降到 0.08/0.05/0.04,**仍然不够**。
## 正确判据:逐个叶子文本节点,采样它**真实渲染的底色**
之前的探测全在数 DOM 祖先链上的 backgroundColor,而 `.app-backdrop` 是
`position:fixed` 的**兄弟节点**(不是祖先)⇒ 永远采不到壁纸与遮罩,
只能退回"壁纸均值 213"→ 得出"底是亮的"但与屏幕不符。
改成截图后用 pngread.mjs 逐像素采样:对每个叶子文本节点取其 bbox 内
出现最多的颜色当作它的实际底色,再算 WCAG 对比度。得到决定性的数字:
通信 最差 1.87:1 / 日历 最差 **1.19:1** / 联系 1.64:1 / 我的 1.42:1
(日历页「廿五」fg=rgb(138,146,161) bg=rgb(134,132,132) —— 字和底几乎同色)
## 根因:`--bg-dim` 是**比例**,比例压不住一张**浅**壁纸
用户 jianf 的壁纸均值 RGB 213(浅照片)、dim=56 ⇒ 213×0.44 ≈ 94,仍是中灰。
近白正文对 94 只有约 3.6:1;次要文字 gray-400 对 94~134 只有 1.2–1.4:1。
**没有任何文字颜色能救** —— 这与 background.test 的契约「玻璃是白色材料」
是同一件事的两面:深色下敢用近白基材,前提就是「背后是深底」,
而这个前提此前没人保证。
## 改动
1. 新增 `--bg-dim-min`(`:root` 0% / `.dark` 92%),遮罩取
`max(var(--bg-dim), var(--bg-dim-min))` —— **取 max 而非覆盖**,
用户调得比下限高时仍以用户的为准,不下调他的选择。
2. 玻璃 alpha 收到 0.04/0.03/0.02(第三层从 `transparent` 改为
`--bg-glass-nested3` 的小值:深色下"透明"= 浅壁纸直接透上来)。
3. `.dark` 的 `--glass-card-wall-a` 0.1 → 0.04 与其它档对齐。
4. 浅色分支完全不变(dimMin=0% ⇒ max() 等价于原值,实测 glass 仍是 0.88、
遮罩仍是 `rgba(255,255,255,0.56)`)。
## 验收(1280×800 真渲染采样,逐个叶子文本节点)
修复前:通信 1.87 / 日历 1.19 / 联系 1.64 / 我的 1.42(<4.5 的节点 17/88/22/26)
修复后:通信 6.14 / 日历 4.90 / 联系 4.96 / 我的 5.38(<4.5 的节点 **0/0/0/0**)
代价(明写在案):深色下浅壁纸被压得很淡(92%)。这是可读性优先的取舍,
壁纸仍在(8% + 玻璃质感 + 模糊),只是不再是主体。
## 判据(theme.test 30 → 38,已同步 run-all 的棘轮)
新增 4 条,针对"归因错"这件事本身:
· 深色下有压暗下限且 ≥90%(浅色下不干预)
· 下限真的作用在遮罩上(**不是只定义变量**)
· 用最坏输入(纯白 255 壁纸)实算 gray-400 对合成底 ≥ 4.5:1
· 判据自检:拿掉下限必须判红
变异自检跑过:下限改回 56% ⇒ 2 条红;下限定义了但没用上 ⇒ 1 条红。
`--revert-mutation` 之外的基本面:theme 38/0、background 44/0、
cross-client-theme 15/0、appearance-defaults 4/0。套件 broken 由 4 降到 3
(build stamp 因重构建而转绿),red 23 不变,无新增失败。
|
2026-09-18 01:08:04 +08:00 |
|
|
|
99a2d7ad7e
|
fix(webui): 深色模式真的落地了 —— 之前 .dark 是「有意留空」的
用户报「深色模式可读性差」。实测(1280×800,读页面计算值)拿到两个数字:
· .glass-card 合成成 rgb(237,237,237) 白卡,而其上 --c-gray-900 文字是
rgb(243,245,248) ⇒ **1.07:1 的白底白字**
· .nav-rail 合成成 rgb(188,189,190) 浅灰条,未选中文字只有 4.03:1
根因写在 index.css 自己的注释里:`.dark { /* 有意留空 */ }`。当年留空的理由
是「组件没有 dark: 变体,只换令牌会半深不浅」——**方向反了**:组件写的是语义
色阶(bg-white / text-gray-900),灰阶反转后本来就会自适应;真正没适配的是
**手写 CSS 里那几处硬编码白色**(.glass-card 的 0.92、--nav-bg 令牌),
它们不在 --c-* 色板里,所以「色板变量已全覆盖」的判据一直是绿的。
改动:
1. .glass-card / .nav-* 全部改走令牌,主题之间只差 alpha;
深色给 --glass-card-a: 0.06 + --nav-bg: 30 35 44/0.72。
遵守既有契约(background.test「玻璃是白色材料」):**基材恒为白**,
只降 alpha,绝不换成深色层。
2. CalendarView 的非本月农历小字 text-gray-300 → gray-400/500。
gray-300 在这套调色板里是**分隔线档**(全仓 68 处 border-gray-300、
当文字只有 5 处),深色下它是 61,68,81,落在深卡上 1.49:1;
而 gray-400 在深色 4.66:1、浅色白卡 2.54:1,两端都更好。
3. 判据:theme.test 补 3 条(手写 CSS 不许有绕过主题的硬编码白色表面 /
深色卡片 alpha 必须降低 / **按实际 alpha 合成后**正文须 ≥4.5:1,
即把 1.07:1 那个事故写成可计算的断言)。background.test 那条
「.dark 里不许出现 --nav-*」**方向反转**为「必须有且底暗字亮」——
它原来守的是「还没有深色主题」这个前提,其注释本就写明
「真做深色主题时这条要一起改」。
验证(不是"改了就算"):
· 自建探针遍历 **9 个页面**(三个通信子页签 / 邮件详情 / 日历 / 联系人 /
我的 / 写信 / 地址补全弹层):修复前通信 14 处、日历 74 处、
联系人 13 处低于 WCAG AA;修复后 **9/9 页面 0 处**。
· 三条新判据逐条**变异自检**(还原修复即变红,且失败信息自带药方);
浅色三个取色值与改动前**逐字节一致**(导航 0.72 白玻璃、卡片 0.92 白)⇒ 零回归。
· theme.test 34 通过 / background.test 44 通过 / tsc 干净。
· 已按 deploy/redeploy-gateway.sh 部署到 systemd 实例,后置清单自动项全绿,
并在生产实例上复量 9 个页面(同为 0 处)+ 端到端发信(状态 read,非仅入库)。
#深色模式 #可读性 #WCAG
|
2026-09-17 22:32:25 +08:00 |
|
|
|
d5cfcbdc9c
|
fix(权限): 409 的第二种含义是「本档不该问」——四桥都补上;状态写入点不再兜默认档
线上事故(jianf 经 pi 转达):补投路径漏传 permission_mode,插件拿 undefined 兜了
workspace 档,把 full 档会话写成 workspace-write + ask —— 不是"拦一次",是一整轮
工具能力降级,且状态留在会话里;随后该会话每次受守卫调用都撞 409。
四件事:
1. **状态写入点不接受默认值**(新增共享 `modeForStateWrite`):缺字段/脏值 → `null`
= 不写状态。"默认值可以出现在**决策**里,不可以出现在**状态写入**里。"
同时保留共享契约的 fail-closed:真读到 workspace 才写 workspace。
2. **409 的两种含义分开处理**。`allowed-once` 只绕过**审批**,改不了**沙箱** ——
所以 dsh 桥在放行前先把服务端给的权威档位**写回会话**(这也就成了自愈路径:
已经降级的会话,下一次带档位的 409 会把它修回来);只认服务端明说的 full,
plan 与"链上没有人类"照旧 fail closed。
3. **同一处缺陷在 zcode / opencode 也在**(`hooks/permission.mjs` 与 `index.js`
都把 409 当永久失败拒绝)。我先前在回信里写过"这两个桥不转发权限询问,不需要改"
—— 那句话是错的,我当时的搜索面只有 `<plugin>/src/*.mjs`。按 pi 的要求把这条
**否定性事实变成常驻判据**后,它第一次运行就红给我看。四桥现在都有
「409 + full → 放行」,且**排在永久失败分支之前**(含顺序变异自检)。
4. **共用测试重新同源**:`test/catchup.test.mjs` 从 `153985e` 起就是分叉的
(我那版把平台专属路径写进了共用文件),而 `deploy/install.sh` 第 24 行会跑
`check-shared-libs.sh` —— 也就是说**部署一直是红的**,我没跑过那个脚本。
共用文件只放契约(值/行为),跨平台配对judge 移到平台专属文件,四份逐字节相同。
另外把"判代码 vs 判理由"从记忆变成代码:`test/lib/read.mjs` 提供 `code()/prose()/bytes()`,
判据目录里不得再裸用 `readFileSync`(新判据 `criteria-hygiene` 管,含读取器自检)。
判据证据(每条都做过"能不能红"的变异):
- 写回去掉 → 红;纠正块挪到普通 409 之后 → 红;状态写入点退回兜默认 → 红;
- zcode/opencode 的放行分支拿掉 → 各自红;共用测试分叉 → check-shared-libs 红。
各套件:dsh 388、pi 443、zcode 387、opencode 333(均经 npm test,含 tsc);
electron `npm test` 15/15 判据绿 + vitest 266 + typecheck;`check-shared-libs.sh` 退出 0;
Go `go test ./...` 全 ok。
|
2026-09-14 16:21:27 +08:00 |
|
|
|
a8ac2fc28b
|
test(criteria): 闭环——判据自报条数 + 每文件期望条数(只增不减的棘轮)
pi 指出的残余缺口:我上一轮加的自检 4 是**文本证据**(文件里有 `test(` / `check(` /
`process.exit(1)`),只能证明"**有能红的路径**",不能证明"**它跑过**"。反例很短:
```js
const check = () => {}; // 实现被换空(现实形态:合并冲突改坏实现)
check('a', false); // 存在、也执行了,但什么都不会红
console.log('主题:通过'); // 有输出
```
## 落地(pi 给的闭环形状)
1. 自定义 `check()` 的判据结尾打一行机器可读汇总 `RESULT pass=<条数> fail=<失败数>`
(`node:test` 的判据不用改,已有 `# pass N`);
2. `run-all.mjs` **只解析这个固定 marker**(不猜口语汇总——「窄屏布局:全部通过」里没有数字,
按数字猜会误报,这一点我上轮已经实测过);
3. 与清单里登记的**期望条数**比对,**低于 → 红**。
关键细节:**计数写在 `check()` 内部**(theme/background 原本就在内部 ++;
narrow-layout 只有 failed 计数,补了 passed;markdown-xss 按 payload 条数算)。
写在调用点或靠扫源码的话,"实现被换空"就看不见了。
棘轮"只增不减":加判据**不用**改那个数,只有"条数掉了"才红。期望值按**实测**回填
(9/52/8/30/42/15/28/5/23/5/3/2)。
附带的可见性收益:这几轮我一直用"13→14""19→28""34→42"当信号,现在它成了判据 ——
某次改动顺手删掉两条判据、或某条被跳过,会立刻红。
## 变异
- pi 那个反例(`check` 换成空函数)→ 红(`自报 0 条 < 登记的 30 条`);
- 删掉 5 条 `check(` 调用 → 红(`自报 47 条 < 登记的 52 条`)。
## 规范
§6.5 新增"涉及运行时行为的结论必须实测过才能写进规范/判据"——同一个错这轮犯了两次
(我从"报告 0 个测试、退出 0"推断"退出码被吞",实测是照传;pi 拿我这个结论又建了一个洞)。
规则:**一次观察只支撑你看到的那一层**。
§6.6 记闭环形状与代价(故意删判据要同步改数字,属于一次可复核的显式编辑)。
⚠️ 并且如实记下一次**我自己违反规范**的事:写 §3 那条"变异后别用 `git checkout` 还原"的人
(就是我)在这次变异验证里又用了 `git checkout -- <文件>`,把刚加、尚未提交的 marker 抹掉了。
规矩写下来不等于会遵守 —— 已把这条实例写进规范,让人知道它是活人踩的坑。
## 验证
`npm test` 退出码 0(12 个判据文件全绿 + vitest 258/258);`run-all` 单独跑也 exit 0。
|
2026-09-14 15:06:14 +08:00 |
|
|
|
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 |
|
|
|
f9d757b5e5
|
chore: directory migration - gateway→server, web→client/electron
|
2026-09-08 19:16:35 +08:00 |
|