dsh-fonttune 0.3.0 → 0.3.2

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,21 @@
2
2
 
3
3
  All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.3.2] - 2026-09-28
6
+
7
+ ### Changed
8
+ - **DSH 0.2.0-rc.1 is supported**: releases now also run on the 0.2 line, and the compatibility table names the host line each release needs.
9
+
10
+ ## [0.3.1] - 2026-09-28
11
+
12
+ ### Fixed
13
+ - **The upper positions of the weight slider jumped to a lower number**: two of them were read as an absolute weight and converted, so the third step showed as −1 and the fourth rendered exactly like 0. A position now stays where it is put.
14
+ - **With the conversation weight at 0, the conversation still picked up the interface offset**: the two axes shared a range, so 0 carried the interface's offset as well and the 0 and +1 positions looked nearly the same. Each axis now keeps to its own scope, and 0 is DSH's own weight.
15
+ - **The line-height preview in the card was taller at 100% than at the notches above it**: the preview and the conversation did not share a base. They do now, so every notch is taller in the preview as well.
16
+
17
+ ### Changed
18
+ - **The weight slider's positions and step come from the font**: one step is one pair of neighbouring faces, and the range starts where body text and bold text can still swap faces — a family whose body text has more weights gets more positions (the range is no longer held back by the heading layer), and the heading layer stops once it is at its boldest; changing the family measures it again.
19
+
5
20
  ## [0.3.0] - 2026-09-28
6
21
 
7
22
  ### Fixed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  **dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.3.2] - 2026-09-28
6
+
7
+ ### 变更
8
+ - **支持的 DSH 版本增加 0.2.0-rc.1**:这一版起在 0.2 线上同样可用;安装前可据兼容表判断某个版本需要哪条宿主线。
9
+
10
+ ## [0.3.1] - 2026-09-28
11
+
12
+ ### 修复
13
+ - **字重滑块偏上的档位会跳到较小的数值**:此前有两个档位会被换算成别的数值,表现为拖到第三档显示成 −1、第四档与 0 档的粗细看起来完全一样。现在拖到哪一档就停在哪一档。
14
+ - **「对话字重偏移」为 0 时,对话还是会跟着「界面字重偏移」一起变粗**:两个轴的范围互相串了,界面设了偏移时对话的 0 档也带上了那个偏移,于是 0 与 +1 两档看起来几乎一样。现在每个轴只管自己的范围,0 档就是 DSH 原样。
15
+ - **卡片里的行高预览在 100% 时比更高的档位还高**:预览与真实对话用的不是同一个基准。现在两者一致,预览里也是每一档都更高。
16
+
17
+ ### 变更
18
+ - **字重滑块的档位与档距按字体实测**:一档就是字体相邻的两张字形,范围从「正文与加粗文字都还能换到字形」的地方起算——正文字重档位多的字体给更多档位(正文能走的范围不再被标题压小),标题那一层到顶后停住;换字体会重新测量。
19
+
5
20
  ## [0.3.0] - 2026-09-28
6
21
 
7
22
  ### 修复
@@ -12,7 +27,7 @@
12
27
 
13
28
  ### 变更
14
29
  - **三个字重轴统一,并按档调整**:对话、界面与代码都改成「在每个元素自身粗细上叠加」的偏移,滑块按当前字体**实际能渲染的粗细分档**——读数就是档数(例如 `-2 … +4`,0 表示保持 DSH 原样),字体有几档就有几档,每挪一格都是一次看得见的变化;换字体后档数会重新测量。
15
- - **旧版本存的绝对字重会自动换算**(400 → 0、450 → +1),不需要重设;文档里保存的仍是通用数值,换字体不会让已有设置失效。
30
+ - **旧版本存的绝对字重会自动换算**(400 → 0、450 → +1),不需要重设;换字体也不会让已有设置失效。
16
31
 
17
32
  ## [0.2.7] - 2026-09-24
18
33
 
package/README.en.md CHANGED
@@ -9,7 +9,7 @@
9
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 — and it is applied **once, at the source** (H1/H2/H3 all derive from the same delta, so the hierarchy holds at any offset and no heading comes out smaller than expected); 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-weight offsets** — the **conversation**, the **interface** and **code** all take the same **relative offset** (-100 ~ +200, unset reads 0 and the readout is signed): the offset is added to each element's **own** weight, so the heading hierarchy DSH ships (h1~h3 700, h4~h6 and bold 600, table head 500, body 400) survives in both directions — headings stay clearly bolder than body text at any setting, and bold text inside a code surface stays bold. The interface offset is one rule over `body` plus an element table, with the whole conversation markdown subtree excluded: measured against computed styles on a live page, it takes every interface text element to the target and moves nothing inside the markdown subtree, so **changing the interface weight never touches the conversation**. The slider **counts steps**: the readout is the step count (for example `-2 -1 0 +1 +2 +3 +4`) and **the font decides how many there are** — the card measures how many distinct weights the chosen family can actually render, then works out the range and the size of one step, so every notch is a visible change and the numbers stay small. What is stored is still a weight offset (older values are converted), so changing the font never breaks a setting; a family with only regular and bold offers three positions in total, and continuous control needs a family with more weights or a variable font
12
+ - **Font-weight offsets** — the **conversation**, the **interface** and **code** all take the same **relative offset** (-100 ~ +200, unset reads 0 and the readout is signed): the offset is added to each element's **own** weight, so the heading hierarchy DSH ships (h1~h3 700, h4~h6 and bold 600, table head 500, body 400) survives in both directions — headings stay clearly bolder than body text at any setting, and bold text inside a code surface stays bold. The interface offset is one rule over `body` plus an element table, with the whole conversation markdown subtree excluded: measured against computed styles on a live page, it takes every interface text element to the target and moves nothing inside the markdown subtree, so **changing the interface weight never touches the conversation**. The slider **counts steps**: the readout is the step count (for example `-1 0 +1`) and **the font decides how many there are** — the card measures how many distinct weights the chosen family can render for body text and for bold text, so one step is one pair of neighbouring faces and the range ends where those two stop swapping faces (the heading layer stops once it is at its boldest) — every notch is a visible change and the numbers stay small. What is stored is still a weight offset (older values are converted), so changing the font never breaks a setting; a family with only regular and bold offers three positions in total, and continuous control needs a family with more weights or a variable font
13
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** (off by default, i.e. synthesis is allowed) — turning it on stops the browser from faking italic and bold for CJK faces, which have neither, so marked text stays upright; left off, the browser synthesises by its own rules, which at least gives a visible "bold" step for a family with no bold face
@@ -22,11 +22,13 @@
22
22
 
23
23
  ## Compatibility
24
24
 
25
- **One package serves three host lines** — 0.1.5-rc.3 (the floor), 0.1.7-alpha.1 / alpha.2 and 0.1.7-rc.1 / rc.2 — and the install is the same on any of them: all three were verified for "the settings card opens, the stylesheet really reaches the page, and a change made in the card is written through".
25
+ **One package serves four host lines** — 0.1.5-rc.3 (the floor), 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2 and 0.2.0-rc.1 — and the install is the same on any of them: all three were verified for "the settings card opens, the stylesheet really reaches the page, and a change made in the card is written through".
26
26
 
27
27
  | Plugin version | Supported DSH versions |
28
28
  | --- | --- |
29
- | **0.3.0** (latest) | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2 |
29
+ | **0.3.2** (latest) | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2, 0.2.0-rc.1 |
30
+ | 0.3.1 | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2 |
31
+ | 0.3.0 | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2 |
30
32
  | 0.2.7 | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2 |
31
33
  | 0.2.6 | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 |
