来源:
notes/documents/网页版阅读面与日程分栏实施记录.md网页版阅读面与日程分栏:实施记录
Status: done(2026-09-23 落地)
上游依据:notes/documents/网页版阅读面自适应与日程分栏阅读.md(note_6f93685ef27c44c4,原型与定案)、
web/docs/adr/0013-page-canvas-and-split-reading.md(第一次让「页」有了页内横向构图)。
代码提交 b0f2150(feat: 阅读面加画布居中与目录轨道,日程改为同页分栏读笔记)、
a8ef290(fix: 叶内正文补回右留白,窄屏的右留白改回 16)。
落地的两件事
| 场景 | 页内构图 | 门槛(页这一列的宽度) |
|---|---|---|
| 读一页文档 | 「页边 72 + 正文 640」整块在页里居中(.page-canvas) |
一律 |
| 一页长文档 | 居中 + 右侧 240px 目录轨道,缝 48(sticky、滚动高亮、点击平滑跳转) | ≥1080,且这篇有 ≥2 个 h2/h3 |
| 日程里点一条笔记 | 同一页分两栏:左栏收缩、右叶滑出 | ≥1196 才并排,否则叶整宽滑入 |
度量一个都没动:页边仍 72、正文仍 640、行宽仍 34 字,动的是它在页里的位置。叶里另算—— 叶 712 = 页边 72 + 正文 600 + 右留白 40,640 是页这一列的度量,叶比它窄时先让给留白(见下)。 实测(本地预览,CSS 像素):
| 视口 | 页这一列 | 画布 | 正文列 | 目录轨道 |
|---|---|---|---|---|
| 1920 | 1556 | 1080,居中 | 714 – 1354 | 240,在正文右侧 48 |
| 1444(刚过门槛) | 1080 | 1080 | 整 640 | 240 |
| 1443(门槛下) | 1079 | 792,居中 | 整 640 | 不出 |
| 1280 | 916 | 792,居中 | 538 – 1178 | 不出 |
| 430(窄屏) | 430 | 不再加留白 | 16 – 398(两边各 16) | 不出 |
日程(1920):左栏 1556 → 844,叶 713 停在 x=1207;1560:左栏 646、叶 551;1559:叶整宽 1196 滑入。






