# 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` 的写法,补成重复声明: ```css 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 无法证伪条件,整块原样保留: ```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 条,全文件范围): 1. `height:` 用 dvh 的声明必须落在 `@supports (height: 1dvh)` 内。 2. 禁止重复声明式回退 —— 它会被压缩器吃掉,测试里写明了原因。 3. `@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`): ```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. 环境缺口 1. **无登录态、无法在真实应用里走查。** §2 的数字是在真实产物 CSS + 逐节点复刻 DOM 的离线页面上量的,不是在跑起来的应用里量的。真实内容(星盘资料的列表/详情、账户与点数的套餐卡)可能有本 harness 没模拟到的溢出行为。浏览器级条目见 `docs/testing/settings-dialog-20260916.md`。 2. **无法确认用户当时的浏览器。** 这是 BUG-698 停在 `investigating` 的唯一原因。需要产品负责人补:哪两个分区、什么浏览器与版本、窗口多大、能否截图。 3. 无 Docker:DB / 部署套件 31 条失败沿用基线(`BLOCKED.md` BLK-002 / BLK-003),与基线逐条比对见 §6.1。 4. 深浅两套主题下「悬停 vs 选中」肉眼可分辨、以及 375px 下四栏观感,都需要真人确认。