# 兰台前端 · 设计规格书(v1 · 落地前置) > 本文是 `holo-paper.html` 原型锁定的设计决策,供 `src-ui` 工程落地对照。 > **「已锁定」是契约,落地时不许回退;未列出的像素细节(字号/间距/图标形状/hover 态)可在真环境再磨。** --- ## 0. 一句话定位 **兰台=汉代皇家档案典籍库。** 产品不再是「聊天窗」,而是**一部正在被编纂的案卷**——人在纸边批注、AI 居中撰文、机器贴底注记。核心是把「等权消息流」换成「注疏层级」。 --- ## 1. 版式范式(已锁定) **注疏横排**。文字全程横排(代码、英文、URL 不折行),但骨架是古籍注疏的层级,不是聊天气泡: - **正文**(答 / 策)居中,是案卷的「经文」,正文位、大一号(17px 起,行距 2.0); - **来文**(问)是人的批注,手迹位 + 朱砂深 16px/1.9(B4 环1 定稿),左侧 2px 红批线; - **夹注**(思)缩进列边(约 86% 宽),石墨铅笔,小一号,虚线勾边; - **脚注**(器)贴底小字,上方一道 44px 石青注线引出; - **抄录**(码)栏内图版(3px 硬左线 + 上下规线 + 米黄底); - **卷首**:**版心天头**(**2026-09-16 用户拍板 B 案重排**)——玉徽(亭台图标 `#icon-lantai` 26px)· 机读眉行(卷次「案卷 Nº 12」)· 题字(36px)· 机读档行(**立卷日「二〇二六年八月廿二日」· N 块**)四行**同轴居中于版心**(2026-10-08 起版心 = 流区宽一半「墨占纸半」,默认 1440 纸下仍 720——原封顶 720 随拉宽流区退役),底部硬规线 + 左缘朱砂短横(**2026-09-02 改档只挂活跃卷**——整屏至多一处红(浸墨③),红在哪卷即活卷;对原型 `.folio-head` 的主动偏离——原型先于真纸材质批与一纸多卷定案);规线与版口钮同样只画版心宽。卷号在卷首**恰出现一次**(有名卷在眉行、无名卷在题字 fallback,眉行退为「兰台 · 案卷」文类行);**卷名真源 = 卷快照 `label`(空 = 未命名)**——「有名/无名」判据、首条来文派生、显示兜底「案卷 N」(一律取**档号**)的单一真源 = `src-ui/src/state/volume-name.ts`(2026-09-18 命名收口:起卷不写数字名,日志头行不再带卷名);立卷日真源 = 卷日志头行 `createdAt`,旧卷无此字段 = 档行只显块数(不编造)。**重排前的病灶**(实测):玉徽居中于整张纸(默认 1440 宽流区的中轴)、眉行/题字/档行却左齐于纸缘内距 16px,而正文块居中于 720 版心——题字比正文左缘还左 344px。原型的「钤印居中于天头」手势保留,改居中于版心; - **界栏**:正文列左右各 1px 栏线(`--rule-strong`),页边注在左栏外(**2026-09-02 翻案**:08-30 把界栏落成「流区四边版框双线框」的读法判死——纸缘交还材料、改物理包边(见 §10 纸层次·流区真纸);版框=「印在纸上的墨」,流区这张「叶」不印框——WebGL 探针 `prototype/webgl-paper.html` 判读定案); - **划词**(2026-09-02 视觉迭代):流区选区=手写朱笔下划线(`src/paper/sel-ink.ts` 纯函数——主笔朱砂实线 1.7px+淡墨 echo 第二笔,种子取选区文本 hash、同选区恒同线;跨块选区也画线);原生 ::selection 洗底在流区退役(输入面照旧)——纸不动、只落墨,被人手划过的字落朱线; - **选区政策**(**2026-09-17 全局化,用户报「太多不该触发文字选中的地方」触发**):**全应用默认不可选,正文自证可选**。`foundation.css` 在 `html` 上落 `user-select: none`——**界面是界面,不是可复制的文档**;放开面只有两处:① **输入面**(`input / textarea / select / [contenteditable]` 一条,Chromium 下祖先 none 会连坐表单件的拖选);② **正文容器自持** `user-select: text`(纸壳 `.pp-block` 即范式,块内结构件/交互件再各自撤回 none)。**判定**:新增产品面若确有可复制正文照 ② 自行放开,界面件一律不写。**旧账的病灶**:此前没人管这件事,谁出事谁补一条(坞拖动期 / 纸条 / 小地图 / 书脊行 / 插件窗题条各写一次)——补一处漏一处,拖动扫选、双击选词、Ctrl+A 把整壳收进选区在其余面上照旧发生;而纸上挂着 document 级 `selectionchange`(选中浮钮 + 划词朱线),任何一次扫选都可能唤醒画布上的划词动作; - **划词的第二道守卫**(2026-09-17):`user-select: none` 只挡「从该面起选」,挡不住「按住家具再扫进纸面」——浏览器把选区锚点**夹到最近的可选内容**(块内文字),于是家具起手的拖拽在锚点上与真划词无法区分,浮钮照弹(同族实测见 paper-minimap 的 `preventDefault` 注:拖小地图会选中画布文字)。故两个选区消费面(浮钮 / 划词朱线)共用**起手面判据**:按下那一刻记下是否落在 `.pp-block` 内,本次按住期间的选区按起手面算;块内输入件(准奏卡意见框)另由 `isEditableSurface` 挡(在自己输入框里选一段不该弹抽纸条浮钮)。**代价(明账)**:纯键盘造出的选区沿用「上一次按下的面」——先在块里点过即照常认,冷启动直接 Ctrl+A 不认(全选不是划词); - **印章**:朱砂「蘭臺」二字竖排印,作品牌签名。 --- ## 2. 字体系统(2026-09-10 三体换代拍板重锁;2026-09-24 机读位复原) | 角色 | 值层 | 装载 | 备注 | |---|---|---|---| | 正文位 `--f-song`(原宋体) | MiSans | `src/app/fonts.css` @font-face(`src/assets/fonts/MiSans-VF.ttf`,VF 单文件 100-900 全字重) | 原宋体/衬线正文 | | 手迹位 `--f-kai`(原楷书) | MiSans | 同上 | 只用于「人的」来文——文类语义保留,手写感随三体退役 | | 机读位 `--f-mono`(原等宽) | Noto Sans Mono CJK SC(OFL 1.1,随包自托管) | 同上 @font-face(`src/assets/fonts/NotoSansMonoCJKsc-Regular.otf`,只带 Regular——粗体走合成) | 代码/命令输出/引文——**中文也等宽**(2026-09-24 复原:09-10 的「非等宽对齐」代价翻案) | 字体栈(CSS 直抄,文类语义键保留): ```css --f-song: "MiSans", "PingFang SC", "Microsoft YaHei", sans-serif; --f-kai: "MiSans", "PingFang SC", "Microsoft YaHei", sans-serif; --f-mono: "Noto Sans Mono CJK SC", "MiSans", "PingFang SC", "Microsoft YaHei", monospace; ``` **装载**:`src/app/fonts.ts` 只 `import './fonts.css'`(本地 @font-face,打包进应用不依赖系统安装);fontsource 四包(eb-garamond / ma-shan-zheng / ibm-plex-mono / noto-serif-sc)已卸载。 > 关键纪律:**斜体走合成伪斜**(MiSans 无 italic 字形,`font-style: italic` 由渲染器合成——拍板接受);canvas 侧字面量栈(type-tokens `FONT_STACKS`、InkLayer `LABEL_FONT_STACK`)与 tokens.css 同步改——`--f-mono` 与 `FONT_STACKS.mono` **逐字同值**由 `tests/paper-visual-decisions.test.ts` 守卫。 > > **`pre > code` 纪律**(2026-09-24 补):UA 样式表对 `` 有直接规则 `font-family: monospace`,**直接规则压继承**——`
` 上声明的 `var(--f-mono)` 到不了真正画字的 ``。故 `foundation.css` 收一条全局口径 **`pre code { font: inherit }`**;新增任何「pre 里套 code」的机器文本面照此写,别在父层声明了事。

---

## 3. 墨色系统(已锁定 · 矿物颜料)

| token | 值 | 语义 | 用在哪 |
|---|---|---|---|
| `--ink-1` | `#26221C` | 松烟墨(正文) | 答 / 策 / 码正文 |
| `--ink-2` | `#55503F` | 次级墨 | 规线 / 次级文字 |
| `--ink-3` | `#8A8172` | 三级墨 | 注记 / 页码 |
| `--ink-4` | `#B5AC9A` | 弱规线 | 分隔 |
| `--seal` | `#A63A2E` | 朱砂(人的批改) | 印章 / 来文 / 圈点 / 朱批 |
| `--seal-deep` | `#8C2F26` | 朱砂深 | 来文文字 |
| `--indigo` | `#3A5B7A` | 石青(机器/仪表) | 脚注 / 拟策策面(左线·淡底·文类签)/ diff 新增 |
| `--graphite` | `#6F6E68` | 石墨(铅笔灰) | 夹注 / 擦改 / diff 删除 |
| `--paper` | `#F6F1E7` | 纸面 | 画布 / 页面底 |
| `--paper-deep` | `#EFE8DA` | 深纸 | 输入条 / 图版底 |
| `--paper-margin` | `#EDE5D5` | 页边 | 舞台底色 |

