dsh-fonttune 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.en.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.2.1] - 2026-09-16
6
+
7
+ ### Fixed
8
+ - **"Turning following off leaves the interface weight at the followed value, and reset does nothing"** (reproduced on a real machine): the stylesheet reaches the page **twice** — the host half renders one copy into the served index for the first frame (`webserver/index-inject`) and the browser half keeps a second one — and the served copy is only rebuilt on the **next index render**. With both present, adding or changing a rule is fine (the browser half's copy comes later and wins at equal specificity), but **removing a rule cannot work**: turn following off, or reset the interface weight, and the browser half's fresh stylesheet no longer contains that `font-weight` rule while the served copy still does — so the interface stayed at the followed 480 until the page was reloaded. Two changes: ① the host half now emits an **`html` row** (`<style data-plugin="dsh-fonttune" data-plugin-css="dsh-fonttune">…</style>` instead of an un-stamped `style` row) and the browser half **adopts that element** instead of appending its own (`createStylesheet` looks it up first, falls back to creating one, and re-creates if it ever leaves the document) — one element for the element's whole life, so rules can be added *and* removed. Verified live: the new "turn following off without reloading" step in `test/weight-verify.mjs` shows the interface going 480 → 400 (DSH's own) immediately, the stylesheet count staying at `count=1`, and the served index really carrying the stamped `<style>`.
9
+ - **Switching from following to not-following no longer bounces the interface back to its own old value**: turning the switch off used to just flip the flag, so the interface fell straight back to whatever it had stored (usually nothing → DSH's default) while the slider still read 400 and disagreed with the page. Now **the values the interface was showing are written into its own two fields as the switch goes off** (the family from the conversation, the weight from the conversation — the effective values `resolveAxes` expanded), so switching off changes nothing visually and the sliders continue from there; an axis the conversation does not set (empty family, unset weight) clears the interface's own field instead, keeping DSH's default. The expected walkthrough is therefore: following at 480 → switch off → the interface is still 480 and the slider reads 480 → adjust it, or reset for 400.
10
+ - **The interface weight is back, and it genuinely cannot reach the conversation**: 0.2.0 retired the whole axis because "a weight can only be written as `body, body *`, which cannot avoid the conversation". This round measured that rule on a live DSH page and found the isolation point: **exclude the whole conversation markdown subtree (and the code surfaces) from the rule with `:not()`**. Measured against computed styles on a live DSH page: with the exclusion, `body, body *` takes every interface text element to the chosen weight and **moves not one element inside the markdown subtree**; without it, the same configuration drags the conversation along. The interface weight is now an independent axis in the **Interface** section (it appears when following is off), 300–600, and its readout shows 400 like the others. The code side gets its fallback back too: once an interface weight is set, code surfaces without a weight of their own are explicitly reset to `normal` (which is what DSH computes there), so code blocks are not dragged along either.
11
+ - **Every "follows the interface" note in the conversation section is gone**: the follow direction has been one-way (the interface follows the conversation) for a while, but four small hints still claimed "unset follows the interface offset / weight / ratio" and "empty follows the interface font". All four now describe what actually happens ("0 keeps DSH's sizes", "unset keeps DSH's own weights", "100% keeps DSH's own line heights"), and a regression test now refuses the strings "跟随界面" / "follows the interface" in those four entries in both dictionaries. The interface section's follow row gained a "Font and weight" hint line naming what the switch covers.
12
+ - **The follow switch now covers the font and the weight**: on (the default), the interface takes the conversation's **font and weight** and its own two controls stay hidden; off, the interface uses its own font and weight and neither side can disturb the other. While following, a conversation that sets no weight still falls back to the interface's own stored value (the same fallback the family uses), so upgrading loses nothing.
13
+ - **The card's previews no longer lie**: the interface weight is an `!important` page-wide rule, so it used to restyle the card's own preview boxes as well. The conversation preview now carries a `dfp-previewDialog` marker, is excluded from the interface rule and receives the conversation weight on its own; the code preview stays on the code axis (it is already in `CODE_SELECTOR`) and the interface preview stays inside the interface rule — all three previews now match the surface they simulate.
14
+
15
+ ## [0.2.0] - 2026-09-16
16
+
17
+ ### Added
18
+ - **Three typography sets**: typography grows from two sets (body / code) to **three** — the **conversation** (markdown paragraphs, tables, headings) owns **every axis** (font, size, line height, weight); the **interface** (settings sheet, sidebars, workspace, headings and buttons) offers **a font only** and **follows the conversation by default** (switchable); **code** stays its own axis. The conversation is scoped through the markdown container (`[class*="_markdown_" i]` plus `:not()` complex selectors that exclude code surfaces) instead of hashed class names.
19
+ - **A conversation size axis**: -3 to +6 px, added **on top of the official "conversation font size"** (`--dsh-content-font-size`), which stays the base. The offset is added once, at the **source**: `--dsh-content-font-size` becomes "official value + offset", and the secondary size, both deltas and every delta-derived heading size and line height follow by derivation — paragraphs, headings and heights move together, with nothing counted twice. The official slider keeps working (its changes arrive through `theme/change`).
20
+ - **A conversation line-height axis**: a ratio of 100%–160% (step 5) that rebuilds the inline heights inside the markdown `font` shorthands; the **code line height** is its own additive offset (-4 to +8 px), composing with the code size offset.
21
+ - **Code ligatures**: a three-mode switch (default / on / off) writing `font-variant-ligatures` — browsers enable them by default, so this is the switch for turning them off — plus a sanitized advanced `font-feature-settings` value field (`"ss01" on, "cv01" 1` shape).
22
+ - **Refuse synthetic styles**: stops synthetic italic and synthetic bold for faces without them (`font-synthesis-style` / `font-synthesis-weight`); simple mode is one switch for both, advanced mode two independent switches.
23
+ - **The interface follows the conversation (switch)**: the conversation is the full set while the interface has a font, so **the interface follows the conversation by default** — the conversation section has no switch and all four of its axes stay adjustable, and the interface section leads with a "Follows the conversation" switch (on by default) whose font control appears only once it is switched off (the interface's own value stays as the fallback meanwhile). Because an unset conversation font falls back to the interface's, **existing configurations look exactly as before**.
24
+ - **Presets**: a **dropdown select** right under the edit mode (the card's own down chevron inside the box), with five built-in presets (默认配置 1 ~ 5) out of the box — picking one applies it, and **every change afterwards auto-saves into the selected preset** (no separate save step). "Rename / Export / Import" manage the list: renaming only changes the name and keeps the stored values, exporting copies JSON and importing merges by name (imports run the same sanitizer pipeline, capped at 20). Presets and the active preset name live in the host settings document, so they travel across browsers. Operation results ("copied to the clipboard") appear on the right of the row below and **fade out by themselves after about 2.6 s**, without moving any layout.
25
+ - **Card information architecture**: top to bottom the open card becomes an edit-mode row, a **preset bar** and three **accordion sections** — **conversation → interface → code** (the conversation leads because it owns every axis) — all collapsed by default, each title row carrying a summary with the chevron pinned to the far right, at most one open at a time; "refuse synthetic styles" is one or two switches, so it **no longer earns an accordion** and sits flat below the three sections. Expanding a section ends with that section's own **inline preview box** (rendering just that part; a floating variant was tried first and reverted after real-machine feedback). **Row format lines up with dsh-quick-toc**: every setting is a field row with 14px vertical padding (was 16px), every on/off control is the joined "one box split in two" segmented control (28px tall, was a 22px pill switch), and the control sits in the same row as its label, right-aligned (`.dfp-inline`, the same shape as quick-toc's `.dqt-pinline`) — so the two cards' rows and controls align.
26
+ - **The interface size / line-height / weight axes are RETIRED (measured)**: DSH exposes **no interface size hook** — the settings sheet, both sidebars and the workspace size their text with literals in upstream CSS (22 literals in settings-plugins, 18 in workspace, 6 in sidebar, **0** uses of the interface ladder tokens), and the ladder tokens (`--dsw-font-xs-13`, `--dsw-font-s-14`, …) are consumed only by 6 conversation-area packages (tool / trajectory / attachment / chat / conversation / cordis). The only official size hook is the conversation one. Scaling the ladder from these fields could not reach the settings sheet or the sidebars, while it reached the **conversation** through `--dsh-content-font-size` — the root cause of "I change the interface and the conversation moves". Weight is worse: it can only be expressed as a `body, body *` rule, which cannot avoid reaching the conversation either (narrowing it off the markdown subtree would also strip the theme's own heading weights). So **size, line height and weight belong to the conversation alone**, and the interface keeps the one axis that measurably works: its font (a page-wide `body` rule plus the theme variable). The `sizeOffset` / `lineHeight` / `weight` fields stay in the schema (old documents and presets keep parsing) but inject nothing at all.
27
+ - **Per-theme values ship out of this release**: light and dark share one value set — the top-bar per-theme switch and the light/dark editor are removed (the dark fields stay in the host schema but dormant, and rendering ignores them), to be re-evaluated in a later release.
28
+
29
+ ### Fixed (real-machine walkthrough)
30
+ - **Preset messages no longer linger, and no longer move the layout**: a note like "Presets copied to the clipboard." used to sit in the card forever and, as a fresh line, push the sections below it down. It now **shares one row** with "With a preset selected, every change saves into it automatically": the message fades in on the **right** of that row and **fades out by itself after 2.6 s** (a 0.24 s opacity transition, then the text is cleared). The row is **always mounted** and exactly one line tall, and the message is clamped with `nowrap + ellipsis` — so appearing and disappearing never changes the row height and nothing below ever moves. Two identical actions in a row still work (each message carries an incrementing id, and the auto-hide timer restarts), and an over-long message (the clipboard-unavailable fallback, say) stays readable on hover through its `title`.
31
+ - **"I change the interface and the conversation moves"**: the interface size axis scaled the conversation's official content-size chain (`--dsh-content-font-size` / the secondary size) alongside the interface ladder, and every markdown size derives from that chain — so an interface edit really was a conversation edit. The interface axis is now explicitly barred from it (`bodyTokenNames` excludes `--dsh-content-font-*`), and the interface size axis itself is retired for the reason above.
32
+ - **The conversation size offset used to move headings but not paragraphs**: the old implementation only rewrote the delta tokens and literal sizes, while paragraphs take their size from `var(--dsh-content-font-size,14px)` — and `var()` inside a custom property resolves where the property is **declared**, so a deeper scoped override cannot reach it. The new implementation declares `--dsh-content-font-size` as "official value + offset" at the source, so paragraphs, headings and line heights all follow.
33
+ - **Dark overrides used to be shadowed by the light values**: the per-theme resolver merged the dark map over an **already-expanded** value set, so as soon as the follow rule changed direction the borrowed values could overwrite the dark ones. The dark overrides are now merged over the raw values and expanded afterwards, so a dark font can no longer be clobbered by a light value.
34
+ - **Weight readouts read 400**: an unset weight used to print "unset" while the slider sat at 400 and the hint said 400 keeps DSH's own — three statements that disagreed. An unset weight now reads **400** (conversation and code weights both).
35
+ - **The % vanished while dragging the line-height slider**: the pending readout had no unit and only gained one on release — the pending readout now keeps the `%` too (the size / code-height `px` prefixes already did).
36
+ - **The conversation section showed English "variable names"**: the dialog axis label keys (`dialog.*`, `size/line/weight.dialogLabel`…) were missing from the bilingual dictionaries, so `translate` fell back to the key names — both dictionaries are complete now.
37
+ - **The first control sat too far from the section title**: field rows carry 16px top padding on top of the section body — the first field of a section body is tightened to 10px.
38
+ - **The chevron left the far right edge when expanded**: with the summary gone nothing pushed it there — an expanded section renders a flexible spacer instead, so the arrow stays pinned in both states.
39
+
40
+ ### Changed
41
+ - **Family variables ride the official theme layer**: `ctx.theme.overrideTokens` registers `--dsw-font-family` and `--ds-font-family-code`/`--dsw-font-mono` as `{light, dark}` pairs, so the presenter owns them on body's inline style after every theme switch — the families no longer depend on stylesheet timing against the token rewrite. The `theme/change` event re-reads the tokens and repaints immediately (the official font-size preference flows through it), with the 4-second poll kept as a fallback.
42
+ - **Compatibility table** lists 0.2.0 as the latest version; the README is restructured around the three typography sets and presets, and `docs/` now holds the 0.2.0 research and specification records (not shipped in the package).
43
+
5
44
  ## [0.1.6] - 2026-09-15
6
45
 
7
46
  ### Fixed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  **dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.2.1] - 2026-09-16
6
+
7
+ ### 修复
8
+ - **「跟随关掉后界面字重不回落,重置也没用」**(真机复现):界面样式表在页面里其实有**两份**——host 半随首页下发一份做首帧(`webserver/index-inject`),客户端半再维护一份,而**下发的那份只在下次首页渲染时才重建**。两份都在时「新增/改值」没问题(客户端那份在后面,同特异性后写的赢),但**「删掉一条规则」不可能生效**:关掉跟随、或把界面字重重置,客户端下发的新样式里已经没有那条 `font-weight` 规则,而首页那份旧规则还挂着,于是界面继续是跟随时的 480,直到刷新页面。修法两步:① host 半改发 **`html` 行**(`<style data-plugin="dsh-fonttune" data-plugin-css="dsh-fonttune">…</style>`,而不是不带属性的 `style` 行),客户端半启动时**认领这个元素**、不再另建一个(`createStylesheet` 先 `querySelector` 再兜底自建,并检查 `isConnected`)——之后页面里永远只有一个该元素的实例,规则可增可删。真机验证:`test/weight-verify.mjs` 新增「不刷新页面把跟随关掉」一步,界面 480 → 400(DSH 自己的值)立刻生效,样式表元素始终 `count=1`,且首页源码里确实带那个打标的 `<style>`。
9
+ - **从跟随切到不跟随,界面不再「跳回自己的旧值」**:以前关闭跟随只是把开关改掉,界面立刻回落到它自己存过的值(多半是空 → DSH 默认),而滑块读数还是 400 与实际不符。现在**关闭的那一刻把界面正在显示的值写进它自己的两个字段**(字体取对话的字体、字重取对话的字重,都是 `resolveAxes` 展开后的有效值),所以关掉开关视觉上零变化、滑块也从当前值继续调;对话没设的那一项(空字体、未设字重)则清掉界面自己的字段,保持 DSH 默认。真机走查的期望由此变成:跟随时 480 → 关掉跟随 → 界面仍是 480 且滑块显示 480 → 手动改或「重置」才回到 400。
10
+ - **界面字重回来了,而且真的碰不到对话**:0.2.0 把界面字重整条退役,理由是「字重只能写成 `body, body *`,无法不波及对话」。这次在真机页面上把这条规则量了一遍,找到了隔离点:**把对话 markdown 整棵子树(以及代码表面)从这条规则里 `:not()` 排除**。实测(活的 DSH 页面、计算样式):`body, body *` 加排除后把界面的文本元素全部改到目标字重、**markdown 子树内的元素一个都没动**;若不加排除,同一份配置会把对话也一起拉动。界面字重现在是「界面」分区里的独立一轴(跟随关闭时出现),范围 300 ~ 600、读数同样显示 400。顺带给界面字重补上代码侧的回退:界面字重一旦设置,未单独设字重的代码表面会显式写回 `normal`(DSH 原本就是这样算的),所以代码块不会被界面字重带跑。
11
+ - **对话分区的“跟随界面”描述全部改掉**:跟随关系早已是单向的(界面跟随对话),但对话分区的四条小字仍在写「未设置时跟随界面偏移 / 字重 / 比例 / 留空表示跟随界面字体」。现在四条都改成如实描述(「0 表示保持 DSH 原样」「未设置时保持 DSH 原本的粗细」「100% 表示保持 DSH 原样」等),并加了回归测试:中英两份词典的这四条文案里不允许再出现「跟随界面 / follows the interface」。界面分区的跟随开关下面新增一行小字「字体与字重」,说明这个开关管的是哪两项。
12
+ - **跟随开关现在管字体和字重两项**:开启(默认)时界面沿用对话的**字体与字重**,界面自己的两个控件隐藏;关闭后界面用自己的字体与字重,两边互不影响。跟随开启时若对话没设字重,界面仍回落到自己存过的值(与字体同一条兜底规则),因此老配置升级后不会丢值。
13
+ - **卡片预览不再说谎**:界面字重是 `!important` 的全页规则,会连卡片自己的预览框一起刷成界面字重。给对话预览加了 `dfp-previewDialog` 标记并把它排除在界面规则之外、单独下发对话字重;代码预览沿用代码轴(已在 `CODE_SELECTOR` 里),界面预览留在界面规则内——三块预览的颜色/字重现在各自对应真实表面。
14
+
15
+ ## [0.2.0] - 2026-09-16
16
+
17
+ ### 新增
18
+ - **三套独立排印**:排印从「正文/代码」两套扩成**三套**——「**对话**」(会话 markdown 正文、表格、标题)拥有**全部轴**(字体、字号、行高、字重);「**界面**」(设置页、侧栏、工作区、标题与按钮)只有**字体**一项,且**默认跟随对话**(可关);「**代码**」独立成轴。对话的实现走 markdown 容器作用域规则(`[class*="_markdown_" i]` + 排除代码表面的 `:not()` 复合选择器),不碰哈希类名。
19
+ - **对话字号轴**:-3 ~ +6 px,**加在官方「对话字号」(`--dsh-content-font-size`)之上**,官方数值始终是基准。实现只在**源头加一次**:`--dsh-content-font-size` 变成「官方值 + 偏移」,次级字号、delta、delta 派生的标题字号与行高全部按派生关系自动跟随——段落、标题、行高一起动,且不会重复计入。官方字号滑块的任何改动仍由 `theme/change` 即时跟随。
20
+ - **对话行高轴**:倍率 100% ~ 160%(step 5),重建 markdown 的 `font` 简写内联行高;**代码行高**是独立的加法偏移(-4 ~ +8 px),与代码字号偏移复合。
21
+ - **代码连字**:三档开关(默认 / 开启 / 关闭)写 `font-variant-ligatures`(浏览器默认即开启,这是给想关掉的人的开关),高级模式另有经消毒的 `font-feature-settings` 值输入(`"ss01" on, "cv01" 1` 形态)。
22
+ - **拒绝合成样式**:禁用浏览器为无斜体/无粗体字面伪造的伪斜体/伪粗体(`font-synthesis-style` / `font-synthesis-weight`);简单模式一个开关同时禁用两者,高级模式两个独立开关。
23
+ - **界面跟随对话(开关)**:对话是「全轴」的那一套,界面只有字体一项,所以**默认由界面跟随对话**——对话分区没有开关、四项始终可调;界面分区顶部是「跟随对话设置」开关(默认开启),开启时界面沿用对话的字体(字体控件隐藏,界面自己的旧值留作兜底),关闭后出现界面自己的字体控件。对话未设置字体时界面回落到自己的值,因此**老配置升级后视觉零变化**。
24
+ - **预设方案**:编辑模式正下方的**下拉选择框**(框内右侧是与卡片一致的下拉箭头),内置五个默认配置(默认配置 1 ~ 5),点开选择即应用;**选中方案后所有改动自动存入该方案**(自动保存,无需手动「保存当前」)。右侧「改名 / 导入 / 导出」管理方案——改名只改名字、已存数值原样保留,导出复制 JSON、导入按名合并(导入走同一条消毒管线,最多 20 个)。方案与当前选中的方案名都存宿主设置文档,跨浏览器跟随。操作结果(如「已复制到剪贴板」)显示在下行的右侧并**约 2.6 秒后自动淡出**(不顶动任何布局)。
25
+ - **卡片信息架构重构**:卡片展开后自上而下是编辑模式行 + **预设条** + **对话 → 界面 → 代码**三个**手风琴分区**(对话排在最前,因为它拥有全部轴),默认全部收起,标题行显示当前值摘要、箭头固定在最右,同一时刻最多展开一个;「拒绝合成样式」只有一两个开关,**不再单独占一个手风琴分区**,直接放在三个分区下面。分区展开后各自在分区末尾出现一个**独立预览框**(只渲染该分区的部分;曾试过悬浮式,真机体验不佳后改回内联)。**行格式与 dsh-quick-toc 对齐**:每项设置是 14px 上下内边距的字段行(原为 16px),开启/关闭一律用「大框切两半」的联合分段控件(28px 高,原为 22px 的胶囊开关),标签在左、控件在同行的右侧(`.dfp-inline` 与 quick-toc 的 `.dqt-pinline` 同构),因此两张卡片的行高与控件对齐一致。
26
+ - **界面的字号 / 行高 / 字重三轴退役(实测结论)**:DSH **没有界面尺寸接口**——设置页、左右侧栏与工作区的文字尺寸是上游 CSS 里的字面量(settings-plugins 22 处、workspace 18 处、sidebar 6 处 `font-size`,**0 处**使用界面阶梯 token),而界面阶梯 token(`--dsw-font-xs-13` / `--dsw-font-s-14` 等)只被会话区的 6 个组件(tool / trajectory / attachment / chat / conversation / cordis)消费;唯一的官方字号接口是「对话字号」。此前用界面阶梯去缩放,既够不到设置页与侧栏,又会顺着 `--dsh-content-font-size` 把**对话**改了;字重更棘手——它只能表达为 `body, body *` 全页规则,无法不波及对话(收窄到 markdown 之外又会连 DSH 给标题的字重一起剥掉)。结论:**字号、行高、字重全部归对话独占**,界面只保留实测生效的字体(`body` 全页 + 主题变量)。`sizeOffset` / `lineHeight` / `weight` 字段保留在 schema 里(旧文档与预设继续解析),但**不再下发任何规则**。
27
+ - **分主题数值移出本版**:深浅色在本版共用同一套数值——顶栏的分主题开关与浅/深编辑切换整体移除(宿主 schema 里的深色字段保留但休眠,渲染层强制忽略),功能留到下个版本再评估。
28
+
29
+ ### 修复(真机走查)
30
+ - **预设提示不再常驻、也不顶动布局**:「方案已复制到剪贴板。」这类提示以前会一直挂在卡片里,且作为新的一行把下面的分区顶下去。现在它和「选中方案后,所有改动自动存入该方案。」**共用一行**:提示出现在该行**右侧**,**2.6 秒后自动渐隐**(透明度过渡 0.24s,之后清空文本)。那一行**始终挂载**且只有一行高,提示文本用 `nowrap + ellipsis` 限宽——出现与消失都不会改变行高,因此下方内容一动不动。连续两次相同操作也不会失灵(消息带自增 id,自动隐藏的定时器会重置),超长文本(如剪贴板不可用时的回落内容)可在悬停时通过 `title` 看全文。
31
+ - **「改界面,动的却是对话」**:界面字号轴此前在缩放界面阶梯 token 的同时,也会缩放对话专用的官方内容字号链(`--dsh-content-font-size` / 次级字号),而 markdown 的每一级尺寸都由这条链派生——所以调界面字号看起来就是在调对话。现在界面轴被明令隔离在这条链之外(`bodyTokenNames` 排除 `--dsh-content-font-*`),并且界面字号轴本身也因「没有接口」而退役(见上)。
32
+ - **对话字号偏移过去只影响标题、不影响段落**:原实现只改写 delta 与字面量字号,而段落的字号取自 `var(--dsh-content-font-size,14px)`(`var()` 在自定义属性里于**声明处**求值,深层作用域覆盖无效),所以段落纹丝不动。新实现改为在源头把 `--dsh-content-font-size` 声明为「官方值 + 偏移」,段落、标题、行高一起跟随。
33
+ - **暗色主题下深色覆盖会被浅色值盖掉**:按主题解析时,深色映射此前是合并在**已展开**的取值上,跟随规则一改就会出现互相覆盖。现在先在原始值上合并深色覆盖、再做展开,深色设的字体不会再被浅色值顶掉。
34
+ - **字重读数显示 400**:未设置的字重以前读数写「未设置」,而滑块其实停在 400、提示也写「400 表示保持 DSH 原样」——三者不一致。现在未设置的读数就是 **400**(对话字重与代码字重同此)。
35
+ - **行高滑块拖动时 % 消失**:拖动中的待定读数没带单位,松手才出现——现在拖动读数同样显示 `%`(字号/代码行高的 `px` 前缀原本就带)。
36
+ - **对话分区全是英文“变量名”**:对话轴的字段标签键(`dialog.*`、`size/line/weight.dialogLabel` 等)没有进双语词典,`translate` 回退显示键名——已补齐中英两份。
37
+ - **手风琴展开后首行离标题太远**:字段行自带 16px 顶部内边距叠加在分区体上——分区体的首个字段收紧到 10px。
38
+ - **展开态箭头不在最右**:展开后摘要消失导致箭头滑到标题旁——展开时渲染一个弹性占位符,箭头两种状态下都钉在最右。
39
+
40
+ ### 变更
41
+ - **字体族写入接入官方主题层**:`ctx.theme.overrideTokens` 注册 `--dsw-font-family` 与 `--ds-font-family-code`/`--dsw-font-mono` 的 `{light, dark}` 对,presenter 在每次主题切换后把数值内联到 body——字体族不再依赖样式表与轮询的时序。`theme/change` 事件即时重读 token 并重涂(官方字号行的变更也在其中),4 秒轮询保留作兜底。
42
+ - **兼容表**改列 0.2.0 为最新版本;README 重写为三套排印 + 预设的结构;新增 `docs/`(0.2.0 的调研与方案记录,不随包发布)。
43
+
5
44
  ## [0.1.6] - 2026-09-15
6
45
 
7
46
  ### 修复
package/README.en.md CHANGED
@@ -2,25 +2,30 @@
2
2
 
3
3
  > [中文](README.md) | **English**
4
4
 
5
- **Font plugin for DeepSeek Harness (DSH)**: set the body and code font families, a font size and a font weight for each, and pick fonts per **Western / CJK** slot. Everything lives in the native card under **Settings → Plugins → Plugin configuration** and applies immediately.
5
+ **Font plugin for DeepSeek Harness (DSH)**: the **conversation** (markdown prose) gets full typographic control — font, size, line height, weight; the **interface** (settings sheet, sidebars, workspace, buttons) takes a font and a weight and **follows the conversation by default**; **code** stays its own axis with ligature and feature switches; and whole setups can be saved as **presets**. Everything lives in the native card under **Settings → Plugins → Plugin configuration** (collapsed sections with summaries) and applies immediately.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Body / code fonts** — two independent fallback lists, Latin families first and CJK after; leaving one empty keeps DSH's own stack untouched
10
- - **Body font-size offset** — -3 ~ +6 px for text and interface sizes, stacked on top of DSH's own font-size setting rather than replacing it; every size step scales together with its line height
11
- - **Code font-size offset** — -3 ~ +6 px for code blocks and inline code, **independent** of the body offset: moving one never moves the other
12
- - **Body font weight** — any integer from 300 to 600, overriding the weight of text and interface copy including headings; unset or 400 keeps DSH's own weight
13
- - **Code font weight** — any integer from 300 to 600 for code blocks, inline code and terminal output only; unset or 400 keeps DSH's own weight and keeps code out of the body weight
14
- - **West / CJK split (simple mode)** — a switch at the top of the card: simple mode gives just a Western and a CJK single-pick slot (one pair for body, one for code), advanced mode is the full stack editor; both views share one stack, so toggling never changes the order you set up
9
+ - **Three typography sets** — the conversation (markdown paragraphs, tables, headings) comes first and owns every axis; the interface (settings sheet, sidebars, workspace, headings and buttons) offers **a font and a weight** and follows the conversation by default (switchable); code stays its own axis
10
+ - **Interface / conversation / code fonts** — three independent fallback lists, Latin families first and CJK after; leaving one empty keeps DSH's own stack untouched
11
+ - **Font-size offsets** — the **conversation** takes -3 ~ +6 px on top of DSH's own "conversation font size" (the official value is the base), moving paragraphs, headings and line heights together; code is its own axis at -3 ~ +6 px. **The interface has no size axis**: verified against DSH, the settings sheet, sidebars and workspace size their text in upstream CSS, and the only official size hook is the conversation one
12
+ - **Font weights** — the **conversation**, the **interface** and **code** each take any integer from 300 to 600 (unset reads 400 in the UI). The interface weight is one `body, body *` rule with the whole conversation markdown subtree excluded: measured against computed styles on a live page, it takes every interface text element to the chosen weight and moves nothing inside the markdown subtree, so **changing the interface weight never touches the conversation** (its bold text and heading weights survive too)
13
+ - **Line heights** — the **conversation** uses a ratio (100% ~ 160%), code an additive offset (-4 ~ +8 px); the interface has no line-height axis, for the same reason as size
14
+ - **Code ligatures** — a three-mode switch (default / on / off; browsers enable them by default, turning them off restores `=>` and `!=` as plain characters), plus an advanced `font-feature-settings` value field
15
+ - **Refuse synthetic styles** — stops the browser from faking italic and bold for CJK faces, which have neither, so marked text stays upright
16
+ - **The interface-follows-conversation switch** — a "Follows the conversation" switch at the top of the interface section (on by default): on, the interface takes the conversation's **font and weight** and its own two controls stay hidden; off, it uses its own font and weight, independent of the conversation. Size and line height belong to the conversation only (the interface has no hooks for them)
17
+ - **Presets** — a dropdown select right under the edit mode: five built-in presets out of the box, pick one to apply it; with a preset selected every change auto-saves into it, and "Rename / Export / Import" manage the list (export as JSON, import-merge a paste)
18
+ - **West / CJK split (simple mode)** — an edit-mode switch: simple mode gives just a Western and a CJK single-pick slot per set, advanced mode is the full stack editor; both views share one stack, so toggling never changes the order you set up
15
19
  - **Font picker panel** — four preset groups (monospace / CJK / Latin / generic), plus an "installed on this machine" group on Chromium; a name that is not in the list can be created with "use xxx"
16
- - **Drag to reorder** — selected families are a chip list you can drag, with earlier / later buttons kept for keyboard and touch
17
- - **Live preview** — a mixed Chinese/English line and a code line, rendered from the current configuration
20
+ - **Drag to reorder** — picked fonts are draggable chips with earlier/later buttons too (keyboard and touch friendly)
21
+ - **Per-section previews** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part with the current configuration
18
22
 
19
23
  ## Compatibility
20
24
 
21
25
  | Plugin version | Supported DSH versions |
22
26
  | --- | --- |
23
- | **0.1.6** (latest) | 0.1.5-rc.2 |
27
+ | **0.2.1** (latest) | 0.1.5-rc.2 |
28
+ | 0.2.0 | 0.1.5-rc.2 |
24
29
 
25
30
  `engines.dsh` requires **0.1.5-rc.2** or newer, and the compatibility declaration lists only the **latest release of the 0.1.5 line**: it carries every fix in that line, which makes it the most stable build of the major version, while older builds of the same major version stay compatible per semantic versioning. Earlier or newer DSH versions are untested and not declared. On install or update, the DSH market runs a host-compatibility preflight against `engines.dsh`, `dsh.compatibility.dshReleases` and `peerDependencies` in `package.json`.
26
31
 
@@ -49,18 +54,25 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
49
54
  ## Usage
50
55
 
51
56
  - **Open the settings** — Settings → Plugins → Plugin configuration → expand the "Font tune" card
52
- - **Pick fonts** — in simple mode click the four slots (Body · Western, Body · CJK, Code · Western, Code · CJK); in advanced mode use "Add font" and the chip editor to maintain the full fallback list
53
- - **Tune sizes and weights** — each slider sits under the thing it changes: body font → body size offset → body weight → code font → code size offset → code weight. Drag and release to apply. Changed fields are marked and can be reset individually, with "Reset all" at the bottom of the card
54
- - **Preview** — the mixed-script and code preview at the bottom of the card renders with the current configuration
57
+ - **Section navigation** — top to bottom: edit mode, presets, then the three collapsed sections **conversation → interface → code** (the conversation leads because it owns every axis), with "refuse synthetic styles" flat below them; each title row carries a summary of the current values with the chevron pinned to the far right, and at most one section is open at a time
58
+ - **Pick fonts** — in simple mode click the Western/CJK slots; in advanced mode use "Add font" and the chip editor to maintain the full fallback list
59
+ - **Tune size / weight / line height** — each slider sits inside its own section; drag and release to apply (the readout keeps its unit while dragging). Changed fields are marked and can be reset individually, with "Reset all" at the bottom of the card
60
+ - **The conversation leads** — the conversation section sets font, size, line height and weight; the interface section shows a single "Follows the conversation" row by default, and its own font and weight controls appear once that is switched off
61
+ - **Switching following off never jumps** — the moment the switch goes off, the interface writes the font and weight it was **already showing** into its own fields (that is what it was following), so nothing changes on screen and the sliders continue from there; an axis the conversation does not set clears the interface's own field instead, keeping DSH's default. "Reset" on that field goes back to DSH's own weight (the readout returns to 400)
62
+ - **The interface weight cannot reach the conversation** — it is the one axis that has to cover the whole page (DSH pins its small labels' weights in upstream class rules, which nothing narrower can reach), so it is implemented as a rule with the **whole conversation markdown subtree excluded**: paragraphs, headings and bold text in the conversation are untouched
63
+ - **Weight readouts** — an unset weight reads 400 (DSH's own body weight, which is where the slider sits); no "unset" wording appears anywhere
64
+ - **Presets** — the dropdown select below the edit mode (默认配置 1 ~ 5, with the card's own chevron inside the box); picking one switches and auto-saves the edits that follow. "Rename" renames the current preset, "Export" copies JSON and "Import" merges a paste. Operation results ("copied to the clipboard") appear on the **right** of the row below and fade out by themselves after about 2.6 s, without moving any layout
65
+ - **Preview** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part
55
66
  - Settings are stored in DSH's settings document (`settings.yaml`), so they travel with your configuration
56
67
 
57
68
  ## Development
58
69
 
59
70
  - `src/shared.cjs` — pure-function core: family sanitizing and parsing, configuration normalization, generated stylesheet
60
- - `src/index.mjs` — host half: registers the `dsh-fonttune` settings namespace (schemastery schema with length and range validation) and ships the saved declarations into the served index through `webserver/index-inject`, so the first paint already uses them
71
+ - `src/index.mjs` — host half: registers the `dsh-fonttune` settings namespace (schemastery schema with length and range validation) and ships a **stamped** `<style>` into the served index through `webserver/index-inject`, so the first paint already uses the saved values; the browser half then **adopts and keeps that element** (there is only ever one copy of the stylesheet, so rules can be added *and* removed)
61
72
  - `src/client.js` — browser half: the settings card, the font picker panel, font enumeration and style injection
62
73
  - `build.mjs` — zero-dependency build: inlines the shared core, wraps the bundle in the `window.__ModuleLoader__.load({id, factory})` shell, copies the host half, and enforces that the client bundle only requires shell-held modules
63
- - `test/run.mjs` — offline checks (hand-built DOM / cordis / settings-surface doubles, the real schemastery schema, CSS generation and injection, sanitizer adversarial cases, dictionary key parity); `test/` also holds headless-browser walk scripts that verify the card and its interactions in a real page
74
+ - `test/run.mjs` — offline checks (hand-built DOM / cordis / settings-surface doubles, the real schemastery schema, CSS generation and injection, sanitizer adversarial cases, dictionary key parity); `test/render-card.mjs` renders the card component through a mini React hooks runtime (including forced-open and forced-state renders, covering the 0.1.0 release-blocking crash class); `test/weight-verify.mjs` measures all three weight axes (interface / conversation / code) in a live page, including that there is only one stylesheet element and that switching following off drops the followed weight without a reload; `test/` also holds headless-browser walk scripts plus the market-entry tooling — `test/market-pr.mjs` (status / update / refresh / reopen / open / about) and the diagnostic `test/market-inspect.mjs` (PR state, comments, CI and branch diff)
75
+ - `docs/` — the 0.2.0 [research](docs/research-0.2.0.md) and [specification](docs/spec-0.2.0.md) records (written during design, with the trade-offs of that moment; where they differ from what shipped, README/CHANGELOG win — see the note at the top of each). **Not part of the npm `files` list, so they are not published**
64
76
  - The client bundle may only require modules from the shell's static table (`react`, `react/jsx-runtime`, `react-dom`, `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-*`…); `dsh.client.inject` declares load order, not require permission
65
77
  - Editing `src/client.js` is enough after a page refresh (client modules are versioned by content hash and DSH's client HMR pushes reloads); editing the host half (`src/index.mjs`) or `cordis.patch.yml` needs a DSH restart
66
78
 
package/README.md CHANGED
@@ -2,25 +2,30 @@
2
2
 
3
3
  > [English](README.en.md) | **中文**
4
4
 
5
- **DeepSeek Harness(DSH)字体插件**:设置正文与代码的字体族、各自独立的字号与字重,并按「西文 / 中文」分栏挑字体;配置入口是 **设置 → 插件 → 插件配置** 里的原生卡片,改完即时生效。
5
+ **DeepSeek Harness(DSH)字体插件**:**对话**(会话 Markdown 正文)拥有完整的排印控制——字体族、字号、行高、字重;**界面**(设置页、侧栏、工作区、按钮)提供字体与字重并**默认跟随对话**;**代码**独立成轴,另有连字与特性开关;整套配置可存为**预设方案**。配置入口是 **设置 → 插件 → 插件配置** 里的原生卡片(分区收起 + 摘要),改完即时生效。
6
6
 
7
7
  ## 功能
8
8
 
9
- - **正文 / 代码字体** —— 两套独立的字体回退列表,拉丁字体在前、中文字体在后;留空表示完全沿用 DSH 的字体栈
10
- - **正文字号偏移** —— -3 ~ +6 px,作用于正文与界面文字;与 DSH 自己的「字号大小」叠加而不是覆盖,每一档字号连同它的行高一起等比缩放
11
- - **代码字号偏移** —— -3 ~ +6 px,**独立**作用于代码块与行内代码:调正文不会带动代码,调代码也不会带动正文
12
- - **正文字重** —— 300 ~ 600 任意整数,覆盖正文与界面文字的粗细(含标题);未设置或 400 表示沿用 DSH 原本的字重
13
- - **代码字重** —— 300 ~ 600 任意整数,只覆盖代码块、行内代码与终端输出;未设置或 400 表示代码保持 DSH 原样,也就是**不被正文字重带着走**
14
- - **西文 / 中文分栏(简单模式)** —— 卡片顶部一个开关:简单模式只给「西文」「中文」两个单选格(正文/代码各一对),高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不会改动已排好的顺序
9
+ - **三套排印**:对话(会话 Markdown 正文、表格与标题)排在第一位、拥有全部轴;界面(设置页、侧栏、工作区与标题按钮)提供**字体与字重**、默认跟随对话(可关);代码独立成轴
10
+ - **界面 / 对话 / 代码字体** —— 各自独立的字体回退列表,拉丁字体在前、中文字体在后;留空表示完全沿用 DSH 的字体栈
11
+ - **字号偏移** —— **对话**字号 -3 ~ +6 px,加在 DSH 自己的「对话字号」之上(官方数值是基准),段落、标题与行高一起跟随;代码字号独立成轴,-3 ~ +6 px。**界面没有字号轴**:经实测 DSH 的设置页、侧栏与工作区把字号写死在上游 CSS 里,唯一官方字号接口就是「对话字号」
12
+ - **字重** —— **对话**、**界面**与**代码**各自 300 ~ 600 任意整数(未设置即 400,读数就显示 400)。界面字重是一条把对话 Markdown 整棵子树排除在外的 `body, body *` 规则:真机实测(活页面计算样式)它把界面的文本元素全部改到目标字重、对话 markdown 里的元素一个都没动,所以**改界面字重不会碰到对话**(对话的粗体、标题粗细也原样保留)
13
+ - **行高** —— **对话**行高用倍率(100% ~ 160%);代码行高用加法偏移(-4 ~ +8 px)。界面没有行高轴(同字号的原因)
14
+ - **代码连字** —— 三档开关(默认 / 开启 / 关闭,浏览器默认即开启,关闭可还原 `=>`、`!=` 这类连字),高级模式另有 `font-feature-settings` 特性值输入
15
+ - **拒绝合成样式** —— 禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰)
16
+ - **界面跟随对话开关** —— 界面分区顶部的「跟随对话设置」开关(默认开启):开启时界面沿用对话的**字体与字重**,界面自己的两个控件隐藏;关闭后界面用自己的字体与字重,与对话互不影响。字号与行高只属于对话(界面没有这两个接口)
17
+ - **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON 分享、导入合并)
18
+ - **西文 / 中文分栏(简单模式)** —— 编辑模式开关:简单模式每套字体只给「西文」「中文」两个单选格,高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不改动已排好的顺序
15
19
  - **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;搜索不到的名字可以直接「使用 xxx」新建
16
20
  - **拖拽排序** —— 已选字体是 chip 列表,可拖动调整回退顺序,同时保留前移 / 后移按钮(键盘与触屏可用)
17
- - **实时预览** —— 中英混排与代码两行,随当前配置即时渲染
21
+ - **分区预览** —— 对话 / 界面 / 代码分区展开后在分区末尾各有一个预览框,只预览该分区的部分
18
22
 
19
23
  ## 兼容性
20
24
 
21
25
  | 插件版本 | 支持的 DSH 版本 |
22
26
  | --- | --- |
23
- | **0.1.6**(最新) | 0.1.5-rc.2 |
27
+ | **0.2.1**(最新) | 0.1.5-rc.2 |
28
+ | 0.2.0 | 0.1.5-rc.2 |
24
29
 
25
30
  `engines.dsh` 下限为 **0.1.5-rc.2**,兼容声明只列 **0.1.5 线的最新版本**:它包含这条线的全部修复,是同一大版本里最稳的一版,而同一大版本内的旧版本按语义化版本约定向上兼容。更早或更新的 DSH 未经验证,不作声明。安装或更新时,DSH 市场会依据 `package.json` 里的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies` 做宿主兼容预检。
26
31
 
@@ -49,18 +54,25 @@ dsh plugin --profile web add <插件目录路径>
49
54
  ## 使用
50
55
 
51
56
  - **打开设置** —— 设置 → 插件 → 插件配置 → 展开「字体增强」卡片
52
- - **换字体** —— 简单模式下点「正文 · 西文字体」「正文 · 中文字体」「代码 · 西文字体」「代码 · 中文字体」四个格子选字体;高级模式下用「添加字体」与 chips 编辑器维护完整回退列表
53
- - **调整字号与字重** —— 滑块就在它作用的对象下面:正文字体 → 正文字号偏移 → 正文字重 → 代码字体 → 代码字号偏移 → 代码字重。拖动松手即生效;改过的字段标「已修改」并可单独重置,卡片底部有「全部重置」
54
- - **预览** —— 卡片底部的中英混排与代码预览用当前配置渲染
57
+ - **分区导航** —— 卡片展开后自上而下是:编辑模式、预设方案、**对话 → 界面 → 代码**三个收起的分区(对话在最前,因为它拥有全部轴),以及直接平铺在分区下面的「拒绝合成样式」;分区标题行右侧显示当前值摘要、箭头固定在最右,同一时刻最多展开一个分区
58
+ - **换字体** —— 简单模式下点「西文/中文」格子选字体;高级模式下用「添加字体」与 chips 编辑器维护完整回退列表
59
+ - **调整字号/字重/行高** —— 滑块就在各自分区里,拖动松手即生效(拖动时读数连同单位 % 一起显示);改过的字段标「已修改」并可单独重置,卡片底部有「全部重置」
60
+ - **对话优先** —— 对话分区可以设置字体、字号、行高、字重四项;界面分区默认只有「跟随对话设置」开关(默认开启),关掉后才出现界面自己的字体与字重
61
+ - **关掉跟随不会跳变** —— 关掉开关的那一刻,界面会把**正在显示的**字体与字重写成自己的值(跟随期间它本来就是这两项),所以画面零变化、滑块从当前值继续调;对话没设的那一项则清掉界面自己的字段、保持 DSH 默认。想回到 DSH 自己的粗细点该项的「重置」(读数会回到 400)
62
+ - **界面字重不会碰到对话** —— 界面字重是唯一必须铺满整页的轴(DSH 把小标签的粗细写死在上游 CSS 的类规则里,窄规则够不到它),实现上是一条把**对话 Markdown 整棵子树排除在外**的规则,所以对话的段落、标题、粗体都不受影响
63
+ - **字重读数** —— 未设置的读数显示 400(DSH 正文的就是 400,滑块也停在 400),不会出现「未设置」这类字眼
64
+ - **预设方案** —— 编辑模式下方的下拉选择框(默认配置 1 ~ 5,框内右侧有与卡片一致的下拉箭头),点开选择即切换并自动保存后续改动;「改名」重命名当前方案,「导出」复制 JSON,「导入」粘贴合并。操作结果(如「已复制到剪贴板」)显示在下面那行的**右侧**,约 2.6 秒后自动淡出,不会顶动任何布局
65
+ - **预览** —— 对话 / 界面 / 代码分区展开后,各自分区末尾有一个预览框,只渲染该分区的样式
55
66
  - 设置保存在 DSH 的设置文档里(`settings.yaml`),跟随配置走
56
67
 
57
68
  ## 开发
58
69
 
59
70
  - `src/shared.cjs` —— 纯函数核心:字体名消毒与解析、配置归一化、注入样式的生成
60
- - `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把已保存的声明随首页下发,首帧即是设置里的字体
71
+ - `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把一个打过标记的 `<style>` 随首页下发,首帧即是设置里的字体;那个元素随后由浏览器半**认领并接管**(页面里只有这一份样式表,规则可增可删)
61
72
  - `src/client.js` —— 浏览器半:设置卡片、选字体面板、字体枚举与样式注入
62
73
  - `build.mjs` —— 零依赖构建:内联共享核心、套上 `window.__ModuleLoader__.load({id, factory})` 外壳、拷贝宿主半,并校验客户端 bundle 只 require shell 预注入的模块
63
- - `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性);`test/` 下另有几个无头浏览器走查脚本,用真实页面验证卡片渲染与交互
74
+ - `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃);`test/weight-verify.mjs` 在真实页面里量三条字重轴(界面 / 对话 / 代码)的独立性与「样式表只有一份、不刷新关掉跟随立即回落」;`test/` 下另有几个无头浏览器走查脚本,以及市场条目的维护脚本 `test/market-pr.mjs`(status / update / refresh / reopen / open / about)与诊断脚本 `test/market-inspect.mjs`(PR 状态、评论、CI 与分支差异)
75
+ - `docs/` —— 0.2.0 的[背景调研](docs/research-0.2.0.md)与[功能规格](docs/spec-0.2.0.md)(设计阶段的记录,含当时的取舍理由;与最终实现不同的地方以 README/CHANGELOG 为准,两份文件文首都有说明)。**不在 npm `files` 清单内,不随包发布**
64
76
  - 客户端模块能 require 的只有 shell 静态表里的模块(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*` 等);`dsh.client.inject` 只是加载顺序声明,不是 require 许可
65
77
  - 改 `src/client.js` 后刷新页面即可看到变化(客户端模块按内容哈希发版,DSH 的客户端 HMR 也会推送重载);改宿主半(`src/index.mjs`)或 `cordis.patch.yml` 需重启 DSH
66
78