来源:
notes/documents/网页版页外空白与页边列:原型与方案.md网页版页外空白与页边列:原型与方案
Status: ready-for-agent
上一轮的 notes/documents/网页版阅读面自适应与日程分栏阅读.md(note_6f93685ef27c44c4)把正文从页的
左边缘挪到了页中间,并给长笔记配了一条目录轨道。这一轮动的是它留下的那件事:居中之后,页的四周还是空的。
度量一个字不动(页边 72 / 正文 640 / 一行 34 字),动的是那圈空白里的东西。
数字与图都来自 2026-09-23 的本地预览(cd web && npm run build && npm run preview:local,真构建产物、
真笔记、真 CSS)。原型不是另写一份 HTML,是「真页面 + 一段覆盖 CSS」;脚本在
.scratch/web-page-margin/prototype/page-margin.mjs,图在 .scratch/web-page-margin/assets/
(本文引用的是同一批图在 notes/documents/assets/folio-page-margin/ 的副本)。
偏差照旧:登录态是本地预览夹具伪造的,鉴权、会话、OAuth 回跳的结论都不成立;布局与度量成立。
Problem Statement
四件事,都是逐屏量出来的。
一、页边那条 72px 的通道,只有最上面三行有内容,然后跟着正文一起滚走。
档号 / 创建 / 更新 是页里唯一用到页边的东西。实测:把页滚到 900px,.stamp 已经离开视口——
一篇七千字的笔记再往下全是空通道。这不是留白,是一条开了头又没人接着写的边。
二、余量随屏宽增长,没有任何一栏吃它。
| 视口 | 页这一列 | 「页边 + 正文 + 轨道」占多少 | 画布两侧余量 |
|---|---|---|---|
| 2560 | 2196 | 1080(49%) | 558 + 558 |
| 1920 | 1556 | 1080(69%) | 238 + 238 |
| 1440 | 1076 | 792(74%) | 142 + 142 |
三、1440 与 1366 上连目录都没有。 轨道的门槛是「页这一列 ≥ 1080px」,对应视口约 1444px: 1440 的笔记本正好差 4px,退回「只有正文居中」,两侧各留 142px。而 1440 恰好是最想要一个 「有哪些节」入口的屏宽。
四、索引列同时太窄又太空。 300px 的索引里可用文字宽 249px,123 篇笔记的中位标题宽 277px、
最长 593px,实测 76 篇(62%) 被截断成 …;而索引列下面那一大片是空的。