语义状态(保留原 `--pass/--warn/--fail` 语义,降饱和到矿物色):`--pass #5E7A55`、`--warn #B0782E`、`--fail #A9443F`。

**一条铁律:朱砂 = 人,石青 = 机,石墨 = 夹注,墨 = 正文。** 不许换义。
> 2026-09-16 口径订正:原写「石墨 = 草稿」与值层不符——`--graphite (#6F6E68)` 实挂夹注 / 擦改 / diff 删除。
> **「草稿」语义由虚线(`--rule-dashed`)表达,不占颜色位。**
唯一豁免(2026-08-23 B5 环2 用户拍板):**抄录(diff)加删色走红绿墨色化**——add=松绿(`--pass`),del=朱砂深+删除线。del 的朱砂深不表「人的声音」,读作擦改/勘误红笔;加删红绿对是代码块的视觉直觉,纯红纯绿不入纸面,必须落在矿物色族。

---

## 4. 块类型映射(已锁定)

现有 `paper/block-model.ts` 的 `BlockKind` 无需改动,只改渲染层视觉:

| BlockKind | 文类签 | 字体 | 墨色 | 视觉 |
|---|---|---|---|---|
| `user` | 来文 | `--f-kai` | `--seal-deep` | 左 2px 红批线 + 圈点关键词;16px / 行距 1.9(B4 环1 拍板:字号收到正文 17 之下,朱砂深保持——问仍是唯一彩色声音);附图缩略行(2026-09-09 multimodal-image B4):64px 方界 tile 规线细框 + wrap 居中多行 + 点击放大浮层,几何 token `--pp-ch-userImages-*`(measure 镜像 `userImagesRowHeight` wrap 行数整除推得);未命中预览/读失败降级 mono 占位盒 |
| `markdown` | 正文 | `--f-song` | `--ink-1` | 17px / 行距 2.0,居中主角 |
| `reasoning` | 夹注 | `--f-song`(英文走 Garamond) | `--graphite` | 86% 宽、虚线勾边、擦改痕迹 |
| `diff` | 抄录 | `--f-mono` | `--ink-1` | 3px 硬左线 + 米黄底图版,add 松绿(--pass)/ del 朱砂深+删除线(B5 环2 红绿墨色化定稿——铁律在 diff 语境豁免:删行读作擦改非人语);**顶层 markdown 围栏一律落本类**(2026-09-23):语言标记留在 `.pp-lang` 语言签,块体按语言分流——真差分(diff/patch)走 +/- 着色,其余语言与裸围栏走 hljs 代码体(几何同源,测高不因分流漂) |
| `tool` | 脚注 | `--f-mono` | `--indigo` | 44px 注线 + 11–11.5px 小字;卡内载荷 = 语义投影(见 §4.1a),不再是协议 JSON |
| `plan` | 拟策 | `--f-song` | `--ink-1` | 石青策面(2026-09-10 拟策卡渲染专项,用户拍板):顶硬线 + 底软线保留 + 3px 石青硬左线 + 石青淡底(indigo 4% 兑纸面)+ 横向内距 16——与来文的朱砂左线对仗(人声朱 / 机策青);内容走完整 markdown 体(`.pp-pc-body` + `.pp-md` 族共享,正文位 17px/2.0),行号机制(01/02/… 石青序号)退役;方案区 + 钤印三钮不变 |
| `notice` | 贴黄 | `--f-song` 小字 | `--warn` | 系统通知(建议作「贴黄」纸条) |

