dsh-fonttune 0.2.5 → 0.2.7

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,22 @@
2
2
 
3
3
  All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.2.7] - 2026-09-24
6
+
7
+ ### Changed
8
+ - **DSH 0.1.7-rc.2 support**: it is a patch update of 0.1.7-rc.1 with the same settings service and configuration-card seats, so no code had to change; each item was verified in an isolated environment (host start, stylesheet applied, card expanded, a change written through), and the compatibility table lists 0.1.7-rc.2.
9
+
10
+ ## [0.2.6] - 2026-09-24
11
+
12
+ ### Fixed
13
+ - **Dragging the UI stuttered**: dragging the sidebar width, the conversation-list width and similar handles dropped frames (the injected stylesheet matched the whole page against every rule on each style recalculation). Each axis now matches only the elements it needs: dragging is smooth again, and the typography and the code axis look unchanged.
14
+ - **Opening "Font tune" on the Plugins page did not show the settings right away**: on 0.1.7 the page treats opening the entry as the expansion gesture, while the card carried a collapsible header of its own and asked for a second click. It now opens expanded, and the chevron still collapses it by hand.
15
+
16
+ ### Changed
17
+ - **DSH 0.1.7-rc.1 support**: it is the 0.1.7 interface on the rc channel, sharing the settings service and the configuration-card seats with 0.1.7-alpha.x, so no code had to change; each item was verified in an isolated environment (host start, stylesheet applied, card open and close, a change written through).
18
+ - **The declared compatibility floor is now 0.1.5-rc.3**: 0.1.5-rc.1 and 0.1.5-rc.2 are no longer claimed (`engines.dsh` tightened to match), and the README compatibility table follows.
19
+ - **The loader row id matches the settings namespace** (`fonttune` → `dsh-fonttune`): from DSH 0.1.7 on, settings are addressed by entry id, and a name that differs from the namespace the plugin registers leaves the saved families and weights in the imported file, unread (which shows up as "the plugin does nothing"). With the matching id the package can also be installed through the profile's `dsh.profile.bundles`.
20
+
5
21
  ## [0.2.5] - 2026-09-23
6
22
 
7
23
  ### Fixed
