来源:notes/documents/网页版页外空白与页边列:原型与方案.md

网页版页外空白与页边列:原型与方案

Status: ready-for-agent

上一轮的 notes/documents/网页版阅读面自适应与日程分栏阅读.mdnote_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%) 被截断成 ;而索引列下面那一大片是空的。

现在 1920:正文顶着页的左半边,两侧各 238px 没有人管;档号只在最上面三行出现过一次

现在 1920(往下滚 900px):档号已经滚走,页边那条 72px 通道整条空着,左边一片白

现在 1440:页这一列 1076px 差 4px 够不到轨道的门槛,目录整条不出现

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. 排版细修(都不动度量)

  • .bodyoverflow-wrap: anywherebreak-wordanywhere 允许把列压到比一个词还窄, 于是「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-trimtext-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 的分档,而不是加一个拖拽手柄

一句话:这一轮的空缺是构图问题,不是零件问题——没有库能替我们决定页边放什么。

原型图

提案 1920:页边列里是书眉(档号 / 创建 / 更新,粘住不动),节号悬挂在版心左边,右边是目录

提案 1920(往下滚):档号还在,节号整篇可见,读到「3 前端」时目录也跟着亮到第 3 节

提案 1920 + 索引 360:截断 76 → 43

提案 1920 + 索引 420(推荐):截断 76 → 19,标题几乎都能读全

提案 1440:轨道档,目录终于出现(外侧余量 142 → 50)

提案 1366:轨道档,目录出现(外侧余量 105 → 13)

提案 1920 · 夜纸:同一组角色换一套值,页边列与节号跟着换

提案窄屏 430:两档都不出现,退回现在的样子(回归)

度量对照(实测)

视口 现在 提案
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 }、h2 counter-increment: sec + counter-reset: sub、 h3 counter-increment: sub),构建期与运行期都不用算号;目录里的号在 DocPage.tsx 里按 headings 的层级算(和 renderDocument 从同一批 h2/h3 来的),两边不会打架。
  • 叶里必须同时关掉两档:日程分栏的叶只有 712px 宽,但 .page 是它上面那个容器——页这一列在 1512 以上 ≥ 1120,叶里会以为自己也够宽。实测(1920,?m=daily&d=2026-09-22&n=…):页边档生效时 叶里的正文被挤到 232px,档号反倒占了 168。NoteReaderrail={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 testtsc -beslintvite build 四条闸门通过。