dsh-fonttune 0.1.4 → 0.1.6

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.1.6] - 2026-09-15
6
+
7
+ ### Fixed
8
+ - **The configuration card's expand/collapse chevron matches the host now**: it used to be the text character `⌄`, rendered at text weight so it looked **thinner** than every other card's arrow, and the rotation sat on the text line box — the glyph's ink is off-centre inside that box (it rides the baseline), so `rotate(180deg)` swung the character sideways instead of turning it around its own centre. The chevron is now the host PluginCard's own 14×14 inline SVG (path extracted byte for byte from DSH's frontend bundle, same fix dsh-quick-toc shipped in 0.6.3); the rotation class goes on the `<svg>` itself, so it turns around the icon's own centre. Verified in a real browser: byte-identical path to the 11 host-rendered chevrons on the same page, same 14×14 size.
9
+
10
+ ## [0.1.5] - 2026-09-15
11
+
12
+ ### Added
13
+ - **Code font weight**: weight is split into **two independent axes** — "Body font weight" for text and interface copy (headings included), and "Code font weight" for code blocks, inline code, tool code bodies and terminal output only. The single slider used to press on both chains, so moving body always dragged code with it; now each moves on its own. The field is `weightCode` (300–600, 0 = leave alone), sitting next to the existing `weight`, whose meaning narrows to "body and interface text".
14
+
15
+ ### Changed
16
+ - **Card layout**: the weight sliders moved into the group they belong to — body font → body size offset → body weight → code font → code size offset → code weight — and the two are labelled separately ("Body font weight" / "Code font weight"). The card description now reads "Body and code fonts, a size and a weight for each, and a West/CJK split".
17
+ - **Code no longer follows the body weight**: weight has no token chain to ride (measured on rc.2 — **zero** `font-weight: var(--dsw-font-…)` usages; the theme writes literal weights in its own class rules), which is why the body axis is a blanket `body, body *` `!important` rule. The code axis now appends a scoped rule **after** it that wins either on specificity (`[class*=…]`) or on source order (`pre` ties with `body *`); the selector covers `pre` / `code` / `kbd` / `samp` / `var` / `tt` / `textarea` / `.cm-editor` **and their descendants** (the blanket rule matches every element, so a highlighted `<span>` inside a `<pre>` has to be named too), plus `[class*="code" i]` and `[class*="terminal" i]` for the tool code bodies and the terminal, which are plain `div`s. **The rule is written even when the code weight is unset** (value `normal`), otherwise the blanket rule would pull code onto the body weight; after upgrading, body-weight behaviour is unchanged and code returns to DSH's own weight.
18
+ - **Compatibility table** now lists 0.1.5 as the latest version.
19
+
5
20
  ## [0.1.4] - 2026-09-15
6
21
 
7
22
  ### Added
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  **dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.1.6] - 2026-09-15
6
+
7
+ ### 修复
8
+ - **配置卡片的展开/收起箭头与宿主不一致**:原为文本字符 `⌄`——按文字字重渲染,比宿主其它卡片的箭头**更细**;且旋转写在文本行盒上,字符墨迹在行盒内不居中(落在基线上),`rotate(180deg)` 绕盒心转时字符被甩开,看起来「不是以自己为中心转」。现改为宿主 PluginCard 同款 14×14 内联 SVG(path 从 DSH 前端 bundle 逐字节提取,与 dsh-quick-toc 0.6.3 的修法同构);旋转类挂在 `<svg>` 本体上,绕图标自身中心转。真机验证:与同页 11 个宿主自家箭头 path **逐字节一致**,尺寸同为 14×14。
9
+
10
+ ## [0.1.5] - 2026-09-15
11
+
12
+ ### 新增
13
+ - **代码字重**:字重从「一个全局偏移」拆成**两个互不影响的轴**——「正文字重」覆盖正文与界面文字(含标题),「代码字重」只覆盖代码块、行内代码、工具代码块与终端输出。原来那一个滑块同时压两条链路,调正文必然带动代码;现在各调各的。字段为 `weightCode`(300~600,0 = 不动),与既有的 `weight` 并列;`weight` 语义收敛为「正文与界面文字」。
14
+
15
+ ### 变更
16
+ - **卡片布局**:字重滑块同样移进对应字体那一组——正文字体 → 正文字号偏移 → 正文字重 → 代码字体 → 代码字号偏移 → 代码字重;两处文案分开(「正文字重」/「代码字重」),卡片描述改为「正文与代码字体、各自的字号与字重、中西文分栏」。
17
+ - **代码轴不再被正文字重带着走**:字重没有可依附的 token 链(rc.2 实测**零处** `font-weight: var(--dsw-font-…)`——主题是在自己的类规则里写字面量),所以正文轴一直是 `body, body *` 的全量 `!important` 规则。代码轴现在在这条规则**之后**追加一条作用域规则,靠更高特异性(`[class*=…]`)或更靠后的同特异性(`pre` 与 `body *` 同分)取胜;选择器覆盖 `pre` / `code` / `kbd` / `samp` / `var` / `tt` / `textarea` / `.cm-editor` **及其后代**(全量规则命中每一个元素,所以 `<pre>` 里高亮用的 `<span>` 也必须点名),再加 `[class*="code" i]` 与 `[class*="terminal" i]` 覆盖工具代码块与终端这类没有语义标签的纯 `div`。**代码字重未设置时也会写这条规则**(值 `normal`),否则全量规则会把代码一起拽到正文字重上;旧配置升级后正文字重行为不变、代码回到 DSH 原值。
18
+ - **兼容表**改列 0.1.5 为最新版本。
19
+
5
20
  ## [0.1.4] - 2026-09-15
6
21
 
7
22
  ### 新增
package/README.en.md CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  > [中文](README.md) | **English**
4
4
 