> `notice`(系统通知)原型未覆盖,建议按「贴黄」(古代奏章上贴的黄纸条)处理:小字、土黄底、一条弱规线,与正文明确区分。

### 4.1 正文(markdown)子元素版式与折叠机制(2026-08-30 补记)

正文块(`markdown`)不再是纯文本平铺——子元素按注疏版式降级渲染;结构模型 = `paper/markdown.ts` 单一解析(渲染与测量共用,块高镜像 `measure.ts` 的 `MD_*` 常量,CSS 镜像 `.pp-md-*`,改版式三处同步):

| 子元素 | 版式 |
|---|---|
| 段落 | 17px/2.0,段间 14px(旧 10px 段距小于行距、段落黏连,专项提到 14) |
| 标题 h1–h4 | 20/18/16.5/15.5px 正文位加粗,padding 承担上下距(首元素 margin 逃逸会破坏测高,禁 margin-top) |
| 列表 | 标记列等宽小字绝对定位(ul `·` / ol `n.`),条目缩进 26、嵌套再缩进 22 |
| 引用 | 石墨左线 + 缩进 + ink-2 |
| 图码 | 石青左线 + 深纸底(**列表/引用内**嵌套围栏 + mermaid 回落的兜底面——顶层围栏 2026-09-23 起升格为独立抄录块,见上表 `diff` 行) |
| 正文图(2026-09-09 远端 B4;2026-10 本地路径扩展 + 点击放大) | 独立行 `![alt](…)` 固定盒 160px(`--pp-md-imgBoxH`,chem 先例同族——加载/失败态都不改版面,measure 静态镜像即精确);图源分类过在解析层(`imageSrcRef` 单一真源):http/https 远端由浏览器直载;**本地绝对路径(盘符/UNC/POSIX 与 `file://` URI 归一)仅图片扩展名(`VIEWER_IMAGE_EXTS`)放行**,渲染期经 `fs_cap read_base64`(用户路径,8MiB 上限)回读成 data URI(块只携路径);非白名单(data:/相对路径/非图扩展名)降级 alt 文本,空 alt 整行不产块——data: 巨串不灌纸面;周框 `--rule-soft` + 深纸底;失败换 mono 弱墨行(远端=alt 文本、本地=「图片不可读:文件名」带原因 title——盒界常在,错误不静默);**点击看大图**:盒内图为按钮(`.pp-md-img-zoom` 无装饰重置 + 光标 zoom-in + title/aria 提示)→ 用户气泡/资产卡同款全局浮层(`.pp-media-preview-overlay`,Esc/遮罩点关) |
| 表格 | 等宽 11.5px + 弱规线(`.pp-grid-table` 同族) |
| 行内 | 加粗 600 / 斜体 / 删除线石墨 / 行内码等宽小号 chip / 链接石青下划线 |

