dsh-fonttune 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +18 -0
- package/CHANGELOG.md +18 -0
- package/README.en.md +104 -104
- package/README.md +104 -104
- package/lib/client.js +290 -26
- package/lib/shared.cjs +46 -0
- package/package.json +4 -2
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## [0.7.2] - 2026-10-28
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **The compatibility table lists DSH 0.2.1-rc.4 and 0.2.2-alpha.2**: both were verified live — the settings card, its writes and the floating panel all behave as documented.
|
|
9
|
+
|
|
10
|
+
## [0.7.1] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **The panel and the dot each decide their own side**: the panel opens into the quadrant its own middle sits in, while the dot still snaps to the corner it remembers, so the two no longer share one rule.
|
|
14
|
+
- **Switching corners takes less travel**: the conversation's middle is the line to cross, so the corner follows the panel's own middle rather than the region's edge.
|
|
15
|
+
- **Snapping is one continuous motion**: after a release the panel and the dot travel to the target corner together, instead of the panel jumping to the other side first and then sliding.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **The panel sometimes rested on the window's bottom-left corner**: while the conversation was not measurable yet, the dot was moved onto the window corner and that position was stored; it now moves only against a measurable conversation and never stores a stand-in.
|
|
19
|
+
- **The dot sat on the sidebar in a narrow window**: a conversation squeezed to a sliver is still the region, instead of falling back to the whole window.
|
|
20
|
+
- **The shadow did not follow the unfold**: the panel's shadow now changes with the shape while it unfolds and folds.
|
|
21
|
+
- **The first frame of an unfold flickered**: the panel no longer blinks before it appears.
|
|
22
|
+
|
|
5
23
|
## [0.7.0] - 2026-10-19
|
|
6
24
|
|
|
7
25
|
### Changed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.7.2] - 2026-10-28
|
|
6
|
+
|
|
7
|
+
### 变更
|
|
8
|
+
- **兼容对照表列出 DSH 0.2.1-rc.4 与 0.2.2-alpha.2**:两条线实测通过,设置卡片、控件写入与悬浮面板均正常。
|
|
9
|
+
|
|
10
|
+
## [0.7.1] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
- **面板与圆点各自决定吸附方向**:面板按自己的中心落在对话区的哪个象限展开,圆点仍吸附到自己记住的角落,两者不再共用一套判断。
|
|
14
|
+
- **换角所需移动更少**:以对话区中心为原点,面板中心越过中线即可换到对应的角,不必先拖到区域边缘。
|
|
15
|
+
- **吸附是一个连续动作**:换角松手后,面板与圆点一同滑向目标角落,面板不再先跳到另一侧再飞过去。
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
- **悬浮面板偶尔停在窗口左下角**:对话区尚未完成布局的瞬间,圆点会被移到窗口角落并记住那个位置;现在只在对话区可测量时移动,也不再保存这种临时位置。
|
|
19
|
+
- **窗口很窄时圆点落在侧边栏上**:对话区被挤成一条窄边时仍按对话区吸附,不再退回整块窗口。
|
|
20
|
+
- **展开与收起时投影不同步**:面板的投影现在随展开、收起的形状一同变化。
|
|
21
|
+
- **展开的第一帧闪动**:面板展开时不再先闪一下再出现。
|
|
22
|
+
|
|
5
23
|
## [0.7.0] - 2026-10-19
|
|
6
24
|
|
|
7
25
|
### 变更
|
package/README.en.md
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
# dsh-fonttune
|
|
2
|
-
|
|
3
|
-
> [中文](README.md) | **English**
|
|
4
|
-
|
|
5
|
-
**Font plugin for DeepSeek Harness (DSH)**: the **conversation** (markdown prose) gets full typographic control — font, size, line height, weight, tracking, word gap, paragraph gap and first-line indent; the **interface** (settings sheet, sidebars, workspace, buttons) takes a font and a weight and **follows the conversation by default**; **code** stays its own axis with ligature and feature switches; and whole setups can be saved as **presets**. Everything lives in this plugin's native card (collapsed sections with summaries) and applies immediately: on the 0.2 line it is on the Plugins page under **Installed** (open this plugin's card), on the rc line under **Settings → Plugins → Plugin configuration**.
|
|
6
|
-
|
|
7
|
-
## The interface
|
|
8
|
-
|
|
9
|
-
The floating tuning panel (left, with its "in force right now" readout) and the text-shaping part of the settings card (right):
|
|
10
|
-
|
|
11
|
-
| Floating panel | Settings card |
|
|
12
|
-
| --- | --- |
|
|
13
|
-
|  |  |
|
|
14
|
-
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- **Three typography sets** — the conversation (markdown paragraphs, tables, headings) comes first and owns every axis; the interface (settings sheet, sidebars, workspace, headings and buttons) offers **a font and a weight** and follows the conversation by default (switchable); code stays its own axis
|
|
18
|
-
- **Light and dark, set separately** — switch "Light and dark" to "Separate" and pick "Dark" to tune the dark theme on its own. The dark set overrides only the items you change, the rest follow the light set, so nothing has to be entered twice; on "One set" both themes share a single set, exactly as before
|
|
19
|
-
- **Preset switching from the panel** — a preset row at the top of the panel applies a whole setup without opening the settings page. The panel edits whichever theme the page is showing: light on a light page, dark on a dark one
|
|
20
|
-
- **A floating tuning panel** — the dot in the conversation's corner unfolds into a card carrying every conversation adjustment (font size, line height, weight, tracking, word gap, paragraph gap and first-line indent); its background is translucent and blurs the conversation behind it, matching the outline panel's material; the dot snaps to the conversation region's corners and unfolds from its own, the panel remembers its size and position; the button on its top right takes you to this plugin's own settings page; an **"in force" section** at the bottom names the fonts the conversation, the interface and code actually render with, plus the weight slider's step count (marked as fine steps when a variable font is in play) — it folds away, and the panel scrolls when it is made short; the panel can be turned off entirely from the settings page (the dot is gone for good); the unfold and fold animations ignore the system's "reduce motion" setting
|
|
21
|
-
- **Interface / conversation / code fonts** — three independent fallback lists, Latin families first and CJK after; leaving one empty keeps DSH's own stack untouched
|
|
22
|
-
- **Font-size offsets** — the **conversation** takes -3 ~ +6 px on top of DSH's own "conversation font size" (the official value is the base), moving paragraphs, headings and line heights together — and it is applied **once, at the source** (H1/H2/H3 all derive from the same delta, so the hierarchy holds at any offset and no heading comes out smaller than expected); code is its own axis at -3 ~ +6 px. **The interface has no size axis**: verified against DSH, the settings sheet, sidebars and workspace size their text in upstream CSS, and the only official size hook is the conversation one
|
|
23
|
-
- **Font-weight offsets** — the **conversation**, the **interface** and **code** all take the same **relative offset** (unset reads 0, the readout is signed, and the range is measured for the current font): the offset is added to each element's **own** weight, so the heading hierarchy DSH ships (h1~h3 700, h4~h6 and bold 600, table head 500, body 400) survives in both directions — headings stay clearly bolder than body text at any setting, and bold text inside a code surface stays bold. The interface offset is one rule over `body` plus an element table, with the whole conversation markdown subtree excluded: measured against computed styles on a live page, it takes every interface text element to the target and moves nothing inside the markdown subtree, so **changing the interface weight never touches the conversation**. The slider **counts steps**: the readout is the step count (for example `-1 0 +1`) and **the font decides how many there are** — the card measures how many distinct weights the chosen family can render for body text, and one step is the smallest jump that is **clearly visible** rather than the smallest possible one, the range ending where body text stops swapping faces (bold text and headings stop once they are at their heaviest) — so the control may offer fewer positions than before while every notch is a visible change and the numbers stay small. What is stored is still a weight offset (older values are converted), so changing the font never breaks a setting; a family with only regular and bold offers two positions in total, and continuous control needs a family with more weights or a variable font
|
|
24
|
-
- **Line heights** — the **conversation** uses a ratio (100% ~ 160%), code an additive offset (-4 ~ +8 px); the interface has no line-height axis, for the same reason as size
|
|
25
|
-
- **Text shaping (conversation)** — four axes that move the shape of the text rather than its face, each added on top of what DSH already ships: **tracking** (-0.4 ~ +2 px, positive opens the text up, negative tightens it), **word gap** (-1 ~ +4 px, Latin word gaps only), **paragraph gap** (-8 ~ +24 px on top of DSH's own paragraph spacing, headings untouched) and **first-line indent** (0 ~ +4 em, the usual Chinese convention), plus a **strict line breaking** switch that keeps Chinese punctuation and small kana off the start of a line. Every one of them treats 0 as "leave DSH alone" and injects **not a single byte** when it is off, and all of them travel with a preset. **Two things are deliberately missing**: automatic CJK-Latin spacing and punctuation hanging — this machine's Chromium does not support the CSS properties behind them, and a switch that does nothing when pressed is worse than saying so
|
|
26
|
-
- **Code ligatures** — a three-mode switch (default / on / off; browsers enable them by default, turning them off restores `=>` and `!=` as plain characters), plus an advanced `font-feature-settings` value field
|
|
27
|
-
- **Text rendering** — three modes: follow the system (default) / sharp / smooth. The same face draws with a different stroke feel — sharp is crisp and firm, smooth is soft and even
|
|
28
|
-
- **Refuse synthetic styles** (off by default, i.e. synthesis is allowed) — turning it on stops the browser from faking italic and bold for CJK faces, which have neither, so marked text stays upright; left off, the browser synthesises by its own rules, which at least gives a visible "bold" step for a family with no bold face
|
|
29
|
-
- **The interface-follows-conversation switch** — a "Follows the conversation" switch at the top of the interface section (on by default): on, the interface takes the conversation's **font and weight** and its own two controls stay hidden; off, it uses its own font and weight, independent of the conversation. Size and line height belong to the conversation only (the interface has no hooks for them)
|
|
30
|
-
- **Presets** — a dropdown select right under the edit mode: five built-in presets out of the box, pick one to apply it; with a preset selected every change auto-saves into it, and "Rename / Export / Import" manage the list (export as JSON, import-merge a paste). When the clipboard is unavailable (a non-secure context, or no clipboard permission) the export does not print a truncated message: the whole JSON goes into a selected, read-only box, so the copy shortcut works, and the box puts itself away once copied. A settings write the document **refuses** is reported on the same row instead of leaving the control quietly showing the old value, and a write the document never took says underneath that the value came from the document
|
|
31
|
-
- **West / CJK split (simple mode)** — an edit-mode switch: simple mode gives just a Western and a CJK single-pick slot per set, advanced mode is the full stack editor; both views share one stack, so toggling never changes the order you set up
|
|
32
|
-
- **Font picker panel** — four curated groups (monospace / CJK / Latin / generic), always listed — while searching and even when enumeration fails — plus an "installed on this machine" group on Chromium that lists only what the curated groups do not, so no family is ever offered twice. A name that is not in the list can be created with "use xxx". A refused font permission is retried after a while, so granting it in the browser and reopening the panel fills the list in
|
|
33
|
-
- **Drag to reorder** — picked fonts are draggable chips with earlier/later buttons too (keyboard and touch friendly)
|
|
34
|
-
- **Per-section previews** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part with the current configuration
|
|
35
|
-
|
|
36
|
-
## Compatibility
|
|
37
|
-
|
|
38
|
-
**One package serves three host lines** — 0.1.7-alpha.1 / alpha.2 (the floor), 0.1.7-rc.1 / rc.2 and 0.2.0-rc.1 / rc.2 — and the install is the same on any of them: all three were verified for "the settings card opens, the stylesheet really reaches the page, and a change made in the card is written through".
|
|
39
|
-
|
|
40
|
-
| Plugin version | Supported DSH versions |
|
|
41
|
-
| --- | --- |
|
|
42
|
-
| **0.7.x** (latest, 0.7.
|
|
43
|
-
| 0.6.x (0.6.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2 |
|
|
44
|
-
| 0.5.x (0.5.0) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2 |
|
|
45
|
-
| 0.4.x (0.4.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2 |
|
|
46
|
-
| 0.3.x (0.3.6) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2 |
|
|
47
|
-
| 0.2.x (0.2.8) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2 |
|
|
48
|
-
|
|
49
|
-
Each major version lists only its newest patch: defects a feature introduced were fixed in the patches after it, so within a major version the newest patch is the one to use; older patches keep working, because the plugin does not break its existing interfaces.
|
|
50
|
-
|
|
51
|
-
## Install
|
|
52
|
-
|
|
53
|
-
Install through the DSH CLI:
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
dsh plugin --profile web add dsh-fonttune
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
From GitHub:
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
dsh plugin --profile web add github:LyaxZ/dsh-fonttune
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Or from a local directory:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
dsh plugin --profile web add <plugin directory>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Restart DSH once afterwards and open the Web UI; changing settings needs no restart after that.
|
|
72
|
-
|
|
73
|
-
## Usage
|
|
74
|
-
|
|
75
|
-
- **Open the settings** — Settings → Plugins → Plugin configuration → expand the "Font tune" card
|
|
76
|
-
- **Section navigation** — top to bottom: edit mode, presets, then the three collapsed sections **conversation → interface → code** (the conversation leads because it owns every axis), with "refuse synthetic styles" flat below them; each title row carries a summary of the current values with the chevron pinned to the far right, and at most one section is open at a time
|
|
77
|
-
- **Pick fonts** — in simple mode click the Western/CJK slots; in advanced mode use "Add font" and the chip editor to maintain the full fallback list
|
|
78
|
-
- **Tune size / weight / line height** — each slider sits inside its own section; drag and release to apply (the readout keeps its unit while dragging). Changed fields are marked and can be reset individually, with "Reset all" at the bottom of the card
|
|
79
|
-
- **The conversation leads** — the conversation section sets font, size, line height and weight; the interface section shows a single "Follows the conversation" row by default, and its own font and weight controls appear once that is switched off
|
|
80
|
-
- **Switching following off never jumps** — the moment the switch goes off, the interface writes the font and weight offset it was **already showing** into its own fields (that is what it was following), so nothing changes on screen and the sliders continue from there; an axis the conversation does not set clears the interface's own field instead, keeping DSH's default. "Reset" on that field goes back to DSH's own weight (all three weight offsets return to 0)
|
|
81
|
-
- **The interface weight cannot reach the conversation** — it is the one axis that has to cover the whole page (DSH pins its small labels' weights in upstream class rules, which nothing narrower can reach), so it is implemented as a rule with the **whole conversation markdown subtree excluded**: paragraphs, headings and bold text in the conversation are untouched
|
|
82
|
-
- **Weight readouts** — all three weight axes read 0 when unset (the slider sits in the middle); no "unset" wording appears anywhere, and an absolute weight stored by an older version (300 ~ 600) is converted to its equivalent offset on read (400 → 0, 450 → +50), so nothing has to be set again
|
|
83
|
-
- **The weight slider counts steps** — the card measures how many distinct weights the current family can render for body text, one step being the smallest jump that is clearly visible rather than the smallest possible one, and the range ending where body text stops swapping faces (bold text and headings stop at their heaviest; the readout is the step count, e.g. `-1 … +2`), so the control may offer fewer positions than before while every notch is a visible change and the numbers stay small; changing the family re-measures it
|
|
84
|
-
- **Presets** — the dropdown select below the edit mode (默认配置 1 ~ 5, with the card's own chevron inside the box); picking one switches and auto-saves the edits that follow. "Rename" renames the current preset, "Export" copies JSON and "Import" merges a paste. Operation results ("copied to the clipboard") appear on the **right** of the row below and fade out by themselves after about 2.6 s, without moving any layout
|
|
85
|
-
- **Preview** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part
|
|
86
|
-
- Settings are stored in DSH's settings document (the profile's `cordis.patch.yml` from 0.1.7 on, `settings.yaml` before that), so they travel with the profile
|
|
87
|
-
|
|
88
|
-
## Development
|
|
89
|
-
|
|
90
|
-
- `src/shared.cjs` — pure-function core: family sanitizing and parsing, configuration normalization, generated stylesheet
|
|
91
|
-
- `src/index.mjs` — host half: registers the `dsh-fonttune` settings namespace (schemastery schema with length and range validation) and ships a **stamped** `<style>` into the served index through `webserver/index-inject`, so the first paint already uses the saved values; the browser half then **adopts and keeps that element** (there is only ever one copy of the stylesheet, so rules can be added *and* removed)
|
|
92
|
-
- `src/client.js` — browser half: the settings card, the font picker panel, font enumeration and style injection
|
|
93
|
-
- `build.mjs` — zero-dependency build: inlines the shared core, wraps the bundle in the `window.__ModuleLoader__.load({id, factory})` shell, copies the host half, and enforces that the client bundle only requires shell-held modules
|
|
94
|
-
- `test/run.mjs` — offline checks (hand-built DOM / cordis / settings-surface doubles, the real schemastery schema, CSS generation and injection, sanitizer adversarial cases, dictionary key parity, the picker's grouping rules, the token-refresh power gate); `test/render-card.mjs` renders the card component through a mini React hooks runtime (including forced-open and forced-state renders, covering the 0.1.0 release-blocking crash class), fires **every control's callback** (an unwired handler only fails when someone actually clicks it) and **renders a second time** to cover a hook order that changes with state; `test/artifacts.mjs` proves `lib/` matches the current `src/` byte for byte (`npm run verify` runs all three). The devDependency is the **newest** schemastery (`^3.18.4`) — the alpha's dialect, whose `.volatile()` makes a resolved field a **reference** rather than a value — so these suites read values through the `plainConfigValue` the host half exports, the same path the plugin takes; at runtime the **host's own copy** is used (3.18.2 on rc.2 / rc.3, with no such modifier, a shape the live probe covers)
|
|
95
|
-
- Checks that need a real page run through `npm run verify:browser -- "<url-with-token>"` (start a managed instance first: `dsh web --port 0 --no-open` prints the URL). The default group is read-only (`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`); `--writers` adds the scripts that **write the settings namespace** (`slider-walk` / `split-walk` / `split-verify` / `weight-verify`, each snapshots the user layer first and restores it before exiting), and `--only a,b` picks a subset. Every script's exit code is its verdict; `test/set-user-layer.mjs <url> '<json>'` puts the layer back after an interrupted run
|
|
96
|
-
- `test/host-line-probe.mjs` is the **host-line-independent** check: it asserts only what must hold on every supported line — the page boots without errors, the plugin owns exactly ONE stylesheet element, the settings really reach the document (`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` also assert the computed values when set), the configuration card is reachable from the settings sheet and renders (`.dfp-card`, whichever seat that line declares), and changing a control there really writes through. Run it first after switching host lines (rc ↔ alpha); set `DFP_PROBE=1` when debugging "the card's writes do not stick on the alpha" — the plugin then logs one `[dfp-probe]` line (which namespaces are served, which one was claimed, and what state the seat is in)
|
|
97
|
-
- `test/weight-verify.mjs` measures all three weight axes (interface / conversation / code) in a live page, including that there is only one stylesheet element and that switching following off drops the followed offset without a reload, and it asserts the **conversation ladder level by level** (at +180 and at −100, h2 > strong > body text, each group equal to its own base plus the offset); `test/split-verify.mjs` measures the conversation and code size axes plus the **retired interface size axis moving nothing**, and uses an h1~h4 probe to prove **every heading level takes the offset exactly once** (±3, with `h1 > h2 > h3 ≥ h4`); `test/slider-walk.mjs` and `test/split-walk.mjs` drive the real card (a drag writes nothing, a release does; the simple-mode slots and the whole stack round-trip); the market-entry tooling is `test/market-pr.mjs` (status / update / refresh / reopen / open / about) and the diagnostic `test/market-inspect.mjs` (PR state, comments, CI and branch diff)
|
|
98
|
-
- `docs/` — the 0.2.0 [research](docs/research-0.2.0.md) and [specification](docs/spec-0.2.0.md) records (written during design, with the trade-offs of that moment; where they differ from what shipped, README/CHANGELOG win — see the note at the top of each). **Not part of the npm `files` list, so they are not published**
|
|
99
|
-
- The client bundle may only require modules from the shell's static table (`react`, `react/jsx-runtime`, `react-dom`, `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-*`…); `dsh.client.inject` declares load order, not require permission
|
|
100
|
-
- Build before refreshing: `node build.mjs` (or `npm run watch`) turns `src/` into `lib/`, and the browser loads `lib/` — editing `src/client.js` without building leaves the page on the previous bundle. `npm test` only checks behaviour; `node test/artifacts.mjs` (part of `npm run verify`) proves the committed `lib/` is byte-for-byte what the current `src/` produces. Editing the host half (`src/index.mjs`) or `cordis.patch.yml` needs a DSH restart
|
|
101
|
-
|
|
102
|
-
## License
|
|
103
|
-
|
|
104
|
-
MIT © 2026 LyaxZ
|
|
1
|
+
# dsh-fonttune
|
|
2
|
+
|
|
3
|
+
> [中文](README.md) | **English**
|
|
4
|
+
|
|
5
|
+
**Font plugin for DeepSeek Harness (DSH)**: the **conversation** (markdown prose) gets full typographic control — font, size, line height, weight, tracking, word gap, paragraph gap and first-line indent; the **interface** (settings sheet, sidebars, workspace, buttons) takes a font and a weight and **follows the conversation by default**; **code** stays its own axis with ligature and feature switches; and whole setups can be saved as **presets**. Everything lives in this plugin's native card (collapsed sections with summaries) and applies immediately: on the 0.2 line it is on the Plugins page under **Installed** (open this plugin's card), on the rc line under **Settings → Plugins → Plugin configuration**.
|
|
6
|
+
|
|
7
|
+
## The interface
|
|
8
|
+
|
|
9
|
+
The floating tuning panel (left, with its "in force right now" readout) and the text-shaping part of the settings card (right):
|
|
10
|
+
|
|
11
|
+
| Floating panel | Settings card |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
|  |  |
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Three typography sets** — the conversation (markdown paragraphs, tables, headings) comes first and owns every axis; the interface (settings sheet, sidebars, workspace, headings and buttons) offers **a font and a weight** and follows the conversation by default (switchable); code stays its own axis
|
|
18
|
+
- **Light and dark, set separately** — switch "Light and dark" to "Separate" and pick "Dark" to tune the dark theme on its own. The dark set overrides only the items you change, the rest follow the light set, so nothing has to be entered twice; on "One set" both themes share a single set, exactly as before
|
|
19
|
+
- **Preset switching from the panel** — a preset row at the top of the panel applies a whole setup without opening the settings page. The panel edits whichever theme the page is showing: light on a light page, dark on a dark one
|
|
20
|
+
- **A floating tuning panel** — the dot in the conversation's corner unfolds into a card carrying every conversation adjustment (font size, line height, weight, tracking, word gap, paragraph gap and first-line indent); its background is translucent and blurs the conversation behind it, matching the outline panel's material; the dot snaps to the conversation region's corners and unfolds from its own, the panel remembers its size and position; the button on its top right takes you to this plugin's own settings page; an **"in force" section** at the bottom names the fonts the conversation, the interface and code actually render with, plus the weight slider's step count (marked as fine steps when a variable font is in play) — it folds away, and the panel scrolls when it is made short; the panel can be turned off entirely from the settings page (the dot is gone for good); the unfold and fold animations ignore the system's "reduce motion" setting
|
|
21
|
+
- **Interface / conversation / code fonts** — three independent fallback lists, Latin families first and CJK after; leaving one empty keeps DSH's own stack untouched
|
|
22
|
+
- **Font-size offsets** — the **conversation** takes -3 ~ +6 px on top of DSH's own "conversation font size" (the official value is the base), moving paragraphs, headings and line heights together — and it is applied **once, at the source** (H1/H2/H3 all derive from the same delta, so the hierarchy holds at any offset and no heading comes out smaller than expected); code is its own axis at -3 ~ +6 px. **The interface has no size axis**: verified against DSH, the settings sheet, sidebars and workspace size their text in upstream CSS, and the only official size hook is the conversation one
|
|
23
|
+
- **Font-weight offsets** — the **conversation**, the **interface** and **code** all take the same **relative offset** (unset reads 0, the readout is signed, and the range is measured for the current font): the offset is added to each element's **own** weight, so the heading hierarchy DSH ships (h1~h3 700, h4~h6 and bold 600, table head 500, body 400) survives in both directions — headings stay clearly bolder than body text at any setting, and bold text inside a code surface stays bold. The interface offset is one rule over `body` plus an element table, with the whole conversation markdown subtree excluded: measured against computed styles on a live page, it takes every interface text element to the target and moves nothing inside the markdown subtree, so **changing the interface weight never touches the conversation**. The slider **counts steps**: the readout is the step count (for example `-1 0 +1`) and **the font decides how many there are** — the card measures how many distinct weights the chosen family can render for body text, and one step is the smallest jump that is **clearly visible** rather than the smallest possible one, the range ending where body text stops swapping faces (bold text and headings stop once they are at their heaviest) — so the control may offer fewer positions than before while every notch is a visible change and the numbers stay small. What is stored is still a weight offset (older values are converted), so changing the font never breaks a setting; a family with only regular and bold offers two positions in total, and continuous control needs a family with more weights or a variable font
|
|
24
|
+
- **Line heights** — the **conversation** uses a ratio (100% ~ 160%), code an additive offset (-4 ~ +8 px); the interface has no line-height axis, for the same reason as size
|
|
25
|
+
- **Text shaping (conversation)** — four axes that move the shape of the text rather than its face, each added on top of what DSH already ships: **tracking** (-0.4 ~ +2 px, positive opens the text up, negative tightens it), **word gap** (-1 ~ +4 px, Latin word gaps only), **paragraph gap** (-8 ~ +24 px on top of DSH's own paragraph spacing, headings untouched) and **first-line indent** (0 ~ +4 em, the usual Chinese convention), plus a **strict line breaking** switch that keeps Chinese punctuation and small kana off the start of a line. Every one of them treats 0 as "leave DSH alone" and injects **not a single byte** when it is off, and all of them travel with a preset. **Two things are deliberately missing**: automatic CJK-Latin spacing and punctuation hanging — this machine's Chromium does not support the CSS properties behind them, and a switch that does nothing when pressed is worse than saying so
|
|
26
|
+
- **Code ligatures** — a three-mode switch (default / on / off; browsers enable them by default, turning them off restores `=>` and `!=` as plain characters), plus an advanced `font-feature-settings` value field
|
|
27
|
+
- **Text rendering** — three modes: follow the system (default) / sharp / smooth. The same face draws with a different stroke feel — sharp is crisp and firm, smooth is soft and even
|
|
28
|
+
- **Refuse synthetic styles** (off by default, i.e. synthesis is allowed) — turning it on stops the browser from faking italic and bold for CJK faces, which have neither, so marked text stays upright; left off, the browser synthesises by its own rules, which at least gives a visible "bold" step for a family with no bold face
|
|
29
|
+
- **The interface-follows-conversation switch** — a "Follows the conversation" switch at the top of the interface section (on by default): on, the interface takes the conversation's **font and weight** and its own two controls stay hidden; off, it uses its own font and weight, independent of the conversation. Size and line height belong to the conversation only (the interface has no hooks for them)
|
|
30
|
+
- **Presets** — a dropdown select right under the edit mode: five built-in presets out of the box, pick one to apply it; with a preset selected every change auto-saves into it, and "Rename / Export / Import" manage the list (export as JSON, import-merge a paste). When the clipboard is unavailable (a non-secure context, or no clipboard permission) the export does not print a truncated message: the whole JSON goes into a selected, read-only box, so the copy shortcut works, and the box puts itself away once copied. A settings write the document **refuses** is reported on the same row instead of leaving the control quietly showing the old value, and a write the document never took says underneath that the value came from the document
|
|
31
|
+
- **West / CJK split (simple mode)** — an edit-mode switch: simple mode gives just a Western and a CJK single-pick slot per set, advanced mode is the full stack editor; both views share one stack, so toggling never changes the order you set up
|
|
32
|
+
- **Font picker panel** — four curated groups (monospace / CJK / Latin / generic), always listed — while searching and even when enumeration fails — plus an "installed on this machine" group on Chromium that lists only what the curated groups do not, so no family is ever offered twice. A name that is not in the list can be created with "use xxx". A refused font permission is retried after a while, so granting it in the browser and reopening the panel fills the list in
|
|
33
|
+
- **Drag to reorder** — picked fonts are draggable chips with earlier/later buttons too (keyboard and touch friendly)
|
|
34
|
+
- **Per-section previews** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part with the current configuration
|
|
35
|
+
|
|
36
|
+
## Compatibility
|
|
37
|
+
|
|
38
|
+
**One package serves three host lines** — 0.1.7-alpha.1 / alpha.2 (the floor), 0.1.7-rc.1 / rc.2 and 0.2.0-rc.1 / rc.2 — and the install is the same on any of them: all three were verified for "the settings card opens, the stylesheet really reaches the page, and a change made in the card is written through".
|
|
39
|
+
|
|
40
|
+
| Plugin version | Supported DSH versions |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| **0.7.x** (latest, 0.7.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2, 0.2.1-rc.4, 0.2.2-alpha.2 |
|
|
43
|
+
| 0.6.x (0.6.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2 |
|
|
44
|
+
| 0.5.x (0.5.0) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2 |
|
|
45
|
+
| 0.4.x (0.4.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2 |
|
|
46
|
+
| 0.3.x (0.3.6) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2 |
|
|
47
|
+
| 0.2.x (0.2.8) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2 |
|
|
48
|
+
|
|
49
|
+
Each major version lists only its newest patch: defects a feature introduced were fixed in the patches after it, so within a major version the newest patch is the one to use; older patches keep working, because the plugin does not break its existing interfaces.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
Install through the DSH CLI:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
dsh plugin --profile web add dsh-fonttune
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
From GitHub:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
dsh plugin --profile web add github:LyaxZ/dsh-fonttune
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or from a local directory:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
dsh plugin --profile web add <plugin directory>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Restart DSH once afterwards and open the Web UI; changing settings needs no restart after that.
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
- **Open the settings** — Settings → Plugins → Plugin configuration → expand the "Font tune" card
|
|
76
|
+
- **Section navigation** — top to bottom: edit mode, presets, then the three collapsed sections **conversation → interface → code** (the conversation leads because it owns every axis), with "refuse synthetic styles" flat below them; each title row carries a summary of the current values with the chevron pinned to the far right, and at most one section is open at a time
|
|
77
|
+
- **Pick fonts** — in simple mode click the Western/CJK slots; in advanced mode use "Add font" and the chip editor to maintain the full fallback list
|
|
78
|
+
- **Tune size / weight / line height** — each slider sits inside its own section; drag and release to apply (the readout keeps its unit while dragging). Changed fields are marked and can be reset individually, with "Reset all" at the bottom of the card
|
|
79
|
+
- **The conversation leads** — the conversation section sets font, size, line height and weight; the interface section shows a single "Follows the conversation" row by default, and its own font and weight controls appear once that is switched off
|
|
80
|
+
- **Switching following off never jumps** — the moment the switch goes off, the interface writes the font and weight offset it was **already showing** into its own fields (that is what it was following), so nothing changes on screen and the sliders continue from there; an axis the conversation does not set clears the interface's own field instead, keeping DSH's default. "Reset" on that field goes back to DSH's own weight (all three weight offsets return to 0)
|
|
81
|
+
- **The interface weight cannot reach the conversation** — it is the one axis that has to cover the whole page (DSH pins its small labels' weights in upstream class rules, which nothing narrower can reach), so it is implemented as a rule with the **whole conversation markdown subtree excluded**: paragraphs, headings and bold text in the conversation are untouched
|
|
82
|
+
- **Weight readouts** — all three weight axes read 0 when unset (the slider sits in the middle); no "unset" wording appears anywhere, and an absolute weight stored by an older version (300 ~ 600) is converted to its equivalent offset on read (400 → 0, 450 → +50), so nothing has to be set again
|
|
83
|
+
- **The weight slider counts steps** — the card measures how many distinct weights the current family can render for body text, one step being the smallest jump that is clearly visible rather than the smallest possible one, and the range ending where body text stops swapping faces (bold text and headings stop at their heaviest; the readout is the step count, e.g. `-1 … +2`), so the control may offer fewer positions than before while every notch is a visible change and the numbers stay small; changing the family re-measures it
|
|
84
|
+
- **Presets** — the dropdown select below the edit mode (默认配置 1 ~ 5, with the card's own chevron inside the box); picking one switches and auto-saves the edits that follow. "Rename" renames the current preset, "Export" copies JSON and "Import" merges a paste. Operation results ("copied to the clipboard") appear on the **right** of the row below and fade out by themselves after about 2.6 s, without moving any layout
|
|
85
|
+
- **Preview** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part
|
|
86
|
+
- Settings are stored in DSH's settings document (the profile's `cordis.patch.yml` from 0.1.7 on, `settings.yaml` before that), so they travel with the profile
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
- `src/shared.cjs` — pure-function core: family sanitizing and parsing, configuration normalization, generated stylesheet
|
|
91
|
+
- `src/index.mjs` — host half: registers the `dsh-fonttune` settings namespace (schemastery schema with length and range validation) and ships a **stamped** `<style>` into the served index through `webserver/index-inject`, so the first paint already uses the saved values; the browser half then **adopts and keeps that element** (there is only ever one copy of the stylesheet, so rules can be added *and* removed)
|
|
92
|
+
- `src/client.js` — browser half: the settings card, the font picker panel, font enumeration and style injection
|
|
93
|
+
- `build.mjs` — zero-dependency build: inlines the shared core, wraps the bundle in the `window.__ModuleLoader__.load({id, factory})` shell, copies the host half, and enforces that the client bundle only requires shell-held modules
|
|
94
|
+
- `test/run.mjs` — offline checks (hand-built DOM / cordis / settings-surface doubles, the real schemastery schema, CSS generation and injection, sanitizer adversarial cases, dictionary key parity, the picker's grouping rules, the token-refresh power gate); `test/render-card.mjs` renders the card component through a mini React hooks runtime (including forced-open and forced-state renders, covering the 0.1.0 release-blocking crash class), fires **every control's callback** (an unwired handler only fails when someone actually clicks it) and **renders a second time** to cover a hook order that changes with state; `test/artifacts.mjs` proves `lib/` matches the current `src/` byte for byte (`npm run verify` runs all three). The devDependency is the **newest** schemastery (`^3.18.4`) — the alpha's dialect, whose `.volatile()` makes a resolved field a **reference** rather than a value — so these suites read values through the `plainConfigValue` the host half exports, the same path the plugin takes; at runtime the **host's own copy** is used (3.18.2 on rc.2 / rc.3, with no such modifier, a shape the live probe covers)
|
|
95
|
+
- Checks that need a real page run through `npm run verify:browser -- "<url-with-token>"` (start a managed instance first: `dsh web --port 0 --no-open` prints the URL). The default group is read-only (`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`); `--writers` adds the scripts that **write the settings namespace** (`slider-walk` / `split-walk` / `split-verify` / `weight-verify`, each snapshots the user layer first and restores it before exiting), and `--only a,b` picks a subset. Every script's exit code is its verdict; `test/set-user-layer.mjs <url> '<json>'` puts the layer back after an interrupted run
|
|
96
|
+
- `test/host-line-probe.mjs` is the **host-line-independent** check: it asserts only what must hold on every supported line — the page boots without errors, the plugin owns exactly ONE stylesheet element, the settings really reach the document (`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` also assert the computed values when set), the configuration card is reachable from the settings sheet and renders (`.dfp-card`, whichever seat that line declares), and changing a control there really writes through. Run it first after switching host lines (rc ↔ alpha); set `DFP_PROBE=1` when debugging "the card's writes do not stick on the alpha" — the plugin then logs one `[dfp-probe]` line (which namespaces are served, which one was claimed, and what state the seat is in)
|
|
97
|
+
- `test/weight-verify.mjs` measures all three weight axes (interface / conversation / code) in a live page, including that there is only one stylesheet element and that switching following off drops the followed offset without a reload, and it asserts the **conversation ladder level by level** (at +180 and at −100, h2 > strong > body text, each group equal to its own base plus the offset); `test/split-verify.mjs` measures the conversation and code size axes plus the **retired interface size axis moving nothing**, and uses an h1~h4 probe to prove **every heading level takes the offset exactly once** (±3, with `h1 > h2 > h3 ≥ h4`); `test/slider-walk.mjs` and `test/split-walk.mjs` drive the real card (a drag writes nothing, a release does; the simple-mode slots and the whole stack round-trip); the market-entry tooling is `test/market-pr.mjs` (status / update / refresh / reopen / open / about) and the diagnostic `test/market-inspect.mjs` (PR state, comments, CI and branch diff)
|
|
98
|
+
- `docs/` — the 0.2.0 [research](docs/research-0.2.0.md) and [specification](docs/spec-0.2.0.md) records (written during design, with the trade-offs of that moment; where they differ from what shipped, README/CHANGELOG win — see the note at the top of each). **Not part of the npm `files` list, so they are not published**
|
|
99
|
+
- The client bundle may only require modules from the shell's static table (`react`, `react/jsx-runtime`, `react-dom`, `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-*`…); `dsh.client.inject` declares load order, not require permission
|
|
100
|
+
- Build before refreshing: `node build.mjs` (or `npm run watch`) turns `src/` into `lib/`, and the browser loads `lib/` — editing `src/client.js` without building leaves the page on the previous bundle. `npm test` only checks behaviour; `node test/artifacts.mjs` (part of `npm run verify`) proves the committed `lib/` is byte-for-byte what the current `src/` produces. Editing the host half (`src/index.mjs`) or `cordis.patch.yml` needs a DSH restart
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT © 2026 LyaxZ
|
package/README.md
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
# dsh-fonttune
|
|
2
|
-
|
|
3
|
-
> [English](README.en.md) | **中文**
|
|
4
|
-
|
|
5
|
-
**DeepSeek Harness(DSH)字体插件**:**对话**(会话 Markdown 正文)拥有完整的排印控制——字体族、字号、行高、字重、字距、词距、段距与首行缩进;**界面**(设置页、侧栏、工作区、按钮)提供字体与字重并**默认跟随对话**;**代码**独立成轴,另有连字与特性开关;整套配置可存为**预设方案**。配置入口是**原生卡片**(分区收起 + 摘要),改完即时生效:0.2 线在「插件」页的**已安装**栏目里(点开本插件的卡片),rc 线在 设置 → 插件 → 插件配置。
|
|
6
|
-
|
|
7
|
-
## 界面
|
|
8
|
-
|
|
9
|
-
悬浮调参面板(左,含底部「当前生效」读数)与设置卡片里的文字排印部分(右):
|
|
10
|
-
|
|
11
|
-
| 悬浮面板 | 设置卡片 |
|
|
12
|
-
| --- | --- |
|
|
13
|
-
|  |  |
|
|
14
|
-
|
|
15
|
-
## 功能
|
|
16
|
-
|
|
17
|
-
- **三套排印**:对话(会话 Markdown 正文、表格与标题)排在第一位、拥有全部轴;界面(设置页、侧栏、工作区与标题按钮)提供**字体与字重**、默认跟随对话(可关);代码独立成轴
|
|
18
|
-
- **亮暗分套** —— 亮色与暗色各一套排印:卡片里把「亮暗分套」切到「亮暗分开」,再选「暗色」,编辑的就是深色主题那一套。暗色只覆盖你调过的项,其余跟随亮色,不必把值重设一遍;「一起设置」时两套共用同一份,行为与之前一致
|
|
19
|
-
- **面板预设切换** —— 面板顶部一行可选预设方案,选中即整套应用,不用打开设置页。面板编辑的是**当前主题**那一套:页面是浅色就只改浅色,深色就只改深色
|
|
20
|
-
- **悬浮调参面板** —— 对话角落的小圆点展开为卡片,对话的调整项全部在这里(字号、行高、字重、字距、词距、段距、首行缩进);面板背景半透明并模糊它后面的对话,与「对话大纲」的侧边面板同一观感;小圆点吸附在对话区域的四角、展开方向随所在角落,面板大小与位置自动记住;面板右上角有直达本插件设置页的按钮,底部**「当前生效」一栏**写出本页实际在用的对话、界面与代码三处字体和字重滑块的档数(连续字重生效时标注「细档」),这一栏可收起,面板被压矮时可滚动查看;设置页里可把面板整体关闭(小圆点彻底隐藏);展开与收起动画不受系统「减少动效」设置影响
|
|
21
|
-
- **界面 / 对话 / 代码字体** —— 各自独立的字体回退列表,拉丁字体在前、中文字体在后;留空表示完全沿用 DSH 的字体栈
|
|
22
|
-
- **字号偏移** —— **对话**字号 -3 ~ +6 px,加在 DSH 自己的「对话字号」之上(官方数值是基准),段落、标题与行高一起跟随,且**只在源头加一次**(H1/H2/H3 的尺寸都由同一条 delta 派生,任意偏移下层级都不变、也不会出现某个标题偏小);代码字号独立成轴,-3 ~ +6 px。**界面没有字号轴**:经实测 DSH 的设置页、侧栏与工作区把字号写死在上游 CSS 里,唯一官方字号接口就是「对话字号」
|
|
23
|
-
- **字重偏移** —— **对话、界面与代码都是同一种相对偏移**(未设置即 0、读数按 `+n` / `-n` 显示,范围按当前字体实测):偏移叠加在每个元素**自身的**字重上,标题仍按 DSH 的层级(h1~h3 700、h4~h6 与加粗 600、表头 500、正文 400)逐级保持"标题明显粗于正文",调重调轻都不会把层级抹平,代码块里的粗体也不会被压平。界面偏移是一条把对话 Markdown 整棵子树排除在外、只铺 `body` 与自带字重的元素表的规则:真机实测(活页面计算样式)它把界面的文本元素改到目标值、对话 markdown 里的元素一个都没动,所以**改界面字重不会碰到对话**。滑块**按档计数**:读数就是档数(例如 `-1 0 +1`),**档数由字体决定**——卡片会在页面里量出所选字体在正文处实际能渲染出几种粗细,一档取**变化足够明显**的那一档(不是最细的那一档),范围到正文再也换不到字形为止(加粗与标题到顶后停住),所以档数可能比之前少,但每挪一格都看得出来,数字也不会出现"几百几百";换字体会重新测量。文档里存的仍是字重偏移(旧值自动换算),换字体不会把设置改坏;字体只有常规与粗体两档时就只有 2 个档位,要连续可选用字重档位多的字体或可变字体
|
|
24
|
-
- **行高** —— **对话**行高用倍率(100% ~ 160%);代码行高用加法偏移(-4 ~ +8 px)。界面没有行高轴(同字号的原因)
|
|
25
|
-
- **文字排印(对话)** —— 四条叠加在 DSH 原有排版之上的轴,改的是文字的形状而不是字体本身:**字距**(-0.4 ~ +2 px,正值更疏、负值更紧)、**词距**(-1 ~ +4 px,只影响西文词间空隙)、**段距**(-8 ~ +24 px,加在 DSH 自己的段落间距上,标题间距不受影响)、**首行缩进**(0 ~ +4 em,中文排版的常规做法),以及**严格避头尾**开关(换行时不让中文标点、小假名等出现在行首)。四条轴都以 0 为「保持 DSH 原样」,关闭时**一个字节都不注入**;四项都进预设方案,可随方案带走。**未做的两项**:中西文自动间距与标点悬挂——本机浏览器(Chromium)尚不支持对应的 CSS 属性,与其放一个按下去没反应的开关,不如说明白
|
|
26
|
-
- **代码连字** —— 三档开关(默认 / 开启 / 关闭,浏览器默认即开启,关闭可还原 `=>`、`!=` 这类连字),高级模式另有 `font-feature-settings` 特性值输入
|
|
27
|
-
- **文字渲染质感** —— 三档:跟随系统(默认)/ 锐利 / 圆润。同一副字体在不同渲染方式下笔画质感不同,锐利更清晰硬朗、圆润更柔和均匀
|
|
28
|
-
- **拒绝合成样式**(默认关闭 = 允许合成) —— 打开后禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰);关着(默认)时由浏览器按自己的规则合成,字体缺粗体字形时至少还能看出"粗体"这一档
|
|
29
|
-
- **界面跟随对话开关** —— 界面分区顶部的「跟随对话设置」开关(默认开启):开启时界面沿用对话的**字体与字重**,界面自己的两个控件隐藏;关闭后界面用自己的字体与字重,与对话互不影响。字号与行高只属于对话(界面没有这两个接口)
|
|
30
|
-
- **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON 分享、导入合并)。剪贴板不可用时(非安全上下文、或没有剪贴板权限)导出不会只给一段被截断的提示,而是把完整 JSON 放进一个已选中、只读的文本框,按复制快捷键即可,复制后自动收起;任何**写设置失败**(宿主拒绝、只读部署等)都会在当前行显示出来,而不是让控件悄悄弹回旧值;写入被接受、文档里的值却没有跟着变时,滑块也会在下方说明已回到文档里的值
|
|
31
|
-
- **西文 / 中文分栏(简单模式)** —— 编辑模式开关:简单模式每套字体只给「西文」「中文」两个单选格,高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不改动已排好的顺序
|
|
32
|
-
- **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;四组预设**任何情况下都在**(包括搜索时,也包括枚举本机字体失败时),本机分组只列出预设分组没有的字体,所以同一个字体不会出现两次;搜索不到的名字可以直接「使用 xxx」新建。字体枚举被浏览器拒绝时会在一段时间后自动重试(用户在浏览器里授予字体权限后,重新打开面板即可看到本机字体)
|
|
33
|
-
- **拖拽排序** —— 已选字体是 chip 列表,可拖动调整回退顺序,同时保留前移 / 后移按钮(键盘与触屏可用)
|
|
34
|
-
- **分区预览** —— 对话 / 界面 / 代码分区展开后在分区末尾各有一个预览框,只预览该分区的部分
|
|
35
|
-
|
|
36
|
-
## 兼容性
|
|
37
|
-
|
|
38
|
-
**一份包同时支持三条宿主线**——0.1.7-alpha.1 / alpha.2(下限)、0.1.7-rc.1 / rc.2、0.2.0-rc.1 / rc.2:安装方式与宿主线无关,三条线都验证过「设置卡片可用、样式真的作用到页面、卡片上的改动写得进去」。
|
|
39
|
-
|
|
40
|
-
| 插件版本 | 支持的 DSH 版本 |
|
|
41
|
-
| --- | --- |
|
|
42
|
-
| **0.7.x**(最新,0.7.
|
|
43
|
-
| 0.6.x(0.6.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.1、0.2.1-rc.2 |
|
|
44
|
-
| 0.5.x(0.5.0) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2 |
|
|
45
|
-
| 0.4.x(0.4.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2 |
|
|
46
|
-
| 0.3.x(0.3.6) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2 |
|
|
47
|
-
| 0.2.x(0.2.8) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2 |
|
|
48
|
-
|
|
49
|
-
每个大版本只列该系列最新的一个补丁版本(新功能引入的缺陷都在其后的补丁里修掉了,所以同一个大版本内直接用最新补丁即可;旧补丁仍可继续用,插件不破坏既有接口)。
|
|
50
|
-
|
|
51
|
-
## 安装
|
|
52
|
-
|
|
53
|
-
通过 DSH CLI 安装:
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
dsh plugin --profile web add dsh-fonttune
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
也可以从 GitHub 安装:
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
dsh plugin --profile web add github:LyaxZ/dsh-fonttune
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
或以本地目录安装:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
dsh plugin --profile web add <插件目录路径>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
安装后重启一次 DSH 并打开 Web UI;之后改设置不需要重启。
|
|
72
|
-
|
|
73
|
-
## 使用
|
|
74
|
-
|
|
75
|
-
- **打开设置** —— 设置 → 插件 → 插件配置 → 展开「字体增强」卡片
|
|
76
|
-
- **分区导航** —— 卡片展开后自上而下是:编辑模式、预设方案、**对话 → 界面 → 代码**三个收起的分区(对话在最前,因为它拥有全部轴),以及直接平铺在分区下面的「拒绝合成样式」;分区标题行右侧显示当前值摘要、箭头固定在最右,同一时刻最多展开一个分区
|
|
77
|
-
- **换字体** —— 简单模式下点「西文/中文」格子选字体;高级模式下用「添加字体」与 chips 编辑器维护完整回退列表
|
|
78
|
-
- **调整字号/字重/行高** —— 滑块就在各自分区里,拖动松手即生效(拖动时读数连同单位 % 一起显示);改过的字段标「已修改」并可单独重置,卡片底部有「全部重置」
|
|
79
|
-
- **对话优先** —— 对话分区可以设置字体、字号、行高、字重四项;界面分区默认只有「跟随对话设置」开关(默认开启),关掉后才出现界面自己的字体与字重
|
|
80
|
-
- **关掉跟随不会跳变** —— 关掉开关的那一刻,界面会把**正在显示的**字体与字重偏移写成自己的值(跟随期间它本来就是这两项),所以画面零变化、滑块从当前值继续调;对话没设的那一项则清掉界面自己的字段、保持 DSH 默认。想回到 DSH 自己的粗细点该项的「重置」(三个字重偏移读数都回到 0)
|
|
81
|
-
- **界面字重不会碰到对话** —— 界面字重是唯一必须铺满整页的轴(DSH 把小标签的粗细写死在上游 CSS 的类规则里,窄规则够不到它),实现上是一条把**对话 Markdown 整棵子树排除在外**的规则,所以对话的段落、标题、粗体都不受影响
|
|
82
|
-
- **字重读数** —— 三个字重轴的未设置读数都显示 0(滑块停在正中);不会出现「未设置」这类字眼;旧版本存的绝对字重(300 ~ 600)会在读取时自动换算成等价偏移(400 → 0、450 → +50),无需手动重设
|
|
83
|
-
- **字重滑块按档计数** —— 卡片会量出当前字体在正文处能渲染出几档粗细,一档取**变化足够明显**的那一档(不是最细的那一档)、上下限以正文还能换字形为限(加粗与标题到顶后停住;读数就是档数,如 `-1 … +2`),所以档数可能比之前少、但每挪一格都看得出变化,也不会出现"几百几百"的数字;换字体后档数会重新测量
|
|
84
|
-
- **预设方案** —— 编辑模式下方的下拉选择框(默认配置 1 ~ 5,框内右侧有与卡片一致的下拉箭头),点开选择即切换并自动保存后续改动;「改名」重命名当前方案,「导出」复制 JSON,「导入」粘贴合并。操作结果(如「已复制到剪贴板」)显示在下面那行的**右侧**,约 2.6 秒后自动淡出,不会顶动任何布局
|
|
85
|
-
- **预览** —— 对话 / 界面 / 代码分区展开后,各自分区末尾有一个预览框,只渲染该分区的样式
|
|
86
|
-
- 设置保存在 DSH 的设置文档里(0.1.7 起是 profile 的 `cordis.patch.yml`,更早的版本是 `settings.yaml`),跟随 profile 走
|
|
87
|
-
|
|
88
|
-
## 开发
|
|
89
|
-
|
|
90
|
-
- `src/shared.cjs` —— 纯函数核心:字体名消毒与解析、配置归一化、注入样式的生成
|
|
91
|
-
- `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把一个打过标记的 `<style>` 随首页下发,首帧即是设置里的字体;那个元素随后由浏览器半**认领并接管**(页面里只有这一份样式表,规则可增可删)
|
|
92
|
-
- `src/client.js` —— 浏览器半:设置卡片、选字体面板、字体枚举与样式注入
|
|
93
|
-
- `build.mjs` —— 零依赖构建:内联共享核心、套上 `window.__ModuleLoader__.load({id, factory})` 外壳、拷贝宿主半,并校验客户端 bundle 只 require shell 预注入的模块
|
|
94
|
-
- `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性、选字体面板的分组规则、令牌轮询的省电闸门);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃),把**每个控件的回调都点一遍**(未接线的回调只有真点下去才会暴露),并**渲染第二次**覆盖「状态变化后 hook 顺序改变」这一类崩溃;`test/artifacts.mjs` 校验 `lib/` 与当前 `src/` 逐字节一致(`npm run verify` 跑全部三套)。devDependency 钉的是**最新的** schemastery(`^3.18.4`),也就是 alpha 那套方言——它带 `.volatile()`,解析出来的字段是**引用**而不是值,所以这几套检查都通过宿主半导出的 `plainConfigValue` 取值(和插件读配置同一条路);运行时用的是**宿主自带的那份**(rc.2/rc.3 是 3.18.2,没有该修饰符,那条形态由真机探针覆盖)
|
|
95
|
-
- 需要真实页面的检查走 `npm run verify:browser -- "<带 token 的地址>"`(先起一个受管实例:`dsh web --port 0 --no-open`,它会打印地址)。默认只跑**只读**的几支(`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`);加 `--writers` 再跑会**写设置命名空间**的几支(`slider-walk` / `split-walk` / `split-verify` / `weight-verify`,各自先快照用户层、退出前还原),`--only a,b` 挑子集;每支的退出码就是判定。`test/set-user-layer.mjs <url> '<json>'` 是在被中断的运行之后还原用户层的工具
|
|
96
|
-
- `test/host-line-probe.mjs` 是**与宿主线无关**的那一支:只断言两条线都必须成立的事——页面无报错、插件只维护**一份**样式表、设置真的作用到文档(`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` 给了就顺带断言计算样式)、设置页里能找到并渲染出 `.dfp-card`(哪条线的座位都行)、以及从卡片上改一个值真的写进去。换宿主线(rc ↔ alpha)后先跑它;调试「alpha 上卡片写不进去」这类问题时设 `DFP_PROBE=1`,插件会往控制台打一行 `[dfp-probe]`(服务了哪些命名空间、认领了哪个、座位当前是什么状态)
|
|
97
|
-
- `test/weight-verify.mjs` 在真实页面里量三条字重轴(界面 / 对话 / 代码)的独立性与「样式表只有一份、不刷新关掉跟随立即回落」,并**逐级断言会话字重的阶梯**(+180 与 −100 两个方向下 h2 > strong > 正文,且每组都等于自身基准 + 偏移);`test/split-verify.mjs` 量对话/代码两条字号轴的独立性、外加**退役的界面字号轴什么都不动**,并用一条 h1~h4 探针断言**每个标题级别只吃一次偏移**(±3 各验一遍,`h1 > h2 > h3 ≥ h4`);`test/slider-walk.mjs` 与 `test/split-walk.mjs` 驱动真实卡片(拖动不写、松手才写;简单模式两格与整条栈的往返);另有市场条目的维护脚本 `test/market-pr.mjs`(status / update / refresh / reopen / open / about)与诊断脚本 `test/market-inspect.mjs`(PR 状态、评论、CI 与分支差异)
|
|
98
|
-
- `docs/` —— 0.2.0 的[背景调研](docs/research-0.2.0.md)与[功能规格](docs/spec-0.2.0.md)(设计阶段的记录,含当时的取舍理由;与最终实现不同的地方以 README/CHANGELOG 为准,两份文件文首都有说明)。**不在 npm `files` 清单内,不随包发布**
|
|
99
|
-
- 客户端模块能 require 的只有 shell 静态表里的模块(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*` 等);`dsh.client.inject` 只是加载顺序声明,不是 require 许可
|
|
100
|
-
- 改完源码要**先构建再刷新**:`node build.mjs`(或 `npm run watch`)把 `src/` 生成到 `lib/`,浏览器加载的是 `lib/`,只改 `src/client.js` 不构建的话页面看到的还是旧包;`npm test` 只检查功能,`node test/artifacts.mjs`(在 `npm run verify` 里)专门校验 `lib/` 与当前 `src/` 逐字节一致。改宿主半(`src/index.mjs`)或 `cordis.patch.yml` 需重启 DSH
|
|
101
|
-
|
|
102
|
-
## License
|
|
103
|
-
|
|
104
|
-
MIT © 2026 LyaxZ
|
|
1
|
+
# dsh-fonttune
|
|
2
|
+
|
|
3
|
+
> [English](README.en.md) | **中文**
|
|
4
|
+
|
|
5
|
+
**DeepSeek Harness(DSH)字体插件**:**对话**(会话 Markdown 正文)拥有完整的排印控制——字体族、字号、行高、字重、字距、词距、段距与首行缩进;**界面**(设置页、侧栏、工作区、按钮)提供字体与字重并**默认跟随对话**;**代码**独立成轴,另有连字与特性开关;整套配置可存为**预设方案**。配置入口是**原生卡片**(分区收起 + 摘要),改完即时生效:0.2 线在「插件」页的**已安装**栏目里(点开本插件的卡片),rc 线在 设置 → 插件 → 插件配置。
|
|
6
|
+
|
|
7
|
+
## 界面
|
|
8
|
+
|
|
9
|
+
悬浮调参面板(左,含底部「当前生效」读数)与设置卡片里的文字排印部分(右):
|
|
10
|
+
|
|
11
|
+
| 悬浮面板 | 设置卡片 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
|  |  |
|
|
14
|
+
|
|
15
|
+
## 功能
|
|
16
|
+
|
|
17
|
+
- **三套排印**:对话(会话 Markdown 正文、表格与标题)排在第一位、拥有全部轴;界面(设置页、侧栏、工作区与标题按钮)提供**字体与字重**、默认跟随对话(可关);代码独立成轴
|
|
18
|
+
- **亮暗分套** —— 亮色与暗色各一套排印:卡片里把「亮暗分套」切到「亮暗分开」,再选「暗色」,编辑的就是深色主题那一套。暗色只覆盖你调过的项,其余跟随亮色,不必把值重设一遍;「一起设置」时两套共用同一份,行为与之前一致
|
|
19
|
+
- **面板预设切换** —— 面板顶部一行可选预设方案,选中即整套应用,不用打开设置页。面板编辑的是**当前主题**那一套:页面是浅色就只改浅色,深色就只改深色
|
|
20
|
+
- **悬浮调参面板** —— 对话角落的小圆点展开为卡片,对话的调整项全部在这里(字号、行高、字重、字距、词距、段距、首行缩进);面板背景半透明并模糊它后面的对话,与「对话大纲」的侧边面板同一观感;小圆点吸附在对话区域的四角、展开方向随所在角落,面板大小与位置自动记住;面板右上角有直达本插件设置页的按钮,底部**「当前生效」一栏**写出本页实际在用的对话、界面与代码三处字体和字重滑块的档数(连续字重生效时标注「细档」),这一栏可收起,面板被压矮时可滚动查看;设置页里可把面板整体关闭(小圆点彻底隐藏);展开与收起动画不受系统「减少动效」设置影响
|
|
21
|
+
- **界面 / 对话 / 代码字体** —— 各自独立的字体回退列表,拉丁字体在前、中文字体在后;留空表示完全沿用 DSH 的字体栈
|
|
22
|
+
- **字号偏移** —— **对话**字号 -3 ~ +6 px,加在 DSH 自己的「对话字号」之上(官方数值是基准),段落、标题与行高一起跟随,且**只在源头加一次**(H1/H2/H3 的尺寸都由同一条 delta 派生,任意偏移下层级都不变、也不会出现某个标题偏小);代码字号独立成轴,-3 ~ +6 px。**界面没有字号轴**:经实测 DSH 的设置页、侧栏与工作区把字号写死在上游 CSS 里,唯一官方字号接口就是「对话字号」
|
|
23
|
+
- **字重偏移** —— **对话、界面与代码都是同一种相对偏移**(未设置即 0、读数按 `+n` / `-n` 显示,范围按当前字体实测):偏移叠加在每个元素**自身的**字重上,标题仍按 DSH 的层级(h1~h3 700、h4~h6 与加粗 600、表头 500、正文 400)逐级保持"标题明显粗于正文",调重调轻都不会把层级抹平,代码块里的粗体也不会被压平。界面偏移是一条把对话 Markdown 整棵子树排除在外、只铺 `body` 与自带字重的元素表的规则:真机实测(活页面计算样式)它把界面的文本元素改到目标值、对话 markdown 里的元素一个都没动,所以**改界面字重不会碰到对话**。滑块**按档计数**:读数就是档数(例如 `-1 0 +1`),**档数由字体决定**——卡片会在页面里量出所选字体在正文处实际能渲染出几种粗细,一档取**变化足够明显**的那一档(不是最细的那一档),范围到正文再也换不到字形为止(加粗与标题到顶后停住),所以档数可能比之前少,但每挪一格都看得出来,数字也不会出现"几百几百";换字体会重新测量。文档里存的仍是字重偏移(旧值自动换算),换字体不会把设置改坏;字体只有常规与粗体两档时就只有 2 个档位,要连续可选用字重档位多的字体或可变字体
|
|
24
|
+
- **行高** —— **对话**行高用倍率(100% ~ 160%);代码行高用加法偏移(-4 ~ +8 px)。界面没有行高轴(同字号的原因)
|
|
25
|
+
- **文字排印(对话)** —— 四条叠加在 DSH 原有排版之上的轴,改的是文字的形状而不是字体本身:**字距**(-0.4 ~ +2 px,正值更疏、负值更紧)、**词距**(-1 ~ +4 px,只影响西文词间空隙)、**段距**(-8 ~ +24 px,加在 DSH 自己的段落间距上,标题间距不受影响)、**首行缩进**(0 ~ +4 em,中文排版的常规做法),以及**严格避头尾**开关(换行时不让中文标点、小假名等出现在行首)。四条轴都以 0 为「保持 DSH 原样」,关闭时**一个字节都不注入**;四项都进预设方案,可随方案带走。**未做的两项**:中西文自动间距与标点悬挂——本机浏览器(Chromium)尚不支持对应的 CSS 属性,与其放一个按下去没反应的开关,不如说明白
|
|
26
|
+
- **代码连字** —— 三档开关(默认 / 开启 / 关闭,浏览器默认即开启,关闭可还原 `=>`、`!=` 这类连字),高级模式另有 `font-feature-settings` 特性值输入
|
|
27
|
+
- **文字渲染质感** —— 三档:跟随系统(默认)/ 锐利 / 圆润。同一副字体在不同渲染方式下笔画质感不同,锐利更清晰硬朗、圆润更柔和均匀
|
|
28
|
+
- **拒绝合成样式**(默认关闭 = 允许合成) —— 打开后禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰);关着(默认)时由浏览器按自己的规则合成,字体缺粗体字形时至少还能看出"粗体"这一档
|
|
29
|
+
- **界面跟随对话开关** —— 界面分区顶部的「跟随对话设置」开关(默认开启):开启时界面沿用对话的**字体与字重**,界面自己的两个控件隐藏;关闭后界面用自己的字体与字重,与对话互不影响。字号与行高只属于对话(界面没有这两个接口)
|
|
30
|
+
- **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON 分享、导入合并)。剪贴板不可用时(非安全上下文、或没有剪贴板权限)导出不会只给一段被截断的提示,而是把完整 JSON 放进一个已选中、只读的文本框,按复制快捷键即可,复制后自动收起;任何**写设置失败**(宿主拒绝、只读部署等)都会在当前行显示出来,而不是让控件悄悄弹回旧值;写入被接受、文档里的值却没有跟着变时,滑块也会在下方说明已回到文档里的值
|
|
31
|
+
- **西文 / 中文分栏(简单模式)** —— 编辑模式开关:简单模式每套字体只给「西文」「中文」两个单选格,高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不改动已排好的顺序
|
|
32
|
+
- **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;四组预设**任何情况下都在**(包括搜索时,也包括枚举本机字体失败时),本机分组只列出预设分组没有的字体,所以同一个字体不会出现两次;搜索不到的名字可以直接「使用 xxx」新建。字体枚举被浏览器拒绝时会在一段时间后自动重试(用户在浏览器里授予字体权限后,重新打开面板即可看到本机字体)
|
|
33
|
+
- **拖拽排序** —— 已选字体是 chip 列表,可拖动调整回退顺序,同时保留前移 / 后移按钮(键盘与触屏可用)
|
|
34
|
+
- **分区预览** —— 对话 / 界面 / 代码分区展开后在分区末尾各有一个预览框,只预览该分区的部分
|
|
35
|
+
|
|
36
|
+
## 兼容性
|
|
37
|
+
|
|
38
|
+
**一份包同时支持三条宿主线**——0.1.7-alpha.1 / alpha.2(下限)、0.1.7-rc.1 / rc.2、0.2.0-rc.1 / rc.2:安装方式与宿主线无关,三条线都验证过「设置卡片可用、样式真的作用到页面、卡片上的改动写得进去」。
|
|
39
|
+
|
|
40
|
+
| 插件版本 | 支持的 DSH 版本 |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| **0.7.x**(最新,0.7.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.1、0.2.1-rc.2、0.2.1-rc.4、0.2.2-alpha.2 |
|
|
43
|
+
| 0.6.x(0.6.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.1、0.2.1-rc.2 |
|
|
44
|
+
| 0.5.x(0.5.0) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2 |
|
|
45
|
+
| 0.4.x(0.4.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2 |
|
|
46
|
+
| 0.3.x(0.3.6) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2 |
|
|
47
|
+
| 0.2.x(0.2.8) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2 |
|
|
48
|
+
|
|
49
|
+
每个大版本只列该系列最新的一个补丁版本(新功能引入的缺陷都在其后的补丁里修掉了,所以同一个大版本内直接用最新补丁即可;旧补丁仍可继续用,插件不破坏既有接口)。
|
|
50
|
+
|
|
51
|
+
## 安装
|
|
52
|
+
|
|
53
|
+
通过 DSH CLI 安装:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
dsh plugin --profile web add dsh-fonttune
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
也可以从 GitHub 安装:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
dsh plugin --profile web add github:LyaxZ/dsh-fonttune
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
或以本地目录安装:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
dsh plugin --profile web add <插件目录路径>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
安装后重启一次 DSH 并打开 Web UI;之后改设置不需要重启。
|
|
72
|
+
|
|
73
|
+
## 使用
|
|
74
|
+
|
|
75
|
+
- **打开设置** —— 设置 → 插件 → 插件配置 → 展开「字体增强」卡片
|
|
76
|
+
- **分区导航** —— 卡片展开后自上而下是:编辑模式、预设方案、**对话 → 界面 → 代码**三个收起的分区(对话在最前,因为它拥有全部轴),以及直接平铺在分区下面的「拒绝合成样式」;分区标题行右侧显示当前值摘要、箭头固定在最右,同一时刻最多展开一个分区
|
|
77
|
+
- **换字体** —— 简单模式下点「西文/中文」格子选字体;高级模式下用「添加字体」与 chips 编辑器维护完整回退列表
|
|
78
|
+
- **调整字号/字重/行高** —— 滑块就在各自分区里,拖动松手即生效(拖动时读数连同单位 % 一起显示);改过的字段标「已修改」并可单独重置,卡片底部有「全部重置」
|
|
79
|
+
- **对话优先** —— 对话分区可以设置字体、字号、行高、字重四项;界面分区默认只有「跟随对话设置」开关(默认开启),关掉后才出现界面自己的字体与字重
|
|
80
|
+
- **关掉跟随不会跳变** —— 关掉开关的那一刻,界面会把**正在显示的**字体与字重偏移写成自己的值(跟随期间它本来就是这两项),所以画面零变化、滑块从当前值继续调;对话没设的那一项则清掉界面自己的字段、保持 DSH 默认。想回到 DSH 自己的粗细点该项的「重置」(三个字重偏移读数都回到 0)
|
|
81
|
+
- **界面字重不会碰到对话** —— 界面字重是唯一必须铺满整页的轴(DSH 把小标签的粗细写死在上游 CSS 的类规则里,窄规则够不到它),实现上是一条把**对话 Markdown 整棵子树排除在外**的规则,所以对话的段落、标题、粗体都不受影响
|
|
82
|
+
- **字重读数** —— 三个字重轴的未设置读数都显示 0(滑块停在正中);不会出现「未设置」这类字眼;旧版本存的绝对字重(300 ~ 600)会在读取时自动换算成等价偏移(400 → 0、450 → +50),无需手动重设
|
|
83
|
+
- **字重滑块按档计数** —— 卡片会量出当前字体在正文处能渲染出几档粗细,一档取**变化足够明显**的那一档(不是最细的那一档)、上下限以正文还能换字形为限(加粗与标题到顶后停住;读数就是档数,如 `-1 … +2`),所以档数可能比之前少、但每挪一格都看得出变化,也不会出现"几百几百"的数字;换字体后档数会重新测量
|
|
84
|
+
- **预设方案** —— 编辑模式下方的下拉选择框(默认配置 1 ~ 5,框内右侧有与卡片一致的下拉箭头),点开选择即切换并自动保存后续改动;「改名」重命名当前方案,「导出」复制 JSON,「导入」粘贴合并。操作结果(如「已复制到剪贴板」)显示在下面那行的**右侧**,约 2.6 秒后自动淡出,不会顶动任何布局
|
|
85
|
+
- **预览** —— 对话 / 界面 / 代码分区展开后,各自分区末尾有一个预览框,只渲染该分区的样式
|
|
86
|
+
- 设置保存在 DSH 的设置文档里(0.1.7 起是 profile 的 `cordis.patch.yml`,更早的版本是 `settings.yaml`),跟随 profile 走
|
|
87
|
+
|
|
88
|
+
## 开发
|
|
89
|
+
|
|
90
|
+
- `src/shared.cjs` —— 纯函数核心:字体名消毒与解析、配置归一化、注入样式的生成
|
|
91
|
+
- `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把一个打过标记的 `<style>` 随首页下发,首帧即是设置里的字体;那个元素随后由浏览器半**认领并接管**(页面里只有这一份样式表,规则可增可删)
|
|
92
|
+
- `src/client.js` —— 浏览器半:设置卡片、选字体面板、字体枚举与样式注入
|
|
93
|
+
- `build.mjs` —— 零依赖构建:内联共享核心、套上 `window.__ModuleLoader__.load({id, factory})` 外壳、拷贝宿主半,并校验客户端 bundle 只 require shell 预注入的模块
|
|
94
|
+
- `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性、选字体面板的分组规则、令牌轮询的省电闸门);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃),把**每个控件的回调都点一遍**(未接线的回调只有真点下去才会暴露),并**渲染第二次**覆盖「状态变化后 hook 顺序改变」这一类崩溃;`test/artifacts.mjs` 校验 `lib/` 与当前 `src/` 逐字节一致(`npm run verify` 跑全部三套)。devDependency 钉的是**最新的** schemastery(`^3.18.4`),也就是 alpha 那套方言——它带 `.volatile()`,解析出来的字段是**引用**而不是值,所以这几套检查都通过宿主半导出的 `plainConfigValue` 取值(和插件读配置同一条路);运行时用的是**宿主自带的那份**(rc.2/rc.3 是 3.18.2,没有该修饰符,那条形态由真机探针覆盖)
|
|
95
|
+
- 需要真实页面的检查走 `npm run verify:browser -- "<带 token 的地址>"`(先起一个受管实例:`dsh web --port 0 --no-open`,它会打印地址)。默认只跑**只读**的几支(`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`);加 `--writers` 再跑会**写设置命名空间**的几支(`slider-walk` / `split-walk` / `split-verify` / `weight-verify`,各自先快照用户层、退出前还原),`--only a,b` 挑子集;每支的退出码就是判定。`test/set-user-layer.mjs <url> '<json>'` 是在被中断的运行之后还原用户层的工具
|
|
96
|
+
- `test/host-line-probe.mjs` 是**与宿主线无关**的那一支:只断言两条线都必须成立的事——页面无报错、插件只维护**一份**样式表、设置真的作用到文档(`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` 给了就顺带断言计算样式)、设置页里能找到并渲染出 `.dfp-card`(哪条线的座位都行)、以及从卡片上改一个值真的写进去。换宿主线(rc ↔ alpha)后先跑它;调试「alpha 上卡片写不进去」这类问题时设 `DFP_PROBE=1`,插件会往控制台打一行 `[dfp-probe]`(服务了哪些命名空间、认领了哪个、座位当前是什么状态)
|
|
97
|
+
- `test/weight-verify.mjs` 在真实页面里量三条字重轴(界面 / 对话 / 代码)的独立性与「样式表只有一份、不刷新关掉跟随立即回落」,并**逐级断言会话字重的阶梯**(+180 与 −100 两个方向下 h2 > strong > 正文,且每组都等于自身基准 + 偏移);`test/split-verify.mjs` 量对话/代码两条字号轴的独立性、外加**退役的界面字号轴什么都不动**,并用一条 h1~h4 探针断言**每个标题级别只吃一次偏移**(±3 各验一遍,`h1 > h2 > h3 ≥ h4`);`test/slider-walk.mjs` 与 `test/split-walk.mjs` 驱动真实卡片(拖动不写、松手才写;简单模式两格与整条栈的往返);另有市场条目的维护脚本 `test/market-pr.mjs`(status / update / refresh / reopen / open / about)与诊断脚本 `test/market-inspect.mjs`(PR 状态、评论、CI 与分支差异)
|
|
98
|
+
- `docs/` —— 0.2.0 的[背景调研](docs/research-0.2.0.md)与[功能规格](docs/spec-0.2.0.md)(设计阶段的记录,含当时的取舍理由;与最终实现不同的地方以 README/CHANGELOG 为准,两份文件文首都有说明)。**不在 npm `files` 清单内,不随包发布**
|
|
99
|
+
- 客户端模块能 require 的只有 shell 静态表里的模块(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*` 等);`dsh.client.inject` 只是加载顺序声明,不是 require 许可
|
|
100
|
+
- 改完源码要**先构建再刷新**:`node build.mjs`(或 `npm run watch`)把 `src/` 生成到 `lib/`,浏览器加载的是 `lib/`,只改 `src/client.js` 不构建的话页面看到的还是旧包;`npm test` 只检查功能,`node test/artifacts.mjs`(在 `npm run verify` 里)专门校验 `lib/` 与当前 `src/` 逐字节一致。改宿主半(`src/index.mjs`)或 `cordis.patch.yml` 需重启 DSH
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT © 2026 LyaxZ
|
package/lib/client.js
CHANGED
|
@@ -1194,6 +1194,15 @@ function floatBoundsPick(rects, viewport) {
|
|
|
1194
1194
|
if (!isFinite(vh) || vh <= 0) vh = FLOAT_VIEWPORT_H;
|
|
1195
1195
|
var full = { left: 0, top: 0, right: vw, bottom: vh, width: vw, height: vh };
|
|
1196
1196
|
var list = Array.isArray(rects) ? rects : [];
|
|
1197
|
+
// A region narrower than one dot used to count as "no region at all", and the
|
|
1198
|
+
// fallback was the WHOLE viewport — so in a narrow window the dot parked on the
|
|
1199
|
+
// window's own corner, over the sidebar, instead of on the conversation's. The
|
|
1200
|
+
// largest readable box is kept as a fallback, and it names itself in `index` so
|
|
1201
|
+
// the watcher holds the box whose size actually defines the region. `full` is
|
|
1202
|
+
// left for a page with no region at all — or only degenerate 0-area rects.
|
|
1203
|
+
var best = null;
|
|
1204
|
+
var bestIndex = -1;
|
|
1205
|
+
var bestArea = -1;
|
|
1197
1206
|
for (var index = 0; index < list.length; index += 1) {
|
|
1198
1207
|
var box = floatBoxOf(list[index]);
|
|
1199
1208
|
if (box === null) continue;
|
|
@@ -1203,12 +1212,18 @@ function floatBoundsPick(rects, viewport) {
|
|
|
1203
1212
|
var bottom = Math.max(0, Math.min(vh, box.bottom));
|
|
1204
1213
|
var width = right - left;
|
|
1205
1214
|
var height = bottom - top;
|
|
1215
|
+
if (width > 0 && height > 0 && width * height > bestArea) {
|
|
1216
|
+
bestArea = width * height;
|
|
1217
|
+
bestIndex = index;
|
|
1218
|
+
best = { left: left, top: top, right: right, bottom: bottom, width: width, height: height };
|
|
1219
|
+
}
|
|
1206
1220
|
if (width < FLOAT_BOUNDS_MIN || height < FLOAT_BOUNDS_MIN) continue;
|
|
1207
1221
|
return {
|
|
1208
1222
|
index: index,
|
|
1209
1223
|
bounds: { left: left, top: top, right: right, bottom: bottom, width: width, height: height },
|
|
1210
1224
|
};
|
|
1211
1225
|
}
|
|
1226
|
+
if (best !== null) return { index: bestIndex, bounds: best };
|
|
1212
1227
|
return { index: -1, bounds: full };
|
|
1213
1228
|
}
|
|
1214
1229
|
|
|
@@ -1327,6 +1342,36 @@ function floatTransformOrigin(quadrant) {
|
|
|
1327
1342
|
return "100% 100%";
|
|
1328
1343
|
}
|
|
1329
1344
|
|
|
1345
|
+
/**
|
|
1346
|
+
* Which side the open CARD grows towards: the one with more room for it.
|
|
1347
|
+
*
|
|
1348
|
+
* The card hangs off the dot, so it can only extend one way from it, and the
|
|
1349
|
+
* side that holds more of the card is the side the conversation has room for.
|
|
1350
|
+
* Each axis compares the room beyond the dot's own box on either side and sends
|
|
1351
|
+
* the card to the larger one — which is also the side its middle ends up on, so
|
|
1352
|
+
* "the panel goes to the corner it sits in" holds by construction.
|
|
1353
|
+
*
|
|
1354
|
+
* This is the CARD's rule, not the dot's: the dot keeps the corner it remembers
|
|
1355
|
+
* living in (see `floatCornerPoint`), while a rule shared by both is what made
|
|
1356
|
+
* the card jump whenever the dot crossed a line the card had not. The answer
|
|
1357
|
+
* depends on the dot and the region and on nothing else — never on a card it
|
|
1358
|
+
* just placed — so it cannot vote itself back and forth.
|
|
1359
|
+
* @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
|
|
1360
|
+
* @param {number} x - the dot's left edge, in CSS pixels.
|
|
1361
|
+
* @param {number} y - the dot's top edge, in CSS pixels.
|
|
1362
|
+
* @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
|
|
1363
|
+
* @returns {string} one of "tl", "tr", "bl", "br" — the quadrant the card is
|
|
1364
|
+
* anchored on, which is the corner it grows away from.
|
|
1365
|
+
*/
|
|
1366
|
+
function floatCardCorner(bounds, x, y, dot) {
|
|
1367
|
+
var box = floatBoxOf(bounds);
|
|
1368
|
+
if (box === null) box = { left: 0, top: 0, right: FLOAT_VIEWPORT_W, bottom: FLOAT_VIEWPORT_H };
|
|
1369
|
+
var size = dot === undefined ? FLOAT_DOT : dot;
|
|
1370
|
+
var growRight = box.right - x >= x + size - box.left;
|
|
1371
|
+
var growDown = box.bottom - y >= y + size - box.top;
|
|
1372
|
+
return (growDown ? "t" : "b") + (growRight ? "l" : "r");
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1330
1375
|
/**
|
|
1331
1376
|
* Which corner of the card lands on the dot, and where the expansion starts.
|
|
1332
1377
|
*
|
|
@@ -4623,6 +4668,7 @@ var shared = {
|
|
|
4623
4668
|
floatExpandDirection: floatExpandDirection,
|
|
4624
4669
|
floatTransformOrigin: floatTransformOrigin,
|
|
4625
4670
|
floatCardAnchor: floatCardAnchor,
|
|
4671
|
+
floatCardCorner: floatCardCorner,
|
|
4626
4672
|
floatClipStart: floatClipStart,
|
|
4627
4673
|
floatClipRest: floatClipRest,
|
|
4628
4674
|
isRestClip: isRestClip,
|
|
@@ -10377,7 +10423,10 @@ var GLASS_OK = (function () {
|
|
|
10377
10423
|
|
|
10378
10424
|
function mountFloatPanel(scope, t, hostCtx) {
|
|
10379
10425
|
/** How far the panel's drop shadow reaches past its own box (10px offset + blur). */
|
|
10380
|
-
var SHADOW_REACH = 56;
|
|
10426
|
+
var SHADOW_REACH = 56;
|
|
10427
|
+
/** The dot's own snap slide, in ms — the stylesheet's `.dfp-floatSnap` transition. */
|
|
10428
|
+
var FLOAT_SNAP_MS = 160;
|
|
10429
|
+
var noop = function () {};
|
|
10381
10430
|
if (typeof document === "undefined" || !document.body) return { destroy: noop, toggle: noop };
|
|
10382
10431
|
// The offline stand-ins hand out element stubs without listeners or
|
|
10383
10432
|
// selectors; without a real element face there is nothing to mount on.
|
|
@@ -10468,6 +10517,19 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10468
10517
|
// end on the same small rectangle the unfold started from, and the card's own
|
|
10469
10518
|
// measurement only exists while it is mounted.
|
|
10470
10519
|
var liveQuadrant = null;
|
|
10520
|
+
// The CARD's own side, re-decided when the panel opens (and when the dot is
|
|
10521
|
+
// dropped or the region changes shape). It is NOT the dot's corner: the card
|
|
10522
|
+
// goes wherever it has the most room, while the dot keeps the corner it
|
|
10523
|
+
// remembers living in. One rule shared by both is what made the card jump
|
|
10524
|
+
// whenever the dot crossed a line the card had not.
|
|
10525
|
+
var cardQuadrant = null;
|
|
10526
|
+
// Whether the NEXT render follows a snap. The card then keeps the place it
|
|
10527
|
+
// already had on screen and TRAVELS to the new corner with the dot, instead of
|
|
10528
|
+
// jumping a panel-width to the other side of it (see the glide in `render`).
|
|
10529
|
+
var anchorGlide = false;
|
|
10530
|
+
// The card's own offset inside the host, as laid out last time: the glide is
|
|
10531
|
+
// the difference between where the new side puts it and that.
|
|
10532
|
+
var lastAnchor = null;
|
|
10471
10533
|
|
|
10472
10534
|
var el = function (tag, className, text) {
|
|
10473
10535
|
var node = document.createElement(tag);
|
|
@@ -10846,6 +10908,11 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10846
10908
|
return null;
|
|
10847
10909
|
};
|
|
10848
10910
|
|
|
10911
|
+
// Whether the last region reading was a real region or the whole-window
|
|
10912
|
+
// stand-in (see `floatBounds`). A stand-in never moves the dot: it means "no
|
|
10913
|
+
// conversation to measure right now", not "the conversation is the window".
|
|
10914
|
+
var boundsAreReal = false;
|
|
10915
|
+
|
|
10849
10916
|
/** The window size the bounds are cut to, with the shared fallback. */
|
|
10850
10917
|
var floatViewport = function () {
|
|
10851
10918
|
return {
|
|
@@ -10975,10 +11042,19 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10975
11042
|
* The conversation when it can be found, the whole window when it cannot (a
|
|
10976
11043
|
* settings page has no prose at all): the window is what this used to be
|
|
10977
11044
|
* everywhere, and it stays the last resort rather than the rule.
|
|
11045
|
+
*
|
|
11046
|
+
* WHICH one answered is remembered in `boundsAreReal`, because the two are not
|
|
11047
|
+
* interchangeable for a MOVE: the whole window is a stand-in for "there is no
|
|
11048
|
+
* conversation to measure at this instant" — a session still mounting, a
|
|
11049
|
+
* message list laid out at 0×0 — and a dot parked on the window's corner over
|
|
11050
|
+
* a page that does have a conversation is exactly the reported "it sits on the
|
|
11051
|
+
* window's left edge instead of the conversation's corner".
|
|
10978
11052
|
* @returns {object} the bounds, in viewport coordinates.
|
|
10979
11053
|
*/
|
|
10980
11054
|
var floatBounds = function () {
|
|
10981
|
-
|
|
11055
|
+
var picked = shared.floatBoundsPick(floatBoundsRects(), floatViewport());
|
|
11056
|
+
boundsAreReal = picked.index >= 0;
|
|
11057
|
+
return picked.bounds;
|
|
10982
11058
|
};
|
|
10983
11059
|
|
|
10984
11060
|
/**
|
|
@@ -11067,6 +11143,36 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11067
11143
|
);
|
|
11068
11144
|
};
|
|
11069
11145
|
|
|
11146
|
+
/**
|
|
11147
|
+
* The shade's scale for the panel's CURRENT visible box.
|
|
11148
|
+
*
|
|
11149
|
+
* Read from the card's own clip every frame, not driven by a CSS transition: the
|
|
11150
|
+
* small value only exists for the two arming frames, and a transition beginning
|
|
11151
|
+
* there is cleared before it can run (measured: the inline value was gone and
|
|
11152
|
+
* `transform` was `none` 138 ms into the unfold, with the shadow at full size the
|
|
11153
|
+
* whole way). Written per frame, the shadow is exactly as big as what the panel
|
|
11154
|
+
* is showing, on every frame, by construction.
|
|
11155
|
+
* @param {object} card - the live card.
|
|
11156
|
+
* @param {string} clip - its computed clip-path.
|
|
11157
|
+
* @returns {string} the inline transform, or "" for the resting panel.
|
|
11158
|
+
*/
|
|
11159
|
+
var shadeFromClip = function (card, clip) {
|
|
11160
|
+
if (clip === "none") return "";
|
|
11161
|
+
var found = /^inset\((-?[\d.]+)px (-?[\d.]+)px (-?[\d.]+)px (-?[\d.]+)px/.exec(clip);
|
|
11162
|
+
if (found === null) return "";
|
|
11163
|
+
var width = 0;
|
|
11164
|
+
var height = 0;
|
|
11165
|
+
try {
|
|
11166
|
+
width = card.offsetWidth;
|
|
11167
|
+
height = card.offsetHeight;
|
|
11168
|
+
} catch (error) {
|
|
11169
|
+
return "";
|
|
11170
|
+
}
|
|
11171
|
+
if (!(width > 0) || !(height > 0)) return "";
|
|
11172
|
+
var visibleW = Math.max(1, width - Number(found[2]) - Number(found[4]));
|
|
11173
|
+
var visibleH = Math.max(1, height - Number(found[1]) - Number(found[3]));
|
|
11174
|
+
return "scale(" + (visibleW / width).toFixed(4) + ", " + (visibleH / height).toFixed(4) + ")";
|
|
11175
|
+
};
|
|
11070
11176
|
var mirrorGlassClip = function (card, glass, ms) { if (glass === null || glass === undefined) return;
|
|
11071
11177
|
var shade = host.querySelector(".dfp-floatShade");
|
|
11072
11178
|
var until = Date.now() + ms;
|
|
@@ -11079,11 +11185,11 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11079
11185
|
return;
|
|
11080
11186
|
}
|
|
11081
11187
|
glass.style.clipPath = clip;
|
|
11082
|
-
|
|
11083
|
-
//
|
|
11084
|
-
//
|
|
11085
|
-
//
|
|
11086
|
-
if (shade !== null && shade !== undefined) shade.style.
|
|
11188
|
+
// The shade is SCALED to the panel's visible box, frame by frame (see
|
|
11189
|
+
// `shadeFromClip`): its shadow is painted from its own box, so a layer left at
|
|
11190
|
+
// the full panel size shows a full-size shadow for the whole unfold — the
|
|
11191
|
+
// "second edge" beside the panel's own. Scaling the layer scales the shadow.
|
|
11192
|
+
if (shade !== null && shade !== undefined) shade.style.transform = shadeFromClip(card, clip);
|
|
11087
11193
|
if (Date.now() < until) globalThis.requestAnimationFrame(step);
|
|
11088
11194
|
};
|
|
11089
11195
|
if (typeof globalThis.requestAnimationFrame === "function") globalThis.requestAnimationFrame(step);
|
|
@@ -11430,6 +11536,56 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11430
11536
|
return fallback;
|
|
11431
11537
|
};
|
|
11432
11538
|
|
|
11539
|
+
/**
|
|
11540
|
+
* The middle of the CARD as it is on the page, or null while it is not.
|
|
11541
|
+
*
|
|
11542
|
+
* Read from the layer the card sits in (the wrapper carries the anchored edge
|
|
11543
|
+
* and the size), so the unfold's own scale — which rides the card inside it —
|
|
11544
|
+
* cannot make a growing panel look like a moving one.
|
|
11545
|
+
* @returns {{x: number, y: number}|null} its middle in viewport coordinates.
|
|
11546
|
+
*/
|
|
11547
|
+
var cardMiddle = function () {
|
|
11548
|
+
var layer = null;
|
|
11549
|
+
try {
|
|
11550
|
+
layer = host.querySelector(".dfp-floatShadow");
|
|
11551
|
+
} catch (error) {
|
|
11552
|
+
layer = null;
|
|
11553
|
+
}
|
|
11554
|
+
if (layer === null || layer === undefined) return null;
|
|
11555
|
+
var rect = rectOf(layer);
|
|
11556
|
+
if (rect === null || !(rect.width > 0) || !(rect.height > 0)) return null;
|
|
11557
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
11558
|
+
};
|
|
11559
|
+
|
|
11560
|
+
/**
|
|
11561
|
+
* Re-decide which side of the conversation the card opens towards.
|
|
11562
|
+
*
|
|
11563
|
+
* The rule is the card's OWN, and it is about ROOM: the card hangs off the
|
|
11564
|
+
* dot, so it can only grow one way from it, and the side that holds more of it
|
|
11565
|
+
* is the side the conversation has room for. Per axis the two rooms on either
|
|
11566
|
+
* side of the dot are compared and the card is sent to the larger one, which
|
|
11567
|
+
* is also the side its middle ends up on — the panel flies to the corner it
|
|
11568
|
+
* sits in, measured against the conversation rather than against the dot.
|
|
11569
|
+
*
|
|
11570
|
+
* The dot keeps its own rule — the corner it remembers living in — and that
|
|
11571
|
+
* difference is the point: a rule shared by both made the card jump whenever
|
|
11572
|
+
* the dot crossed a line the card had not. This answer depends on the dot and
|
|
11573
|
+
* the region and on nothing else, so it cannot vote itself back and forth.
|
|
11574
|
+
*
|
|
11575
|
+
* Called at the moments a side can legitimately change — an open, when the
|
|
11576
|
+
* card does not exist yet, a release that lands the dot on another corner, and
|
|
11577
|
+
* a region that changed shape — and never from `render`. A caller that changes
|
|
11578
|
+
* the side must also place the card WITHOUT a slide: a side is an anchored
|
|
11579
|
+
* edge, so the card moves by its own width when it changes.
|
|
11580
|
+
* @param {object} [bounds] - the region, when the caller has it already.
|
|
11581
|
+
*/
|
|
11582
|
+
var voteCardQuadrant = function (bounds, at) {
|
|
11583
|
+
var region = bounds === undefined ? floatBounds() : bounds;
|
|
11584
|
+
var origin = at === undefined ? pos : at;
|
|
11585
|
+
if (origin === null || origin === undefined) return;
|
|
11586
|
+
cardQuadrant = shared.floatCardCorner(region, origin.x, origin.y);
|
|
11587
|
+
};
|
|
11588
|
+
|
|
11433
11589
|
/**
|
|
11434
11590
|
* Snap the dot to the corner it LIVES in, with a short slide.
|
|
11435
11591
|
*
|
|
@@ -11452,8 +11608,15 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11452
11608
|
* to send the dot to the top-left corner the user never asked for.
|
|
11453
11609
|
* @returns {{x: number, y: number}} the corner the dot now sits on.
|
|
11454
11610
|
*/
|
|
11455
|
-
var snapToCorner = function () {
|
|
11611
|
+
var snapToCorner = function (instant) {
|
|
11456
11612
|
var bounds = floatBounds();
|
|
11613
|
+
// A stand-in region is not a home: the window's corner is what the dot would
|
|
11614
|
+
// land on over a page whose conversation is merely not measurable yet, which
|
|
11615
|
+
// is the reported "it snaps to the window's left edge instead of the
|
|
11616
|
+
// conversation's corner". A dot the document already placed keeps the corner
|
|
11617
|
+
// the last REAL region gave it; the observer settles it again the moment one
|
|
11618
|
+
// exists (see `reflowIntoBounds`).
|
|
11619
|
+
if (!boundsAreReal && pos !== null) return pos;
|
|
11457
11620
|
var snapped;
|
|
11458
11621
|
if (pos !== null) {
|
|
11459
11622
|
snapped = cornerHome(bounds, pos);
|
|
@@ -11475,7 +11638,11 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11475
11638
|
}
|
|
11476
11639
|
pos = snapped;
|
|
11477
11640
|
try {
|
|
11478
|
-
|
|
11641
|
+
// An instant settle must also DROP a snap class left over from an earlier
|
|
11642
|
+
// move, or the very same `left`/`top` write is animated and the panel appears
|
|
11643
|
+
// to drift over from where it used to be.
|
|
11644
|
+
if (instant === true) host.classList.remove("dfp-floatSnap");
|
|
11645
|
+
else host.classList.add("dfp-floatSnap");
|
|
11479
11646
|
} catch (error) {
|
|
11480
11647
|
// A host without classes still lands on the snapped corner.
|
|
11481
11648
|
}
|
|
@@ -11586,9 +11753,15 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11586
11753
|
* dot rather than the document handing it a position.
|
|
11587
11754
|
* @returns {boolean} true when the dot was moved onto its corner.
|
|
11588
11755
|
*/
|
|
11589
|
-
var settleOnCorner = function (regionMoved) {
|
|
11756
|
+
var settleOnCorner = function (regionMoved, instant) {
|
|
11590
11757
|
if (drag !== null || resize !== null) return false;
|
|
11591
11758
|
var bounds = floatBounds();
|
|
11759
|
+
// Nothing real to settle onto: a shut panel on a settings page, or a
|
|
11760
|
+
// conversation that is not measurable at this instant, leaves the dot where
|
|
11761
|
+
// it is — moving it onto the window's corner is the bug, not the fallback.
|
|
11762
|
+
// With nothing stored at all there is no home to keep, so the stylesheet's
|
|
11763
|
+
// own corner is pinned as before.
|
|
11764
|
+
if (!boundsAreReal && pos !== null) return false;
|
|
11592
11765
|
var at = pos;
|
|
11593
11766
|
if (at === null) {
|
|
11594
11767
|
var rect = rectOf(host);
|
|
@@ -11628,7 +11801,7 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11628
11801
|
at.y + shared.FLOAT_DOT <= bounds.bottom + 0.5
|
|
11629
11802
|
);
|
|
11630
11803
|
pos = { x: Math.round(at.x), y: Math.round(at.y) };
|
|
11631
|
-
snapToCorner();
|
|
11804
|
+
snapToCorner(instant);
|
|
11632
11805
|
// Both fields in ONE write: the dot is on the corner and the document is
|
|
11633
11806
|
// told which corner that is, in the same `mutate`. A region that changed
|
|
11634
11807
|
// shape is the caller that has to store the move it just made, wherever the
|
|
@@ -11653,8 +11826,22 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11653
11826
|
* `settleOnCorner`).
|
|
11654
11827
|
*/
|
|
11655
11828
|
var reflowIntoBounds = function () {
|
|
11656
|
-
|
|
11657
|
-
|
|
11829
|
+
var bounds = floatBounds();
|
|
11830
|
+
var wasSide = cardQuadrant;
|
|
11831
|
+
if (pos !== null) {
|
|
11832
|
+
// The card's side belongs to the corner the dot is going to, so it is
|
|
11833
|
+
// decided BEFORE the dot is moved — the two then land in the same frame,
|
|
11834
|
+
// and a changed side never sweeps across the middle behind a slide.
|
|
11835
|
+
var home = cornerHome(bounds, pos);
|
|
11836
|
+
cardQuadrant = shared.floatCardCorner(bounds, home.x, home.y);
|
|
11837
|
+
}
|
|
11838
|
+
var moved = settleOnCorner(true);
|
|
11839
|
+
if (open && (moved || cardQuadrant !== wasSide)) {
|
|
11840
|
+
// The region moved the dot and possibly the card's side with it: follow it
|
|
11841
|
+
// in one motion rather than in a jump and a slide (see `anchorGlide`).
|
|
11842
|
+
anchorGlide = moved;
|
|
11843
|
+
render();
|
|
11844
|
+
}
|
|
11658
11845
|
};
|
|
11659
11846
|
|
|
11660
11847
|
/**
|
|
@@ -11948,7 +12135,10 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11948
12135
|
// browser cancelled) would otherwise unfold the card in the middle of the
|
|
11949
12136
|
// conversation. An ordinary open — the dot already on its corner — moves
|
|
11950
12137
|
// nothing at all.
|
|
11951
|
-
settleOnCorner();
|
|
12138
|
+
settleOnCorner(false, true);
|
|
12139
|
+
// Which side the unfold goes to: the card's own vote, taken once, here —
|
|
12140
|
+
// the one moment before it exists on the page (see `voteCardQuadrant`).
|
|
12141
|
+
voteCardQuadrant();
|
|
11952
12142
|
playEnter = true;
|
|
11953
12143
|
if (closeTimer !== null) {
|
|
11954
12144
|
globalThis.clearTimeout(closeTimer);
|
|
@@ -12242,10 +12432,35 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12242
12432
|
// region is where it lives from now on. Every other path reads the
|
|
12243
12433
|
// remembered corner instead (see `settleOnCorner`), which is what keeps a
|
|
12244
12434
|
// region change from re-deciding it from stale coordinates.
|
|
12245
|
-
|
|
12435
|
+
var bounds = floatBounds();
|
|
12436
|
+
// Which corner the dot goes to is read from the PANEL's own middle while
|
|
12437
|
+
// the panel is up — a panel dragged by its head hangs a full card away
|
|
12438
|
+
// from the dot, so asking where the DOT is made the user haul it far past
|
|
12439
|
+
// the conversation's own middle to reach the far side. The conversation's
|
|
12440
|
+
// middle is the origin either way: above it is up, right of it is right.
|
|
12441
|
+
var ref = cardMiddle();
|
|
12442
|
+
if (ref === null) {
|
|
12443
|
+
ref = { x: pos.x + shared.FLOAT_DOT / 2, y: pos.y + shared.FLOAT_DOT / 2 };
|
|
12444
|
+
}
|
|
12445
|
+
corner = shared.floatQuadrantIn(bounds, ref.x, ref.y);
|
|
12446
|
+
// The card's side is decided for the corner the dot LANDS on, not for
|
|
12447
|
+
// where the hand let go: a dot moved to the opposite corner has to take
|
|
12448
|
+
// the card with it, or the panel hangs off the conversation on the side
|
|
12449
|
+
// the old corner had room on.
|
|
12450
|
+
var home = shared.floatCornerPoint(bounds, corner);
|
|
12451
|
+
// Voted for the corner the dot is LANDING on, without moving the dot
|
|
12452
|
+
// first: a stand-in region refuses to move the dot, and a `pos` written
|
|
12453
|
+
// here would then claim a corner the host never went to.
|
|
12454
|
+
voteCardQuadrant(bounds, home);
|
|
12455
|
+
// The snap itself is always animated. The card re-anchors in the same
|
|
12456
|
+
// frame, so a side that changed is already on its new edge while the dot
|
|
12457
|
+
// slides — one reposition at the release point rather than a card dragged
|
|
12458
|
+
// across the screen with its far edge pinned.
|
|
12246
12459
|
snapToCorner();
|
|
12247
12460
|
var live = readState();
|
|
12248
12461
|
writeLivePos(live);
|
|
12462
|
+
// The rebuild that follows rides the snap: a side that changed glides.
|
|
12463
|
+
anchorGlide = open;
|
|
12249
12464
|
render();
|
|
12250
12465
|
};
|
|
12251
12466
|
var up = function () {
|
|
@@ -12457,7 +12672,12 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12457
12672
|
if (open && !(playEnter && !motionReduced()) && typeof dot.classList !== "undefined") {
|
|
12458
12673
|
dot.classList.add("dfp-dotAway");
|
|
12459
12674
|
}
|
|
12460
|
-
if (!open)
|
|
12675
|
+
if (!open) {
|
|
12676
|
+
// A shut panel has no card to glide; a glide armed by a snap is spent here
|
|
12677
|
+
// rather than left to surprise the next open.
|
|
12678
|
+
anchorGlide = false;
|
|
12679
|
+
return;
|
|
12680
|
+
}
|
|
12461
12681
|
|
|
12462
12682
|
// The shadow's own layer: the card is clipped while it animates, and a clip
|
|
12463
12683
|
// on the card would clip the card's own filter away with it (see the sheet).
|
|
@@ -12483,15 +12703,24 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12483
12703
|
// first one is a decision, not a coincidence.
|
|
12484
12704
|
anchor = { x: bounds.right, y: bounds.bottom };
|
|
12485
12705
|
}
|
|
12486
|
-
var quadrant
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12706
|
+
var quadrant;
|
|
12707
|
+
if (cardQuadrant !== null) {
|
|
12708
|
+
// The card's side is its own answer (see `voteCardQuadrant`): the side of
|
|
12709
|
+
// the dot that holds more of the card, voted when a side may change.
|
|
12710
|
+
quadrant = cardQuadrant;
|
|
12711
|
+
} else {
|
|
12712
|
+
// A card that has never been placed anywhere unfolds out of the dot, which
|
|
12713
|
+
// is also where a first panel ends up.
|
|
12714
|
+
quadrant = shared.floatQuadrantIn(bounds, anchor.x, anchor.y);
|
|
12715
|
+
// The remembered corner is the answer the card is really anchored on: a
|
|
12716
|
+
// region narrow enough puts a corner's coordinates on the other side of its
|
|
12717
|
+
// own middle, and the card still has to grow AWAY from the corner the dot
|
|
12718
|
+
// lives in rather than flip around it. A dot that is not on its remembered
|
|
12719
|
+
// corner (mid-drag, or a position just adopted) is read from where it is.
|
|
12720
|
+
// Asked through a helper: `corner` below is the CARD's anchored corner, and
|
|
12721
|
+
// a local of that name here would shadow the dot's identity.
|
|
12722
|
+
quadrant = dotQuadrant(bounds, anchor, quadrant);
|
|
12723
|
+
}
|
|
12495
12724
|
liveQuadrant = quadrant;
|
|
12496
12725
|
var expand = shared.floatExpandDirection(quadrant);
|
|
12497
12726
|
try {
|
|
@@ -12822,10 +13051,45 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12822
13051
|
});
|
|
12823
13052
|
card.appendChild(grip);
|
|
12824
13053
|
})();
|
|
12825
|
-
|
|
13054
|
+
var shadeEl = el("div", "dfp-floatShade");
|
|
13055
|
+
shadeEl.style.transformOrigin = cardCorner.origin;
|
|
13056
|
+
shadow.appendChild(shadeEl);
|
|
12826
13057
|
shadow.appendChild(glass);
|
|
12827
13058
|
shadow.appendChild(card);
|
|
12828
13059
|
host.appendChild(shadow);
|
|
13060
|
+
// A side is an anchored EDGE, so switching it moves the card by its own
|
|
13061
|
+
// width in one frame. On a snap that would be a jump to the middle of the
|
|
13062
|
+
// screen FOLLOWED by the slide to the corner — two motions. The card is put
|
|
13063
|
+
// back where it already was on screen with a translate, and that translate
|
|
13064
|
+
// is released over the dot's own 160 ms: the panel then travels from where
|
|
13065
|
+
// the user let go straight to the corner, in one motion, with the dot.
|
|
13066
|
+
var anchorLeft = 0;
|
|
13067
|
+
var anchorTop = 0;
|
|
13068
|
+
try {
|
|
13069
|
+
anchorLeft = shadow.offsetLeft;
|
|
13070
|
+
anchorTop = shadow.offsetTop;
|
|
13071
|
+
} catch (error) {
|
|
13072
|
+
// An unmeasurable layer keeps no glide; the side change is instant.
|
|
13073
|
+
}
|
|
13074
|
+
if (anchorGlide && lastAnchor !== null) {
|
|
13075
|
+
anchorGlide = false;
|
|
13076
|
+
var glideX = lastAnchor.left - anchorLeft;
|
|
13077
|
+
var glideY = lastAnchor.top - anchorTop;
|
|
13078
|
+
if (glideX !== 0 || glideY !== 0) {
|
|
13079
|
+
try {
|
|
13080
|
+
shadow.style.transform = "translate(" + glideX + "px," + glideY + "px)";
|
|
13081
|
+
// One forced layout commits that frame, so the next write has a value
|
|
13082
|
+
// to leave FROM; without it both writes land in one recalc and the
|
|
13083
|
+
// glide is the same jump it was meant to remove.
|
|
13084
|
+
shadow.getBoundingClientRect();
|
|
13085
|
+
shadow.style.transition = "transform " + FLOAT_SNAP_MS + "ms ease";
|
|
13086
|
+
shadow.style.transform = "none";
|
|
13087
|
+
} catch (error) {
|
|
13088
|
+
shadow.style.transform = "";
|
|
13089
|
+
}
|
|
13090
|
+
}
|
|
13091
|
+
}
|
|
13092
|
+
lastAnchor = { left: anchorLeft, top: anchorTop };
|
|
12829
13093
|
// Enter animation, only on a real shut->open transition: the card starts
|
|
12830
13094
|
// clipped to the dot's own rectangle, a hair smaller and dimmer, and the
|
|
12831
13095
|
// next frames let the transition carry all three home (clip/transform/
|
package/lib/shared.cjs
CHANGED
|
@@ -1185,6 +1185,15 @@ function floatBoundsPick(rects, viewport) {
|
|
|
1185
1185
|
if (!isFinite(vh) || vh <= 0) vh = FLOAT_VIEWPORT_H;
|
|
1186
1186
|
var full = { left: 0, top: 0, right: vw, bottom: vh, width: vw, height: vh };
|
|
1187
1187
|
var list = Array.isArray(rects) ? rects : [];
|
|
1188
|
+
// A region narrower than one dot used to count as "no region at all", and the
|
|
1189
|
+
// fallback was the WHOLE viewport — so in a narrow window the dot parked on the
|
|
1190
|
+
// window's own corner, over the sidebar, instead of on the conversation's. The
|
|
1191
|
+
// largest readable box is kept as a fallback, and it names itself in `index` so
|
|
1192
|
+
// the watcher holds the box whose size actually defines the region. `full` is
|
|
1193
|
+
// left for a page with no region at all — or only degenerate 0-area rects.
|
|
1194
|
+
var best = null;
|
|
1195
|
+
var bestIndex = -1;
|
|
1196
|
+
var bestArea = -1;
|
|
1188
1197
|
for (var index = 0; index < list.length; index += 1) {
|
|
1189
1198
|
var box = floatBoxOf(list[index]);
|
|
1190
1199
|
if (box === null) continue;
|
|
@@ -1194,12 +1203,18 @@ function floatBoundsPick(rects, viewport) {
|
|
|
1194
1203
|
var bottom = Math.max(0, Math.min(vh, box.bottom));
|
|
1195
1204
|
var width = right - left;
|
|
1196
1205
|
var height = bottom - top;
|
|
1206
|
+
if (width > 0 && height > 0 && width * height > bestArea) {
|
|
1207
|
+
bestArea = width * height;
|
|
1208
|
+
bestIndex = index;
|
|
1209
|
+
best = { left: left, top: top, right: right, bottom: bottom, width: width, height: height };
|
|
1210
|
+
}
|
|
1197
1211
|
if (width < FLOAT_BOUNDS_MIN || height < FLOAT_BOUNDS_MIN) continue;
|
|
1198
1212
|
return {
|
|
1199
1213
|
index: index,
|
|
1200
1214
|
bounds: { left: left, top: top, right: right, bottom: bottom, width: width, height: height },
|
|
1201
1215
|
};
|
|
1202
1216
|
}
|
|
1217
|
+
if (best !== null) return { index: bestIndex, bounds: best };
|
|
1203
1218
|
return { index: -1, bounds: full };
|
|
1204
1219
|
}
|
|
1205
1220
|
|
|
@@ -1318,6 +1333,36 @@ function floatTransformOrigin(quadrant) {
|
|
|
1318
1333
|
return "100% 100%";
|
|
1319
1334
|
}
|
|
1320
1335
|
|
|
1336
|
+
/**
|
|
1337
|
+
* Which side the open CARD grows towards: the one with more room for it.
|
|
1338
|
+
*
|
|
1339
|
+
* The card hangs off the dot, so it can only extend one way from it, and the
|
|
1340
|
+
* side that holds more of the card is the side the conversation has room for.
|
|
1341
|
+
* Each axis compares the room beyond the dot's own box on either side and sends
|
|
1342
|
+
* the card to the larger one — which is also the side its middle ends up on, so
|
|
1343
|
+
* "the panel goes to the corner it sits in" holds by construction.
|
|
1344
|
+
*
|
|
1345
|
+
* This is the CARD's rule, not the dot's: the dot keeps the corner it remembers
|
|
1346
|
+
* living in (see `floatCornerPoint`), while a rule shared by both is what made
|
|
1347
|
+
* the card jump whenever the dot crossed a line the card had not. The answer
|
|
1348
|
+
* depends on the dot and the region and on nothing else — never on a card it
|
|
1349
|
+
* just placed — so it cannot vote itself back and forth.
|
|
1350
|
+
* @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
|
|
1351
|
+
* @param {number} x - the dot's left edge, in CSS pixels.
|
|
1352
|
+
* @param {number} y - the dot's top edge, in CSS pixels.
|
|
1353
|
+
* @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
|
|
1354
|
+
* @returns {string} one of "tl", "tr", "bl", "br" — the quadrant the card is
|
|
1355
|
+
* anchored on, which is the corner it grows away from.
|
|
1356
|
+
*/
|
|
1357
|
+
function floatCardCorner(bounds, x, y, dot) {
|
|
1358
|
+
var box = floatBoxOf(bounds);
|
|
1359
|
+
if (box === null) box = { left: 0, top: 0, right: FLOAT_VIEWPORT_W, bottom: FLOAT_VIEWPORT_H };
|
|
1360
|
+
var size = dot === undefined ? FLOAT_DOT : dot;
|
|
1361
|
+
var growRight = box.right - x >= x + size - box.left;
|
|
1362
|
+
var growDown = box.bottom - y >= y + size - box.top;
|
|
1363
|
+
return (growDown ? "t" : "b") + (growRight ? "l" : "r");
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1321
1366
|
/**
|
|
1322
1367
|
* Which corner of the card lands on the dot, and where the expansion starts.
|
|
1323
1368
|
*
|
|
@@ -4614,6 +4659,7 @@ var shared = {
|
|
|
4614
4659
|
floatExpandDirection: floatExpandDirection,
|
|
4615
4660
|
floatTransformOrigin: floatTransformOrigin,
|
|
4616
4661
|
floatCardAnchor: floatCardAnchor,
|
|
4662
|
+
floatCardCorner: floatCardCorner,
|
|
4617
4663
|
floatClipStart: floatClipStart,
|
|
4618
4664
|
floatClipRest: floatClipRest,
|
|
4619
4665
|
isRestClip: isRestClip,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fonttune",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"icon": "icon.svg",
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
"0.2.1-alpha.4": "compatible",
|
|
49
49
|
"0.2.1-alpha.5": "compatible",
|
|
50
50
|
"0.2.1-rc.1": "compatible",
|
|
51
|
-
"0.2.1-rc.2": "compatible"
|
|
51
|
+
"0.2.1-rc.2": "compatible",
|
|
52
|
+
"0.2.1-rc.4": "compatible",
|
|
53
|
+
"0.2.2-alpha.2": "compatible"
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
},
|