dsh-fonttune 0.5.0 → 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 +11 -0
- package/CHANGELOG.md +11 -0
- package/README.en.md +13 -3
- package/README.md +13 -3
- package/docs/card-shaping.jpg +0 -0
- package/docs/panel.jpg +0 -0
- package/lib/client.js +731 -12
- package/lib/index.js +38 -0
- package/lib/shared.cjs +204 -0
- 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
|
*
|
|
@@ -3436,6 +3563,11 @@ function isDormant(config) {
|
|
|
3436
3563
|
clampWeightDelta(axis[CODE_WEIGHT_FIELD], true) === WEIGHT_UNSET &&
|
|
3437
3564
|
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
3438
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 &&
|
|
3439
3571
|
clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
|
|
3440
3572
|
sanitizeFeatures(axis[FEATURES_FIELD]) === "" &&
|
|
3441
3573
|
toBool(axis[NO_SYNTHETIC_ITALIC_FIELD]) === false &&
|
|
@@ -3819,6 +3951,60 @@ function buildAxisCss(axis, baseTokens, isDark, hints, variable) {
|
|
|
3819
3951
|
}
|
|
3820
3952
|
}
|
|
3821
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
|
+
|
|
3822
4008
|
// ---- code axes ----
|
|
3823
4009
|
var codeSize = axis[CODE_SIZE_FIELD];
|
|
3824
4010
|
var codeLine = axis[CODE_LINE_HEIGHT_FIELD];
|
|
@@ -4440,6 +4626,24 @@ var shared = {
|
|
|
4440
4626
|
LINE_HEIGHT_MAX: LINE_HEIGHT_MAX,
|
|
4441
4627
|
CODE_LINE_HEIGHT_MIN: CODE_LINE_HEIGHT_MIN,
|
|
4442
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,
|
|
4443
4647
|
LIGATURES_DEFAULT: LIGATURES_DEFAULT,
|
|
4444
4648
|
SMOOTHING_FIELD: SMOOTHING_FIELD,
|
|
4445
4649
|
SMOOTHING_AUTO: SMOOTHING_AUTO,
|
|
@@ -4606,6 +4810,23 @@ var WEIGHT_DIALOG_FIELD = shared.WEIGHT_DIALOG_FIELD;
|
|
|
4606
4810
|
var CODE_WEIGHT_FIELD = shared.CODE_WEIGHT_FIELD;
|
|
4607
4811
|
var LINE_HEIGHT_DIALOG_FIELD = shared.LINE_HEIGHT_DIALOG_FIELD;
|
|
4608
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;
|
|
4609
4830
|
var LIGATURES_FIELD = shared.LIGATURES_FIELD;
|
|
4610
4831
|
var FEATURES_FIELD = shared.FEATURES_FIELD;
|
|
4611
4832
|
var NO_SYNTHETIC_ITALIC_FIELD = shared.NO_SYNTHETIC_ITALIC_FIELD;
|
|
@@ -4808,7 +5029,7 @@ var DICTS = {
|
|
|
4808
5029
|
en: {
|
|
4809
5030
|
"card.title": "Font tune",
|
|
4810
5031
|
"card.description":
|
|
4811
|
-
"Interface and conversation fonts,
|
|
5032
|
+
"Interface and conversation fonts, size, weight and line height, CJK type shaping, code extras and presets",
|
|
4812
5033
|
"card.expand": "Expand",
|
|
4813
5034
|
"card.collapse": "Collapse",
|
|
4814
5035
|
"card.resetAll": "Reset all",
|
|
@@ -4887,6 +5108,37 @@ var DICTS = {
|
|
|
4887
5108
|
"Adds {offset} to code line heights only; 0 keeps DSH's own line heights.",
|
|
4888
5109
|
"line.unit": "%",
|
|
4889
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
|
+
|
|
4890
5142
|
"lig.label": "Code ligatures",
|
|
4891
5143
|
"lig.hint":
|
|
4892
5144
|
"Programming ligatures render multiple characters as one glyph (=> as an arrow, != as ≠). Browsers enable them by default.",
|
|
@@ -4954,6 +5206,7 @@ var DICTS = {
|
|
|
4954
5206
|
"preview.monoCaption": "Code",
|
|
4955
5207
|
"preview.sample":
|
|
4956
5208
|
"The quick brown fox jumps over the lazy dog — 中文排版预览,标点符号,数字 0123456789。",
|
|
5209
|
+
"preview.sampleSecond": "第二段:段距与首行缩进在这里显现出来,正文其余部分不变。",
|
|
4957
5210
|
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
4958
5211
|
|
|
4959
5212
|
"panel.dot": "Tuning panel",
|
|
@@ -4972,7 +5225,7 @@ var DICTS = {
|
|
|
4972
5225
|
zh: {
|
|
4973
5226
|
"card.title": "字体增强",
|
|
4974
5227
|
"card.description":
|
|
4975
|
-
"
|
|
5228
|
+
"界面与对话字体、字号/字重/行高,字距/段距/首行缩进等中文排印,代码增强与预设方案",
|
|
4976
5229
|
"card.expand": "展开",
|
|
4977
5230
|
"card.collapse": "收起",
|
|
4978
5231
|
"card.resetAll": "全部重置",
|
|
@@ -5047,6 +5300,36 @@ var DICTS = {
|
|
|
5047
5300
|
"line.codeHint": "只给代码行高加 {offset}px;0 表示保持原样。",
|
|
5048
5301
|
"line.unit": "%",
|
|
5049
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
|
+
|
|
5050
5333
|
"lig.label": "代码连字",
|
|
5051
5334
|
"lig.hint":
|
|
5052
5335
|
"把多个字符连成一个字形(如 => 变箭头、!= 变 ≠)。浏览器默认就是开启的。",
|
|
@@ -5113,6 +5396,8 @@ var DICTS = {
|
|
|
5113
5396
|
"preview.monoCaption": "代码",
|
|
5114
5397
|
"preview.sample":
|
|
5115
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.",
|
|
5116
5401
|
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
5117
5402
|
|
|
5118
5403
|
"panel.dot": "调参面板",
|
|
@@ -5349,7 +5634,11 @@ var CARD_CSS = [
|
|
|
5349
5634
|
// IS the card's corner: the clip, the scale and the fade still ride the card
|
|
5350
5635
|
// (and every measurement still reads it), while the anchored edge, the size
|
|
5351
5636
|
// and the shadow belong to the wrapper around it.
|
|
5352
|
-
".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}",
|
|
5353
5642
|
// the enter frame: the card is clipped to the dot's own rectangle, a hair
|
|
5354
5643
|
// smaller and dimmer, and the transition (started by dropping this class two
|
|
5355
5644
|
// frames later) carries all three home at once. Once it has arrived the card
|
|
@@ -5405,6 +5694,16 @@ var CARD_CSS = [
|
|
|
5405
5694
|
// the write-failure line takes no room at all while it carries no message
|
|
5406
5695
|
".dfp-floatStatus{margin-top:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
5407
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)}",
|
|
5408
5707
|
].join("");
|
|
5409
5708
|
|
|
5410
5709
|
/**
|
|
@@ -6211,7 +6510,7 @@ function notifyPromoted() {
|
|
|
6211
6510
|
* The client script is cached, and "I refreshed" has been wrong before: this is what
|
|
6212
6511
|
* a live page reports about itself (the panel host carries it as `data-dfp-build`).
|
|
6213
6512
|
*/
|
|
6214
|
-
var CLIENT_BUILD = "2026-10-
|
|
6513
|
+
var CLIENT_BUILD = "2026-10-15-1";
|
|
6215
6514
|
|
|
6216
6515
|
/** The families this page promoted to continuous weight, in map order. */
|
|
6217
6516
|
function continuousNames() {
|
|
@@ -6460,6 +6759,27 @@ function liveCodeFamily() {
|
|
|
6460
6759
|
}
|
|
6461
6760
|
}
|
|
6462
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
|
+
|
|
6463
6783
|
/* ------------------------------------------------------------------ *
|
|
6464
6784
|
* hooks and controls
|
|
6465
6785
|
* ------------------------------------------------------------------ */
|
|
@@ -8549,6 +8869,102 @@ function FontCard(props) {
|
|
|
8549
8869
|
);
|
|
8550
8870
|
};
|
|
8551
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
|
+
|
|
8552
8968
|
// ---- ligature segmented control ----
|
|
8553
8969
|
var ligatureField = function () {
|
|
8554
8970
|
var value = editing[LIGATURES_FIELD];
|
|
@@ -8982,6 +9398,29 @@ function FontCard(props) {
|
|
|
8982
9398
|
// conversation does instead of one flat weight.
|
|
8983
9399
|
previewDialogStyle["--dfp-wdelta"] = editing[WEIGHT_DIALOG_FIELD];
|
|
8984
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
|
+
}
|
|
8985
9424
|
var previewCodeStyle = {};
|
|
8986
9425
|
if (editing[MONO_FIELD] !== "") {
|
|
8987
9426
|
previewCodeStyle.fontFamily = shared.withVariableFaces(formatStack(parseStack(editing[MONO_FIELD])), variableFamilies);
|
|
@@ -9233,6 +9672,49 @@ function FontCard(props) {
|
|
|
9233
9672
|
value: editing[WEIGHT_DIALOG_FIELD],
|
|
9234
9673
|
stack: dialogStack,
|
|
9235
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
|
+
}),
|
|
9236
9718
|
h(
|
|
9237
9719
|
"div",
|
|
9238
9720
|
{ className: "dfp-previewBox" },
|
|
@@ -9240,7 +9722,8 @@ function FontCard(props) {
|
|
|
9240
9722
|
h(
|
|
9241
9723
|
"div",
|
|
9242
9724
|
{ className: "dfp-previewText dfp-previewDialog", style: previewDialogStyle },
|
|
9243
|
-
t("preview.sample")
|
|
9725
|
+
h("p", { style: { margin: "0" } }, t("preview.sample")),
|
|
9726
|
+
h("p", { style: previewSecondStyle }, t("preview.sampleSecond"))
|
|
9244
9727
|
)
|
|
9245
9728
|
)
|
|
9246
9729
|
),
|
|
@@ -9676,6 +10159,10 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
9676
10159
|
var corner = null;
|
|
9677
10160
|
// The live card size while resizing; otherwise the document's size rules.
|
|
9678
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;
|
|
9679
10166
|
// The document value `pos` was last reconciled with: a snapshot carrying
|
|
9680
10167
|
// anything else is an edit from elsewhere and is adopted. Our own writes
|
|
9681
10168
|
// set `pos` first, so their echo always matches and is a no-op.
|
|
@@ -11059,6 +11546,128 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11059
11546
|
return { row: row, input: input, shown: shown };
|
|
11060
11547
|
};
|
|
11061
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
|
+
|
|
11062
11671
|
/**
|
|
11063
11672
|
* Start a dot drag from a press on the dot or the card head.
|
|
11064
11673
|
*
|
|
@@ -11431,9 +12040,16 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11431
12040
|
// a card sized on a big screen must still fit a small one. The card fills
|
|
11432
12041
|
// the wrapper, so the wrapper is the box that gets the size.
|
|
11433
12042
|
shadow.style.width = "min(" + sized.w + "px,calc(100vw - 32px))";
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
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 = "";
|
|
11437
12053
|
}
|
|
11438
12054
|
var head = el("div", "dfp-floatHead");
|
|
11439
12055
|
// Plain padding on one side only: the two round buttons are out of flow in
|
|
@@ -11493,9 +12109,15 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11493
12109
|
startDrag(event, head, state.writable);
|
|
11494
12110
|
});
|
|
11495
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
|
+
|
|
11496
12118
|
var size = config[SIZE_DIALOG_FIELD];
|
|
11497
12119
|
sliderRow(
|
|
11498
|
-
|
|
12120
|
+
body,
|
|
11499
12121
|
t("size.dialogLabel"),
|
|
11500
12122
|
function (next) {
|
|
11501
12123
|
return (next > 0 ? "+" + next : String(next)) + " " + t("size.unit");
|
|
@@ -11511,7 +12133,7 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11511
12133
|
|
|
11512
12134
|
var line = config[LINE_HEIGHT_DIALOG_FIELD];
|
|
11513
12135
|
sliderRow(
|
|
11514
|
-
|
|
12136
|
+
body,
|
|
11515
12137
|
t("line.dialogLabel"),
|
|
11516
12138
|
function (next) {
|
|
11517
12139
|
return String(next) + " " + t("line.unit");
|
|
@@ -11575,10 +12197,107 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11575
12197
|
else commit({ weightDialog: count * shape.unit });
|
|
11576
12198
|
});
|
|
11577
12199
|
row.appendChild(input);
|
|
11578
|
-
|
|
12200
|
+
body.appendChild(row);
|
|
11579
12201
|
})();
|
|
11580
12202
|
|
|
11581
|
-
|
|
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);
|
|
11582
12301
|
// Edge resize handles: right, bottom and the corner. The card follows the
|
|
11583
12302
|
// pointer live; one size write lands on release (see `startResize`).
|
|
11584
12303
|
(function () {
|