dsh-fonttune 0.2.1 → 0.2.3

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,44 @@
2
2
 
3
3
  All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.2.3] - 2026-09-22
6
+
7
+ ### Fixed
8
+ - **A refused settings write is no longer swallowed**: `scope.set` / `scope.unset` failures used to hang off an empty `catch` — the control snapped back to the stored value and the user believed it saved. Both a synchronous throw and a rejected promise now print "The setting was not saved: <reason>" (clipped at 160 characters, full text on hover) on the right of the preset row, using the same row that fades itself out after 2.6 s, so nothing else moves.
9
+ - **An export the clipboard refuses no longer hands over truncated JSON**: the old fallback cut the JSON to 120/200 characters and printed it in that one-line status row, which the user could not copy from. Three layers now: (1) the async Clipboard API; (2) outside a secure context (the remote HTTP origin) `navigator.clipboard` does not exist at all, so a hidden textarea plus `document.execCommand("copy")` — the legacy path still works there; (3) only when both fail does the **whole** JSON land in an auto-focused, fully selected, read-only box that reuses the field surface, where one copy shortcut is all that is left (it puts itself away on `onCopy`). The status row says just "Clipboard unavailable — press Ctrl+C to copy the selected text."
10
+ - **Three problems in the font picker**: (1) the four curated groups (monospace / CJK / Latin / generic) used to vanish whenever the local enumeration *worked* and a search was running, so a curated family this machine does not have could not be found by name at all; they are now always listed, filtered by the search text. (2) The local group no longer repeats what a curated heading already offers — a family is listed once per panel, and the curated heading wins, because on Windows nearly the whole Latin group is installed and letting the local list win would empty the short list. (3) A refused enumeration was cached **forever**, so granting the font permission in the browser changed nothing; a refusal is now trusted for 20 seconds and the next panel open retries — a still-blocked permission is rejected instantly without prompting again, so the retry costs nothing.
11
+ - **The token refresh polls cheaply**: the size axis has to re-read its base values after DSH writes its runtime tokens, and the old loop walked every property of every rule of every stylesheet every four seconds. Each tick now compares three cheap signals first (body's `style` attribute, the root class, the sheet count) and returns untouched when none moved; every eighth tick still does a full sweep, which is what catches an in-place rewrite inside the CSS-in-JS sheet, and `theme/change` still forces one. An offline counting stub proves it: no CSS read while the environment is unchanged, the very next cheap tick follows a moved inline token, and the eighth tick sweeps anyway.
12
+ - **The per-theme "code font inside the dialog" rule missed its theme prefix** (dormant: per-theme is off today). `pushScopedCodeFamily` emitted its scoped rule without `body[data-ds-dark-theme]`, so with per-theme on the dark set would re-assert the **light** code family inside a dark dialog. It is prefixed like every other rule now, and a case asserts that every per-theme rule starts with the theme attribute.
13
+ - **`test/split-verify.mjs` was measuring a retired axis**: it still wrote `sizeOffset` (the interface size axis retired in 0.2.0) and expected the conversation chain to follow, so it rightly went red on a real machine. It now writes `sizeOffsetDialog` + `sizeOffsetCode`, computes its ratios from the measured baseline (the conversation offset rides DSH's own size, the code axis scales over the 16px reference — the two expectations differ), and adds a live check that the retired interface size axis moves nothing.
14
+
15
+ ### Changed
16
+ - **A smaller stylesheet**: the cascaded `:not(a):not(b):not(c)…` chain collapsed into one `:not(a,b,c…)`. That chain was copied into both dialog selectors and the interface rule, and each copy saves roughly 120 bytes: a real user's injected CSS went **2735 → 2440 B** with `:not(` occurrences **62 → 3**. Specificity really does drop (`:not(a):not(b)` sums its arguments, `:not(a,b)` takes the most specific one), so the risk was measured first: the entire DSH 0.1.5-rc.2 frontend holds exactly one `!important` typography declaration (`font-size` on inline markdown `code`, and this plugin never emits a `font-size`), no installed plugin adds another `!important` weight/family rule, every rule here is `!important` (which already beats the theme's non-important class rules) and the rules that could collide are held apart by these very exclusions rather than by their weight. A live re-run of WEIGHT OK passes all 17 checks (the interface moves 137 text elements, the markdown subtree 0). The per-theme prefix helper now splits a selector list on **top-level** commas only, so the commas inside `:not(a,b)` cannot cut a selector in half.
17
+ - **The retired size and line-height axes left the value surface**: `sizeOffset` and `lineHeight` (the interface's size and line height, which have emitted nothing since 0.2.0) stay in the schema and the document — old configurations still parse and `normalizeConfig` still carries them — but they are no longer part of a preset snapshot and no longer decide whether the dark set is a second set, so a light/dark pair that differs only there emits one stylesheet instead of two. "Reset every axis" **still clears them**, so a value nothing shows cannot hide in a document forever.
18
+ - **Dead code removed**: 31 dictionary keys nobody renders (both locales — the retired interface size/line-height copy, the per-theme editor, the old preset button row, the fixed preview label), one dead CSS rule (`.dfp-presetForm`) and seven dead exports (`MARKER`, `isShorthandToken`, `bodyTokenNames`, `uiLineTokenNames`, `deltaTokenNames`, `codeTokenNames`, `uiShorthandTokens`, `scaleFontShorthand`). `VALUE_FIELDS` / `RETIRED_FIELDS` / `DURABLE_FIELDS` now name the three ideas — value axes, stored-but-unrendered, every durable field — separately.
19
+ - **One entry point for the browser checks**: `npm run verify:browser` (`test/browser-verify.mjs`) runs the read-only `browser-probe` / `ui-walk` / `style-verify` by default, adds the four scripts that write the settings namespace under `--writers` (each snapshots the user layer and restores it before exiting), takes `--only a,b` for a subset, and prints a per-script time and exit-code summary. Two walk scripts were rewritten for the 0.2.x card: `slider-walk` expands the section it needs first (the card opens fully collapsed) and asserts "a drag writes nothing, a release writes, each axis lands on its own field"; `split-walk` writes a three-entry stack and asserts the simple-mode slots and remainder note, that a mode round-trip leaves the stack alone, and that picking a Western family in the real panel replaces exactly that slot. **A missing control is now a failure** — the old scripts printed "not found" and exited zero, which is a false green.
20
+ - Compatibility table gained 0.2.3.
21
+
22
+ ### Tests
23
+ - `test/run.mjs` (86 checks): the picker's grouping rules (curated groups always present, no family twice, the local cap and sanitizing, the refusal-retry contract) and the polling gate with a counting stub (negative control included).
24
+ - `test/render-card.mjs` (21 checks): a second render after a state change keeping the hook order, both shapes of a refused write being reported, the export fallback handing over the whole JSON selected, reset clearing the retired axes, and a retired axis not being a value axis.
25
+ - Live (managed instance + headless Edge): WEIGHT OK (17 checks), SPLIT OK (9), SLIDER OK (9), SPLIT WALK OK (14), browser-probe and ui-walk green.
26
+
27
+ ## [0.2.2] - 2026-09-16
28
+
29
+ ### Fixed
30
+ - **Simple mode: picking or clearing the CJK slot threw and lost the change** (introduced in 0.2.0). The two handlers behind that slot read `edit[field]`, and no binding of that name exists in the module (the real one is `editing`). The western slot was fine; only the CJK slot broke — in **any** of the three family sections (conversation / interface / code), picking a family in the 中文 slot or clearing that chip threw `ReferenceError: edit is not defined`, wrote nothing and changed nothing on screen. The fix is those two references. **Why it went unnoticed**: the card tests only ever rendered the tree and never invoked a single control callback, so an unwired handler stayed green offline.
31
+ - **A guard for that whole class of bug**: `test/render-card.mjs` gained two cases that **fire every rendered control's callback with the payload its control passes** (a slider's min/max, every option of a segmented control, the family picker's `onPick`/`onRemove`, the preset select's full preset entry, a text input's DOM event, every `<button>`'s `onClick`) and assert that none throws — plus a separate assertion that the three CJK slots really *wrote* into the settings, not merely failed to throw. Negative control: injecting the 0.2.1 code turns both cases red; restoring the fix turns them green.
32
+ - **`lib/` can no longer drift from `src/` silently**: the new `test/artifacts.mjs` re-renders the build in memory and compares it byte for byte with the committed `lib/client.js`, `lib/shared.cjs` and `lib/index.js` (run by `npm run verify` and by `prepublishOnly`). Every other suite reads `lib/`, so a source-only edit used to be tested — and shipped — as the previous build. `build.mjs` was split into pure functions (`renderArtifacts` / `readSources`) and no longer has side effects when imported.
33
+ - **The release workflow never ran the tests**: `npm pack` does **not** trigger `prepublishOnly` (only `npm publish` does), so the tag-triggered release had always packed an untested artifact. It now installs and runs `npm run verify` before packing — deliberately **without** building first, because `test/artifacts.mjs` is exactly the check that the committed `lib/` is what the current `src/` produces, and a build step would mask that — and asserts that the tag equals the `package.json` version.
34
+ - **The CHANGELOG extraction matched version prefixes**: the release body used `index(line, "## " pat)`, so `0.2.1` also matched a `0.2.10` heading. It now matches `## <version> ` exactly and **fails the job** when the version has no section (it used to fall back to generated notes silently).
35
+ - **Two common CJK family names were not recognised**: `MS PGothic` and `MS PMincho` were treated as non-CJK (while `MS Gothic` and `MS Mincho` were fine), which feeds the simple mode's CJK slot. Fixed to `ms ?p?(?:gothic|mincho)`, the safe direction for a name heuristic.
36
+ - **`shiftTokens` guards zero**: a zero offset used to emit a real `calc((14px) + 0px)` declaration that disagrees with `isDormant` and `sameValueSet`. Unreachable today (the call sites guard), but a trap for the next caller.
37
+ - **`build.mjs --watch` watched files instead of the directory**: after an editor renamed a source, the process kept waiting on a dead inode and never rebuilt. It now watches `src/` and filters by name, and logs each successful rebuild.
38
+
39
+ ### Changed
40
+ - **Documentation corrections**: the README's "editing `src/client.js` is enough after a refresh" omitted the essential step — the browser loads `lib/`, so `node build.mjs` comes first (both languages). The claim that "the DSH market runs a host-compatibility preflight against `engines.dsh` / `dshReleases` / `peerDependencies`" has no implementation anywhere in the local rc.2 distribution, so it now says what is true: the market entry **states** those requirements so the host a version needs can be checked before installing.
41
+ - `package.json`: added the test-only `devDependencies` entry (`@deepseek-ai/schemastery`) and a `verify` script; the compatibility table gained 0.2.2.
42
+
5
43
  ## [0.2.1] - 2026-09-16
6
44
 
7
45
  ### Fixed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  **dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.2.3] - 2026-09-22
6
+
7
+ ### 修复
8
+ - **写设置失败不再被吞掉**:以前 `scope.set` / `scope.unset` 的失败只挂一个空的 `catch`——控件弹回旧值、用户以为改成功了。现在同步抛出的错误和 Promise 拒绝都会在预设行右侧显示一句「设置没有保存成功:<原因>」(超长截断到 160 字,悬停可看全文),走的是同一个 2.6 秒自动淡出的行,不占额外版面。
9
+ - **剪贴板不可用时,导出不再只给一段被截断的 JSON**:以前导出失败会把 JSON 截到 120/200 字塞进那一行提示里,用户根本拿不到完整内容。现在三级兜底:① 异步 Clipboard API;② 非安全上下文(远程 HTTP 入口)里 `navigator.clipboard` 压根不存在,退回隐藏 textarea + `document.execCommand("copy")` 这条老路(在非安全上下文里照样可用);③ 两条都失败才把**完整 JSON** 放进一个自动聚焦、已全选、只读的文本框(复用输入框样式),按一次复制快捷键即可,`onCopy` 之后自动收起。状态行只说一句「剪贴板不可用,按 Ctrl+C 复制下方已选中的内容」。
10
+ - **选字体面板的三个问题**:① 内置四组(等宽 / 中文 / 拉丁 / 通用)以前在「本机字体枚举成功**并且**正在搜索」时整组消失,于是本机没装的预设字体**搜名字都搜不到**;现在四组任何情况下都在(搜索时按关键词过滤)。② 本机分组不再重复预设组已经列出的字体,同一个字体在一个面板里只会出现一次(预设标题优先,因为它才是那张短名单);实测 Windows 上拉丁预设组几乎全部已安装,若让本机分组优先,预设组会被掏空。③ 枚举被拒(`queryLocalFonts` 拒绝)以前会**永久缓存**,用户去浏览器里授予字体权限后也不会再变;现在拒绝结果只信任 20 秒,之后再次打开面板就会重试——还是被拒时浏览器是**立即**拒绝、不会重新弹窗,所以重试没有代价。
11
+ - **令牌轮询的省电闸门**:界面字号轴需要在 DSH 运行时写入令牌后重新读取基准值,原来每 4 秒无条件把每个样式表的每条规则的每个属性扫一遍。现在每 tick 只比较三个廉价信号(`body` 的 `style` 属性字符串、`html` 的 class、样式表数量),三者都没动就直接返回;每第 8 次 tick 仍做一次全量扫描,接住 CSS-in-JS 样式表**就地改写**规则的情况;`theme/change` 事件照旧强制全量。离线用计数桩证明:环境不变时一次 CSS 都不读,内联令牌一变下一个廉价 tick 就立刻跟上,第 8 次 tick 照常兜底。
12
+ - **逐主题的「对话框内代码字体」规则漏了主题前缀**(休眠缺陷,逐主题当前关闭):`pushScopedCodeFamily` 生成的作用域规则没带 `body[data-ds-dark-theme]`,一旦开启逐主题,深色那一套会用**浅色**的代码字体覆盖深色对话框。现在与其他规则一样带前缀,并加了「每一条逐主题规则都必须以主题属性开头」的用例。
13
+ - **`test/split-verify.mjs` 测的是早已退役的轴**:脚本还在写 `sizeOffset`(0.2.0 起退役的界面字号轴)并期望对话链路跟着动,真机自然红。现在它写 `sizeOffsetDialog` + `sizeOffsetCode`,比值按**实测基准**算(对话轴加在 DSH 自己的字号上、代码轴按 16px 基准缩放,两者不同),并新增一条「退役的界面字号轴什么都不动」的真机断言。
14
+
15
+ ### 变更
16
+ - **样式表瘦身**:把级联的 `:not(a):not(b):not(c)…` 收成一个 `:not(a,b,c…)`,那条排除链原本会被复制进对话框规则的两个选择器与界面规则各一份,每份省约 120 字节。用户真实配置的注入样式 **2735 → 2440 B**,`:not(` 出现次数 **62 → 3**。**特异性确实被降低了**(`:not(a):not(b)` 累加参数特异性,`:not(a,b)` 只取最高的那个),所以先量了风险:DSH 0.1.5-rc.2 全部前端资产里只有一条 `!important` 排版声明(markdown 行内 `code` 的 `font-size`,而本插件从不写 `font-size`),已装插件里也没有第二条 `!important` 的字重/字体规则;本插件的规则全部带 `!important`(本来就赢得过主题那些非 important 的 class 规则),而且彼此靠这些排除列表**互不重叠**。真机复测 WEIGHT OK 17 项全过(界面移动 137 个文本元素、markdown 子树 0 个)。顺带把逐主题用的前缀函数改成只按**顶层逗号**切分选择器列表(`:not(a,b)` 里的逗号不再被切断)。
17
+ - **退役的字号/行高轴退出「值表面」**:`sizeOffset` 与 `lineHeight`(界面的字号与行高,0.2.0 起不产生任何规则)仍然留在 schema 与文档字段里(老配置照常解析、`normalizeConfig` 照常带出来),但不再进预设快照、不再参与明暗对比——只在这两个字段上有差别的明暗配置不再生成第二份样式表;卡片上的「全部重置」**仍然会清掉它们**,免得一个永远不显示的值烂在文档里。
18
+ - **清理死代码**:删掉 31 个不再渲染的词典键(中英各 31 个,含退役的界面字号/行高、逐主题编辑器、旧预设按钮组与固定预览的文案)、1 条死 CSS(`.dfp-presetForm`)与 7 个死导出(`MARKER`、`isShorthandToken`、`bodyTokenNames`、`uiLineTokenNames`、`deltaTokenNames`、`codeTokenNames`、`uiShorthandTokens`、`scaleFontShorthand`)。`VALUE_FIELDS` / `RETIRED_FIELDS` / `DURABLE_FIELDS` 三个清单把「值轴」「仍存储但不渲染」「全部持久字段」分开写清楚了。
19
+ - **`npm run verify:browser` 统一入口**(`test/browser-verify.mjs`):默认跑只读的 `browser-probe` / `ui-walk` / `style-verify`,`--writers` 再加会写设置命名空间的四支(各自快照并在退出前还原),`--only a,b` 挑子集,逐支打印耗时与退出码并汇总。两支走查脚本按 0.2.x 的卡片重写:`slider-walk` 先展开分区再拖(卡片默认全部收起),断言「拖动不写、松手才写、每条轴落到自己的字段」;`split-walk` 先写入三档字体栈,断言简单模式两格与剩余提示、模式往返不改动栈、真实面板里选一个西文字体只替换西文槽。**找不到控件现在是失败**(以前打印一句「not found」然后退出 0,等于假绿)。
20
+ - 兼容表补 0.2.3。
21
+
22
+ ### 测试
23
+ - `test/run.mjs`(86 项):新增面板分组规则(预设四组恒在、同字体不重复、本机分组上限与消毒、拒绝后的重试契约)与轮询闸门的计数用例(含负对照:去掉闸门后该用例红)。
24
+ - `test/render-card.mjs`(21 项):新增「第二次渲染(状态变化后 hook 顺序不变)」「拒绝写入的两种形态都上报」「导出兜底交出完整 JSON 并被选中」「重置清掉退役轴」「退役轴不再是值轴」。
25
+ - 真机(受管实例 + 无头 Edge):WEIGHT OK(17 项)、SPLIT OK(9 项)、SLIDER OK(9 项)、SPLIT WALK OK(14 项)、browser-probe / ui-walk 通过。
26
+
27
+ ## [0.2.2] - 2026-09-16
28
+
29
+ ### 修复
30
+ - **简单模式下选/清「中文字体」直接报错、改动丢失**(0.2.0 引入):中文槽的两个处理函数读的是 `edit[field]`,而模块里根本没有 `edit` 这个绑定(真正的变量叫 `editing`)。西文槽正常,只有中文槽踩雷——**任一字体分区**(对话 / 界面 / 代码)在**简单模式**下点中文格子选字体、或点中文 chip 的 ×,都会抛 `ReferenceError: edit is not defined`,设置不写入、界面无变化。修法就是那两处改成 `editing[field]`。**为什么一直没被发现**:卡片测试只渲染、从不触发任何控件回调,所以这类「回调没接线」的错误离线全绿。
31
+ - **补上这一类错误的护栏**:`test/render-card.mjs` 新增两条用例,把渲染出来的**每个控件的回调都按真实载荷调一遍**(滑块的 min/max、分段控件的每个选项、字体选择器的 onPick/onRemove、预设下拉的整条预设对象、文本输入的事件、以及每个 `<button>` 的 onClick),断言「一个都不许抛」;并单独断言三个字体分区的**中文槽确实写进了设置**(不只是没报错)。用 0.2.1 的写法做对照实验:注入旧代码后这两条用例红,恢复后绿。
32
+ - **`lib/` 与 `src/` 不再可能悄悄不同步**:新增 `test/artifacts.mjs`,把 `build.mjs` 的产物在内存里重新渲染一遍,与仓库里的 `lib/client.js`、`lib/shared.cjs`、`lib/index.js` **逐字节比对**(`npm run verify` 跑它,`prepublishOnly` 也跑它)。此前所有测试都只读 `lib/`,只改 `src/` 不构建的话测试照样全绿、发出去的还是旧包。顺带把 `build.mjs` 拆出纯函数(`renderArtifacts` / `readSources`)并加了「被 import 时不产生副作用」的判定。
33
+ - **发布 workflow 从来没跑过测试**:`npm pack` **不会**触发 `prepublishOnly`(只有 `npm publish` 会),所以 tag 触发的发布流程一直在打包未测试的产物。现在加了 `npm install` + `npm run verify` 两步(**放在打包之前、且不先构建**——`test/artifacts.mjs` 要校验的正是「仓库里的 `lib/` 就是当前 `src/` 的产物」,先构建会把这个问题掩盖掉),并补一条「tag 必须等于 `package.json` 版本」。
34
+ - **CHANGELOG 提取的前缀匹配会串版本**:release 正文用 `index(line, "## " pat)` 取章节,`0.2.1` 会命中 `0.2.10`。现在按 `## <版本> ` 精确匹配,并在找不到该版本章节时**直接让 job 失败**(以前会静默退回 GitHub 自动生成的 notes)。
35
+ - **CJK 判定漏了两个常见字体名**:`MS PGothic` / `MS PMincho` 被判成非中文字体(`MS Gothic` / `MS Mincho` 正常),而这会直接影响简单模式的「中文槽」推导,故按名字启发式的方向修成 `ms ?p?(?:gothic|mincho)`。
36
+ - **`shiftTokens` 补上零保护**:偏移为 0 时会写出 `calc((14px) + 0px)` 这种真声明,与 `isDormant` / `sameValueSet` 的语义冲突。当前调用侧都拦着,属于给未来调用者拆雷。
37
+ - **`build.mjs --watch` 盯的是文件而不是目录**:源文件被编辑器改名后,进程还守着已经消失的 inode,永远不再重建。现在监听 `src/` 目录并按文件名过滤,重建成功也会打一行日志。
38
+
39
+ ### 变更
40
+ - **文档纠错**:README 里「改 `src/client.js` 后刷新页面即可」漏了最关键的一步——浏览器加载的是 `lib/`,必须先 `node build.mjs`(中英两份都改了);「市场会依据 `engines.dsh` / `dshReleases` / `peerDependencies` 做宿主兼容预检」这句在本地 rc.2 全量包里找不到任何对应实现,改成如实描述(市场条目**声明**这些兼容信息,供安装前判断)。
41
+ - `package.json` 增加 `devDependencies`(`@deepseek-ai/schemastery`,测试用)与 `verify` 脚本;兼容表补 0.2.2。
42
+
5
43
  ## [0.2.1] - 2026-09-16
6
44
 
7
45
  ### 修复
package/README.en.md CHANGED
@@ -14,9 +14,9 @@
14
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
15
  - **Refuse synthetic styles** — stops the browser from faking italic and bold for CJK faces, which have neither, so marked text stays upright
16
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)
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). When the clipboard is unavailable (a non-secure context, or no clipboard permission) the export does not print a truncated message: the whole JSON goes into a selected, read-only box, so the copy shortcut works, and the box puts itself away once copied. A settings write the document **refuses** is reported on the same row instead of leaving the control quietly showing the old value
18
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
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"
19
+ - **Font picker panel** — four curated groups (monospace / CJK / Latin / generic), always listed — while searching and even when enumeration fails — plus an "installed on this machine" group on Chromium that lists only what the curated groups do not, so no family is ever offered twice. A name that is not in the list can be created with "use xxx". A refused font permission is retried after a while, so granting it in the browser and reopening the panel fills the list in
20
20
  - **Drag to reorder** — picked fonts are draggable chips with earlier/later buttons too (keyboard and touch friendly)