Solution
一句话:页装得下,就把页边做成真的页边。
A. 页边档(页这一列 ≥ 1120px):档号搬进页边,节的序号挂在页边
- 页边 72 → 168px:档号 / 创建 / 更新 整块搬进页边列,标签一行、值一行(值折行),
整块
position: sticky粘在页顶——读到哪儿都看得见自己在读哪一篇。这是这本书的书眉。 - 每一节的序号悬挂在版心左边的页边上:h2 是
1 2 3,h3 是2.1。页边那条通道从此整篇都有内容, 而且是最有用的内容——「我在第几节」。它是页码的等价物,只不过刻度是节,不是页。 - 目录里显示同一套号,两处指着同一件事。
- 正文 640 一个数字都不动(0013 的教训:这套设计最不该动的就是度量)。
- 附带的好处:档号不再占正文头顶的三行,标题因此上移,页的顶部也省下一段竖直空白。
B. 轨道档(页这一列 ≥ 976px):目录下放到 1440,1366 也有
- 门槛 1080 → 976(= 页边 72 + 正文 640 + 缝 32 + 轨道 200 + 画布两侧留白 32), 对应视口约 1340px:1366 与 1440 从此都有目录。
- 缝 48 → 32,画布留白 40 → 16,轨道 240 → 200。档号在这档仍留在页里(页不够宽,装不下页边列)。
C. 索引分档:宽屏的余量先给索引(唯一动到外壳数字的一条)
| 索引宽 | 门槛 | 123 条里截断 | 1920 上页这一列 | 页边档还装得下吗 |
|---|---|---|---|---|
| 300(现在) | — | 76 | 1556 | 装得下 |
| 360 | 视口 ≥ 1544 | 43 | 1496 | 装得下 |
| 420(推荐) | 视口 ≥ 1604 | 19 | 1436 | 装得下(1120 = 门槛,正好) |
推荐 420:93% 的标题不再被截断,而页这一列仍 ≥ 1120,页边档一点不吃亏。门槛 1604 就是这么算出来的 (脊 64 + 索引 420 + 页边档 1120)。保守一点就取 360;两个数都是「宽屏多出来的地方先补坏掉的标题」。
D. 排版细修(都不动度量)
.body的overflow-wrap: anywhere→break-word:anywhere允许把列压到比一个词还窄, 于是「Markdown」在表格里被折成「Markd / own」(现状截图里就有)。同一张表实测最大列宽 307 → 245, 单词不再被切开。- 三条标准 CSS,都用
@supports包住,老浏览器照旧:段末孤字text-wrap: pretty、 中文标点挤压text-spacing-trim: trim-start、标题的幽灵行距text-box: trim-both。
E. 库:这一轮一个都不引
| 候选 | 想解决的问题 | 结论 |
|---|---|---|
@tanstack/react-virtual |
索引 123 条一次渲染 | 不引:123 行、行高固定,实测滚动没有卡顿;到上千条再说 |
heti / pangu.js |
中文标点挤压、中西文间距 | 不引:text-spacing-trim 与 text-autospace 已是标准 CSS(Chrome 123+),不必再进一份要跟着浏览器版本维护的第三方 CSS |
capsize |
字体度量对齐、去掉标题上下的幽灵行距 | 不引:text-box: trim-both(CSS Inline 3,Chrome 133+)就是这件事,而且是原生的 |
@fontsource-variable/noto-serif-sc |
正文宋体跨机器一致 | 暂不引:现在靠系统字体(本机落到 Noto Serif CJK),要自托管得先做子集化;与留白无关,另开 |
react-resizable-panels |
索引 / 正文可拖拽改宽 | 不引:外壳契约(0009)里索引是固定列;真要改宽就用 C 的分档,而不是加一个拖拽手柄 |
一句话:这一轮的空缺是构图问题,不是零件问题——没有库能替我们决定页边放什么。
原型图