@@ -23,154 +39,132 @@ All notable changes to **dsh-fonttune** are documented here. Chinese version: [C
23
39
  ## [0.2.3] - 2026-09-22
24
40
 
25
41
  ### Fixed
26
- - **A refused settings write is no longer swallowed**: `scope.set` / `scope.unset` failures used to hang off an empty `catch` — the control snapped back to the stored value and the user believed it saved. Both a synchronous throw and a rejected promise now print "The setting was not saved: <reason>" (clipped at 160 characters, full text on hover) on the right of the preset row, using the same row that fades itself out after 2.6 s, so nothing else moves.
27
- - **An export the clipboard refuses no longer hands over truncated JSON**: the old fallback cut the JSON to 120/200 characters and printed it in that one-line status row, which the user could not copy from. Three layers now: (1) the async Clipboard API; (2) outside a secure context (the remote HTTP origin) `navigator.clipboard` does not exist at all, so a hidden textarea plus `document.execCommand("copy")` — the legacy path still works there; (3) only when both fail does the **whole** JSON land in an auto-focused, fully selected, read-only box that reuses the field surface, where one copy shortcut is all that is left (it puts itself away on `onCopy`). The status row says just "Clipboard unavailable — press Ctrl+C to copy the selected text."
28
- - **Three problems in the font picker**: (1) the four curated groups (monospace / CJK / Latin / generic) used to vanish whenever the local enumeration *worked* and a search was running, so a curated family this machine does not have could not be found by name at all; they are now always listed, filtered by the search text. (2) The local group no longer repeats what a curated heading already offers — a family is listed once per panel, and the curated heading wins, because on Windows nearly the whole Latin group is installed and letting the local list win would empty the short list. (3) A refused enumeration was cached **forever**, so granting the font permission in the browser changed nothing; a refusal is now trusted for 20 seconds and the next panel open retries — a still-blocked permission is rejected instantly without prompting again, so the retry costs nothing.
29
- - **The token refresh polls cheaply**: the size axis has to re-read its base values after DSH writes its runtime tokens, and the old loop walked every property of every rule of every stylesheet every four seconds. Each tick now compares three cheap signals first (body's `style` attribute, the root class, the sheet count) and returns untouched when none moved; every eighth tick still does a full sweep, which is what catches an in-place rewrite inside the CSS-in-JS sheet, and `theme/change` still forces one. An offline counting stub proves it: no CSS read while the environment is unchanged, the very next cheap tick follows a moved inline token, and the eighth tick sweeps anyway.
30
- - **The per-theme "code font inside the dialog" rule missed its theme prefix** (dormant: per-theme is off today). `pushScopedCodeFamily` emitted its scoped rule without `body[data-ds-dark-theme]`, so with per-theme on the dark set would re-assert the **light** code family inside a dark dialog. It is prefixed like every other rule now, and a case asserts that every per-theme rule starts with the theme attribute.
31
- - **`test/split-verify.mjs` was measuring a retired axis**: it still wrote `sizeOffset` (the interface size axis retired in 0.2.0) and expected the conversation chain to follow, so it rightly went red on a real machine. It now writes `sizeOffsetDialog` + `sizeOffsetCode`, computes its ratios from the measured baseline (the conversation offset rides DSH's own size, the code axis scales over the 16px reference — the two expectations differ), and adds a live check that the retired interface size axis moves nothing.
42
+ - **A refused settings write no longer looks like a successful one**: when the host rejects a save, the preset row says "The setting was not saved: <reason>" instead of silently snapping the control back to the stored value.
43
+ - **Export hands over the whole thing even without a clipboard**: the clipboard is tried first; on an origin that has none (the remote HTTP entry) the legacy copy path is used; only when both fail does the full JSON land in a fully selected read-only box, one copy shortcut away.
44
+ - **The font picker**: the four curated groups (monospace / CJK / Latin / generic) no longer vanish once local enumeration succeeds, so a curated family this machine does not have can still be found by name; a family is listed once; a refused font permission is no longer remembered forever — reopening the panel retries after twenty seconds.
45
+ - Every per-theme rule now carries its theme prefix (per-theme is not open yet; a latent bug fixed ahead of time).
32
46
 
33
47
  ### Changed
34
- - **A smaller stylesheet**: the cascaded `:not(a):not(b):not(c)…` chain collapsed into one `:not(a,b,c…)`. That chain was copied into both dialog selectors and the interface rule, and each copy saves roughly 120 bytes: a real user's injected CSS went **2735 → 2440 B** with `:not(` occurrences **62 → 3**. Specificity really does drop (`:not(a):not(b)` sums its arguments, `:not(a,b)` takes the most specific one), so the risk was measured first: the entire DSH 0.1.5-rc.2 frontend holds exactly one `!important` typography declaration (`font-size` on inline markdown `code`, and this plugin never emits a `font-size`), no installed plugin adds another `!important` weight/family rule, every rule here is `!important` (which already beats the theme's non-important class rules) and the rules that could collide are held apart by these very exclusions rather than by their weight. A live re-run of WEIGHT OK passes all 17 checks (the interface moves 137 text elements, the markdown subtree 0). The per-theme prefix helper now splits a selector list on **top-level** commas only, so the commas inside `:not(a,b)` cannot cut a selector in half.
35
- - **The retired size and line-height axes left the value surface**: `sizeOffset` and `lineHeight` (the interface's size and line height, which have emitted nothing since 0.2.0) stay in the schema and the document — old configurations still parse and `normalizeConfig` still carries them — but they are no longer part of a preset snapshot and no longer decide whether the dark set is a second set, so a light/dark pair that differs only there emits one stylesheet instead of two. "Reset every axis" **still clears them**, so a value nothing shows cannot hide in a document forever.
36
- - **Dead code removed**: 31 dictionary keys nobody renders (both locales — the retired interface size/line-height copy, the per-theme editor, the old preset button row, the fixed preview label), one dead CSS rule (`.dfp-presetForm`) and seven dead exports (`MARKER`, `isShorthandToken`, `bodyTokenNames`, `uiLineTokenNames`, `deltaTokenNames`, `codeTokenNames`, `uiShorthandTokens`, `scaleFontShorthand`). `VALUE_FIELDS` / `RETIRED_FIELDS` / `DURABLE_FIELDS` now name the three ideas — value axes, stored-but-unrendered, every durable field — separately.
37
- - **One entry point for the browser checks**: `npm run verify:browser` (`test/browser-verify.mjs`) runs the read-only `browser-probe` / `ui-walk` / `style-verify` by default, adds the four scripts that write the settings namespace under `--writers` (each snapshots the user layer and restores it before exiting), takes `--only a,b` for a subset, and prints a per-script time and exit-code summary. Two walk scripts were rewritten for the 0.2.x card: `slider-walk` expands the section it needs first (the card opens fully collapsed) and asserts "a drag writes nothing, a release writes, each axis lands on its own field"; `split-walk` writes a three-entry stack and asserts the simple-mode slots and remainder note, that a mode round-trip leaves the stack alone, and that picking a Western family in the real panel replaces exactly that slot. **A missing control is now a failure** — the old scripts printed "not found" and exited zero, which is a false green.
48
+ - **A smaller stylesheet and a cheaper background**: collapsing the exclusion lists took one real configuration's stylesheet from 2735 to 2440 bytes, and the size axis now compares three cheap signals before touching any stylesheet at all.
38
49
  - Compatibility table gained 0.2.3.
39
50
 
40
- ### Tests
41
- - `test/run.mjs` (86 checks): the picker's grouping rules (curated groups always present, no family twice, the local cap and sanitizing, the refusal-retry contract) and the polling gate with a counting stub (negative control included).
42
- - `test/render-card.mjs` (21 checks): a second render after a state change keeping the hook order, both shapes of a refused write being reported, the export fallback handing over the whole JSON selected, reset clearing the retired axes, and a retired axis not being a value axis.
43
- - Live (managed instance + headless Edge): WEIGHT OK (17 checks), SPLIT OK (9), SLIDER OK (9), SPLIT WALK OK (14), browser-probe and ui-walk green.
44
51
 
45
52
  ## [0.2.2] - 2026-09-16
46
53
 
47
54
  ### Fixed
48
- - **Simple mode: picking or clearing the CJK slot threw and lost the change** (introduced in 0.2.0). The two handlers behind that slot read `edit[field]`, and no binding of that name exists in the module (the real one is `editing`). The western slot was fine; only the CJK slot broke — in **any** of the three family sections (conversation / interface / code), picking a family in the 中文 slot or clearing that chip threw `ReferenceError: edit is not defined`, wrote nothing and changed nothing on screen. The fix is those two references. **Why it went unnoticed**: the card tests only ever rendered the tree and never invoked a single control callback, so an unwired handler stayed green offline.
49
- - **A guard for that whole class of bug**: `test/render-card.mjs` gained two cases that **fire every rendered control's callback with the payload its control passes** (a slider's min/max, every option of a segmented control, the family picker's `onPick`/`onRemove`, the preset select's full preset entry, a text input's DOM event, every `<button>`'s `onClick`) and assert that none throws — plus a separate assertion that the three CJK slots really *wrote* into the settings, not merely failed to throw. Negative control: injecting the 0.2.1 code turns both cases red; restoring the fix turns them green.
50
- - **`lib/` can no longer drift from `src/` silently**: the new `test/artifacts.mjs` re-renders the build in memory and compares it byte for byte with the committed `lib/client.js`, `lib/shared.cjs` and `lib/index.js` (run by `npm run verify` and by `prepublishOnly`). Every other suite reads `lib/`, so a source-only edit used to be tested — and shipped — as the previous build. `build.mjs` was split into pure functions (`renderArtifacts` / `readSources`) and no longer has side effects when imported.
51
- - **The release workflow never ran the tests**: `npm pack` does **not** trigger `prepublishOnly` (only `npm publish` does), so the tag-triggered release had always packed an untested artifact. It now installs and runs `npm run verify` before packing — deliberately **without** building first, because `test/artifacts.mjs` is exactly the check that the committed `lib/` is what the current `src/` produces, and a build step would mask that — and asserts that the tag equals the `package.json` version.
52
- - **The CHANGELOG extraction matched version prefixes**: the release body used `index(line, "## " pat)`, so `0.2.1` also matched a `0.2.10` heading. It now matches `## <version> ` exactly and **fails the job** when the version has no section (it used to fall back to generated notes silently).
53
- - **Two common CJK family names were not recognised**: `MS PGothic` and `MS PMincho` were treated as non-CJK (while `MS Gothic` and `MS Mincho` were fine), which feeds the simple mode's CJK slot. Fixed to `ms ?p?(?:gothic|mincho)`, the safe direction for a name heuristic.
54
- - **`shiftTokens` guards zero**: a zero offset used to emit a real `calc((14px) + 0px)` declaration that disagrees with `isDormant` and `sameValueSet`. Unreachable today (the call sites guard), but a trap for the next caller.
55
- - **`build.mjs --watch` watched files instead of the directory**: after an editor renamed a source, the process kept waiting on a dead inode and never rebuilt. It now watches `src/` and filters by name, and logs each successful rebuild.
55
+ - **In simple mode the CJK slot could not be picked or cleared**: choosing a family there, or clearing that chip, threw and lost the change (the western slot was fine); all three family sections now write their CJK slot properly.
56
+ - **`MS PGothic` / `MS PMincho` are recognised as CJK**: they used to be treated as western, which feeds simple mode's CJK slot.
57
+ - **A guard for unwired controls**: the offline checks now fire every control's callback (sliders, segmented controls, the family picker, the preset select, text inputs, every button), so a handler that was never connected fails immediately.
56
58
 
57
59
  ### Changed
58
- - **Documentation corrections**: the README's "editing `src/client.js` is enough after a refresh" omitted the essential step — the browser loads `lib/`, so `node build.mjs` comes first (both languages). The claim that "the DSH market runs a host-compatibility preflight against `engines.dsh` / `dshReleases` / `peerDependencies`" has no implementation anywhere in the local rc.2 distribution, so it now says what is true: the market entry **states** those requirements so the host a version needs can be checked before installing.
59
- - `package.json`: added the test-only `devDependencies` entry (`@deepseek-ai/schemastery`) and a `verify` script; the compatibility table gained 0.2.2.
60
+ - **A release can no longer ship an untested package**: the tag-triggered build runs the full check before packing, and asserts that the packed artifact matches the sources and that the tag matches the version.
61
+ - Documentation: editing a source file requires a build first (the browser loads the built artifact), and the market entry only *states* its compatibility — there is no pre-install preflight.
62
+
60
63
 
61
64
  ## [0.2.1] - 2026-09-16
62
65
 
63
66
  ### Fixed
64
- - **"Turning following off leaves the interface weight at the followed value, and reset does nothing"** (reproduced on a real machine): the stylesheet reaches the page **twice** — the host half renders one copy into the served index for the first frame (`webserver/index-inject`) and the browser half keeps a second one — and the served copy is only rebuilt on the **next index render**. With both present, adding or changing a rule is fine (the browser half's copy comes later and wins at equal specificity), but **removing a rule cannot work**: turn following off, or reset the interface weight, and the browser half's fresh stylesheet no longer contains that `font-weight` rule while the served copy still does — so the interface stayed at the followed 480 until the page was reloaded. Two changes: ① the host half now emits an **`html` row** (`<style data-plugin="dsh-fonttune" data-plugin-css="dsh-fonttune">…</style>` instead of an un-stamped `style` row) and the browser half **adopts that element** instead of appending its own (`createStylesheet` looks it up first, falls back to creating one, and re-creates if it ever leaves the document) — one element for the element's whole life, so rules can be added *and* removed. Verified live: the new "turn following off without reloading" step in `test/weight-verify.mjs` shows the interface going 480 → 400 (DSH's own) immediately, the stylesheet count staying at `count=1`, and the served index really carrying the stamped `<style>`.
65
- - **Switching from following to not-following no longer bounces the interface back to its own old value**: turning the switch off used to just flip the flag, so the interface fell straight back to whatever it had stored (usually nothing → DSH's default) while the slider still read 400 and disagreed with the page. Now **the values the interface was showing are written into its own two fields as the switch goes off** (the family from the conversation, the weight from the conversation — the effective values `resolveAxes` expanded), so switching off changes nothing visually and the sliders continue from there; an axis the conversation does not set (empty family, unset weight) clears the interface's own field instead, keeping DSH's default. The expected walkthrough is therefore: following at 480 → switch off → the interface is still 480 and the slider reads 480 → adjust it, or reset for 400.
66
- - **The interface weight is back, and it genuinely cannot reach the conversation**: 0.2.0 retired the whole axis because "a weight can only be written as `body, body *`, which cannot avoid the conversation". This round measured that rule on a live DSH page and found the isolation point: **exclude the whole conversation markdown subtree (and the code surfaces) from the rule with `:not()`**. Measured against computed styles on a live DSH page: with the exclusion, `body, body *` takes every interface text element to the chosen weight and **moves not one element inside the markdown subtree**; without it, the same configuration drags the conversation along. The interface weight is now an independent axis in the **Interface** section (it appears when following is off), 300–600, and its readout shows 400 like the others. The code side gets its fallback back too: once an interface weight is set, code surfaces without a weight of their own are explicitly reset to `normal` (which is what DSH computes there), so code blocks are not dragged along either.
67
- - **Every "follows the interface" note in the conversation section is gone**: the follow direction has been one-way (the interface follows the conversation) for a while, but four small hints still claimed "unset follows the interface offset / weight / ratio" and "empty follows the interface font". All four now describe what actually happens ("0 keeps DSH's sizes", "unset keeps DSH's own weights", "100% keeps DSH's own line heights"), and a regression test now refuses the strings "跟随界面" / "follows the interface" in those four entries in both dictionaries. The interface section's follow row gained a "Font and weight" hint line naming what the switch covers.
68
- - **The follow switch now covers the font and the weight**: on (the default), the interface takes the conversation's **font and weight** and its own two controls stay hidden; off, the interface uses its own font and weight and neither side can disturb the other. While following, a conversation that sets no weight still falls back to the interface's own stored value (the same fallback the family uses), so upgrading loses nothing.
69
- - **The card's previews no longer lie**: the interface weight is an `!important` page-wide rule, so it used to restyle the card's own preview boxes as well. The conversation preview now carries a `dfp-previewDialog` marker, is excluded from the interface rule and receives the conversation weight on its own; the code preview stays on the code axis (it is already in `CODE_SELECTOR`) and the interface preview stays inside the interface rule — all three previews now match the surface they simulate.
67
+ - **After switching "interface follows conversation" off, the interface weight did not fall back and reset did nothing**: two copies of the injected stylesheet lived in the page, and the served one is only rebuilt on the next page load, so any change that *removed* a rule never took effect. There is only one copy now, and both switching the follow off and resetting apply immediately.
68
+ - **Going from following to not following no longer snaps the interface back to its own old value**: the value the interface was showing is stored as its own the moment the switch goes off, so nothing moves visually and the slider stays where it was; a conversation axis that was never set keeps DSH's own value.
69
+ - **The interface weight is back, and it really cannot touch the conversation**: the whole conversation body and code subtree is excluded from the interface weight rule — every interface text element moves, not a single conversation element does.
70
+ - **The conversation section no longer claims to "follow the interface" when unset** (the following has always been one-way: the interface follows the conversation).
71
+ - The previews inside the card are no longer dragged along by the interface weight: conversation, code and interface previews each match the real surface.
72
+
73
+ ### Changed
74
+ - **The follow switch now covers font and weight together**: while it is on the interface uses the conversation's family and weight (its own two controls are hidden); with it off the interface uses its own, and neither side affects the other.
75
+
70
76
 
71
77
  ## [0.2.0] - 2026-09-16
72
78
 
73
79
  ### Added
74
- - **Three typography sets**: typography grows from two sets (body / code) to **three** — the **conversation** (markdown paragraphs, tables, headings) owns **every axis** (font, size, line height, weight); the **interface** (settings sheet, sidebars, workspace, headings and buttons) offers **a font only** and **follows the conversation by default** (switchable); **code** stays its own axis. The conversation is scoped through the markdown container (`[class*="_markdown_" i]` plus `:not()` complex selectors that exclude code surfaces) instead of hashed class names.
75
- - **A conversation size axis**: -3 to +6 px, added **on top of the official "conversation font size"** (`--dsh-content-font-size`), which stays the base. The offset is added once, at the **source**: `--dsh-content-font-size` becomes "official value + offset", and the secondary size, both deltas and every delta-derived heading size and line height follow by derivation — paragraphs, headings and heights move together, with nothing counted twice. The official slider keeps working (its changes arrive through `theme/change`).
76
- - **A conversation line-height axis**: a ratio of 100%–160% (step 5) that rebuilds the inline heights inside the markdown `font` shorthands; the **code line height** is its own additive offset (-4 to +8 px), composing with the code size offset.
77
- - **Code ligatures**: a three-mode switch (default / on / off) writing `font-variant-ligatures` — browsers enable them by default, so this is the switch for turning them off — plus a sanitized advanced `font-feature-settings` value field (`"ss01" on, "cv01" 1` shape).
78
- - **Refuse synthetic styles**: stops synthetic italic and synthetic bold for faces without them (`font-synthesis-style` / `font-synthesis-weight`); simple mode is one switch for both, advanced mode two independent switches.
79
- - **The interface follows the conversation (switch)**: the conversation is the full set while the interface has a font, so **the interface follows the conversation by default** — the conversation section has no switch and all four of its axes stay adjustable, and the interface section leads with a "Follows the conversation" switch (on by default) whose font control appears only once it is switched off (the interface's own value stays as the fallback meanwhile). Because an unset conversation font falls back to the interface's, **existing configurations look exactly as before**.
80
- - **Presets**: a **dropdown select** right under the edit mode (the card's own down chevron inside the box), with five built-in presets (默认配置 1 ~ 5) out of the box — picking one applies it, and **every change afterwards auto-saves into the selected preset** (no separate save step). "Rename / Export / Import" manage the list: renaming only changes the name and keeps the stored values, exporting copies JSON and importing merges by name (imports run the same sanitizer pipeline, capped at 20). Presets and the active preset name live in the host settings document, so they travel across browsers. 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.
81
- - **Card information architecture**: top to bottom the open card becomes an edit-mode row, a **preset bar** and three **accordion sections** — **conversation → interface → code** (the conversation leads because it owns every axis) — all collapsed by default, each title row carrying a summary with the chevron pinned to the far right, at most one open at a time; "refuse synthetic styles" is one or two switches, so it **no longer earns an accordion** and sits flat below the three sections. Expanding a section ends with that section's own **inline preview box** (rendering just that part; a floating variant was tried first and reverted after real-machine feedback). **Row format lines up with dsh-quick-toc**: every setting is a field row with 14px vertical padding (was 16px), every on/off control is the joined "one box split in two" segmented control (28px tall, was a 22px pill switch), and the control sits in the same row as its label, right-aligned (`.dfp-inline`, the same shape as quick-toc's `.dqt-pinline`) — so the two cards' rows and controls align.
82
- - **The interface size / line-height / weight axes are RETIRED (measured)**: DSH exposes **no interface size hook** — the settings sheet, both sidebars and the workspace size their text with literals in upstream CSS (22 literals in settings-plugins, 18 in workspace, 6 in sidebar, **0** uses of the interface ladder tokens), and the ladder tokens (`--dsw-font-xs-13`, `--dsw-font-s-14`, …) are consumed only by 6 conversation-area packages (tool / trajectory / attachment / chat / conversation / cordis). The only official size hook is the conversation one. Scaling the ladder from these fields could not reach the settings sheet or the sidebars, while it reached the **conversation** through `--dsh-content-font-size` — the root cause of "I change the interface and the conversation moves". Weight is worse: it can only be expressed as a `body, body *` rule, which cannot avoid reaching the conversation either (narrowing it off the markdown subtree would also strip the theme's own heading weights). So **size, line height and weight belong to the conversation alone**, and the interface keeps the one axis that measurably works: its font (a page-wide `body` rule plus the theme variable). The `sizeOffset` / `lineHeight` / `weight` fields stay in the schema (old documents and presets keep parsing) but inject nothing at all.
83
- - **Per-theme values ship out of this release**: light and dark share one value set — the top-bar per-theme switch and the light/dark editor are removed (the dark fields stay in the host schema but dormant, and rendering ignores them), to be re-evaluated in a later release.
84
-
85
- ### Fixed (real-machine walkthrough)
86
- - **Preset messages no longer linger, and no longer move the layout**: a note like "Presets copied to the clipboard." used to sit in the card forever and, as a fresh line, push the sections below it down. It now **shares one row** with "With a preset selected, every change saves into it automatically": the message fades in on the **right** of that row and **fades out by itself after 2.6 s** (a 0.24 s opacity transition, then the text is cleared). The row is **always mounted** and exactly one line tall, and the message is clamped with `nowrap + ellipsis` — so appearing and disappearing never changes the row height and nothing below ever moves. Two identical actions in a row still work (each message carries an incrementing id, and the auto-hide timer restarts), and an over-long message (the clipboard-unavailable fallback, say) stays readable on hover through its `title`.
87
- - **"I change the interface and the conversation moves"**: the interface size axis scaled the conversation's official content-size chain (`--dsh-content-font-size` / the secondary size) alongside the interface ladder, and every markdown size derives from that chain — so an interface edit really was a conversation edit. The interface axis is now explicitly barred from it (`bodyTokenNames` excludes `--dsh-content-font-*`), and the interface size axis itself is retired for the reason above.
88
- - **The conversation size offset used to move headings but not paragraphs**: the old implementation only rewrote the delta tokens and literal sizes, while paragraphs take their size from `var(--dsh-content-font-size,14px)` — and `var()` inside a custom property resolves where the property is **declared**, so a deeper scoped override cannot reach it. The new implementation declares `--dsh-content-font-size` as "official value + offset" at the source, so paragraphs, headings and line heights all follow.
89
- - **Dark overrides used to be shadowed by the light values**: the per-theme resolver merged the dark map over an **already-expanded** value set, so as soon as the follow rule changed direction the borrowed values could overwrite the dark ones. The dark overrides are now merged over the raw values and expanded afterwards, so a dark font can no longer be clobbered by a light value.
90
- - **Weight readouts read 400**: an unset weight used to print "unset" while the slider sat at 400 and the hint said 400 keeps DSH's own — three statements that disagreed. An unset weight now reads **400** (conversation and code weights both).
91
- - **The % vanished while dragging the line-height slider**: the pending readout had no unit and only gained one on release — the pending readout now keeps the `%` too (the size / code-height `px` prefixes already did).
92
- - **The conversation section showed English "variable names"**: the dialog axis label keys (`dialog.*`, `size/line/weight.dialogLabel`…) were missing from the bilingual dictionaries, so `translate` fell back to the key names — both dictionaries are complete now.
93
- - **The first control sat too far from the section title**: field rows carry 16px top padding on top of the section body — the first field of a section body is tightened to 10px.
94
- - **The chevron left the far right edge when expanded**: with the summary gone nothing pushed it there — an expanded section renders a flexible spacer instead, so the arrow stays pinned in both states.
80
+ - **Three separate sets of typography**: **conversation** (body, tables, headings — family, size, line height and weight), **interface** (family only, following the conversation by default) and **code** (its own set, with ligatures and feature switches).
81
+ - **Conversation size**: −3 to +6 px on top of DSH's own conversation size, moving paragraphs, headings and line height together.
82
+ - **Conversation line height**: 100%–160%; the code line height is a separate additive offset.
83
+ - **Code ligatures**: default / on / off, plus a sanitized `font-feature-settings` field in advanced mode.
84
+ - **Refuse synthetic italics and bolds**: one switch in simple mode, two independent ones in advanced mode.
85
+ - **Presets**: a dropdown in the card with five defaults, applied on pick; once one is selected every edit is saved into it; rename, import and export sit beside it (import merges by name, at most 20, synced across browsers).
86
+ - **A rebuilt card**: edit mode, a preset bar, then collapsible conversation / interface / code sections that start collapsed, one open at a time, each ending in its own preview; row heights and controls match dsh-quick-toc.
95
87
 
96
88
  ### Changed
97
- - **Family variables ride the official theme layer**: `ctx.theme.overrideTokens` registers `--dsw-font-family` and `--ds-font-family-code`/`--dsw-font-mono` as `{light, dark}` pairs, so the presenter owns them on body's inline style after every theme switch — the families no longer depend on stylesheet timing against the token rewrite. The `theme/change` event re-reads the tokens and repaints immediately (the official font-size preference flows through it), with the 4-second poll kept as a fallback.
98
- - **Compatibility table** lists 0.2.0 as the latest version; the README is restructured around the three typography sets and presets, and `docs/` now holds the 0.2.0 research and specification records (not shipped in the package).
89
+ - **The interface's size, line height and weight axes retired**: DSH has no interface-size interface (the settings page and sidebars use hard-coded sizes upstream), so those axes could not reach the interface and only disturbed the conversation. Size, line height and weight now belong to the conversation alone; the interface keeps the family, which does work. The old fields stay in the schema and keep parsing.
90
+ - Font families are delivered through the official theme layer, so a theme switch applies immediately.
91
+ - Light and dark share one set of values in this release; per-theme values are deferred.
92
+
93
+ ### Fixed
94
+ - **"Changing the interface changed the conversation"**: the interface size axis used to scale the conversation's size chain too; it is isolated now.
95
+ - **The conversation size offset only moved headings, not paragraphs**: the offset is added once at the source, so paragraphs, headings and line height all follow.
96
+ - **In the dark theme a dark value could be overridden by a light one.**
97
+ - **The weight readout said "not set" while the slider sat at 400**: the readout is 400 now.
98
+ - **The `%` disappeared while dragging the line-height slider**: the pending readout carries its unit too.
99
+ - **The conversation section showed English variable names as labels**: both dictionaries are complete.
100
+ - The first field of an expanded section sat too far from the heading, and the expanded chevron was not pinned to the right.
101
+
99
102
 
100
103
  ## [0.1.6] - 2026-09-15
101
104
 
102
105
  ### Fixed
103
- - **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.
106
+ - **The card's expand chevron did not match the host's**: it was a text character — thinner than the host's arrows and off-centre when rotated. It is now the host's own inline chevron, rotating around its own centre.
107
+
104
108
 
105
109
  ## [0.1.5] - 2026-09-15
106
110
 
107
111
  ### Added
108
- - **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".
112
+ - **Code weight**: weight split into independent "body weight" and "code weight" sliders (300–600 each, 400 meaning unchanged), so changing the body no longer drags the code along.
109
113
 
110
114
  ### Changed
111
- - **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".
112
- - **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.
113
- - **Compatibility table** now lists 0.1.5 as the latest version.
115
+ - The weight slider moved into the family group it belongs to, with separate labels for the two.
116
+ - An unset code weight now writes DSH's own value back explicitly, so code does not follow the body weight after upgrading.
117
+
114
118
 
115
119
  ## [0.1.4] - 2026-09-15
116
120
 
117
121
  ### Added
118
- - **Code font-size offset**: the size control is split into **two independent axes** — "Body font size offset" for text and interface sizes, and "Code font size offset" for code blocks and inline code only. The single slider used to scale both chains, so moving the body always dragged code along with it; now each moves on its own. The field is `sizeOffsetCode` (-3 ~ +6 px, 0 = leave alone), sitting next to the existing `sizeOffset`, whose meaning narrows to "body and interface text". After upgrading, an existing configuration leaves code at DSH's own sizes while body behaviour is unchanged.
122
+ - **Code size**: size split into independent "body size" and "code size" sliders (−3 to +6 px each, 0 meaning unchanged), so changing the body no longer drags the code along.
119
123
 
120
124
  ### Changed
121
- - **Card layout**: each size slider now sits under the font it resizes — body font → body size offset → code font → code size offset → weight. The two sliders are labelled separately ("Body font size offset" / "Code font size offset"), and the card description reads "Body and code fonts, a size offset for each, weight, and a West/CJK split".
122
- - **How the code chain is consumed**: DSH's markdown code tokens are `font` shorthands (`--dsw-font-markdown-code` / `-code-block` / `-code-block-small`, valued like `11px/19px <family>`), and the shorthands — not their `-font-size` parts — are what the shipped stylesheets actually read, so the code axis rewrites those too (size and line height scaled, family list kept verbatim) alongside the split parts. The embedded fallback map is corrected to rc.2's real values (it claimed 13px/20px) and gains the three shorthand names.
123
- - **Compatibility table** now lists 0.1.4 as the latest version.
125
+ - The size slider moved under the family it belongs to, with separate labels; after upgrading the body behaves as before and code returns to DSH's own value.
126
+
124
127
 
125
128
  ## [0.1.3] - 2026-09-14
126
129
 
127
130
  ### Changed
128
- - **Switch row wording and shape**: the label on the left is now **编辑模式 / "Edit mode"** (was 编辑方式), and the two options are back to a **joined segmented control** (one shared frame with a divider between the options), while the selected option keeps its grey `--dsw-specific-sidebar-nav-item-active` surface and primary-coloured text (0.1.2 had split them into two separate buttons; this reverts that shape).
129
- - **Card description**: now mentions the **West/CJK split** as well, in both languages, so it matches what the card actually offers.
130
- - **Feature list trimmed in the README**: the "commit on release", "no first-paint flash" and "bilingual" bullets are gone — they describe implementation details rather than features, and the behaviour itself is unchanged (still recorded under 0.1.0 in this file).
131
- - **Compatibility table** now lists 0.1.3 as the latest version.
131
+ - The switch row's label became "edit mode", and its two options went back to a joined segmented control with one divider between them.
132
+ - The card description now mentions the western/CJK split.
133
+ - The README dropped three implementation notes ("written on release", "no font flash on the first frame", "bilingual") — behaviour unchanged.
134
+
132
135
 
133
136
  ## [0.1.2] - 2026-09-14
134
137
 
135
138
  ### Changed
136
- - **README rewritten**: it now covers features, install, usage, compatibility and development only — design trade-offs, implementation rationale and development pitfalls are no longer part of it. Both the Chinese and the English file were rewritten together.
137
- - **Tighter compatibility declaration**: `engines.dsh` stays `>=0.1.5-rc.2`, while the compatibility table lists only the **latest release of the 0.1.5 line** (`0.1.5-rc.2`) — it carries every fix in that line and is the most stable build of the major version; older builds of the same major version stay compatible per semantic versioning, so they are no longer enumerated.
138
- - **Trimmed npm description**: the "configured from Settings…" clause and the `(DSH)` parenthetical are gone, replaced by one functional sentence. Published-version metadata cannot be edited, so this takes effect from 0.1.2.
139
- - **English card title**: `Font plus` → `Font tune`, matching the package name (the Chinese title stays 字体增强).
139
+ - The README was rewritten as features / install / usage / compatibility / development; design trade-offs and development pitfalls left it (both languages).
140
+ - The compatibility statement is narrower: only the latest patch of the 0.1.5 line is listed — older patches in the same major are compatible per semantic versioning and are no longer enumerated.
141
+ - The npm description was trimmed (published metadata cannot be edited, so this takes effect from 0.1.2), and the English card title became "Font tune", matching the package name.
142
+
140
143
 
141
144
  ## [0.1.1] - 2026-09-14
142
145
 
143
146
  ### Added
144
- - **English changelog**: `CHANGELOG.en.md` was added and ships inside the package.
145
- - **GitHub Release workflow**: pushing a `v*` tag packs the plugin and creates or updates the Release with a **version-free** `dsh-fonttune.tgz` asset (the market's `tarball:` field points at it, so the link cannot rot on the next release). The release body takes the Chinese section of this file for that version and folds the English section into `<details>`.
147
+ - An English changelog, `CHANGELOG.en.md`, shipped with the package.
148
+ - Pushing a `v*` tag now packs and creates/updates a Release automatically, including a version-less `dsh-fonttune.tgz` for the market entry to link to; the release body is the Chinese section for that version with the English one folded inside.
146
149
 
147
- ### Changed
148
- - The stale offline-check count in the README and changelog was corrected (35 → 42). The running code is identical to 0.1.0.
149
150
 
150
151
  ## [0.1.0] - 2026-09-14
151
152
 
152
- First release. The architecture is a dual-half plugin (host + client): the client half owns the settings card and the style injection, the host half registers the settings namespace and ships the saved declarations with the served index.
153
+ The first release: a two-half plugin. The browser half owns the settings card and the style injection; the host half registers the settings namespace and serves the saved declarations with the index, so the first frame already uses them.
153
154
 
154
155
  ### Added
155
- - **Plugin settings card**: a host settings namespace registered under `settings.plugin.item` with the key `dsh-fonttune`, so it appears in **Settings → Plugins → Plugin configuration**; saving and resetting go through DSH's own settings document (`settings.yaml`).
156
- - **Body / code font families**: two independent CSS font-family fallback lists, each allowed to be empty (= leave DSH's own stack alone).
157
- - **Font picker panel**: four preset groups (monospace / CJK / Latin / generic); on Chromium `queryLocalFonts()` adds an "installed on this machine" group; a name that is not in the list can be created with "use xxx"; every row renders in its own font.
158
- - **West / CJK split (simple mode)**: a switch at the top of the card — **simple mode** splits the stack into a Western and a CJK single-pick slot (one pair for body, one for code) while keeping one stack underneath; **advanced mode** is the full chip editor. The semantics are "simple mode only touches the front": the Western slot is the first non-CJK entry (picking replaces it in place, or prepends when there is none) and the CJK slot is the first CJK entry (picking replaces it in place, or **inserts right after the Western slot** when there is none, which keeps `Western, CJK, generic` CSS semantics correct). Everything beyond the two slots keeps its order and is shown as an "other fallbacks" note. **Switching modes is a pure view switch that writes nothing.** CJK classification is name-heuristic first (a broad regex plus localized names containing CJK characters); canvas measurement only confirms names that do not look CJK, so a preset family that is not installed still lands in the CJK slot. The switch row is a label on the left and a segmented toggle on the right.
159
- - **Drag to reorder**: the selected families are a chip list with drag reordering, plus earlier/later buttons for keyboard and touch.
160
- - **Live preview**: mixed Chinese/English plus a code line, rendered from the current configuration.
161
- - **Global font-size offset** (-3 ~ +6 px): rescales DSH's own size tokens (`--dsh-content-font-size`, `--dsw-font-*-font-size` / `-line-height`) with the base read live, so it **stacks** with DSH's own font-size setting instead of overriding it; the token list is discovered at runtime with a built-in fallback map.
162
- - **Global font weight** (300 ~ 600, any integer): applied through `body, body *`; 400 and "unset" both mean "leave DSH's own weight alone".
163
- - **Commit-on-release sliders**: dragging the size-offset or weight slider only updates a local value and its readout — the write happens once on `pointerup` / `touchend` (window capture-level listeners) or on blur/keyup, so dragging never recomputes the size tokens and rewrites the settings document per step. **The local value also stays on screen until the host confirms it**: clearing it immediately would show the old committed number for one frame ("bounces back, then settles"); a repeated `pointerup` is guarded by an "already awaiting confirmation" flag so the same value is never written twice.
164
- - **No first-paint font flash**: the host half listens on `webserver/index-inject` and injects the same declarations, so the first paint is already correct.
165
- - **Bilingual copy** (Chinese/English) with a Chinese fallback; `queryLocalFonts` unavailability or refusal falls back to the built-in list silently.
166
- - **Family-name sanitizing**: names are filtered through an allowlist and then quoted as a whole (letters/digits/space/`.` `,` `_` `-` only); the host schema adds a `^[^{};<>\\]*$` pattern and a length cap. A hand-crafted CSS injection cannot close a declaration or start a rule.
156
+ - **The plugin configuration card**: in Settings → Plugins → Plugin configuration, reading and writing DSH's own settings document.
157
+ - **Two font stacks**, one for the body and one for code, each a fallback list that may be empty (= leave DSH's fonts alone).
158
+ - **A font picker**: the built-in monospace / CJK / Latin / generic groups, the machine's installed families on Chromium, a "use <name>" entry for anything not listed, and every row rendered in its own family.
159
+ - **Western/CJK split (simple mode)**: the stack becomes a western slot and a CJK slot while every other entry and the order stay untouched; advanced mode is the full chip editor with drag reordering and a live preview.
160
+ - **A global size offset** (−3 to +6 px, stacking with DSH's own size slider) and **a global weight** (300–600, 400 meaning unchanged).
161
+ - **Sliders write on release**: nothing is written while dragging, and the readout holds until the host confirms.
162
+ - **Bilingual copy**; family names are sanitized so they cannot close a declaration or open a rule.
167
163
 
168
164
  ### Fixed
169
- - **The card crashed on first render** (a release blocker): `scope.subscribe` was passed to React's `useSyncExternalStore` as a bare method reference, while the host's `SettingsScopeController.subscribe` is a prototype method reading `this.store` — called detached, `this` is undefined → the slot entry crashed silently and the card never appeared in the plugin configuration page. Fixed by wrapping the subscribe in a closure that preserves `this`. The offline tests had missed it (the double's subscribe did not depend on `this` and the React double never called subscribe); both doubles now reproduce the bug.
170
- - **Three size-offset defects**: ① missing `!important` — the theme writes `--dsh-content-font-size` **inline on body**, and an inline declaration beats a normal stylesheet rule, so the body itself was not scaled while descendants were, splitting the page's font size in two; ② a **self-pollution compounding loop** — the 4-second token refresh re-read the plugin's own stylesheet declarations as its base and multiplied the ratio again on every cycle ("the fonts keep growing"); fixed by skipping the plugin's own tags, reading `--dsh-content-font-*` from the body inline style, and no longer using computed styles as a base source; ③ **double scaling through the `var()` chain** — DSH's derived tokens (delta/secondary/markdown) all derive from `var(--dsh-content-font-size)`, so scaling them too multiplied the ratio twice; fixed by skipping tokens whose base contains `var(` and letting the variable chain carry them.
171
- - **Families did not reach the conversation or sidebar**: markdown and sidebar CSS declare `font-family: var(--dsw-font-family)` themselves, truncating inheritance, so a body-level `font-family` never got through. Fixed by overriding at the **variable source** (`:root,body{--dsw-font-family:…!important}` plus `--dsw-font-mono` / `--ds-font-family-code`), with the explicit `body` / `pre,code` rules kept as a second path.
172
- - **Weight snapped to ±100**: values used to snap to 300/400/500/600, which looked like a ±100 step while dragging; the chosen integer is now written to CSS verbatim (variable fonts are fully linear, static fonts round to their nearest available weight natively) with a step of 1.
173
- - **The sanitizer was too permissive**: `Arial"; } body { background: url(evil) }` left `:` `(` `)` behind; rewritten as an allowlist.
174
- - **The host half importing named exports from the CJS shared module broke ESM loading** (`Named export not found`); switched to a default import.
175
- - **The size-token regex missed `--dsh-content-font-size`** (the name has no `-font-` segment) and its `-secondary` variants.
176
- - **Timers now go through `globalThis`** instead of assuming `window` carries them.
165
+ - **The card crashed on its first render** (a release blocker): the subscribe method was passed bare, losing `this`, so the card never appeared on the plugin configuration page.
166
+ - **Three problems in the size offset**: the body itself was not scaled, a refresh multiplied the plugin's own declarations as if they were the baseline, and derived sizes were scaled twice.
167
+ - **The family did not reach the conversation area or the sidebars** (those areas declare the font variable themselves).
168
+ - **The weight was snapped to the nearest hundred**: any integer now applies as chosen.
169
+ - **Family sanitizing was too permissive.**
170
+