21
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
22
22
 
@@ -24,10 +24,12 @@
24
24
 
25
25
  | Plugin version | Supported DSH versions |
26
26
  | --- | --- |
27
- | **0.2.1** (latest) | 0.1.5-rc.2 |
27
+ | **0.2.3** (latest) | 0.1.5-rc.2 |
28
+ | 0.2.2 | 0.1.5-rc.2 |
29
+ | 0.2.1 | 0.1.5-rc.2 |
28
30
  | 0.2.0 | 0.1.5-rc.2 |
29
31
 
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`.
32
+ `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. The market entry also states these requirements (`engines.dsh`, `dsh.compatibility.dshReleases` and `peerDependencies` in `package.json`), so the host a version needs can be checked before installing.
31
33
 
32
34
  ## Install
33
35
 
@@ -71,10 +73,12 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
71
73
  - `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)
72
74
  - `src/client.js` — browser half: the settings card, the font picker panel, font enumeration and style injection
73
75
  - `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
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)
76
+ - `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, the picker's grouping rules, the token-refresh power gate); `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), fires **every control's callback** (an unwired handler only fails when someone actually clicks it) and **renders a second time** to cover a hook order that changes with state; `test/artifacts.mjs` proves `lib/` matches the current `src/` byte for byte (`npm run verify` runs all three)
77
+ - Checks that need a real page run through `npm run verify:browser -- "<url-with-token>"` (start a managed instance first: `dsh web --port 0 --no-open` prints the URL). The default group is read-only (`browser-probe` / `ui-walk` / `style-verify`); `--writers` adds the scripts that **write the settings namespace** (`slider-walk` / `split-walk` / `split-verify` / `weight-verify`, each snapshots the user layer first and restores it before exiting), and `--only a,b` picks a subset. Every script's exit code is its verdict; `test/set-user-layer.mjs <url> '<json>'` puts the layer back after an interrupted run
78
+ - `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/split-verify.mjs` measures the conversation and code size axes plus the **retired interface size axis moving nothing**; `test/slider-walk.mjs` and `test/split-walk.mjs` drive the real card (a drag writes nothing, a release does; the simple-mode slots and the whole stack round-trip); the market-entry tooling is `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
79
  - `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**