5
- **Font plugin for DeepSeek Harness (DSH)**: set the body and code font families, a font-size offset for each, and a global font weight, and pick fonts per **Western / CJK** slot. Everything lives in the native card under **Settings → Plugins → Plugin configuration** and applies immediately.
5
+ **Font plugin for DeepSeek Harness (DSH)**: set the body and code font families, a font size and a font weight for each, and pick fonts per **Western / CJK** slot. Everything lives in the native card under **Settings → Plugins → Plugin configuration** and applies immediately.
6
6
 
7
7
  ## Features
8
8
 
9
9
  - **Body / code fonts** — two independent fallback lists, Latin families first and CJK after; leaving one empty keeps DSH's own stack untouched
10
10
  - **Body font-size offset** — -3 ~ +6 px for text and interface sizes, stacked on top of DSH's own font-size setting rather than replacing it; every size step scales together with its line height
11
11
  - **Code font-size offset** — -3 ~ +6 px for code blocks and inline code, **independent** of the body offset: moving one never moves the other
12
- - **Global font weight** — any integer from 300 to 600; unset or 400 both mean "keep DSH's own weight"
12
+ - **Body font weight** — any integer from 300 to 600, overriding the weight of text and interface copy including headings; unset or 400 keeps DSH's own weight
13
+ - **Code font weight** — any integer from 300 to 600 for code blocks, inline code and terminal output only; unset or 400 keeps DSH's own weight and keeps code out of the body weight
13
14
  - **West / CJK split (simple mode)** — a switch at the top of the card: simple mode gives just a Western and a CJK single-pick slot (one pair for body, one for code), advanced mode is the full stack editor; both views share one stack, so toggling never changes the order you set up
14
15
  - **Font picker panel** — four preset groups (monospace / CJK / Latin / generic), plus an "installed on this machine" group on Chromium; a name that is not in the list can be created with "use xxx"
15
16
  - **Drag to reorder** — selected families are a chip list you can drag, with earlier / later buttons kept for keyboard and touch
@@ -19,7 +20,7 @@
19
20
 
20
21
  | Plugin version | Supported DSH versions |
21
22
  | --- | --- |
22
- | **0.1.4** (latest) | 0.1.5-rc.2 |
23
+ | **0.1.6** (latest) | 0.1.5-rc.2 |
23
24
 
24
25
  `engines.dsh` requires **0.1.5-rc.2** or newer, and the compatibility declaration lists only the **latest release of the 0.1.5 line**: it carries every fix in that line, which makes it the most stable build of the major version, while older builds of the same major version stay compatible per semantic versioning. Earlier or newer DSH versions are untested and not declared. On install or update, the DSH market runs a host-compatibility preflight against `engines.dsh`, `dsh.compatibility.dshReleases` and `peerDependencies` in `package.json`.
25
26
 
@@ -49,7 +50,7 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
49
50
 
50
51
  - **Open the settings** — Settings → Plugins → Plugin configuration → expand the "Font tune" card
51
52
  - **Pick fonts** — in simple mode click the four slots (Body · Western, Body · CJK, Code · Western, Code · CJK); in advanced mode use "Add font" and the chip editor to maintain the full fallback list
52
- - **Tune sizes** — each size slider sits directly under the font it resizes: "Body font size offset" under the body font, "Code font size offset" under the code font, with the weight slider below. Drag and release to apply. Changed fields are marked and can be reset individually, with "Reset all" at the bottom of the card
53
+ - **Tune sizes and weights** — each slider sits under the thing it changes: body font → body size offset → body weight → code font → code size offset → code weight. Drag and release to apply. Changed fields are marked and can be reset individually, with "Reset all" at the bottom of the card
53
54
  - **Preview** — the mixed-script and code preview at the bottom of the card renders with the current configuration
54
55
  - Settings are stored in DSH's settings document (`settings.yaml`), so they travel with your configuration
55
56
 
package/README.md CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  > [English](README.en.md) | **中文**
4
4
 
5
- **DeepSeek Harness(DSH)字体插件**:设置正文与代码的字体族、各自独立的字号偏移与字重,并按「西文 / 中文」分栏挑字体;配置入口是 **设置 → 插件 → 插件配置** 里的原生卡片,改完即时生效。
5
+ **DeepSeek Harness(DSH)字体插件**:设置正文与代码的字体族、各自独立的字号与字重,并按「西文 / 中文」分栏挑字体;配置入口是 **设置 → 插件 → 插件配置** 里的原生卡片,改完即时生效。
6
6
 
7
7
  ## 功能
8
8
 
9
9
  - **正文 / 代码字体** —— 两套独立的字体回退列表,拉丁字体在前、中文字体在后;留空表示完全沿用 DSH 的字体栈
10
10
  - **正文字号偏移** —— -3 ~ +6 px,作用于正文与界面文字;与 DSH 自己的「字号大小」叠加而不是覆盖,每一档字号连同它的行高一起等比缩放
11
11
  - **代码字号偏移** —— -3 ~ +6 px,**独立**作用于代码块与行内代码:调正文不会带动代码,调代码也不会带动正文
12
- - **全局字重** —— 300 ~ 600 任意整数;未设置或 400 都表示沿用 DSH 原本的字重
12
+ - **正文字重** —— 300 ~ 600 任意整数,覆盖正文与界面文字的粗细(含标题);未设置或 400 表示沿用 DSH 原本的字重
13
+ - **代码字重** —— 300 ~ 600 任意整数,只覆盖代码块、行内代码与终端输出;未设置或 400 表示代码保持 DSH 原样,也就是**不被正文字重带着走**
13
14
  - **西文 / 中文分栏(简单模式)** —— 卡片顶部一个开关:简单模式只给「西文」「中文」两个单选格(正文/代码各一对),高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不会改动已排好的顺序
14
15
  - **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;搜索不到的名字可以直接「使用 xxx」新建
15
16
  - **拖拽排序** —— 已选字体是 chip 列表,可拖动调整回退顺序,同时保留前移 / 后移按钮(键盘与触屏可用)
