@citisen/dsh-font 0.1.0 → 0.2.0
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/README.md +274 -14
- package/README.zh.md +120 -9
- package/lib/client.js +1911 -145
- package/lib/index.js +66 -4
- package/package.json +9 -3
- package/scripts/build-client.mjs +24 -1
- package/scripts/release.mjs +91 -0
- package/scripts/verify-client.mjs +1177 -12
- package/scripts/verify-host.mjs +59 -5
- package/scripts/verify-profile.mjs +64 -16
- package/src/client.js +1893 -145
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
5
|
Customize the DeepSeek Harness **Web GUI fonts** from Settings: the interface
|
|
6
|
-
font
|
|
6
|
+
and code font stacks (each with its weight) and three independent size axes.
|
|
7
7
|
|
|
8
8
|
This is a third-party [dsh](https://github.com/deepseek-ai/deepseek-harness)
|
|
9
9
|
profile bundle. It ships as one dual-face package: a Node half that owns a
|
|
@@ -16,25 +16,197 @@ are present in the `latest` and `next` release channels.
|
|
|
16
16
|
|
|
17
17
|
## What it adds
|
|
18
18
|
|
|
19
|
-
A **Fonts** row in *Settings → General*, with five
|
|
19
|
+
A **Fonts** row in *Settings → General*, with five fields: two font queries and
|
|
20
|
+
three size axes.
|
|
20
21
|
|
|
21
|
-
|
|
|
22
|
+
| Field | Effect | Value |
|
|
22
23
|
| --- | --- | --- |
|
|
23
|
-
| Interface font | `--dsw-font-family` — all non-code UI text |
|
|
24
|
-
| Code font | `--ds-font-family-code` — code blocks, inline code, monospace |
|
|
24
|
+
| Interface font | `--dsw-font-family` — all non-code UI text | a font query: families, optionally with a weight |
|
|
25
|
+
| Code font | `--ds-font-family-code` — code blocks, inline code, monospace | a font query |
|
|
25
26
|
| Interface text size | Scales every hard-coded UI text size | 75% – 150%, step 5% |
|
|
26
27
|
| Conversation text size | Message bodies, headings, and tables | 12 – 20 px |
|
|
27
28
|
| Code text size | Code blocks and inline code | 10 – 20 px |
|
|
28
29
|
|
|
29
|
-
Both
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
Both font fields are **editors for a small font-query language**, not plain text
|
|
31
|
+
inputs: the whole stack is one piece of text you edit directly, with syntax
|
|
32
|
+
colouring, soft wrapping, and a completion list built from the fonts actually on
|
|
33
|
+
your machine. The weight is written *in* the query, next to the family it belongs
|
|
34
|
+
to, so changing a font and its weight is one edit in one place:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Geist Mono medium, "Zhuque Fangsong (technical preview)", monospace
|
|
38
|
+
└─────┬────┘ └──┬─┘
|
|
39
|
+
family weight
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Order is the text's order.** The first family the browser can resolve wins,
|
|
43
|
+
and reordering is an edit: cut and paste it, or put the caret in an entry and
|
|
44
|
+
press <kbd>Alt</kbd>+<kbd>↑</kbd>/<kbd>↓</kbd>. There are no chips to drag.
|
|
45
|
+
- **The text belongs to you.** Nothing rewrites it: not quoting, not reordering,
|
|
46
|
+
not tidying, and not refilling an empty box. A new font goes exactly where the
|
|
47
|
+
caret is, because that is where you typed it — putting one in front of the
|
|
48
|
+
stack is `Inter, ` typed by hand, or a pick from the completion list at the
|
|
49
|
+
start of an entry.
|
|
50
|
+
- **The list knows your fonts.** It completes family names, the generic keywords,
|
|
51
|
+
and — once a family is named — the weights *that family has*, read off its
|
|
52
|
+
faces. Any name can still be typed by hand; the catalogue is never presented as
|
|
53
|
+
complete.
|
|
54
|
+
- **The box is always the same monospace.** The painted layer and the real
|
|
55
|
+
textarea share one box, and a textarea cannot style a substring, so the field
|
|
56
|
+
uses one fixed system monospace at weight 400 with ligatures and kerning turned
|
|
57
|
+
off — whatever font the query names. A face with ligatures (`->` in Fira Code)
|
|
58
|
+
would draw one glyph in the layer and two in the field, and a synthesized
|
|
59
|
+
weight would differ between them. The readout line under the field is rendered
|
|
60
|
+
in the axis's own font, so the family you picked is still visible there.
|
|
61
|
+
- **Enter applies, it never completes.** The query is parsed and written on
|
|
62
|
+
<kbd>Enter</kbd> or when you leave the field; <kbd>Tab</kbd> (or a click) takes
|
|
63
|
+
the highlighted completion, which is the one case where the text is replaced —
|
|
64
|
+
because that is what picking a suggestion means.
|
|
65
|
+
- **A wrong query is marked, not fixed.** A family this machine does not have, a
|
|
66
|
+
weight the family lacks, an unclosed quote, a stray word: each is reported
|
|
67
|
+
under the field, in warning or error colour, and the text is left as typed. An
|
|
68
|
+
empty box writes nothing at all and says so — the saved stack stays in use, and
|
|
69
|
+
*Reset to defaults* is how you go back to the shipped one.
|
|
70
|
+
|
|
71
|
+
Every value is saved through the host settings document
|
|
72
|
+
(`$DSH_HOME/settings.yaml`, namespace `ui-font`), so settings survive a restart
|
|
73
|
+
and are shared by every browser pointed at the same host.
|
|
33
74
|
|
|
34
75
|
The *Interface text size* and *Conversation text size* axes are independent on
|
|
35
76
|
purpose: bumping the interface makes the surrounding chrome easier to read
|
|
36
77
|
without changing how much message text fits on screen, and vice versa.
|
|
37
78
|
|
|
79
|
+
### Choosing a weight: `Geist Mono medium`
|
|
80
|
+
|
|
81
|
+
CSS cannot put a weight in a `font-family` — `font-family: "Geist Mono" 500,
|
|
82
|
+
monospace` is simply an invalid declaration, and the whole stack would be
|
|
83
|
+
dropped. The query language therefore writes the weight as a word beside the
|
|
84
|
+
family, and the plugin reads the pair back into the two declarations that
|
|
85
|
+
actually apply:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Geist Mono medium, monospace → font-family: "Geist Mono", monospace
|
|
89
|
+
→ font-weight: 500
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`Thin`, `ExtraLight`, `Light`, `Book`, `Regular`, `Medium`, `SemiBold`, `Bold`,
|
|
93
|
+
`ExtraBold`, `Black` and their usual aliases (`Hairline`, `DemiBold`,
|
|
94
|
+
`UltraBold`, …) are all recognized, case-insensitively. The weight belongs to the
|
|
95
|
+
axis rather than to one family, so it is honoured wherever it is written and
|
|
96
|
+
counted once; the canonical form puts it after the first family — the one in
|
|
97
|
+
effect.
|
|
98
|
+
|
|
99
|
+
The **shipped weight is implicit**: an axis at its factory 400 carries no word at
|
|
100
|
+
all, because a `regular` that appears after every family is noise nobody asked
|
|
101
|
+
for — and for the interface axis 400 literally means "no override". A word
|
|
102
|
+
appears exactly when a weight is chosen, and deleting it returns the axis to 400.
|
|
103
|
+
What the axis currently stands at is always stated in the line under the field
|
|
104
|
+
(`Interface weight: Regular 400 (shipped, not overridden)`), so nothing is hidden
|
|
105
|
+
by leaving the word out.
|
|
106
|
+
|
|
107
|
+
The weight is a closed list rather than a free number, because a `font-weight`
|
|
108
|
+
the chosen family does not have is *synthesized* by the browser, and offering
|
|
109
|
+
faux-bold as a normal choice is worse than not offering it. When the faces could
|
|
110
|
+
be read — the Local Font Access API reports them — the popup offers *that*
|
|
111
|
+
family's weights, and a weight the family lacks is called out below the field:
|
|
112
|
+
|
|
113
|
+
> Geist Mono has no 700 face on this machine (bold), so the browser will
|
|
114
|
+
> synthesize it
|
|
115
|
+
|
|
116
|
+
Deleting the word does not quietly reset anything: the axis keeps the weight it
|
|
117
|
+
was set to, the line under the field states it, and the word comes back the next
|
|
118
|
+
time the row is built from the stored values. To go back to the shipped weight,
|
|
119
|
+
pick its row from the list (`Regular 400`) — a pick is an instruction, so it sets
|
|
120
|
+
the value even though its text is just the family name.
|
|
121
|
+
|
|
122
|
+
One deliberate escape hatch: if what you type is itself a family on your machine,
|
|
123
|
+
the trailing word is read as part of the name. `Book Antiqua` and
|
|
124
|
+
`Franklin Gothic Medium` are real families, and applying a weight to them instead
|
|
125
|
+
of picking them would be silent misbehaviour. The price is that a name which is
|
|
126
|
+
genuinely both — a family literally called `Geist Mono Medium` — is also read as
|
|
127
|
+
a name. The catalogue decides, so the judgement uses the same font list the
|
|
128
|
+
completion list shows you. Quoting is the way out: `"Book Antiqua"` always means
|
|
129
|
+
the name, verbatim.
|
|
130
|
+
|
|
131
|
+
### The interface weight is opt-in
|
|
132
|
+
|
|
133
|
+
The code surface has no weight hierarchy to preserve, so its weight is simply the
|
|
134
|
+
one written in the query. The interface does have one: headings are 700, table
|
|
135
|
+
heads 500, body 400. Setting an interface weight therefore moves the base and
|
|
136
|
+
carries the heading steps with it, keeping each step's shipped distance from 400
|
|
137
|
+
instead of flattening them:
|
|
138
|
+
|
|
139
|
+
```css
|
|
140
|
+
/* --dsh-font-ui-weight: 500 */
|
|
141
|
+
html body { font-weight: 500 }
|
|
142
|
+
--dsh-font-markdown-base: 500 … /* body text */
|
|
143
|
+
--dsh-font-markdown-h1: 800 … /* 700 + (500 - 400) */
|
|
144
|
+
--dsh-font-markdown-table-head: 600 … /* 500 + (500 - 400) */
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Text that *inherits* its weight moves; a label or a button whose weight the
|
|
148
|
+
design system fixes keeps it, because its own rule still wins. And the shipped
|
|
149
|
+
400 emits **no rule at all**, so an untouched install paints exactly what the
|
|
150
|
+
design system paints.
|
|
151
|
+
|
|
152
|
+
### The query language
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
query := entry ("," entry)*
|
|
156
|
+
entry := family | weight
|
|
157
|
+
family := '"' … '"' | "'" … "'" | word (space word)*
|
|
158
|
+
weight := thin | extralight | light | book | regular | roman | medium |
|
|
159
|
+
demibold | semibold | bold | extrabold | black | heavy | …
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The grammar is the CSS `font-family` list plus the one thing the list cannot
|
|
163
|
+
carry. Only the LAST word of an unquoted entry may be a weight, and only when the
|
|
164
|
+
whole entry is not itself a catalogued family. A bare weight word (`medium`) sets
|
|
165
|
+
the axis weight without naming a family. Commas inside quotes are not separators,
|
|
166
|
+
so `"Foo, Bar"` is one family.
|
|
167
|
+
|
|
168
|
+
The reader is forgiving on purpose — this text is typed by hand, not generated —
|
|
169
|
+
so anything it cannot place is kept as written and reported under the field
|
|
170
|
+
instead of being dropped: an unclosed quote, a stray word after a quoted name, a
|
|
171
|
+
weight written twice, a family the machine does not list, a weight the family has
|
|
172
|
+
no face for. The field also names the family that is in effect, which is the
|
|
173
|
+
first one the browser can actually resolve.
|
|
174
|
+
|
|
175
|
+
### How the font list is discovered
|
|
176
|
+
|
|
177
|
+
Order matters in a CSS font stack — the first installed family wins — so the
|
|
178
|
+
editor is built around the text rather than around a picker.
|
|
179
|
+
|
|
180
|
+
Reading the real installed-font list needs the
|
|
181
|
+
[Local Font Access API](https://developer.mozilla.org/en-US/docs/Web/API/Local_Font_Access_API)
|
|
182
|
+
(`queryLocalFonts`), which is **Chromium-only, experimental, and
|
|
183
|
+
permission-gated**; MDN also notes browsers are not obliged to return the
|
|
184
|
+
complete list. So the catalogue is layered:
|
|
185
|
+
|
|
186
|
+
1. **`queryLocalFonts()`** when the browser offers it — the machine's own list,
|
|
187
|
+
which needs no badge to explain itself. Its per-face `style` names (`Regular`,
|
|
188
|
+
`SemiBold`, `Bold Italic`) are kept alongside the families, which is what lets
|
|
189
|
+
the completion list offer a family's real weights and warn about one it lacks.
|
|
190
|
+
The first use shows a permission prompt; declining falls back silently rather
|
|
191
|
+
than erroring.
|
|
192
|
+
2. **Measurement probing** everywhere else — each candidate family is rendered
|
|
193
|
+
off-screen and compared against a `monospace` baseline; a different width or
|
|
194
|
+
height means it is installed. No permission needed, works in every browser.
|
|
195
|
+
It only sees the curated probe list, so it under-reports by design and can say
|
|
196
|
+
nothing about weights — which is the one case worth calling out, so the row
|
|
197
|
+
then says so once, above the fields, and adds that any family can still be
|
|
198
|
+
typed.
|
|
199
|
+
3. **Always** the curated catalogue plus the generic families, so a font that
|
|
200
|
+
neither source found can still be typed.
|
|
201
|
+
|
|
202
|
+
The result is cached for the session, and discovery only runs when the row is
|
|
203
|
+
actually rendered — never at startup.
|
|
204
|
+
|
|
205
|
+
The editor also warns when a query has **no generic family at the end** (such as
|
|
206
|
+
`sans-serif`), because a missing font then falls back unpredictably. Quoting is
|
|
207
|
+
automatic: `Fira Code` is stored as `"Fira Code"`, and generic keywords are
|
|
208
|
+
deliberately left unquoted, so `"sans-serif"` can never be written by accident.
|
|
209
|
+
|
|
38
210
|
## Install
|
|
39
211
|
|
|
40
212
|
```sh
|
|
@@ -162,10 +334,56 @@ theme change; the plugin re-asserts its own on every settings change. The row's
|
|
|
162
334
|
help text tells the user this control wins over the *Font size* row in
|
|
163
335
|
Appearance.
|
|
164
336
|
|
|
337
|
+
### Where the code weight is written
|
|
338
|
+
|
|
339
|
+
The design system has **no** weight token at all — every `font:` declaration in
|
|
340
|
+
the interface is a literal, and the code ladder is literally `400`. So the
|
|
341
|
+
weight cannot ride the family token the way a family does, and it is applied on
|
|
342
|
+
two levels:
|
|
343
|
+
|
|
344
|
+
```css
|
|
345
|
+
/* the three code tokens carry it in their font: shorthand … */
|
|
346
|
+
--dsw-font-markdown-code: var(--dsh-font-code-weight,400) var(--dsh-font-code-size,12px) / … ;
|
|
347
|
+
|
|
348
|
+
/* … and everything styled directly with the code family is matched structurally */
|
|
349
|
+
html body pre, html body code, html body [class*="code" i] {
|
|
350
|
+
font-family: var(--ds-font-family-code) !important;
|
|
351
|
+
font-weight: var(--dsh-font-code-weight,400) !important;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The second rule exists because most code in the interface never reads a token:
|
|
356
|
+
the tool I/O cards and the terminal output are styled with `font-family:
|
|
357
|
+
var(--ds-font-family-code)` and their own literal weight inside a component
|
|
358
|
+
stylesheet, so there is nothing to override but the element. The `!important` is
|
|
359
|
+
what outranks `font: 500 12px/18px …`. It is deliberately **not** a universal
|
|
360
|
+
rule: the surrounding labels keep the shipped hierarchy.
|
|
361
|
+
|
|
362
|
+
### Where the interface weight is written
|
|
363
|
+
|
|
364
|
+
The interface weight is opt-in, so the shipped `400` emits no rule at all and an
|
|
365
|
+
untouched install paints exactly what the design system paints. Anything else is
|
|
366
|
+
written on two levels:
|
|
367
|
+
|
|
368
|
+
```css
|
|
369
|
+
/* text that inherits its weight moves as a whole … */
|
|
370
|
+
html body { font-weight: 500 }
|
|
371
|
+
|
|
372
|
+
/* … while the Markdown ladder the plugin already owns is shifted by the same
|
|
373
|
+
distance from the shipped 400, so headings keep their contrast */
|
|
374
|
+
--dsh-font-markdown-base: 500 …
|
|
375
|
+
--dsh-font-markdown-h1: 800 … /* 700 + (500 - 400) */
|
|
376
|
+
--dsh-font-markdown-table-head: 600 … /* 500 + (500 - 400) */
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The ladder has to name the weight explicitly: a `font:` shorthand with no weight
|
|
380
|
+
component resets `font-weight` to `normal`, so `html body` alone would be
|
|
381
|
+
cancelled by the plugin's own tokens.
|
|
382
|
+
|
|
165
383
|
### Pre-paint
|
|
166
384
|
|
|
167
385
|
The Node half answers `webserver/index-inject` with an inline `<script>` that
|
|
168
|
-
installs the stylesheet and sets the
|
|
386
|
+
installs the stylesheet and sets the family and weight variables before the shell
|
|
169
387
|
mounts, so the first frame is already in the user's fonts. It reads the same
|
|
170
388
|
`ui-font` settings section at render time, and falls back to the schema defaults
|
|
171
389
|
when the settings provider is absent.
|
|
@@ -174,9 +392,10 @@ when the settings provider is absent.
|
|
|
174
392
|
|
|
175
393
|
```sh
|
|
176
394
|
npm run build # src/client.js -> lib/client.js
|
|
177
|
-
npm run check #
|
|
178
|
-
|
|
179
|
-
|
|
395
|
+
npm run check # the release gate: bundle in sync, host and client verified
|
|
396
|
+
npm run check:all # adds the profile-composition check (needs a local dsh)
|
|
397
|
+
npm run verify # just the host and client verifiers
|
|
398
|
+
npm run watch # rebuild on save, for dsh-client-hmr
|
|
180
399
|
```
|
|
181
400
|
|
|
182
401
|
`src/client.js` is the single source of truth for the browser half. It is
|
|
@@ -194,6 +413,10 @@ its module table (`react`, `react/jsx-runtime`, `react-dom`,
|
|
|
194
413
|
`@deepseek-ai/dsh-client-ui-dockkit`); anything else must be declared in
|
|
195
414
|
`dsh.client.external` and shipped as its own graph row. The build enforces this.
|
|
196
415
|
|
|
416
|
+
`verify-profile.mjs` needs a dsh installation and an initialized profile, so it
|
|
417
|
+
**skips** (exit 0) when neither is present — a clean CI runner has no dsh. Set
|
|
418
|
+
`DSH_REQUIRE=1` to turn that skip into a failure.
|
|
419
|
+
|
|
197
420
|
To iterate on the browser half against a running host, run the watcher and let
|
|
198
421
|
`dsh-client-hmr` swap the plugin in — it stat-polls client bundles every 500 ms,
|
|
199
422
|
so a saved rebuild reaches the open page without a refresh:
|
|
@@ -209,15 +432,26 @@ dsh --profile web
|
|
|
209
432
|
A page refresh always picks up a newly composed graph, so the watcher is a
|
|
210
433
|
convenience rather than a requirement.
|
|
211
434
|
|
|
435
|
+
### Releasing
|
|
436
|
+
|
|
437
|
+
Releases are staged by GitHub Actions over npm trusted publishing and then
|
|
438
|
+
approved by a maintainer, so no `NPM_TOKEN` exists in this repository and none
|
|
439
|
+
should ever be added. [RELEASING.md](RELEASING.md) is the step-by-step runbook;
|
|
440
|
+
[PUBLISHING.md](PUBLISHING.md) covers the one-time npm setup and what the
|
|
441
|
+
arrangement does and does not protect against.
|
|
442
|
+
|
|
212
443
|
## Package layout
|
|
213
444
|
|
|
214
445
|
| Path | Role |
|
|
215
446
|
| --- | --- |
|
|
216
447
|
| `lib/index.js` | Host half: settings namespace, pre-paint injection. Loaded by the loader. |
|
|
217
|
-
| `lib/client.js` | Browser half, **generated** from `src/client.js`. Served at `/plugins/dsh-font/client.js`. |
|
|
448
|
+
| `lib/client.js` | Browser half, **generated** from `src/client.js`. Served at `/plugins/@citisen/dsh-font/client.js`. |
|
|
218
449
|
| `src/client.js` | Browser-half source. |
|
|
219
450
|
| `cordis.patch.yml` | The profile layer this bundle contributes. |
|
|
220
451
|
| `scripts/` | Build and verification scripts. |
|
|
452
|
+
| `.github/workflows/stage.yml` | The CI half of the only publishing path. |
|
|
453
|
+
| `PUBLISHING.md` | Trusted-publishing setup, and what it does not protect against. |
|
|
454
|
+
| `RELEASING.md` | The runbook for shipping a change. |
|
|
221
455
|
| `package.json` | Declares `dsh.bundle` (profile layer) and `dsh.client` (browser roster entry). |
|
|
222
456
|
|
|
223
457
|
## Known limitations
|
|
@@ -234,6 +468,32 @@ convenience rather than a requirement.
|
|
|
234
468
|
- **Per-element stamping is proportional to the DOM.** The scale pass measures
|
|
235
469
|
each element once and caches the result, and re-stamps on mutation, so it is
|
|
236
470
|
bounded — but it is not free on a very large transcript.
|
|
471
|
+
- **The code weight is not a per-surface choice.** It applies to every code
|
|
472
|
+
surface at once, and where it lands on `pre`, `code`, and any element with
|
|
473
|
+
`code` in its class name — so a `font-weight` a component sets inside a code
|
|
474
|
+
block (syntax highlighting, a bold diff line) is overridden too. The interface
|
|
475
|
+
weight is the mirror image: only text that *inherits* its weight moves, and a
|
|
476
|
+
label or button whose weight the design system fixes keeps it. A weight the
|
|
477
|
+
family lacks is still synthesized by the browser; the closed list and the
|
|
478
|
+
"no such face on this machine" warning only keep it out of reach.
|
|
479
|
+
- **The query language is a convention, not CSS.** It lives only in this row's
|
|
480
|
+
two font fields; what is stored is still a valid CSS `font-family` list plus a
|
|
481
|
+
numeric `font-weight`, so disabling the plugin leaves nothing behind. Whether a
|
|
482
|
+
trailing word is a weight or part of the name is decided by the loaded
|
|
483
|
+
catalogue, so a family whose real name ends in a weight word (`Book Antiqua`,
|
|
484
|
+
`Franklin Gothic Medium`) is left intact only when that exact name was
|
|
485
|
+
discovered — both are curated, so this normally holds, but a name outside the
|
|
486
|
+
probe list can be split when the Local Font Access prompt is declined. Quoting
|
|
487
|
+
settles it: `"Book Antiqua"` is always the name.
|
|
488
|
+
- **The heading shift is one formula, not a redesign.** Each step becomes
|
|
489
|
+
`shipped + (interface weight - 400)`, clamped at 900 — so an interface weight
|
|
490
|
+
of 900 flattens the ladder against that ceiling.
|
|
491
|
+
- **The box is not a copy of the setting.** What is stored is the plugin's
|
|
492
|
+
serialization of the query (quoted, weight word after the first family); what
|
|
493
|
+
the box shows is your text, kept verbatim for as long as the row lives. A
|
|
494
|
+
reload rebuilds the box from the two stored values, so a weight word you
|
|
495
|
+
deleted can reappear there, and it may be spelled differently from what you
|
|
496
|
+
typed.
|
|
237
497
|
- **The settings row is English/Chinese only**, matching the shipped locale pair.
|
|
238
498
|
|
|
239
499
|
## License
|
package/README.zh.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
在设置里自定义 DeepSeek Harness **Web
|
|
5
|
+
在设置里自定义 DeepSeek Harness **Web 界面字体**:界面字体与代码字体两条字体栈(各带自己的字重),以及三条互相独立的字号轴。
|
|
6
6
|
|
|
7
7
|
这是一个第三方 [dsh](https://github.com/deepseek-ai/deepseek-harness) profile bundle(插件包)。它是一个「双面」包:Node 半边负责持久化的设置命名空间与首屏绘制前的样式注入,浏览器半边负责实际绘制并注册设置项。
|
|
8
8
|
|
|
@@ -10,20 +10,84 @@
|
|
|
10
10
|
|
|
11
11
|
## 功能
|
|
12
12
|
|
|
13
|
-
在 *设置 → 通用* 中新增一个 **字体**
|
|
13
|
+
在 *设置 → 通用* 中新增一个 **字体** 行,包含五项:两个字体查询加三条字号轴。
|
|
14
14
|
|
|
15
|
-
|
|
|
15
|
+
| 字段 | 作用 | 取值 |
|
|
16
16
|
| --- | --- | --- |
|
|
17
|
-
| 界面字体 | `--dsw-font-family`,所有非代码的界面文字 |
|
|
18
|
-
| 代码字体 | `--ds-font-family-code`,代码块、行内代码、等宽文本 |
|
|
17
|
+
| 界面字体 | `--dsw-font-family`,所有非代码的界面文字 | 字体查询:字体族,可带字重 |
|
|
18
|
+
| 代码字体 | `--ds-font-family-code`,代码块、行内代码、等宽文本 | 字体查询 |
|
|
19
19
|
| 界面字号 | 按比例缩放所有写死的界面文字尺寸 | 75% – 150%,步进 5% |
|
|
20
20
|
| 会话正文字号 | 消息正文、标题与表格 | 12 – 20 px |
|
|
21
21
|
| 代码字号 | 代码块与行内代码 | 10 – 20 px |
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
两个字体框都是**一门小型字体查询语言的编辑器**,而不是普通输入框:整条字体栈就是一段可以逐字编辑的文本,带语法高亮、自动换行,以及一份来自本机实际字体的补全列表。字重写在查询**里面**、紧挨着它所属的字体族,所以换字体和换字重是同一处的同一次编辑:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Geist Mono medium, "Zhuque Fangsong (technical preview)", monospace
|
|
27
|
+
└─────┬────┘ └──┬─┘
|
|
28
|
+
字体族 字重
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **顺序就是文本的顺序。** 浏览器能解析到的第一个字体族生效,排序靠编辑文本完成:剪切粘贴,或把光标放进某个词条按 <kbd>Alt</kbd>+<kbd>↑</kbd>/<kbd>↓</kbd>。没有需要拖动的胶囊了。
|
|
32
|
+
- **这段文字归你。** 插件不会改写它:不加引号、不重排、不整理,也不会把清空的框重新填满。新字体就落在光标处——因为你是在那里打的字;把某个字体放到最前面,要么自己敲 `Inter, `,要么在词条开头从补全列表里选。
|
|
33
|
+
- **列表认识你机器上的字体。** 它补全字体族、通用族,以及某个字体族**实际拥有**的字重(从它的字面读取)。本机字体读取不可用或被拒绝时,会退回"探测到的常用字体"名单,并在行上方说明一次;任何名字始终可以手写,目录从不自称完整。
|
|
34
|
+
- **输入框永远是同一个等宽字体。** 高亮层和真正的 textarea 共用一个盒子,而 textarea 没法给不同的字符串设不同字体,所以这个框统一使用一套系统等宽字体、字重固定 400、并关闭连字与字距调整——无论查询里写的是什么字体。带连字的字体(Fira Code 的 `->`)会在高亮层画成一个字形、在输入框里画成两个,合成字重也会两边不一致。框下那行读数用的是这条轴自己的字体,所以你选的字体在那里仍然看得见。
|
|
35
|
+
- **Enter 只负责应用,从不替你补全。** 查询在 <kbd>Enter</kbd> 或离开输入框时解析并写入;<kbd>Tab</kbd>(或点击)才会接受高亮的那条补全——那是唯一会替换文本的情况,因为"选中一条建议"本身就是这个意思。
|
|
36
|
+
- **写错只标出来,不改。** 本机没有的字体族、字体族没有的字面、没闭合的引号、多出来的词,都会在框下分别以警告色或错误色标出,文本保持你写的样子。空输入框**什么都不写**并说明这一点:已保存的字体栈仍在生效,要回到出厂值按「恢复默认」。
|
|
37
|
+
|
|
38
|
+
所有取值都通过宿主设置文档持久化(`$DSH_HOME/settings.yaml`,命名空间 `ui-font`),因此重启后依然生效,并且对指向同一台宿主的所有浏览器共享。
|
|
24
39
|
|
|
25
40
|
*界面字号* 与 *会话正文字号* 刻意设计成两条独立的轴:放大界面可以让周围的控件更好读,而不改变屏幕上能显示多少正文;反过来也一样。
|
|
26
41
|
|
|
42
|
+
### 如何选字重:`Geist Mono medium`
|
|
43
|
+
|
|
44
|
+
CSS 没法把字重写进 `font-family` —— `font-family: "Geist Mono" 500, monospace` 本身就是非法声明,整条字体栈会被丢弃。所以这门查询语言把字重写成一个词,紧跟在它所属的字体族后面,插件再把这一对还原成真正生效的两条声明:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Geist Mono medium, monospace → font-family: "Geist Mono", monospace
|
|
48
|
+
→ font-weight: 500
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`Thin`、`ExtraLight`、`Light`、`Book`、`Regular`、`Medium`、`SemiBold`、`Bold`、`ExtraBold`、`Black` 以及常见别名(`Hairline`、`DemiBold`、`UltraBold` 等)都能识别,大小写不敏感。字重属于整条轴而不是某一个字体族,所以写在哪里都算数,但只算一次;规范形式把它写在第一个字体族(也就是生效的那个)后面。
|
|
52
|
+
|
|
53
|
+
**出厂字重是隐含的**:某条轴停在出厂的 400 时,文本里不会出现任何字重词——每个字体族后面都跟一个 `regular` 是谁也没要求的噪音,而对界面轴来说 400 本来就等于"不做覆盖"。只有真正选了字重,这个词才会出现;把它删掉就回到 400。这条轴当前到底是多少,永远写在输入框下面那一行(`界面字重: 常规 400(出厂值,未覆盖)`),所以省掉那个词并没有藏起任何信息。
|
|
54
|
+
|
|
55
|
+
字重是一个封闭列表而不是任意数字:所选字体族没有的字重会被浏览器**合成**(伪粗体),把伪粗体当成正常选项提供,比不提供更糟。在能读到字面的机器上(Local Font Access API 会报告每个字面),补全列表只给**这个字体族真正拥有**的字重,缺哪个会在框下点出来:
|
|
56
|
+
|
|
57
|
+
> Geist Mono 在本机没有 700 这个字面(bold),浏览器会合成
|
|
58
|
+
|
|
59
|
+
把字重词删掉不会悄悄重置任何东西:这条轴保持它被设成的值,输入框下面那行会写出来;等下次行组件按存储值重建时,那个词也会回来。要回到出厂字重,从列表里选它那一档(`常规 400`)——选中是一条指令,所以即使它的文本只是字体族名,也会把值设成 400。
|
|
60
|
+
|
|
61
|
+
有一处刻意的例外:如果你输入的内容本身就是本机上的字体族,末尾那个词会被当成名字的一部分。`Book Antiqua`、`Franklin Gothic Medium` 都是真实存在的字体族,把它们当成"字体族 + 字重"处理会静默改错。代价是:真正两者皆是的名字(真有一个字体族叫 `Geist Mono Medium`)也会被当作名字。判断依据就是补全列表给出的那份字体目录。想明确表达"这就是名字",加引号即可:`"Book Antiqua"` 永远是名字本身。
|
|
62
|
+
|
|
63
|
+
### 界面字重是可选覆盖
|
|
64
|
+
|
|
65
|
+
代码区域没有需要保留的字重层级,所以它的字重就是查询里写的那个数。界面有:标题 700、表头 500、正文 400。因此设置界面字重时,基准值会带着标题阶梯一起移动,每一级保持它与 400 的出厂间距,而不是被压平:
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
/* --dsh-font-ui-weight: 500 */
|
|
69
|
+
html body { font-weight: 500 }
|
|
70
|
+
--dsh-font-markdown-base: 500 … /* 正文 */
|
|
71
|
+
--dsh-font-markdown-h1: 800 … /* 700 + (500 - 400) */
|
|
72
|
+
--dsh-font-markdown-table-head: 600 … /* 500 + (500 - 400) */
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
会跟着变的是**继承**字重的那部分文字;设计系统自己写死字重的标签、按钮不受影响,因为它们自己的规则仍然胜出。而出厂的 400 **不产生任何规则**,所以未改动的安装画出来的就是设计系统原本的样子。
|
|
76
|
+
|
|
77
|
+
### 这门查询语言
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
query := entry ("," entry)*
|
|
81
|
+
entry := family | weight
|
|
82
|
+
family := '"' … '"' | "'" … "'" | word (space word)*
|
|
83
|
+
weight := thin | extralight | light | book | regular | roman | medium |
|
|
84
|
+
demibold | semibold | bold | extrabold | black | heavy | …
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
语法就是 CSS `font-family` 列表,加上列表本身装不下的那一样东西。只有**未加引号**词条的**最后一个**词可能是字重,而且整个词条本身不是已收录的字体族时才成立。单独一个 `medium` 也表示"只设字重、不改字体族"。引号里的逗号不是分隔符,所以 `"Foo, Bar"` 是一个字体族。
|
|
88
|
+
|
|
89
|
+
解析器刻意宽容——这段文字是人手敲的,不是生成的——凡是放不下的东西都按原样保留并在框下报告,而不是悄悄丢掉:没闭合的引号、引号后面多出来的词、写了两次的字重、本机目录里没有的字体族、这个字体族没有的字面。框下同时会写出当前**生效**的字体族,也就是浏览器真正能用上的第一个。
|
|
90
|
+
|
|
27
91
|
## 安装
|
|
28
92
|
|
|
29
93
|
```sh
|
|
@@ -112,15 +176,49 @@ document.body.style.setProperty('--dsh-content-font-size', `${contentSize}px`)
|
|
|
112
176
|
|
|
113
177
|
样式表随后以这个值为基准,用绝对 px 重新推导整条 Markdown 字号阶梯。变量仍然归 `ui-theme` 所有,它每次主题变化仍会写自己的值;插件在每次设置变化时重新写入自己的值。设置项的说明文字会告诉用户:这里的取值会盖过「外观」里的「字号大小」。
|
|
114
178
|
|
|
179
|
+
### 代码字重写在哪里
|
|
180
|
+
|
|
181
|
+
设计系统里**完全没有**字重 token——界面上每一处 `font:` 都是字面量,代码那条阶梯写死就是 `400`。所以字重没法像字体族那样搭在字体 token 上,它分两层生效:
|
|
182
|
+
|
|
183
|
+
```css
|
|
184
|
+
/* 三个代码 token 在各自的 font: 简写里带上它…… */
|
|
185
|
+
--dsw-font-markdown-code: var(--dsh-font-code-weight,400) var(--dsh-font-code-size,12px) / … ;
|
|
186
|
+
|
|
187
|
+
/* ……而直接用代码字体族写死的地方,按结构匹配 */
|
|
188
|
+
html body pre, html body code, html body [class*="code" i] {
|
|
189
|
+
font-family: var(--ds-font-family-code) !important;
|
|
190
|
+
font-weight: var(--dsh-font-code-weight,400) !important;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
第二条规则的存在,是因为界面上大多数代码根本不读 token:工具 I/O 卡片和终端输出是在组件样式表里用 `font-family: var(--ds-font-family-code)` 加上自己的字面量字重写死的,除了覆盖元素本身没有别的地方可改。`!important` 正是为了压过 `font: 500 12px/18px …`。这条规则刻意**不是**通配规则:周围标签仍保留出厂的字重层级。
|
|
195
|
+
|
|
196
|
+
### 界面字重写在哪里
|
|
197
|
+
|
|
198
|
+
界面字重是**可选**的:出厂值 400 不产生任何规则,所以未改动的安装画出来的就是设计系统原本的样子。设成别的值时,插件做两件事:
|
|
199
|
+
|
|
200
|
+
```css
|
|
201
|
+
/* 会继承字重的界面文字整体移动…… */
|
|
202
|
+
html body { font-weight: 500 }
|
|
203
|
+
|
|
204
|
+
/* ……而插件自己已经接管的那条 Markdown 阶梯按出厂的相对差值整体平移 */
|
|
205
|
+
--dsh-font-markdown-base: 500 …
|
|
206
|
+
--dsh-font-markdown-h1: 800 …
|
|
207
|
+
--dsh-font-markdown-table-head: 600 …
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
第二条必须显式写出字重,因为 `font:` 简写里不带字重分量时会把 `font-weight` 重置成 `normal`——只写 `html body` 会被这条简写悄悄抵消。平移而不是压平,是为了让标题相对正文的粗细关系保持出厂的距离。
|
|
211
|
+
|
|
115
212
|
### 首屏绘制前
|
|
116
213
|
|
|
117
|
-
Node 半边响应 `webserver/index-inject`,注入一段内联 `<script>`,在 shell
|
|
214
|
+
Node 半边响应 `webserver/index-inject`,注入一段内联 `<script>`,在 shell 挂载之前就装好样式表并设好字体族与字重变量,因此第一帧就已经是用户的字体。它在渲染时读取同一个 `ui-font` 设置段;当设置服务缺失时回退到 schema 默认值。
|
|
118
215
|
|
|
119
216
|
## 开发
|
|
120
217
|
|
|
121
218
|
```sh
|
|
122
219
|
npm run build # src/client.js -> lib/client.js
|
|
123
|
-
npm run check #
|
|
220
|
+
npm run check # 发布闸门:产物同步 + host/client 校验
|
|
221
|
+
npm run check:all # 再加上 profile 组合校验(需要本机有 dsh)
|
|
124
222
|
npm run verify # 只跑校验脚本
|
|
125
223
|
npm run watch # 保存即重建,配合 dsh-client-hmr
|
|
126
224
|
```
|
|
@@ -129,6 +227,8 @@ npm run watch # 保存即重建,配合 dsh-client-hmr
|
|
|
129
227
|
|
|
130
228
|
浏览器半边只允许请求 shell 注入模块表的那九个模块(`react`、`react/jsx-runtime`、`react-dom`、`react-dom/client`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-store`、`@deepseek-ai/dsh-client-ui-slots`、`@deepseek-ai/dsh-client-ui-primitives`、`@deepseek-ai/dsh-client-ui-dockkit`);其它模块必须写进 `dsh.client.external` 并作为独立的图节点发布。构建会强制检查这一点。
|
|
131
229
|
|
|
230
|
+
`verify-profile.mjs` 需要本机有 dsh 安装和已初始化的 profile,所以在两者都不存在时会**跳过**(exit 0)——干净的 CI runner 上没有 dsh。设 `DSH_REQUIRE=1` 可以把「跳过」变成失败。
|
|
231
|
+
|
|
132
232
|
要在运行中的宿主上迭代浏览器半边,开一个 watcher,让 `dsh-client-hmr` 热替换插件——它每 500 ms 轮询一次客户端 bundle,因此保存后的重建无需刷新页面即可生效:
|
|
133
233
|
|
|
134
234
|
```sh
|
|
@@ -141,15 +241,22 @@ dsh --profile web
|
|
|
141
241
|
|
|
142
242
|
刷新页面总能拿到新组合的图,所以 watcher 只是便利,不是必需。
|
|
143
243
|
|
|
244
|
+
### 发布
|
|
245
|
+
|
|
246
|
+
发布先由 GitHub Actions 通过 npm trusted publishing 完成**暂存(stage)**,再由维护者审批,因此本仓库里没有 `NPM_TOKEN`,也不应该出现。逐步流程见 [RELEASING.md](RELEASING.md);一次性配置以及这套机制防不住什么见 [PUBLISHING.md](PUBLISHING.md)。
|
|
247
|
+
|
|
144
248
|
## 包结构
|
|
145
249
|
|
|
146
250
|
| 路径 | 作用 |
|
|
147
251
|
| --- | --- |
|
|
148
252
|
| `lib/index.js` | Node 半边:设置命名空间、首屏绘制前的注入。由 loader 加载。 |
|
|
149
|
-
| `lib/client.js` | 浏览器半边,**由 `src/client.js` 生成**。由 `/plugins/dsh-font/client.js` 提供。 |
|
|
253
|
+
| `lib/client.js` | 浏览器半边,**由 `src/client.js` 生成**。由 `/plugins/@citisen/dsh-font/client.js` 提供。 |
|
|
150
254
|
| `src/client.js` | 浏览器半边源码。 |
|
|
151
255
|
| `cordis.patch.yml` | 本 bundle 贡献的 profile 层。 |
|
|
152
256
|
| `scripts/` | 构建与校验脚本。 |
|
|
257
|
+
| `.github/workflows/stage.yml` | 唯一发布路径的 CI 半边。 |
|
|
258
|
+
| `PUBLISHING.md` | trusted publishing 一次性配置,以及这套机制防不住什么。 |
|
|
259
|
+
| `RELEASING.md` | 改完代码之后怎么发布的完整流程。 |
|
|
153
260
|
| `package.json` | 声明 `dsh.bundle`(profile 层)与 `dsh.client`(浏览器节点)。 |
|
|
154
261
|
|
|
155
262
|
## 已知限制
|
|
@@ -158,6 +265,10 @@ dsh --profile web
|
|
|
158
265
|
- **正文字号会盖过 `ui-theme` 的字号设置项。** 两者都写 `--dsh-content-font-size`,后写者生效,而本插件在设置变化时总会写。请只用其中一个。
|
|
159
266
|
- **字体不会被安装。** 只有浏览器或操作系统能解析到的字体名才会生效;插件不打包也不下载 webfont,因此写错名字只会静默回退到列表末尾的通用字体。
|
|
160
267
|
- **逐元素盖章的开销与 DOM 规模成正比。** 缩放扫描对每个元素只测量一次并缓存结果,之后按 DOM 变化增量盖章,因此开销有界——但在超长会话里并非免费。
|
|
268
|
+
- **代码字重不能按区域分别设置。** 它一次作用于所有代码区域,覆盖范围是 `pre`、`code` 以及类名里带 `code` 的元素——因此组件自己在代码块里设的字重(语法高亮、加粗的 diff 行)同样会被覆盖。界面字重同理,只是范围相反:只有会继承字重的那部分界面文字跟着变,设计系统写死字重的标签、按钮保持原样。字体族没有的字重浏览器仍会合成,封闭列表和"本机没有这个字面"的提示只是把它挡在选择之外。
|
|
269
|
+
- **查询语言是本插件自己的约定,不是 CSS。** 它只存在于这一行的两个字体框里,存储的仍然是合法的 CSS `font-family` 列表加上一个 `font-weight` 数字,所以关掉插件不会有残留。末尾的词算字重还是算名字的一部分,由已加载的字体目录决定,所以本身就以字重词结尾的真实字体族(`Book Antiqua`、`Franklin Gothic Medium`)只有在目录里确实列出了这一整名时才会保持原样——这两个词都在探测列表里,因此正常都能保住;而列表之外的同类名字在拒绝字体权限提示时可能被拆开,用引号可以明确表达"这是名字"。
|
|
270
|
+
- **界面字重的平移是固定公式,不是重新设计。** 标题按"出厂字重 +(界面字重 − 400)"平移并封顶在 900,所以界面字重设到 900 时所有阶梯都会撞到同一个上限。
|
|
271
|
+
- **输入框不等于设置值。** 存储的是插件对查询的序列化结果(加了引号、字重词落在生效字体族后面);框里显示的是你的文本,只要这一行还在就原样保留。重新加载页面时会用两个存储值重建输入框,所以你删掉的字重词可能又出现,拼写也可能和你写的不一样。
|
|
161
272
|
- **设置项只有中英两种文案**,与官方内置的语言对一致。
|
|
162
273
|
|
|
163
274
|
## 许可证
|