76
80
  - 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
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
81
+ - Build before refreshing: `node build.mjs` (or `npm run watch`) turns `src/` into `lib/`, and the browser loads `lib/` — editing `src/client.js` without building leaves the page on the previous bundle. `npm test` only checks behaviour; `node test/artifacts.mjs` (part of `npm run verify`) proves the committed `lib/` is byte-for-byte what the current `src/` produces. Editing the host half (`src/index.mjs`) or `cordis.patch.yml` needs a DSH restart
78
82
 
79
83
  ## License
80
84
 
package/README.md CHANGED
@@ -14,9 +14,9 @@
14
14
  - **代码连字** —— 三档开关(默认 / 开启 / 关闭,浏览器默认即开启,关闭可还原 `=>`、`!=` 这类连字),高级模式另有 `font-feature-settings` 特性值输入
15
15
  - **拒绝合成样式** —— 禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰)
16
16
  - **界面跟随对话开关** —— 界面分区顶部的「跟随对话设置」开关(默认开启):开启时界面沿用对话的**字体与字重**,界面自己的两个控件隐藏;关闭后界面用自己的字体与字重,与对话互不影响。字号与行高只属于对话(界面没有这两个接口)
17
- - **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON 分享、导入合并)
17
+ - **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON 分享、导入合并)。剪贴板不可用时(非安全上下文、或没有剪贴板权限)导出不会只给一段被截断的提示,而是把完整 JSON 放进一个已选中、只读的文本框,按复制快捷键即可,复制后自动收起;任何**写设置失败**(宿主拒绝、只读部署等)都会在当前行显示出来,而不是让控件悄悄弹回旧值
18
18
  - **西文 / 中文分栏(简单模式)** —— 编辑模式开关:简单模式每套字体只给「西文」「中文」两个单选格,高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不改动已排好的顺序