32
34
  | 0.2.5 | 0.1.5-rc.1 / rc.2 / rc.3, 0.1.7-alpha.1 / alpha.2 |
@@ -36,7 +38,7 @@
36
38
  | 0.2.1 | 0.1.5-rc.2 |
37
39
  | 0.2.0 | 0.1.5-rc.2 |
38
40
 
39
- `engines.dsh` is declared as **`>=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`** (a prerelease needs its own branch: node-semver admits one only when some comparator in the range sits on that version's exact `major.minor.patch` tuple and carries a prerelease tag of its own, so a plain `>=0.1.5-rc.3` never matches `0.1.7-alpha.1`). Every difference between the lines is settled at runtime rather than by version number:
41
+ `engines.dsh` is declared as **`>=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.3.0-0`** (a prerelease needs its own branch: node-semver admits one only when some comparator in the range sits on that version's exact `major.minor.patch` tuple and carries a prerelease tag of its own, so a plain `>=0.1.5-rc.3` never matches `0.1.7-alpha.1`). Every difference between the lines is settled at runtime rather than by version number:
40
42
 
41
43
  - **The settings service**: up to 0.1.5-rc.x it is `settingsScope.bind({namespace})` (by namespace); 0.1.7-alpha.x and 0.1.7-rc.x use `configForms.get(<profile entry id>)` (by entry id, which the installing profile decides — so the plugin claims the form whose served schema names its own fields). Both expose the same `getSnapshot`/`subscribe`/`set`/`unset` face, so one implementation covers them.
42
44
  - **The loader row id IS the settings key**: from 0.1.7-rc.1 on, settings are stored per entry id, and the first start imports the removed `settings.yaml` by matching section names against it — so the row id this package ships equals its settings namespace (`dsh-fonttune`). That same id also lets the package be installed through the profile's `dsh.profile.bundles`: the plugin manager then shows it as running while the configuration page stays reachable.
@@ -79,7 +81,7 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
79
81
  - **Switching following off never jumps** — the moment the switch goes off, the interface writes the font and weight offset 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 (all three weight offsets return to 0)
80
82
  - **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
81
83
  - **Weight readouts** — all three weight axes read 0 when unset (the slider sits in the middle); no "unset" wording appears anywhere, and an absolute weight stored by an older version (300 ~ 600) is converted to its equivalent offset on read (400 → 0, 450 → +50), so nothing has to be set again
82
- - **The weight slider counts steps** — the card measures how many distinct weights the current family can render and derives the slider's range and step size from that (the readout is the step count, e.g. `-2` … `+4`), so every notch is a visible change and the numbers stay small; changing the family re-measures it
84
+ - **The weight slider counts steps** — the card measures how many distinct weights the current family can render for body text and for bold text, one step being one pair of neighbouring faces, and the range ending where those two stop swapping faces (the readout is the step count, e.g. `-2 … +2`), so every notch is a visible change and the numbers stay small; changing the family re-measures it
83
85
  - **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
84
86
  - **Preview** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part
85
87
  - Settings are stored in DSH's settings document (the profile's `cordis.patch.yml` from 0.1.7 on, `settings.yaml` before that), so they travel with the profile
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  - **三套排印**:对话(会话 Markdown 正文、表格与标题)排在第一位、拥有全部轴;界面(设置页、侧栏、工作区与标题按钮)提供**字体与字重**、默认跟随对话(可关);代码独立成轴
10
10
  - **界面 / 对话 / 代码字体** —— 各自独立的字体回退列表,拉丁字体在前、中文字体在后;留空表示完全沿用 DSH 的字体栈
11
11
  - **字号偏移** —— **对话**字号 -3 ~ +6 px,加在 DSH 自己的「对话字号」之上(官方数值是基准),段落、标题与行高一起跟随,且**只在源头加一次**(H1/H2/H3 的尺寸都由同一条 delta 派生,任意偏移下层级都不变、也不会出现某个标题偏小);代码字号独立成轴,-3 ~ +6 px。**界面没有字号轴**:经实测 DSH 的设置页、侧栏与工作区把字号写死在上游 CSS 里,唯一官方字号接口就是「对话字号」
12
- - **字重偏移** —— **对话、界面与代码都是同一种相对偏移**(-100 ~ +200,未设置即 0、读数按 `+n` / `-n` 显示):偏移叠加在每个元素**自身的**字重上,标题仍按 DSH 的层级(h1~h3 700、h4~h6 与加粗 600、表头 500、正文 400)逐级保持"标题明显粗于正文",调重调轻都不会把层级抹平,代码块里的粗体也不会被压平。界面偏移是一条把对话 Markdown 整棵子树排除在外、只铺 `body` 与自带字重的元素表的规则:真机实测(活页面计算样式)它把界面的文本元素改到目标值、对话 markdown 里的元素一个都没动,所以**改界面字重不会碰到对话**。滑块**按档计数**:读数就是档数(例如 `-2 -1 0 +1 +2 +3 +4`),**档数由字体决定**——卡片会在页面里量出所选字体实际能渲染出几种粗细,再据此决定一共有几档、每一档差多少,所以每挪一格都是一次看得见的变化,换字体会重新测量。文档里存的仍是字重偏移(旧值自动换算),换字体不会把设置改坏;字体只有常规与粗体两档时全范围就只有 3 档,要连续可选用字重档位多的字体或可变字体
12
+ - **字重偏移** —— **对话、界面与代码都是同一种相对偏移**(-100 ~ +200,未设置即 0、读数按 `+n` / `-n` 显示):偏移叠加在每个元素**自身的**字重上,标题仍按 DSH 的层级(h1~h3 700、h4~h6 与加粗 600、表头 500、正文 400)逐级保持"标题明显粗于正文",调重调轻都不会把层级抹平,代码块里的粗体也不会被压平。界面偏移是一条把对话 Markdown 整棵子树排除在外、只铺 `body` 与自带字重的元素表的规则:真机实测(活页面计算样式)它把界面的文本元素改到目标值、对话 markdown 里的元素一个都没动,所以**改界面字重不会碰到对话**。滑块**按档计数**:读数就是档数(例如 `-1 0 +1`),**档数由字体决定**——卡片会在页面里量出所选字体在正文与加粗文字两处实际能渲染出几种粗细,一档就是相邻的两张字形,范围也以这两处还能换到字形为限(标题那一层到顶后停住),所以每挪一格都是看得见的变化,数字也不会出现"几百几百";换字体会重新测量。文档里存的仍是字重偏移(旧值自动换算),换字体不会把设置改坏;字体只有常规与粗体两档时全范围就只有 3 档,要连续可选用字重档位多的字体或可变字体
13
13
  - **行高** —— **对话**行高用倍率(100% ~ 160%);代码行高用加法偏移(-4 ~ +8 px)。界面没有行高轴(同字号的原因)
14
14
  - **代码连字** —— 三档开关(默认 / 开启 / 关闭,浏览器默认即开启,关闭可还原 `=>`、`!=` 这类连字),高级模式另有 `font-feature-settings` 特性值输入
15
15
  - **拒绝合成样式**(默认关闭 = 允许合成) —— 打开后禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰);关着(默认)时由浏览器按自己的规则合成,字体缺粗体字形时至少还能看出"粗体"这一档
@@ -22,11 +22,13 @@
22
22
 
23
23
  ## 兼容性
24
24
 
25
- **一份包同时支持三条宿主线**——0.1.5-rc.3(下限)、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2:安装方式与宿主线无关,三条线都验证过「设置卡片可用、样式真的作用到页面、卡片上的改动写得进去」。
25
+ **一份包同时支持四条宿主线**——0.1.5-rc.3(下限)、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2、0.2.0-rc.1:安装方式与宿主线无关,三条线都验证过「设置卡片可用、样式真的作用到页面、卡片上的改动写得进去」。
26
26
 
