dsh-fonttune 0.4.2 → 0.6.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 +24 -0
- package/CHANGELOG.md +24 -0
- package/README.en.md +14 -3
- package/README.md +14 -3
- package/docs/card-shaping.jpg +0 -0
- package/docs/panel.jpg +0 -0
- package/lib/client.js +1299 -43
- package/lib/index.js +38 -0
- package/lib/shared.cjs +445 -10
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +4 -2
package/lib/client.js
CHANGED
|
@@ -318,6 +318,67 @@ var LINE_HEIGHT_MAX = 160;
|
|
|
318
318
|
var CODE_LINE_HEIGHT_MIN = -4;
|
|
319
319
|
var CODE_LINE_HEIGHT_MAX = 8;
|
|
320
320
|
|
|
321
|
+
/* ------------------------------------------------------------------ *
|
|
322
|
+
* conversation text shaping (measured against DSH's own markdown)
|
|
323
|
+
* ------------------------------------------------------------------ */
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Field carrying the letter-spacing offset in px (0 = DSH's own `normal`).
|
|
327
|
+
* DSH leaves tracking at `normal` on the markdown scope (measured 2026-10-13), so
|
|
328
|
+
* the offset can be written as an absolute value: 0 emits no rule at all.
|
|
329
|
+
*/
|
|
330
|
+
var LETTER_SPACING_DIALOG_FIELD = "letterSpacingDialog";
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Field carrying the word-spacing offset in px (0 = DSH's own `0px`).
|
|
334
|
+
* Word spacing is measured 0px on the same scope, so it is an absolute value too.
|
|
335
|
+
*/
|
|
336
|
+
var WORD_SPACING_DIALOG_FIELD = "wordSpacingDialog";
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Field carrying the paragraph-gap offset in px, added on top of DSH's own 16px.
|
|
340
|
+
* The paragraphs' margins are NOT zero on the shipped markdown (first 0/16, the
|
|
341
|
+
* rest 16/16, headings 32/16 — measured 2026-10-13), so this axis is an offset
|
|
342
|
+
* built on that baseline rather than an absolute value.
|
|
343
|
+
*/
|
|
344
|
+
var PARAGRAPH_SPACING_DIALOG_FIELD = "paragraphSpacingDialog";
|
|
345
|
+
|
|
346
|
+
/** Field carrying the first-line indent, in em (0 = DSH's own none). */
|
|
347
|
+
var INDENT_DIALOG_FIELD = "indentDialog";
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Field asking for strict CJK line breaking (`line-break:strict`) in the
|
|
351
|
+
* conversation: no small kana, no dash starting a line. DSH leaves `auto`.
|
|
352
|
+
*/
|
|
353
|
+
var LINE_BREAK_STRICT_FIELD = "lineBreakStrict";
|
|
354
|
+
|
|
355
|
+
/** Letter-spacing offset range, in px, and its notch (measured: 0.1px is visible). */
|
|
356
|
+
var LETTER_SPACING_MIN = -0.4;
|
|
357
|
+
var LETTER_SPACING_MAX = 2.0;
|
|
358
|
+
var LETTER_SPACING_STEP = 0.1;
|
|
359
|
+
|
|
360
|
+
/** Word-spacing offset range, in px, and its notch. */
|
|
361
|
+
var WORD_SPACING_MIN = -1.0;
|
|
362
|
+
var WORD_SPACING_MAX = 4.0;
|
|
363
|
+
var WORD_SPACING_STEP = 0.2;
|
|
364
|
+
|
|
365
|
+
/** Paragraph-gap offset range, in px, over DSH's own 16px. */
|
|
366
|
+
var PARAGRAPH_SPACING_MIN = -8;
|
|
367
|
+
var PARAGRAPH_SPACING_MAX = 24;
|
|
368
|
+
var PARAGRAPH_SPACING_STEP = 1;
|
|
369
|
+
|
|
370
|
+
/** First-line indent range, in em, and its notch. */
|
|
371
|
+
var INDENT_MIN = 0;
|
|
372
|
+
var INDENT_MAX = 4;
|
|
373
|
+
var INDENT_STEP = 0.25;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The paragraph gap DSH's own markdown ships, in px. Measured on the conversation
|
|
377
|
+
* scope (2026-10-13): paragraphs carry 16px top/bottom margins, so that is the
|
|
378
|
+
* baseline every offset is added to.
|
|
379
|
+
*/
|
|
380
|
+
var PARAGRAPH_BASELINE_PX = 16;
|
|
381
|
+
|
|
321
382
|
/** Ligature modes. */
|
|
322
383
|
var LIGATURES_DEFAULT = 0;
|
|
323
384
|
var LIGATURES_ON = 1;
|
|
@@ -343,6 +404,11 @@ var DEFAULTS = {
|
|
|
343
404
|
lineHeight: LINE_HEIGHT_MIN,
|
|
344
405
|
lineHeightDialog: LINE_HEIGHT_MIN,
|
|
345
406
|
lineHeightCode: 0,
|
|
407
|
+
letterSpacingDialog: 0,
|
|
408
|
+
wordSpacingDialog: 0,
|
|
409
|
+
paragraphSpacingDialog: 0,
|
|
410
|
+
indentDialog: 0,
|
|
411
|
+
lineBreakStrict: false,
|
|
346
412
|
codeLigatures: LIGATURES_DEFAULT,
|
|
347
413
|
codeFeatures: "",
|
|
348
414
|
noSyntheticItalic: false,
|
|
@@ -374,6 +440,11 @@ var VALUE_FIELDS = [
|
|
|
374
440
|
CODE_WEIGHT_FIELD,
|
|
375
441
|
LINE_HEIGHT_DIALOG_FIELD,
|
|
376
442
|
CODE_LINE_HEIGHT_FIELD,
|
|
443
|
+
LETTER_SPACING_DIALOG_FIELD,
|
|
444
|
+
WORD_SPACING_DIALOG_FIELD,
|
|
445
|
+
PARAGRAPH_SPACING_DIALOG_FIELD,
|
|
446
|
+
INDENT_DIALOG_FIELD,
|
|
447
|
+
LINE_BREAK_STRICT_FIELD,
|
|
377
448
|
LIGATURES_FIELD,
|
|
378
449
|
FEATURES_FIELD,
|
|
379
450
|
NO_SYNTHETIC_ITALIC_FIELD,
|
|
@@ -1516,6 +1587,16 @@ function normalizeField(field, value, relative) {
|
|
|
1516
1587
|
return clampRatio(value);
|
|
1517
1588
|
case CODE_LINE_HEIGHT_FIELD:
|
|
1518
1589
|
return clampLineOffset(value);
|
|
1590
|
+
case LETTER_SPACING_DIALOG_FIELD:
|
|
1591
|
+
return clampSpacing(value, LETTER_SPACING_MIN, LETTER_SPACING_MAX);
|
|
1592
|
+
case WORD_SPACING_DIALOG_FIELD:
|
|
1593
|
+
return clampSpacing(value, WORD_SPACING_MIN, WORD_SPACING_MAX);
|
|
1594
|
+
case PARAGRAPH_SPACING_DIALOG_FIELD:
|
|
1595
|
+
return clampSpacing(value, PARAGRAPH_SPACING_MIN, PARAGRAPH_SPACING_MAX);
|
|
1596
|
+
case INDENT_DIALOG_FIELD:
|
|
1597
|
+
return clampIndent(value);
|
|
1598
|
+
case LINE_BREAK_STRICT_FIELD:
|
|
1599
|
+
return toBool(value);
|
|
1519
1600
|
case LIGATURES_FIELD:
|
|
1520
1601
|
return clampLigatures(value);
|
|
1521
1602
|
case WEIGHT_OFFSETS_FIELD:
|
|
@@ -1599,6 +1680,52 @@ function clampWeight(value) {
|
|
|
1599
1680
|
return Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
|
|
1600
1681
|
}
|
|
1601
1682
|
|
|
1683
|
+
/**
|
|
1684
|
+
* Clamp one spacing offset (letter, word) to its allowed range, in px.
|
|
1685
|
+
*
|
|
1686
|
+
* These axes sit on top of a DSH default that is already zero (`normal` / `0px`,
|
|
1687
|
+
* measured on the conversation scope), so no migration and no baseline is involved:
|
|
1688
|
+
* the offset is written as it stands, and 0 means "emit nothing".
|
|
1689
|
+
* @param {unknown} value - candidate offset.
|
|
1690
|
+
* @param {number} min - the lowest allowed offset.
|
|
1691
|
+
* @param {number} max - the highest allowed offset.
|
|
1692
|
+
* @returns {number} the offset the stylesheet may use.
|
|
1693
|
+
*/
|
|
1694
|
+
function clampSpacing(value, min, max) {
|
|
1695
|
+
var offset = Number(value);
|
|
1696
|
+
if (!isFinite(offset)) return 0;
|
|
1697
|
+
// Rounded to two decimals: the notches are tenths, and the sum of a few of them
|
|
1698
|
+
// must not turn into 0.30000000000000004 in the emitted CSS.
|
|
1699
|
+
offset = Math.round(offset * 100) / 100;
|
|
1700
|
+
if (offset > max) return max;
|
|
1701
|
+
if (offset < min) return min;
|
|
1702
|
+
return offset;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Clamp the first-line indent to its allowed range, in em.
|
|
1707
|
+
* @param {unknown} value - candidate indent.
|
|
1708
|
+
* @returns {number} the indent the stylesheet may use.
|
|
1709
|
+
*/
|
|
1710
|
+
function clampIndent(value) {
|
|
1711
|
+
var indent = Number(value);
|
|
1712
|
+
if (!isFinite(indent)) return 0;
|
|
1713
|
+
indent = Math.round(indent * 100) / 100;
|
|
1714
|
+
if (indent > INDENT_MAX) return INDENT_MAX;
|
|
1715
|
+
if (indent < INDENT_MIN) return INDENT_MIN;
|
|
1716
|
+
return indent;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Format a spacing value for CSS, without scientific notation or float noise.
|
|
1721
|
+
* @param {number} value - a clamped px value.
|
|
1722
|
+
* @returns {string} the value as a px length.
|
|
1723
|
+
*/
|
|
1724
|
+
function pxText(value) {
|
|
1725
|
+
var rounded = Math.round(value * 100) / 100;
|
|
1726
|
+
return String(rounded) + "px";
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1602
1729
|
/**
|
|
1603
1730
|
* Clamp one RELATIVE weight to its allowed range, migrating an absolute value.
|
|
1604
1731
|
*
|
|
@@ -2338,6 +2465,27 @@ function rebuildShorthand(parts) {
|
|
|
2338
2465
|
* token classification
|
|
2339
2466
|
* ------------------------------------------------------------------ */
|
|
2340
2467
|
|
|
2468
|
+
/**
|
|
2469
|
+
* Whether a CSS selector declares something for the WHOLE page.
|
|
2470
|
+
*
|
|
2471
|
+
* The typography tokens live on `:root`, `html` or `body`. A selector scoped to a class
|
|
2472
|
+
* or an attribute declares a LOCAL value: another plugin's module stylesheet may reuse a
|
|
2473
|
+
* token name for its own compact panel (measured: `dsh-better-sidebar` sets
|
|
2474
|
+
* `--dsw-font-markdown-h1` to `600 13px/18px` on `.…_taskMarkdown`), and reading that as
|
|
2475
|
+
* the base hands the conversation a size that panel picked for itself.
|
|
2476
|
+
* @param {string} selector - a rule's selector text.
|
|
2477
|
+
* @returns {boolean} true when every selector in the list is a root scope.
|
|
2478
|
+
*/
|
|
2479
|
+
function isRootScopeSelector(selector) {
|
|
2480
|
+
if (typeof selector !== "string" || selector.trim() === "") return false;
|
|
2481
|
+
var parts = selector.split(",");
|
|
2482
|
+
for (var index = 0; index < parts.length; index += 1) {
|
|
2483
|
+
var part = parts[index].trim().toLowerCase();
|
|
2484
|
+
if (part !== ":root" && part !== "html" && part !== "body" && part !== "*") return false;
|
|
2485
|
+
}
|
|
2486
|
+
return true;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2341
2489
|
/**
|
|
2342
2490
|
* Match DSH's typography PART tokens: the design system's sizes plus the
|
|
2343
2491
|
* content size the theme plugin writes on `body` (whose secondary variant
|
|
@@ -3178,6 +3326,180 @@ function weightStepRange(profile, value) {
|
|
|
3178
3326
|
return { min: min, max: max, unit: unit, value: count };
|
|
3179
3327
|
}
|
|
3180
3328
|
|
|
3329
|
+
/* ------------------------------------------------------------------ *
|
|
3330
|
+
* continuous weight (FT-060)
|
|
3331
|
+
* ------------------------------------------------------------------ */
|
|
3332
|
+
|
|
3333
|
+
/** Prefix of the alias family a continuous family is drawn through. */
|
|
3334
|
+
var VARIABLE_ALIAS_PREFIX = "dfp-vf-";
|
|
3335
|
+
|
|
3336
|
+
/**
|
|
3337
|
+
* The weights the continuity probe lays out. Dense on purpose: a variable font
|
|
3338
|
+
* that moves in small steps shows a width change at every one of them, while a
|
|
3339
|
+
* family that snaps to named instances repeats its width across each stretch.
|
|
3340
|
+
*/
|
|
3341
|
+
var CONTINUITY_WEIGHTS = [400, 420, 440, 460, 480, 500, 550, 600, 650, 700, 750, 800, 900];
|
|
3342
|
+
|
|
3343
|
+
/**
|
|
3344
|
+
* The share of adjacent probe pairs that must differ before a family's laid-out
|
|
3345
|
+
* WIDTH counts as continuous. Measured: a variable family drawn through its alias
|
|
3346
|
+
* changes width at every sampled weight (share 1.0), while a family snapping to
|
|
3347
|
+
* named instances repeats its width across each stretch (share 0.25–0.42).
|
|
3348
|
+
*/
|
|
3349
|
+
var CONTINUOUS_MIN_SHARE = 0.9;
|
|
3350
|
+
|
|
3351
|
+
/**
|
|
3352
|
+
* The notches offered for a continuous family, finest first.
|
|
3353
|
+
*
|
|
3354
|
+
* The shape is still measured (the alias is drawn on the same weight grid as any
|
|
3355
|
+
* other family), but the candidates are fine: a continuous axis can answer at every
|
|
3356
|
+
* one of them, which is the whole point of the mode. `weightProfileFrom` walks this
|
|
3357
|
+
* list from the END, so the finest candidate that still yields enough visible
|
|
3358
|
+
* positions wins.
|
|
3359
|
+
*/
|
|
3360
|
+
var CONTINUOUS_CANDIDATES = [10, 20, 25];
|
|
3361
|
+
|
|
3362
|
+
/**
|
|
3363
|
+
* The nominal continuous slider, used when the measured shape cannot be taken
|
|
3364
|
+
* (no canvas, a refused probe). One notch is 20 weight units; the range is the
|
|
3365
|
+
* widest offset the ceiling allows either way.
|
|
3366
|
+
*/
|
|
3367
|
+
var CONTINUOUS_STEP = 20;
|
|
3368
|
+
var CONTINUOUS_RANGE = 200;
|
|
3369
|
+
|
|
3370
|
+
/**
|
|
3371
|
+
* How many different values a list of measurements holds.
|
|
3372
|
+
* @param {number[]} values - the measurements.
|
|
3373
|
+
* @returns {number} the count of distinct values.
|
|
3374
|
+
*/
|
|
3375
|
+
function distinctCount(values) {
|
|
3376
|
+
if (!Array.isArray(values)) return 0;
|
|
3377
|
+
var seen = [];
|
|
3378
|
+
for (var index = 0; index < values.length; index += 1) {
|
|
3379
|
+
if (seen.indexOf(values[index]) < 0) seen.push(values[index]);
|
|
3380
|
+
}
|
|
3381
|
+
return seen.length;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
/**
|
|
3385
|
+
* Does the same family answer more finely through its alias than through itself?
|
|
3386
|
+
*
|
|
3387
|
+
* This is the discriminator the continuity probe trusts, and it needs no threshold:
|
|
3388
|
+
* a family that snaps to named instances renders IDENTICALLY either way, so both
|
|
3389
|
+
* responses hold the same number of distinct values; a variable family drawn through
|
|
3390
|
+
* the alias moves at weights where the system path already stopped. Comparing the
|
|
3391
|
+
* family against ITSELF also keeps the answer honest for a narrow axis, where a
|
|
3392
|
+
* share-of-neighbours rule would read saturation as "static".
|
|
3393
|
+
* @param {number[]} throughAlias - measurements taken through the alias face.
|
|
3394
|
+
* @param {number[]} plain - the same measurements taken through the family itself.
|
|
3395
|
+
* @returns {boolean} true when the alias resolves finer detail.
|
|
3396
|
+
*/
|
|
3397
|
+
function respondsFinerThan(throughAlias, plain) {
|
|
3398
|
+
if (!Array.isArray(throughAlias) || !Array.isArray(plain)) return false;
|
|
3399
|
+
var fine = distinctCount(throughAlias);
|
|
3400
|
+
return fine >= 3 && fine > distinctCount(plain);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
|
|
3404
|
+
/**
|
|
3405
|
+
* The alias family name for one family. A hash, not the family itself, so the
|
|
3406
|
+
* name is a plain identifier whatever the family is called.
|
|
3407
|
+
* @param {string} family - a family name.
|
|
3408
|
+
* @returns {string} the alias, stable across sessions.
|
|
3409
|
+
*/
|
|
3410
|
+
function variableAliasFor(family) {
|
|
3411
|
+
var text = sanitizeFamily(family);
|
|
3412
|
+
var hash = 0;
|
|
3413
|
+
for (var index = 0; index < text.length; index += 1) {
|
|
3414
|
+
hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
|
|
3415
|
+
}
|
|
3416
|
+
return VARIABLE_ALIAS_PREFIX + hash.toString(36);
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
/**
|
|
3420
|
+
* The `@font-face` rule that draws one family through its own variable axis.
|
|
3421
|
+
*
|
|
3422
|
+
* `local()` points the face at the installed family, and the weight RANGE is the
|
|
3423
|
+
* part that matters: it tells the engine the family is continuous, so `font-weight`
|
|
3424
|
+
* picks a point on the axis instead of the nearest named instance. (Without the
|
|
3425
|
+
* range the same family snaps; `font-variation-settings` does not help a system
|
|
3426
|
+
* family at all — measured, see FT-060.)
|
|
3427
|
+
* @param {string} family - the installed family name.
|
|
3428
|
+
* @param {string} alias - the alias from `variableAliasFor`.
|
|
3429
|
+
* @returns {string} one CSS rule.
|
|
3430
|
+
*/
|
|
3431
|
+
function variableFaceRule(family, alias) {
|
|
3432
|
+
var text = sanitizeFamily(family).replace(/"/g, "");
|
|
3433
|
+
return '@font-face{font-family:"' + alias + '";src:local("' + text + '");font-weight:100 900;}';
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
/**
|
|
3437
|
+
* Every `@font-face` rule for a map of family → alias.
|
|
3438
|
+
* @param {Record<string, string>} variable - the families that measured continuous.
|
|
3439
|
+
* @returns {string} the rules, one per line; "" when there are none.
|
|
3440
|
+
*/
|
|
3441
|
+
function buildVariableFaces(variable) {
|
|
3442
|
+
var rules = [];
|
|
3443
|
+
if (variable === null || typeof variable !== "object") return "";
|
|
3444
|
+
for (var family in variable) {
|
|
3445
|
+
if (Object.prototype.hasOwnProperty.call(variable, family)) {
|
|
3446
|
+
rules.push(variableFaceRule(family, variable[family]));
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return rules.join("\n");
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
/**
|
|
3453
|
+
* Put the alias IN FRONT of every continuous family a stack names.
|
|
3454
|
+
*
|
|
3455
|
+
* The family stays in the list after its alias, so a glyph the alias lacks still
|
|
3456
|
+
* falls through to the same face the reader chose, and a stack that names no
|
|
3457
|
+
* continuous family is returned unchanged.
|
|
3458
|
+
* @param {string} stack - a formatted font-family value.
|
|
3459
|
+
* @param {Record<string, string>} variable - family → alias map.
|
|
3460
|
+
* @returns {string} the stack, with aliases prepended where they apply.
|
|
3461
|
+
*/
|
|
3462
|
+
function withVariableFaces(stack, variable) {
|
|
3463
|
+
if (typeof stack !== "string" || stack === "" || variable === null || typeof variable !== "object") {
|
|
3464
|
+
return stack;
|
|
3465
|
+
}
|
|
3466
|
+
// The stack may already carry its alias — a family read back from the page's own
|
|
3467
|
+
// computed style does, once the mode is on. Prepending it again would stack up a
|
|
3468
|
+
// duplicate on every rebuild.
|
|
3469
|
+
var names = parseStack(stack);
|
|
3470
|
+
var lead = names.length > 0 ? names[0] : "";
|
|
3471
|
+
var prefix = [];
|
|
3472
|
+
for (var family in variable) {
|
|
3473
|
+
if (!Object.prototype.hasOwnProperty.call(variable, family)) continue;
|
|
3474
|
+
if (stack.indexOf(quoteFamily(family)) < 0) continue;
|
|
3475
|
+
if (lead === variable[family]) continue;
|
|
3476
|
+
prefix.push('"' + variable[family] + '"');
|
|
3477
|
+
}
|
|
3478
|
+
return prefix.length === 0 ? stack : prefix.join(", ") + ", " + stack;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
/**
|
|
3482
|
+
* Did a family respond continuously to weight in the probe?
|
|
3483
|
+
* @param {number[]} widths - laid-out widths, in `CONTINUITY_WEIGHTS` order.
|
|
3484
|
+
* @returns {boolean} true when enough neighbours differ.
|
|
3485
|
+
*/
|
|
3486
|
+
function isContinuousResponse(widths) {
|
|
3487
|
+
if (!Array.isArray(widths) || widths.length < 3) return false;
|
|
3488
|
+
var changes = 0;
|
|
3489
|
+
for (var index = 1; index < widths.length; index += 1) {
|
|
3490
|
+
if (widths[index] !== widths[index - 1]) changes += 1;
|
|
3491
|
+
}
|
|
3492
|
+
return changes / (widths.length - 1) >= CONTINUOUS_MIN_SHARE;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
/**
|
|
3496
|
+
* The slider shape for a continuous family: the finest notch, centred on 0.
|
|
3497
|
+
* @returns {{min: number, max: number, step: number}} the profile, in weight units.
|
|
3498
|
+
*/
|
|
3499
|
+
function continuousWeightProfile() {
|
|
3500
|
+
return { min: -CONTINUOUS_RANGE, max: CONTINUOUS_RANGE, step: CONTINUOUS_STEP };
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3181
3503
|
/**
|
|
3182
3504
|
* What a slider's local (unconfirmed) value should do when a value arrives from
|
|
3183
3505
|
* the settings document.
|
|
@@ -3241,6 +3563,11 @@ function isDormant(config) {
|
|
|
3241
3563
|
clampWeightDelta(axis[CODE_WEIGHT_FIELD], true) === WEIGHT_UNSET &&
|
|
3242
3564
|
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
3243
3565
|
clampLineOffset(axis[CODE_LINE_HEIGHT_FIELD]) === 0 &&
|
|
3566
|
+
clampSpacing(axis[LETTER_SPACING_DIALOG_FIELD], LETTER_SPACING_MIN, LETTER_SPACING_MAX) === 0 &&
|
|
3567
|
+
clampSpacing(axis[WORD_SPACING_DIALOG_FIELD], WORD_SPACING_MIN, WORD_SPACING_MAX) === 0 &&
|
|
3568
|
+
clampSpacing(axis[PARAGRAPH_SPACING_DIALOG_FIELD], PARAGRAPH_SPACING_MIN, PARAGRAPH_SPACING_MAX) === 0 &&
|
|
3569
|
+
clampIndent(axis[INDENT_DIALOG_FIELD]) === 0 &&
|
|
3570
|
+
toBool(axis[LINE_BREAK_STRICT_FIELD]) === false &&
|
|
3244
3571
|
clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
|
|
3245
3572
|
sanitizeFeatures(axis[FEATURES_FIELD]) === "" &&
|
|
3246
3573
|
toBool(axis[NO_SYNTHETIC_ITALIC_FIELD]) === false &&
|
|
@@ -3265,10 +3592,12 @@ function isDormant(config) {
|
|
|
3265
3592
|
* @param {boolean} isDark - prefix every rule with the dark-theme attribute.
|
|
3266
3593
|
* @returns {string} declarations for one `<style>` element.
|
|
3267
3594
|
*/
|
|
3268
|
-
function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
var
|
|
3595
|
+
function buildAxisCss(axis, baseTokens, isDark, hints, variable) {
|
|
3596
|
+
// A continuous family leads its stack with its alias (see `withVariableFaces`);
|
|
3597
|
+
// with no map, every stack stays exactly as written.
|
|
3598
|
+
var sans = withVariableFaces(formatStack(parseStack(axis[SANS_FIELD])), variable);
|
|
3599
|
+
var mono = withVariableFaces(formatStack(parseStack(axis[MONO_FIELD])), variable);
|
|
3600
|
+
var dialog = withVariableFaces(formatStack(parseStack(axis[STACK_DIALOG_FIELD])), variable);
|
|
3272
3601
|
// Normalised once: an expanded axis always carries a number, but a caller
|
|
3273
3602
|
// handing in a raw object may leave the field out, and "absent" has to mean
|
|
3274
3603
|
// "unset" rather than "write the string undefined". The conversation's value
|
|
@@ -3622,6 +3951,60 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
3622
3951
|
}
|
|
3623
3952
|
}
|
|
3624
3953
|
|
|
3954
|
+
// ---- conversation text shaping (tracking, word gap, paragraph gap, indent) ----
|
|
3955
|
+
//
|
|
3956
|
+
// Every value here is written on the CONVERSATION scope, and the measurements that
|
|
3957
|
+
// fixed the shape were taken there (2026-10-13): the markdown's tracking is `normal`
|
|
3958
|
+
// and its word gap `0px`, so those two axes are absolute and 0 emits nothing; the
|
|
3959
|
+
// paragraphs carry 16px margins, so the paragraph axis is an offset over that
|
|
3960
|
+
// baseline. Code surfaces are excluded by the same scope rule the weight ladder uses,
|
|
3961
|
+
// so a code block's own tracking is never rewritten by a conversation axis.
|
|
3962
|
+
var letterSpacing = clampSpacing(axis[LETTER_SPACING_DIALOG_FIELD], LETTER_SPACING_MIN, LETTER_SPACING_MAX);
|
|
3963
|
+
var wordSpacing = clampSpacing(axis[WORD_SPACING_DIALOG_FIELD], WORD_SPACING_MIN, WORD_SPACING_MAX);
|
|
3964
|
+
var paragraphSpacing = clampSpacing(
|
|
3965
|
+
axis[PARAGRAPH_SPACING_DIALOG_FIELD],
|
|
3966
|
+
PARAGRAPH_SPACING_MIN,
|
|
3967
|
+
PARAGRAPH_SPACING_MAX
|
|
3968
|
+
);
|
|
3969
|
+
var indent = clampIndent(axis[INDENT_DIALOG_FIELD]);
|
|
3970
|
+
if (letterSpacing !== 0) {
|
|
3971
|
+
declarations.push(weightScopeRule("letter-spacing:" + pxText(letterSpacing) + " !important"));
|
|
3972
|
+
}
|
|
3973
|
+
if (wordSpacing !== 0) {
|
|
3974
|
+
declarations.push(weightScopeRule("word-spacing:" + pxText(wordSpacing) + " !important"));
|
|
3975
|
+
}
|
|
3976
|
+
if (indent !== 0) {
|
|
3977
|
+
// A first-line indent is a PROSE convention, so it belongs to paragraphs. Emitted for
|
|
3978
|
+
// the whole conversation scope it inherited into every block under it; each of these
|
|
3979
|
+
// was measured on the page with the axis at 2em (32px on a 16px base):
|
|
3980
|
+
// - a heading sat 32px in, where no Chinese reader expects one;
|
|
3981
|
+
// - an ordered-list item's TEXT moved 32px while the number stayed at the content
|
|
3982
|
+
// edge (the marker sits outside the content box), so the number ended up in front
|
|
3983
|
+
// of the indent the text had taken;
|
|
3984
|
+
// - inline code is `display: inline-flex` in DSH's markdown — a block container of
|
|
3985
|
+
// its own — so the indent landed INSIDE the chip as blank space before its own
|
|
3986
|
+
// text, wherever the chip sat in the line;
|
|
3987
|
+
// - a code block's first line shifted the same way.
|
|
3988
|
+
// Paragraphs are the only place the convention exists, and their descendants are
|
|
3989
|
+
// zeroed so a chip that happens to be its own block still draws flush.
|
|
3990
|
+
var indentDeclarations = "text-indent:" + String(indent) + "em !important";
|
|
3991
|
+
declarations.push(weightScopeRule("p{" + indentDeclarations + "}"));
|
|
3992
|
+
declarations.push(weightScopeRule("p *{text-indent:0 !important}"));
|
|
3993
|
+
}
|
|
3994
|
+
if (paragraphSpacing !== 0) {
|
|
3995
|
+
// Both margins move together: adjacent siblings collapse to the LARGER of the two,
|
|
3996
|
+
// so shifting only `margin-top` would silently ignore every negative offset.
|
|
3997
|
+
var gap =
|
|
3998
|
+
paragraphSpacing > 0
|
|
3999
|
+
? "calc(" + PARAGRAPH_BASELINE_PX + "px + " + pxText(paragraphSpacing) + ")"
|
|
4000
|
+
: "calc(" + PARAGRAPH_BASELINE_PX + "px - " + pxText(-paragraphSpacing) + ")";
|
|
4001
|
+
declarations.push(weightScopeRule("p{margin-bottom:" + gap + " !important}"));
|
|
4002
|
+
declarations.push(weightScopeRule("p:not(:first-child){margin-top:" + gap + " !important}"));
|
|
4003
|
+
}
|
|
4004
|
+
if (toBool(axis[LINE_BREAK_STRICT_FIELD])) {
|
|
4005
|
+
declarations.push(weightScopeRule("line-break:strict !important"));
|
|
4006
|
+
}
|
|
4007
|
+
|
|
3625
4008
|
// ---- code axes ----
|
|
3626
4009
|
var codeSize = axis[CODE_SIZE_FIELD];
|
|
3627
4010
|
var codeLine = axis[CODE_LINE_HEIGHT_FIELD];
|
|
@@ -3668,7 +4051,28 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
3668
4051
|
if (noBold) declarations.push(bodyRule("font-synthesis-weight:none !important"));
|
|
3669
4052
|
}
|
|
3670
4053
|
|
|
3671
|
-
return declarations.join("\n");
|
|
4054
|
+
return hardenDeltaRefs(declarations.join("\n"));
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
/**
|
|
4058
|
+
* Give every reference to the content-size delta a fallback value.
|
|
4059
|
+
*
|
|
4060
|
+
* A heading's `font` shorthand is the markdown token, and DSH's own text for it reads
|
|
4061
|
+
* `calc(21px + var(--dsh-content-font-delta))` — a reference with NO fallback. Nothing
|
|
4062
|
+
* outside this plugin defines that property, so any rebuild that emits the reference
|
|
4063
|
+
* without the definition leaves it resolving to nothing, which makes the WHOLE shorthand
|
|
4064
|
+
* invalid at computed-value time: every font longhand falls back to `inherit`, and the
|
|
4065
|
+
* heading is painted at body size. Measured on a live page: h1 23px → 16px the moment the
|
|
4066
|
+
* property resolved to nothing, back to 23px when it returned.
|
|
4067
|
+
*
|
|
4068
|
+
* The fallback cannot change a page that defines the property (the definition wins), and
|
|
4069
|
+
* where the definition is missing it keeps the design's own size instead of collapsing.
|
|
4070
|
+
* @param {string} css - generated declarations.
|
|
4071
|
+
* @returns {string} the same declarations, with the reference made safe.
|
|
4072
|
+
*/
|
|
4073
|
+
function hardenDeltaRefs(css) {
|
|
4074
|
+
if (typeof css !== "string" || css === "") return css;
|
|
4075
|
+
return css.split("var(--dsh-content-font-delta)").join("var(--dsh-content-font-delta, 0px)");
|
|
3672
4076
|
}
|
|
3673
4077
|
|
|
3674
4078
|
/**
|
|
@@ -3760,7 +4164,7 @@ function scaleCodeTokens(baseTokens, scale, lineOffset) {
|
|
|
3760
4164
|
* correct everywhere and costs an attribute scan per element per rule.
|
|
3761
4165
|
* @returns {string} declarations for one `<style>` element ("" when dormant).
|
|
3762
4166
|
*/
|
|
3763
|
-
function buildFontCss(config, baseTokens, hints) {
|
|
4167
|
+
function buildFontCss(config, baseTokens, hints, variable) {
|
|
3764
4168
|
var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
|
|
3765
4169
|
var sets = resolveAxes(config);
|
|
3766
4170
|
// The rendering preference is NOT an axis: it must paint even when every
|
|
@@ -3768,16 +4172,16 @@ function buildFontCss(config, baseTokens, hints) {
|
|
|
3768
4172
|
// use case with everything else untouched.
|
|
3769
4173
|
var smoothing = smoothingRule(config !== null && typeof config === "object" ? config[SMOOTHING_FIELD] : "");
|
|
3770
4174
|
if (isDormant(sets.light) && isDormant(sets.dark)) return smoothing;
|
|
3771
|
-
var light = buildAxisCss(sets.light, tokens, false, hints);
|
|
4175
|
+
var light = buildAxisCss(sets.light, tokens, false, hints, variable);
|
|
3772
4176
|
if (sameValueSet(sets.light, sets.dark)) {
|
|
3773
|
-
return smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing;
|
|
4177
|
+
return hardenDeltaRefs(smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing);
|
|
3774
4178
|
}
|
|
3775
|
-
var dark = buildAxisCss(sets.dark, tokens, true, hints);
|
|
4179
|
+
var dark = buildAxisCss(sets.dark, tokens, true, hints, variable);
|
|
3776
4180
|
var parts = [];
|
|
3777
4181
|
if (light !== "") parts.push(light);
|
|
3778
4182
|
if (dark !== "") parts.push(dark);
|
|
3779
4183
|
if (smoothing !== "") parts.push(smoothing);
|
|
3780
|
-
return parts.join("\n");
|
|
4184
|
+
return hardenDeltaRefs(parts.join("\n"));
|
|
3781
4185
|
}
|
|
3782
4186
|
|
|
3783
4187
|
/**
|
|
@@ -4222,6 +4626,24 @@ var shared = {
|
|
|
4222
4626
|
LINE_HEIGHT_MAX: LINE_HEIGHT_MAX,
|
|
4223
4627
|
CODE_LINE_HEIGHT_MIN: CODE_LINE_HEIGHT_MIN,
|
|
4224
4628
|
CODE_LINE_HEIGHT_MAX: CODE_LINE_HEIGHT_MAX,
|
|
4629
|
+
LETTER_SPACING_DIALOG_FIELD: LETTER_SPACING_DIALOG_FIELD,
|
|
4630
|
+
WORD_SPACING_DIALOG_FIELD: WORD_SPACING_DIALOG_FIELD,
|
|
4631
|
+
PARAGRAPH_SPACING_DIALOG_FIELD: PARAGRAPH_SPACING_DIALOG_FIELD,
|
|
4632
|
+
INDENT_DIALOG_FIELD: INDENT_DIALOG_FIELD,
|
|
4633
|
+
LINE_BREAK_STRICT_FIELD: LINE_BREAK_STRICT_FIELD,
|
|
4634
|
+
LETTER_SPACING_MIN: LETTER_SPACING_MIN,
|
|
4635
|
+
LETTER_SPACING_MAX: LETTER_SPACING_MAX,
|
|
4636
|
+
LETTER_SPACING_STEP: LETTER_SPACING_STEP,
|
|
4637
|
+
WORD_SPACING_MIN: WORD_SPACING_MIN,
|
|
4638
|
+
WORD_SPACING_MAX: WORD_SPACING_MAX,
|
|
4639
|
+
WORD_SPACING_STEP: WORD_SPACING_STEP,
|
|
4640
|
+
PARAGRAPH_SPACING_MIN: PARAGRAPH_SPACING_MIN,
|
|
4641
|
+
PARAGRAPH_SPACING_MAX: PARAGRAPH_SPACING_MAX,
|
|
4642
|
+
PARAGRAPH_SPACING_STEP: PARAGRAPH_SPACING_STEP,
|
|
4643
|
+
INDENT_MIN: INDENT_MIN,
|
|
4644
|
+
INDENT_MAX: INDENT_MAX,
|
|
4645
|
+
INDENT_STEP: INDENT_STEP,
|
|
4646
|
+
PARAGRAPH_BASELINE_PX: PARAGRAPH_BASELINE_PX,
|
|
4225
4647
|
LIGATURES_DEFAULT: LIGATURES_DEFAULT,
|
|
4226
4648
|
SMOOTHING_FIELD: SMOOTHING_FIELD,
|
|
4227
4649
|
SMOOTHING_AUTO: SMOOTHING_AUTO,
|
|
@@ -4299,6 +4721,19 @@ var shared = {
|
|
|
4299
4721
|
WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
|
|
4300
4722
|
WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
|
|
4301
4723
|
WEIGHT_MIN_POSITIONS: WEIGHT_MIN_POSITIONS,
|
|
4724
|
+
VARIABLE_ALIAS_PREFIX: VARIABLE_ALIAS_PREFIX,
|
|
4725
|
+
CONTINUITY_WEIGHTS: CONTINUITY_WEIGHTS,
|
|
4726
|
+
isRootScopeSelector: isRootScopeSelector,
|
|
4727
|
+
hardenDeltaRefs: hardenDeltaRefs,
|
|
4728
|
+
CONTINUOUS_CANDIDATES: CONTINUOUS_CANDIDATES,
|
|
4729
|
+
distinctCount: distinctCount,
|
|
4730
|
+
respondsFinerThan: respondsFinerThan,
|
|
4731
|
+
variableAliasFor: variableAliasFor,
|
|
4732
|
+
variableFaceRule: variableFaceRule,
|
|
4733
|
+
buildVariableFaces: buildVariableFaces,
|
|
4734
|
+
withVariableFaces: withVariableFaces,
|
|
4735
|
+
isContinuousResponse: isContinuousResponse,
|
|
4736
|
+
continuousWeightProfile: continuousWeightProfile,
|
|
4302
4737
|
WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
|
|
4303
4738
|
WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
|
|
4304
4739
|
WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
|
|
@@ -4375,6 +4810,23 @@ var WEIGHT_DIALOG_FIELD = shared.WEIGHT_DIALOG_FIELD;
|
|
|
4375
4810
|
var CODE_WEIGHT_FIELD = shared.CODE_WEIGHT_FIELD;
|
|
4376
4811
|
var LINE_HEIGHT_DIALOG_FIELD = shared.LINE_HEIGHT_DIALOG_FIELD;
|
|
4377
4812
|
var CODE_LINE_HEIGHT_FIELD = shared.CODE_LINE_HEIGHT_FIELD;
|
|
4813
|
+
var LETTER_SPACING_DIALOG_FIELD = shared.LETTER_SPACING_DIALOG_FIELD;
|
|
4814
|
+
var WORD_SPACING_DIALOG_FIELD = shared.WORD_SPACING_DIALOG_FIELD;
|
|
4815
|
+
var PARAGRAPH_SPACING_DIALOG_FIELD = shared.PARAGRAPH_SPACING_DIALOG_FIELD;
|
|
4816
|
+
var INDENT_DIALOG_FIELD = shared.INDENT_DIALOG_FIELD;
|
|
4817
|
+
var LINE_BREAK_STRICT_FIELD = shared.LINE_BREAK_STRICT_FIELD;
|
|
4818
|
+
var LETTER_SPACING_MIN = shared.LETTER_SPACING_MIN;
|
|
4819
|
+
var LETTER_SPACING_MAX = shared.LETTER_SPACING_MAX;
|
|
4820
|
+
var LETTER_SPACING_STEP = shared.LETTER_SPACING_STEP;
|
|
4821
|
+
var WORD_SPACING_MIN = shared.WORD_SPACING_MIN;
|
|
4822
|
+
var WORD_SPACING_MAX = shared.WORD_SPACING_MAX;
|
|
4823
|
+
var WORD_SPACING_STEP = shared.WORD_SPACING_STEP;
|
|
4824
|
+
var PARAGRAPH_SPACING_MIN = shared.PARAGRAPH_SPACING_MIN;
|
|
4825
|
+
var PARAGRAPH_SPACING_MAX = shared.PARAGRAPH_SPACING_MAX;
|
|
4826
|
+
var PARAGRAPH_SPACING_STEP = shared.PARAGRAPH_SPACING_STEP;
|
|
4827
|
+
var INDENT_MIN = shared.INDENT_MIN;
|
|
4828
|
+
var INDENT_MAX = shared.INDENT_MAX;
|
|
4829
|
+
var INDENT_STEP = shared.INDENT_STEP;
|
|
4378
4830
|
var LIGATURES_FIELD = shared.LIGATURES_FIELD;
|
|
4379
4831
|
var FEATURES_FIELD = shared.FEATURES_FIELD;
|
|
4380
4832
|
var NO_SYNTHETIC_ITALIC_FIELD = shared.NO_SYNTHETIC_ITALIC_FIELD;
|
|
@@ -4577,7 +5029,7 @@ var DICTS = {
|
|
|
4577
5029
|
en: {
|
|
4578
5030
|
"card.title": "Font tune",
|
|
4579
5031
|
"card.description":
|
|
4580
|
-
"Interface and conversation fonts,
|
|
5032
|
+
"Interface and conversation fonts, size, weight and line height, CJK type shaping, code extras and presets",
|
|
4581
5033
|
"card.expand": "Expand",
|
|
4582
5034
|
"card.collapse": "Collapse",
|
|
4583
5035
|
"card.resetAll": "Reset all",
|
|
@@ -4640,13 +5092,13 @@ var DICTS = {
|
|
|
4640
5092
|
|
|
4641
5093
|
"weight.uiLabel": "Interface font-weight offset",
|
|
4642
5094
|
"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
|
|
5095
|
+
"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
5096
|
"weight.dialogLabel": "Conversation font-weight offset",
|
|
4645
5097
|
"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
|
|
5098
|
+
"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
5099
|
"weight.codeLabel": "Code font-weight offset",
|
|
4648
5100
|
"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
|
|
5101
|
+
"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
5102
|
|
|
4651
5103
|
"line.dialogLabel": "Conversation line height",
|
|
4652
5104
|
"line.dialogHint":
|
|
@@ -4656,6 +5108,37 @@ var DICTS = {
|
|
|
4656
5108
|
"Adds {offset} to code line heights only; 0 keeps DSH's own line heights.",
|
|
4657
5109
|
"line.unit": "%",
|
|
4658
5110
|
|
|
5111
|
+
"shape.letterLabel": "Letter spacing",
|
|
5112
|
+
"shape.letterHint":
|
|
5113
|
+
"Adds {offset} to the conversation's own letter spacing; 0 keeps DSH's own tracking. Positive values open the text up, negative values tighten it.",
|
|
5114
|
+
"shape.wordLabel": "Word spacing",
|
|
5115
|
+
"shape.wordHint":
|
|
5116
|
+
"Adds {offset} to the gaps between words in the conversation; 0 keeps DSH's own word gaps. Only Latin text has word gaps to widen.",
|
|
5117
|
+
"shape.paragraphLabel": "Paragraph spacing",
|
|
5118
|
+
"shape.paragraphHint":
|
|
5119
|
+
"Adds {offset} to the space between conversation paragraphs; 0 keeps DSH's own paragraph spacing. Headings keep their own spacing either way.",
|
|
5120
|
+
"shape.indentLabel": "First-line indent",
|
|
5121
|
+
"shape.indentHint":
|
|
5122
|
+
"Indents the first line of each conversation paragraph by {offset}; 0 leaves paragraphs flush left. Paragraphs only — headings, lists and code keep their own alignment.",
|
|
5123
|
+
"shape.breakLabel": "Strict line breaking",
|
|
5124
|
+
"shape.breakHint":
|
|
5125
|
+
"Keeps Chinese punctuation and small kana from starting a line when the text wraps.",
|
|
5126
|
+
"shape.off": "Off",
|
|
5127
|
+
"shape.on": "On",
|
|
5128
|
+
|
|
5129
|
+
"status.title": "In force",
|
|
5130
|
+
"status.dialog": "Conversation",
|
|
5131
|
+
"status.ui": "Interface",
|
|
5132
|
+
"status.code": "Code",
|
|
5133
|
+
"status.weight": "Weight slider",
|
|
5134
|
+
"status.steps": "{count} steps",
|
|
5135
|
+
"status.continuous": "fine",
|
|
5136
|
+
"status.hostDefault": "DSH's own",
|
|
5137
|
+
"status.followDialog": "follows the conversation",
|
|
5138
|
+
"status.usesHost": "not set here: DSH's own font",
|
|
5139
|
+
"status.usesInterface": "not set on its own: uses the interface font",
|
|
5140
|
+
"status.uiFollowsNote": "the interface uses the conversation's font",
|
|
5141
|
+
|
|
4659
5142
|
"lig.label": "Code ligatures",
|
|
4660
5143
|
"lig.hint":
|
|
4661
5144
|
"Programming ligatures render multiple characters as one glyph (=> as an arrow, != as ≠). Browsers enable them by default.",
|
|
@@ -4723,6 +5206,7 @@ var DICTS = {
|
|
|
4723
5206
|
"preview.monoCaption": "Code",
|
|
4724
5207
|
"preview.sample":
|
|
4725
5208
|
"The quick brown fox jumps over the lazy dog — 中文排版预览,标点符号,数字 0123456789。",
|
|
5209
|
+
"preview.sampleSecond": "第二段:段距与首行缩进在这里显现出来,正文其余部分不变。",
|
|
4726
5210
|
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
4727
5211
|
|
|
4728
5212
|
"panel.dot": "Tuning panel",
|
|
@@ -4741,7 +5225,7 @@ var DICTS = {
|
|
|
4741
5225
|
zh: {
|
|
4742
5226
|
"card.title": "字体增强",
|
|
4743
5227
|
"card.description":
|
|
4744
|
-
"
|
|
5228
|
+
"界面与对话字体、字号/字重/行高,字距/段距/首行缩进等中文排印,代码增强与预设方案",
|
|
4745
5229
|
"card.expand": "展开",
|
|
4746
5230
|
"card.collapse": "收起",
|
|
4747
5231
|
"card.resetAll": "全部重置",
|
|
@@ -4802,13 +5286,13 @@ var DICTS = {
|
|
|
4802
5286
|
|
|
4803
5287
|
"weight.uiLabel": "界面字重偏移",
|
|
4804
5288
|
"weight.uiHint":
|
|
4805
|
-
"在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
5289
|
+
"在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会变粗或变细;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
4806
5290
|
"weight.dialogLabel": "对话字重偏移",
|
|
4807
5291
|
"weight.dialogHint":
|
|
4808
|
-
"在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
5292
|
+
"在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会变粗或变细;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
4809
5293
|
"weight.codeLabel": "代码字重偏移",
|
|
4810
5294
|
"weight.codeHint":
|
|
4811
|
-
"在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
5295
|
+
"在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会变粗或变细;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
4812
5296
|
|
|
4813
5297
|
"line.dialogLabel": "对话行高",
|
|
4814
5298
|
"line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
|
|
@@ -4816,6 +5300,36 @@ var DICTS = {
|
|
|
4816
5300
|
"line.codeHint": "只给代码行高加 {offset}px;0 表示保持原样。",
|
|
4817
5301
|
"line.unit": "%",
|
|
4818
5302
|
|
|
5303
|
+
"shape.letterLabel": "字距",
|
|
5304
|
+
"shape.letterHint":
|
|
5305
|
+
"给对话自身的字距加 {offset};0 表示保持 DSH 原样。正值更疏、负值更紧。",
|
|
5306
|
+
"shape.wordLabel": "词距",
|
|
5307
|
+
"shape.wordHint":
|
|
5308
|
+
"给对话里词与词之间的间隔加 {offset};0 表示保持 DSH 原样。只有西文有词间空隙可调。",
|
|
5309
|
+
"shape.paragraphLabel": "段距",
|
|
5310
|
+
"shape.paragraphHint":
|
|
5311
|
+
"给对话段落之间的间隔加 {offset};0 表示保持 DSH 原样。标题自身的间距不受影响。",
|
|
5312
|
+
"shape.indentLabel": "首行缩进",
|
|
5313
|
+
"shape.indentHint":
|
|
5314
|
+
"把对话每段的第一行缩进 {offset};0 表示与 DSH 一样顶格。只缩段落——标题、列表与代码保持原样对齐。",
|
|
5315
|
+
"shape.breakLabel": "严格避头尾",
|
|
5316
|
+
"shape.breakHint": "换行时不让中文标点、小假名等出现在行首。",
|
|
5317
|
+
"shape.off": "关闭",
|
|
5318
|
+
"shape.on": "开启",
|
|
5319
|
+
|
|
5320
|
+
"status.title": "当前生效",
|
|
5321
|
+
"status.dialog": "对话",
|
|
5322
|
+
"status.ui": "界面",
|
|
5323
|
+
"status.code": "代码",
|
|
5324
|
+
"status.weight": "字重滑块",
|
|
5325
|
+
"status.steps": "{count} 档",
|
|
5326
|
+
"status.continuous": "细档",
|
|
5327
|
+
"status.hostDefault": "DSH 自带",
|
|
5328
|
+
"status.followDialog": "跟随对话",
|
|
5329
|
+
"status.usesHost": "本插件未设置,用 DSH 自带的字体",
|
|
5330
|
+
"status.usesInterface": "未单独设置,沿用界面字体",
|
|
5331
|
+
"status.uiFollowsNote": "界面沿用对话的字体",
|
|
5332
|
+
|
|
4819
5333
|
"lig.label": "代码连字",
|
|
4820
5334
|
"lig.hint":
|
|
4821
5335
|
"把多个字符连成一个字形(如 => 变箭头、!= 变 ≠)。浏览器默认就是开启的。",
|
|
@@ -4882,6 +5396,8 @@ var DICTS = {
|
|
|
4882
5396
|
"preview.monoCaption": "代码",
|
|
4883
5397
|
"preview.sample":
|
|
4884
5398
|
"The quick brown fox jumps over the lazy dog —— 中文排版预览,标点符号,数字 0123456789。",
|
|
5399
|
+
"preview.sampleSecond":
|
|
5400
|
+
"Second paragraph: the paragraph gap and the first-line indent show up here, and nothing else in the text changes.",
|
|
4885
5401
|
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
4886
5402
|
|
|
4887
5403
|
"panel.dot": "调参面板",
|
|
@@ -5118,7 +5634,11 @@ var CARD_CSS = [
|
|
|
5118
5634
|
// IS the card's corner: the clip, the scale and the fade still ride the card
|
|
5119
5635
|
// (and every measurement still reads it), while the anchored edge, the size
|
|
5120
5636
|
// and the shadow belong to the wrapper around it.
|
|
5121
|
-
".dfp-floatCard{position:relative;box-sizing:border-box;width:100%;height:100%;max-height:calc(100vh - 24px);overflow:hidden;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);border-radius:16px;padding:10px 12px 12px;pointer-events:auto;clip-path:inset(0px 0px 0px 0px round 16px);transition:clip-path .28s cubic-bezier(.22,1,.28,1),opacity .28s cubic-bezier(.22,1,.28,1),transform .28s cubic-bezier(.22,1,.28,1)}",
|
|
5637
|
+
".dfp-floatCard{position:relative;box-sizing:border-box;display:flex;flex-direction:column;width:100%;height:100%;max-height:calc(100vh - 24px);overflow:hidden;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);border-radius:16px;padding:10px 12px 12px;pointer-events:auto;clip-path:inset(0px 0px 0px 0px round 16px);transition:clip-path .28s cubic-bezier(.22,1,.28,1),opacity .28s cubic-bezier(.22,1,.28,1),transform .28s cubic-bezier(.22,1,.28,1)}",
|
|
5638
|
+
// The body owns the scrolling: seven sliders and the status block do not fit a panel
|
|
5639
|
+
// that was resized to its minimum, and a clipped slider is worse than a scrollbar.
|
|
5640
|
+
".dfp-floatHead{flex:0 0 auto}",
|
|
5641
|
+
".dfp-floatBody{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column}",
|
|
5122
5642
|
// the enter frame: the card is clipped to the dot's own rectangle, a hair
|
|
5123
5643
|
// smaller and dimmer, and the transition (started by dropping this class two
|
|
5124
5644
|
// frames later) carries all three home at once. Once it has arrived the card
|
|
@@ -5174,6 +5694,16 @@ var CARD_CSS = [
|
|
|
5174
5694
|
// the write-failure line takes no room at all while it carries no message
|
|
5175
5695
|
".dfp-floatStatus{margin-top:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
5176
5696
|
".dfp-floatStatus:empty{display:none}",
|
|
5697
|
+
".dfp-floatInfo{margin-top:2px;padding-top:6px;border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
5698
|
+
".dfp-floatInfoHead{display:flex;align-items:center;justify-content:space-between;gap:6px;width:100%;margin:0;padding:2px 0;border:0;background:none;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;text-align:left}",
|
|
5699
|
+
".dfp-floatInfoHead:hover .dfp-chevron,.dfp-floatInfoHead:hover .dfp-floatInfoTitle{color:var(--dsw-alias-label-secondary)}",
|
|
5700
|
+
".dfp-floatInfoClip{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .24s cubic-bezier(.22,1,.28,1),opacity .24s cubic-bezier(.22,1,.28,1)}",
|
|
5701
|
+
".dfp-floatInfoOpen .dfp-floatInfoClip{grid-template-rows:1fr;opacity:1}",
|
|
5702
|
+
".dfp-floatInfoLines{min-height:0;overflow:hidden}",
|
|
5703
|
+
".dfp-floatInfoLine{display:flex;align-items:baseline;gap:6px;margin-top:2px}",
|
|
5704
|
+
".dfp-floatInfoKey{flex:0 0 auto;opacity:.75}",
|
|
5705
|
+
".dfp-floatInfoValue{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary)}",
|
|
5706
|
+
".dfp-floatInfoFlag{flex:0 0 auto;padding:0 6px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}",
|
|
5177
5707
|
].join("");
|
|
5178
5708
|
|
|
5179
5709
|
/**
|
|
@@ -5223,6 +5753,24 @@ function isOwnSheet(sheet) {
|
|
|
5223
5753
|
}
|
|
5224
5754
|
}
|
|
5225
5755
|
|
|
5756
|
+
/**
|
|
5757
|
+
* Whether a style rule declares its custom properties for the WHOLE page.
|
|
5758
|
+
*
|
|
5759
|
+
* The design tokens live on `:root`, `html` or `body`. A rule scoped to a class
|
|
5760
|
+
* declares a LOCAL value — another plugin's module stylesheet may reuse one of the
|
|
5761
|
+
* token names for its own compact panel, and reading that as the base hands the
|
|
5762
|
+
* conversation a size that panel chose for itself.
|
|
5763
|
+
* @param {CSSStyleRule} rule - any rule from a document stylesheet.
|
|
5764
|
+
* @returns {boolean} true when every selector in the rule is a root scope.
|
|
5765
|
+
*/
|
|
5766
|
+
function isRootScopedRule(rule) {
|
|
5767
|
+
try {
|
|
5768
|
+
return shared.isRootScopeSelector(rule.selectorText);
|
|
5769
|
+
} catch (error) {
|
|
5770
|
+
return false;
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
|
|
5226
5774
|
/**
|
|
5227
5775
|
* Snapshot the typography tokens the size offsets scale.
|
|
5228
5776
|
*
|
|
@@ -5238,6 +5786,13 @@ function isOwnSheet(sheet) {
|
|
|
5238
5786
|
* collected too: they are what the shipped stylesheets actually consume, and
|
|
5239
5787
|
* the line-height axes rebuild their inline heights.
|
|
5240
5788
|
*
|
|
5789
|
+
* Declarations come in two ranks, because a token name is not owned by the design
|
|
5790
|
+
* system alone: `dsh-better-sidebar` redefines `--dsw-font-markdown-h1` as a compact
|
|
5791
|
+
* `600 13px/18px` on its own panel class, and reading that as the base is how every
|
|
5792
|
+
* conversation heading shrank once the line-height axis rebuilt those shorthands. A
|
|
5793
|
+
* ROOT-scoped declaration is the page's value; a locally scoped one only fills a gap
|
|
5794
|
+
* (a host line that declares its tokens nowhere else still gets them).
|
|
5795
|
+
*
|
|
5241
5796
|
* @returns {Record<string, string>} token name to its untouched value.
|
|
5242
5797
|
*/
|
|
5243
5798
|
function readBaseTokens() {
|
|
@@ -5252,6 +5807,11 @@ function readBaseTokens() {
|
|
|
5252
5807
|
|
|
5253
5808
|
try {
|
|
5254
5809
|
var sheets = document.styleSheets;
|
|
5810
|
+
// A local (class-scoped) declaration is kept APART and only fills gaps: another
|
|
5811
|
+
// plugin may reuse a token name for its own panel, so it must never override the
|
|
5812
|
+
// page's own value — but a host line that declares its tokens nowhere else still
|
|
5813
|
+
// needs them. Root-scoped declarations always win.
|
|
5814
|
+
var local = {};
|
|
5255
5815
|
for (var sheetIndex = 0; sheetIndex < sheets.length; sheetIndex += 1) {
|
|
5256
5816
|
if (isOwnSheet(sheets[sheetIndex])) continue;
|
|
5257
5817
|
var rules = null;
|
|
@@ -5262,16 +5822,24 @@ function readBaseTokens() {
|
|
|
5262
5822
|
}
|
|
5263
5823
|
if (!rules) continue;
|
|
5264
5824
|
for (var ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
|
|
5265
|
-
var
|
|
5825
|
+
var rule = rules[ruleIndex];
|
|
5826
|
+
var style = rule.style;
|
|
5266
5827
|
if (!style) continue;
|
|
5828
|
+
var rootScoped = isRootScopedRule(rule);
|
|
5267
5829
|
for (var propertyIndex = 0; propertyIndex < style.length; propertyIndex += 1) {
|
|
5268
5830
|
var property = style[propertyIndex];
|
|
5269
5831
|
if (property.slice(0, 2) !== "--" || !isScaledToken(property)) continue;
|
|
5270
5832
|
var declared = style.getPropertyValue(property).trim();
|
|
5271
|
-
if (declared
|
|
5833
|
+
if (declared === "") continue;
|
|
5834
|
+
if (rootScoped) out[property] = declared;
|
|
5835
|
+
else local[property] = declared;
|
|
5272
5836
|
}
|
|
5273
5837
|
}
|
|
5274
5838
|
}
|
|
5839
|
+
for (name in local) {
|
|
5840
|
+
if (!Object.prototype.hasOwnProperty.call(local, name)) continue;
|
|
5841
|
+
if (!Object.prototype.hasOwnProperty.call(out, name)) out[name] = local[name];
|
|
5842
|
+
}
|
|
5275
5843
|
} catch (error) {
|
|
5276
5844
|
// Keep what the earlier sources produced.
|
|
5277
5845
|
}
|
|
@@ -5345,7 +5913,11 @@ function createStylesheet(tokens) {
|
|
|
5345
5913
|
var lastCss = null;
|
|
5346
5914
|
return function apply(config) {
|
|
5347
5915
|
if (typeof document === "undefined") return;
|
|
5348
|
-
|
|
5916
|
+
// Continuous families (FT-060) bring their own faces: the aliases are declared
|
|
5917
|
+
// here, and `buildFontCss` leads the matching stacks with them.
|
|
5918
|
+
var faces = shared.buildVariableFaces(variableFamilies);
|
|
5919
|
+
var body = buildFontCss(config, tokens(), harvestScope(), variableFamilies);
|
|
5920
|
+
var css = faces === "" ? body : faces + "\n" + body;
|
|
5349
5921
|
if (css === lastCss && tag !== null && tag.isConnected) return;
|
|
5350
5922
|
lastCss = css;
|
|
5351
5923
|
if (tag === null || !tag.isConnected) {
|
|
@@ -5748,6 +6320,21 @@ var context = weightProbeCanvas.getContext("2d", { willReadFrequently: true });
|
|
|
5748
6320
|
return ink + "/" + advance;
|
|
5749
6321
|
}
|
|
5750
6322
|
|
|
6323
|
+
/**
|
|
6324
|
+
* The ink share of a weight signature, as a number.
|
|
6325
|
+
*
|
|
6326
|
+
* `sampleWeightSignature` answers "<ink>/<advance>": the advance is the laid-out
|
|
6327
|
+
* width, which a monospace family holds constant, so ink is the signal that still
|
|
6328
|
+
* moves for those. Unreadable signatures answer 0, which simply never counts as a
|
|
6329
|
+
* change.
|
|
6330
|
+
* @param {string} signature - a signature from `sampleWeightSignature`.
|
|
6331
|
+
* @returns {number} the summed alpha, or 0.
|
|
6332
|
+
*/
|
|
6333
|
+
function inkOfSignature(signature) {
|
|
6334
|
+
var ink = Number(String(signature).split("/")[0]);
|
|
6335
|
+
return isFinite(ink) ? ink : 0;
|
|
6336
|
+
}
|
|
6337
|
+
|
|
5751
6338
|
/**
|
|
5752
6339
|
* The slider shape to use for one family: its range and its notch.
|
|
5753
6340
|
*
|
|
@@ -5764,18 +6351,40 @@ var context = weightProbeCanvas.getContext("2d", { willReadFrequently: true });
|
|
|
5764
6351
|
*/
|
|
5765
6352
|
function weightProfileFor(stack, top) {
|
|
5766
6353
|
var base = typeof top === "number" ? top : shared.WEIGHT_LADDER_TOP;
|
|
5767
|
-
var
|
|
6354
|
+
var resolved = String(stack) === "" ? liveFamily() : String(stack);
|
|
6355
|
+
// `hasOwnProperty` rather than a truthiness test: the alias map is keyed by family
|
|
6356
|
+
// name, and a family called "constructor" must not read as promoted.
|
|
6357
|
+
var promoted = Object.prototype.hasOwnProperty.call(variableFamilies, primaryFamilyOf(resolved));
|
|
6358
|
+
var alias = promoted ? variableFamilies[primaryFamilyOf(resolved)] : "";
|
|
6359
|
+
// The promotion state is part of the key. The warm-up measures a family BEFORE the
|
|
6360
|
+
// probe has finished (the probe awaits a font load), so without it the stepped shape
|
|
6361
|
+
// measured first would be served from the cache forever after the family was
|
|
6362
|
+
// promoted — the card then keeps offering the coarse notches the mode replaced.
|
|
6363
|
+
var key = String(stack) + "\u0000" + base + "\u0000" + (promoted ? "continuous" : "stepped");
|
|
5768
6364
|
if (Object.prototype.hasOwnProperty.call(weightProfileCache, key)) {
|
|
5769
6365
|
return weightProfileCache[key];
|
|
5770
6366
|
}
|
|
5771
|
-
var profile =
|
|
6367
|
+
var profile = promoted
|
|
6368
|
+
? shared.continuousWeightProfile()
|
|
6369
|
+
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
5772
6370
|
try {
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
6371
|
+
// A continuous family is measured THROUGH ITS ALIAS, on the fine candidate grid:
|
|
6372
|
+
// the axis answers at every notch, so the finest one that still moves body text
|
|
6373
|
+
// wins, and the range stops where the family's own axis does instead of running
|
|
6374
|
+
// past it into weights that render the same.
|
|
6375
|
+
var target = promoted ? '"' + alias + '"' : resolved;
|
|
6376
|
+
var seen = {};
|
|
6377
|
+
var sample = function (weight) {
|
|
6378
|
+
if (!Object.prototype.hasOwnProperty.call(seen, weight)) {
|
|
6379
|
+
seen[weight] = sampleWeightSignature(target, weight);
|
|
6380
|
+
}
|
|
6381
|
+
return seen[weight];
|
|
6382
|
+
};
|
|
6383
|
+
profile = weightProfileFrom(sample, promoted ? { top: base, candidates: shared.CONTINUOUS_CANDIDATES } : { top: base });
|
|
5777
6384
|
} catch (error) {
|
|
5778
|
-
profile =
|
|
6385
|
+
profile = promoted
|
|
6386
|
+
? shared.continuousWeightProfile()
|
|
6387
|
+
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
5779
6388
|
}
|
|
5780
6389
|
weightProfileCache[key] = profile;
|
|
5781
6390
|
return profile;
|
|
@@ -5837,14 +6446,17 @@ var scheduleWeightWarm = function (editing) {
|
|
|
5837
6446
|
warmSignature = signature;
|
|
5838
6447
|
var jobs = [
|
|
5839
6448
|
function () {
|
|
6449
|
+
probeContinuity(stacks.dialog);
|
|
5840
6450
|
weightProfileFor(stacks.dialog, shared.WEIGHT_LADDER_STRONG);
|
|
5841
6451
|
probeLog("weight-warm", { axis: "dialog", stack: stacks.dialog });
|
|
5842
6452
|
},
|
|
5843
6453
|
function () {
|
|
6454
|
+
probeContinuity(stacks.ui);
|
|
5844
6455
|
weightProfileFor(stacks.ui, shared.WEIGHT_LADDER_STRONG);
|
|
5845
6456
|
probeLog("weight-warm", { axis: "ui", stack: stacks.ui });
|
|
5846
6457
|
},
|
|
5847
6458
|
function () {
|
|
6459
|
+
probeContinuity(stacks.code);
|
|
5848
6460
|
weightProfileFor(stacks.code, shared.WEIGHT_BASE);
|
|
5849
6461
|
probeLog("weight-warm", { axis: "code", stack: stacks.code });
|
|
5850
6462
|
},
|
|
@@ -5858,6 +6470,201 @@ var scheduleWeightWarm = function (editing) {
|
|
|
5858
6470
|
deferWeightWarm(runNext);
|
|
5859
6471
|
};
|
|
5860
6472
|
|
|
6473
|
+
/**
|
|
6474
|
+
* Families measured as continuous, family → alias (see FT-060).
|
|
6475
|
+
*
|
|
6476
|
+
* Filled by `probeContinuity` once a family has proven, in THIS page's own
|
|
6477
|
+
* rendering, that it moves in small steps through its variable axis. A family
|
|
6478
|
+
* that is not in here keeps the measured notch shape it always had.
|
|
6479
|
+
*/
|
|
6480
|
+
var variableFamilies = {};
|
|
6481
|
+
var continuityProbed = {};
|
|
6482
|
+
|
|
6483
|
+
/**
|
|
6484
|
+
* What to re-render when a family is promoted.
|
|
6485
|
+
*
|
|
6486
|
+
* The promotion lands a second or so after load, while the floating panel may already
|
|
6487
|
+
* be on screen with its weight row built from the stepped shape. Rebuilding the
|
|
6488
|
+
* stylesheet is not enough — the row caches the shape it was rendered with, so the
|
|
6489
|
+
* panel would keep offering the coarse notches until something else happened to
|
|
6490
|
+
* redraw it. Each mounted panel pushes its own render here and takes it back on
|
|
6491
|
+
* destroy.
|
|
6492
|
+
*/
|
|
6493
|
+
var promotionListeners = [];
|
|
6494
|
+
|
|
6495
|
+
/** Redraw whatever is showing a weight shape that the promotion just changed. */
|
|
6496
|
+
function notifyPromoted() {
|
|
6497
|
+
var pending = promotionListeners.slice();
|
|
6498
|
+
for (var index = 0; index < pending.length; index += 1) {
|
|
6499
|
+
try {
|
|
6500
|
+
pending[index]();
|
|
6501
|
+
} catch (error) {
|
|
6502
|
+
// A listener must never break the probe.
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
|
|
6507
|
+
/**
|
|
6508
|
+
* A stamp for the build the page is running.
|
|
6509
|
+
*
|
|
6510
|
+
* The client script is cached, and "I refreshed" has been wrong before: this is what
|
|
6511
|
+
* a live page reports about itself (the panel host carries it as `data-dfp-build`).
|
|
6512
|
+
*/
|
|
6513
|
+
var CLIENT_BUILD = "2026-10-15-1";
|
|
6514
|
+
|
|
6515
|
+
/** The families this page promoted to continuous weight, in map order. */
|
|
6516
|
+
function continuousNames() {
|
|
6517
|
+
return Object.keys(variableFamilies);
|
|
6518
|
+
}
|
|
6519
|
+
|
|
6520
|
+
/**
|
|
6521
|
+
* Publish the promoted families on the panel host, when one is on screen.
|
|
6522
|
+
*
|
|
6523
|
+
* Promotion happens AFTER the first render (the probe awaits a font load), so the
|
|
6524
|
+
* attribute set during render is re-stamped here: the host element is then the one
|
|
6525
|
+
* place a reader — or a support session — can look without opening the console.
|
|
6526
|
+
*/
|
|
6527
|
+
function announceContinuous() {
|
|
6528
|
+
if (typeof document === "undefined") return;
|
|
6529
|
+
try {
|
|
6530
|
+
var nodes = document.querySelectorAll("[data-dfp-build]");
|
|
6531
|
+
var names = continuousNames().join(", ");
|
|
6532
|
+
for (var index = 0; index < nodes.length; index += 1) {
|
|
6533
|
+
if (names === "") nodes[index].removeAttribute("data-dfp-continuous");
|
|
6534
|
+
else nodes[index].setAttribute("data-dfp-continuous", names);
|
|
6535
|
+
}
|
|
6536
|
+
} catch (error) {
|
|
6537
|
+
// Diagnostics only: a host without attributes costs nothing.
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
|
|
6541
|
+
/**
|
|
6542
|
+
* The first family a stack names that is a REAL family, or "" when there is none.
|
|
6543
|
+
*
|
|
6544
|
+
* Our own alias families are skipped: once a family is promoted, any stack read back
|
|
6545
|
+
* from the page (`liveFamily()`) leads with the alias, and treating that as the
|
|
6546
|
+
* family name would miss the promotion map and hand back the stepped shape — which is
|
|
6547
|
+
* how the floating panel kept offering the coarse notches after the card moved on.
|
|
6548
|
+
* @param {string} stack - a formatted font-family value.
|
|
6549
|
+
* @returns {string} the family the stack really draws with.
|
|
6550
|
+
*/
|
|
6551
|
+
function primaryFamilyOf(stack) {
|
|
6552
|
+
var names = parseStack(stack);
|
|
6553
|
+
for (var index = 0; index < names.length; index += 1) {
|
|
6554
|
+
if (names[index].indexOf(shared.VARIABLE_ALIAS_PREFIX) !== 0) return names[index];
|
|
6555
|
+
}
|
|
6556
|
+
return "";
|
|
6557
|
+
}
|
|
6558
|
+
|
|
6559
|
+
/**
|
|
6560
|
+
* Ask the page whether a family is continuous under weight, once per family.
|
|
6561
|
+
*
|
|
6562
|
+
* The probe does what the page does: it installs the alias face for the family
|
|
6563
|
+
* (`local()` plus a weight range) and waits for the browser to load it. It then
|
|
6564
|
+
* asks TWO ways, both through that alias:
|
|
6565
|
+
*
|
|
6566
|
+
* * the laid-out width, in the real DOM — what a reader's eye follows in prose;
|
|
6567
|
+
* * the canvas ink, which is the only signal a monospace family ever moves on,
|
|
6568
|
+
* since its advance is the same at every weight.
|
|
6569
|
+
*
|
|
6570
|
+
* A family that answers continuously on either signal is promoted to
|
|
6571
|
+
* `variableFamilies`; the stylesheet is then rebuilt so the alias leads its
|
|
6572
|
+
* stacks. Anything that cannot be measured (no font loading API, a rejected load)
|
|
6573
|
+
* leaves the family as it was.
|
|
6574
|
+
* @param {string} stack - the axis's formatted font-family value.
|
|
6575
|
+
*/
|
|
6576
|
+
function probeContinuity(stack) {
|
|
6577
|
+
var family = primaryFamilyOf(stack);
|
|
6578
|
+
if (family === "" || continuityProbed[family] === true) return;
|
|
6579
|
+
continuityProbed[family] = true;
|
|
6580
|
+
if (typeof document === "undefined" || !document.fonts || typeof document.fonts.load !== "function") return;
|
|
6581
|
+
var alias = shared.variableAliasFor(family);
|
|
6582
|
+
var style = document.createElement("style");
|
|
6583
|
+
style.setAttribute("data-dfp-continuity", alias);
|
|
6584
|
+
style.textContent = shared.variableFaceRule(family, alias);
|
|
6585
|
+
document.head.appendChild(style);
|
|
6586
|
+
var host = document.createElement("span");
|
|
6587
|
+
host.style.cssText =
|
|
6588
|
+
"position:fixed;left:-10000px;top:0;white-space:nowrap;font-size:64px;font-family:\"" + alias + "\"";
|
|
6589
|
+
host.textContent = WEIGHT_SAMPLE_TEXT;
|
|
6590
|
+
document.body.appendChild(host);
|
|
6591
|
+
var keepFace = false;
|
|
6592
|
+
var finish = function () {
|
|
6593
|
+
host.remove();
|
|
6594
|
+
// A promoted family keeps its face: the stylesheet will declare the same alias,
|
|
6595
|
+
// and the weight profile is measured THROUGH it after promotion, so removing the
|
|
6596
|
+
// rule here would leave that measurement drawing with the fallback.
|
|
6597
|
+
if (!keepFace) style.remove();
|
|
6598
|
+
};
|
|
6599
|
+
// Two signals, because neither covers every family on its own: the DOM width
|
|
6600
|
+
// moves for a proportional font, while a monospace font never changes width and
|
|
6601
|
+
// can only be seen in the canvas ink. The ink is also read through the FAMILY
|
|
6602
|
+
// itself, and the two responses are compared: a family that snaps to named
|
|
6603
|
+
// instances answers identically either way, while a variable family moves through
|
|
6604
|
+
// its alias at weights where its own path already stopped.
|
|
6605
|
+
//
|
|
6606
|
+
// All values equal means the face has not taken effect yet (the page is still
|
|
6607
|
+
// drawing with its fallback), not that the family is stepped: a stepped family
|
|
6608
|
+
// still changes between its faces. Wait a beat and measure again, a few times at
|
|
6609
|
+
// most, so a slow face is not mistaken for a static one.
|
|
6610
|
+
var attempts = 0;
|
|
6611
|
+
var measure = function () {
|
|
6612
|
+
attempts += 1;
|
|
6613
|
+
// `!important` on purpose: the page's own weight rules are `!important`, and a
|
|
6614
|
+
// plain inline weight loses to them, which measured every width as one value.
|
|
6615
|
+
var widths = shared.CONTINUITY_WEIGHTS.map(function (weight) {
|
|
6616
|
+
host.style.setProperty("font-weight", String(weight), "important");
|
|
6617
|
+
return host.getBoundingClientRect().width;
|
|
6618
|
+
});
|
|
6619
|
+
var inks = shared.CONTINUITY_WEIGHTS.map(function (weight) {
|
|
6620
|
+
return inkOfSignature(sampleWeightSignature('"' + alias + '"', weight));
|
|
6621
|
+
});
|
|
6622
|
+
var plainInks = shared.CONTINUITY_WEIGHTS.map(function (weight) {
|
|
6623
|
+
return inkOfSignature(sampleWeightSignature('"' + family + '"', weight));
|
|
6624
|
+
});
|
|
6625
|
+
var unchanged = function (list) {
|
|
6626
|
+
return list.every(function (value) {
|
|
6627
|
+
return value === list[0];
|
|
6628
|
+
});
|
|
6629
|
+
};
|
|
6630
|
+
if (unchanged(widths) && unchanged(inks) && attempts < 8) {
|
|
6631
|
+
globalThis.setTimeout(measure, 80);
|
|
6632
|
+
return;
|
|
6633
|
+
}
|
|
6634
|
+
// The alias must answer more finely than the family itself. A family that snaps
|
|
6635
|
+
// to named instances cannot: both responses hold the same values.
|
|
6636
|
+
var finer = shared.respondsFinerThan(inks, plainInks);
|
|
6637
|
+
var continuous = finer || shared.isContinuousResponse(widths);
|
|
6638
|
+
keepFace = continuous;
|
|
6639
|
+
finish();
|
|
6640
|
+
probeLog("continuity", {
|
|
6641
|
+
family: family,
|
|
6642
|
+
continuous: continuous,
|
|
6643
|
+
attempts: attempts,
|
|
6644
|
+
byWidth: shared.isContinuousResponse(widths),
|
|
6645
|
+
aliasInk: shared.distinctCount(inks),
|
|
6646
|
+
plainInk: shared.distinctCount(plainInks),
|
|
6647
|
+
widths: widths.map(function (width) {
|
|
6648
|
+
return Math.round(width * 100) / 100;
|
|
6649
|
+
}),
|
|
6650
|
+
});
|
|
6651
|
+
if (!continuous) return;
|
|
6652
|
+
variableFamilies[family] = alias;
|
|
6653
|
+
announceContinuous();
|
|
6654
|
+
notifyPromoted();
|
|
6655
|
+
if (typeof repaint === "function") repaint();
|
|
6656
|
+
};
|
|
6657
|
+
document.fonts.load('400 64px "' + alias + '"').then(
|
|
6658
|
+
function () {
|
|
6659
|
+
measure();
|
|
6660
|
+
},
|
|
6661
|
+
function () {
|
|
6662
|
+
finish();
|
|
6663
|
+
probeLog("continuity", { family: family, continuous: false, error: "load" });
|
|
6664
|
+
}
|
|
6665
|
+
);
|
|
6666
|
+
}
|
|
6667
|
+
|
|
5861
6668
|
/**
|
|
5862
6669
|
* The family the page actually renders with, as a `font-family` value.
|
|
5863
6670
|
* @returns {string} the computed family, or "" when unreadable.
|
|
@@ -5952,6 +6759,27 @@ function liveCodeFamily() {
|
|
|
5952
6759
|
}
|
|
5953
6760
|
}
|
|
5954
6761
|
|
|
6762
|
+
/**
|
|
6763
|
+
* The family the conversation renders with when the conversation axis sets none.
|
|
6764
|
+
*
|
|
6765
|
+
* Read from a real conversation surface rather than from `body`: the two can differ (the
|
|
6766
|
+
* interface's family is its own axis), and a panel that named the body's family while the
|
|
6767
|
+
* conversation drew with something else would be reporting the wrong surface.
|
|
6768
|
+
* @returns {string} the computed family, or "" when unreadable.
|
|
6769
|
+
*/
|
|
6770
|
+
function liveDialogFamily() {
|
|
6771
|
+
if (typeof document === "undefined") return "";
|
|
6772
|
+
try {
|
|
6773
|
+
// `dialogSelector()` is what the stylesheet itself scopes the conversation with, so
|
|
6774
|
+
// the sample is the same kind of element the rules land on.
|
|
6775
|
+
var sample = document.querySelector(shared.dialogSelector(null));
|
|
6776
|
+
if (sample === null) return liveFamily();
|
|
6777
|
+
return document.defaultView.getComputedStyle(sample).fontFamily || liveFamily();
|
|
6778
|
+
} catch (error) {
|
|
6779
|
+
return "";
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
|
|
5955
6783
|
/* ------------------------------------------------------------------ *
|
|
5956
6784
|
* hooks and controls
|
|
5957
6785
|
* ------------------------------------------------------------------ */
|
|
@@ -8041,6 +8869,102 @@ function FontCard(props) {
|
|
|
8041
8869
|
);
|
|
8042
8870
|
};
|
|
8043
8871
|
|
|
8872
|
+
// ---- conversation text shaping: tracking, word gap, paragraph gap, indent ----
|
|
8873
|
+
//
|
|
8874
|
+
// Four axes that move the shape of the conversation text rather than its face. Every
|
|
8875
|
+
// value here is an OFFSET over what DSH already ships, measured on the conversation
|
|
8876
|
+
// scope (2026-10-13): tracking `normal`, word gap `0px`, paragraph margins 16px, no
|
|
8877
|
+
// indent. 0 therefore means "leave DSH alone" and emits no rule at all, exactly like
|
|
8878
|
+
// the size and weight offsets.
|
|
8879
|
+
var spacingField = function (props) {
|
|
8880
|
+
var value = editing[props.field];
|
|
8881
|
+
var neutral = props.neutral === undefined ? 0 : props.neutral;
|
|
8882
|
+
var within = value !== neutral;
|
|
8883
|
+
var shown = function (number) {
|
|
8884
|
+
var text = props.decimals === 2 ? number.toFixed(2) : String(number);
|
|
8885
|
+
return (number > 0 ? "+" : "") + text + props.unit;
|
|
8886
|
+
};
|
|
8887
|
+
return h(
|
|
8888
|
+
FieldShell,
|
|
8889
|
+
{
|
|
8890
|
+
t: t,
|
|
8891
|
+
label: t(props.labelKey),
|
|
8892
|
+
// The hint names the value the reader is looking at: without the parameter the
|
|
8893
|
+
// literal `{offset}` ships to the screen (caught by a screenshot, not by a test).
|
|
8894
|
+
hint: t(props.hintKey, { offset: shown(value) }),
|
|
8895
|
+
overridden: overridden(props.field) && within,
|
|
8896
|
+
disabled: !writable,
|
|
8897
|
+
onReset: function () {
|
|
8898
|
+
resetField(props.field);
|
|
8899
|
+
},
|
|
8900
|
+
},
|
|
8901
|
+
h(NumberSlider, {
|
|
8902
|
+
min: props.min,
|
|
8903
|
+
max: props.max,
|
|
8904
|
+
step: props.step,
|
|
8905
|
+
value: value,
|
|
8906
|
+
disabled: !writable,
|
|
8907
|
+
label: t(props.labelKey),
|
|
8908
|
+
revertedText: t("write.reverted"),
|
|
8909
|
+
refusedToken: refused[props.field] || 0,
|
|
8910
|
+
readout: shown(value),
|
|
8911
|
+
minLabel: shown(props.min),
|
|
8912
|
+
maxLabel: shown(props.max),
|
|
8913
|
+
pendingText: shown,
|
|
8914
|
+
onChange: function (next) {
|
|
8915
|
+
setField(props.field, next);
|
|
8916
|
+
},
|
|
8917
|
+
})
|
|
8918
|
+
);
|
|
8919
|
+
};
|
|
8920
|
+
|
|
8921
|
+
/**
|
|
8922
|
+
* A switch for a property DSH itself leaves alone, so "off" writes nothing.
|
|
8923
|
+
* @param {{field: string, labelKey: string, hintKey: string}} props - the axis.
|
|
8924
|
+
* @returns {object} the row.
|
|
8925
|
+
*/
|
|
8926
|
+
var switchField = function (props) {
|
|
8927
|
+
var value = editing[props.field] === true;
|
|
8928
|
+
return h(
|
|
8929
|
+
FieldShell,
|
|
8930
|
+
{
|
|
8931
|
+
t: t,
|
|
8932
|
+
label: t(props.labelKey),
|
|
8933
|
+
hint: t(props.hintKey),
|
|
8934
|
+
overridden: overridden(props.field) && value,
|
|
8935
|
+
disabled: !writable,
|
|
8936
|
+
onReset: function () {
|
|
8937
|
+
resetField(props.field);
|
|
8938
|
+
},
|
|
8939
|
+
inline: true,
|
|
8940
|
+
},
|
|
8941
|
+
h(Segmented, {
|
|
8942
|
+
label: t(props.labelKey),
|
|
8943
|
+
value: value ? 1 : 0,
|
|
8944
|
+
options: [
|
|
8945
|
+
{ value: 0, label: t("shape.off") },
|
|
8946
|
+
{ value: 1, label: t("shape.on") },
|
|
8947
|
+
],
|
|
8948
|
+
onChange: function (next) {
|
|
8949
|
+
setField(props.field, next === 1);
|
|
8950
|
+
},
|
|
8951
|
+
})
|
|
8952
|
+
);
|
|
8953
|
+
};
|
|
8954
|
+
|
|
8955
|
+
/**
|
|
8956
|
+
* Strict CJK line breaking, as a switch: DSH leaves `line-break` at `auto`, so
|
|
8957
|
+
* "off" writes nothing and the reader keeps DSH's own breaking.
|
|
8958
|
+
* @returns {object} the row.
|
|
8959
|
+
*/
|
|
8960
|
+
var lineBreakField = function () {
|
|
8961
|
+
return switchField({
|
|
8962
|
+
field: LINE_BREAK_STRICT_FIELD,
|
|
8963
|
+
labelKey: "shape.breakLabel",
|
|
8964
|
+
hintKey: "shape.breakHint",
|
|
8965
|
+
});
|
|
8966
|
+
};
|
|
8967
|
+
|
|
8044
8968
|
// ---- ligature segmented control ----
|
|
8045
8969
|
var ligatureField = function () {
|
|
8046
8970
|
var value = editing[LIGATURES_FIELD];
|
|
@@ -8445,12 +9369,17 @@ function FontCard(props) {
|
|
|
8445
9369
|
// nothing else — the weight, size and line height all come from the
|
|
8446
9370
|
// conversation section.
|
|
8447
9371
|
var previewUiStyle = {};
|
|
8448
|
-
if (uiStack !== "") previewUiStyle.fontFamily = uiStack;
|
|
9372
|
+
if (uiStack !== "") previewUiStyle.fontFamily = shared.withVariableFaces(uiStack, variableFamilies);
|
|
8449
9373
|
var previewDialogStyle = {};
|
|
8450
9374
|
// Independent and unset: the conversation keeps DSH's own family, so the
|
|
8451
9375
|
// preview shows the theme's default rather than the interface stack the page
|
|
8452
9376
|
// rule would otherwise paint into this box.
|
|
8453
|
-
|
|
9377
|
+
//
|
|
9378
|
+
// The alias leads these stacks exactly as it leads the real surfaces: without it
|
|
9379
|
+
// the preview would draw through the system family, where the fine notches snap to
|
|
9380
|
+
// the same named instance — so a notch that visibly moves the conversation would
|
|
9381
|
+
// do nothing here.
|
|
9382
|
+
if (dialogStack !== "") previewDialogStyle.fontFamily = shared.withVariableFaces(dialogStack, variableFamilies);
|
|
8454
9383
|
previewDialogStyle.fontSize = 13 + editing[SIZE_DIALOG_FIELD] + "px";
|
|
8455
9384
|
// The axis is a percentage OF DSH's own line height, so the preview multiplies the
|
|
8456
9385
|
// measured base by the same percentage — and does it at every notch including 100,
|
|
@@ -8469,9 +9398,32 @@ function FontCard(props) {
|
|
|
8469
9398
|
// conversation does instead of one flat weight.
|
|
8470
9399
|
previewDialogStyle["--dfp-wdelta"] = editing[WEIGHT_DIALOG_FIELD];
|
|
8471
9400
|
}
|
|
9401
|
+
// The shaping axes ride the preview inline, for the same reason the weight offset
|
|
9402
|
+
// does: the reader has to see the same change here that the conversation gets. The
|
|
9403
|
+
// paragraph gap needs two paragraphs to be visible at all, so the preview renders a
|
|
9404
|
+
// second, shorter one — the pair is what makes the axis honest.
|
|
9405
|
+
if (editing[LETTER_SPACING_DIALOG_FIELD] !== 0) {
|
|
9406
|
+
previewDialogStyle.letterSpacing = String(editing[LETTER_SPACING_DIALOG_FIELD]) + "px";
|
|
9407
|
+
}
|
|
9408
|
+
if (editing[WORD_SPACING_DIALOG_FIELD] !== 0) {
|
|
9409
|
+
previewDialogStyle.wordSpacing = String(editing[WORD_SPACING_DIALOG_FIELD]) + "px";
|
|
9410
|
+
}
|
|
9411
|
+
if (editing[INDENT_DIALOG_FIELD] !== 0) {
|
|
9412
|
+
previewDialogStyle.textIndent = String(editing[INDENT_DIALOG_FIELD]) + "em";
|
|
9413
|
+
}
|
|
9414
|
+
if (editing[LINE_BREAK_STRICT_FIELD] === true) {
|
|
9415
|
+
previewDialogStyle.lineBreak = "strict";
|
|
9416
|
+
}
|
|
9417
|
+
var previewDialogGap = editing[PARAGRAPH_SPACING_DIALOG_FIELD];
|
|
9418
|
+
// The preview paragraph keeps DSH's own 16px gap as its baseline, exactly like the
|
|
9419
|
+
// conversation rule does, so a "+6px" reads as the same change in both places.
|
|
9420
|
+
var previewSecondStyle = { marginTop: String(shared.PARAGRAPH_BASELINE_PX + previewDialogGap) + "px" };
|
|
9421
|
+
if (editing[INDENT_DIALOG_FIELD] !== 0) {
|
|
9422
|
+
previewSecondStyle.textIndent = String(editing[INDENT_DIALOG_FIELD]) + "em";
|
|
9423
|
+
}
|
|
8472
9424
|
var previewCodeStyle = {};
|
|
8473
9425
|
if (editing[MONO_FIELD] !== "") {
|
|
8474
|
-
previewCodeStyle.fontFamily = formatStack(parseStack(editing[MONO_FIELD]));
|
|
9426
|
+
previewCodeStyle.fontFamily = shared.withVariableFaces(formatStack(parseStack(editing[MONO_FIELD])), variableFamilies);
|
|
8475
9427
|
}
|
|
8476
9428
|
previewCodeStyle.fontSize = 13 + editing[CODE_SIZE_FIELD] + "px";
|
|
8477
9429
|
if (editing[CODE_LINE_HEIGHT_FIELD] !== 0) {
|
|
@@ -8720,6 +9672,49 @@ function FontCard(props) {
|
|
|
8720
9672
|
value: editing[WEIGHT_DIALOG_FIELD],
|
|
8721
9673
|
stack: dialogStack,
|
|
8722
9674
|
}),
|
|
9675
|
+
// Text shaping: the four spacing axes, then the line-breaking switch. Each
|
|
9676
|
+
// one leaves DSH's own value alone at its neutral position.
|
|
9677
|
+
lineBreakField(),
|
|
9678
|
+
spacingField({
|
|
9679
|
+
field: LETTER_SPACING_DIALOG_FIELD,
|
|
9680
|
+
labelKey: "shape.letterLabel",
|
|
9681
|
+
hintKey: "shape.letterHint",
|
|
9682
|
+
min: LETTER_SPACING_MIN,
|
|
9683
|
+
max: LETTER_SPACING_MAX,
|
|
9684
|
+
step: LETTER_SPACING_STEP,
|
|
9685
|
+
unit: "px",
|
|
9686
|
+
decimals: 2,
|
|
9687
|
+
}),
|
|
9688
|
+
spacingField({
|
|
9689
|
+
field: WORD_SPACING_DIALOG_FIELD,
|
|
9690
|
+
labelKey: "shape.wordLabel",
|
|
9691
|
+
hintKey: "shape.wordHint",
|
|
9692
|
+
min: WORD_SPACING_MIN,
|
|
9693
|
+
max: WORD_SPACING_MAX,
|
|
9694
|
+
step: WORD_SPACING_STEP,
|
|
9695
|
+
unit: "px",
|
|
9696
|
+
decimals: 2,
|
|
9697
|
+
}),
|
|
9698
|
+
spacingField({
|
|
9699
|
+
field: PARAGRAPH_SPACING_DIALOG_FIELD,
|
|
9700
|
+
labelKey: "shape.paragraphLabel",
|
|
9701
|
+
hintKey: "shape.paragraphHint",
|
|
9702
|
+
min: PARAGRAPH_SPACING_MIN,
|
|
9703
|
+
max: PARAGRAPH_SPACING_MAX,
|
|
9704
|
+
step: PARAGRAPH_SPACING_STEP,
|
|
9705
|
+
unit: "px",
|
|
9706
|
+
decimals: 2,
|
|
9707
|
+
}),
|
|
9708
|
+
spacingField({
|
|
9709
|
+
field: INDENT_DIALOG_FIELD,
|
|
9710
|
+
labelKey: "shape.indentLabel",
|
|
9711
|
+
hintKey: "shape.indentHint",
|
|
9712
|
+
min: INDENT_MIN,
|
|
9713
|
+
max: INDENT_MAX,
|
|
9714
|
+
step: INDENT_STEP,
|
|
9715
|
+
unit: "em",
|
|
9716
|
+
decimals: 2,
|
|
9717
|
+
}),
|
|
8723
9718
|
h(
|
|
8724
9719
|
"div",
|
|
8725
9720
|
{ className: "dfp-previewBox" },
|
|
@@ -8727,7 +9722,8 @@ function FontCard(props) {
|
|
|
8727
9722
|
h(
|
|
8728
9723
|
"div",
|
|
8729
9724
|
{ className: "dfp-previewText dfp-previewDialog", style: previewDialogStyle },
|
|
8730
|
-
t("preview.sample")
|
|
9725
|
+
h("p", { style: { margin: "0" } }, t("preview.sample")),
|
|
9726
|
+
h("p", { style: previewSecondStyle }, t("preview.sampleSecond"))
|
|
8731
9727
|
)
|
|
8732
9728
|
)
|
|
8733
9729
|
),
|
|
@@ -9163,6 +10159,10 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
9163
10159
|
var corner = null;
|
|
9164
10160
|
// The live card size while resizing; otherwise the document's size rules.
|
|
9165
10161
|
var liveSize = null;
|
|
10162
|
+
// Whether the panel's "in force" block is unfolded. It is view state, not a setting:
|
|
10163
|
+
// it lives for the session and is deliberately not written to the document, because a
|
|
10164
|
+
// fold the reader chose is not a configuration anyone should have to sync.
|
|
10165
|
+
var infoOpen = true;
|
|
9166
10166
|
// The document value `pos` was last reconciled with: a snapshot carrying
|
|
9167
10167
|
// anything else is an edit from elsewhere and is adopted. Our own writes
|
|
9168
10168
|
// set `pos` first, so their echo always matches and is a no-op.
|
|
@@ -10546,6 +11546,128 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10546
11546
|
return { row: row, input: input, shown: shown };
|
|
10547
11547
|
};
|
|
10548
11548
|
|
|
11549
|
+
/**
|
|
11550
|
+
* The panel's "in force right now" block: what is actually on the page.
|
|
11551
|
+
*
|
|
11552
|
+
* Three rounds of "it did not take effect" reports were settled by reading the panel's
|
|
11553
|
+
* own attributes in DevTools, which is not a thing a reader should have to do. This
|
|
11554
|
+
* prints the same facts where the reader already is, and it prints the FAMILY each
|
|
11555
|
+
* surface renders with rather than a relationship: an unset conversation shows the name
|
|
11556
|
+
* it actually uses plus "DSH default", and the only place the word "follows" appears is
|
|
11557
|
+
* the interface following the conversation, which is the one relationship the plugin
|
|
11558
|
+
* ships by default.
|
|
11559
|
+
*
|
|
11560
|
+
* The block folds away, because a panel that is being used to drag a slider does not
|
|
11561
|
+
* need four lines of prose under it.
|
|
11562
|
+
* @param {HTMLElement} body - the card body.
|
|
11563
|
+
* @param {object} state - the resolved families, the notch count and the fold state.
|
|
11564
|
+
* @returns {HTMLElement} the block.
|
|
11565
|
+
*/
|
|
11566
|
+
var infoBlock = function (body, state) {
|
|
11567
|
+
var block = el("div", "dfp-floatInfo" + (state.open ? " dfp-floatInfoOpen" : ""));
|
|
11568
|
+
var head = el("button", "dfp-floatInfoHead");
|
|
11569
|
+
head.type = "button";
|
|
11570
|
+
head.setAttribute("aria-expanded", state.open ? "true" : "false");
|
|
11571
|
+
head.appendChild(el("span", "dfp-floatInfoTitle", t("status.title")));
|
|
11572
|
+
// The card's own chevron, so the two surfaces fold the same way round: it starts
|
|
11573
|
+
// pointing down (there is something below) and turns over when the block is open.
|
|
11574
|
+
// Built with the SVG helper, not with `h`: the panel is plain DOM, and a React
|
|
11575
|
+
// element appended to it is not a node at all.
|
|
11576
|
+
var glyph = svgEl("svg", {
|
|
11577
|
+
class: "dfp-chevron" + (state.open ? " dfp-chevronOpen" : ""),
|
|
11578
|
+
viewBox: "0 0 14 14",
|
|
11579
|
+
width: "14",
|
|
11580
|
+
height: "14",
|
|
11581
|
+
fill: "none",
|
|
11582
|
+
"aria-hidden": "true",
|
|
11583
|
+
focusable: "false",
|
|
11584
|
+
});
|
|
11585
|
+
glyph.appendChild(svgEl("path", { d: CHEVRON_PATH, fill: "currentColor" }));
|
|
11586
|
+
head.appendChild(glyph);
|
|
11587
|
+
head.addEventListener("click", function () {
|
|
11588
|
+
infoOpen = !infoOpen;
|
|
11589
|
+
block.classList.toggle("dfp-floatInfoOpen", infoOpen);
|
|
11590
|
+
head.setAttribute("aria-expanded", infoOpen ? "true" : "false");
|
|
11591
|
+
glyph.setAttribute("class", "dfp-chevron" + (infoOpen ? " dfp-chevronOpen" : ""));
|
|
11592
|
+
});
|
|
11593
|
+
block.appendChild(head);
|
|
11594
|
+
// The grid-row transition needs a single child that clips: 0fr cannot be animated on
|
|
11595
|
+
// the row container itself.
|
|
11596
|
+
var clip = el("div", "dfp-floatInfoClip");
|
|
11597
|
+
var lines = el("div", "dfp-floatInfoLines");
|
|
11598
|
+
/**
|
|
11599
|
+
* One fact: a key, the family it resolves to, and — only when the family is NOT this
|
|
11600
|
+
* surface's own setting — where it came from. The reason lives in the tooltip: the
|
|
11601
|
+
* visible line stays short, and "why" is one hover away.
|
|
11602
|
+
* @param {string} key - the localized label.
|
|
11603
|
+
* @param {string} value - the family name.
|
|
11604
|
+
* @param {string} tag - the provenance tag, or "" for none.
|
|
11605
|
+
* @param {string} note - the tooltip sentence, or "" for none.
|
|
11606
|
+
* @returns {HTMLElement} the line.
|
|
11607
|
+
*/
|
|
11608
|
+
var line = function (key, value, tag, note) {
|
|
11609
|
+
var row = el("div", "dfp-floatInfoLine");
|
|
11610
|
+
row.appendChild(el("span", "dfp-floatInfoKey", key));
|
|
11611
|
+
var shown = el("span", "dfp-floatInfoValue", value);
|
|
11612
|
+
if (note !== "") shown.title = note;
|
|
11613
|
+
row.appendChild(shown);
|
|
11614
|
+
if (tag !== "") row.appendChild(el("span", "dfp-floatInfoFlag", tag));
|
|
11615
|
+
return row;
|
|
11616
|
+
};
|
|
11617
|
+
var hostDefault = t("status.hostDefault");
|
|
11618
|
+
var usesHost = t("status.usesHost");
|
|
11619
|
+
// "Set by this plugin" means the surface's own field OR — for the conversation — the
|
|
11620
|
+
// interface font it takes when its own field is empty (`expandFollow`). Naming a family
|
|
11621
|
+
// the reader chose "DSH's own" was simply wrong: measured on the reporting machine the
|
|
11622
|
+
// conversation field is empty, the interface font is "Noto Serif SC", and the
|
|
11623
|
+
// conversation really does draw with it. The flags come in through `state` because
|
|
11624
|
+
// `config` belongs to the caller.
|
|
11625
|
+
lines.appendChild(
|
|
11626
|
+
line(
|
|
11627
|
+
t("status.dialog"),
|
|
11628
|
+
state.dialog.family,
|
|
11629
|
+
state.dialog.own ? "" : hostDefault,
|
|
11630
|
+
// The note follows where the family CAME FROM, not whether it is the reader's:
|
|
11631
|
+
// a conversation field left empty while the interface font is set is still the
|
|
11632
|
+
// reader's own font, and the tooltip is the only place that says so.
|
|
11633
|
+
state.dialog.fromInterface
|
|
11634
|
+
? t("status.usesInterface")
|
|
11635
|
+
: state.dialog.own
|
|
11636
|
+
? ""
|
|
11637
|
+
: usesHost
|
|
11638
|
+
)
|
|
11639
|
+
);
|
|
11640
|
+
lines.appendChild(
|
|
11641
|
+
line(
|
|
11642
|
+
t("status.ui"),
|
|
11643
|
+
state.ui.family,
|
|
11644
|
+
state.ui.follows ? t("status.followDialog") : state.ui.own ? "" : hostDefault,
|
|
11645
|
+
state.ui.follows ? t("status.uiFollowsNote") : state.ui.own ? "" : usesHost
|
|
11646
|
+
)
|
|
11647
|
+
);
|
|
11648
|
+
lines.appendChild(
|
|
11649
|
+
line(
|
|
11650
|
+
t("status.code"),
|
|
11651
|
+
state.code.family,
|
|
11652
|
+
state.code.own ? "" : hostDefault,
|
|
11653
|
+
state.code.own ? "" : usesHost
|
|
11654
|
+
)
|
|
11655
|
+
);
|
|
11656
|
+
if (state.notches > 0) {
|
|
11657
|
+
var weight = line(
|
|
11658
|
+
t("status.weight"),
|
|
11659
|
+
t("status.steps", { count: state.notches }),
|
|
11660
|
+
state.continuous === true ? t("status.continuous") : "",
|
|
11661
|
+
""
|
|
11662
|
+
);
|
|
11663
|
+
lines.appendChild(weight);
|
|
11664
|
+
}
|
|
11665
|
+
clip.appendChild(lines);
|
|
11666
|
+
block.appendChild(clip);
|
|
11667
|
+
body.appendChild(block);
|
|
11668
|
+
return block;
|
|
11669
|
+
};
|
|
11670
|
+
|
|
10549
11671
|
/**
|
|
10550
11672
|
* Start a dot drag from a press on the dot or the card head.
|
|
10551
11673
|
*
|
|
@@ -10818,8 +11940,13 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10818
11940
|
host.textContent = "";
|
|
10819
11941
|
// One line in the console answers "which build is live": with a linked plugin
|
|
10820
11942
|
// and a cached client script, "I refreshed" is not evidence on its own.
|
|
10821
|
-
host.setAttribute("data-dfp-build",
|
|
10822
|
-
|
|
11943
|
+
host.setAttribute("data-dfp-build", CLIENT_BUILD);
|
|
11944
|
+
// And which families this build promoted to continuous weight, so a report of
|
|
11945
|
+
// "the slider still has four notches" can be answered without a console session:
|
|
11946
|
+
// an empty attribute means the probe has not (yet) promoted the reader's family,
|
|
11947
|
+
// a missing one means the page is running a build from before the mode existed.
|
|
11948
|
+
if (continuousNames().length > 0) host.setAttribute("data-dfp-continuous", continuousNames().join(", "));
|
|
11949
|
+
else host.removeAttribute("data-dfp-continuous"); var dot = el("button", "dfp-dot");
|
|
10823
11950
|
dot.type = "button";
|
|
10824
11951
|
dot.setAttribute("aria-label", t("panel.dot"));
|
|
10825
11952
|
dot.title = t("panel.dot");
|
|
@@ -10913,9 +12040,16 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10913
12040
|
// a card sized on a big screen must still fit a small one. The card fills
|
|
10914
12041
|
// the wrapper, so the wrapper is the box that gets the size.
|
|
10915
12042
|
shadow.style.width = "min(" + sized.w + "px,calc(100vw - 32px))";
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
12043
|
+
// The stored HEIGHT is a cap on the card, not a fixed box for it. As a fixed box it
|
|
12044
|
+
// left a hole wherever the content had been: folding the panel's own block kept the
|
|
12045
|
+
// card at its stored height with 260px of nothing under the sliders (measured). As a
|
|
12046
|
+
// cap the panel is exactly as tall as its content up to that height, so folding gives
|
|
12047
|
+
// the space back — while a panel dragged shorter than its content still scrolls,
|
|
12048
|
+
// because the card is a flex column whose body owns the overflow. The wrapper is left
|
|
12049
|
+
// to size itself, except mid-drag, where the pointer's own height is in charge.
|
|
12050
|
+
card.style.maxHeight =
|
|
12051
|
+
sized.h > 0 ? "min(" + sized.h + "px,calc(100vh - 24px))" : "";
|
|
12052
|
+
if (resize === null) shadow.style.height = "";
|
|
10919
12053
|
}
|
|
10920
12054
|
var head = el("div", "dfp-floatHead");
|
|
10921
12055
|
// Plain padding on one side only: the two round buttons are out of flow in
|
|
@@ -10975,9 +12109,15 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10975
12109
|
startDrag(event, head, state.writable);
|
|
10976
12110
|
});
|
|
10977
12111
|
|
|
12112
|
+
// Every axis lives here, in the same order the card uses, inside a body that
|
|
12113
|
+
// scrolls when the panel has been resized smaller than its content: seven sliders
|
|
12114
|
+
// and a status block do not fit a 160px-tall panel, and a clipped slider is worse
|
|
12115
|
+
// than a scrollbar.
|
|
12116
|
+
var body = el("div", "dfp-floatBody");
|
|
12117
|
+
|
|
10978
12118
|
var size = config[SIZE_DIALOG_FIELD];
|
|
10979
12119
|
sliderRow(
|
|
10980
|
-
|
|
12120
|
+
body,
|
|
10981
12121
|
t("size.dialogLabel"),
|
|
10982
12122
|
function (next) {
|
|
10983
12123
|
return (next > 0 ? "+" + next : String(next)) + " " + t("size.unit");
|
|
@@ -10993,7 +12133,7 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10993
12133
|
|
|
10994
12134
|
var line = config[LINE_HEIGHT_DIALOG_FIELD];
|
|
10995
12135
|
sliderRow(
|
|
10996
|
-
|
|
12136
|
+
body,
|
|
10997
12137
|
t("line.dialogLabel"),
|
|
10998
12138
|
function (next) {
|
|
10999
12139
|
return String(next) + " " + t("line.unit");
|
|
@@ -11009,7 +12149,13 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11009
12149
|
|
|
11010
12150
|
// The dialog weight in the family's own steps, like the card's control:
|
|
11011
12151
|
// the document keeps the offset, the readout counts steps.
|
|
11012
|
-
|
|
12152
|
+
//
|
|
12153
|
+
// The stack comes from the SAME helper the card and the warm-up use. Resolving it
|
|
12154
|
+
// here instead (the configured field alone) meant an unset stack fell through to
|
|
12155
|
+
// the page's computed family, which carries our alias once a family is promoted —
|
|
12156
|
+
// so the panel missed the promotion and kept showing the coarse notches while the
|
|
12157
|
+
// card showed the fine ones.
|
|
12158
|
+
var dialogStack = weightAxisStacks(config).dialog;
|
|
11013
12159
|
var profile = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
11014
12160
|
try {
|
|
11015
12161
|
profile = weightProfileFor(dialogStack, shared.WEIGHT_LADDER_STRONG);
|
|
@@ -11021,6 +12167,13 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11021
12167
|
{ min: Math.min(profile.min, storedWeight), max: Math.max(profile.max, storedWeight), step: profile.step },
|
|
11022
12168
|
storedWeight
|
|
11023
12169
|
);
|
|
12170
|
+
probeLog("panel-weight", {
|
|
12171
|
+
stack: dialogStack,
|
|
12172
|
+
profile: profile,
|
|
12173
|
+
stored: storedWeight,
|
|
12174
|
+
shape: shape,
|
|
12175
|
+
promoted: continuousNames(),
|
|
12176
|
+
});
|
|
11024
12177
|
(function () {
|
|
11025
12178
|
var row = el("div", "dfp-floatRow");
|
|
11026
12179
|
var label = el("div", "dfp-floatLabel");
|
|
@@ -11044,10 +12197,107 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11044
12197
|
else commit({ weightDialog: count * shape.unit });
|
|
11045
12198
|
});
|
|
11046
12199
|
row.appendChild(input);
|
|
11047
|
-
|
|
12200
|
+
body.appendChild(row);
|
|
11048
12201
|
})();
|
|
11049
12202
|
|
|
11050
|
-
|
|
12203
|
+
// Every one of the four shaping axes lives here too, in the card's own order, so the
|
|
12204
|
+
// panel can tune the whole conversation without the settings page.
|
|
12205
|
+
sliderRow(
|
|
12206
|
+
body,
|
|
12207
|
+
t("shape.letterLabel"),
|
|
12208
|
+
function (next) {
|
|
12209
|
+
return (next > 0 ? "+" : "") + Number(next).toFixed(2) + "px";
|
|
12210
|
+
},
|
|
12211
|
+
LETTER_SPACING_MIN,
|
|
12212
|
+
LETTER_SPACING_MAX,
|
|
12213
|
+
LETTER_SPACING_STEP,
|
|
12214
|
+
config[LETTER_SPACING_DIALOG_FIELD],
|
|
12215
|
+
function (next) {
|
|
12216
|
+
commit({ letterSpacingDialog: next });
|
|
12217
|
+
}
|
|
12218
|
+
);
|
|
12219
|
+
sliderRow(
|
|
12220
|
+
body,
|
|
12221
|
+
t("shape.wordLabel"),
|
|
12222
|
+
function (next) {
|
|
12223
|
+
return (next > 0 ? "+" : "") + Number(next).toFixed(2) + "px";
|
|
12224
|
+
},
|
|
12225
|
+
WORD_SPACING_MIN,
|
|
12226
|
+
WORD_SPACING_MAX,
|
|
12227
|
+
WORD_SPACING_STEP,
|
|
12228
|
+
config[WORD_SPACING_DIALOG_FIELD],
|
|
12229
|
+
function (next) {
|
|
12230
|
+
commit({ wordSpacingDialog: next });
|
|
12231
|
+
}
|
|
12232
|
+
);
|
|
12233
|
+
sliderRow(
|
|
12234
|
+
body,
|
|
12235
|
+
t("shape.paragraphLabel"),
|
|
12236
|
+
function (next) {
|
|
12237
|
+
return (next > 0 ? "+" : "") + Number(next).toFixed(2) + "px";
|
|
12238
|
+
},
|
|
12239
|
+
PARAGRAPH_SPACING_MIN,
|
|
12240
|
+
PARAGRAPH_SPACING_MAX,
|
|
12241
|
+
PARAGRAPH_SPACING_STEP,
|
|
12242
|
+
config[PARAGRAPH_SPACING_DIALOG_FIELD],
|
|
12243
|
+
function (next) {
|
|
12244
|
+
commit({ paragraphSpacingDialog: next });
|
|
12245
|
+
}
|
|
12246
|
+
);
|
|
12247
|
+
sliderRow(
|
|
12248
|
+
body,
|
|
12249
|
+
t("shape.indentLabel"),
|
|
12250
|
+
function (next) {
|
|
12251
|
+
return Number(next).toFixed(2) + "em";
|
|
12252
|
+
},
|
|
12253
|
+
INDENT_MIN,
|
|
12254
|
+
INDENT_MAX,
|
|
12255
|
+
INDENT_STEP,
|
|
12256
|
+
config[INDENT_DIALOG_FIELD],
|
|
12257
|
+
function (next) {
|
|
12258
|
+
commit({ indentDialog: next });
|
|
12259
|
+
}
|
|
12260
|
+
);
|
|
12261
|
+
|
|
12262
|
+
// What is actually in force, in the panel itself: the three families the page is
|
|
12263
|
+
// really rendering with, then the weight control's shape. An axis that sets no family
|
|
12264
|
+
// reports the one the page hands that surface — measured from the surface itself, not
|
|
12265
|
+
// from `body`, because the two really can differ (the interface is its own axis).
|
|
12266
|
+
var resolved = weightAxisStacks(config);
|
|
12267
|
+
var dialogConfigured = config[STACK_DIALOG_FIELD] !== "";
|
|
12268
|
+
var dialogFamily = primaryFamilyOf(
|
|
12269
|
+
dialogConfigured ? resolved.dialog : liveDialogFamily()
|
|
12270
|
+
);
|
|
12271
|
+
var uiConfigured = config[SANS_FIELD] !== "";
|
|
12272
|
+
var uiFollows = config[UI_FOLLOWS_FIELD] !== false;
|
|
12273
|
+
infoBlock(body, {
|
|
12274
|
+
open: infoOpen,
|
|
12275
|
+
dialog: {
|
|
12276
|
+
family: dialogFamily,
|
|
12277
|
+
// Own: the conversation's own field. From the interface: its field is empty and
|
|
12278
|
+
// the interface font is the one filling it in — still the reader's own choice.
|
|
12279
|
+
own: dialogConfigured || uiConfigured,
|
|
12280
|
+
fromInterface: !dialogConfigured && uiConfigured,
|
|
12281
|
+
},
|
|
12282
|
+
ui: {
|
|
12283
|
+
// Following the conversation means showing the conversation's family: the same
|
|
12284
|
+
// fallback the stylesheet resolves, so the two lines can never disagree.
|
|
12285
|
+
family: uiFollows
|
|
12286
|
+
? dialogFamily
|
|
12287
|
+
: primaryFamilyOf(uiConfigured ? resolved.ui : liveFamily()),
|
|
12288
|
+
follows: uiFollows,
|
|
12289
|
+
own: uiConfigured,
|
|
12290
|
+
},
|
|
12291
|
+
code: {
|
|
12292
|
+
family: primaryFamilyOf(
|
|
12293
|
+
config[MONO_FIELD] !== "" ? resolved.code : liveCodeFamily()
|
|
12294
|
+
),
|
|
12295
|
+
own: config[MONO_FIELD] !== "",
|
|
12296
|
+
},
|
|
12297
|
+
continuous: continuousNames().indexOf(dialogFamily) >= 0,
|
|
12298
|
+
notches: shape.max - shape.min + 1,
|
|
12299
|
+
});
|
|
12300
|
+
card.appendChild(body);
|
|
11051
12301
|
// Edge resize handles: right, bottom and the corner. The card follows the
|
|
11052
12302
|
// pointer live; one size write lands on release (see `startResize`).
|
|
11053
12303
|
(function () {
|
|
@@ -11233,8 +12483,14 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11233
12483
|
// A document without events still gets the interval.
|
|
11234
12484
|
}
|
|
11235
12485
|
|
|
12486
|
+
// A promotion changes the weight row's shape; the panel redraws itself when one
|
|
12487
|
+
// lands, and stops listening when it goes away.
|
|
12488
|
+
promotionListeners.push(render);
|
|
12489
|
+
|
|
11236
12490
|
return {
|
|
11237
12491
|
destroy: function () {
|
|
12492
|
+
var at = promotionListeners.indexOf(render);
|
|
12493
|
+
if (at >= 0) promotionListeners.splice(at, 1);
|
|
11238
12494
|
if (statusTimer !== null) globalThis.clearTimeout(statusTimer);
|
|
11239
12495
|
if (closeTimer !== null) globalThis.clearTimeout(closeTimer);
|
|
11240
12496
|
if (reflowTimer !== null) globalThis.clearTimeout(reflowTimer);
|