设置弹窗的固定高度只用 dvh 写、没有回退。不认识该单位的引擎会把整条 height 与 max-height 作废,盒子退回按内容撑开,于是切分区就跳大小—— 这正是 BUG-554 现象的复发,而 BUG-554 的防复发「必须同时声明 width 与 height」只检查声明存不存在,挡不住「写了但没生效」。 实测(Chrome 151,真实产物 CSS + 复刻 DOM,1440×900):dvh 正常时四个 分区恒定 866.80×640px,**事故不复现**;摘掉 dvh 后变成 313/313/378/1130, 宽度不动——与用户描述的形状完全一致。因此机制已证实,但用户当时的浏览器 未定位,BUG-698 记为 investigating 而非 resolved。 附带发现:任务书要求照抄的重复声明式回退 `height: 100vh; height: 100dvh;` 在本仓根本发布不出去——Lightning CSS 会合并同名属性的重复声明只留最后一条, 全仓唯一那处回退(sidebar-provider)在线上早就是死的,还有一条测试专门守着 这个从未发布过的写法。改用 @supports (height: 1dvh):vh 作基线,dvh 作升级。 修复后不支持 dvh 的引擎也收敛到恒定 640px,支持的逐像素无变化。 同轮按产品决策去掉设置分区菜单的左侧/下方强调色条,选中与悬停改用面与 墨色等级区分,不用色相、不用字重。左侧会话列表的色条本轮不动。 - 新增 viewport-unit-fallback-contract(3 条,全文件),三次破坏性验证各自打红 - account-dialog-overlay 新增同尺寸契约与分区菜单契约 - 三条钉死旧 dvh 字面量的既有断言按「原值/新值/原因」更新,均未弱化 - tsc 0 错;lint 0 error / 118 warning(持平);npm test 3346/3300/fail 31, 失败清单与基线逐字相同;/ 仍 ○ Static;样式 gzip +0.38%; 快速门 pytest 段 792 passed / 1 skipped / 0 failed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
17 KiB
PROGRESS · 设置弹窗尺寸(BUG-698)+ 分区菜单去强调条
- 日期:2026-09-16
- 任务书:
docs/tasks/TASK-settings-dialog-size-and-nav-20260915.md - 分支:
codex/settings-dialog-size-and-nav-20260916 - 工作树:
.worktrees/settings-dialog-size-and-nav-20260916 - 实际基线:
origin/staging@5094fd26(任务书写的6c748d86已被十余个提交超车;三条前提逐条复核仍成立,见 §1) - BUG 编号:
docs/BUG_HISTORY.md当前最大号 BUG-736,BUG-698未被占用,按任务书使用BUG-698,未顺延。
1. 开工前置:前提仍然成立
| 任务书断言 | 5094fd26 实测 |
结论 |
|---|---|---|
accountDialogClasses 四分区全是 "settings-modal" |
home-types.ts:211-217 四项均为 settings-modal |
成立 |
.settings-modal 同时写死 width 与 height |
globals.css:1757,width: min(100vw - 32px, 880px); height: min(84dvh, 640px) |
成立 |
桌面强调条 inset 2px 0 0 var(--color-action) |
globals.css:1762 |
成立 |
移动端强调条 inset 0 -2px 0 var(--color-action) |
globals.css:1948 |
成立 |
| 选中态与悬停态共用一条规则 | globals.css:1761 |
成立 |
| BUG-554 防复发「必须同时声明 width 与 height」 | 仍在,且仍绿 | 成立(本轮不推翻) |
AccountDialogOverlay 四个分区的 DOM 结构完全一致(account-dialog-overlay.tsx:76-94),dialog 与 model.dialogClass 在同一次 render 里都来自 activeAccountDialog(page.tsx:1510-1512、1827-1831),不存在切换瞬间掉类的中间帧。所以外框尺寸只可能由 CSS 决定。
2. 任务 1.1 复现:有浏览器,有数字
会话没有登录态,但本机有 google-chrome 151.0.7922.169。做法是绕开登录、直接量 CSS:把 next build 产出的真实 chunk CSS(.next/static/chunks/*.css)和按 account-dialog-overlay.tsx 逐节点复刻的 DOM 组成离线页面,四个分区各塞 6 / 24 / 3 / 1 段内容模拟真实内容量差,headless Chrome 量 section.account-modal.settings-modal 的 getBoundingClientRect() 与 computed style。窗口 1440×900。
harness:gen.js + supported.html / nodvh.html(scratchpad,未提交)。
2.1 基线 5094fd26,dvh 正常的引擎(Chrome 151)
| 分区 | rect w | rect h | computed height | computed max-height |
|---|---|---|---|---|
| 个人资料 | 866.80 | 630.40 | 640px | 640px |
| 星盘资料 | 866.80 | 630.40 | 640px | 640px |
| 账户与点数 | 866.80 | 630.40 | 640px | 640px |
| 通用设置 | 866.80 | 630.40 | 640px | 640px |
(rect 630.40 < computed 640 是 account-dialog-enter 入场动画的 transform,四个分区同值,不影响结论。)
四个分区宽高完全一致,height 声明没有被划掉,计算值不是 auto。按任务书 §5 任务 1.1 的分支判断,事故在当前浏览器上不复现。
2.2 同一份 CSS,去掉 dvh 支持
把 chunk 里所有 dvh 声明摘掉,模拟不认识该单位的引擎:
| 分区 | rect w | rect h | computed height | computed max-height |
|---|---|---|---|---|
| 个人资料 | 866.80 | 378.58 | 384.344px | none |
| 星盘资料 | 866.80 | 1130.16 | 1147.38px | none |
| 账户与点数 | 866.80 | 313.23 | 318px | none |
| 通用设置 | 866.80 | 313.23 | 318px | none |
宽度纹丝不动(走 vw),高度随内容量在 313 / 378 / 1130 之间跳。 这正是用户原话「大小不一样,选别的就立马缩小了」的形状。任务书 §2.2 推测的机制被证实为唯一能产生该现象的 CSS 路径,但它以「引擎不认识 dvh」为前提。
2.3 结论与诚实边界
dvh的支持面是 Chrome/Edge 108(2022-11)、Safari 15.4(2022-03)、Firefox 101(2022-05)、Android WebView 108。任何当前浏览器都支持,所以产品负责人若在新版 Chrome / Safari 上看到该现象,本机制解释不了。- 仍然可能命中的环境:iOS 15.0–15.3、未升级的 Android System WebView(< 108)、以及部分把旧 WebView 钉死的 App 内置浏览器(微信/QQ 等)。对一个中文消费产品这不是零概率,但本轮无法证明用户就在其中。
- 因此 BUG-698 记为
investigating,不是resolved。机制已确认、已加固,但用户实际环境未定位。
3. 任务书的修复方案不成立(本轮最重要的发现)
任务书 §1.2 让我们照抄 globals.css:637 的写法,补成重复声明:
height: min(84vh, 640px);
height: min(84dvh, 640px);
这个写法在本仓根本到不了浏览器。 先按任务书实现了一版,next build 后 grep 产物:
.settings-modal{...;width:min(100vw - 32px,880px);height:min(84dvh,640px);max-height:min(84dvh,640px);...}
vh 那条不见了。再去查任务书引为范本的那一行:
- 源码
globals.css:637:.group\/sidebar-provider[data-viewport] { height: 100vh; height: 100dvh; ... } - 产物:
sidebar-provider[data-viewport]{height:100dvh;min-height:0;overflow:hidden}
Lightning CSS(Tailwind v4 的压缩器)会合并同一规则内同名属性的重复声明,只保留最后一条。 也就是说:全仓唯一那处「标准写法」的 vh 回退,在线上早就是死的;任务书若原样交付,等于交一个 no-op。
改用特性查询,Lightning CSS 无法证伪条件,整块原样保留:
.settings-modal { width: min(100vw - 32px, 880px); height: min(84vh, 640px); max-height: min(84vh, 640px); ... }
@supports (height: 1dvh) {
.settings-modal { height: min(84dvh, 640px); max-height: min(84dvh, 640px); }
}
产物核对(@supports 完整保留):
@supports (height:1dvh){.standalone-page,.app-loading,.group\/sidebar-provider[data-viewport],.admin-app-shell,.auth-page{height:100dvh}.account-modal{max-height:min(84dvh,760px)}.settings-modal{height:min(84dvh,640px);max-height:min(84dvh,640px)}}
@supports (height:1dvh){[data-sidebar=sidebar]{height:100dvh}.account-modal{min-height:100dvh;max-height:100dvh}.settings-modal{height:100dvh;max-height:100dvh}}
3.1 修复后重测(同 harness,同窗口)
| 引擎 | 四个分区 rect | computed height |
|---|---|---|
| dvh 正常(Chrome 151) | 866.80 × 630.40 ×4,全等 | 640px ×4 |
去掉 @supports 块(模拟不支持 dvh) |
866.80 × 630.40 ×4,全等 | 640px ×4 |
支持 dvh 的浏览器行为与修复前逐像素一致(无回归);不支持 dvh 的引擎从 313/378/1130 的跳变收敛到恒定 640px。
覆盖范围:凡 height(不含 max-height / min-height)用 dvh 的 8 处全部改为 vh 基线 + @supports 升级 —— .standalone-page、.app-loading、.group\/sidebar-provider[data-viewport]、.admin-app-shell、.auth-page、[data-sidebar="sidebar"]、.settings-modal(桌面与移动端)。另把 .account-modal / .settings-modal 的 max-height / min-height 一并纳入,因为 §2.2 实测里 max-height 塌成 none 也是尺寸失控的一环。其余只写 max-height 的(.session-actions-positioner、.account-menu-popup、.auth-shell、.select-content、.select-list)按任务书不动 —— 作废后只是少一个上限,不会让盒子随内容跳变。
.admin-app-shell 在 globals.css 里,属于布局类而非 antd / Refine 组件,改的是同一条 dvh 规则,不触碰任务书 §4.5 的 admin 红线。
3.2 自查抓到一个自己引入的回归(已修)
第一版把全部 7 个选择器的 dvh 升级都放进顶层 @supports。复查 diff 时发现 .auth-page 的高度基线只存在于 @media (max-width: 767px) 内(globals.css:1952),顶层那条 .auth-page { display: grid; place-items: center; padding: … } 本来没有高度。放到顶层等于给桌面登录页新加了一个满视口高度约束 —— 桌面登录页原本是随内容高的。已把 .auth-page 的升级挪进移动端作用域内的 @supports。
新增的契约测试没能抓到这个:它只验「文件里某处存在 vh 基线」,不验基线与升级是否在同一个 at-rule 作用域。这条限制已写进测试文件头部注释,提醒后来者手工对齐作用域。其余 6 个选择器的基线都在顶层,作用域本就一致,逐个复核过。
4. 任务 1.3 防复发:从「写没写」升级到「量不量得到」
新增 frontend/tests/viewport-unit-fallback-contract.test.ts(3 条,全文件范围):
height:用 dvh 的声明必须落在@supports (height: 1dvh)内。- 禁止重复声明式回退 —— 它会被压缩器吃掉,测试里写明了原因。
@supports里升级过的每个选择器,块外必须有 vh 基线(否则不支持的引擎干脆没有高度)。
解析前先剥 CSS 注释:本文件自己的说明注释里引用了 height: 100vh; height: 100dvh; 这个坏写法,不剥会自伤(首次运行就被这条打红,已修)。
破坏性验证(三次,跑完都还原,未提交):
| 破坏 | 预期 | 实测 |
|---|---|---|
.settings-modal 基线换回 dvh |
第 1 条红 | not ok 1,2/3 绿 |
.admin-app-shell 改回 height: 100vh; height: 100dvh; |
第 2 条红 | not ok 1 + not ok 2 |
删掉 .auth-page 的 vh 基线只留 @supports |
第 3 条红 | not ok 3,1/2 绿 |
| 还原 | 全绿 | ok 1 / ok 2 / ok 3 |
account-dialog-overlay.test.ts 新增 2 条:
- 同尺寸契约:
accountDialogClasses四个设置分区映射到同一个类名(直接 import 常量断言,不再只验单个分区的渲染),且logout与之不同。 - 分区菜单契约:
[aria-current="page"]与:hover规则内都不含box-shadow、不含font-weight,且两者必须是各自独立的规则块。破坏性验证:把强调条与合并规则塞回去 →not ok 5,还原 →ok 5。
原有 assert.match(styles, /\.settings-modal \{[^}]*width:[^}]*height:/) 未改动(新 CSS 仍然匹配),只在上方加注释说明它是存在性检查、真正的防线是上述两处。本轮没有修改任何既有断言,所以不涉及「原值 / 新值 / 原因」三栏。
5. 任务 2:分区菜单
改动(globals.css):
/* 前 */
.settings-dialog-nav-item:hover, .settings-dialog-nav-item[aria-current="page"] { background: var(--color-canvas-muted); color: var(--color-ink); }
.settings-dialog-nav-item[aria-current="page"] { box-shadow: inset 2px 0 0 var(--color-action); }
/* 移动端 */ .settings-dialog-nav-item[aria-current="page"] { box-shadow: inset 0 -2px 0 var(--color-action); }
/* 后 */
.settings-dialog-nav-item:hover { background: color-mix(in srgb, var(--color-canvas-muted) 55%, transparent); color: var(--color-ink-secondary); }
.settings-dialog-nav-item[aria-current="page"] { background: var(--color-canvas-muted); color: var(--color-ink); }
/* 移动端那条整条删除 */
- 三态可分:默认透明 + 次级墨 → 悬停 55% 淡面 + 次级墨 → 选中实面 + 主墨。选中比悬停重一档,靠面与墨色等级,不用色相、不用
font-weight(任务书 §3.3)。 - 取 55% 的理由:
--color-canvas-muted浅色#ebe9e3压在--color-canvas#fbfaf7上本就只差一档,悬停若取满会和选中撞;55% 是仓库里已有的同族用法(globals.css现有 84 处color-mix,如.evidence-audit-panel用 68%、.evidence-audit-row用 64%),没有引入新字面色值、没有新 token。 - 深色:
--color-canvas-muted#30302d、--color-ink#f2f0ea、--color-ink-secondary#b3afa4三者在深色块里都已定义,改动只用既有 token,dark-theme-contract.test.ts无需新增条目(实跑仍绿)。深浅两套的肉眼可分辨留给真人清单。 - 移动端(≤767px)导航是顶部四栏,触摸没有 hover,选中态即实面 + 主墨,删掉下边框后仍与其余三项可分。
- 可达性:
:focus-visible描边规则未动;aria-current="page"仍在 DOM 上,屏幕阅读器不受影响 —— 选中态不是只靠颜色。 - 左侧会话列表的 2px
--sidebar-ring本轮未动(任务书 §3.2),两处观感暂时不一致,已在DESIGN.md写明是已知且授权的。
6. 验收
| 项 | 基线 5094fd26 |
本轮 | 结论 |
|---|---|---|---|
tsc --noEmit |
0 | 0 | 通过(中途新测试有 1 个 TS2345,已修) |
npm run lint |
0 error / 118 warning | 0 error / 118 warning | 通过,warning 未上升 |
npm test |
tests 3341 / pass 3295 / fail 31 / skipped 15 | tests 3346 / pass 3300 / fail 31 / skipped 15 | 通过,失败清单与基线逐字相同 |
next build / |
○ Static |
○ Static |
通过 |
| 样式 gzip | 37,547 B | 37,688 B | +141 B / +0.38%,在 ±2% 内 |
| 快速门 pytest 段 | 792 passed / 1 skipped / 0 failed | 792 passed / 1 skipped / 0 failed | 通过 |
Home() useState / useRef |
36 / 37 | 36 / 37(未碰 page.tsx) |
未增长 |
page.tsx 行数 |
1846 | 1846(未改动) | 未增长 |
gzip 口径:Next 16 的路由表不再打印 First Load JS,本轮又只改 CSS,所以量的是产物样式 chunk 本身——把新增的两个 @supports 块摘掉再 gzip 作对照(37,547 B),带上是 37,688 B。
本轮只动 globals.css、DESIGN.md 与五个测试文件,page.tsx 与任何 Home 级 hook 都没碰,所以没有新增 Home 级 hook,不涉及 OPTIONAL_HOME_HOOKS 登记(BUG-736)。
6.1 全量套件
tests 3341 → 3346(+5,正是本轮新增的 5 条),pass 3295 → 3300(+5),fail 31 → 31,skipped 15 → 15。测试总数没有下降。
失败清单与基线 diff 结果为完全一致(31 条,全是无 Docker 的 DB / 部署 / 引擎套件,BLOCKED.md BLK-002 / BLK-003):
comm -13 fails-baseline.txt fails-final.txt # 新增失败:空
comm -23 fails-baseline.txt fails-final.txt # 消失失败:空
diff -q fails-baseline.txt fails-final.txt # IDENTICAL
中途出现过 3 条新失败,已修,原因记在 §6.1.1。
6.1.1 三条既有断言按「原值 / 新值 / 原因」改动
改 dvh 写法打红了三条把旧文本钉死的断言。三条都保留原有意图(满视口高度 / 全视口滚动容器),只把断言对象换成真正会发布的写法,没有一条被弱化或删除:
| 文件 | 原值 | 新值 | 原因 |
|---|---|---|---|
tests/birth-time-mobile-scroll-contract.test.ts |
/\.group\\\/sidebar-provider\[data-viewport\]\s*\{[^}]*height:\s*100vh;[^}]*height:\s*100dvh/ |
拆成两条:块内 height:\s*100vh;,外加 @supports (height: 1dvh) 内该选择器 height: 100dvh |
这条原本守着一个从未发布过的写法。 重复声明式回退被 Lightning CSS 合并,产物里只剩 height:100dvh;断言读的是源码文本所以一直绿。换成断两半后,vh 基线与 dvh 升级都必须真在。 |
tests/mobile-interaction-contract.test.ts |
css.indexOf(".auth-page { height: 100dvh; overflow-x: hidden; overflow-y: auto;") |
同串改 height: 100vh,并新增一条断 @supports 内有 .auth-page { height: 100dvh; } |
登录页的满视口高度改为 vh 基线 + 特性查询升级。原断言只认旧字面量。断言数量从 1 增到 2,未放宽。 |
tests/admin-payments-contract.test.ts |
/\.admin-app-shell \{ height: 100dvh; min-height: 0; overflow-y: auto; \}/ |
同串改 height: 100vh,并新增一条断 @supports 内有 .admin-app-shell { height: 100dvh; } |
同上。后台外壳仍是独立全视口滚动容器,只是高度写法换了。断言数量从 1 增到 2,未放宽。 |
6.2 快速门
.venv/bin/python scripts/run_quality_gate.py --profile quick,pytest 段 792 passed / 1 skipped / 0 failed,与基线一致。
快速门整体退出 1,唯一原因是本机无 rsync、无 Docker 的既有环境缺口(BLOCKED.md BLK-002 / BLK-003),与本轮改动无关。退出码直接取自命令本身,没有隔着管道。
7. 环境缺口
- 无登录态、无法在真实应用里走查。 §2 的数字是在真实产物 CSS + 逐节点复刻 DOM 的离线页面上量的,不是在跑起来的应用里量的。真实内容(星盘资料的列表/详情、账户与点数的套餐卡)可能有本 harness 没模拟到的溢出行为。浏览器级条目见
docs/testing/settings-dialog-20260916.md。 - 无法确认用户当时的浏览器。 这是 BUG-698 停在
investigating的唯一原因。需要产品负责人补:哪两个分区、什么浏览器与版本、窗口多大、能否截图。 - 无 Docker:DB / 部署套件 31 条失败沿用基线(
BLOCKED.mdBLK-002 / BLK-003),与基线逐条比对见 §6.1。 - 深浅两套主题下「悬停 vs 选中」肉眼可分辨、以及 375px 下四栏观感,都需要真人确认。