27
27
  | 插件版本 | 支持的 DSH 版本 |
28
28
  | --- | --- |
29
- | **0.3.0**(最新) | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2 |
29
+ | **0.3.2**(最新) | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2、0.2.0-rc.1 |
30
+ | 0.3.1 | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2 |
31
+ | 0.3.0 | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2 |
30
32
  | 0.2.7 | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2 |
31
33
  | 0.2.6 | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 |
32
34
  | 0.2.5 | 0.1.5-rc.1 / rc.2 / rc.3、0.1.7-alpha.1 / alpha.2 |
@@ -36,7 +38,7 @@
36
38
  | 0.2.1 | 0.1.5-rc.2 |
37
39
  | 0.2.0 | 0.1.5-rc.2 |
38
40
 
39
- `engines.dsh` 声明为 **`>=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`**(预发布版本必须显式留分支:node-semver 只在一个范围的某个比较符与该版本落在同一个 `major.minor.patch` 元组、且自身带预发布标签时才放行,写成 `>=0.1.5-rc.3` 匹配不到 `0.1.7-alpha.1`)。三条线的差别**全部在运行时二选一,不看版本号**:
41
+ `engines.dsh` 声明为 **`>=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.3.0-0`**(预发布版本必须显式留分支:node-semver 只在一个范围的某个比较符与该版本落在同一个 `major.minor.patch` 元组、且自身带预发布标签时才放行,写成 `>=0.1.5-rc.3` 匹配不到 `0.1.7-alpha.1`)。三条线的差别**全部在运行时二选一,不看版本号**:
40
42
 
41
43
  - **设置服务**:0.1.5-rc.x 是 `settingsScope.bind({namespace})`(按命名空间);0.1.7-alpha.x 与 0.1.7-rc.x 换成了 `configForms.get(<profile 条目 id>)`(按条目 id,id 由安装方的 profile 决定,所以插件靠「宿主服务的那份 schema 里有没有本插件自己的字段」来认领)。两者都是 `getSnapshot/subscribe/set/unset` 同一张脸,插件只写一套。
42
44
  - **loader 行 id 就是设置键**:0.1.7-rc.1 起设置按入口 id 存档,首次启动还会按**同名**把旧的 `settings.yaml` 搬过去——本包自带的 loader 行 id 因此与设置命名空间一致(`dsh-fonttune`)。同名的行 id 也让这个包可以写进 profile 的 `dsh.profile.bundles` 安装:插件管理页会把它显示为运行中,配置页照旧可达。
@@ -79,7 +81,7 @@ dsh plugin --profile web add <插件目录路径>
79
81
  - **关掉跟随不会跳变** —— 关掉开关的那一刻,界面会把**正在显示的**字体与字重偏移写成自己的值(跟随期间它本来就是这两项),所以画面零变化、滑块从当前值继续调;对话没设的那一项则清掉界面自己的字段、保持 DSH 默认。想回到 DSH 自己的粗细点该项的「重置」(三个字重偏移读数都回到 0)
80
82
  - **界面字重不会碰到对话** —— 界面字重是唯一必须铺满整页的轴(DSH 把小标签的粗细写死在上游 CSS 的类规则里,窄规则够不到它),实现上是一条把**对话 Markdown 整棵子树排除在外**的规则,所以对话的段落、标题、粗体都不受影响
81
83
  - **字重读数** —— 三个字重轴的未设置读数都显示 0(滑块停在正中);不会出现「未设置」这类字眼;旧版本存的绝对字重(300 ~ 600)会在读取时自动换算成等价偏移(400 → 0、450 → +50),无需手动重设
82
- - **字重滑块按档计数** —— 卡片会量出当前字体能渲染出几档粗细,据此决定滑块的上下限与每档的大小(读数就是档数,如 `-2` ~ `+4`),所以每挪一格都看得出变化、也不会出现"几百几百"的数字;换字体后档数会重新测量
84
+ - **字重滑块按档计数** —— 卡片会量出当前字体在正文与加粗文字两处各能渲染出几档粗细,一档 = 相邻两张字形、上下限以这两处还能换字形为限(读数就是档数,如 `-2 … +2`),所以每挪一格都看得出变化、也不会出现"几百几百"的数字;换字体后档数会重新测量
83
85
  - **预设方案** —— 编辑模式下方的下拉选择框(默认配置 1 ~ 5,框内右侧有与卡片一致的下拉箭头),点开选择即切换并自动保存后续改动;「改名」重命名当前方案,「导出」复制 JSON,「导入」粘贴合并。操作结果(如「已复制到剪贴板」)显示在下面那行的**右侧**,约 2.6 秒后自动淡出,不会顶动任何布局
84
86
  - **预览** —— 对话 / 界面 / 代码分区展开后,各自分区末尾有一个预览框,只渲染该分区的样式
85
87
  - 设置保存在 DSH 的设置文档里(0.1.7 起是 profile 的 `cordis.patch.yml`,更早的版本是 `settings.yaml`),跟随 profile 走
package/lib/client.js CHANGED
@@ -176,6 +176,16 @@ var WEIGHT_DELTA_CEIL = 600;
176
176
  /** The weight DSH gives an element that declares none of its own. */
177
177
  var WEIGHT_BASE = 400;
178
178
 
179
+ /**
180
+ * The weight range CSS accepts at all.
181
+ *
182
+ * Named because it is easy to confuse with `WEIGHT_MIN…WEIGHT_MAX`, which is the
183
+ * window an ABSOLUTE weight from a pre-relative document occupies (300…600) and
184
+ * has nothing to do with what a font can render.
185
+ */
186
+ var WEIGHT_CSS_MIN = 100;
187
+ var WEIGHT_CSS_MAX = 900;
188
+
179
189
  /**
180
190
  * The weight ladder of the conversation: each group carries the weight DSH's
181
191
  * own stylesheet gives those elements, so an offset lands ON TOP of it.
@@ -195,6 +205,43 @@ var DIALOG_WEIGHT_GROUPS = [
195
205
  },
196
206
  ];
197
207
 
208
+ /**
209
+ * The heaviest base in the conversation ladder — the layer with the least room.
210
+ *
211
+ * Derived from the ladder so the two cannot drift apart.
212
+ */
213
+ var WEIGHT_LADDER_TOP = DIALOG_WEIGHT_GROUPS.reduce(function (top, group) {
214
+ return group.base > top ? group.base : top;
215
+ }, WEIGHT_BASE);
216
+
217
+ /**
218
+ * How far above its base an offset may reach at all.
219
+ *
220
+ * Every axis applies the offset to each element's OWN weight, and the heaviest
221
+ * layer of the ladder sits at `WEIGHT_LADDER_TOP`; past `WEIGHT_CSS_MAX` minus
222
+ * that, the heaviest layer cannot move any further, so a longer range would only
223
+ * add positions that change nothing for the boldest text on screen.
224
+ *
225
+ * Staying inside it has a second, load-bearing effect: offsets then never land in
226
+ * `WEIGHT_MIN…WEIGHT_MAX`, the range an ABSOLUTE weight from a pre-relative
227
+ * document occupies (see `clampWeightDelta`). The two meanings cannot be confused,
228
+ * so a stored offset is never rescaled into a different one.
229
+ */
230
+ var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX - WEIGHT_LADDER_TOP;
231
+
232
+ /**
233
+ * The heaviest layer a notch still has to move: bold text.
234
+ *
235
+ * The heading layer above it is deliberately left out of that requirement. A heavy
236
+ * family spaces its top faces far apart (a 700 cut next to a 900), so headings
237
+ * reach their heaviest rendering part-way up the range and would otherwise hold the
238
+ * whole control down to one wide step. Every notch must move the text you read and
239
+ * the text you emphasise; a heading that is already at its boldest face stays there.
240
+ */
241
+ var WEIGHT_LADDER_STRONG = DIALOG_WEIGHT_GROUPS.reduce(function (strong, group) {
242
+ return group.base < WEIGHT_LADDER_TOP && group.base > strong ? group.base : strong;
243
+ }, WEIGHT_BASE);
244
+
198
245
  /** Allowed line-height ratio range, in percent (100 = untouched). */