@@ -19,7 +20,7 @@
19
20
 
20
21
  | 插件版本 | 支持的 DSH 版本 |
21
22
  | --- | --- |
22
- | **0.1.4**(最新) | 0.1.5-rc.2 |
23
+ | **0.1.6**(最新) | 0.1.5-rc.2 |
23
24
 
24
25
  `engines.dsh` 下限为 **0.1.5-rc.2**,兼容声明只列 **0.1.5 线的最新版本**:它包含这条线的全部修复,是同一大版本里最稳的一版,而同一大版本内的旧版本按语义化版本约定向上兼容。更早或更新的 DSH 未经验证,不作声明。安装或更新时,DSH 市场会依据 `package.json` 里的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies` 做宿主兼容预检。
25
26
 
@@ -49,7 +50,7 @@ dsh plugin --profile web add <插件目录路径>
49
50
 
50
51
  - **打开设置** —— 设置 → 插件 → 插件配置 → 展开「字体增强」卡片
51
52
  - **换字体** —— 简单模式下点「正文 · 西文字体」「正文 · 中文字体」「代码 · 西文字体」「代码 · 中文字体」四个格子选字体;高级模式下用「添加字体」与 chips 编辑器维护完整回退列表
52
- - **调整字号** —— 字号滑块就在对应的字体下面:「正文字号偏移」跟着正文字体、「代码字号偏移」跟着代码字体;再往下是「字重」。拖动松手即生效;改过的字段标「已修改」并可单独重置,卡片底部有「全部重置」
53
+ - **调整字号与字重** —— 滑块就在它作用的对象下面:正文字体 → 正文字号偏移 → 正文字重 → 代码字体 → 代码字号偏移 → 代码字重。拖动松手即生效;改过的字段标「已修改」并可单独重置,卡片底部有「全部重置」
53
54
  - **预览** —— 卡片底部的中英混排与代码预览用当前配置渲染
54
55
  - 设置保存在 DSH 的设置文档里(`settings.yaml`),跟随配置走
55
56
 
package/lib/client.js CHANGED
@@ -38,9 +38,12 @@ var SIZE_FIELD = "sizeOffset";
38
38
  /** Field carrying the code font-size offset in px (0 = leave DSH alone). */
39
39
  var CODE_SIZE_FIELD = "sizeOffsetCode";
40
40
 
41
- /** Field carrying the global font weight (0 = leave DSH alone). */
41
+ /** Field carrying the body/UI font weight (0 = leave DSH alone). */
42
42
  var WEIGHT_FIELD = "weight";
43
43
 
44
+ /** Field carrying the code font weight (0 = leave DSH alone). */
45
+ var CODE_WEIGHT_FIELD = "weightCode";
46
+
44
47
  /** Allowed font-size offset range. The upper bound stays under a 2x scale. */
45
48
  var SIZE_MIN = -3;
46
49
  var SIZE_MAX = 6;
@@ -66,6 +69,7 @@ var DEFAULTS = {
66
69
  sizeOffset: 0,
67
70
  sizeOffsetCode: 0,
68
71
  weight: WEIGHT_UNSET,
72
+ weightCode: WEIGHT_UNSET,
69
73
  };
70
74
 
71
75
  /**
@@ -156,18 +160,15 @@ function normalizeConfig(value) {
156
160
  var source = value !== null && typeof value === "object" ? value : {};
157
161
  var size = clampOffset(source[SIZE_FIELD]);
158
162
  var codeSize = clampOffset(source[CODE_SIZE_FIELD]);
159
- var weight = Number(source[WEIGHT_FIELD]);
160
- if (!isFinite(weight)) weight = WEIGHT_UNSET;
161
- weight = Math.round(weight);
162
- if (weight !== WEIGHT_UNSET) {
163
- weight = Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
164
- }
163
+ var weight = clampWeight(source[WEIGHT_FIELD]);
164
+ var codeWeight = clampWeight(source[CODE_WEIGHT_FIELD]);
165
165
  var config = {};
166
166
  config[SANS_FIELD] = sanitize(source[SANS_FIELD]).slice(0, MAX_STACK_LENGTH);
167
167
  config[MONO_FIELD] = sanitize(source[MONO_FIELD]).slice(0, MAX_STACK_LENGTH);
168
168
  config[SIZE_FIELD] = size;
169
169
  config[CODE_SIZE_FIELD] = codeSize;
170
170
  config[WEIGHT_FIELD] = weight;
171
+ config[CODE_WEIGHT_FIELD] = codeWeight;
171
172
  return config;
172
173
  }
173
174
 
@@ -182,6 +183,19 @@ function clampOffset(value) {
182
183
  return Math.round(Math.min(SIZE_MAX, Math.max(SIZE_MIN, size)));
183
184
  }
184
185
 
186
+ /**
187
+ * Clamp one weight to the allowed range, keeping 0 as "do not touch".
188
+ * @param {unknown} value - candidate weight.
189
+ * @returns {number} the weight the stylesheet may use.
190
+ */
191
+ function clampWeight(value) {
192
+ var weight = Number(value);
193
+ if (!isFinite(weight)) return WEIGHT_UNSET;
194
+ weight = Math.round(weight);
195
+ if (weight === WEIGHT_UNSET) return WEIGHT_UNSET;
196
+ return Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
197
+ }
198
+
185
199
  /**
186
200
  * Format a family list as a CSS font-family value.
187
201
  * @param {readonly string[]} families - family names in precedence order.
@@ -385,7 +399,8 @@ function isDormant(config) {
385
399
  config[MONO_FIELD] === "" &&
386
400
  config[SIZE_FIELD] === 0 &&
387
401
  config[CODE_SIZE_FIELD] === 0 &&
388
- config[WEIGHT_FIELD] === WEIGHT_UNSET
402
+ config[WEIGHT_FIELD] === WEIGHT_UNSET &&
403
+ config[CODE_WEIGHT_FIELD] === WEIGHT_UNSET
389
404
  );
390
405
  }
391
406
 
@@ -415,7 +430,14 @@ function isDormant(config) {
415
430
  * what the shipped stylesheets use), which is why those are re-declared too,
416
431
  * not just their `-font-size` / `-line-height` parts.
417
432
  *
418
- * @param {{sans: string, mono: string, sizeOffset: number, sizeOffsetCode: number, weight: number}} config - normalized config.
433
+ * Weight has no token chain to ride: no shipped rule reads a `-font-weight`
434
+ * token (measured — zero `font-weight: var(--dsw-font-…)` in rc.2), the theme
435
+ * writes literal weights on its own class rules, and that is exactly why the
436
+ * body axis has to be a blanket `!important` rule. The code axis is therefore a
437
+ * selector-scoped `!important` rule placed after it, which wins on specificity
438
+ * (`[class*=…]`) or on source order (`pre, …` ties with `body *`).
439
+ *
440
+ * @param {{sans: string, mono: string, sizeOffset: number, sizeOffsetCode: number, weight: number, weightCode: number}} config - normalized config.
419
441
  * @param {Record<string, string>} [baseTokens] - token name to untouched value.
420
442
  * @returns {string} declarations for one `<style>` element ("" when dormant).
421
443
  */
@@ -426,6 +448,7 @@ function buildFontCss(config, baseTokens) {
426
448
  var offset = normalized[SIZE_FIELD];
427
449
  var codeOffset = normalized[CODE_SIZE_FIELD];
428
450
  var weight = normalized[WEIGHT_FIELD];
451
+ var codeWeight = normalized[CODE_WEIGHT_FIELD];
429
452
  if (isDormant(normalized)) return "";
430
453
  var declarations = [];
431
454
 
@@ -464,9 +487,54 @@ function buildFontCss(config, baseTokens) {
464
487
  // a static one rounds to its own nearest step by itself.
465
488
  declarations.push(bodyAndDescendants("font-weight:" + weight + " !important"));
466
489
  }
490
+ if (codeWeight !== WEIGHT_UNSET) {
491
+ declarations.push(CODE_SELECTOR + "{font-weight:" + codeWeight + " !important}");
492
+ } else if (weight !== WEIGHT_UNSET) {
493
+ // The body axis is a blanket `body, body *` rule, so code would inherit the
494
+ // body weight unless it is explicitly taken back out. `normal` is what DSH
495
+ // itself computes there (the code `font` shorthands carry no weight), so an
496
+ // unset code weight means "code stays exactly as DSH shipped it".
497
+ declarations.push(CODE_SELECTOR + "{font-weight:normal !important}");
498
+ }
467
499
  return declarations.join("\n");
468
500
  }
469
501
 
502
+ /**
503
+ * What counts as code text for the weight axis.
504
+ *
505
+ * There is no token to hook here (see `buildFontCss`), so the code axis has to
506
+ * name the elements. Two layers, both measured against rc.2:
507
+ *
508
+ * 1. The tags DSH's own stylesheets size with a code token: markdown `pre` /
509
+ * inline `code`, the tool card's `:where(pre)` block, form controls, and the
510
+ * CodeMirror editor. Their descendants are listed too, because the blanket
511
+ * body rule matches EVERY element — a `<span>` inside a highlighted block
512
+ * would otherwise be pulled back to the body weight.
513
+ * 2. Containers that only a class name identifies: the terminal and tool-code
514
+ * bodies are plain `div`s (`…_terminal`, `…_codeBody`, `md-code-block`).
515
+ *
516
+ * The `i` flag keeps a casing change in a future DSH from silently dropping the
517
+ * rule, and an unmatched token/class simply means that surface follows the body
518
+ * axis — the same degradation as an unknown token name on the size axes.
519
+ */
520
+ var CODE_SELECTOR = [
521
+ "pre",
522
+ "code",
523
+ "kbd",
524
+ "samp",
525
+ "var",
526
+ "tt",
527
+ "textarea",
528
+ ".cm-editor",
529
+ ".dfp-previewCode",
530
+ '[class*="code" i]',
531
+ '[class*="terminal" i]',
532
+ ]
533
+ .map(function (selector) {
534
+ return selector + "," + selector + " *";
535
+ })
536
+ .join(",");
537
+
470
538
  /**
471
539
  * Values a ratio may multiply: a number with a unit, a bare number, or an
472
540
  * expression. A keyword (`unset`, `inherit`) or an unrecognized shape is left
@@ -810,6 +878,7 @@ var shared = {
810
878
  SIZE_FIELD: SIZE_FIELD,
811
879
  CODE_SIZE_FIELD: CODE_SIZE_FIELD,
812
880
  WEIGHT_FIELD: WEIGHT_FIELD,
881
+ CODE_WEIGHT_FIELD: CODE_WEIGHT_FIELD,
813
882
  SIZE_MIN: SIZE_MIN,
814
883
  SIZE_MAX: SIZE_MAX,
815
884
  WEIGHT_MIN: WEIGHT_MIN,
@@ -827,6 +896,9 @@ var shared = {
827
896
  sanitizeFamily: sanitizeFamily,
828
897
  quoteFamily: quoteFamily,
829
898
  normalizeConfig: normalizeConfig,
899
+ clampOffset: clampOffset,
900
+ clampWeight: clampWeight,
901
+ CODE_SELECTOR: CODE_SELECTOR,
830
902
  formatStack: formatStack,
831
903
  parseStack: parseStack,
832
904
  scaleFor: scaleFor,
@@ -891,6 +963,7 @@ var MONO_FIELD = shared.MONO_FIELD;
891
963
  var SIZE_FIELD = shared.SIZE_FIELD;
892
964
  var CODE_SIZE_FIELD = shared.CODE_SIZE_FIELD;
893
965
  var WEIGHT_FIELD = shared.WEIGHT_FIELD;
966
+ var CODE_WEIGHT_FIELD = shared.CODE_WEIGHT_FIELD;
894
967
  var SIZE_MIN = shared.SIZE_MIN;
895
968
  var SIZE_MAX = shared.SIZE_MAX;
896
969
  var WEIGHT_MIN = shared.WEIGHT_MIN;
@@ -923,6 +996,24 @@ var NEUTRAL_WEIGHT = 400;
923
996
  /** Panel width, kept in one place because positioning reads it too. */
924
997
  var PANEL_WIDTH = 320;
925
998
 
999
+ /**
1000
+ * The host PluginCard's chevron, copied byte for byte from DSH's own frontend
1001
+ * bundle (dsh-quick-toc extracted it first and verified the match on the page;
1002
+ * their copy is archived in Temp\dqt-chevron-path.txt). A `⌄` character is the
1003
+ * wrong icon twice over: at text weight it is thinner than every other card's
1004
+ * arrow, and rotating the text span turns it around the text box centre — the
1005
+ * glyph sits off-centre on its baseline — so the rotation swings it sideways.
1006
+ * An `<svg>` rotates around its own box centre.
1007
+ */
1008
+ var CHEVRON_PATH =
1009
+ "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785" +
1010
+ "C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602" +
1011
+ "C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137" +
1012
+ "L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623" +
1013
+ "C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047" +
1014
+ "C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273" +
1015
+ "L10.5762 5.07617L11 4.65137L11.8486 5.5Z";
1016
+
926
1017
  /** Panel height cap, used to decide whether it opens up or down. */
927
1018
  var PANEL_HEIGHT = 380;
928
1019
 
@@ -940,7 +1031,7 @@ var DICTS = {
940
1031
  en: {
941
1032
  "card.title": "Font tune",
942
1033
  "card.description":
943
- "Body and code fonts, a size offset for each, weight, and a West/CJK split",
1034
+ "Body and code fonts, a size and a weight for each, and a West/CJK split",
944
1035
  "card.expand": "Expand",
945
1036
  "card.collapse": "Collapse",
946
1037
  "card.resetAll": "Reset all",
@@ -1000,8 +1091,12 @@ var DICTS = {
1000
1091
  "Adds {offset} to code blocks and inline code only; the body offset does not reach them. 0 keeps DSH's sizes.",
1001
1092
  "size.unit": "px",
1002
1093
 
1003
- "weight.label": "Font weight",
1004
- "weight.hint": "Overrides text weight everywhere, headings included. Default weight is 400.",
1094
+ "weight.bodyLabel": "Body font weight",
1095
+ "weight.bodyHint":
1096
+ "Overrides the weight of text and interface copy, headings included. Code keeps its own weight. Default weight is 400.",
1097
+ "weight.codeLabel": "Code font weight",
1098
+ "weight.codeHint":
1099
+ "Overrides code blocks, inline code and terminal output only. 400 or unset keeps DSH's own weight and takes code out of the body weight.",
1005
1100
  "weight.unset": "Unset",
1006
1101
 
1007
1102
  "preview.label": "Preview",
@@ -1016,7 +1111,7 @@ var DICTS = {
1016
1111
  },
1017
1112
  zh: {
1018
1113
  "card.title": "字体增强",
1019
- "card.description": "正文与代码字体、各自的字号偏移、字重、中西文分栏",
1114
+ "card.description": "正文与代码字体、各自的字号与字重、中西文分栏",
1020
1115
  "card.expand": "展开",
1021
1116
  "card.collapse": "收起",
1022
1117
  "card.resetAll": "全部重置",
@@ -1074,8 +1169,12 @@ var DICTS = {
1074
1169
  "只作用于代码块与行内代码,与正文字号互不影响;0 表示保持原样。",
1075
1170
  "size.unit": "px",
1076
1171
 
1077
- "weight.label": "字重",
1078
- "weight.hint": "覆盖全局文字粗细(含标题);默认字重为400。",
1172
+ "weight.bodyLabel": "正文字重",
1173
+ "weight.bodyHint":
1174
+ "覆盖正文与界面文字的粗细(含标题);代码不受它影响。默认字重为400。",
1175
+ "weight.codeLabel": "代码字重",
1176
+ "weight.codeHint":
1177
+ "只覆盖代码块、行内代码与终端输出;400 或未设置表示保持 DSH 原样,也就是不被正文字重带着走。",
1079
1178
  "weight.unset": "未设置",
1080
1179
 
1081
1180
  "preview.label": "预览",
@@ -1126,7 +1225,14 @@ var CARD_CSS = [
1126
1225
  ".dfp-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
1127
1226
  ".dfp-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
1128
1227
  ".dfp-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
1129
- ".dfp-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
1228
+ // The chevron is the host PluginCard's own 14×14 SVG icon, not a text glyph:
1229
+ // a `⌄` character rendered at text weight looked thinner than every other
1230
+ // card's arrow, and rotating the TEXT SPAN swung it around the text box
1231
+ // centre (the glyph sits off-centre on its baseline) instead of turning in
1232
+ // place. As an `<svg>` the class goes on the icon itself and
1233
+ // `transform:rotate(180deg)` rotates around the icon's own box centre —
1234
+ // exactly what dsh-quick-toc's card does.
1235
+ ".dfp-chevron{color:var(--dsw-alias-label-tertiary);flex:none;display:block;transition:transform .16s}",
1130
1236
  ".dfp-chevronOpen{transform:rotate(180deg)}",
1131
1237
  ".dfp-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}",
1132
1238
  ".dfp-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}",
@@ -2272,6 +2378,7 @@ function FontCard(props) {
2272
2378
  var offset = config[SIZE_FIELD];
2273
2379
  var codeOffset = config[CODE_SIZE_FIELD];
2274
2380
  var weight = config[WEIGHT_FIELD];
2381
+ var codeWeight = config[CODE_WEIGHT_FIELD];
2275
2382
  var offsetText = offset > 0 ? "+" + offset : String(offset);
2276
2383
  var codeOffsetText = codeOffset > 0 ? "+" + codeOffset : String(codeOffset);
2277
2384
 
@@ -2312,6 +2419,43 @@ function FontCard(props) {
2312
2419
  );
2313
2420
  };
2314
2421
 
2422
+ /**
2423
+ * One weight slider, under the family it reweights.
2424
+ * @param {object} props - field, label keys and current value.
2425
+ * @returns {object} the field shell wrapping the slider.
2426
+ */
2427
+ var weightField = function (props) {
2428
+ return h(
2429
+ FieldShell,
2430
+ {
2431
+ t: t,
2432
+ label: t(props.labelKey),
2433
+ hint: t(props.hintKey),
2434
+ overridden: overridden(props.field) && props.value !== WEIGHT_UNSET,
2435
+ disabled: !writable,
2436
+ onReset: function () {
2437
+ resetField(props.field);
2438
+ },
2439
+ },
2440
+ h(NumberSlider, {
2441
+ min: WEIGHT_MIN,
2442
+ max: WEIGHT_MAX,
2443
+ value: props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value,
2444
+ disabled: !writable,
2445
+ label: t(props.labelKey),
2446
+ readout: props.value === WEIGHT_UNSET ? t("weight.unset") : String(props.value),
2447
+ minLabel: String(WEIGHT_MIN),
2448
+ maxLabel: String(WEIGHT_MAX),
2449
+ onChange: function (value) {
2450
+ // 400 is DSH's own body weight, so choosing it means "leave the axis
2451
+ // alone" rather than "write 400 on every element".
2452
+ if (value === NEUTRAL_WEIGHT) resetField(props.field);
2453
+ else setField(props.field, value);
2454
+ },
2455
+ })
2456
+ );
2457
+ };
2458
+
2315
2459
  return h(
2316
2460
  "li",
2317
2461
  { className: "dfp-card" + (open ? " dfp-cardOpen" : "") },
@@ -2333,9 +2477,16 @@ function FontCard(props) {
2333
2477
  h("span", { className: "dfp-description" }, t("card.description"))
2334
2478
  ),
2335
2479
  h(
2336
- "span",
2337
- { className: "dfp-chevron" + (open ? " dfp-chevronOpen" : ""), "aria-hidden": "true" },
2338
- "⌄"
2480
+ "svg",
2481
+ {
2482
+ className: "dfp-chevron" + (open ? " dfp-chevronOpen" : ""),
2483
+ width: 14,
2484
+ height: 14,
2485
+ viewBox: "0 0 14 14",
2486
+ fill: "none",
2487
+ "aria-hidden": "true",
2488
+ },
2489
+ h("path", { d: CHEVRON_PATH, fill: "currentColor" })
2339
2490
  )
2340
2491
  ),
2341
2492
  open
@@ -2427,6 +2578,13 @@ function FontCard(props) {
2427
2578
  text: offsetText,
2428
2579
  }),
2429
2580
 
2581
+ weightField({
2582
+ field: WEIGHT_FIELD,
2583
+ labelKey: "weight.bodyLabel",
2584
+ hintKey: "weight.bodyHint",
2585
+ value: weight,
2586
+ }),
2587
+
2430
2588
  view === "simple"
2431
2589
  ? h(SimpleFamilyField, {
2432
2590
  t: t,
@@ -2476,35 +2634,12 @@ function FontCard(props) {
2476
2634
  text: codeOffsetText,
2477
2635
  }),
2478
2636
 
2479
- h(
2480
- FieldShell,
2481
- {
2482
- t: t,
2483
- label: t("weight.label"),
2484
- hint: t("weight.hint"),
2485
- overridden: overridden(WEIGHT_FIELD) && weight !== WEIGHT_UNSET,
2486
- disabled: !writable,
2487
- onReset: function () {
2488
- resetField(WEIGHT_FIELD);
2489
- },
2490
- },
2491
- h(NumberSlider, {
2492
- min: WEIGHT_MIN,
2493
- max: WEIGHT_MAX,
2494
- value: weight === WEIGHT_UNSET ? NEUTRAL_WEIGHT : weight,
2495
- disabled: !writable,
2496
- label: t("weight.label"),
2497
- readout: weight === WEIGHT_UNSET ? t("weight.unset") : String(weight),
2498
- minLabel: String(WEIGHT_MIN),
2499
- maxLabel: String(WEIGHT_MAX),
2500
- onChange: function (value) {
2501
- // 400 is DSH's own body weight, so choosing it means "leave the
2502
- // axis alone" rather than "write 400 on every element".
2503
- if (value === NEUTRAL_WEIGHT) resetField(WEIGHT_FIELD);
2504
- else setField(WEIGHT_FIELD, value);
2505
- },
2506
- })
2507
- ),
2637
+ weightField({
2638
+ field: CODE_WEIGHT_FIELD,
2639
+ labelKey: "weight.codeLabel",
2640
+ hintKey: "weight.codeHint",
2641
+ value: codeWeight,
2642
+ }),
2508
2643
 
2509
2644
  h(
2510
2645
  "div",
@@ -2537,7 +2672,7 @@ function FontCard(props) {
2537
2672
  h(
2538
2673
  "div",
2539
2674
  {
2540
- className: "dfp-previewText",
2675
+ className: "dfp-previewText dfp-previewCode",
2541
2676
  style:
2542
2677
  config[MONO_FIELD] === ""
2543
2678
  ? undefined
@@ -2563,6 +2698,7 @@ function FontCard(props) {
2563
2698
  resetField(SIZE_FIELD);
2564
2699
  resetField(CODE_SIZE_FIELD);
2565
2700
  resetField(WEIGHT_FIELD);
2701
+ resetField(CODE_WEIGHT_FIELD);
2566
2702
  },
2567
2703
  },
2568
2704
  t("card.resetAll")
package/lib/index.js CHANGED
@@ -23,6 +23,7 @@ import shared from "./shared.cjs";
23
23
  const {
24
24
  buildFontCss,
25
25
  CODE_SIZE_FIELD,
26
+ CODE_WEIGHT_FIELD,
26
27
  FALLBACK_TOKENS,
27
28
  MONO_FIELD,
28
29
  NAMESPACE,
@@ -53,8 +54,9 @@ const MAX_STACK = 200;
53
54
  *
54
55
  * Every field defaults to "leave DSH alone": an empty stack injects no family
55
56
  * rule, a zero offset injects no size rule on either axis, and a zero weight
56
- * injects no weight rule. Installing the plugin therefore changes nothing until
57
- * the user asks for something.
57
+ * injects no weight rule (an unset CODE weight still writes the rule that keeps
58
+ * code out of the body weight). Installing the plugin therefore changes nothing
59
+ * until the user asks for something.
58
60
  */
59
61
  export const Config = z.object({
60
62
  [SANS_FIELD]: z
@@ -91,7 +93,15 @@ export const Config = z.object({
91
93
  .max(WEIGHT_MAX)
92
94
  .default(WEIGHT_UNSET)
93
95
  .description(
94
- `Global font weight (${WEIGHT_MIN}..${WEIGHT_MAX}, ${WEIGHT_UNSET} keeps DSH's own weights)`
96
+ `Body/UI font weight (${WEIGHT_MIN}..${WEIGHT_MAX}, ${WEIGHT_UNSET} keeps DSH's own weights; code is not affected)`
97
+ ),
98
+ [CODE_WEIGHT_FIELD]: z
99
+ .number()
100
+ .min(WEIGHT_UNSET)
101
+ .max(WEIGHT_MAX)
102
+ .default(WEIGHT_UNSET)
103
+ .description(
104
+ `Code font weight (${WEIGHT_MIN}..${WEIGHT_MAX}, ${WEIGHT_UNSET} keeps DSH's own weights; independent of the body weight)`
95
105
  ),