**块体白空格纪律**:`.pp-body` 恒 `white-space: pre-wrap`——measure.ts 的 canvas 测量一律按 pre-wrap 计行,渲染端折叠换行会产生「测得多、渲染少」的幻影块间距(2026-08-30 间距根因之一,已修)。

**折叠机制**(`paper/fold.ts`;规则态 + 用户覆盖态):夹注(reasoning)恒折叠——流内只留一行预览;脚注(tool)/程文(code)按状态折叠——`running`/`error` 展开(过程与错误可见),`done`/`pending` 收起(完成即收);折叠行 = 块顶等宽小字(单工具 `▸ 名字 判别摘要 · 读数` / `▾ 收起`),用户点击写入覆盖态(状态翻转自动收回不打架)。**读数** = 语义投影的 `summary`(`42 行` / `9 处 · 1 文件` / `17 个改动` / `exit 0 · 110s`,见 §4.1a;无 summary 回落「输出 N 字」),名字走领域显示名 `fs(read)`。**判别量**(stream-rhythm 刀4a,2026-09-03):压缩的是重复不是信息——工具组折叠行露判别字段(`▸ 工具 ×3 · read_file src/a.ts / src/b.ts / src/c.ts`;shell 露命令;截断/同值去重补 ×N 总账;无判别字段回退名字计数),折叠行恒一行(10px mono,测高镜像零变化);**版面契约(2026-10-29 实机回归)**:`.pp-fold` 必须 `width: 100%`(`