199
246
  var LINE_HEIGHT_MIN = 100;
200
247
  var LINE_HEIGHT_MAX = 160;
@@ -1669,19 +1716,25 @@ function ceilTo(value, step) {
1669
1716
 
1670
1717
  /**
1671
1718
  * Does every position of `[min..max]` in `step` increments render differently?
1719
+ *
1720
+ * Checked at every base the offset is added to: a step that is fine for body text
1721
+ * can be a no-op one layer up, where the faces are further apart.
1672
1722
  * @param {(weight: number) => string|number} sample - probe.
1673
- * @param {number} base - the weight the offsets are added to.
1723
+ * @param {number[]} bases - the weights the offsets are added to.
1674
1724
  * @param {number} min - first offset.
1675
1725
  * @param {number} max - last offset.
1676
1726
  * @param {number} step - offset increment.
1677
- * @returns {boolean} true when no two neighbours render the same.
1678
- */
1679
- function everyPositionDiffers(sample, base, min, max, step) {
1680
- var previous = null;
1681
- for (var position = min; position <= max; position += step) {
1682
- var signature = sample(base + position);
1683
- if (previous !== null && signature === previous) return false;
1684
- previous = signature;
1727
+ * @returns {boolean} true when no two neighbours render the same at any base.
1728
+ */
1729
+ function everyPositionDiffers(sample, bases, min, max, step) {
1730
+ for (var index = 0; index < bases.length; index += 1) {
1731
+ var base = bases[index];
1732
+ var previous = null;
1733
+ for (var position = min; position <= max; position += step) {
1734
+ var signature = sample(base + position);
1735
+ if (previous !== null && signature === previous) return false;
1736
+ previous = signature;
1737
+ }
1685
1738
  }
1686
1739
  return true;
1687
1740
  }
@@ -1692,17 +1745,21 @@ function everyPositionDiffers(sample, base, min, max, step) {
1692
1745
  * A font with six faces can be made lighter or heavier only as far as those faces
1693
1746
  * reach, and only in steps between them; a family with a continuous axis gets a
1694
1747
  * fine step and a wide range. Measured on a weight grid:
1695
- * - the top of the range is the LOWEST weight that already renders as heavy as
1696
- * the family can (asking for more cannot change anything), and the bottom is
1697
- * the highest weight that still renders as light as it can;
1698
- * - the step is the finest candidate whose positions all differ, so no notch is
1699
- * a no-op. A family whose faces are unevenly spaced (a 700 next to a 900) may
1700
- * end up with one repeated position whichever uniform step is used — the
1701
- * coarsest candidate is then the honest answer, never a crash.
1748
+ * - the downward reach is the highest weight that still renders as light as the
1749
+ * family can (asking for less cannot change anything);
1750
+ * - the upward reach is the LOWEST weight at or above the layer a notch has to
1751
+ * move that already renders as heavy as the family can, so the range ends where
1752
+ * bold text stops changing instead of running past it (see `WEIGHT_DELTA_UP_LIMIT`
1753
+ * for why that ceiling is also the widest one allowed);
1754
+ * - the step is the finest candidate whose positions all differ at every base a
1755
+ * notch has to move, so no notch is a no-op for body text or bold text. A family whose faces are unevenly spaced
1756
+ * (a 700 next to a 900) may end up with one repeated position whichever uniform
1757
+ * step is used — the coarsest candidate is then the honest answer, never a crash.
1702
1758
  * @param {(weight: number) => string|number} sample - renders a probe at one
1703
1759
  * weight and returns a signature; equal signatures mean identical rendering.
1704
- * @param {{candidates?: number[], floor?: number, ceil?: number}} [options]
1705
- * overrides for the candidates and the widest allowed range.
1760
+ * @param {{candidates?: number[], floor?: number, ceil?: number, top?: number}}
1761
+ * [options] overrides for the candidates, the widest allowed range and the
1762
+ * heaviest base the offset is added to (the ladder's, or the axis's own).
1706
1763
  * @returns {{min: number, max: number, step: number}} the slider's shape; the
1707
1764
  * range always contains 0, which is the "leave DSH alone" position.
1708
1765
  */
@@ -1714,6 +1771,7 @@ function weightProfileFrom(sample, options) {
1714
1771
  : WEIGHT_STEP_CANDIDATES;
1715
1772
  var floor = typeof settings.floor === "number" ? settings.floor : WEIGHT_DELTA_FLOOR;
1716
1773
  var ceil = typeof settings.ceil === "number" ? settings.ceil : WEIGHT_DELTA_CEIL;
1774
+ var top = typeof settings.top === "number" ? settings.top : WEIGHT_LADDER_STRONG;
1717
1775
  var topSignature = sample(WEIGHT_PROBE_HIGH);
1718
1776
  var bottomSignature = sample(WEIGHT_PROBE_LOW);
1719
1777
  var heaviest = WEIGHT_PROBE_HIGH;
@@ -1730,23 +1788,41 @@ function weightProfileFrom(sample, options) {
1730
1788
  break;
1731
1789
  }
1732
1790
  }
1733
- var reachUp = Math.min(ceil, Math.max(0, heaviest - WEIGHT_BASE));
1791
+ // How far the offset can go before NO layer that must move can move any more:
1792
+ // the widest horizon among those bases, since a layer that could still change
1793
+ // must not be cut off by one that already sits at its heaviest face.
1794
+ var headroom = Math.max(0, heaviest - WEIGHT_BASE);
1795
+ if (top !== WEIGHT_BASE) {
1796
+ for (var up = top; up <= WEIGHT_PROBE_HIGH; up += WEIGHT_PROBE_STEP) {
1797
+ if (sample(up) === topSignature) {
1798
+ headroom = Math.max(headroom, up - top);
1799
+ break;
1800
+ }
1801
+ }
1802
+ }
1803
+ var reachUp = Math.min(ceil, WEIGHT_DELTA_UP_LIMIT, headroom);
1734
1804
  var reachDown = Math.max(floor, Math.min(0, lightest - WEIGHT_BASE));
1805
+ var bases = top === WEIGHT_BASE ? [WEIGHT_BASE] : [WEIGHT_BASE, top];
1735
1806
  for (var index = 0; index < candidates.length; index += 1) {
1736
1807
  var step = candidates[index];
1737
1808
  if (!(step > 0)) continue;
1738
1809
  var min = floorTo(reachDown, step);
1739
- var max = ceilTo(reachUp, step);
1740
- if (everyPositionDiffers(sample, WEIGHT_BASE, min, max, step)) {
1810
+ // Rounding up to the step must not push the range past the limit: the widest
1811
+ // offset has to stay out of the absolute-weight window.
1812
+ var max = Math.min(ceilTo(reachUp, step), WEIGHT_DELTA_UP_LIMIT);
1813
+ // A step wider than the range would leave only one reachable position.
1814
+ if (max > min && step > max - min) continue;
1815
+ if (everyPositionDiffers(sample, bases, min, max, step)) {
1741
1816
  return { min: min, max: max, step: step };
1742
1817
  }
1743
1818
  }
1744
1819
  var coarsest = candidates[candidates.length - 1];
1745
- return {
1746
- min: floorTo(reachDown, coarsest),
1747
- max: ceilTo(reachUp, coarsest),
1748
- step: coarsest,
1749
- };
1820
+ var min = floorTo(reachDown, coarsest);
1821
+ var max = Math.min(ceilTo(reachUp, coarsest), WEIGHT_DELTA_UP_LIMIT);
1822
+ // Nothing kept every position alive: the coarsest step that still fits is the
1823
+ // honest answer, never a step the control cannot reach.
1824
+ var span = max - min;
1825
+ return { min: min, max: max, step: span > 0 ? Math.min(coarsest, span) : 1 };
1750
1826
  }
1751
1827
 
1752
1828
  /**
@@ -1930,7 +2006,9 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
1930
2006
  * @returns {string} the `font-weight` value.
1931
2007
  */
1932
2008
  function ladderWeight(base) {
1933
- return "clamp(100,calc(" + base + " + var(--dfp-wdelta,0)),900)";
2009
+ return (
2010
+ "clamp(" + WEIGHT_CSS_MIN + ",calc(" + base + " + var(--dfp-wdelta,0))," + WEIGHT_CSS_MAX + ")"
2011
+ );
1934
2012
  }
1935
2013
  /**
1936
2014
  * The scope the conversation WEIGHT rules hang on: the cheap markdown wrapper
@@ -2088,6 +2166,16 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
2088
2166
  // ladder below re-states each group's own base PLUS the offset, so the
2089
2167
  // hierarchy survives any offset, in both directions, and `d = 0` injects
2090
2168
  // nothing at all.
2169
+ if (dialogDelta === WEIGHT_UNSET && interfaceDelta !== WEIGHT_UNSET) {
2170
+ // The conversation has NO offset of its own — which is exactly why it has to
2171
+ // say so. The interface axis declares `--dfp-wdelta` on `:root,body`, so an
2172
+ // unset conversation INHERITED it: with the interface at +80 the card showed 0
2173
+ // for the conversation while the page rendered +80, and the first notch above
2174
+ // it only added 20 units — a step nobody can see (measured on 0.1.7-rc.2:
2175
+ // `delta` read 80 at the "0" position and 100 at "+1"). Pinning zero keeps
2176
+ // "0 = leave DSH alone" true no matter what the other axes are set to.
2177
+ declarations.push(weightScopeRule("--dfp-wdelta:0"));
2178
+ }
2091
2179
  if (dialogDelta !== WEIGHT_UNSET) {
2092
2180
  // Every ladder rule reads `--dfp-wdelta`, so the conversation pins it to its
2093
2181
  // own offset: the interface's value must not leak in, and an element inside
@@ -2793,6 +2881,11 @@ var shared = {
2793
2881
  WEIGHT_STEP_CANDIDATES: WEIGHT_STEP_CANDIDATES,
2794
2882
  WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
2795
2883
  WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
2884
+ WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
2885
+ WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
2886
+ WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
2887
+ WEIGHT_CSS_MIN: WEIGHT_CSS_MIN,
2888
+ WEIGHT_CSS_MAX: WEIGHT_CSS_MAX,
2796
2889
  weightProfileFrom: weightProfileFrom,
2797
2890
  weightStepRange: weightStepRange,
2798
2891
  isShorthandName: isShorthandName,
@@ -3120,13 +3213,13 @@ var DICTS = {
3120
3213
 
3121
3214
  "weight.uiLabel": "Interface font-weight offset",
3122
3215
  "weight.uiHint":
3123
- "Adds to the weight of the whole interface (sidebars, settings, buttons, headings): a positive value is bolder, a negative one lighter. The conversation keeps its own. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); another font offers a different number of them.",
3216
+ "Adds to the weight of the whole interface (sidebars, settings, buttons, headings): a positive value is bolder, a negative one lighter. The conversation keeps its own. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch swaps in a different face for body text and headings alike, and another font offers a different number of steps.",
3124
3217
  "weight.dialogLabel": "Conversation font-weight offset",
3125
3218
  "weight.dialogHint":
3126
- "Adds to the weight of each conversation element, so headings stay bolder than body text and bold text stays bold. Unset keeps DSH's own weights. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); another font offers a different number of them.",
3219
+ "Adds to the weight of each conversation element, so headings stay bolder than body text and bold text stays bold. Unset keeps DSH's own weights. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch swaps in a different face for body text and headings alike, and another font offers a different number of steps.",
3127
3220
  "weight.codeLabel": "Code font-weight offset",
3128
3221
  "weight.codeHint":
3129
- "Adds to the weight of each code surface (code blocks, inline code, terminal output); 0 or unset keeps DSH's own weight. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); another font offers a different number of them.",
3222
+ "Adds to the weight of each code surface (code blocks, inline code, terminal output); 0 or unset keeps DSH's own weight. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch swaps in a different face for body text and headings alike, and another font offers a different number of steps.",
3130
3223
 
3131
3224
  "line.dialogLabel": "Conversation line height",
3132
3225
  "line.dialogHint":
@@ -3264,13 +3357,13 @@ var DICTS = {
3264
3357
 
3265
3358
  "weight.uiLabel": "界面字重偏移",
3266
3359
  "weight.uiHint":
3267
- "在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):换个字体,档数也会跟着变。",
3360
+ "在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。",
3268
3361
  "weight.dialogLabel": "对话字重偏移",
3269
3362
  "weight.dialogHint":
3270
- "在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):换个字体,档数也会跟着变。",
3363
+ "在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。",
3271
3364
  "weight.codeLabel": "代码字重偏移",
3272
3365
  "weight.codeHint":
3273
- "在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):换个字体,档数也会跟着变。",
3366
+ "在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。",
3274
3367
 
3275
3368
  "line.dialogLabel": "对话行高",
3276
3369
  "line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
@@ -3747,7 +3840,11 @@ function sampleWeightSignature(stack, weight) {
3747
3840
  weightProbeCanvas.width = 260;
3748
3841
  weightProbeCanvas.height = 56;
3749
3842
  }
3750
- var context = weightProbeCanvas.getContext("2d");
3843
+ // `willReadFrequently` matters here: the profile probe reads the canvas back once
3844
+ // per sampled weight (~80 times on the first measurement), and without it the
3845
+ // browser keeps a GPU-backed surface and warns about the readbacks — which is part
3846
+ // of why dragging the weight felt heavy.
3847
+ var context = weightProbeCanvas.getContext("2d", { willReadFrequently: true });
3751
3848
  context.clearRect(0, 0, weightProbeCanvas.width, weightProbeCanvas.height);
3752
3849
  context.font = weight + " 32px " + stack;
3753
3850
  context.textBaseline = "top";
@@ -3765,22 +3862,27 @@ function sampleWeightSignature(stack, weight) {
3765
3862
  *
3766
3863
  * A family can only be made as light or as heavy as the faces it ships, so the
3767
3864
  * range is measured rather than fixed (`weightProfileFrom`), which also makes
3768
- * every notch a change. Measured once per family and cached; any failure (no
3769
- * canvas, no 2D context) falls back to the nominal window with single units.
3865
+ * every notch a change — for every layer of the ladder, so the measurement is
3866
+ * told which base is the heaviest one the offset lands on. Measured once per
3867
+ * family and base and cached; any failure (no canvas, no 2D context) falls back
3868
+ * to the nominal window with single units.
3770
3869
  * @param {string} stack - resolved `font-family` value, "" for the page default.
3870
+ * @param {number} [top] - the heaviest base the offset is added to; the ladder's
3871
+ * own top for the conversation and interface axes, the axis's base for code.
3771
3872
  * @returns {{min: number, max: number, step: number}} the slider shape.
3772
3873
  */
3773
- function weightProfileFor(stack) {
3774
- var key = String(stack);
3874
+ function weightProfileFor(stack, top) {
3875
+ var base = typeof top === "number" ? top : shared.WEIGHT_LADDER_TOP;
3876
+ var key = String(stack) + "\u0000" + base;
3775
3877
  if (Object.prototype.hasOwnProperty.call(weightProfileCache, key)) {
3776
3878
  return weightProfileCache[key];
3777
3879
  }
3778
3880
  var profile = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
3779
3881
  try {
3780
- var resolved = key === "" ? liveFamily() : key;
3882
+ var resolved = String(stack) === "" ? liveFamily() : String(stack);
3781
3883
  profile = weightProfileFrom(function (weight) {
3782
3884
  return sampleWeightSignature(resolved, weight);
3783
- });
3885
+ }, { top: base });
3784
3886
  } catch (error) {
3785
3887
  profile = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
3786
3888
  }
@@ -3801,6 +3903,72 @@ function liveFamily() {
3801
3903
  }
3802
3904
  }
3803
3905
 
3906
+ /**
3907
+ * The conversation's own line-height ratio, measured once.
3908
+ *
3909
+ * The dialog line-height axis is a percentage OF DSH's own height (`28px × 1.05`),
3910
+ * so a preview that used the percentage as an ABSOLUTE ratio was wrong twice over:
3911
+ * at 100% it fell back to DSH's real (taller) default, while 105% asked for 1.05 —
3912
+ * shorter than that default and out of order with the notches above it.
3913
+ *
3914
+ * The base is read from a hidden node carrying the markdown class whose rule
3915
+ * consumes DSH's markdown shorthand, so it is the same ratio the page multiplies.
3916
+ * @returns {number} line height ÷ font size; a plain default when unmeasurable.
3917
+ */
3918
+ var DIALOG_LINE_RATIO_FALLBACK = 1.5;
3919
+ var dialogLineRatioCache = null;
3920
+ function dialogLineRatio() {
3921
+ if (typeof dialogLineRatioCache === "number") return dialogLineRatioCache;
3922
+ if (typeof document === "undefined" || typeof document.defaultView === "undefined") {
3923
+ return DIALOG_LINE_RATIO_FALLBACK;
3924
+ }
3925
+ try {
3926
+ var className = "";
3927
+ var sheets = document.styleSheets || [];
3928
+ for (var index = 0; index < sheets.length && className === ""; index += 1) {
3929
+ var rules = null;
3930
+ try {
3931
+ rules = sheets[index].cssRules;
3932
+ } catch (error) {
3933
+ rules = null;
3934
+ }
3935
+ if (!rules) continue;
3936
+ for (var ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
3937
+ var selector = rules[ruleIndex].selectorText || "";
3938
+ var body = rules[ruleIndex].style ? rules[ruleIndex].style.cssText : "";
3939
+ if (selector.indexOf("_markdown_") < 0) continue;
3940
+ if (body.indexOf("--dsw-font-markdown-base") < 0) continue;
3941
+ var found = selector.match(/\.(_markdown_[A-Za-z0-9_-]+)/);
3942
+ if (found) className = found[1];
3943
+ }
3944
+ }
3945
+ if (className !== "") {
3946
+ var host = document.createElement("div");
3947
+ host.className = className;
3948
+ host.setAttribute("data-dss-prose", "");
3949
+ host.style.position = "absolute";
3950
+ host.style.visibility = "hidden";
3951
+ host.style.left = "-9999px";
3952
+ var probe = document.createElement("p");
3953
+ probe.textContent = "line";
3954
+ probe.style.margin = "0";
3955
+ host.appendChild(probe);
3956
+ document.body.appendChild(host);
3957
+ var style = document.defaultView.getComputedStyle(probe);
3958
+ var size = parseFloat(style.fontSize);
3959
+ var height = parseFloat(style.lineHeight);
3960
+ if (isFinite(size) && size > 0 && isFinite(height) && height > 0) {
3961
+ dialogLineRatioCache = Math.round((height / size) * 10000) / 10000;
3962
+ }
3963
+ if (host.parentNode) host.parentNode.removeChild(host);
3964
+ }
3965
+ } catch (error) {
3966
+ /* the fallback stands */
3967
+ }
3968
+ if (typeof dialogLineRatioCache !== "number") dialogLineRatioCache = DIALOG_LINE_RATIO_FALLBACK;
3969
+ return dialogLineRatioCache;
3970
+ }
3971
+
3804
3972
  /**
3805
3973
  * The family code surfaces render with when the code axis sets none.
3806
3974
  * @returns {string} the computed family, or "" when unreadable.
@@ -5322,7 +5490,10 @@ function FontCard(props) {
5322
5490
  // measured on the page. The VALUE written to the document stays a weight offset,
5323
5491
  // so nothing stored before has to be migrated and the stylesheet is unchanged.
5324
5492
  var weightField = function (props) {
5325
- var profile = weightProfileFor(props.stack === undefined ? "" : props.stack);
5493
+ var profile = weightProfileFor(
5494
+ props.stack === undefined ? "" : props.stack,
5495
+ props.top === undefined ? shared.WEIGHT_LADDER_STRONG : props.top
5496
+ );
5326
5497
  var stored = props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value;
5327
5498
  // A stored offset outside the measured range (the font changed since it was
5328
5499
  // set) widens the control, so it is never unreachable.
@@ -5741,9 +5912,10 @@ function FontCard(props) {
5741
5912
  * @returns {number} the step count.
5742
5913
  */
5743
5914
  var weightCount = function (field, value) {
5915
+ var isCode = field === CODE_WEIGHT_FIELD;
5744
5916
  var stack =
5745
- field === WEIGHT_DIALOG_FIELD ? dialogStack : field === CODE_WEIGHT_FIELD ? codeStack : uiStack;
5746
- var profile = weightProfileFor(stack);
5917
+ field === WEIGHT_DIALOG_FIELD ? dialogStack : isCode ? codeStack : uiStack;
5918
+ var profile = weightProfileFor(stack, isCode ? shared.WEIGHT_BASE : shared.WEIGHT_LADDER_STRONG);
5747
5919
  return weightStepRange(profile, value === WEIGHT_UNSET ? 0 : value).value;
5748
5920
  };
5749
5921
 
@@ -5760,9 +5932,13 @@ function FontCard(props) {
5760
5932
  // rule would otherwise paint into this box.
5761
5933
  if (dialogStack !== "") previewDialogStyle.fontFamily = dialogStack;
5762
5934
  previewDialogStyle.fontSize = 13 + editing[SIZE_DIALOG_FIELD] + "px";
5763
- if (editing[LINE_HEIGHT_DIALOG_FIELD] !== LINE_HEIGHT_MIN) {
5764
- previewDialogStyle.lineHeight = String(editing[LINE_HEIGHT_DIALOG_FIELD] / 100);
5765
- }
5935
+ // The axis is a percentage OF DSH's own line height, so the preview multiplies the
5936
+ // measured base by the same percentage — and does it at every notch including 100,
5937
+ // or the preview would show a shorter line at 105% than at 100%.
5938
+ previewDialogStyle.lineHeight = String(
5939
+ Math.round(dialogLineRatio() * (editing[LINE_HEIGHT_DIALOG_FIELD] / LINE_HEIGHT_MIN) * 10000) /
5940
+ 10000
5941
+ );
5766
5942
  if (editing[WEIGHT_DIALOG_FIELD] !== WEIGHT_UNSET) {
5767
5943
  // The offset, not a weight: the injected stylesheet's conversation ladder
5768
5944
  // reads `--dfp-wdelta` on this very class and re-states each element's own
@@ -6149,6 +6325,10 @@ function FontCard(props) {
6149
6325
  labelKey: "weight.codeLabel",
6150
6326
  hintKey: "weight.codeHint",
6151
6327
  value: editing[CODE_WEIGHT_FIELD],
6328
+ // The code axis puts one weight on the whole surface, so its own base
6329
+ // is the heaviest layer there is: any lower base would cap the range
6330
+ // short of what code text can actually reach.
6331
+ top: shared.WEIGHT_BASE,
6152
6332
  stack: codeStack,
6153
6333
  }),
6154
6334
  ligatureField(),
package/lib/shared.cjs CHANGED
@@ -167,6 +167,16 @@ var WEIGHT_DELTA_CEIL = 600;
167
167
  /** The weight DSH gives an element that declares none of its own. */
168
168
  var WEIGHT_BASE = 400;
169
169
 
170
+ /**
171
+ * The weight range CSS accepts at all.
172
+ *
173
+ * Named because it is easy to confuse with `WEIGHT_MIN…WEIGHT_MAX`, which is the
174
+ * window an ABSOLUTE weight from a pre-relative document occupies (300…600) and
175
+ * has nothing to do with what a font can render.
176
+ */
177
+ var WEIGHT_CSS_MIN = 100;
178
+ var WEIGHT_CSS_MAX = 900;
179
+
170
180
  /**
171
181
  * The weight ladder of the conversation: each group carries the weight DSH's
172
182
  * own stylesheet gives those elements, so an offset lands ON TOP of it.
@@ -186,6 +196,43 @@ var DIALOG_WEIGHT_GROUPS = [
186
196
  },
187
197
  ];
188
198
 
199
+ /**
200
+ * The heaviest base in the conversation ladder — the layer with the least room.
201
+ *
202
+ * Derived from the ladder so the two cannot drift apart.
203
+ */
204
+ var WEIGHT_LADDER_TOP = DIALOG_WEIGHT_GROUPS.reduce(function (top, group) {
205
+ return group.base > top ? group.base : top;
206
+ }, WEIGHT_BASE);
207
+
208
+ /**
209
+ * How far above its base an offset may reach at all.
210
+ *
211
+ * Every axis applies the offset to each element's OWN weight, and the heaviest
212
+ * layer of the ladder sits at `WEIGHT_LADDER_TOP`; past `WEIGHT_CSS_MAX` minus
213
+ * that, the heaviest layer cannot move any further, so a longer range would only
214
+ * add positions that change nothing for the boldest text on screen.
215
+ *
216
+ * Staying inside it has a second, load-bearing effect: offsets then never land in
217
+ * `WEIGHT_MIN…WEIGHT_MAX`, the range an ABSOLUTE weight from a pre-relative
218
+ * document occupies (see `clampWeightDelta`). The two meanings cannot be confused,
219
+ * so a stored offset is never rescaled into a different one.
220
+ */
221
+ var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX - WEIGHT_LADDER_TOP;
222
+
223
+ /**
224
+ * The heaviest layer a notch still has to move: bold text.
225
+ *
226
+ * The heading layer above it is deliberately left out of that requirement. A heavy
227
+ * family spaces its top faces far apart (a 700 cut next to a 900), so headings
228
+ * reach their heaviest rendering part-way up the range and would otherwise hold the
229
+ * whole control down to one wide step. Every notch must move the text you read and
230
+ * the text you emphasise; a heading that is already at its boldest face stays there.
231
+ */
232
+ var WEIGHT_LADDER_STRONG = DIALOG_WEIGHT_GROUPS.reduce(function (strong, group) {
233
+ return group.base < WEIGHT_LADDER_TOP && group.base > strong ? group.base : strong;
234
+ }, WEIGHT_BASE);
235
+
189
236
  /** Allowed line-height ratio range, in percent (100 = untouched). */
190
237
  var LINE_HEIGHT_MIN = 100;
191
238
  var LINE_HEIGHT_MAX = 160;
@@ -1660,19 +1707,25 @@ function ceilTo(value, step) {
1660
1707
 
1661
1708
  /**
1662
1709
  * Does every position of `[min..max]` in `step` increments render differently?
1710
+ *
1711
+ * Checked at every base the offset is added to: a step that is fine for body text
1712
+ * can be a no-op one layer up, where the faces are further apart.
1663
1713
  * @param {(weight: number) => string|number} sample - probe.
1664
- * @param {number} base - the weight the offsets are added to.
1714
+ * @param {number[]} bases - the weights the offsets are added to.
1665
1715
  * @param {number} min - first offset.
1666
1716
  * @param {number} max - last offset.
1667
1717
  * @param {number} step - offset increment.
1668
- * @returns {boolean} true when no two neighbours render the same.
1669
- */
1670
- function everyPositionDiffers(sample, base, min, max, step) {
1671
- var previous = null;
1672
- for (var position = min; position <= max; position += step) {
1673
- var signature = sample(base + position);
1674
- if (previous !== null && signature === previous) return false;
1675
- previous = signature;
1718
+ * @returns {boolean} true when no two neighbours render the same at any base.
1719
+ */
1720
+ function everyPositionDiffers(sample, bases, min, max, step) {
1721
+ for (var index = 0; index < bases.length; index += 1) {
1722
+ var base = bases[index];
1723
+ var previous = null;
1724
+ for (var position = min; position <= max; position += step) {
1725
+ var signature = sample(base + position);
1726
+ if (previous !== null && signature === previous) return false;
1727
+ previous = signature;
1728
+ }
1676
1729
  }
1677
1730
  return true;
1678
1731
  }
@@ -1683,17 +1736,21 @@ function everyPositionDiffers(sample, base, min, max, step) {
1683
1736
  * A font with six faces can be made lighter or heavier only as far as those faces
1684
1737
  * reach, and only in steps between them; a family with a continuous axis gets a
1685
1738
  * fine step and a wide range. Measured on a weight grid:
1686
- * - the top of the range is the LOWEST weight that already renders as heavy as
1687
- * the family can (asking for more cannot change anything), and the bottom is
1688
- * the highest weight that still renders as light as it can;
1689
- * - the step is the finest candidate whose positions all differ, so no notch is
1690
- * a no-op. A family whose faces are unevenly spaced (a 700 next to a 900) may
1691
- * end up with one repeated position whichever uniform step is used — the
1692
- * coarsest candidate is then the honest answer, never a crash.
1739
+ * - the downward reach is the highest weight that still renders as light as the
1740
+ * family can (asking for less cannot change anything);
1741
+ * - the upward reach is the LOWEST weight at or above the layer a notch has to
1742
+ * move that already renders as heavy as the family can, so the range ends where
1743
+ * bold text stops changing instead of running past it (see `WEIGHT_DELTA_UP_LIMIT`
1744
+ * for why that ceiling is also the widest one allowed);
1745
+ * - the step is the finest candidate whose positions all differ at every base a
1746
+ * notch has to move, so no notch is a no-op for body text or bold text. A family whose faces are unevenly spaced
1747
+ * (a 700 next to a 900) may end up with one repeated position whichever uniform
1748
+ * step is used — the coarsest candidate is then the honest answer, never a crash.
1693
1749
  * @param {(weight: number) => string|number} sample - renders a probe at one
1694
1750
  * weight and returns a signature; equal signatures mean identical rendering.
1695
- * @param {{candidates?: number[], floor?: number, ceil?: number}} [options]
1696
- * overrides for the candidates and the widest allowed range.
1751
+ * @param {{candidates?: number[], floor?: number, ceil?: number, top?: number}}
1752
+ * [options] overrides for the candidates, the widest allowed range and the
1753
+ * heaviest base the offset is added to (the ladder's, or the axis's own).
1697
1754
  * @returns {{min: number, max: number, step: number}} the slider's shape; the
1698
1755
  * range always contains 0, which is the "leave DSH alone" position.
1699
1756
  */
@@ -1705,6 +1762,7 @@ function weightProfileFrom(sample, options) {
1705
1762
  : WEIGHT_STEP_CANDIDATES;
1706
1763
  var floor = typeof settings.floor === "number" ? settings.floor : WEIGHT_DELTA_FLOOR;
1707
1764
  var ceil = typeof settings.ceil === "number" ? settings.ceil : WEIGHT_DELTA_CEIL;
1765
+ var top = typeof settings.top === "number" ? settings.top : WEIGHT_LADDER_STRONG;
1708
1766
  var topSignature = sample(WEIGHT_PROBE_HIGH);
1709
1767
  var bottomSignature = sample(WEIGHT_PROBE_LOW);
1710
1768
  var heaviest = WEIGHT_PROBE_HIGH;
@@ -1721,23 +1779,41 @@ function weightProfileFrom(sample, options) {
1721
1779
  break;
1722
1780
  }
1723
1781
  }
1724
- var reachUp = Math.min(ceil, Math.max(0, heaviest - WEIGHT_BASE));
1782
+ // How far the offset can go before NO layer that must move can move any more:
1783
+ // the widest horizon among those bases, since a layer that could still change
1784
+ // must not be cut off by one that already sits at its heaviest face.
1785
+ var headroom = Math.max(0, heaviest - WEIGHT_BASE);
1786
+ if (top !== WEIGHT_BASE) {
1787
+ for (var up = top; up <= WEIGHT_PROBE_HIGH; up += WEIGHT_PROBE_STEP) {
1788
+ if (sample(up) === topSignature) {
1789
+ headroom = Math.max(headroom, up - top);
1790
+ break;
1791
+ }
1792
+ }
1793
+ }
1794
+ var reachUp = Math.min(ceil, WEIGHT_DELTA_UP_LIMIT, headroom);
1725
1795
  var reachDown = Math.max(floor, Math.min(0, lightest - WEIGHT_BASE));
1796
+ var bases = top === WEIGHT_BASE ? [WEIGHT_BASE] : [WEIGHT_BASE, top];
1726
1797
  for (var index = 0; index < candidates.length; index += 1) {
1727
1798
  var step = candidates[index];
1728
1799
  if (!(step > 0)) continue;
1729
1800
  var min = floorTo(reachDown, step);
1730
- var max = ceilTo(reachUp, step);
1731
- if (everyPositionDiffers(sample, WEIGHT_BASE, min, max, step)) {
1801
+ // Rounding up to the step must not push the range past the limit: the widest
1802
+ // offset has to stay out of the absolute-weight window.
1803
+ var max = Math.min(ceilTo(reachUp, step), WEIGHT_DELTA_UP_LIMIT);
1804
+ // A step wider than the range would leave only one reachable position.
1805
+ if (max > min && step > max - min) continue;
1806
+ if (everyPositionDiffers(sample, bases, min, max, step)) {
1732
1807
  return { min: min, max: max, step: step };
1733
1808
  }
1734
1809
  }
1735
1810
  var coarsest = candidates[candidates.length - 1];
1736
- return {
1737
- min: floorTo(reachDown, coarsest),
1738
- max: ceilTo(reachUp, coarsest),
1739
- step: coarsest,
1740
- };
1811
+ var min = floorTo(reachDown, coarsest);
1812
+ var max = Math.min(ceilTo(reachUp, coarsest), WEIGHT_DELTA_UP_LIMIT);
1813
+ // Nothing kept every position alive: the coarsest step that still fits is the
1814
+ // honest answer, never a step the control cannot reach.
1815
+ var span = max - min;
1816
+ return { min: min, max: max, step: span > 0 ? Math.min(coarsest, span) : 1 };
1741
1817
  }
1742
1818
 
1743
1819
  /**
@@ -1921,7 +1997,9 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
1921
1997
  * @returns {string} the `font-weight` value.
1922
1998
  */
1923
1999
  function ladderWeight(base) {
1924
- return "clamp(100,calc(" + base + " + var(--dfp-wdelta,0)),900)";
2000
+ return (
2001
+ "clamp(" + WEIGHT_CSS_MIN + ",calc(" + base + " + var(--dfp-wdelta,0))," + WEIGHT_CSS_MAX + ")"
2002
+ );
1925
2003
  }
1926
2004
  /**
1927
2005
  * The scope the conversation WEIGHT rules hang on: the cheap markdown wrapper
@@ -2079,6 +2157,16 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
2079
2157
  // ladder below re-states each group's own base PLUS the offset, so the
2080
2158
  // hierarchy survives any offset, in both directions, and `d = 0` injects
2081
2159
  // nothing at all.
2160
+ if (dialogDelta === WEIGHT_UNSET && interfaceDelta !== WEIGHT_UNSET) {
2161
+ // The conversation has NO offset of its own — which is exactly why it has to
2162
+ // say so. The interface axis declares `--dfp-wdelta` on `:root,body`, so an
2163
+ // unset conversation INHERITED it: with the interface at +80 the card showed 0
2164
+ // for the conversation while the page rendered +80, and the first notch above
2165
+ // it only added 20 units — a step nobody can see (measured on 0.1.7-rc.2:
2166
+ // `delta` read 80 at the "0" position and 100 at "+1"). Pinning zero keeps
2167
+ // "0 = leave DSH alone" true no matter what the other axes are set to.
2168
+ declarations.push(weightScopeRule("--dfp-wdelta:0"));
2169
+ }
2082
2170
  if (dialogDelta !== WEIGHT_UNSET) {
2083
2171
  // Every ladder rule reads `--dfp-wdelta`, so the conversation pins it to its
2084
2172
  // own offset: the interface's value must not leak in, and an element inside
@@ -2784,6 +2872,11 @@ var shared = {
2784
2872
  WEIGHT_STEP_CANDIDATES: WEIGHT_STEP_CANDIDATES,
2785
2873
  WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
2786
2874
  WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
2875
+ WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
2876
+ WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
2877
+ WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
2878
+ WEIGHT_CSS_MIN: WEIGHT_CSS_MIN,
2879
+ WEIGHT_CSS_MAX: WEIGHT_CSS_MAX,
2787
2880
  weightProfileFrom: weightProfileFrom,
2788
2881
  weightStepRange: weightStepRange,
2789
2882
  isShorthandName: isShorthandName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fonttune",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
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",
@@ -19,7 +19,7 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "engines": {
22
- "dsh": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
22
+ "dsh": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.3.0-0"
23
23
  },
24
24
  "dsh": {
25
25
  "bundle": {
@@ -34,7 +34,8 @@
34
34
  "0.1.7-alpha.1": "compatible",
35
35
  "0.1.7-alpha.2": "compatible",
36
36
  "0.1.7-rc.1": "compatible",
37
- "0.1.7-rc.2": "compatible"
37
+ "0.1.7-rc.2": "compatible",
38
+ "0.2.0-rc.1": "compatible"
38
39
  }
39
40
  }
40
41
  },