19
- - **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;搜索不到的名字可以直接「使用 xxx」新建
19
+ - **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;四组预设**任何情况下都在**(包括搜索时,也包括枚举本机字体失败时),本机分组只列出预设分组没有的字体,所以同一个字体不会出现两次;搜索不到的名字可以直接「使用 xxx」新建。字体枚举被浏览器拒绝时会在一段时间后自动重试(用户在浏览器里授予字体权限后,重新打开面板即可看到本机字体)
20
20
  - **拖拽排序** —— 已选字体是 chip 列表,可拖动调整回退顺序,同时保留前移 / 后移按钮(键盘与触屏可用)
21
21
  - **分区预览** —— 对话 / 界面 / 代码分区展开后在分区末尾各有一个预览框,只预览该分区的部分
22
22
 
@@ -24,10 +24,12 @@
24
24
 
25
25
  | 插件版本 | 支持的 DSH 版本 |
26
26
  | --- | --- |
27
- | **0.2.1**(最新) | 0.1.5-rc.2 |
27
+ | **0.2.3**(最新) | 0.1.5-rc.2 |
28
+ | 0.2.2 | 0.1.5-rc.2 |
29
+ | 0.2.1 | 0.1.5-rc.2 |
28
30
  | 0.2.0 | 0.1.5-rc.2 |