度量对照(实测)
| 视口 | 现在 | 提案 |
|---|---|---|
| 2560 | 画布 1080,余量 558 + 558 | 页边档:画布 1120,余量 538 + 538 |
| 1920 | 画布 1080,余量 238 + 238 | 页边档:画布 1120,余量 218 + 218(索引 420 时 178 + 178) |
| 1512 | 画布 1080,余量 74 + 74 | 页边档:画布 1120,余量 14 + 14 |
| 1440 | 画布 792,无目录,余量 142 + 142 | 轨道档:画布 976,余量 50 + 50,有目录 |
| 1366 | 画布 792,无目录,余量 105 + 105 | 轨道档:画布 976,余量 13 + 13,有目录 |
| 1280 | 画布 792,无目录,余量 62 + 62 | 不变(页这一列 916 < 976) |
| 430 | 窄屏降级 | 不变 |
正文列在六档里量出来全是 640(门槛就是装图宽度本身,所以正文永远不会被挤瘦——见下)。 余量并没有小多少(1920 上每侧 238 → 218),变的是那圈余量里现在有东西: 左边是书眉与节号,右边是目录,索引加宽后连标题也不再被切。
Implementation Decisions
- 两档都用
@container page,沿用 0013 的写法:门槛问的是「页这一列多宽」,不是视口多宽。 两个门槛的算法就是构图本身的宽度,所以正文永远不缩水:- 页边档:
168 + 32 + 640 + 32 + 216 + 画布留白 32 = 1120 - 轨道档:
72 + 640 + 32 + 200 + 32 = 976实测边界:页这一列 1120 → 正文 640;强行在 1096 上套页边档,正文掉到 616;975 上套则掉到 495。 所以门槛只能等于构图宽度,不能「差不多」。
- 页边档:
- 悬挂号用 CSS counter(
.body { counter-reset: sec }、h2counter-increment: sec+counter-reset: sub、 h3counter-increment: sub),构建期与运行期都不用算号;目录里的号在DocPage.tsx里按headings的层级算(和renderDocument从同一批 h2/h3 来的),两边不会打架。 - 叶里必须同时关掉两档:日程分栏的叶只有 712px 宽,但
.page是它上面那个容器——页这一列在 1512 以上 ≥ 1120,叶里会以为自己也够宽。实测(1920,?m=daily&d=2026-09-22&n=…):页边档生效时 叶里的正文被挤到 232px,档号反倒占了 168。NoteReader的rail={false}已经关掉轨道, 页边档跟着同一个开关走。 - 页边的档号块加一层
background: var(--paper):悬挂号滚到它下面时被盖住,而不是两条文字叠在一起 (号在缝里,距页边列右缘只有 16px)。三位数的号(Elon Musk 全文那类 266 节的文档)会向左探进页边 7px, 正好落在这层底下。 - 索引分档用
@media,页内两档用@container:外壳的三个数字本来就与视口绑定,页的构图只认页自己多宽。 - 要改两条 ADR:0009(页边的角色从「72px 标签道」扩成「页够宽就是 168px 的档号列 + 悬挂节号」; 索引 300 变成「300 起、宽屏 420」)与 0013(页内构图多一档,门槛与轨道宽度重算;轨道档的 门槛从 1080 降到 976)。两条都要写清楚理由,否则下一个模块会自己发明一套。
Considered Options
- 把正文加宽到 900 / 1200。 最省事、立刻填满空白——但 640px 一行 34 字是量过的度量 (中国读者一行 25–45 字),0013 已经说过一次「不该动的是度量」。同样不动。
- 正文分两栏(对开)。 宽屏上确实能把空白吃干,但
columns的阅读顺序是断的(读完左栏要回到顶部 读右栏),要么就得上横向分页。代价与收益不成比例。 - 只把余量压小(画布贴索引、留白从 40 压到 16)。 空白的绝对量小一点,页还是「一段正文 + 一圈 没人做决定的空白」。页边列是把空白变成结构,不只是压小。
- 页边再放「字数 / 预计阅读 / 读到 42%」。 试过:字数是二手信息,读到哪儿节号已经说了。 按「花一处、其余安静」砍掉,页边只留书眉与节号。
- 索引改成标题折两行。 不用加宽就能救回大部分截断,但索引的行高节奏会不齐(现在每条固定两行: 标题 + 日期)。先只加宽,折行等真觉得不够再说。
- 轨道加一条阅读进度线。 小节高亮已经表达了「读到哪儿」;进度条是常见的装饰,加它只是多一个会动的元素。
- 给节号加「§」或「第 N 节」。 页边只有 168px,短号比长标签安静,号本身就是结构。
边界与没做的
- 两档都不在窄屏出现(<976px 页宽照旧:页边在正文上方,无轨道)。窄屏实测无横向滚动。
- 叶(日程分栏)里两档都不出,本轮规则一个字都不进叶。
- 不改正文的度量、字体、行距;不改脊 64;不改路由与数据。
- 「其他文档」走同一个
DocPage,因此一起改;任务模块还没装订,不动。 - 索引分档只改宽,不改条目的排版(标题仍单行、仍带日期)。
验收
- 页这一列 1120px → 正文正好 640;1096px → 退回轨道档;976px → 正文仍 640;975px → 无轨道。
- 1920 / 1512 / 1440 / 1366 / 1280 / 430 六档截图与本文原型图逐张对齐(真数据、真 CSS)。
- 页滚到底:档号块一直在视口里(sticky),节号整篇可见,目录当前节跟着走。
- 索引在 420 档下 123 条里截断 ≤ 20(现在 76)。
- 日程分栏(
?m=daily&d=…&n=…)叶里的正文仍是 600–640,不再被挤到 232。 - 窄屏 430:
documentElement.scrollWidth <= innerWidth。 npm test、tsc -b、eslint、vite build四条闸门通过。