96
106
  });
97
107
 
package/lib/shared.cjs CHANGED
@@ -29,9 +29,12 @@ var SIZE_FIELD = "sizeOffset";
29
29
  /** Field carrying the code font-size offset in px (0 = leave DSH alone). */
30
30
  var CODE_SIZE_FIELD = "sizeOffsetCode";
31
31
 
32
- /** Field carrying the global font weight (0 = leave DSH alone). */
32
+ /** Field carrying the body/UI font weight (0 = leave DSH alone). */
33
33
  var WEIGHT_FIELD = "weight";
34
34
 
35
+ /** Field carrying the code font weight (0 = leave DSH alone). */
36
+ var CODE_WEIGHT_FIELD = "weightCode";
37
+
35
38
  /** Allowed font-size offset range. The upper bound stays under a 2x scale. */
36
39
  var SIZE_MIN = -3;
37
40
  var SIZE_MAX = 6;
@@ -57,6 +60,7 @@ var DEFAULTS = {
57
60
  sizeOffset: 0,
58
61
  sizeOffsetCode: 0,
59
62
  weight: WEIGHT_UNSET,
63
+ weightCode: WEIGHT_UNSET,
60
64
  };
61
65
 
62
66
  /**
@@ -147,18 +151,15 @@ function normalizeConfig(value) {
147
151
  var source = value !== null && typeof value === "object" ? value : {};
148
152
  var size = clampOffset(source[SIZE_FIELD]);
149
153
  var codeSize = clampOffset(source[CODE_SIZE_FIELD]);
150
- var weight = Number(source[WEIGHT_FIELD]);
151
- if (!isFinite(weight)) weight = WEIGHT_UNSET;
152
- weight = Math.round(weight);
153
- if (weight !== WEIGHT_UNSET) {
154
- weight = Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
155
- }
154
+ var weight = clampWeight(source[WEIGHT_FIELD]);
155
+ var codeWeight = clampWeight(source[CODE_WEIGHT_FIELD]);
156
156
  var config = {};
157
157
  config[SANS_FIELD] = sanitize(source[SANS_FIELD]).slice(0, MAX_STACK_LENGTH);
158
158
  config[MONO_FIELD] = sanitize(source[MONO_FIELD]).slice(0, MAX_STACK_LENGTH);
159
159
  config[SIZE_FIELD] = size;
160
160
  config[CODE_SIZE_FIELD] = codeSize;
161
161
  config[WEIGHT_FIELD] = weight;
162
+ config[CODE_WEIGHT_FIELD] = codeWeight;
162
163
  return config;
163
164
  }
164
165
 
@@ -173,6 +174,19 @@ function clampOffset(value) {
173
174
  return Math.round(Math.min(SIZE_MAX, Math.max(SIZE_MIN, size)));
174
175
  }
175
176
 
177
+ /**
178
+ * Clamp one weight to the allowed range, keeping 0 as "do not touch".
179
+ * @param {unknown} value - candidate weight.
180
+ * @returns {number} the weight the stylesheet may use.
181
+ */
182
+ function clampWeight(value) {
183
+ var weight = Number(value);
184
+ if (!isFinite(weight)) return WEIGHT_UNSET;
185
+ weight = Math.round(weight);
186
+ if (weight === WEIGHT_UNSET) return WEIGHT_UNSET;
187
+ return Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
188
+ }
189
+
176
190
  /**
177
191
  * Format a family list as a CSS font-family value.
178
192
  * @param {readonly string[]} families - family names in precedence order.
@@ -376,7 +390,8 @@ function isDormant(config) {
376
390
  config[MONO_FIELD] === "" &&
377
391
  config[SIZE_FIELD] === 0 &&
378
392
  config[CODE_SIZE_FIELD] === 0 &&
379
- config[WEIGHT_FIELD] === WEIGHT_UNSET
393
+ config[WEIGHT_FIELD] === WEIGHT_UNSET &&
394
+ config[CODE_WEIGHT_FIELD] === WEIGHT_UNSET
380
395
  );
381
396
  }
382
397
 
@@ -406,7 +421,14 @@ function isDormant(config) {
406
421
  * what the shipped stylesheets use), which is why those are re-declared too,
407
422
  * not just their `-font-size` / `-line-height` parts.
408
423
  *
409
- * @param {{sans: string, mono: string, sizeOffset: number, sizeOffsetCode: number, weight: number}} config - normalized config.
424
+ * Weight has no token chain to ride: no shipped rule reads a `-font-weight`
425
+ * token (measured — zero `font-weight: var(--dsw-font-…)` in rc.2), the theme
426
+ * writes literal weights on its own class rules, and that is exactly why the
427
+ * body axis has to be a blanket `!important` rule. The code axis is therefore a
428
+ * selector-scoped `!important` rule placed after it, which wins on specificity
429
+ * (`[class*=…]`) or on source order (`pre, …` ties with `body *`).
430
+ *
431
+ * @param {{sans: string, mono: string, sizeOffset: number, sizeOffsetCode: number, weight: number, weightCode: number}} config - normalized config.
410
432
  * @param {Record<string, string>} [baseTokens] - token name to untouched value.
411
433
  * @returns {string} declarations for one `<style>` element ("" when dormant).
412
434
  */
