dsh-fonttune 0.4.1 → 0.5.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/CHANGELOG.en.md +18 -0
- package/CHANGELOG.md +18 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/lib/client.js +569 -32
- package/lib/shared.cjs +241 -10
- 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.5.0] - 2026-10-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Finer weight steps on fonts with a continuous weight range**: for a font that supports one, the weight slider of the conversation, the interface and code goes from a handful of steps to a couple of dozen, and every notch changes the text. Machines whose fonts do not support it keep the steps they had.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Headings shrank when the conversation line height was adjusted**: the axis now moves the spacing alone, leaving the size and the hierarchy of headings and body text untouched.
|
|
12
|
+
- **The floating panel offered different weight steps than the settings card**: both now read and write the same steps and the same range.
|
|
13
|
+
- **The preview box did not match the real conversation**: moving the weight by one step now shows in the preview as well.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **The weight hints no longer describe a change of face per step**: on a font with a continuous weight range that is not what happens, so they now say every notch makes the text a little bolder or lighter.
|
|
17
|
+
|
|
18
|
+
## [0.4.2] - 2026-10-12
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **DSH 0.2.1-alpha.1 and 0.2.1-alpha.2 join the compatibility table**: both were verified live — the settings card, the floating panel's tweaks and their writes, and the plugin's page all behave as documented.
|
|
22
|
+
|
|
5
23
|
## [0.4.1] - 2026-10-04
|
|
6
24
|
|
|
7
25
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.5.0] - 2026-10-13
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- **字重滑块在无级字重的字体上更细**:字体本身支持无级字重时,对话、界面、代码三处的字重滑块从原来的几档变为二十几档,每挪一格文字都有变化;字体不支持的机器上档位与之前相同。
|
|
9
|
+
|
|
10
|
+
### 修复
|
|
11
|
+
- **调整对话行高时标题字号变小**:现在只改变行距,标题与正文的字号与层级保持不变。
|
|
12
|
+
- **浮动面板的字重档位与设置页不一致**:两处显示与写入的档位、范围现在相同。
|
|
13
|
+
- **预览框与真实对话的字体表现不一致**:预览里挪动字重看不出变化的问题不再出现。
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
- **字重提示文字不再描述"每一格都换一张字形"**:在无级字重的字体上这句话不成立,改为说明每挪一格文字都会变粗或变细。
|
|
17
|
+
|
|
18
|
+
## [0.4.2] - 2026-10-12
|
|
19
|
+
|
|
20
|
+
### 变更
|
|
21
|
+
- **兼容对照表列出 DSH 0.2.1-alpha.1 与 0.2.1-alpha.2**:两个新预发布版实测通过,设置卡片、悬浮面板调参与写入、插件页卡片均正常。
|
|
22
|
+
|
|
5
23
|
## [0.4.1] - 2026-10-04
|
|
6
24
|
|
|
7
25
|
### 修复
|
package/README.en.md
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
| Plugin version | Supported DSH versions |
|
|
30
30
|
| --- | --- |
|
|
31
|
-
| **0.
|
|
31
|
+
| **0.5.x** (latest, 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 |
|
|
32
|
+
| 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 |
|
|
32
33
|
| 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 |
|
|
33
34
|
| 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 |
|
|
34
35
|
|
package/README.md
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
| 插件版本 | 支持的 DSH 版本 |
|
|
30
30
|
| --- | --- |
|
|
31
|
-
| **0.
|
|
31
|
+
| **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 |
|
|
32
|
+
| 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 |
|
|
32
33
|
| 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 |
|
|
33
34
|
| 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 |
|
|
34
35
|
|
package/lib/client.js
CHANGED
|
@@ -2338,6 +2338,27 @@ function rebuildShorthand(parts) {
|
|
|
2338
2338
|
* token classification
|
|
2339
2339
|
* ------------------------------------------------------------------ */
|
|
2340
2340
|
|
|
2341
|
+
/**
|
|
2342
|
+
* Whether a CSS selector declares something for the WHOLE page.
|
|
2343
|
+
*
|
|
2344
|
+
* The typography tokens live on `:root`, `html` or `body`. A selector scoped to a class
|
|
2345
|
+
* or an attribute declares a LOCAL value: another plugin's module stylesheet may reuse a
|
|
2346
|
+
* token name for its own compact panel (measured: `dsh-better-sidebar` sets
|
|
2347
|
+
* `--dsw-font-markdown-h1` to `600 13px/18px` on `.…_taskMarkdown`), and reading that as
|
|
2348
|
+
* the base hands the conversation a size that panel picked for itself.
|
|
2349
|
+
* @param {string} selector - a rule's selector text.
|
|
2350
|
+
* @returns {boolean} true when every selector in the list is a root scope.
|
|
2351
|
+
*/
|
|
2352
|
+
function isRootScopeSelector(selector) {
|
|
2353
|
+
if (typeof selector !== "string" || selector.trim() === "") return false;
|
|
2354
|
+
var parts = selector.split(",");
|
|
2355
|
+
for (var index = 0; index < parts.length; index += 1) {
|
|
2356
|
+
var part = parts[index].trim().toLowerCase();
|
|
2357
|
+
if (part !== ":root" && part !== "html" && part !== "body" && part !== "*") return false;
|
|
2358
|
+
}
|
|
2359
|
+
return true;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2341
2362
|
/**
|
|
2342
2363
|
* Match DSH's typography PART tokens: the design system's sizes plus the
|
|
2343
2364
|
* content size the theme plugin writes on `body` (whose secondary variant
|
|
@@ -3178,6 +3199,180 @@ function weightStepRange(profile, value) {
|
|
|
3178
3199
|
return { min: min, max: max, unit: unit, value: count };
|
|
3179
3200
|
}
|
|
3180
3201
|
|
|
3202
|
+
/* ------------------------------------------------------------------ *
|
|
3203
|
+
* continuous weight (FT-060)
|
|
3204
|
+
* ------------------------------------------------------------------ */
|
|
3205
|
+
|
|
3206
|
+
/** Prefix of the alias family a continuous family is drawn through. */
|
|
3207
|
+
var VARIABLE_ALIAS_PREFIX = "dfp-vf-";
|
|
3208
|
+
|
|
3209
|
+
/**
|
|
3210
|
+
* The weights the continuity probe lays out. Dense on purpose: a variable font
|
|
3211
|
+
* that moves in small steps shows a width change at every one of them, while a
|
|
3212
|
+
* family that snaps to named instances repeats its width across each stretch.
|
|
3213
|
+
*/
|
|
3214
|
+
var CONTINUITY_WEIGHTS = [400, 420, 440, 460, 480, 500, 550, 600, 650, 700, 750, 800, 900];
|
|
3215
|
+
|
|
3216
|
+
/**
|
|
3217
|
+
* The share of adjacent probe pairs that must differ before a family's laid-out
|
|
3218
|
+
* WIDTH counts as continuous. Measured: a variable family drawn through its alias
|
|
3219
|
+
* changes width at every sampled weight (share 1.0), while a family snapping to
|
|
3220
|
+
* named instances repeats its width across each stretch (share 0.25–0.42).
|
|
3221
|
+
*/
|
|
3222
|
+
var CONTINUOUS_MIN_SHARE = 0.9;
|
|
3223
|
+
|
|
3224
|
+
/**
|
|
3225
|
+
* The notches offered for a continuous family, finest first.
|
|
3226
|
+
*
|
|
3227
|
+
* The shape is still measured (the alias is drawn on the same weight grid as any
|
|
3228
|
+
* other family), but the candidates are fine: a continuous axis can answer at every
|
|
3229
|
+
* one of them, which is the whole point of the mode. `weightProfileFrom` walks this
|
|
3230
|
+
* list from the END, so the finest candidate that still yields enough visible
|
|
3231
|
+
* positions wins.
|
|
3232
|
+
*/
|
|
3233
|
+
var CONTINUOUS_CANDIDATES = [10, 20, 25];
|
|
3234
|
+
|
|
3235
|
+
/**
|
|
3236
|
+
* The nominal continuous slider, used when the measured shape cannot be taken
|
|
3237
|
+
* (no canvas, a refused probe). One notch is 20 weight units; the range is the
|
|
3238
|
+
* widest offset the ceiling allows either way.
|
|
3239
|
+
*/
|
|
3240
|
+
var CONTINUOUS_STEP = 20;
|
|
3241
|
+
var CONTINUOUS_RANGE = 200;
|
|
3242
|
+
|
|
3243
|
+
/**
|
|
3244
|
+
* How many different values a list of measurements holds.
|
|
3245
|
+
* @param {number[]} values - the measurements.
|
|
3246
|
+
* @returns {number} the count of distinct values.
|
|
3247
|
+
*/
|
|
3248
|
+
function distinctCount(values) {
|
|
3249
|
+
if (!Array.isArray(values)) return 0;
|
|
3250
|
+
var seen = [];
|
|
3251
|
+
for (var index = 0; index < values.length; index += 1) {
|
|
3252
|
+
if (seen.indexOf(values[index]) < 0) seen.push(values[index]);
|
|
3253
|
+
}
|
|
3254
|
+
return seen.length;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
/**
|
|
3258
|
+
* Does the same family answer more finely through its alias than through itself?
|
|
3259
|
+
*
|
|
3260
|
+
* This is the discriminator the continuity probe trusts, and it needs no threshold:
|
|
3261
|
+
* a family that snaps to named instances renders IDENTICALLY either way, so both
|
|
3262
|
+
* responses hold the same number of distinct values; a variable family drawn through
|
|
3263
|
+
* the alias moves at weights where the system path already stopped. Comparing the
|
|
3264
|
+
* family against ITSELF also keeps the answer honest for a narrow axis, where a
|
|
3265
|
+
* share-of-neighbours rule would read saturation as "static".
|
|
3266
|
+
* @param {number[]} throughAlias - measurements taken through the alias face.
|
|
3267
|
+
* @param {number[]} plain - the same measurements taken through the family itself.
|
|
3268
|
+
* @returns {boolean} true when the alias resolves finer detail.
|
|
3269
|
+
*/
|
|
3270
|
+
function respondsFinerThan(throughAlias, plain) {
|
|
3271
|
+
if (!Array.isArray(throughAlias) || !Array.isArray(plain)) return false;
|
|
3272
|
+
var fine = distinctCount(throughAlias);
|
|
3273
|
+
return fine >= 3 && fine > distinctCount(plain);
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
|
|
3277
|
+
/**
|
|
3278
|
+
* The alias family name for one family. A hash, not the family itself, so the
|
|
3279
|
+
* name is a plain identifier whatever the family is called.
|
|
3280
|
+
* @param {string} family - a family name.
|
|
3281
|
+
* @returns {string} the alias, stable across sessions.
|
|
3282
|
+
*/
|
|
3283
|
+
function variableAliasFor(family) {
|
|
3284
|
+
var text = sanitizeFamily(family);
|
|
3285
|
+
var hash = 0;
|
|
3286
|
+
for (var index = 0; index < text.length; index += 1) {
|
|
3287
|
+
hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
|
|
3288
|
+
}
|
|
3289
|
+
return VARIABLE_ALIAS_PREFIX + hash.toString(36);
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
/**
|
|
3293
|
+
* The `@font-face` rule that draws one family through its own variable axis.
|
|
3294
|
+
*
|
|
3295
|
+
* `local()` points the face at the installed family, and the weight RANGE is the
|
|
3296
|
+
* part that matters: it tells the engine the family is continuous, so `font-weight`
|
|
3297
|
+
* picks a point on the axis instead of the nearest named instance. (Without the
|
|
3298
|
+
* range the same family snaps; `font-variation-settings` does not help a system
|
|
3299
|
+
* family at all — measured, see FT-060.)
|
|
3300
|
+
* @param {string} family - the installed family name.
|
|
3301
|
+
* @param {string} alias - the alias from `variableAliasFor`.
|
|
3302
|
+
* @returns {string} one CSS rule.
|
|
3303
|
+
*/
|
|
3304
|
+
function variableFaceRule(family, alias) {
|
|
3305
|
+
var text = sanitizeFamily(family).replace(/"/g, "");
|
|
3306
|
+
return '@font-face{font-family:"' + alias + '";src:local("' + text + '");font-weight:100 900;}';
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
/**
|
|
3310
|
+
* Every `@font-face` rule for a map of family → alias.
|
|
3311
|
+
* @param {Record<string, string>} variable - the families that measured continuous.
|
|
3312
|
+
* @returns {string} the rules, one per line; "" when there are none.
|
|
3313
|
+
*/
|
|
3314
|
+
function buildVariableFaces(variable) {
|
|
3315
|
+
var rules = [];
|
|
3316
|
+
if (variable === null || typeof variable !== "object") return "";
|
|
3317
|
+
for (var family in variable) {
|
|
3318
|
+
if (Object.prototype.hasOwnProperty.call(variable, family)) {
|
|
3319
|
+
rules.push(variableFaceRule(family, variable[family]));
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return rules.join("\n");
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
/**
|
|
3326
|
+
* Put the alias IN FRONT of every continuous family a stack names.
|
|
3327
|
+
*
|
|
3328
|
+
* The family stays in the list after its alias, so a glyph the alias lacks still
|
|
3329
|
+
* falls through to the same face the reader chose, and a stack that names no
|
|
3330
|
+
* continuous family is returned unchanged.
|
|
3331
|
+
* @param {string} stack - a formatted font-family value.
|
|
3332
|
+
* @param {Record<string, string>} variable - family → alias map.
|
|
3333
|
+
* @returns {string} the stack, with aliases prepended where they apply.
|
|
3334
|
+
*/
|
|
3335
|
+
function withVariableFaces(stack, variable) {
|
|
3336
|
+
if (typeof stack !== "string" || stack === "" || variable === null || typeof variable !== "object") {
|
|
3337
|
+
return stack;
|
|
3338
|
+
}
|
|
3339
|
+
// The stack may already carry its alias — a family read back from the page's own
|
|
3340
|
+
// computed style does, once the mode is on. Prepending it again would stack up a
|
|
3341
|
+
// duplicate on every rebuild.
|
|
3342
|
+
var names = parseStack(stack);
|
|
3343
|
+
var lead = names.length > 0 ? names[0] : "";
|
|
3344
|
+
var prefix = [];
|
|
3345
|
+
for (var family in variable) {
|
|
3346
|
+
if (!Object.prototype.hasOwnProperty.call(variable, family)) continue;
|
|
3347
|
+
if (stack.indexOf(quoteFamily(family)) < 0) continue;
|
|
3348
|
+
if (lead === variable[family]) continue;
|
|
3349
|
+
prefix.push('"' + variable[family] + '"');
|
|
3350
|
+
}
|
|
3351
|
+
return prefix.length === 0 ? stack : prefix.join(", ") + ", " + stack;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
/**
|
|
3355
|
+
* Did a family respond continuously to weight in the probe?
|
|
3356
|
+
* @param {number[]} widths - laid-out widths, in `CONTINUITY_WEIGHTS` order.
|
|
3357
|
+
* @returns {boolean} true when enough neighbours differ.
|
|
3358
|
+
*/
|
|
3359
|
+
function isContinuousResponse(widths) {
|
|
3360
|
+
if (!Array.isArray(widths) || widths.length < 3) return false;
|
|
3361
|
+
var changes = 0;
|
|
3362
|
+
for (var index = 1; index < widths.length; index += 1) {
|
|
3363
|
+
if (widths[index] !== widths[index - 1]) changes += 1;
|
|
3364
|
+
}
|
|
3365
|
+
return changes / (widths.length - 1) >= CONTINUOUS_MIN_SHARE;
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
/**
|
|
3369
|
+
* The slider shape for a continuous family: the finest notch, centred on 0.
|
|
3370
|
+
* @returns {{min: number, max: number, step: number}} the profile, in weight units.
|
|
3371
|
+
*/
|
|
3372
|
+
function continuousWeightProfile() {
|
|
3373
|
+
return { min: -CONTINUOUS_RANGE, max: CONTINUOUS_RANGE, step: CONTINUOUS_STEP };
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3181
3376
|
/**
|
|
3182
3377
|
* What a slider's local (unconfirmed) value should do when a value arrives from
|
|
3183
3378
|
* the settings document.
|
|
@@ -3265,10 +3460,12 @@ function isDormant(config) {
|
|
|
3265
3460
|
* @param {boolean} isDark - prefix every rule with the dark-theme attribute.
|
|
3266
3461
|
* @returns {string} declarations for one `<style>` element.
|
|
3267
3462
|
*/
|
|
3268
|
-
function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
var
|
|
3463
|
+
function buildAxisCss(axis, baseTokens, isDark, hints, variable) {
|
|
3464
|
+
// A continuous family leads its stack with its alias (see `withVariableFaces`);
|
|
3465
|
+
// with no map, every stack stays exactly as written.
|
|
3466
|
+
var sans = withVariableFaces(formatStack(parseStack(axis[SANS_FIELD])), variable);
|
|
3467
|
+
var mono = withVariableFaces(formatStack(parseStack(axis[MONO_FIELD])), variable);
|
|
3468
|
+
var dialog = withVariableFaces(formatStack(parseStack(axis[STACK_DIALOG_FIELD])), variable);
|
|
3272
3469
|
// Normalised once: an expanded axis always carries a number, but a caller
|
|
3273
3470
|
// handing in a raw object may leave the field out, and "absent" has to mean
|
|
3274
3471
|
// "unset" rather than "write the string undefined". The conversation's value
|
|
@@ -3668,7 +3865,28 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
3668
3865
|
if (noBold) declarations.push(bodyRule("font-synthesis-weight:none !important"));
|
|
3669
3866
|
}
|
|
3670
3867
|
|
|
3671
|
-
return declarations.join("\n");
|
|
3868
|
+
return hardenDeltaRefs(declarations.join("\n"));
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
/**
|
|
3872
|
+
* Give every reference to the content-size delta a fallback value.
|
|
3873
|
+
*
|
|
3874
|
+
* A heading's `font` shorthand is the markdown token, and DSH's own text for it reads
|
|
3875
|
+
* `calc(21px + var(--dsh-content-font-delta))` — a reference with NO fallback. Nothing
|
|
3876
|
+
* outside this plugin defines that property, so any rebuild that emits the reference
|
|
3877
|
+
* without the definition leaves it resolving to nothing, which makes the WHOLE shorthand
|
|
3878
|
+
* invalid at computed-value time: every font longhand falls back to `inherit`, and the
|
|
3879
|
+
* heading is painted at body size. Measured on a live page: h1 23px → 16px the moment the
|
|
3880
|
+
* property resolved to nothing, back to 23px when it returned.
|
|
3881
|
+
*
|
|
3882
|
+
* The fallback cannot change a page that defines the property (the definition wins), and
|
|
3883
|
+
* where the definition is missing it keeps the design's own size instead of collapsing.
|
|
3884
|
+
* @param {string} css - generated declarations.
|
|
3885
|
+
* @returns {string} the same declarations, with the reference made safe.
|
|
3886
|
+
*/
|
|
3887
|
+
function hardenDeltaRefs(css) {
|
|
3888
|
+
if (typeof css !== "string" || css === "") return css;
|
|
3889
|
+
return css.split("var(--dsh-content-font-delta)").join("var(--dsh-content-font-delta, 0px)");
|
|
3672
3890
|
}
|
|
3673
3891
|
|
|
3674
3892
|
/**
|
|
@@ -3760,7 +3978,7 @@ function scaleCodeTokens(baseTokens, scale, lineOffset) {
|
|
|
3760
3978
|
* correct everywhere and costs an attribute scan per element per rule.
|
|
3761
3979
|
* @returns {string} declarations for one `<style>` element ("" when dormant).
|
|
3762
3980
|
*/
|
|
3763
|
-
function buildFontCss(config, baseTokens, hints) {
|
|
3981
|
+
function buildFontCss(config, baseTokens, hints, variable) {
|
|
3764
3982
|
var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
|
|
3765
3983
|
var sets = resolveAxes(config);
|
|
3766
3984
|
// The rendering preference is NOT an axis: it must paint even when every
|
|
@@ -3768,16 +3986,16 @@ function buildFontCss(config, baseTokens, hints) {
|
|
|
3768
3986
|
// use case with everything else untouched.
|
|
3769
3987
|
var smoothing = smoothingRule(config !== null && typeof config === "object" ? config[SMOOTHING_FIELD] : "");
|
|
3770
3988
|
if (isDormant(sets.light) && isDormant(sets.dark)) return smoothing;
|
|
3771
|
-
var light = buildAxisCss(sets.light, tokens, false, hints);
|
|
3989
|
+
var light = buildAxisCss(sets.light, tokens, false, hints, variable);
|
|
3772
3990
|
if (sameValueSet(sets.light, sets.dark)) {
|
|
3773
|
-
return smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing;
|
|
3991
|
+
return hardenDeltaRefs(smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing);
|
|
3774
3992
|
}
|
|
3775
|
-
var dark = buildAxisCss(sets.dark, tokens, true, hints);
|
|
3993
|
+
var dark = buildAxisCss(sets.dark, tokens, true, hints, variable);
|
|
3776
3994
|
var parts = [];
|
|
3777
3995
|
if (light !== "") parts.push(light);
|
|
3778
3996
|
if (dark !== "") parts.push(dark);
|
|
3779
3997
|
if (smoothing !== "") parts.push(smoothing);
|
|
3780
|
-
return parts.join("\n");
|
|
3998
|
+
return hardenDeltaRefs(parts.join("\n"));
|
|
3781
3999
|
}
|
|
3782
4000
|
|
|
3783
4001
|
/**
|
|
@@ -4299,6 +4517,19 @@ var shared = {
|
|
|
4299
4517
|
WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
|
|
4300
4518
|
WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
|
|
4301
4519
|
WEIGHT_MIN_POSITIONS: WEIGHT_MIN_POSITIONS,
|
|
4520
|
+
VARIABLE_ALIAS_PREFIX: VARIABLE_ALIAS_PREFIX,
|
|
4521
|
+
CONTINUITY_WEIGHTS: CONTINUITY_WEIGHTS,
|
|
4522
|
+
isRootScopeSelector: isRootScopeSelector,
|
|
4523
|
+
hardenDeltaRefs: hardenDeltaRefs,
|
|
4524
|
+
CONTINUOUS_CANDIDATES: CONTINUOUS_CANDIDATES,
|
|
4525
|
+
distinctCount: distinctCount,
|
|
4526
|
+
respondsFinerThan: respondsFinerThan,
|
|
4527
|
+
variableAliasFor: variableAliasFor,
|
|
4528
|
+
variableFaceRule: variableFaceRule,
|
|
4529
|
+
buildVariableFaces: buildVariableFaces,
|
|
4530
|
+
withVariableFaces: withVariableFaces,
|
|
4531
|
+
isContinuousResponse: isContinuousResponse,
|
|
4532
|
+
continuousWeightProfile: continuousWeightProfile,
|
|
4302
4533
|
WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
|
|
4303
4534
|
WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
|
|
4304
4535
|
WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
|
|
@@ -4640,13 +4871,13 @@ var DICTS = {
|
|
|
4640
4871
|
|
|
4641
4872
|
"weight.uiLabel": "Interface font-weight offset",
|
|
4642
4873
|
"weight.uiHint":
|
|
4643
|
-
"Adds to the weight of the whole interface (sidebars, settings, buttons, headings): a positive value is bolder, a negative one lighter. The conversation keeps its own. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch
|
|
4874
|
+
"Adds to the weight of the whole interface (sidebars, settings, buttons, headings): a positive value is bolder, a negative one lighter. The conversation keeps its own. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch makes body text and headings a little bolder or lighter, and another font offers a different number of steps. The step is measured for the current font, and its smallest step is the one that looks weakest.",
|
|
4644
4875
|
"weight.dialogLabel": "Conversation font-weight offset",
|
|
4645
4876
|
"weight.dialogHint":
|
|
4646
|
-
"Adds to the weight of each conversation element, so headings stay bolder than body text and bold text stays bold. Unset keeps DSH's own weights. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch
|
|
4877
|
+
"Adds to the weight of each conversation element, so headings stay bolder than body text and bold text stays bold. Unset keeps DSH's own weights. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch makes body text and headings a little bolder or lighter, and another font offers a different number of steps. The step is measured for the current font, and its smallest step is the one that looks weakest.",
|
|
4647
4878
|
"weight.codeLabel": "Code font-weight offset",
|
|
4648
4879
|
"weight.codeHint":
|
|
4649
|
-
"Adds to the weight of each code surface (code blocks, inline code, terminal output); 0 or unset keeps DSH's own weight. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch
|
|
4880
|
+
"Adds to the weight of each code surface (code blocks, inline code, terminal output); 0 or unset keeps DSH's own weight. The slider is divided into the weights the current font can actually render, so the readout counts STEPS (0 = leave DSH alone); every notch makes body text and headings a little bolder or lighter, and another font offers a different number of steps. The step is measured for the current font, and its smallest step is the one that looks weakest.",
|
|
4650
4881
|
|
|
4651
4882
|
"line.dialogLabel": "Conversation line height",
|
|
4652
4883
|
"line.dialogHint":
|
|
@@ -4802,13 +5033,13 @@ var DICTS = {
|
|
|
4802
5033
|
|
|
4803
5034
|
"weight.uiLabel": "界面字重偏移",
|
|
4804
5035
|
"weight.uiHint":
|
|
4805
|
-
"在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
5036
|
+
"在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会变粗或变细;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
4806
5037
|
"weight.dialogLabel": "对话字重偏移",
|
|
4807
5038
|
"weight.dialogHint":
|
|
4808
|
-
"在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
5039
|
+
"在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会变粗或变细;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
4809
5040
|
"weight.codeLabel": "代码字重偏移",
|
|
4810
5041
|
"weight.codeHint":
|
|
4811
|
-
"在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
5042
|
+
"在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会变粗或变细;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
4812
5043
|
|
|
4813
5044
|
"line.dialogLabel": "对话行高",
|
|
4814
5045
|
"line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
|
|
@@ -5223,6 +5454,24 @@ function isOwnSheet(sheet) {
|
|
|
5223
5454
|
}
|
|
5224
5455
|
}
|
|
5225
5456
|
|
|
5457
|
+
/**
|
|
5458
|
+
* Whether a style rule declares its custom properties for the WHOLE page.
|
|
5459
|
+
*
|
|
5460
|
+
* The design tokens live on `:root`, `html` or `body`. A rule scoped to a class
|
|
5461
|
+
* declares a LOCAL value — another plugin's module stylesheet may reuse one of the
|
|
5462
|
+
* token names for its own compact panel, and reading that as the base hands the
|
|
5463
|
+
* conversation a size that panel chose for itself.
|
|
5464
|
+
* @param {CSSStyleRule} rule - any rule from a document stylesheet.
|
|
5465
|
+
* @returns {boolean} true when every selector in the rule is a root scope.
|
|
5466
|
+
*/
|
|
5467
|
+
function isRootScopedRule(rule) {
|
|
5468
|
+
try {
|
|
5469
|
+
return shared.isRootScopeSelector(rule.selectorText);
|
|
5470
|
+
} catch (error) {
|
|
5471
|
+
return false;
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5226
5475
|
/**
|
|
5227
5476
|
* Snapshot the typography tokens the size offsets scale.
|
|
5228
5477
|
*
|
|
@@ -5238,6 +5487,13 @@ function isOwnSheet(sheet) {
|
|
|
5238
5487
|
* collected too: they are what the shipped stylesheets actually consume, and
|
|
5239
5488
|
* the line-height axes rebuild their inline heights.
|
|
5240
5489
|
*
|
|
5490
|
+
* Declarations come in two ranks, because a token name is not owned by the design
|
|
5491
|
+
* system alone: `dsh-better-sidebar` redefines `--dsw-font-markdown-h1` as a compact
|
|
5492
|
+
* `600 13px/18px` on its own panel class, and reading that as the base is how every
|
|
5493
|
+
* conversation heading shrank once the line-height axis rebuilt those shorthands. A
|
|
5494
|
+
* ROOT-scoped declaration is the page's value; a locally scoped one only fills a gap
|
|
5495
|
+
* (a host line that declares its tokens nowhere else still gets them).
|
|
5496
|
+
*
|
|
5241
5497
|
* @returns {Record<string, string>} token name to its untouched value.
|
|
5242
5498
|
*/
|
|
5243
5499
|
function readBaseTokens() {
|
|
@@ -5252,6 +5508,11 @@ function readBaseTokens() {
|
|
|
5252
5508
|
|
|
5253
5509
|
try {
|
|
5254
5510
|
var sheets = document.styleSheets;
|
|
5511
|
+
// A local (class-scoped) declaration is kept APART and only fills gaps: another
|
|
5512
|
+
// plugin may reuse a token name for its own panel, so it must never override the
|
|
5513
|
+
// page's own value — but a host line that declares its tokens nowhere else still
|
|
5514
|
+
// needs them. Root-scoped declarations always win.
|
|
5515
|
+
var local = {};
|
|
5255
5516
|
for (var sheetIndex = 0; sheetIndex < sheets.length; sheetIndex += 1) {
|
|
5256
5517
|
if (isOwnSheet(sheets[sheetIndex])) continue;
|
|
5257
5518
|
var rules = null;
|
|
@@ -5262,16 +5523,24 @@ function readBaseTokens() {
|
|
|
5262
5523
|
}
|
|
5263
5524
|
if (!rules) continue;
|
|
5264
5525
|
for (var ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
|
|
5265
|
-
var
|
|
5526
|
+
var rule = rules[ruleIndex];
|
|
5527
|
+
var style = rule.style;
|
|
5266
5528
|
if (!style) continue;
|
|
5529
|
+
var rootScoped = isRootScopedRule(rule);
|
|
5267
5530
|
for (var propertyIndex = 0; propertyIndex < style.length; propertyIndex += 1) {
|
|
5268
5531
|
var property = style[propertyIndex];
|
|
5269
5532
|
if (property.slice(0, 2) !== "--" || !isScaledToken(property)) continue;
|
|
5270
5533
|
var declared = style.getPropertyValue(property).trim();
|
|
5271
|
-
if (declared
|
|
5534
|
+
if (declared === "") continue;
|
|
5535
|
+
if (rootScoped) out[property] = declared;
|
|
5536
|
+
else local[property] = declared;
|
|
5272
5537
|
}
|
|
5273
5538
|
}
|
|
5274
5539
|
}
|
|
5540
|
+
for (name in local) {
|
|
5541
|
+
if (!Object.prototype.hasOwnProperty.call(local, name)) continue;
|
|
5542
|
+
if (!Object.prototype.hasOwnProperty.call(out, name)) out[name] = local[name];
|
|
5543
|
+
}
|
|
5275
5544
|
} catch (error) {
|
|
5276
5545
|
// Keep what the earlier sources produced.
|
|
5277
5546
|
}
|
|
@@ -5345,7 +5614,11 @@ function createStylesheet(tokens) {
|
|
|
5345
5614
|
var lastCss = null;
|
|
5346
5615
|
return function apply(config) {
|
|
5347
5616
|
if (typeof document === "undefined") return;
|
|
5348
|
-
|
|
5617
|
+
// Continuous families (FT-060) bring their own faces: the aliases are declared
|
|
5618
|
+
// here, and `buildFontCss` leads the matching stacks with them.
|
|
5619
|
+
var faces = shared.buildVariableFaces(variableFamilies);
|
|
5620
|
+
var body = buildFontCss(config, tokens(), harvestScope(), variableFamilies);
|
|
5621
|
+
var css = faces === "" ? body : faces + "\n" + body;
|
|
5349
5622
|
if (css === lastCss && tag !== null && tag.isConnected) return;
|
|
5350
5623
|
lastCss = css;
|
|
5351
5624
|
if (tag === null || !tag.isConnected) {
|
|
@@ -5748,6 +6021,21 @@ var context = weightProbeCanvas.getContext("2d", { willReadFrequently: true });
|
|
|
5748
6021
|
return ink + "/" + advance;
|
|
5749
6022
|
}
|
|
5750
6023
|
|
|
6024
|
+
/**
|
|
6025
|
+
* The ink share of a weight signature, as a number.
|
|
6026
|
+
*
|
|
6027
|
+
* `sampleWeightSignature` answers "<ink>/<advance>": the advance is the laid-out
|
|
6028
|
+
* width, which a monospace family holds constant, so ink is the signal that still
|
|
6029
|
+
* moves for those. Unreadable signatures answer 0, which simply never counts as a
|
|
6030
|
+
* change.
|
|
6031
|
+
* @param {string} signature - a signature from `sampleWeightSignature`.
|
|
6032
|
+
* @returns {number} the summed alpha, or 0.
|
|
6033
|
+
*/
|
|
6034
|
+
function inkOfSignature(signature) {
|
|
6035
|
+
var ink = Number(String(signature).split("/")[0]);
|
|
6036
|
+
return isFinite(ink) ? ink : 0;
|
|
6037
|
+
}
|
|
6038
|
+
|
|
5751
6039
|
/**
|
|
5752
6040
|
* The slider shape to use for one family: its range and its notch.
|
|
5753
6041
|
*
|
|
@@ -5764,18 +6052,40 @@ var context = weightProbeCanvas.getContext("2d", { willReadFrequently: true });
|
|
|
5764
6052
|
*/
|
|
5765
6053
|
function weightProfileFor(stack, top) {
|
|
5766
6054
|
var base = typeof top === "number" ? top : shared.WEIGHT_LADDER_TOP;
|
|
5767
|
-
var
|
|
6055
|
+
var resolved = String(stack) === "" ? liveFamily() : String(stack);
|
|
6056
|
+
// `hasOwnProperty` rather than a truthiness test: the alias map is keyed by family
|
|
6057
|
+
// name, and a family called "constructor" must not read as promoted.
|
|
6058
|
+
var promoted = Object.prototype.hasOwnProperty.call(variableFamilies, primaryFamilyOf(resolved));
|
|
6059
|
+
var alias = promoted ? variableFamilies[primaryFamilyOf(resolved)] : "";
|
|
6060
|
+
// The promotion state is part of the key. The warm-up measures a family BEFORE the
|
|
6061
|
+
// probe has finished (the probe awaits a font load), so without it the stepped shape
|
|
6062
|
+
// measured first would be served from the cache forever after the family was
|
|
6063
|
+
// promoted — the card then keeps offering the coarse notches the mode replaced.
|
|
6064
|
+
var key = String(stack) + "\u0000" + base + "\u0000" + (promoted ? "continuous" : "stepped");
|
|
5768
6065
|
if (Object.prototype.hasOwnProperty.call(weightProfileCache, key)) {
|
|
5769
6066
|
return weightProfileCache[key];
|
|
5770
6067
|
}
|
|
5771
|
-
var profile =
|
|
6068
|
+
var profile = promoted
|
|
6069
|
+
? shared.continuousWeightProfile()
|
|
6070
|
+
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
5772
6071
|
try {
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
6072
|
+
// A continuous family is measured THROUGH ITS ALIAS, on the fine candidate grid:
|
|
6073
|
+
// the axis answers at every notch, so the finest one that still moves body text
|
|
6074
|
+
// wins, and the range stops where the family's own axis does instead of running
|
|
6075
|
+
// past it into weights that render the same.
|
|
6076
|
+
var target = promoted ? '"' + alias + '"' : resolved;
|
|
6077
|
+
var seen = {};
|
|
6078
|
+
var sample = function (weight) {
|
|
6079
|
+
if (!Object.prototype.hasOwnProperty.call(seen, weight)) {
|
|
6080
|
+
seen[weight] = sampleWeightSignature(target, weight);
|
|
6081
|
+
}
|
|
6082
|
+
return seen[weight];
|
|
6083
|
+
};
|
|
6084
|
+
profile = weightProfileFrom(sample, promoted ? { top: base, candidates: shared.CONTINUOUS_CANDIDATES } : { top: base });
|
|
5777
6085
|
} catch (error) {
|
|
5778
|
-
profile =
|
|
6086
|
+
profile = promoted
|
|
6087
|
+
? shared.continuousWeightProfile()
|
|
6088
|
+
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
5779
6089
|
}
|
|
5780
6090
|
weightProfileCache[key] = profile;
|
|
5781
6091
|
return profile;
|
|
@@ -5837,14 +6147,17 @@ var scheduleWeightWarm = function (editing) {
|
|
|
5837
6147
|
warmSignature = signature;
|
|
5838
6148
|
var jobs = [
|
|
5839
6149
|
function () {
|
|
6150
|
+
probeContinuity(stacks.dialog);
|
|
5840
6151
|
weightProfileFor(stacks.dialog, shared.WEIGHT_LADDER_STRONG);
|
|
5841
6152
|
probeLog("weight-warm", { axis: "dialog", stack: stacks.dialog });
|
|
5842
6153
|
},
|
|
5843
6154
|
function () {
|
|
6155
|
+
probeContinuity(stacks.ui);
|
|
5844
6156
|
weightProfileFor(stacks.ui, shared.WEIGHT_LADDER_STRONG);
|
|
5845
6157
|
probeLog("weight-warm", { axis: "ui", stack: stacks.ui });
|
|
5846
6158
|
},
|
|
5847
6159
|
function () {
|
|
6160
|
+
probeContinuity(stacks.code);
|
|
5848
6161
|
weightProfileFor(stacks.code, shared.WEIGHT_BASE);
|
|
5849
6162
|
probeLog("weight-warm", { axis: "code", stack: stacks.code });
|
|
5850
6163
|
},
|
|
@@ -5858,6 +6171,201 @@ var scheduleWeightWarm = function (editing) {
|
|
|
5858
6171
|
deferWeightWarm(runNext);
|
|
5859
6172
|
};
|
|
5860
6173
|
|
|
6174
|
+
/**
|
|
6175
|
+
* Families measured as continuous, family → alias (see FT-060).
|
|
6176
|
+
*
|
|
6177
|
+
* Filled by `probeContinuity` once a family has proven, in THIS page's own
|
|
6178
|
+
* rendering, that it moves in small steps through its variable axis. A family
|
|
6179
|
+
* that is not in here keeps the measured notch shape it always had.
|
|
6180
|
+
*/
|
|
6181
|
+
var variableFamilies = {};
|
|
6182
|
+
var continuityProbed = {};
|
|
6183
|
+
|
|
6184
|
+
/**
|
|
6185
|
+
* What to re-render when a family is promoted.
|
|
6186
|
+
*
|
|
6187
|
+
* The promotion lands a second or so after load, while the floating panel may already
|
|
6188
|
+
* be on screen with its weight row built from the stepped shape. Rebuilding the
|
|
6189
|
+
* stylesheet is not enough — the row caches the shape it was rendered with, so the
|
|
6190
|
+
* panel would keep offering the coarse notches until something else happened to
|
|
6191
|
+
* redraw it. Each mounted panel pushes its own render here and takes it back on
|
|
6192
|
+
* destroy.
|
|
6193
|
+
*/
|
|
6194
|
+
var promotionListeners = [];
|
|
6195
|
+
|
|
6196
|
+
/** Redraw whatever is showing a weight shape that the promotion just changed. */
|
|
6197
|
+
function notifyPromoted() {
|
|
6198
|
+
var pending = promotionListeners.slice();
|
|
6199
|
+
for (var index = 0; index < pending.length; index += 1) {
|
|
6200
|
+
try {
|
|
6201
|
+
pending[index]();
|
|
6202
|
+
} catch (error) {
|
|
6203
|
+
// A listener must never break the probe.
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
/**
|
|
6209
|
+
* A stamp for the build the page is running.
|
|
6210
|
+
*
|
|
6211
|
+
* The client script is cached, and "I refreshed" has been wrong before: this is what
|
|
6212
|
+
* a live page reports about itself (the panel host carries it as `data-dfp-build`).
|
|
6213
|
+
*/
|
|
6214
|
+
var CLIENT_BUILD = "2026-10-12-3";
|
|
6215
|
+
|
|
6216
|
+
/** The families this page promoted to continuous weight, in map order. */
|
|
6217
|
+
function continuousNames() {
|
|
6218
|
+
return Object.keys(variableFamilies);
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
/**
|
|
6222
|
+
* Publish the promoted families on the panel host, when one is on screen.
|
|
6223
|
+
*
|
|
6224
|
+
* Promotion happens AFTER the first render (the probe awaits a font load), so the
|
|
6225
|
+
* attribute set during render is re-stamped here: the host element is then the one
|
|
6226
|
+
* place a reader — or a support session — can look without opening the console.
|
|
6227
|
+
*/
|
|
6228
|
+
function announceContinuous() {
|
|
6229
|
+
if (typeof document === "undefined") return;
|
|
6230
|
+
try {
|
|
6231
|
+
var nodes = document.querySelectorAll("[data-dfp-build]");
|
|
6232
|
+
var names = continuousNames().join(", ");
|
|
6233
|
+
for (var index = 0; index < nodes.length; index += 1) {
|
|
6234
|
+
if (names === "") nodes[index].removeAttribute("data-dfp-continuous");
|
|
6235
|
+
else nodes[index].setAttribute("data-dfp-continuous", names);
|
|
6236
|
+
}
|
|
6237
|
+
} catch (error) {
|
|
6238
|
+
// Diagnostics only: a host without attributes costs nothing.
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
|
|
6242
|
+
/**
|
|
6243
|
+
* The first family a stack names that is a REAL family, or "" when there is none.
|
|
6244
|
+
*
|
|
6245
|
+
* Our own alias families are skipped: once a family is promoted, any stack read back
|
|
6246
|
+
* from the page (`liveFamily()`) leads with the alias, and treating that as the
|
|
6247
|
+
* family name would miss the promotion map and hand back the stepped shape — which is
|
|
6248
|
+
* how the floating panel kept offering the coarse notches after the card moved on.
|
|
6249
|
+
* @param {string} stack - a formatted font-family value.
|
|
6250
|
+
* @returns {string} the family the stack really draws with.
|
|
6251
|
+
*/
|
|
6252
|
+
function primaryFamilyOf(stack) {
|
|
6253
|
+
var names = parseStack(stack);
|
|
6254
|
+
for (var index = 0; index < names.length; index += 1) {
|
|
6255
|
+
if (names[index].indexOf(shared.VARIABLE_ALIAS_PREFIX) !== 0) return names[index];
|
|
6256
|
+
}
|
|
6257
|
+
return "";
|
|
6258
|
+
}
|
|
6259
|
+
|
|
6260
|
+
/**
|
|
6261
|
+
* Ask the page whether a family is continuous under weight, once per family.
|
|
6262
|
+
*
|
|
6263
|
+
* The probe does what the page does: it installs the alias face for the family
|
|
6264
|
+
* (`local()` plus a weight range) and waits for the browser to load it. It then
|
|
6265
|
+
* asks TWO ways, both through that alias:
|
|
6266
|
+
*
|
|
6267
|
+
* * the laid-out width, in the real DOM — what a reader's eye follows in prose;
|
|
6268
|
+
* * the canvas ink, which is the only signal a monospace family ever moves on,
|
|
6269
|
+
* since its advance is the same at every weight.
|
|
6270
|
+
*
|
|
6271
|
+
* A family that answers continuously on either signal is promoted to
|
|
6272
|
+
* `variableFamilies`; the stylesheet is then rebuilt so the alias leads its
|
|
6273
|
+
* stacks. Anything that cannot be measured (no font loading API, a rejected load)
|
|
6274
|
+
* leaves the family as it was.
|
|
6275
|
+
* @param {string} stack - the axis's formatted font-family value.
|
|
6276
|
+
*/
|
|
6277
|
+
function probeContinuity(stack) {
|
|
6278
|
+
var family = primaryFamilyOf(stack);
|
|
6279
|
+
if (family === "" || continuityProbed[family] === true) return;
|
|
6280
|
+
continuityProbed[family] = true;
|
|
6281
|
+
if (typeof document === "undefined" || !document.fonts || typeof document.fonts.load !== "function") return;
|
|
6282
|
+
var alias = shared.variableAliasFor(family);
|
|
6283
|
+
var style = document.createElement("style");
|
|
6284
|
+
style.setAttribute("data-dfp-continuity", alias);
|
|
6285
|
+
style.textContent = shared.variableFaceRule(family, alias);
|
|
6286
|
+
document.head.appendChild(style);
|
|
6287
|
+
var host = document.createElement("span");
|
|
6288
|
+
host.style.cssText =
|
|
6289
|
+
"position:fixed;left:-10000px;top:0;white-space:nowrap;font-size:64px;font-family:\"" + alias + "\"";
|
|
6290
|
+
host.textContent = WEIGHT_SAMPLE_TEXT;
|
|
6291
|
+
document.body.appendChild(host);
|
|
6292
|
+
var keepFace = false;
|
|
6293
|
+
var finish = function () {
|
|
6294
|
+
host.remove();
|
|
6295
|
+
// A promoted family keeps its face: the stylesheet will declare the same alias,
|
|
6296
|
+
// and the weight profile is measured THROUGH it after promotion, so removing the
|
|
6297
|
+
// rule here would leave that measurement drawing with the fallback.
|
|
6298
|
+
if (!keepFace) style.remove();
|
|
6299
|
+
};
|
|
6300
|
+
// Two signals, because neither covers every family on its own: the DOM width
|
|
6301
|
+
// moves for a proportional font, while a monospace font never changes width and
|
|
6302
|
+
// can only be seen in the canvas ink. The ink is also read through the FAMILY
|
|
6303
|
+
// itself, and the two responses are compared: a family that snaps to named
|
|
6304
|
+
// instances answers identically either way, while a variable family moves through
|
|
6305
|
+
// its alias at weights where its own path already stopped.
|
|
6306
|
+
//
|
|
6307
|
+
// All values equal means the face has not taken effect yet (the page is still
|
|
6308
|
+
// drawing with its fallback), not that the family is stepped: a stepped family
|
|
6309
|
+
// still changes between its faces. Wait a beat and measure again, a few times at
|
|
6310
|
+
// most, so a slow face is not mistaken for a static one.
|
|
6311
|
+
var attempts = 0;
|
|
6312
|
+
var measure = function () {
|
|
6313
|
+
attempts += 1;
|
|
6314
|
+
// `!important` on purpose: the page's own weight rules are `!important`, and a
|
|
6315
|
+
// plain inline weight loses to them, which measured every width as one value.
|
|
6316
|
+
var widths = shared.CONTINUITY_WEIGHTS.map(function (weight) {
|
|
6317
|
+
host.style.setProperty("font-weight", String(weight), "important");
|
|
6318
|
+
return host.getBoundingClientRect().width;
|
|
6319
|
+
});
|
|
6320
|
+
var inks = shared.CONTINUITY_WEIGHTS.map(function (weight) {
|
|
6321
|
+
return inkOfSignature(sampleWeightSignature('"' + alias + '"', weight));
|
|
6322
|
+
});
|
|
6323
|
+
var plainInks = shared.CONTINUITY_WEIGHTS.map(function (weight) {
|
|
6324
|
+
return inkOfSignature(sampleWeightSignature('"' + family + '"', weight));
|
|
6325
|
+
});
|
|
6326
|
+
var unchanged = function (list) {
|
|
6327
|
+
return list.every(function (value) {
|
|
6328
|
+
return value === list[0];
|
|
6329
|
+
});
|
|
6330
|
+
};
|
|
6331
|
+
if (unchanged(widths) && unchanged(inks) && attempts < 8) {
|
|
6332
|
+
globalThis.setTimeout(measure, 80);
|
|
6333
|
+
return;
|
|
6334
|
+
}
|
|
6335
|
+
// The alias must answer more finely than the family itself. A family that snaps
|
|
6336
|
+
// to named instances cannot: both responses hold the same values.
|
|
6337
|
+
var finer = shared.respondsFinerThan(inks, plainInks);
|
|
6338
|
+
var continuous = finer || shared.isContinuousResponse(widths);
|
|
6339
|
+
keepFace = continuous;
|
|
6340
|
+
finish();
|
|
6341
|
+
probeLog("continuity", {
|
|
6342
|
+
family: family,
|
|
6343
|
+
continuous: continuous,
|
|
6344
|
+
attempts: attempts,
|
|
6345
|
+
byWidth: shared.isContinuousResponse(widths),
|
|
6346
|
+
aliasInk: shared.distinctCount(inks),
|
|
6347
|
+
plainInk: shared.distinctCount(plainInks),
|
|
6348
|
+
widths: widths.map(function (width) {
|
|
6349
|
+
return Math.round(width * 100) / 100;
|
|
6350
|
+
}),
|
|
6351
|
+
});
|
|
6352
|
+
if (!continuous) return;
|
|
6353
|
+
variableFamilies[family] = alias;
|
|
6354
|
+
announceContinuous();
|
|
6355
|
+
notifyPromoted();
|
|
6356
|
+
if (typeof repaint === "function") repaint();
|
|
6357
|
+
};
|
|
6358
|
+
document.fonts.load('400 64px "' + alias + '"').then(
|
|
6359
|
+
function () {
|
|
6360
|
+
measure();
|
|
6361
|
+
},
|
|
6362
|
+
function () {
|
|
6363
|
+
finish();
|
|
6364
|
+
probeLog("continuity", { family: family, continuous: false, error: "load" });
|
|
6365
|
+
}
|
|
6366
|
+
);
|
|
6367
|
+
}
|
|
6368
|
+
|
|
5861
6369
|
/**
|
|
5862
6370
|
* The family the page actually renders with, as a `font-family` value.
|
|
5863
6371
|
* @returns {string} the computed family, or "" when unreadable.
|
|
@@ -8445,12 +8953,17 @@ function FontCard(props) {
|
|
|
8445
8953
|
// nothing else — the weight, size and line height all come from the
|
|
8446
8954
|
// conversation section.
|
|
8447
8955
|
var previewUiStyle = {};
|
|
8448
|
-
if (uiStack !== "") previewUiStyle.fontFamily = uiStack;
|
|
8956
|
+
if (uiStack !== "") previewUiStyle.fontFamily = shared.withVariableFaces(uiStack, variableFamilies);
|
|
8449
8957
|
var previewDialogStyle = {};
|
|
8450
8958
|
// Independent and unset: the conversation keeps DSH's own family, so the
|
|
8451
8959
|
// preview shows the theme's default rather than the interface stack the page
|
|
8452
8960
|
// rule would otherwise paint into this box.
|
|
8453
|
-
|
|
8961
|
+
//
|
|
8962
|
+
// The alias leads these stacks exactly as it leads the real surfaces: without it
|
|
8963
|
+
// the preview would draw through the system family, where the fine notches snap to
|
|
8964
|
+
// the same named instance — so a notch that visibly moves the conversation would
|
|
8965
|
+
// do nothing here.
|
|
8966
|
+
if (dialogStack !== "") previewDialogStyle.fontFamily = shared.withVariableFaces(dialogStack, variableFamilies);
|
|
8454
8967
|
previewDialogStyle.fontSize = 13 + editing[SIZE_DIALOG_FIELD] + "px";
|
|
8455
8968
|
// The axis is a percentage OF DSH's own line height, so the preview multiplies the
|
|
8456
8969
|
// measured base by the same percentage — and does it at every notch including 100,
|
|
@@ -8471,7 +8984,7 @@ function FontCard(props) {
|
|
|
8471
8984
|
}
|
|
8472
8985
|
var previewCodeStyle = {};
|
|
8473
8986
|
if (editing[MONO_FIELD] !== "") {
|
|
8474
|
-
previewCodeStyle.fontFamily = formatStack(parseStack(editing[MONO_FIELD]));
|
|
8987
|
+
previewCodeStyle.fontFamily = shared.withVariableFaces(formatStack(parseStack(editing[MONO_FIELD])), variableFamilies);
|
|
8475
8988
|
}
|
|
8476
8989
|
previewCodeStyle.fontSize = 13 + editing[CODE_SIZE_FIELD] + "px";
|
|
8477
8990
|
if (editing[CODE_LINE_HEIGHT_FIELD] !== 0) {
|
|
@@ -10818,8 +11331,13 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10818
11331
|
host.textContent = "";
|
|
10819
11332
|
// One line in the console answers "which build is live": with a linked plugin
|
|
10820
11333
|
// and a cached client script, "I refreshed" is not evidence on its own.
|
|
10821
|
-
host.setAttribute("data-dfp-build",
|
|
10822
|
-
|
|
11334
|
+
host.setAttribute("data-dfp-build", CLIENT_BUILD);
|
|
11335
|
+
// And which families this build promoted to continuous weight, so a report of
|
|
11336
|
+
// "the slider still has four notches" can be answered without a console session:
|
|
11337
|
+
// an empty attribute means the probe has not (yet) promoted the reader's family,
|
|
11338
|
+
// a missing one means the page is running a build from before the mode existed.
|
|
11339
|
+
if (continuousNames().length > 0) host.setAttribute("data-dfp-continuous", continuousNames().join(", "));
|
|
11340
|
+
else host.removeAttribute("data-dfp-continuous"); var dot = el("button", "dfp-dot");
|
|
10823
11341
|
dot.type = "button";
|
|
10824
11342
|
dot.setAttribute("aria-label", t("panel.dot"));
|
|
10825
11343
|
dot.title = t("panel.dot");
|
|
@@ -11009,7 +11527,13 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11009
11527
|
|
|
11010
11528
|
// The dialog weight in the family's own steps, like the card's control:
|
|
11011
11529
|
// the document keeps the offset, the readout counts steps.
|
|
11012
|
-
|
|
11530
|
+
//
|
|
11531
|
+
// The stack comes from the SAME helper the card and the warm-up use. Resolving it
|
|
11532
|
+
// here instead (the configured field alone) meant an unset stack fell through to
|
|
11533
|
+
// the page's computed family, which carries our alias once a family is promoted —
|
|
11534
|
+
// so the panel missed the promotion and kept showing the coarse notches while the
|
|
11535
|
+
// card showed the fine ones.
|
|
11536
|
+
var dialogStack = weightAxisStacks(config).dialog;
|
|
11013
11537
|
var profile = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
11014
11538
|
try {
|
|
11015
11539
|
profile = weightProfileFor(dialogStack, shared.WEIGHT_LADDER_STRONG);
|
|
@@ -11021,6 +11545,13 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11021
11545
|
{ min: Math.min(profile.min, storedWeight), max: Math.max(profile.max, storedWeight), step: profile.step },
|
|
11022
11546
|
storedWeight
|
|
11023
11547
|
);
|
|
11548
|
+
probeLog("panel-weight", {
|
|
11549
|
+
stack: dialogStack,
|
|
11550
|
+
profile: profile,
|
|
11551
|
+
stored: storedWeight,
|
|
11552
|
+
shape: shape,
|
|
11553
|
+
promoted: continuousNames(),
|
|
11554
|
+
});
|
|
11024
11555
|
(function () {
|
|
11025
11556
|
var row = el("div", "dfp-floatRow");
|
|
11026
11557
|
var label = el("div", "dfp-floatLabel");
|
|
@@ -11233,8 +11764,14 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11233
11764
|
// A document without events still gets the interval.
|
|
11234
11765
|
}
|
|
11235
11766
|
|
|
11767
|
+
// A promotion changes the weight row's shape; the panel redraws itself when one
|
|
11768
|
+
// lands, and stops listening when it goes away.
|
|
11769
|
+
promotionListeners.push(render);
|
|
11770
|
+
|
|
11236
11771
|
return {
|
|
11237
11772
|
destroy: function () {
|
|
11773
|
+
var at = promotionListeners.indexOf(render);
|
|
11774
|
+
if (at >= 0) promotionListeners.splice(at, 1);
|
|
11238
11775
|
if (statusTimer !== null) globalThis.clearTimeout(statusTimer);
|
|
11239
11776
|
if (closeTimer !== null) globalThis.clearTimeout(closeTimer);
|
|
11240
11777
|
if (reflowTimer !== null) globalThis.clearTimeout(reflowTimer);
|
package/lib/shared.cjs
CHANGED
|
@@ -2329,6 +2329,27 @@ function rebuildShorthand(parts) {
|
|
|
2329
2329
|
* token classification
|
|
2330
2330
|
* ------------------------------------------------------------------ */
|
|
2331
2331
|
|
|
2332
|
+
/**
|
|
2333
|
+
* Whether a CSS selector declares something for the WHOLE page.
|
|
2334
|
+
*
|
|
2335
|
+
* The typography tokens live on `:root`, `html` or `body`. A selector scoped to a class
|
|
2336
|
+
* or an attribute declares a LOCAL value: another plugin's module stylesheet may reuse a
|
|
2337
|
+
* token name for its own compact panel (measured: `dsh-better-sidebar` sets
|
|
2338
|
+
* `--dsw-font-markdown-h1` to `600 13px/18px` on `.…_taskMarkdown`), and reading that as
|
|
2339
|
+
* the base hands the conversation a size that panel picked for itself.
|
|
2340
|
+
* @param {string} selector - a rule's selector text.
|
|
2341
|
+
* @returns {boolean} true when every selector in the list is a root scope.
|
|
2342
|
+
*/
|
|
2343
|
+
function isRootScopeSelector(selector) {
|
|
2344
|
+
if (typeof selector !== "string" || selector.trim() === "") return false;
|
|
2345
|
+
var parts = selector.split(",");
|
|
2346
|
+
for (var index = 0; index < parts.length; index += 1) {
|
|
2347
|
+
var part = parts[index].trim().toLowerCase();
|
|
2348
|
+
if (part !== ":root" && part !== "html" && part !== "body" && part !== "*") return false;
|
|
2349
|
+
}
|
|
2350
|
+
return true;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2332
2353
|
/**
|
|
2333
2354
|
* Match DSH's typography PART tokens: the design system's sizes plus the
|
|
2334
2355
|
* content size the theme plugin writes on `body` (whose secondary variant
|
|
@@ -3169,6 +3190,180 @@ function weightStepRange(profile, value) {
|
|
|
3169
3190
|
return { min: min, max: max, unit: unit, value: count };
|
|
3170
3191
|
}
|
|
3171
3192
|
|
|
3193
|
+
/* ------------------------------------------------------------------ *
|
|
3194
|
+
* continuous weight (FT-060)
|
|
3195
|
+
* ------------------------------------------------------------------ */
|
|
3196
|
+
|
|
3197
|
+
/** Prefix of the alias family a continuous family is drawn through. */
|
|
3198
|
+
var VARIABLE_ALIAS_PREFIX = "dfp-vf-";
|
|
3199
|
+
|
|
3200
|
+
/**
|
|
3201
|
+
* The weights the continuity probe lays out. Dense on purpose: a variable font
|
|
3202
|
+
* that moves in small steps shows a width change at every one of them, while a
|
|
3203
|
+
* family that snaps to named instances repeats its width across each stretch.
|
|
3204
|
+
*/
|
|
3205
|
+
var CONTINUITY_WEIGHTS = [400, 420, 440, 460, 480, 500, 550, 600, 650, 700, 750, 800, 900];
|
|
3206
|
+
|
|
3207
|
+
/**
|
|
3208
|
+
* The share of adjacent probe pairs that must differ before a family's laid-out
|
|
3209
|
+
* WIDTH counts as continuous. Measured: a variable family drawn through its alias
|
|
3210
|
+
* changes width at every sampled weight (share 1.0), while a family snapping to
|
|
3211
|
+
* named instances repeats its width across each stretch (share 0.25–0.42).
|
|
3212
|
+
*/
|
|
3213
|
+
var CONTINUOUS_MIN_SHARE = 0.9;
|
|
3214
|
+
|
|
3215
|
+
/**
|
|
3216
|
+
* The notches offered for a continuous family, finest first.
|
|
3217
|
+
*
|
|
3218
|
+
* The shape is still measured (the alias is drawn on the same weight grid as any
|
|
3219
|
+
* other family), but the candidates are fine: a continuous axis can answer at every
|
|
3220
|
+
* one of them, which is the whole point of the mode. `weightProfileFrom` walks this
|
|
3221
|
+
* list from the END, so the finest candidate that still yields enough visible
|
|
3222
|
+
* positions wins.
|
|
3223
|
+
*/
|
|
3224
|
+
var CONTINUOUS_CANDIDATES = [10, 20, 25];
|
|
3225
|
+
|
|
3226
|
+
/**
|
|
3227
|
+
* The nominal continuous slider, used when the measured shape cannot be taken
|
|
3228
|
+
* (no canvas, a refused probe). One notch is 20 weight units; the range is the
|
|
3229
|
+
* widest offset the ceiling allows either way.
|
|
3230
|
+
*/
|
|
3231
|
+
var CONTINUOUS_STEP = 20;
|
|
3232
|
+
var CONTINUOUS_RANGE = 200;
|
|
3233
|
+
|
|
3234
|
+
/**
|
|
3235
|
+
* How many different values a list of measurements holds.
|
|
3236
|
+
* @param {number[]} values - the measurements.
|
|
3237
|
+
* @returns {number} the count of distinct values.
|
|
3238
|
+
*/
|
|
3239
|
+
function distinctCount(values) {
|
|
3240
|
+
if (!Array.isArray(values)) return 0;
|
|
3241
|
+
var seen = [];
|
|
3242
|
+
for (var index = 0; index < values.length; index += 1) {
|
|
3243
|
+
if (seen.indexOf(values[index]) < 0) seen.push(values[index]);
|
|
3244
|
+
}
|
|
3245
|
+
return seen.length;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
/**
|
|
3249
|
+
* Does the same family answer more finely through its alias than through itself?
|
|
3250
|
+
*
|
|
3251
|
+
* This is the discriminator the continuity probe trusts, and it needs no threshold:
|
|
3252
|
+
* a family that snaps to named instances renders IDENTICALLY either way, so both
|
|
3253
|
+
* responses hold the same number of distinct values; a variable family drawn through
|
|
3254
|
+
* the alias moves at weights where the system path already stopped. Comparing the
|
|
3255
|
+
* family against ITSELF also keeps the answer honest for a narrow axis, where a
|
|
3256
|
+
* share-of-neighbours rule would read saturation as "static".
|
|
3257
|
+
* @param {number[]} throughAlias - measurements taken through the alias face.
|
|
3258
|
+
* @param {number[]} plain - the same measurements taken through the family itself.
|
|
3259
|
+
* @returns {boolean} true when the alias resolves finer detail.
|
|
3260
|
+
*/
|
|
3261
|
+
function respondsFinerThan(throughAlias, plain) {
|
|
3262
|
+
if (!Array.isArray(throughAlias) || !Array.isArray(plain)) return false;
|
|
3263
|
+
var fine = distinctCount(throughAlias);
|
|
3264
|
+
return fine >= 3 && fine > distinctCount(plain);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
|
|
3268
|
+
/**
|
|
3269
|
+
* The alias family name for one family. A hash, not the family itself, so the
|
|
3270
|
+
* name is a plain identifier whatever the family is called.
|
|
3271
|
+
* @param {string} family - a family name.
|
|
3272
|
+
* @returns {string} the alias, stable across sessions.
|
|
3273
|
+
*/
|
|
3274
|
+
function variableAliasFor(family) {
|
|
3275
|
+
var text = sanitizeFamily(family);
|
|
3276
|
+
var hash = 0;
|
|
3277
|
+
for (var index = 0; index < text.length; index += 1) {
|
|
3278
|
+
hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
|
|
3279
|
+
}
|
|
3280
|
+
return VARIABLE_ALIAS_PREFIX + hash.toString(36);
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
/**
|
|
3284
|
+
* The `@font-face` rule that draws one family through its own variable axis.
|
|
3285
|
+
*
|
|
3286
|
+
* `local()` points the face at the installed family, and the weight RANGE is the
|
|
3287
|
+
* part that matters: it tells the engine the family is continuous, so `font-weight`
|
|
3288
|
+
* picks a point on the axis instead of the nearest named instance. (Without the
|
|
3289
|
+
* range the same family snaps; `font-variation-settings` does not help a system
|
|
3290
|
+
* family at all — measured, see FT-060.)
|
|
3291
|
+
* @param {string} family - the installed family name.
|
|
3292
|
+
* @param {string} alias - the alias from `variableAliasFor`.
|
|
3293
|
+
* @returns {string} one CSS rule.
|
|
3294
|
+
*/
|
|
3295
|
+
function variableFaceRule(family, alias) {
|
|
3296
|
+
var text = sanitizeFamily(family).replace(/"/g, "");
|
|
3297
|
+
return '@font-face{font-family:"' + alias + '";src:local("' + text + '");font-weight:100 900;}';
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
/**
|
|
3301
|
+
* Every `@font-face` rule for a map of family → alias.
|
|
3302
|
+
* @param {Record<string, string>} variable - the families that measured continuous.
|
|
3303
|
+
* @returns {string} the rules, one per line; "" when there are none.
|
|
3304
|
+
*/
|
|
3305
|
+
function buildVariableFaces(variable) {
|
|
3306
|
+
var rules = [];
|
|
3307
|
+
if (variable === null || typeof variable !== "object") return "";
|
|
3308
|
+
for (var family in variable) {
|
|
3309
|
+
if (Object.prototype.hasOwnProperty.call(variable, family)) {
|
|
3310
|
+
rules.push(variableFaceRule(family, variable[family]));
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return rules.join("\n");
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
/**
|
|
3317
|
+
* Put the alias IN FRONT of every continuous family a stack names.
|
|
3318
|
+
*
|
|
3319
|
+
* The family stays in the list after its alias, so a glyph the alias lacks still
|
|
3320
|
+
* falls through to the same face the reader chose, and a stack that names no
|
|
3321
|
+
* continuous family is returned unchanged.
|
|
3322
|
+
* @param {string} stack - a formatted font-family value.
|
|
3323
|
+
* @param {Record<string, string>} variable - family → alias map.
|
|
3324
|
+
* @returns {string} the stack, with aliases prepended where they apply.
|
|
3325
|
+
*/
|
|
3326
|
+
function withVariableFaces(stack, variable) {
|
|
3327
|
+
if (typeof stack !== "string" || stack === "" || variable === null || typeof variable !== "object") {
|
|
3328
|
+
return stack;
|
|
3329
|
+
}
|
|
3330
|
+
// The stack may already carry its alias — a family read back from the page's own
|
|
3331
|
+
// computed style does, once the mode is on. Prepending it again would stack up a
|
|
3332
|
+
// duplicate on every rebuild.
|
|
3333
|
+
var names = parseStack(stack);
|
|
3334
|
+
var lead = names.length > 0 ? names[0] : "";
|
|
3335
|
+
var prefix = [];
|
|
3336
|
+
for (var family in variable) {
|
|
3337
|
+
if (!Object.prototype.hasOwnProperty.call(variable, family)) continue;
|
|
3338
|
+
if (stack.indexOf(quoteFamily(family)) < 0) continue;
|
|
3339
|
+
if (lead === variable[family]) continue;
|
|
3340
|
+
prefix.push('"' + variable[family] + '"');
|
|
3341
|
+
}
|
|
3342
|
+
return prefix.length === 0 ? stack : prefix.join(", ") + ", " + stack;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
/**
|
|
3346
|
+
* Did a family respond continuously to weight in the probe?
|
|
3347
|
+
* @param {number[]} widths - laid-out widths, in `CONTINUITY_WEIGHTS` order.
|
|
3348
|
+
* @returns {boolean} true when enough neighbours differ.
|
|
3349
|
+
*/
|
|
3350
|
+
function isContinuousResponse(widths) {
|
|
3351
|
+
if (!Array.isArray(widths) || widths.length < 3) return false;
|
|
3352
|
+
var changes = 0;
|
|
3353
|
+
for (var index = 1; index < widths.length; index += 1) {
|
|
3354
|
+
if (widths[index] !== widths[index - 1]) changes += 1;
|
|
3355
|
+
}
|
|
3356
|
+
return changes / (widths.length - 1) >= CONTINUOUS_MIN_SHARE;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
/**
|
|
3360
|
+
* The slider shape for a continuous family: the finest notch, centred on 0.
|
|
3361
|
+
* @returns {{min: number, max: number, step: number}} the profile, in weight units.
|
|
3362
|
+
*/
|
|
3363
|
+
function continuousWeightProfile() {
|
|
3364
|
+
return { min: -CONTINUOUS_RANGE, max: CONTINUOUS_RANGE, step: CONTINUOUS_STEP };
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3172
3367
|
/**
|
|
3173
3368
|
* What a slider's local (unconfirmed) value should do when a value arrives from
|
|
3174
3369
|
* the settings document.
|
|
@@ -3256,10 +3451,12 @@ function isDormant(config) {
|
|
|
3256
3451
|
* @param {boolean} isDark - prefix every rule with the dark-theme attribute.
|
|
3257
3452
|
* @returns {string} declarations for one `<style>` element.
|
|
3258
3453
|
*/
|
|
3259
|
-
function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
var
|
|
3454
|
+
function buildAxisCss(axis, baseTokens, isDark, hints, variable) {
|
|
3455
|
+
// A continuous family leads its stack with its alias (see `withVariableFaces`);
|
|
3456
|
+
// with no map, every stack stays exactly as written.
|
|
3457
|
+
var sans = withVariableFaces(formatStack(parseStack(axis[SANS_FIELD])), variable);
|
|
3458
|
+
var mono = withVariableFaces(formatStack(parseStack(axis[MONO_FIELD])), variable);
|
|
3459
|
+
var dialog = withVariableFaces(formatStack(parseStack(axis[STACK_DIALOG_FIELD])), variable);
|
|
3263
3460
|
// Normalised once: an expanded axis always carries a number, but a caller
|
|
3264
3461
|
// handing in a raw object may leave the field out, and "absent" has to mean
|
|
3265
3462
|
// "unset" rather than "write the string undefined". The conversation's value
|
|
@@ -3659,7 +3856,28 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
3659
3856
|
if (noBold) declarations.push(bodyRule("font-synthesis-weight:none !important"));
|
|
3660
3857
|
}
|
|
3661
3858
|
|
|
3662
|
-
return declarations.join("\n");
|
|
3859
|
+
return hardenDeltaRefs(declarations.join("\n"));
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
/**
|
|
3863
|
+
* Give every reference to the content-size delta a fallback value.
|
|
3864
|
+
*
|
|
3865
|
+
* A heading's `font` shorthand is the markdown token, and DSH's own text for it reads
|
|
3866
|
+
* `calc(21px + var(--dsh-content-font-delta))` — a reference with NO fallback. Nothing
|
|
3867
|
+
* outside this plugin defines that property, so any rebuild that emits the reference
|
|
3868
|
+
* without the definition leaves it resolving to nothing, which makes the WHOLE shorthand
|
|
3869
|
+
* invalid at computed-value time: every font longhand falls back to `inherit`, and the
|
|
3870
|
+
* heading is painted at body size. Measured on a live page: h1 23px → 16px the moment the
|
|
3871
|
+
* property resolved to nothing, back to 23px when it returned.
|
|
3872
|
+
*
|
|
3873
|
+
* The fallback cannot change a page that defines the property (the definition wins), and
|
|
3874
|
+
* where the definition is missing it keeps the design's own size instead of collapsing.
|
|
3875
|
+
* @param {string} css - generated declarations.
|
|
3876
|
+
* @returns {string} the same declarations, with the reference made safe.
|
|
3877
|
+
*/
|
|
3878
|
+
function hardenDeltaRefs(css) {
|
|
3879
|
+
if (typeof css !== "string" || css === "") return css;
|
|
3880
|
+
return css.split("var(--dsh-content-font-delta)").join("var(--dsh-content-font-delta, 0px)");
|
|
3663
3881
|
}
|
|
3664
3882
|
|
|
3665
3883
|
/**
|
|
@@ -3751,7 +3969,7 @@ function scaleCodeTokens(baseTokens, scale, lineOffset) {
|
|
|
3751
3969
|
* correct everywhere and costs an attribute scan per element per rule.
|
|
3752
3970
|
* @returns {string} declarations for one `<style>` element ("" when dormant).
|
|
3753
3971
|
*/
|
|
3754
|
-
function buildFontCss(config, baseTokens, hints) {
|
|
3972
|
+
function buildFontCss(config, baseTokens, hints, variable) {
|
|
3755
3973
|
var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
|
|
3756
3974
|
var sets = resolveAxes(config);
|
|
3757
3975
|
// The rendering preference is NOT an axis: it must paint even when every
|
|
@@ -3759,16 +3977,16 @@ function buildFontCss(config, baseTokens, hints) {
|
|
|
3759
3977
|
// use case with everything else untouched.
|
|
3760
3978
|
var smoothing = smoothingRule(config !== null && typeof config === "object" ? config[SMOOTHING_FIELD] : "");
|
|
3761
3979
|
if (isDormant(sets.light) && isDormant(sets.dark)) return smoothing;
|
|
3762
|
-
var light = buildAxisCss(sets.light, tokens, false, hints);
|
|
3980
|
+
var light = buildAxisCss(sets.light, tokens, false, hints, variable);
|
|
3763
3981
|
if (sameValueSet(sets.light, sets.dark)) {
|
|
3764
|
-
return smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing;
|
|
3982
|
+
return hardenDeltaRefs(smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing);
|
|
3765
3983
|
}
|
|
3766
|
-
var dark = buildAxisCss(sets.dark, tokens, true, hints);
|
|
3984
|
+
var dark = buildAxisCss(sets.dark, tokens, true, hints, variable);
|
|
3767
3985
|
var parts = [];
|
|
3768
3986
|
if (light !== "") parts.push(light);
|
|
3769
3987
|
if (dark !== "") parts.push(dark);
|
|
3770
3988
|
if (smoothing !== "") parts.push(smoothing);
|
|
3771
|
-
return parts.join("\n");
|
|
3989
|
+
return hardenDeltaRefs(parts.join("\n"));
|
|
3772
3990
|
}
|
|
3773
3991
|
|
|
3774
3992
|
/**
|
|
@@ -4290,6 +4508,19 @@ var shared = {
|
|
|
4290
4508
|
WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
|
|
4291
4509
|
WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
|
|
4292
4510
|
WEIGHT_MIN_POSITIONS: WEIGHT_MIN_POSITIONS,
|
|
4511
|
+
VARIABLE_ALIAS_PREFIX: VARIABLE_ALIAS_PREFIX,
|
|
4512
|
+
CONTINUITY_WEIGHTS: CONTINUITY_WEIGHTS,
|
|
4513
|
+
isRootScopeSelector: isRootScopeSelector,
|
|
4514
|
+
hardenDeltaRefs: hardenDeltaRefs,
|
|
4515
|
+
CONTINUOUS_CANDIDATES: CONTINUOUS_CANDIDATES,
|
|
4516
|
+
distinctCount: distinctCount,
|
|
4517
|
+
respondsFinerThan: respondsFinerThan,
|
|
4518
|
+
variableAliasFor: variableAliasFor,
|
|
4519
|
+
variableFaceRule: variableFaceRule,
|
|
4520
|
+
buildVariableFaces: buildVariableFaces,
|
|
4521
|
+
withVariableFaces: withVariableFaces,
|
|
4522
|
+
isContinuousResponse: isContinuousResponse,
|
|
4523
|
+
continuousWeightProfile: continuousWeightProfile,
|
|
4293
4524
|
WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
|
|
4294
4525
|
WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
|
|
4295
4526
|
WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fonttune",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"0.1.7-rc.1": "compatible",
|
|
38
38
|
"0.1.7-rc.2": "compatible",
|
|
39
39
|
"0.2.0-rc.1": "compatible",
|
|
40
|
-
"0.2.0-rc.2": "compatible"
|
|
40
|
+
"0.2.0-rc.2": "compatible",
|
|
41
|
+
"0.2.1-alpha.1": "compatible",
|
|
42
|
+
"0.2.1-alpha.2": "compatible"
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
},
|