dsh-fonttune 0.2.0 → 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 +10 -0
- package/CHANGELOG.md +10 -0
- package/README.en.md +12 -9
- package/README.md +12 -9
- package/lib/client.js +206 -81
- package/lib/index.js +28 -8
- package/lib/shared.cjs +128 -54
- package/package.json +2 -2
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
5
15
|
## [0.2.0] - 2026-09-16
|
|
6
16
|
|
|
7
17
|
### Added
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
5
15
|
## [0.2.0] - 2026-09-16
|
|
6
16
|
|
|
7
17
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
> [中文](README.md) | **English**
|
|
4
4
|
|
|
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)
|
|
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
|
-
- **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
|
|
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
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
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** and **code** each take any integer from 300 to 600 (unset reads 400 in the UI). The interface weight
|
|
13
|
-
- **Line heights** — the **conversation** uses a ratio (100% ~ 160%), code an additive offset (-4 ~ +8 px); the interface has no line-height axis
|
|
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
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
|
-
- **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 its own
|
|
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
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
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
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"
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
|
|
25
25
|
| Plugin version | Supported DSH versions |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| **0.2.
|
|
27
|
+
| **0.2.1** (latest) | 0.1.5-rc.2 |
|
|
28
|
+
| 0.2.0 | 0.1.5-rc.2 |
|
|
28
29
|
|
|
29
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`.
|
|
30
31
|
|
|
@@ -56,7 +57,9 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
|
|
|
56
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
|
|
57
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
|
|
58
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
|
|
59
|
-
- **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
|
|
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
|
|
60
63
|
- **Weight readouts** — an unset weight reads 400 (DSH's own body weight, which is where the slider sits); no "unset" wording appears anywhere
|
|
61
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
|
|
62
65
|
- **Preview** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part
|
|
@@ -65,10 +68,10 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
|
|
|
65
68
|
## Development
|
|
66
69
|
|
|
67
70
|
- `src/shared.cjs` — pure-function core: family sanitizing and parsing, configuration normalization, generated stylesheet
|
|
68
|
-
- `src/index.mjs` — host half: registers the `dsh-fonttune` settings namespace (schemastery schema with length and range validation) and ships
|
|
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)
|
|
69
72
|
- `src/client.js` — browser half: the settings card, the font picker panel, font enumeration and style injection
|
|
70
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
|
|
71
|
-
- `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/` also holds headless-browser walk scripts plus the market-entry tooling — `test/market-pr.mjs` (status / update / refresh / reopen / open) and the diagnostic `test/market-inspect.mjs` (PR state, comments, CI and branch diff)
|
|
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)
|
|
72
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**
|
|
73
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
|
|
74
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
|
package/README.md
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
> [English](README.en.md) | **中文**
|
|
4
4
|
|
|
5
|
-
**DeepSeek Harness(DSH)字体插件**:**对话**(会话 Markdown
|
|
5
|
+
**DeepSeek Harness(DSH)字体插件**:**对话**(会话 Markdown 正文)拥有完整的排印控制——字体族、字号、行高、字重;**界面**(设置页、侧栏、工作区、按钮)提供字体与字重并**默认跟随对话**;**代码**独立成轴,另有连字与特性开关;整套配置可存为**预设方案**。配置入口是 **设置 → 插件 → 插件配置** 里的原生卡片(分区收起 + 摘要),改完即时生效。
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- **三套排印**:对话(会话 Markdown
|
|
9
|
+
- **三套排印**:对话(会话 Markdown 正文、表格与标题)排在第一位、拥有全部轴;界面(设置页、侧栏、工作区与标题按钮)提供**字体与字重**、默认跟随对话(可关);代码独立成轴
|
|
10
10
|
- **界面 / 对话 / 代码字体** —— 各自独立的字体回退列表,拉丁字体在前、中文字体在后;留空表示完全沿用 DSH 的字体栈
|
|
11
11
|
- **字号偏移** —— **对话**字号 -3 ~ +6 px,加在 DSH 自己的「对话字号」之上(官方数值是基准),段落、标题与行高一起跟随;代码字号独立成轴,-3 ~ +6 px。**界面没有字号轴**:经实测 DSH 的设置页、侧栏与工作区把字号写死在上游 CSS 里,唯一官方字号接口就是「对话字号」
|
|
12
|
-
- **字重** ——
|
|
13
|
-
- **行高** —— **对话**行高用倍率(100% ~ 160%);代码行高用加法偏移(-4 ~ +8 px
|
|
12
|
+
- **字重** —— **对话**、**界面**与**代码**各自 300 ~ 600 任意整数(未设置即 400,读数就显示 400)。界面字重是一条把对话 Markdown 整棵子树排除在外的 `body, body *` 规则:真机实测(活页面计算样式)它把界面的文本元素全部改到目标字重、对话 markdown 里的元素一个都没动,所以**改界面字重不会碰到对话**(对话的粗体、标题粗细也原样保留)
|
|
13
|
+
- **行高** —— **对话**行高用倍率(100% ~ 160%);代码行高用加法偏移(-4 ~ +8 px)。界面没有行高轴(同字号的原因)
|
|
14
14
|
- **代码连字** —— 三档开关(默认 / 开启 / 关闭,浏览器默认即开启,关闭可还原 `=>`、`!=` 这类连字),高级模式另有 `font-feature-settings` 特性值输入
|
|
15
15
|
- **拒绝合成样式** —— 禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰)
|
|
16
|
-
- **界面跟随对话开关** ——
|
|
16
|
+
- **界面跟随对话开关** —— 界面分区顶部的「跟随对话设置」开关(默认开启):开启时界面沿用对话的**字体与字重**,界面自己的两个控件隐藏;关闭后界面用自己的字体与字重,与对话互不影响。字号与行高只属于对话(界面没有这两个接口)
|
|
17
17
|
- **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON 分享、导入合并)
|
|
18
18
|
- **西文 / 中文分栏(简单模式)** —— 编辑模式开关:简单模式每套字体只给「西文」「中文」两个单选格,高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不改动已排好的顺序
|
|
19
19
|
- **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;搜索不到的名字可以直接「使用 xxx」新建
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
|
|
25
25
|
| 插件版本 | 支持的 DSH 版本 |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| **0.2.
|
|
27
|
+
| **0.2.1**(最新) | 0.1.5-rc.2 |
|
|
28
|
+
| 0.2.0 | 0.1.5-rc.2 |
|
|
28
29
|
|
|
29
30
|
`engines.dsh` 下限为 **0.1.5-rc.2**,兼容声明只列 **0.1.5 线的最新版本**:它包含这条线的全部修复,是同一大版本里最稳的一版,而同一大版本内的旧版本按语义化版本约定向上兼容。更早或更新的 DSH 未经验证,不作声明。安装或更新时,DSH 市场会依据 `package.json` 里的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies` 做宿主兼容预检。
|
|
30
31
|
|
|
@@ -56,7 +57,9 @@ dsh plugin --profile web add <插件目录路径>
|
|
|
56
57
|
- **分区导航** —— 卡片展开后自上而下是:编辑模式、预设方案、**对话 → 界面 → 代码**三个收起的分区(对话在最前,因为它拥有全部轴),以及直接平铺在分区下面的「拒绝合成样式」;分区标题行右侧显示当前值摘要、箭头固定在最右,同一时刻最多展开一个分区
|
|
57
58
|
- **换字体** —— 简单模式下点「西文/中文」格子选字体;高级模式下用「添加字体」与 chips 编辑器维护完整回退列表
|
|
58
59
|
- **调整字号/字重/行高** —— 滑块就在各自分区里,拖动松手即生效(拖动时读数连同单位 % 一起显示);改过的字段标「已修改」并可单独重置,卡片底部有「全部重置」
|
|
59
|
-
- **对话优先** ——
|
|
60
|
+
- **对话优先** —— 对话分区可以设置字体、字号、行高、字重四项;界面分区默认只有「跟随对话设置」开关(默认开启),关掉后才出现界面自己的字体与字重
|
|
61
|
+
- **关掉跟随不会跳变** —— 关掉开关的那一刻,界面会把**正在显示的**字体与字重写成自己的值(跟随期间它本来就是这两项),所以画面零变化、滑块从当前值继续调;对话没设的那一项则清掉界面自己的字段、保持 DSH 默认。想回到 DSH 自己的粗细点该项的「重置」(读数会回到 400)
|
|
62
|
+
- **界面字重不会碰到对话** —— 界面字重是唯一必须铺满整页的轴(DSH 把小标签的粗细写死在上游 CSS 的类规则里,窄规则够不到它),实现上是一条把**对话 Markdown 整棵子树排除在外**的规则,所以对话的段落、标题、粗体都不受影响
|
|
60
63
|
- **字重读数** —— 未设置的读数显示 400(DSH 正文的就是 400,滑块也停在 400),不会出现「未设置」这类字眼
|
|
61
64
|
- **预设方案** —— 编辑模式下方的下拉选择框(默认配置 1 ~ 5,框内右侧有与卡片一致的下拉箭头),点开选择即切换并自动保存后续改动;「改名」重命名当前方案,「导出」复制 JSON,「导入」粘贴合并。操作结果(如「已复制到剪贴板」)显示在下面那行的**右侧**,约 2.6 秒后自动淡出,不会顶动任何布局
|
|
62
65
|
- **预览** —— 对话 / 界面 / 代码分区展开后,各自分区末尾有一个预览框,只渲染该分区的样式
|
|
@@ -65,10 +68,10 @@ dsh plugin --profile web add <插件目录路径>
|
|
|
65
68
|
## 开发
|
|
66
69
|
|
|
67
70
|
- `src/shared.cjs` —— 纯函数核心:字体名消毒与解析、配置归一化、注入样式的生成
|
|
68
|
-
- `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject`
|
|
71
|
+
- `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把一个打过标记的 `<style>` 随首页下发,首帧即是设置里的字体;那个元素随后由浏览器半**认领并接管**(页面里只有这一份样式表,规则可增可删)
|
|
69
72
|
- `src/client.js` —— 浏览器半:设置卡片、选字体面板、字体枚举与样式注入
|
|
70
73
|
- `build.mjs` —— 零依赖构建:内联共享核心、套上 `window.__ModuleLoader__.load({id, factory})` 外壳、拷贝宿主半,并校验客户端 bundle 只 require shell 预注入的模块
|
|
71
|
-
- `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃);`test/` 下另有几个无头浏览器走查脚本,以及市场条目的维护脚本 `test/market-pr.mjs`(status / update / refresh / reopen / open)与诊断脚本 `test/market-inspect.mjs`(PR 状态、评论、CI 与分支差异)
|
|
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 与分支差异)
|
|
72
75
|
- `docs/` —— 0.2.0 的[背景调研](docs/research-0.2.0.md)与[功能规格](docs/spec-0.2.0.md)(设计阶段的记录,含当时的取舍理由;与最终实现不同的地方以 README/CHANGELOG 为准,两份文件文首都有说明)。**不在 npm `files` 清单内,不随包发布**
|
|
73
76
|
- 客户端模块能 require 的只有 shell 静态表里的模块(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*` 等);`dsh.client.inject` 只是加载顺序声明,不是 require 许可
|
|
74
77
|
- 改 `src/client.js` 后刷新页面即可看到变化(客户端模块按内容哈希发版,DSH 的客户端 HMR 也会推送重载);改宿主半(`src/index.mjs`)或 `cordis.patch.yml` 需重启 DSH
|
package/lib/client.js
CHANGED
|
@@ -19,13 +19,25 @@ var __dfpShared = (function () {
|
|
|
19
19
|
* configuration produces, and the sanitizing that keeps user-typed font names
|
|
20
20
|
* from breaking out of the injected stylesheet.
|
|
21
21
|
*
|
|
22
|
-
* 0.2.
|
|
23
|
-
* chrome), dialog (the conversation markdown) and code.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* 0.2.x shape: the axes are grouped into three SURFACES — interface (the UI
|
|
23
|
+
* chrome), dialog (the conversation markdown) and code. The CONVERSATION owns
|
|
24
|
+
* every axis: a family, a size offset, a weight and a line-height ratio. The
|
|
25
|
+
* interface owns a family and a weight, and by default FOLLOWS the conversation
|
|
26
|
+
* on both (the switch turns that off and reveals its own controls); DSH exposes
|
|
27
|
+
* no interface size or line-height hook at all, so those two axes are retired
|
|
28
|
+
* and the durable fields inject nothing. Code carries a family, a size offset,
|
|
29
|
+
* a weight, an additive line-height offset and the ligature/feature controls.
|
|
30
|
+
* `perTheme` enables a second value set for the dark theme, stored as a sparse
|
|
31
|
+
* override map.
|
|
32
|
+
*
|
|
33
|
+
* The interface weight is the one axis that has to reach the WHOLE page (DSH
|
|
34
|
+
* pins its labels with literal weights on class rules, so nothing narrower can
|
|
35
|
+
* move them), which is why it is a `body, body *` rule with an exclusion for
|
|
36
|
+
* the conversation markdown subtree: the conversation's own weight rule then
|
|
37
|
+
* owns that subtree completely and neither axis can disturb the other. That
|
|
38
|
+
* exclusion is what makes the axis safe — measured on a live page (computed
|
|
39
|
+
* styles, `test/weight-verify.mjs`): every text element outside the markdown
|
|
40
|
+
* subtree takes the weight, and not one element inside it moves.
|
|
29
41
|
*
|
|
30
42
|
* @module dsh-fonttune/shared
|
|
31
43
|
*/
|
|
@@ -41,7 +53,12 @@ var NAMESPACE = "dsh-fonttune";
|
|
|
41
53
|
/** Field carrying the interface CSS font-family stack (empty = leave DSH). */
|
|
42
54
|
var SANS_FIELD = "sans";
|
|
43
55
|
|
|
44
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Field carrying the dialog stack. The conversation owns the family: empty
|
|
58
|
+
* means DSH's own default family. A document written before 0.2.0 only has the
|
|
59
|
+
* interface family, so an empty dialog takes it (see `expandFollow`) — that is
|
|
60
|
+
* a migration bridge, not a live rule the card describes.
|
|
61
|
+
*/
|
|
45
62
|
var STACK_DIALOG_FIELD = "stackDialog";
|
|
46
63
|
|
|
47
64
|
/** Field carrying the code CSS font-family stack (empty = leave DSH). */
|
|
@@ -50,16 +67,20 @@ var MONO_FIELD = "mono";
|
|
|
50
67
|
/** Field carrying the interface font-size offset in px (0 = leave DSH alone). */
|
|
51
68
|
var SIZE_FIELD = "sizeOffset";
|
|
52
69
|
|
|
53
|
-
/** Field carrying the dialog font-size offset in px (0 =
|
|
70
|
+
/** Field carrying the dialog font-size offset in px (0 = DSH's own sizes). */
|
|
54
71
|
var SIZE_DIALOG_FIELD = "sizeOffsetDialog";
|
|
55
72
|
|
|
56
73
|
/** Field carrying the code font-size offset in px (0 = leave DSH alone). */
|
|
57
74
|
var CODE_SIZE_FIELD = "sizeOffsetCode";
|
|
58
75
|
|
|
59
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Field carrying the interface font weight (0 = leave DSH alone). While the
|
|
78
|
+
* interface follows the conversation this is the fallback for a conversation
|
|
79
|
+
* that sets no weight of its own.
|
|
80
|
+
*/
|
|
60
81
|
var WEIGHT_FIELD = "weight";
|
|
61
82
|
|
|
62
|
-
/** Field carrying the dialog font weight (0 =
|
|
83
|
+
/** Field carrying the dialog font weight (0 = DSH's own weights). */
|
|
63
84
|
var WEIGHT_DIALOG_FIELD = "weightDialog";
|
|
64
85
|
|
|
65
86
|
/** Field carrying the code font weight (0 = leave DSH alone). */
|
|
@@ -72,7 +93,7 @@ var CODE_WEIGHT_FIELD = "weightCode";
|
|
|
72
93
|
*/
|
|
73
94
|
var LINE_HEIGHT_FIELD = "lineHeight";
|
|
74
95
|
|
|
75
|
-
/** Field carrying the dialog line-height ratio in percent (100 =
|
|
96
|
+
/** Field carrying the dialog line-height ratio in percent (100 = DSH's own). */
|
|
76
97
|
var LINE_HEIGHT_DIALOG_FIELD = "lineHeightDialog";
|
|
77
98
|
|
|
78
99
|
/** Field carrying the code line-height offset in px (0 = leave DSH alone). */
|
|
@@ -107,10 +128,10 @@ var ACTIVE_PRESET_FIELD = "activePreset";
|
|
|
107
128
|
|
|
108
129
|
/**
|
|
109
130
|
* Whether the INTERFACE follows the conversation. The conversation owns every
|
|
110
|
-
* axis; the interface
|
|
131
|
+
* axis; the interface has a family and a weight of its own (DSH exposes no
|
|
111
132
|
* interface size or line-height hook), so on — the default — the conversation's
|
|
112
|
-
* family and weight win and the interface's own values
|
|
113
|
-
*
|
|
133
|
+
* family and weight win and the interface's own values stand as the fallback
|
|
134
|
+
* for an axis the conversation does not set.
|
|
114
135
|
*/
|
|
115
136
|
var UI_FOLLOWS_FIELD = "uiFollowsDialog";
|
|
116
137
|
|
|
@@ -1251,6 +1272,28 @@ var MARKDOWN_SELECTOR =
|
|
|
1251
1272
|
/** The dark-theme attribute DSH's ThemePresenter writes on body. */
|
|
1252
1273
|
var DARK_ATTR = "body[data-ds-dark-theme]";
|
|
1253
1274
|
|
|
1275
|
+
/**
|
|
1276
|
+
* Everything the INTERFACE weight rule must stay out of.
|
|
1277
|
+
*
|
|
1278
|
+
* A weight can only be expressed as a `body, body *` rule: DSH pins its labels
|
|
1279
|
+
* with literal weights on class rules (`…_label{font-weight:500}`,
|
|
1280
|
+
* `…_heading{font-weight:600}`) that nothing narrower reaches. That rule would
|
|
1281
|
+
* otherwise cover the conversation too, so the conversation markdown subtree is
|
|
1282
|
+
* excluded here — and with it every code surface, because the code axis owns
|
|
1283
|
+
* those and the two rules would otherwise fight over the same elements (the
|
|
1284
|
+
* `:not()` arguments raise this rule's specificity above a single class).
|
|
1285
|
+
*
|
|
1286
|
+
* `dfp-previewDialog` is the card's conversation preview: it carries the
|
|
1287
|
+
* conversation's own weight, so the interface rule must not flatten it. The
|
|
1288
|
+
* interface's own preview stays inside the rule, and `dfp-previewCode` is a
|
|
1289
|
+
* code surface like any other.
|
|
1290
|
+
*/
|
|
1291
|
+
var INTERFACE_EXCLUDES =
|
|
1292
|
+
CODE_EXCLUDES +
|
|
1293
|
+
":not(.cm-editor):not(.cm-editor *):not(.dfp-previewCode):not(.dfp-previewCode *)" +
|
|
1294
|
+
':not([class*="_markdown_" i]):not([class*="_markdown_" i] *)' +
|
|
1295
|
+
":not(.dfp-previewDialog):not(.dfp-previewDialog *)";
|
|
1296
|
+
|
|
1254
1297
|
/**
|
|
1255
1298
|
* Prefix every selector in a comma list, for the per-theme rules.
|
|
1256
1299
|
* @param {string} selectorList - a comma-separated selector list.
|
|
@@ -1271,10 +1314,17 @@ function prefixSelector(selectorList, prefix) {
|
|
|
1271
1314
|
* ------------------------------------------------------------------ */
|
|
1272
1315
|
|
|
1273
1316
|
/**
|
|
1274
|
-
* Expand the FOLLOW semantics over one normalized configuration
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1317
|
+
* Expand the FOLLOW semantics over one normalized configuration.
|
|
1318
|
+
*
|
|
1319
|
+
* The conversation owns every axis. Follow — the default — makes the interface
|
|
1320
|
+
* take the conversation's family and weight, keeping its own value only for an
|
|
1321
|
+
* axis the conversation leaves unset. Follow off leaves both of the interface's
|
|
1322
|
+
* axes standing on their own; the conversation is never touched either way,
|
|
1323
|
+
* because the interface's rules exclude the markdown subtree.
|
|
1324
|
+
*
|
|
1325
|
+
* The interface's size and line-height axes are retired and render nothing, so
|
|
1326
|
+
* nothing follows for them (see `buildAxisCss` for the measurement).
|
|
1327
|
+
*
|
|
1278
1328
|
* @param {Record<string, unknown>} raw - a normalized configuration.
|
|
1279
1329
|
* @returns {Record<string, unknown>} the set with the follow rule applied.
|
|
1280
1330
|
*/
|
|
@@ -1283,19 +1333,16 @@ function expandFollow(raw) {
|
|
|
1283
1333
|
for (var field in raw) {
|
|
1284
1334
|
if (Object.prototype.hasOwnProperty.call(raw, field)) out[field] = raw[field];
|
|
1285
1335
|
}
|
|
1286
|
-
//
|
|
1287
|
-
//
|
|
1288
|
-
//
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
// are the only ones that render for them.
|
|
1292
|
-
out[STACK_DIALOG_FIELD] = raw[STACK_DIALOG_FIELD] !== "" ? raw[STACK_DIALOG_FIELD] : raw[SANS_FIELD];
|
|
1336
|
+
// Migration bridge, not a live rule: a document written before 0.2.0 carries
|
|
1337
|
+
// the family on the interface only, so an empty conversation family takes it.
|
|
1338
|
+
// The card never describes an empty field as "follows the interface".
|
|
1339
|
+
out[STACK_DIALOG_FIELD] =
|
|
1340
|
+
raw[STACK_DIALOG_FIELD] !== "" ? raw[STACK_DIALOG_FIELD] : raw[SANS_FIELD];
|
|
1293
1341
|
out[WEIGHT_DIALOG_FIELD] = raw[WEIGHT_DIALOG_FIELD];
|
|
1294
|
-
// The interface follows the conversation: the conversation's family wins
|
|
1295
|
-
// whenever the conversation sets one, and the interface's own family stands as
|
|
1296
|
-
// the fallback otherwise.
|
|
1297
1342
|
if (raw[UI_FOLLOWS_FIELD] !== false) {
|
|
1298
1343
|
out[SANS_FIELD] = out[STACK_DIALOG_FIELD];
|
|
1344
|
+
out[WEIGHT_FIELD] =
|
|
1345
|
+
raw[WEIGHT_DIALOG_FIELD] !== WEIGHT_UNSET ? raw[WEIGHT_DIALOG_FIELD] : raw[WEIGHT_FIELD];
|
|
1299
1346
|
}
|
|
1300
1347
|
return out;
|
|
1301
1348
|
}
|
|
@@ -1348,14 +1395,16 @@ function isDormant(config) {
|
|
|
1348
1395
|
}
|
|
1349
1396
|
}
|
|
1350
1397
|
return (
|
|
1351
|
-
// The retired interface
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1398
|
+
// The retired interface size and line-height axes are deliberately absent:
|
|
1399
|
+
// a configuration that only sets them renders nothing, so it is dormant like
|
|
1400
|
+
// an empty one. The interface WEIGHT is not retired — it renders a blanket
|
|
1401
|
+
// rule — so it counts.
|
|
1354
1402
|
sanitize(axis[SANS_FIELD]) === "" &&
|
|
1355
1403
|
sanitize(axis[STACK_DIALOG_FIELD]) === "" &&
|
|
1356
1404
|
sanitize(axis[MONO_FIELD]) === "" &&
|
|
1357
1405
|
clampOffset(axis[SIZE_DIALOG_FIELD]) === 0 &&
|
|
1358
1406
|
clampOffset(axis[CODE_SIZE_FIELD]) === 0 &&
|
|
1407
|
+
clampWeight(axis[WEIGHT_FIELD]) === WEIGHT_UNSET &&
|
|
1359
1408
|
clampWeight(axis[WEIGHT_DIALOG_FIELD]) === WEIGHT_UNSET &&
|
|
1360
1409
|
clampWeight(axis[CODE_WEIGHT_FIELD]) === WEIGHT_UNSET &&
|
|
1361
1410
|
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
@@ -1389,8 +1438,14 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1389
1438
|
var mono = formatStack(parseStack(axis[MONO_FIELD]));
|
|
1390
1439
|
var declarations = [];
|
|
1391
1440
|
|
|
1392
|
-
function blanket(declarations) {
|
|
1393
|
-
|
|
1441
|
+
function blanket(declarations, excludes) {
|
|
1442
|
+
// The exclusion reaches the descendants only: `body` itself inherits
|
|
1443
|
+
// nothing, and neither the body element nor its dark attribute can be a
|
|
1444
|
+
// markdown container or a code surface.
|
|
1445
|
+
var suffix = excludes === undefined ? "" : excludes;
|
|
1446
|
+
var selector = isDark
|
|
1447
|
+
? DARK_ATTR + "," + DARK_ATTR + " *" + suffix
|
|
1448
|
+
: "body,body *" + suffix;
|
|
1394
1449
|
return selector + "{" + declarations + "}";
|
|
1395
1450
|
}
|
|
1396
1451
|
function codeRule(declarations) {
|
|
@@ -1448,22 +1503,33 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1448
1503
|
// literals inside their own CSS modules. There is therefore no interface
|
|
1449
1504
|
// size or line-height hook: scaling the ladder from these fields moved
|
|
1450
1505
|
// nothing a user could recognise as "interface", and scaling the content
|
|
1451
|
-
// chain from them moved the conversation
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1459
|
-
//
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1506
|
+
// chain from them moved the conversation. The durable fields stay in the
|
|
1507
|
+
// schema (old documents keep parsing, presets keep round-tripping) but inject
|
|
1508
|
+
// nothing at all. The interface axis that DOES work is the family (a
|
|
1509
|
+
// page-wide `font-family` on body plus the theme variable) and the weight
|
|
1510
|
+
// (below).
|
|
1511
|
+
|
|
1512
|
+
// ---- interface weight ----
|
|
1513
|
+
// Measured on rc.2 (live page, computed styles): a `body{font-weight}` rule
|
|
1514
|
+
// only reaches the elements that INHERIT their weight, while `body, body *`
|
|
1515
|
+
// also reaches the labels DSH pins with a literal weight on a class rule —
|
|
1516
|
+
// which is why the axis has to be blanket, and why the rule without the
|
|
1517
|
+
// markdown exclusion drags the conversation along with it. With the
|
|
1518
|
+
// exclusion the interface still moves and the markdown subtree does not.
|
|
1519
|
+
var interfaceWeight = axis[WEIGHT_FIELD];
|
|
1520
|
+
var codeWeight = axis[CODE_WEIGHT_FIELD];
|
|
1521
|
+
if (interfaceWeight !== WEIGHT_UNSET) {
|
|
1522
|
+
declarations.push(
|
|
1523
|
+
blanket("font-weight:" + interfaceWeight + " !important", INTERFACE_EXCLUDES)
|
|
1524
|
+
);
|
|
1525
|
+
if (codeWeight === WEIGHT_UNSET) {
|
|
1526
|
+
// Code surfaces are excluded from the blanket rule, so without this they
|
|
1527
|
+
// would inherit the interface weight from body. `normal` is what DSH
|
|
1528
|
+
// computes for them (the code `font` shorthands carry no weight), so an
|
|
1529
|
+
// unset code weight keeps code exactly as DSH shipped it.
|
|
1530
|
+
declarations.push(codeRule("font-weight:normal !important"));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1467
1533
|
|
|
1468
1534
|
// ---- dialog family (scoped; skipped when it equals the interface) ----
|
|
1469
1535
|
var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
|
|
@@ -1475,10 +1541,18 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1475
1541
|
}
|
|
1476
1542
|
}
|
|
1477
1543
|
|
|
1478
|
-
// ---- dialog weight (
|
|
1544
|
+
// ---- dialog weight (uniform inside the conversation) ----
|
|
1479
1545
|
var dialogWeight = axis[WEIGHT_DIALOG_FIELD];
|
|
1480
1546
|
if (dialogWeight !== WEIGHT_UNSET) {
|
|
1481
1547
|
declarations.push(dialogRule("font-weight:" + dialogWeight + " !important"));
|
|
1548
|
+
// The card's conversation preview simulates this surface, so it takes the
|
|
1549
|
+
// same weight (and is excluded from the interface rule above).
|
|
1550
|
+
declarations.push(
|
|
1551
|
+
(isDark ? DARK_ATTR + " " : "") +
|
|
1552
|
+
".dfp-previewDialog{font-weight:" +
|
|
1553
|
+
dialogWeight +
|
|
1554
|
+
" !important}"
|
|
1555
|
+
);
|
|
1482
1556
|
}
|
|
1483
1557
|
|
|
1484
1558
|
// ---- dialog size offset (additive, on the official content-size source) ----
|
|
@@ -1574,9 +1648,9 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1574
1648
|
);
|
|
1575
1649
|
if (codeScaled.length > 0) declarations.push(blanket(codeScaled.join(";")));
|
|
1576
1650
|
}
|
|
1577
|
-
var
|
|
1578
|
-
if (
|
|
1579
|
-
declarations.push(codeRule("font-weight:" +
|
|
1651
|
+
var codeWeightRule = axis[CODE_WEIGHT_FIELD];
|
|
1652
|
+
if (codeWeightRule !== WEIGHT_UNSET) {
|
|
1653
|
+
declarations.push(codeRule("font-weight:" + codeWeightRule + " !important"));
|
|
1580
1654
|
}
|
|
1581
1655
|
var ligatures = axis[LIGATURES_FIELD];
|
|
1582
1656
|
if (ligatures === LIGATURES_ON) {
|
|
@@ -2027,15 +2101,16 @@ if (typeof module !== "undefined" && module.exports) module.exports = shared;
|
|
|
2027
2101
|
* configuration by rewriting a single `<style>` element plus the official
|
|
2028
2102
|
* `theme.overrideTokens` layer for the family variables.
|
|
2029
2103
|
*
|
|
2030
|
-
* 0.2.
|
|
2031
|
-
* code / global fine-tuning), each collapsed to a title plus a
|
|
2032
|
-
* current values, at most one open at a time; an expanded
|
|
2033
|
-
* inline preview of just that part. The preset bar sits
|
|
2034
|
-
* dropdown of the presets plus rename/import/export, and
|
|
2035
|
-
* selected every edit auto-saves into it. The
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2038
|
-
*
|
|
2104
|
+
* 0.2.x shape: the card is an accordion — four sections (conversation /
|
|
2105
|
+
* interface / code / global fine-tuning), each collapsed to a title plus a
|
|
2106
|
+
* summary of the current values, at most one open at a time; an expanded
|
|
2107
|
+
* section ends with an inline preview of just that part. The preset bar sits
|
|
2108
|
+
* under the edit mode: a dropdown of the presets plus rename/import/export, and
|
|
2109
|
+
* with a preset selected every edit auto-saves into it. The conversation owns
|
|
2110
|
+
* every axis; the interface's section leads with the follow switch — on (the
|
|
2111
|
+
* default) it takes the conversation's family and weight and shows no controls
|
|
2112
|
+
* of its own, off reveals its own family and weight. Light and dark themes share
|
|
2113
|
+
* one value set; the per-theme editor ships in a later release.
|
|
2039
2114
|
*
|
|
2040
2115
|
* Module scope stays side-effect free: the loader materializes the factory
|
|
2041
2116
|
* only when the plugin is first used, and everything that touches the document
|
|
@@ -2063,6 +2138,7 @@ var STACK_DIALOG_FIELD = shared.STACK_DIALOG_FIELD;
|
|
|
2063
2138
|
var MONO_FIELD = shared.MONO_FIELD;
|
|
2064
2139
|
var SIZE_DIALOG_FIELD = shared.SIZE_DIALOG_FIELD;
|
|
2065
2140
|
var CODE_SIZE_FIELD = shared.CODE_SIZE_FIELD;
|
|
2141
|
+
var WEIGHT_FIELD = shared.WEIGHT_FIELD;
|
|
2066
2142
|
var WEIGHT_DIALOG_FIELD = shared.WEIGHT_DIALOG_FIELD;
|
|
2067
2143
|
var CODE_WEIGHT_FIELD = shared.CODE_WEIGHT_FIELD;
|
|
2068
2144
|
var LINE_HEIGHT_DIALOG_FIELD = shared.LINE_HEIGHT_DIALOG_FIELD;
|
|
@@ -2215,15 +2291,18 @@ var DICTS = {
|
|
|
2215
2291
|
"Adds {offset} to interface text sizes, on top of DSH's own font-size setting. 0 keeps DSH's sizes.",
|
|
2216
2292
|
"size.dialogLabel": "Conversation font size offset",
|
|
2217
2293
|
"size.dialogHint":
|
|
2218
|
-
"Adds {offset} to the conversation text sizes, on top of DSH's own font-size setting
|
|
2294
|
+
"Adds {offset} to the conversation text sizes, on top of DSH's own font-size setting. 0 keeps DSH's sizes.",
|
|
2219
2295
|
"size.codeLabel": "Code font size offset",
|
|
2220
2296
|
"size.codeHint":
|
|
2221
2297
|
"Adds {offset} to code blocks and inline code only; the interface offset does not reach them. 0 keeps DSH's sizes.",
|
|
2222
2298
|
"size.unit": "px",
|
|
2223
2299
|
|
|
2300
|
+
"weight.uiLabel": "Interface font weight",
|
|
2301
|
+
"weight.uiHint":
|
|
2302
|
+
"Sets the weight of the whole interface (sidebars, settings, buttons, headings). The conversation keeps its own.",
|
|
2224
2303
|
"weight.dialogLabel": "Conversation font weight",
|
|
2225
2304
|
"weight.dialogHint":
|
|
2226
|
-
"
|
|
2305
|
+
"Sets the weight of the conversation markdown; unset keeps DSH's own weights.",
|
|
2227
2306
|
"weight.codeLabel": "Code font weight",
|
|
2228
2307
|
"weight.codeHint":
|
|
2229
2308
|
"Overrides code blocks, inline code and terminal output only. 400 or unset keeps DSH's own weight.",
|
|
@@ -2233,7 +2312,7 @@ var DICTS = {
|
|
|
2233
2312
|
"Scales every interface line height by {ratio}. 100% keeps DSH's own line heights.",
|
|
2234
2313
|
"line.dialogLabel": "Conversation line height",
|
|
2235
2314
|
"line.dialogHint":
|
|
2236
|
-
"Scales every conversation line height by {ratio}
|
|
2315
|
+
"Scales every conversation line height by {ratio}. 100% keeps DSH's own line heights.",
|
|
2237
2316
|
"line.codeLabel": "Code line height",
|
|
2238
2317
|
"line.codeHint":
|
|
2239
2318
|
"Adds {offset} to code line heights only; 0 keeps DSH's own line heights.",
|
|
@@ -2277,6 +2356,7 @@ var DICTS = {
|
|
|
2277
2356
|
"section.close": "Collapse {name}",
|
|
2278
2357
|
|
|
2279
2358
|
"ui.follow": "Follows the conversation",
|
|
2359
|
+
"ui.followHint": "Font and weight",
|
|
2280
2360
|
"ui.own": "Own values",
|
|
2281
2361
|
"dialog.default": "DSH defaults",
|
|
2282
2362
|
|
|
@@ -2290,7 +2370,7 @@ var DICTS = {
|
|
|
2290
2370
|
|
|
2291
2371
|
"dialog.label": "Conversation font",
|
|
2292
2372
|
"dialog.hint":
|
|
2293
|
-
"Applies to the conversation markdown (paragraphs, tables, headings); code surfaces keep the code font.
|
|
2373
|
+
"Applies to the conversation markdown (paragraphs, tables, headings); code surfaces keep the code font.",
|
|
2294
2374
|
"dialogWest.label": "Conversation · Western",
|
|
2295
2375
|
"dialogEast.label": "Conversation · CJK",
|
|
2296
2376
|
|
|
@@ -2391,14 +2471,17 @@ var DICTS = {
|
|
|
2391
2471
|
"给界面文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持原样。",
|
|
2392
2472
|
"size.dialogLabel": "对话字号偏移",
|
|
2393
2473
|
"size.dialogHint":
|
|
2394
|
-
"给对话文字统一加 {offset}
|
|
2474
|
+
"给对话文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持 DSH 原样。",
|
|
2395
2475
|
"size.codeLabel": "代码字号偏移",
|
|
2396
2476
|
"size.codeHint":
|
|
2397
2477
|
"只作用于代码块与行内代码,与界面字号互不影响;0 表示保持原样。",
|
|
2398
2478
|
"size.unit": "px",
|
|
2399
2479
|
|
|
2480
|
+
"weight.uiLabel": "界面字重",
|
|
2481
|
+
"weight.uiHint":
|
|
2482
|
+
"设置整个界面的字重(侧栏、设置、按钮、标题);对话 Markdown 不受影响。",
|
|
2400
2483
|
"weight.dialogLabel": "对话字重",
|
|
2401
|
-
"weight.dialogHint": "
|
|
2484
|
+
"weight.dialogHint": "设置对话 Markdown 的粗细;未设置时保持 DSH 原本的粗细。",
|
|
2402
2485
|
"weight.codeLabel": "代码字重",
|
|
2403
2486
|
"weight.codeHint":
|
|
2404
2487
|
"只覆盖代码块、行内代码与终端输出;400 或未设置表示保持 DSH 原样。",
|
|
@@ -2406,7 +2489,7 @@ var DICTS = {
|
|
|
2406
2489
|
"line.bodyLabel": "界面行高",
|
|
2407
2490
|
"line.bodyHint": "把界面行高整体缩放为 {ratio};100% 表示保持原样。",
|
|
2408
2491
|
"line.dialogLabel": "对话行高",
|
|
2409
|
-
"line.dialogHint": "把对话行高整体缩放为 {ratio}
|
|
2492
|
+
"line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
|
|
2410
2493
|
"line.codeLabel": "代码行高",
|
|
2411
2494
|
"line.codeHint": "只给代码行高加 {offset}px;0 表示保持原样。",
|
|
2412
2495
|
"line.unit": "%",
|
|
@@ -2452,12 +2535,13 @@ var DICTS = {
|
|
|
2452
2535
|
"theme.mismatch": "当前页面是{active}主题,下方预览显示的是正在编辑的那套数值。",
|
|
2453
2536
|
|
|
2454
2537
|
"ui.follow": "跟随对话设置",
|
|
2538
|
+
"ui.followHint": "字体与字重",
|
|
2455
2539
|
"ui.own": "独立数值",
|
|
2456
2540
|
"dialog.default": "DSH 默认",
|
|
2457
2541
|
|
|
2458
2542
|
"dialog.label": "对话字体",
|
|
2459
2543
|
"dialog.hint":
|
|
2460
|
-
"作用于会话里的 Markdown
|
|
2544
|
+
"作用于会话里的 Markdown(段落、表格、标题);代码表面仍用代码字体。",
|
|
2461
2545
|
"dialogWest.label": "对话 · 西文字体",
|
|
2462
2546
|
"dialogEast.label": "对话 · 中文字体",
|
|
2463
2547
|
|
|
@@ -2792,6 +2876,15 @@ function readBaseTokens() {
|
|
|
2792
2876
|
|
|
2793
2877
|
/**
|
|
2794
2878
|
* Create the writer that keeps one `<style>` element in sync.
|
|
2879
|
+
*
|
|
2880
|
+
* The host half renders the same declarations into the served index so the
|
|
2881
|
+
* first frame is already correct, and that element carries this plugin's
|
|
2882
|
+
* `data-plugin-css` stamp. This half ADOPTS it instead of appending a second
|
|
2883
|
+
* copy: the served copy is only rebuilt on the next index render, so two
|
|
2884
|
+
* elements could never stay in sync — a rule a configuration no longer
|
|
2885
|
+
* produces (unset a weight, drop a size offset) would keep applying from the
|
|
2886
|
+
* stale copy until the user reloaded the page.
|
|
2887
|
+
*
|
|
2795
2888
|
* @param {() => Record<string, string>} tokens - reads the current base tokens.
|
|
2796
2889
|
* @returns {(config: unknown) => void} the applier.
|
|
2797
2890
|
*/
|
|
@@ -2801,9 +2894,13 @@ function createStylesheet(tokens) {
|
|
|
2801
2894
|
return function apply(config) {
|
|
2802
2895
|
if (typeof document === "undefined") return;
|
|
2803
2896
|
var css = buildFontCss(config, tokens());
|
|
2804
|
-
if (css === lastCss) return;
|
|
2897
|
+
if (css === lastCss && tag !== null && tag.isConnected) return;
|
|
2805
2898
|
lastCss = css;
|
|
2806
|
-
if (tag === null) {
|
|
2899
|
+
if (tag === null || !tag.isConnected) {
|
|
2900
|
+
// The served row first (it is already in the document), then our own.
|
|
2901
|
+
tag = document.querySelector('style[data-plugin-css="' + STYLE_TAG + '"]');
|
|
2902
|
+
}
|
|
2903
|
+
if (tag === null || !tag.isConnected) {
|
|
2807
2904
|
tag = document.createElement("style");
|
|
2808
2905
|
tag.dataset.plugin = "dsh-fonttune";
|
|
2809
2906
|
tag.dataset.pluginCss = STYLE_TAG;
|
|
@@ -3948,6 +4045,7 @@ function sectionSummaries(axis, uiFollows, t) {
|
|
|
3948
4045
|
if (!uiFollows) {
|
|
3949
4046
|
var family = axis[SANS_FIELD] === "" ? null : firstFamily(axis[SANS_FIELD]);
|
|
3950
4047
|
if (family !== null) uiParts.push(family);
|
|
4048
|
+
if (axis[WEIGHT_FIELD] !== WEIGHT_UNSET) uiParts.push(String(axis[WEIGHT_FIELD]));
|
|
3951
4049
|
}
|
|
3952
4050
|
var codeParts = [];
|
|
3953
4051
|
if (axis[MONO_FIELD] !== "") codeParts.push(firstFamily(axis[MONO_FIELD]));
|
|
@@ -3981,9 +4079,9 @@ function firstFamily(stack) {
|
|
|
3981
4079
|
*
|
|
3982
4080
|
* Structure: a top bar (edit mode), the preset bar (dropdown select +
|
|
3983
4081
|
* rename/import/export — with a preset selected every edit auto-saves into
|
|
3984
|
-
* it), and four collapsed accordion sections —
|
|
4082
|
+
* it), and four collapsed accordion sections — conversation / interface /
|
|
3985
4083
|
* code / global fine-tuning. Only one section is open at a time, so the card
|
|
3986
|
-
* never sprawls; the
|
|
4084
|
+
* never sprawls; the interface section leads with the follow switch, and an
|
|
3987
4085
|
* expanded section ends with its own inline preview of just that part. Light
|
|
3988
4086
|
* and dark themes share one value set (the per-theme editor ships in a later
|
|
3989
4087
|
* release).
|
|
@@ -4800,7 +4898,7 @@ function FontCard(props) {
|
|
|
4800
4898
|
h("div", { className: "dfp-previewCaption" }, t("preview.dialogCaption")),
|
|
4801
4899
|
h(
|
|
4802
4900
|
"div",
|
|
4803
|
-
{ className: "dfp-previewText", style: previewDialogStyle },
|
|
4901
|
+
{ className: "dfp-previewText dfp-previewDialog", style: previewDialogStyle },
|
|
4804
4902
|
t("preview.sample")
|
|
4805
4903
|
)
|
|
4806
4904
|
)
|
|
@@ -4816,9 +4914,9 @@ function FontCard(props) {
|
|
|
4816
4914
|
setExpanded(expanded === "ui" ? null : "ui");
|
|
4817
4915
|
},
|
|
4818
4916
|
},
|
|
4819
|
-
// The interface
|
|
4820
|
-
//
|
|
4821
|
-
//
|
|
4917
|
+
// The interface owns a family and a weight. While it follows the
|
|
4918
|
+
// conversation both come from the section above and only this row
|
|
4919
|
+
// shows; with the switch off its own two controls appear.
|
|
4822
4920
|
h(
|
|
4823
4921
|
"div",
|
|
4824
4922
|
{ className: "dfp-field" + (uiFollows ? " dfp-fieldLast" : "") },
|
|
@@ -4838,11 +4936,30 @@ function FontCard(props) {
|
|
|
4838
4936
|
],
|
|
4839
4937
|
disabled: !writable,
|
|
4840
4938
|
onChange: function (next) {
|
|
4841
|
-
|
|
4939
|
+
if (next === "on") {
|
|
4940
|
+
setField(UI_FOLLOWS_FIELD, true);
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4943
|
+
// Turning the switch off must not change what the page
|
|
4944
|
+
// shows: the interface keeps the values it was following,
|
|
4945
|
+
// written into its own two fields, and the sliders start
|
|
4946
|
+
// from there. An axis the conversation does not set
|
|
4947
|
+
// (empty family, unset weight) clears the interface's own
|
|
4948
|
+
// field instead, so it stays on DSH's defaults.
|
|
4949
|
+
if (editing[SANS_FIELD] === "") resetField(SANS_FIELD);
|
|
4950
|
+
else if (editing[SANS_FIELD] !== config[SANS_FIELD]) {
|
|
4951
|
+
setField(SANS_FIELD, editing[SANS_FIELD]);
|
|
4952
|
+
}
|
|
4953
|
+
if (editing[WEIGHT_FIELD] === WEIGHT_UNSET) resetField(WEIGHT_FIELD);
|
|
4954
|
+
else if (editing[WEIGHT_FIELD] !== config[WEIGHT_FIELD]) {
|
|
4955
|
+
setField(WEIGHT_FIELD, editing[WEIGHT_FIELD]);
|
|
4956
|
+
}
|
|
4957
|
+
setField(UI_FOLLOWS_FIELD, false);
|
|
4842
4958
|
},
|
|
4843
4959
|
})
|
|
4844
4960
|
)
|
|
4845
|
-
)
|
|
4961
|
+
),
|
|
4962
|
+
h("p", { className: "dfp-hint" }, t("ui.followHint"))
|
|
4846
4963
|
),
|
|
4847
4964
|
uiFollows
|
|
4848
4965
|
? null
|
|
@@ -4853,6 +4970,14 @@ function FontCard(props) {
|
|
|
4853
4970
|
westLabel: "sansWest.label",
|
|
4854
4971
|
eastLabel: "sansEast.label",
|
|
4855
4972
|
}),
|
|
4973
|
+
uiFollows
|
|
4974
|
+
? null
|
|
4975
|
+
: weightField({
|
|
4976
|
+
field: WEIGHT_FIELD,
|
|
4977
|
+
labelKey: "weight.uiLabel",
|
|
4978
|
+
hintKey: "weight.uiHint",
|
|
4979
|
+
value: editing[WEIGHT_FIELD],
|
|
4980
|
+
}),
|
|
4856
4981
|
h(
|
|
4857
4982
|
"div",
|
|
4858
4983
|
{ className: "dfp-previewBox" },
|
package/lib/index.js
CHANGED
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
* in the Host user-settings document next to every other preference. The
|
|
8
8
|
* plugin's own composition entry is the base layer, so a value the user
|
|
9
9
|
* clears falls back to the profile's config rather than to nothing.
|
|
10
|
-
* 2. Contribute
|
|
11
|
-
* the same declarations once it activates, but that
|
|
12
|
-
* already painted; injecting here is what keeps the
|
|
13
|
-
* flashing DSH's default typography.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* 2. Contribute the first-frame stylesheet to the served index as an `html` row.
|
|
11
|
+
* The browser half applies the same declarations once it activates, but that
|
|
12
|
+
* is after the shell has already painted; injecting here is what keeps the
|
|
13
|
+
* first frame from flashing DSH's default typography. The row carries this
|
|
14
|
+
* plugin's `data-plugin-css` stamp on purpose: the browser half adopts that
|
|
15
|
+
* element instead of appending a second copy, because a served copy is only
|
|
16
|
+
* rebuilt on the next index render and could never drop a rule the settings
|
|
17
|
+
* no longer produce. When `perTheme` is on, the row carries the dark theme's
|
|
18
|
+
* rules prefixed with the dark-theme attribute, so the first paint of each
|
|
19
|
+
* theme already uses that theme's own values.
|
|
16
20
|
*
|
|
17
21
|
* @module dsh-fonttune
|
|
18
22
|
*/
|
|
@@ -45,6 +49,7 @@ const {
|
|
|
45
49
|
SIZE_DIALOG_FIELD,
|
|
46
50
|
SIZE_FIELD,
|
|
47
51
|
STACK_DIALOG_FIELD,
|
|
52
|
+
STYLE_TAG,
|
|
48
53
|
WEIGHT_DIALOG_FIELD,
|
|
49
54
|
WEIGHT_FIELD,
|
|
50
55
|
} = shared;
|
|
@@ -219,11 +224,18 @@ export const Config = z.object({
|
|
|
219
224
|
|
|
220
225
|
/**
|
|
221
226
|
* One row of the structured index injection table.
|
|
222
|
-
* @typedef {{kind: "style", text: string}} StyleRow
|
|
227
|
+
* @typedef {{kind: "style", text: string} | {kind: "html", placement: "head", html: string}} StyleRow
|
|
223
228
|
*/
|
|
224
229
|
|
|
225
230
|
/**
|
|
226
231
|
* The style row carrying one configuration.
|
|
232
|
+
*
|
|
233
|
+
* Deliberately an `html` row rather than a `style` row: the served `<style>`
|
|
234
|
+
* has to carry the same `data-plugin-css` stamp the browser half writes, so the
|
|
235
|
+
* browser half can ADOPT this element instead of appending a second one. Two
|
|
236
|
+
* copies of the stylesheet cannot be kept in sync — the served row is only
|
|
237
|
+
* rebuilt on the next index render, so a rule the settings no longer produce
|
|
238
|
+
* would keep applying from the stale copy until the user reloads the page.
|
|
227
239
|
* @param {unknown} resolved - the current settings section.
|
|
228
240
|
* @returns {StyleRow} the row the web server renders into `<head>`.
|
|
229
241
|
*/
|
|
@@ -233,7 +245,15 @@ function styleRow(resolved) {
|
|
|
233
245
|
// (the dark fields remain in the schema, dormant, for a future release).
|
|
234
246
|
cfg[PER_THEME_FIELD] = false;
|
|
235
247
|
const text = buildFontCss(cfg, FALLBACK_TOKENS);
|
|
236
|
-
|
|
248
|
+
// The family sanitizer already refuses `<`, `>` and `/`, so no configuration
|
|
249
|
+
// can close the element early; the guard is here because this row is raw
|
|
250
|
+
// markup and a future input path must not be able to break out of it.
|
|
251
|
+
const safe = text.includes("</style") ? "" : text;
|
|
252
|
+
return {
|
|
253
|
+
kind: "html",
|
|
254
|
+
placement: "head",
|
|
255
|
+
html: `<style data-plugin="${NAMESPACE}" data-plugin-css="${STYLE_TAG}">${safe}</style>`,
|
|
256
|
+
};
|
|
237
257
|
}
|
|
238
258
|
|
|
239
259
|
/**
|
package/lib/shared.cjs
CHANGED
|
@@ -10,13 +10,25 @@
|
|
|
10
10
|
* configuration produces, and the sanitizing that keeps user-typed font names
|
|
11
11
|
* from breaking out of the injected stylesheet.
|
|
12
12
|
*
|
|
13
|
-
* 0.2.
|
|
14
|
-
* chrome), dialog (the conversation markdown) and code.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* 0.2.x shape: the axes are grouped into three SURFACES — interface (the UI
|
|
14
|
+
* chrome), dialog (the conversation markdown) and code. The CONVERSATION owns
|
|
15
|
+
* every axis: a family, a size offset, a weight and a line-height ratio. The
|
|
16
|
+
* interface owns a family and a weight, and by default FOLLOWS the conversation
|
|
17
|
+
* on both (the switch turns that off and reveals its own controls); DSH exposes
|
|
18
|
+
* no interface size or line-height hook at all, so those two axes are retired
|
|
19
|
+
* and the durable fields inject nothing. Code carries a family, a size offset,
|
|
20
|
+
* a weight, an additive line-height offset and the ligature/feature controls.
|
|
21
|
+
* `perTheme` enables a second value set for the dark theme, stored as a sparse
|
|
22
|
+
* override map.
|
|
23
|
+
*
|
|
24
|
+
* The interface weight is the one axis that has to reach the WHOLE page (DSH
|
|
25
|
+
* pins its labels with literal weights on class rules, so nothing narrower can
|
|
26
|
+
* move them), which is why it is a `body, body *` rule with an exclusion for
|
|
27
|
+
* the conversation markdown subtree: the conversation's own weight rule then
|
|
28
|
+
* owns that subtree completely and neither axis can disturb the other. That
|
|
29
|
+
* exclusion is what makes the axis safe — measured on a live page (computed
|
|
30
|
+
* styles, `test/weight-verify.mjs`): every text element outside the markdown
|
|
31
|
+
* subtree takes the weight, and not one element inside it moves.
|
|
20
32
|
*
|
|
21
33
|
* @module dsh-fonttune/shared
|
|
22
34
|
*/
|
|
@@ -32,7 +44,12 @@ var NAMESPACE = "dsh-fonttune";
|
|
|
32
44
|
/** Field carrying the interface CSS font-family stack (empty = leave DSH). */
|
|
33
45
|
var SANS_FIELD = "sans";
|
|
34
46
|
|
|
35
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Field carrying the dialog stack. The conversation owns the family: empty
|
|
49
|
+
* means DSH's own default family. A document written before 0.2.0 only has the
|
|
50
|
+
* interface family, so an empty dialog takes it (see `expandFollow`) — that is
|
|
51
|
+
* a migration bridge, not a live rule the card describes.
|
|
52
|
+
*/
|
|
36
53
|
var STACK_DIALOG_FIELD = "stackDialog";
|
|
37
54
|
|
|
38
55
|
/** Field carrying the code CSS font-family stack (empty = leave DSH). */
|
|
@@ -41,16 +58,20 @@ var MONO_FIELD = "mono";
|
|
|
41
58
|
/** Field carrying the interface font-size offset in px (0 = leave DSH alone). */
|
|
42
59
|
var SIZE_FIELD = "sizeOffset";
|
|
43
60
|
|
|
44
|
-
/** Field carrying the dialog font-size offset in px (0 =
|
|
61
|
+
/** Field carrying the dialog font-size offset in px (0 = DSH's own sizes). */
|
|
45
62
|
var SIZE_DIALOG_FIELD = "sizeOffsetDialog";
|
|
46
63
|
|
|
47
64
|
/** Field carrying the code font-size offset in px (0 = leave DSH alone). */
|
|
48
65
|
var CODE_SIZE_FIELD = "sizeOffsetCode";
|
|
49
66
|
|
|
50
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Field carrying the interface font weight (0 = leave DSH alone). While the
|
|
69
|
+
* interface follows the conversation this is the fallback for a conversation
|
|
70
|
+
* that sets no weight of its own.
|
|
71
|
+
*/
|
|
51
72
|
var WEIGHT_FIELD = "weight";
|
|
52
73
|
|
|
53
|
-
/** Field carrying the dialog font weight (0 =
|
|
74
|
+
/** Field carrying the dialog font weight (0 = DSH's own weights). */
|
|
54
75
|
var WEIGHT_DIALOG_FIELD = "weightDialog";
|
|
55
76
|
|
|
56
77
|
/** Field carrying the code font weight (0 = leave DSH alone). */
|
|
@@ -63,7 +84,7 @@ var CODE_WEIGHT_FIELD = "weightCode";
|
|
|
63
84
|
*/
|
|
64
85
|
var LINE_HEIGHT_FIELD = "lineHeight";
|
|
65
86
|
|
|
66
|
-
/** Field carrying the dialog line-height ratio in percent (100 =
|
|
87
|
+
/** Field carrying the dialog line-height ratio in percent (100 = DSH's own). */
|
|
67
88
|
var LINE_HEIGHT_DIALOG_FIELD = "lineHeightDialog";
|
|
68
89
|
|
|
69
90
|
/** Field carrying the code line-height offset in px (0 = leave DSH alone). */
|
|
@@ -98,10 +119,10 @@ var ACTIVE_PRESET_FIELD = "activePreset";
|
|
|
98
119
|
|
|
99
120
|
/**
|
|
100
121
|
* Whether the INTERFACE follows the conversation. The conversation owns every
|
|
101
|
-
* axis; the interface
|
|
122
|
+
* axis; the interface has a family and a weight of its own (DSH exposes no
|
|
102
123
|
* interface size or line-height hook), so on — the default — the conversation's
|
|
103
|
-
* family and weight win and the interface's own values
|
|
104
|
-
*
|
|
124
|
+
* family and weight win and the interface's own values stand as the fallback
|
|
125
|
+
* for an axis the conversation does not set.
|
|
105
126
|
*/
|
|
106
127
|
var UI_FOLLOWS_FIELD = "uiFollowsDialog";
|
|
107
128
|
|
|
@@ -1242,6 +1263,28 @@ var MARKDOWN_SELECTOR =
|
|
|
1242
1263
|
/** The dark-theme attribute DSH's ThemePresenter writes on body. */
|
|
1243
1264
|
var DARK_ATTR = "body[data-ds-dark-theme]";
|
|
1244
1265
|
|
|
1266
|
+
/**
|
|
1267
|
+
* Everything the INTERFACE weight rule must stay out of.
|
|
1268
|
+
*
|
|
1269
|
+
* A weight can only be expressed as a `body, body *` rule: DSH pins its labels
|
|
1270
|
+
* with literal weights on class rules (`…_label{font-weight:500}`,
|
|
1271
|
+
* `…_heading{font-weight:600}`) that nothing narrower reaches. That rule would
|
|
1272
|
+
* otherwise cover the conversation too, so the conversation markdown subtree is
|
|
1273
|
+
* excluded here — and with it every code surface, because the code axis owns
|
|
1274
|
+
* those and the two rules would otherwise fight over the same elements (the
|
|
1275
|
+
* `:not()` arguments raise this rule's specificity above a single class).
|
|
1276
|
+
*
|
|
1277
|
+
* `dfp-previewDialog` is the card's conversation preview: it carries the
|
|
1278
|
+
* conversation's own weight, so the interface rule must not flatten it. The
|
|
1279
|
+
* interface's own preview stays inside the rule, and `dfp-previewCode` is a
|
|
1280
|
+
* code surface like any other.
|
|
1281
|
+
*/
|
|
1282
|
+
var INTERFACE_EXCLUDES =
|
|
1283
|
+
CODE_EXCLUDES +
|
|
1284
|
+
":not(.cm-editor):not(.cm-editor *):not(.dfp-previewCode):not(.dfp-previewCode *)" +
|
|
1285
|
+
':not([class*="_markdown_" i]):not([class*="_markdown_" i] *)' +
|
|
1286
|
+
":not(.dfp-previewDialog):not(.dfp-previewDialog *)";
|
|
1287
|
+
|
|
1245
1288
|
/**
|
|
1246
1289
|
* Prefix every selector in a comma list, for the per-theme rules.
|
|
1247
1290
|
* @param {string} selectorList - a comma-separated selector list.
|
|
@@ -1262,10 +1305,17 @@ function prefixSelector(selectorList, prefix) {
|
|
|
1262
1305
|
* ------------------------------------------------------------------ */
|
|
1263
1306
|
|
|
1264
1307
|
/**
|
|
1265
|
-
* Expand the FOLLOW semantics over one normalized configuration
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1308
|
+
* Expand the FOLLOW semantics over one normalized configuration.
|
|
1309
|
+
*
|
|
1310
|
+
* The conversation owns every axis. Follow — the default — makes the interface
|
|
1311
|
+
* take the conversation's family and weight, keeping its own value only for an
|
|
1312
|
+
* axis the conversation leaves unset. Follow off leaves both of the interface's
|
|
1313
|
+
* axes standing on their own; the conversation is never touched either way,
|
|
1314
|
+
* because the interface's rules exclude the markdown subtree.
|
|
1315
|
+
*
|
|
1316
|
+
* The interface's size and line-height axes are retired and render nothing, so
|
|
1317
|
+
* nothing follows for them (see `buildAxisCss` for the measurement).
|
|
1318
|
+
*
|
|
1269
1319
|
* @param {Record<string, unknown>} raw - a normalized configuration.
|
|
1270
1320
|
* @returns {Record<string, unknown>} the set with the follow rule applied.
|
|
1271
1321
|
*/
|
|
@@ -1274,19 +1324,16 @@ function expandFollow(raw) {
|
|
|
1274
1324
|
for (var field in raw) {
|
|
1275
1325
|
if (Object.prototype.hasOwnProperty.call(raw, field)) out[field] = raw[field];
|
|
1276
1326
|
}
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
// are the only ones that render for them.
|
|
1283
|
-
out[STACK_DIALOG_FIELD] = raw[STACK_DIALOG_FIELD] !== "" ? raw[STACK_DIALOG_FIELD] : raw[SANS_FIELD];
|
|
1327
|
+
// Migration bridge, not a live rule: a document written before 0.2.0 carries
|
|
1328
|
+
// the family on the interface only, so an empty conversation family takes it.
|
|
1329
|
+
// The card never describes an empty field as "follows the interface".
|
|
1330
|
+
out[STACK_DIALOG_FIELD] =
|
|
1331
|
+
raw[STACK_DIALOG_FIELD] !== "" ? raw[STACK_DIALOG_FIELD] : raw[SANS_FIELD];
|
|
1284
1332
|
out[WEIGHT_DIALOG_FIELD] = raw[WEIGHT_DIALOG_FIELD];
|
|
1285
|
-
// The interface follows the conversation: the conversation's family wins
|
|
1286
|
-
// whenever the conversation sets one, and the interface's own family stands as
|
|
1287
|
-
// the fallback otherwise.
|
|
1288
1333
|
if (raw[UI_FOLLOWS_FIELD] !== false) {
|
|
1289
1334
|
out[SANS_FIELD] = out[STACK_DIALOG_FIELD];
|
|
1335
|
+
out[WEIGHT_FIELD] =
|
|
1336
|
+
raw[WEIGHT_DIALOG_FIELD] !== WEIGHT_UNSET ? raw[WEIGHT_DIALOG_FIELD] : raw[WEIGHT_FIELD];
|
|
1290
1337
|
}
|
|
1291
1338
|
return out;
|
|
1292
1339
|
}
|
|
@@ -1339,14 +1386,16 @@ function isDormant(config) {
|
|
|
1339
1386
|
}
|
|
1340
1387
|
}
|
|
1341
1388
|
return (
|
|
1342
|
-
// The retired interface
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1389
|
+
// The retired interface size and line-height axes are deliberately absent:
|
|
1390
|
+
// a configuration that only sets them renders nothing, so it is dormant like
|
|
1391
|
+
// an empty one. The interface WEIGHT is not retired — it renders a blanket
|
|
1392
|
+
// rule — so it counts.
|
|
1345
1393
|
sanitize(axis[SANS_FIELD]) === "" &&
|
|
1346
1394
|
sanitize(axis[STACK_DIALOG_FIELD]) === "" &&
|
|
1347
1395
|
sanitize(axis[MONO_FIELD]) === "" &&
|
|
1348
1396
|
clampOffset(axis[SIZE_DIALOG_FIELD]) === 0 &&
|
|
1349
1397
|
clampOffset(axis[CODE_SIZE_FIELD]) === 0 &&
|
|
1398
|
+
clampWeight(axis[WEIGHT_FIELD]) === WEIGHT_UNSET &&
|
|
1350
1399
|
clampWeight(axis[WEIGHT_DIALOG_FIELD]) === WEIGHT_UNSET &&
|
|
1351
1400
|
clampWeight(axis[CODE_WEIGHT_FIELD]) === WEIGHT_UNSET &&
|
|
1352
1401
|
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
@@ -1380,8 +1429,14 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1380
1429
|
var mono = formatStack(parseStack(axis[MONO_FIELD]));
|
|
1381
1430
|
var declarations = [];
|
|
1382
1431
|
|
|
1383
|
-
function blanket(declarations) {
|
|
1384
|
-
|
|
1432
|
+
function blanket(declarations, excludes) {
|
|
1433
|
+
// The exclusion reaches the descendants only: `body` itself inherits
|
|
1434
|
+
// nothing, and neither the body element nor its dark attribute can be a
|
|
1435
|
+
// markdown container or a code surface.
|
|
1436
|
+
var suffix = excludes === undefined ? "" : excludes;
|
|
1437
|
+
var selector = isDark
|
|
1438
|
+
? DARK_ATTR + "," + DARK_ATTR + " *" + suffix
|
|
1439
|
+
: "body,body *" + suffix;
|
|
1385
1440
|
return selector + "{" + declarations + "}";
|
|
1386
1441
|
}
|
|
1387
1442
|
function codeRule(declarations) {
|
|
@@ -1439,22 +1494,33 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1439
1494
|
// literals inside their own CSS modules. There is therefore no interface
|
|
1440
1495
|
// size or line-height hook: scaling the ladder from these fields moved
|
|
1441
1496
|
// nothing a user could recognise as "interface", and scaling the content
|
|
1442
|
-
// chain from them moved the conversation
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
|
|
1448
|
-
//
|
|
1449
|
-
|
|
1450
|
-
//
|
|
1451
|
-
//
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1497
|
+
// chain from them moved the conversation. The durable fields stay in the
|
|
1498
|
+
// schema (old documents keep parsing, presets keep round-tripping) but inject
|
|
1499
|
+
// nothing at all. The interface axis that DOES work is the family (a
|
|
1500
|
+
// page-wide `font-family` on body plus the theme variable) and the weight
|
|
1501
|
+
// (below).
|
|
1502
|
+
|
|
1503
|
+
// ---- interface weight ----
|
|
1504
|
+
// Measured on rc.2 (live page, computed styles): a `body{font-weight}` rule
|
|
1505
|
+
// only reaches the elements that INHERIT their weight, while `body, body *`
|
|
1506
|
+
// also reaches the labels DSH pins with a literal weight on a class rule —
|
|
1507
|
+
// which is why the axis has to be blanket, and why the rule without the
|
|
1508
|
+
// markdown exclusion drags the conversation along with it. With the
|
|
1509
|
+
// exclusion the interface still moves and the markdown subtree does not.
|
|
1510
|
+
var interfaceWeight = axis[WEIGHT_FIELD];
|
|
1511
|
+
var codeWeight = axis[CODE_WEIGHT_FIELD];
|
|
1512
|
+
if (interfaceWeight !== WEIGHT_UNSET) {
|
|
1513
|
+
declarations.push(
|
|
1514
|
+
blanket("font-weight:" + interfaceWeight + " !important", INTERFACE_EXCLUDES)
|
|
1515
|
+
);
|
|
1516
|
+
if (codeWeight === WEIGHT_UNSET) {
|
|
1517
|
+
// Code surfaces are excluded from the blanket rule, so without this they
|
|
1518
|
+
// would inherit the interface weight from body. `normal` is what DSH
|
|
1519
|
+
// computes for them (the code `font` shorthands carry no weight), so an
|
|
1520
|
+
// unset code weight keeps code exactly as DSH shipped it.
|
|
1521
|
+
declarations.push(codeRule("font-weight:normal !important"));
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1458
1524
|
|
|
1459
1525
|
// ---- dialog family (scoped; skipped when it equals the interface) ----
|
|
1460
1526
|
var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
|
|
@@ -1466,10 +1532,18 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1466
1532
|
}
|
|
1467
1533
|
}
|
|
1468
1534
|
|
|
1469
|
-
// ---- dialog weight (
|
|
1535
|
+
// ---- dialog weight (uniform inside the conversation) ----
|
|
1470
1536
|
var dialogWeight = axis[WEIGHT_DIALOG_FIELD];
|
|
1471
1537
|
if (dialogWeight !== WEIGHT_UNSET) {
|
|
1472
1538
|
declarations.push(dialogRule("font-weight:" + dialogWeight + " !important"));
|
|
1539
|
+
// The card's conversation preview simulates this surface, so it takes the
|
|
1540
|
+
// same weight (and is excluded from the interface rule above).
|
|
1541
|
+
declarations.push(
|
|
1542
|
+
(isDark ? DARK_ATTR + " " : "") +
|
|
1543
|
+
".dfp-previewDialog{font-weight:" +
|
|
1544
|
+
dialogWeight +
|
|
1545
|
+
" !important}"
|
|
1546
|
+
);
|
|
1473
1547
|
}
|
|
1474
1548
|
|
|
1475
1549
|
// ---- dialog size offset (additive, on the official content-size source) ----
|
|
@@ -1565,9 +1639,9 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1565
1639
|
);
|
|
1566
1640
|
if (codeScaled.length > 0) declarations.push(blanket(codeScaled.join(";")));
|
|
1567
1641
|
}
|
|
1568
|
-
var
|
|
1569
|
-
if (
|
|
1570
|
-
declarations.push(codeRule("font-weight:" +
|
|
1642
|
+
var codeWeightRule = axis[CODE_WEIGHT_FIELD];
|
|
1643
|
+
if (codeWeightRule !== WEIGHT_UNSET) {
|
|
1644
|
+
declarations.push(codeRule("font-weight:" + codeWeightRule + " !important"));
|
|
1571
1645
|
}
|
|
1572
1646
|
var ligatures = axis[LIGATURES_FIELD];
|
|
1573
1647
|
if (ligatures === LIGATURES_ON) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fonttune",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|