29
31
 
30
- `engines.dsh` 下限为 **0.1.5-rc.2**,兼容声明只列 **0.1.5 线的最新版本**:它包含这条线的全部修复,是同一大版本里最稳的一版,而同一大版本内的旧版本按语义化版本约定向上兼容。更早或更新的 DSH 未经验证,不作声明。安装或更新时,DSH 市场会依据 `package.json` 里的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies` 做宿主兼容预检。
32
+ `engines.dsh` 下限为 **0.1.5-rc.2**,兼容声明只列 **0.1.5 线的最新版本**:它包含这条线的全部修复,是同一大版本里最稳的一版,而同一大版本内的旧版本按语义化版本约定向上兼容。更早或更新的 DSH 未经验证,不作声明。市场条目同时声明这些兼容信息(`package.json` 的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies`),安装前可据此判断这一版需要的宿主版本。
31
33
 
32
34
  ## 安装
33
35
 
@@ -71,10 +73,12 @@ dsh plugin --profile web add <插件目录路径>
71
73
  - `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把一个打过标记的 `<style>` 随首页下发,首帧即是设置里的字体;那个元素随后由浏览器半**认领并接管**(页面里只有这一份样式表,规则可增可删)
72
74
  - `src/client.js` —— 浏览器半:设置卡片、选字体面板、字体枚举与样式注入
73
75
  - `build.mjs` —— 零依赖构建:内联共享核心、套上 `window.__ModuleLoader__.load({id, factory})` 外壳、拷贝宿主半,并校验客户端 bundle 只 require shell 预注入的模块
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 与分支差异)
76
+ - `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性、选字体面板的分组规则、令牌轮询的省电闸门);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃),把**每个控件的回调都点一遍**(未接线的回调只有真点下去才会暴露),并**渲染第二次**覆盖「状态变化后 hook 顺序改变」这一类崩溃;`test/artifacts.mjs` 校验 `lib/` 与当前 `src/` 逐字节一致(`npm run verify` 跑全部三套)
77
+ - 需要真实页面的检查走 `npm run verify:browser -- "<带 token 的地址>"`(先起一个受管实例:`dsh web --port 0 --no-open`,它会打印地址)。默认只跑**只读**的几支(`browser-probe` / `ui-walk` / `style-verify`);加 `--writers` 再跑会**写设置命名空间**的几支(`slider-walk` / `split-walk` / `split-verify` / `weight-verify`,各自先快照用户层、退出前还原),`--only a,b` 挑子集;每支的退出码就是判定。`test/set-user-layer.mjs <url> '<json>'` 是在被中断的运行之后还原用户层的工具
78
+ - `test/weight-verify.mjs` 在真实页面里量三条字重轴(界面 / 对话 / 代码)的独立性与「样式表只有一份、不刷新关掉跟随立即回落」;`test/split-verify.mjs` 量对话/代码两条字号轴的独立性,外加**退役的界面字号轴什么都不动**;`test/slider-walk.mjs` 与 `test/split-walk.mjs` 驱动真实卡片(拖动不写、松手才写;简单模式两格与整条栈的往返);另有市场条目的维护脚本 `test/market-pr.mjs`(status / update / refresh / reopen / open / about)与诊断脚本 `test/market-inspect.mjs`(PR 状态、评论、CI 与分支差异)
75
79
  - `docs/` —— 0.2.0 的[背景调研](docs/research-0.2.0.md)与[功能规格](docs/spec-0.2.0.md)(设计阶段的记录,含当时的取舍理由;与最终实现不同的地方以 README/CHANGELOG 为准,两份文件文首都有说明)。**不在 npm `files` 清单内,不随包发布**
76
80
  - 客户端模块能 require 的只有 shell 静态表里的模块(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*` 等);`dsh.client.inject` 只是加载顺序声明,不是 require 许可
77
- - 改 `src/client.js` 后刷新页面即可看到变化(客户端模块按内容哈希发版,DSH 的客户端 HMR 也会推送重载);改宿主半(`src/index.mjs`)或 `cordis.patch.yml` 需重启 DSH
81
+ - 改完源码要**先构建再刷新**:`node build.mjs`(或 `npm run watch`)把 `src/` 生成到 `lib/`,浏览器加载的是 `lib/`,只改 `src/client.js` 不构建的话页面看到的还是旧包;`npm test` 只检查功能,`node test/artifacts.mjs`(在 `npm run verify` 里)专门校验 `lib/` 与当前 `src/` 逐字节一致。改宿主半(`src/index.mjs`)或 `cordis.patch.yml` 需重启 DSH
78
82
 
79
83
  ## License
80
84