dsh-fonttune 0.3.0 → 0.3.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 +11 -1
- package/README.en.md +4 -3
- package/README.md +4 -3
- package/lib/client.js +226 -46
- package/lib/shared.cjs +120 -27
- package/package.json +1 -1
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.3.1] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **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.
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
5
15
|
## [0.3.0] - 2026-09-28
|
|
6
16
|
|
|
7
17
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.3.1] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **字重滑块偏上的档位会跳到较小的数值**:此前有两个档位会被换算成别的数值,表现为拖到第三档显示成 −1、第四档与 0 档的粗细看起来完全一样。现在拖到哪一档就停在哪一档。
|
|
9
|
+
- **「对话字重偏移」为 0 时,对话还是会跟着「界面字重偏移」一起变粗**:两个轴的范围互相串了,界面设了偏移时对话的 0 档也带上了那个偏移,于是 0 与 +1 两档看起来几乎一样。现在每个轴只管自己的范围,0 档就是 DSH 原样。
|
|
10
|
+
- **卡片里的行高预览在 100% 时比更高的档位还高**:预览与真实对话用的不是同一个基准。现在两者一致,预览里也是每一档都更高。
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
- **字重滑块的档位与档距按字体实测**:一档就是字体相邻的两张字形,范围从「正文与加粗文字都还能换到字形」的地方起算——正文字重档位多的字体给更多档位(正文能走的范围不再被标题压小),标题那一层到顶后停住;换字体会重新测量。
|
|
14
|
+
|
|
5
15
|
## [0.3.0] - 2026-09-28
|
|
6
16
|
|
|
7
17
|
### 修复
|
|
@@ -12,7 +22,7 @@
|
|
|
12
22
|
|
|
13
23
|
### 变更
|
|
14
24
|
- **三个字重轴统一,并按档调整**:对话、界面与代码都改成「在每个元素自身粗细上叠加」的偏移,滑块按当前字体**实际能渲染的粗细分档**——读数就是档数(例如 `-2 … +4`,0 表示保持 DSH 原样),字体有几档就有几档,每挪一格都是一次看得见的变化;换字体后档数会重新测量。
|
|
15
|
-
- **旧版本存的绝对字重会自动换算**(400 → 0、450 → +1
|
|
25
|
+
- **旧版本存的绝对字重会自动换算**(400 → 0、450 → +1),不需要重设;换字体也不会让已有设置失效。
|
|
16
26
|
|
|
17
27
|
## [0.2.7] - 2026-09-24
|
|
18
28
|
|
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 `-
|
|
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
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
| Plugin version | Supported DSH versions |
|
|
28
28
|
| --- | --- |
|
|
29
|
-
| **0.3.
|
|
29
|
+
| **0.3.1** (latest) | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2 |
|
|
30
|
+
| 0.3.0 | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2 |
|
|
30
31
|
| 0.2.7 | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 / rc.2 |
|
|
31
32
|
| 0.2.6 | 0.1.5-rc.3, 0.1.7-alpha.1 / alpha.2, 0.1.7-rc.1 |
|
|
32
33
|
| 0.2.5 | 0.1.5-rc.1 / rc.2 / rc.3, 0.1.7-alpha.1 / alpha.2 |
|
|
@@ -79,7 +80,7 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
|
|
|
79
80
|
- **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
81
|
- **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
82
|
- **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
|
|
83
|
+
- **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
84
|
- **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
85
|
- **Preview** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part
|
|
85
86
|
- 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 里的元素一个都没动,所以**改界面字重不会碰到对话**。滑块**按档计数**:读数就是档数(例如 `-
|
|
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
|
- **拒绝合成样式**(默认关闭 = 允许合成) —— 打开后禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰);关着(默认)时由浏览器按自己的规则合成,字体缺粗体字形时至少还能看出"粗体"这一档
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
| 插件版本 | 支持的 DSH 版本 |
|
|
28
28
|
| --- | --- |
|
|
29
|
-
| **0.3.
|
|
29
|
+
| **0.3.1**(最新) | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2 |
|
|
30
|
+
| 0.3.0 | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2 |
|
|
30
31
|
| 0.2.7 | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 / rc.2 |
|
|
31
32
|
| 0.2.6 | 0.1.5-rc.3、0.1.7-alpha.1 / alpha.2、0.1.7-rc.1 |
|
|
32
33
|
| 0.2.5 | 0.1.5-rc.1 / rc.2 / rc.3、0.1.7-alpha.1 / alpha.2 |
|
|
@@ -79,7 +80,7 @@ dsh plugin --profile web add <插件目录路径>
|
|
|
79
80
|
- **关掉跟随不会跳变** —— 关掉开关的那一刻,界面会把**正在显示的**字体与字重偏移写成自己的值(跟随期间它本来就是这两项),所以画面零变化、滑块从当前值继续调;对话没设的那一项则清掉界面自己的字段、保持 DSH 默认。想回到 DSH 自己的粗细点该项的「重置」(三个字重偏移读数都回到 0)
|
|
80
81
|
- **界面字重不会碰到对话** —— 界面字重是唯一必须铺满整页的轴(DSH 把小标签的粗细写死在上游 CSS 的类规则里,窄规则够不到它),实现上是一条把**对话 Markdown 整棵子树排除在外**的规则,所以对话的段落、标题、粗体都不受影响
|
|
81
82
|
- **字重读数** —— 三个字重轴的未设置读数都显示 0(滑块停在正中);不会出现「未设置」这类字眼;旧版本存的绝对字重(300 ~ 600)会在读取时自动换算成等价偏移(400 → 0、450 → +50),无需手动重设
|
|
82
|
-
- **字重滑块按档计数** ——
|
|
83
|
+
- **字重滑块按档计数** —— 卡片会量出当前字体在正文与加粗文字两处各能渲染出几档粗细,一档 = 相邻两张字形、上下限以这两处还能换字形为限(读数就是档数,如 `-2 … +2`),所以每挪一格都看得出变化、也不会出现"几百几百"的数字;换字体后档数会重新测量
|
|
83
84
|
- **预设方案** —— 编辑模式下方的下拉选择框(默认配置 1 ~ 5,框内右侧有与卡片一致的下拉箭头),点开选择即切换并自动保存后续改动;「改名」重命名当前方案,「导出」复制 JSON,「导入」粘贴合并。操作结果(如「已复制到剪贴板」)显示在下面那行的**右侧**,约 2.6 秒后自动淡出,不会顶动任何布局
|
|
84
85
|
- **预览** —— 对话 / 界面 / 代码分区展开后,各自分区末尾有一个预览框,只渲染该分区的样式
|
|
85
86
|
- 设置保存在 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}
|
|
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,
|
|
1680
|
-
var
|
|
1681
|
-
|
|
1682
|
-
var
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
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}}
|
|
1705
|
-
* overrides for the candidates
|
|
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
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
3769
|
-
*
|
|
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
|
|
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 =
|
|
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(
|
|
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 :
|
|
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
|
-
|
|
5764
|
-
|
|
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}
|
|
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,
|
|
1671
|
-
var
|
|
1672
|
-
|
|
1673
|
-
var
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
|
1687
|
-
*
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
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}}
|
|
1696
|
-
* overrides for the candidates
|
|
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
|
-
|
|
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
|
-
|
|
1731
|
-
|
|
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
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|