@@ -417,6 +439,7 @@ function buildFontCss(config, baseTokens) {
417
439
  var offset = normalized[SIZE_FIELD];
418
440
  var codeOffset = normalized[CODE_SIZE_FIELD];
419
441
  var weight = normalized[WEIGHT_FIELD];
442
+ var codeWeight = normalized[CODE_WEIGHT_FIELD];
420
443
  if (isDormant(normalized)) return "";
421
444
  var declarations = [];
422
445
 
@@ -455,9 +478,54 @@ function buildFontCss(config, baseTokens) {
455
478
  // a static one rounds to its own nearest step by itself.
456
479
  declarations.push(bodyAndDescendants("font-weight:" + weight + " !important"));
457
480
  }
481
+ if (codeWeight !== WEIGHT_UNSET) {
482
+ declarations.push(CODE_SELECTOR + "{font-weight:" + codeWeight + " !important}");
483
+ } else if (weight !== WEIGHT_UNSET) {
484
+ // The body axis is a blanket `body, body *` rule, so code would inherit the
485
+ // body weight unless it is explicitly taken back out. `normal` is what DSH
486
+ // itself computes there (the code `font` shorthands carry no weight), so an
487
+ // unset code weight means "code stays exactly as DSH shipped it".
488
+ declarations.push(CODE_SELECTOR + "{font-weight:normal !important}");
489
+ }
458
490
  return declarations.join("\n");
459
491
  }