实现期改掉的三处(原型里没有的东西)
一、门槛从「视口 ≥1400」改成「页这一列 ≥1080」,用 @container 而不是媒体查询。 照原型里视口的
1400 编出来一量:视口 1400–1443 这段里,页这一列(1036–1079)装不下整张画布(1080),网格把正文列
挤到 596px——度量动了,而这套设计最不该动的就是它。视口宽度还和脊(64)、索引(300)的宽度耦合,
那两个数字将来一改,媒体查询就悄悄错。改成 .page { container-type: inline-size } + @container page,
门槛问的就是页这一列自己:1080 = 72 + 640 + 48 + 240 再加画布两侧 80 的留白,1196 = 叶 712 + 日历侧 484。
二、叶的 ref 回调每次提交都会重排一次 setShown(true)。 原写法是 ref={(node) => { leafRef.current = node; slide.attach(node) }}——内联箭头每次渲染都是新函数,React 于是在每次提交都回收再交回这个 ref,attach 里的 rAF 就把 shown 又翻成 true。后果:关的那一帧刚摘掉 is-open,下一帧又被加回来,叶原地不动、280ms 后直接消失(看起来是硬切),藏在叶里的焦点也被抢回去。close.py 采样看得很清楚:Esc 之后 .split 在第 26ms 就又是 split is-open,叶的 translateX 全程 0,到 267ms 直接卸载。改成 ref 回调稳定(useCallback 到 slide.attach)并「只认第一次挂载的节点」,关闭才真的往外滑(128 → 711,245ms 走完)。
三、close() 里先 navigate() 再读 ref。 那次渲染已经把 ref 摘成 null(叶不再是这一条),
于是 row.focus() 落空、焦点掉到 body 上。改成先把那一行取住、再 navigate、再 focus。
用 HTMLElement.prototype.focus 打桩抓栈才看清:close() 的 focus 确实打到了那一行,紧接着又被
上面第二条那个 effect 的 leaf.focus() 抢走——两个毛病叠在一起,只修一个都看不出来。
验证
npm run lint、npx tsc -b、npm test(53/53)全过。- 本地预览(
npm run build && npm run preview:local -- --port=4199)实测:上表两组几何、控制台 0 error、prefers-reduced-motion: reduce下两段过渡为none、深链?m=daily&n=…&d=…直接出叶并把焦点给叶、 换日期自动收叶、Esc 关闭后地址栏只剩?m=daily&d=…且焦点回到row row-note(aria-expanded=false)。 - 平滑性的证据分两处:把
--slide临时拉长 7 倍(缓动曲线不变)后用 rAF 采样,叶的translateX713 → 0 与左栏 1556 → 844 同步、连续、单调、带减速;再在真实时长下抓中间帧(就是上面第 6 张图, 抓到时m41 = 624/713)。踩过的坑:一开始想用 rAF 直接采样真实的 280ms 动画来判断平滑性, headless 下采样读数会跳变——那是测量假象,不能用它下结论,换成「拉长时长采样 + 抓中间帧」才对。 - 回退说明:本机没有 Browser 插件,按
frontend-testing-debugging的回退规矩改用 Python Playwright + 本机 Chromium(~/.cache/ms-playwright/chromium-1134/)跑本地预览。预览的身份是伪造的, 所以这批结论里没有任何一条是关于鉴权、会话或 OAuth 的;布局、渲染与交互的结论成立。
复查时又修的两处(都是「正文贴到边缘」)
落地之后按「有没有东西贴到边缘」把四个模块、五档宽度(375 / 430 / 900 / 1280 / 1920)扫了一遍,
量出两处同类的毛病,都出在我给画布写的那条 padding-right: 0 上:
一、叶里的正文贴到屏幕右边缘。 .leaf .page-canvas 把画布的左右留白都清零后,712 的叶正好被
「页边 72 + 正文 640」塞满——1920 上实测 h1 的右边缘 = 1920,标题的最后一个字顶在屏幕边上。
原型里不是这样:叶内正文 600 + 右侧留白 40(h1 1280–1880)。宽屏把 40 补回去,实测与原型
逐像素一致。
二、窄屏的右留白也被清零了。 padding-right: 0 的用意是「宽屏时右留白让给画布」(画布自己
有 40),但窄屏画布不留白,这条规则却仍在生效:430 上实测正文 16 – 414,左边有 16、右边贴屏。
而改动前的线上是两边各 16(16 – 398)。拿原型对照才看清责任:原型的 layout=current
(改前的线上)是 16 / 16,layout=proposed 是 16 / 0——这个毛病是从原型里带进来的,
不是实现时写坏的。窄屏把 16 还给 .content。



顺带核对过、结论是「不用改」的:叶与窄屏里的正文图片都按度量缩放(640 / 600 / 398),
scrollWidth 与 clientWidth 相等,没有横向溢出;375 宽度下正文比视口宽 10px,与改动前
一模一样,是那个宽度的既有状态。
留下的边界
- 轨道只收 h2/h3,不做折叠;叶里不放轨道(叶自己不够宽)。
- 不支持同时开两篇笔记;窄屏没有手势关闭,只有「关闭」与 Esc。
SLIDE_MS(TSX,280)与--slide(CSS)是同一条时长的两份拷贝,改一个必须改另一个。- 门槛用容器查询:不支持
@container的浏览器退回「只居中、不出轨道」,不会坏在别处。 - 叶里的正文是 600(不是 640),右留白 40 是定值(ADR 0013 里记着),别当成「少了 40」去修。