460
492
 
493
+ /**
494
+ * What counts as code text for the weight axis.
495
+ *
496
+ * There is no token to hook here (see `buildFontCss`), so the code axis has to
497
+ * name the elements. Two layers, both measured against rc.2:
498
+ *
499
+ * 1. The tags DSH's own stylesheets size with a code token: markdown `pre` /
500
+ * inline `code`, the tool card's `:where(pre)` block, form controls, and the
501
+ * CodeMirror editor. Their descendants are listed too, because the blanket
502
+ * body rule matches EVERY element — a `<span>` inside a highlighted block
503
+ * would otherwise be pulled back to the body weight.
504
+ * 2. Containers that only a class name identifies: the terminal and tool-code
505
+ * bodies are plain `div`s (`…_terminal`, `…_codeBody`, `md-code-block`).
506
+ *
507
+ * The `i` flag keeps a casing change in a future DSH from silently dropping the
508
+ * rule, and an unmatched token/class simply means that surface follows the body
509
+ * axis — the same degradation as an unknown token name on the size axes.
510
+ */
511
+ var CODE_SELECTOR = [
512
+ "pre",
513
+ "code",
514
+ "kbd",
515
+ "samp",
516
+ "var",
517
+ "tt",
518
+ "textarea",
519
+ ".cm-editor",
520
+ ".dfp-previewCode",
521
+ '[class*="code" i]',
522
+ '[class*="terminal" i]',
523
+ ]
524
+ .map(function (selector) {
525
+ return selector + "," + selector + " *";
526
+ })
527
+ .join(",");
528
+
461
529
  /**
462
530
  * Values a ratio may multiply: a number with a unit, a bare number, or an
463
531
  * expression. A keyword (`unset`, `inherit`) or an unrecognized shape is left
@@ -801,6 +869,7 @@ var shared = {
801
869
  SIZE_FIELD: SIZE_FIELD,
802
870
  CODE_SIZE_FIELD: CODE_SIZE_FIELD,
803
871
  WEIGHT_FIELD: WEIGHT_FIELD,
872
+ CODE_WEIGHT_FIELD: CODE_WEIGHT_FIELD,
804
873
  SIZE_MIN: SIZE_MIN,
805
874
  SIZE_MAX: SIZE_MAX,
806
875
  WEIGHT_MIN: WEIGHT_MIN,
@@ -818,6 +887,9 @@ var shared = {
818
887
  sanitizeFamily: sanitizeFamily,
819
888
  quoteFamily: quoteFamily,
820
889
  normalizeConfig: normalizeConfig,
890
+ clampOffset: clampOffset,
891
+ clampWeight: clampWeight,
892
+ CODE_SELECTOR: CODE_SELECTOR,
821
893
  formatStack: formatStack,
822
894
  parseStack: parseStack,
823
895
  scaleFor: scaleFor,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-fonttune",
3
- "version": "0.1.4",
4
- "description": "Font plugin for the DeepSeek Harness Web GUI: body and code font families, a separate font-size offset for each, a global font weight, and a West/CJK split picker.",
3
+ "version": "0.1.6",
4
+ "description": "Font plugin for the DeepSeek Harness Web GUI: body and code font families, a separate font size and weight for each, and a West/CJK split picker.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {