dsh-fonttune 0.3.3 → 0.3.5
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 +20 -1
- package/CHANGELOG.md +20 -1
- package/README.en.md +7 -26
- package/README.md +7 -26
- package/lib/client.js +266 -74
- package/lib/index.js +6 -0
- package/lib/shared.cjs +198 -60
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -86,6 +86,23 @@ var WEIGHT_DIALOG_FIELD = "weightDialog";
|
|
|
86
86
|
/** Field carrying the code font weight (0 = leave DSH alone). */
|
|
87
87
|
var CODE_WEIGHT_FIELD = "weightCode";
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Marks a document whose weight fields are OFFSETS, not absolute weights.
|
|
91
|
+
*
|
|
92
|
+
* Before 0.2.8 the three weight axes stored an absolute CSS weight (`300…600`,
|
|
93
|
+
* `400` = normal) and `clampWeightDelta` converted one on sight. That inference
|
|
94
|
+
* only works while offsets never land inside the absolute window, which capped
|
|
95
|
+
* the offset range at +200 — and +200 is not enough room for a step the eye can
|
|
96
|
+
* see on a family with few faces (measured: one 200-weight step is a +30% change
|
|
97
|
+
* on Noto Serif SC, one 100-weight step only +16%).
|
|
98
|
+
*
|
|
99
|
+
* So the document says which scale it is on, and the inference is borrowed for
|
|
100
|
+
* exactly one write: the first time 0.3.5+ writes a weight, the card converts any
|
|
101
|
+
* remaining absolute value, converts the saved presets the same way, and sets
|
|
102
|
+
* this field. From then on a stored weight is an offset, whatever its size.
|
|
103
|
+
*/
|
|
104
|
+
var WEIGHT_OFFSETS_FIELD = "weightOffsets";
|
|
105
|
+
|
|
89
106
|
/**
|
|
90
107
|
* Field carrying the interface line-height ratio in percent (100 = untouched).
|
|
91
108
|
* A ratio rather than a px offset: interface line heights form a design ladder
|
|
@@ -217,17 +234,18 @@ var WEIGHT_LADDER_TOP = DIALOG_WEIGHT_GROUPS.reduce(function (top, group) {
|
|
|
217
234
|
/**
|
|
218
235
|
* How far above its base an offset may reach at all.
|
|
219
236
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
237
|
+
* The ceiling comes from the layer a notch MUST move: body text sits at
|
|
238
|
+
* `WEIGHT_BASE`, so the range runs as far as the text a reader is reading can
|
|
239
|
+
* still change — measured up to +400 on a family whose faces reach 900. Bold text
|
|
240
|
+
* stops moving at +300 and headings at +200; that is accepted, because a heading
|
|
241
|
+
* already at its heaviest face stays there (see `WEIGHT_LADDER_STRONG`).
|
|
224
242
|
*
|
|
225
|
-
*
|
|
226
|
-
* `WEIGHT_MIN…WEIGHT_MAX`, the
|
|
227
|
-
* document occupies
|
|
228
|
-
*
|
|
243
|
+
* It used to stop at `WEIGHT_CSS_MAX - WEIGHT_LADDER_TOP` (= +200) so an offset
|
|
244
|
+
* could never land inside `WEIGHT_MIN…WEIGHT_MAX`, the window an ABSOLUTE weight
|
|
245
|
+
* from a pre-relative document occupies. The document's own marker resolves that
|
|
246
|
+
* window now (`WEIGHT_OFFSETS_FIELD`), so the range is free to grow.
|
|
229
247
|
*/
|
|
230
|
-
var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX -
|
|
248
|
+
var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX - WEIGHT_BASE;
|
|
231
249
|
|
|
232
250
|
/**
|
|
233
251
|
* The heaviest layer a notch still has to move: bold text.
|
|
@@ -323,9 +341,12 @@ var VALUE_FIELDS = [
|
|
|
323
341
|
var RETIRED_FIELDS = [SIZE_FIELD, LINE_HEIGHT_FIELD];
|
|
324
342
|
|
|
325
343
|
/**
|
|
326
|
-
* Every durable
|
|
344
|
+
* Every durable field normalization covers: the live axes, the retired pair, and
|
|
345
|
+
* the scale marker (`WEIGHT_OFFSETS_FIELD`, which is deliberately NOT a value
|
|
346
|
+
* axis — a preset must never carry it, or applying that preset would re-label a
|
|
347
|
+
* document still holding absolute weights).
|
|
327
348
|
*/
|
|
328
|
-
var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS);
|
|
349
|
+
var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS, [WEIGHT_OFFSETS_FIELD]);
|
|
329
350
|
|
|
330
351
|
/**
|
|
331
352
|
* Longest accepted font stack, in characters (mirrored by the host schema).
|
|
@@ -444,7 +465,7 @@ var FEATURES_UNSAFE = /[^a-zA-Z0-9"' ,]/g;
|
|
|
444
465
|
* ------------------------------------------------------------------ */
|
|
445
466
|
|
|
446
467
|
/** Clamp/validate one field by name (shared by config and value sets). */
|
|
447
|
-
function normalizeField(field, value) {
|
|
468
|
+
function normalizeField(field, value, relative) {
|
|
448
469
|
switch (field) {
|
|
449
470
|
case SANS_FIELD:
|
|
450
471
|
case STACK_DIALOG_FIELD:
|
|
@@ -457,7 +478,7 @@ function normalizeField(field, value) {
|
|
|
457
478
|
case WEIGHT_FIELD:
|
|
458
479
|
case WEIGHT_DIALOG_FIELD:
|
|
459
480
|
case CODE_WEIGHT_FIELD:
|
|
460
|
-
return clampWeightDelta(value);
|
|
481
|
+
return clampWeightDelta(value, relative);
|
|
461
482
|
case LINE_HEIGHT_FIELD:
|
|
462
483
|
case LINE_HEIGHT_DIALOG_FIELD:
|
|
463
484
|
return clampRatio(value);
|
|
@@ -465,6 +486,9 @@ function normalizeField(field, value) {
|
|
|
465
486
|
return clampLineOffset(value);
|
|
466
487
|
case LIGATURES_FIELD:
|
|
467
488
|
return clampLigatures(value);
|
|
489
|
+
case WEIGHT_OFFSETS_FIELD:
|
|
490
|
+
// The document's own scale marker: false/absent = the pre-relative scale.
|
|
491
|
+
return toBool(value);
|
|
468
492
|
case FEATURES_FIELD:
|
|
469
493
|
return sanitizeFeatures(value);
|
|
470
494
|
case NO_SYNTHETIC_ITALIC_FIELD:
|
|
@@ -488,14 +512,17 @@ function normalizeField(field, value) {
|
|
|
488
512
|
*/
|
|
489
513
|
function normalizeConfig(value) {
|
|
490
514
|
var source = value !== null && typeof value === "object" ? value : {};
|
|
515
|
+
// The scale marker is read BEFORE the axes: it decides whether a weight inside
|
|
516
|
+
// `300…600` is a legacy absolute value or an offset (see `clampWeightDelta`).
|
|
517
|
+
var relative = toBool(source[WEIGHT_OFFSETS_FIELD]);
|
|
491
518
|
var config = {};
|
|
492
519
|
for (var index = 0; index < DURABLE_FIELDS.length; index += 1) {
|
|
493
520
|
var field = DURABLE_FIELDS[index];
|
|
494
|
-
config[field] = normalizeField(field, source[field]);
|
|
521
|
+
config[field] = normalizeField(field, source[field], relative);
|
|
495
522
|
}
|
|
496
523
|
config[PER_THEME_FIELD] = toBool(source[PER_THEME_FIELD]);
|
|
497
|
-
config[DARK_VALUES_FIELD] = normalizeDarkValues(source[DARK_VALUES_FIELD]);
|
|
498
|
-
config[PRESETS_FIELD] = normalizePresets(source[PRESETS_FIELD]);
|
|
524
|
+
config[DARK_VALUES_FIELD] = normalizeDarkValues(source[DARK_VALUES_FIELD], relative);
|
|
525
|
+
config[PRESETS_FIELD] = normalizePresets(source[PRESETS_FIELD], relative);
|
|
499
526
|
config[ACTIVE_PRESET_FIELD] = sanitize(source[ACTIVE_PRESET_FIELD]).slice(0, MAX_PRESET_NAME);
|
|
500
527
|
return config;
|
|
501
528
|
}
|
|
@@ -533,21 +560,21 @@ function clampWeight(value) {
|
|
|
533
560
|
* Clamp one RELATIVE weight to its allowed range, migrating an absolute value.
|
|
534
561
|
*
|
|
535
562
|
* A document written before the axes became relative stores an absolute weight
|
|
536
|
-
* (`300…600`, `400` = normal)
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
* instead of jumping to the top of the new scale.
|
|
563
|
+
* (`300…600`, `400` = normal) and has no marker, so a value inside `300…600` is
|
|
564
|
+
* converted to its distance from normal (`400 → 0`, `560 → +160`, `300 → −100`).
|
|
565
|
+
* A marked document is on the offset scale already and is never converted — that
|
|
566
|
+
* is what lets an offset grow past `+200` without being mistaken for one of those
|
|
567
|
+
* absolute values (the two windows overlap; see `WEIGHT_OFFSETS_FIELD`).
|
|
542
568
|
*
|
|
543
569
|
* @param {unknown} value - candidate offset, or a pre-0.2.8 absolute weight.
|
|
570
|
+
* @param {boolean} [relative] - true when the document carries the offset marker.
|
|
544
571
|
* @returns {number} the offset the stylesheet may use.
|
|
545
572
|
*/
|
|
546
|
-
function clampWeightDelta(value) {
|
|
573
|
+
function clampWeightDelta(value, relative) {
|
|
547
574
|
var delta = Number(value);
|
|
548
575
|
if (!isFinite(delta)) return WEIGHT_UNSET;
|
|
549
576
|
delta = Math.round(delta);
|
|
550
|
-
if (delta >= WEIGHT_MIN && delta <= WEIGHT_MAX) delta -= WEIGHT_BASE;
|
|
577
|
+
if (relative !== true && delta >= WEIGHT_MIN && delta <= WEIGHT_MAX) delta -= WEIGHT_BASE;
|
|
551
578
|
// The slider's own range comes from the font (see `weightProfileFrom`) and can
|
|
552
579
|
// reach further than the nominal one, so the hard bounds are the widest range
|
|
553
580
|
// any font-derived slider may use.
|
|
@@ -595,13 +622,13 @@ function clampLigatures(value) {
|
|
|
595
622
|
* @param {unknown} values - candidate map.
|
|
596
623
|
* @returns {Record<string, string|number|boolean>} the safe subset.
|
|
597
624
|
*/
|
|
598
|
-
function normalizeValueSet(values) {
|
|
625
|
+
function normalizeValueSet(values, relative) {
|
|
599
626
|
var out = {};
|
|
600
627
|
if (values === null || typeof values !== "object" || Array.isArray(values)) return out;
|
|
601
628
|
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
602
629
|
var field = VALUE_FIELDS[index];
|
|
603
630
|
if (!Object.prototype.hasOwnProperty.call(values, field)) continue;
|
|
604
|
-
out[field] = normalizeField(field, values[field]);
|
|
631
|
+
out[field] = normalizeField(field, values[field], relative);
|
|
605
632
|
}
|
|
606
633
|
return out;
|
|
607
634
|
}
|
|
@@ -611,7 +638,7 @@ function normalizeValueSet(values) {
|
|
|
611
638
|
* @param {unknown} value - candidate JSON text or object.
|
|
612
639
|
* @returns {Record<string, string|number|boolean>} the sparse override map.
|
|
613
640
|
*/
|
|
614
|
-
function normalizeDarkValues(value) {
|
|
641
|
+
function normalizeDarkValues(value, relative) {
|
|
615
642
|
if (typeof value === "string") {
|
|
616
643
|
if (value.trim() === "") return {};
|
|
617
644
|
try {
|
|
@@ -620,7 +647,70 @@ function normalizeDarkValues(value) {
|
|
|
620
647
|
return {};
|
|
621
648
|
}
|
|
622
649
|
}
|
|
623
|
-
return normalizeValueSet(value);
|
|
650
|
+
return normalizeValueSet(value, relative);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** The three axes that share the weight scale (and its marker). */
|
|
654
|
+
var WEIGHT_FIELDS = [WEIGHT_FIELD, WEIGHT_DIALOG_FIELD, CODE_WEIGHT_FIELD];
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Convert the weight fields of saved preset snapshots to offsets.
|
|
658
|
+
*
|
|
659
|
+
* Runs exactly once per document, right before the card sets
|
|
660
|
+
* `WEIGHT_OFFSETS_FIELD`: a snapshot saved before 0.2.8 holds an absolute weight,
|
|
661
|
+
* and once the document is marked a stored weight is an offset, so applying that
|
|
662
|
+
* snapshot later would read `450` as +450 instead of +50. Converting the snapshots
|
|
663
|
+
* in the same breath as the document keeps the two on one scale.
|
|
664
|
+
*
|
|
665
|
+
* Only values inside the legacy absolute window are touched, so a snapshot that
|
|
666
|
+
* already holds offsets (any 0.3.x one) comes back unchanged and `changed` stays
|
|
667
|
+
* false — the caller then writes nothing.
|
|
668
|
+
*
|
|
669
|
+
* @param {unknown} presets - normalized presets, or anything shaped like them.
|
|
670
|
+
* @returns {{presets: object[], changed: boolean}} the converted list.
|
|
671
|
+
*/
|
|
672
|
+
function migratePresetWeights(presets) {
|
|
673
|
+
if (!Array.isArray(presets)) return { presets: [], changed: false };
|
|
674
|
+
var changed = false;
|
|
675
|
+
var out = [];
|
|
676
|
+
for (var index = 0; index < presets.length; index += 1) {
|
|
677
|
+
var entry = presets[index];
|
|
678
|
+
if (entry === null || typeof entry !== "object" || entry.values === null) {
|
|
679
|
+
out.push(entry);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
var values = entry.values;
|
|
683
|
+
if (typeof values !== "object") {
|
|
684
|
+
out.push(entry);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
var converted = null;
|
|
688
|
+
for (var axis = 0; axis < WEIGHT_FIELDS.length; axis += 1) {
|
|
689
|
+
var field = WEIGHT_FIELDS[axis];
|
|
690
|
+
if (!Object.prototype.hasOwnProperty.call(values, field)) continue;
|
|
691
|
+
var stored = Number(values[field]);
|
|
692
|
+
if (!isFinite(stored) || stored < WEIGHT_MIN || stored > WEIGHT_MAX) continue;
|
|
693
|
+
if (converted === null) {
|
|
694
|
+
converted = {};
|
|
695
|
+
for (var key in values) {
|
|
696
|
+
if (Object.prototype.hasOwnProperty.call(values, key)) converted[key] = values[key];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
converted[field] = stored - WEIGHT_BASE;
|
|
700
|
+
}
|
|
701
|
+
if (converted === null) {
|
|
702
|
+
out.push(entry);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
changed = true;
|
|
706
|
+
var copy = {};
|
|
707
|
+
for (var name in entry) {
|
|
708
|
+
if (Object.prototype.hasOwnProperty.call(entry, name)) copy[name] = entry[name];
|
|
709
|
+
}
|
|
710
|
+
copy.values = converted;
|
|
711
|
+
out.push(copy);
|
|
712
|
+
}
|
|
713
|
+
return { presets: out, changed: changed };
|
|
624
714
|
}
|
|
625
715
|
|
|
626
716
|
/**
|
|
@@ -628,7 +718,7 @@ function normalizeDarkValues(value) {
|
|
|
628
718
|
* @param {unknown} value - candidate JSON text or array.
|
|
629
719
|
* @returns {{name: string, values: object, savedAt: number}[]}
|
|
630
720
|
*/
|
|
631
|
-
function normalizePresets(value) {
|
|
721
|
+
function normalizePresets(value, relative) {
|
|
632
722
|
var list = value;
|
|
633
723
|
if (typeof value === "string") {
|
|
634
724
|
if (value.trim() === "") return [];
|
|
@@ -648,7 +738,7 @@ function normalizePresets(value) {
|
|
|
648
738
|
var stamp = Math.round(Number(entry.savedAt));
|
|
649
739
|
out.push({
|
|
650
740
|
name: name,
|
|
651
|
-
values: normalizeValueSet(entry.values),
|
|
741
|
+
values: normalizeValueSet(entry.values, relative),
|
|
652
742
|
savedAt: isFinite(stamp) && stamp > 0 ? stamp : 0,
|
|
653
743
|
});
|
|
654
744
|
}
|
|
@@ -1701,6 +1791,17 @@ function resolveAxes(config) {
|
|
|
1701
1791
|
*/
|
|
1702
1792
|
var WEIGHT_STEP_CANDIDATES = [1, 5, 10, 25, 50, 100, 200, 300];
|
|
1703
1793
|
|
|
1794
|
+
/**
|
|
1795
|
+
* How many positions a slider must still offer before a coarser step is refused.
|
|
1796
|
+
*
|
|
1797
|
+
* Coarseness is the point (see `weightProfileFrom`), but a control with two
|
|
1798
|
+
* positions is not a slider. Four is the smallest count that still lets a reader
|
|
1799
|
+
* go one notch either side of the neutral position and one further up — and on
|
|
1800
|
+
* the measured seven-face family it lands on a 200-weight step, where one notch is
|
|
1801
|
+
* a +30% change in ink, against +16% at 100 (which is why 100 was not enough).
|
|
1802
|
+
*/
|
|
1803
|
+
var WEIGHT_MIN_POSITIONS = 4;
|
|
1804
|
+
|
|
1704
1805
|
/** The weight grid the profile is measured on. */
|
|
1705
1806
|
var WEIGHT_PROBE_LOW = 100;
|
|
1706
1807
|
var WEIGHT_PROBE_HIGH = 900;
|
|
@@ -1742,19 +1843,28 @@ function everyPositionDiffers(sample, bases, min, max, step) {
|
|
|
1742
1843
|
/**
|
|
1743
1844
|
* The slider a family deserves: how far it can go, and by how much.
|
|
1744
1845
|
*
|
|
1745
|
-
* A font
|
|
1746
|
-
*
|
|
1747
|
-
* fine step and a wide range. Measured on a weight grid:
|
|
1846
|
+
* A font can be made lighter or heavier only as far as its faces reach, and only
|
|
1847
|
+
* in jumps between them. Measured on a weight grid:
|
|
1748
1848
|
* - the downward reach is the highest weight that still renders as light as the
|
|
1749
1849
|
* family can (asking for less cannot change anything);
|
|
1750
|
-
* - the upward reach is the LOWEST weight at or above
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
1850
|
+
* - the upward reach is the LOWEST weight at or above body text that already
|
|
1851
|
+
* renders as heavy as the family can, so the range ends where the text a reader
|
|
1852
|
+
* is reading stops changing (see `WEIGHT_DELTA_UP_LIMIT` for the ceiling);
|
|
1853
|
+
* - the step is the COARSEST candidate that still leaves `WEIGHT_MIN_POSITIONS`
|
|
1854
|
+
* positions with body text changing at every one of them. Coarse is the point:
|
|
1855
|
+
* a finer step splits one face into several positions that all render the same,
|
|
1856
|
+
* so the control feels dead (measured on Noto Serif SC, whose faces are
|
|
1857
|
+
* 100-250 / 300 / 400 / 500 / 600 / 700 / 800-900: one 100-weight step is a
|
|
1858
|
+
* +16% change in ink, one 200-weight step +30%).
|
|
1859
|
+
*
|
|
1860
|
+
* Bold text and headings are deliberately NOT required to change at every
|
|
1861
|
+
* position: the heaviest ladder layer sits at 600 and reaches the ceiling at +300,
|
|
1862
|
+
* so no range can move it further. Body text is what a reader reads, so it carries
|
|
1863
|
+
* the requirement, and a heading already at its boldest face stays there.
|
|
1864
|
+
*
|
|
1865
|
+
* Every position is a whole number of steps from 0, so the card's step counts map
|
|
1866
|
+
* back onto exactly the offsets the document stores.
|
|
1867
|
+
*
|
|
1758
1868
|
* @param {(weight: number) => string|number} sample - renders a probe at one
|
|
1759
1869
|
* weight and returns a signature; equal signatures mean identical rendering.
|
|
1760
1870
|
* @param {{candidates?: number[], floor?: number, ceil?: number, top?: number}}
|
|
@@ -1802,27 +1912,49 @@ function weightProfileFrom(sample, options) {
|
|
|
1802
1912
|
}
|
|
1803
1913
|
var reachUp = Math.min(ceil, WEIGHT_DELTA_UP_LIMIT, headroom);
|
|
1804
1914
|
var reachDown = Math.max(floor, Math.min(0, lightest - WEIGHT_BASE));
|
|
1805
|
-
var
|
|
1806
|
-
|
|
1915
|
+
var cap = Math.min(reachUp, WEIGHT_DELTA_UP_LIMIT);
|
|
1916
|
+
/**
|
|
1917
|
+
* The shape one candidate step describes.
|
|
1918
|
+
*
|
|
1919
|
+
* Every position is a whole number of steps from 0, so the card's step counts
|
|
1920
|
+
* multiply back into exactly the offsets the document stores. The reach is
|
|
1921
|
+
* snapped to that grid without ever passing the hard floor or the ceiling —
|
|
1922
|
+
* rounding the top UP (which the first version did) put a position past the
|
|
1923
|
+
* widest allowed offset, and rounding the bottom DOWN could pass the floor.
|
|
1924
|
+
*/
|
|
1925
|
+
var shapeAt = function (step) {
|
|
1926
|
+
var down = Math.min(ceilTo(-reachDown, step), floorTo(-floor, step));
|
|
1927
|
+
return { min: -down, max: floorTo(cap, step), step: step };
|
|
1928
|
+
};
|
|
1929
|
+
var best = null;
|
|
1930
|
+
var bestCount = 0;
|
|
1931
|
+
// Coarsest candidate first: the coarsest step that still leaves enough visible
|
|
1932
|
+
// positions is the one a reader can actually work with.
|
|
1933
|
+
for (var index = candidates.length - 1; index >= 0; index -= 1) {
|
|
1807
1934
|
var step = candidates[index];
|
|
1808
1935
|
if (!(step > 0)) continue;
|
|
1809
|
-
var
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
var
|
|
1813
|
-
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
1816
|
-
|
|
1936
|
+
var shape = shapeAt(step);
|
|
1937
|
+
if (shape.max < shape.min) continue;
|
|
1938
|
+
if (!everyPositionDiffers(sample, [WEIGHT_BASE], shape.min, shape.max, step)) continue;
|
|
1939
|
+
var count = (shape.max - shape.min) / step + 1;
|
|
1940
|
+
if (count >= WEIGHT_MIN_POSITIONS) return shape;
|
|
1941
|
+
if (count > bestCount) {
|
|
1942
|
+
best = shape;
|
|
1943
|
+
bestCount = count;
|
|
1817
1944
|
}
|
|
1818
1945
|
}
|
|
1946
|
+
if (best !== null) {
|
|
1947
|
+
// Fewer positions than asked for is still usable; one position is the whole
|
|
1948
|
+
// range, and any step describes it, so use the finest and keep the unit small.
|
|
1949
|
+
if (best.max === best.min) return { min: best.min, max: best.max, step: 1 };
|
|
1950
|
+
return best;
|
|
1951
|
+
}
|
|
1952
|
+
// No step kept body text alive: the coarsest one that still fits is the honest
|
|
1953
|
+
// answer, never a step the control cannot reach.
|
|
1819
1954
|
var coarsest = candidates[candidates.length - 1];
|
|
1820
|
-
var
|
|
1821
|
-
var
|
|
1822
|
-
|
|
1823
|
-
// honest answer, never a step the control cannot reach.
|
|
1824
|
-
var span = max - min;
|
|
1825
|
-
return { min: min, max: max, step: span > 0 ? Math.min(coarsest, span) : 1 };
|
|
1955
|
+
var plain = shapeAt(coarsest);
|
|
1956
|
+
var span = plain.max - plain.min;
|
|
1957
|
+
return { min: plain.min, max: plain.max, step: span > 0 ? Math.min(coarsest, span) : 1 };
|
|
1826
1958
|
}
|
|
1827
1959
|
|
|
1828
1960
|
/**
|
|
@@ -1905,9 +2037,11 @@ function isDormant(config) {
|
|
|
1905
2037
|
sanitize(axis[MONO_FIELD]) === "" &&
|
|
1906
2038
|
clampOffset(axis[SIZE_DIALOG_FIELD]) === 0 &&
|
|
1907
2039
|
clampOffset(axis[CODE_SIZE_FIELD]) === 0 &&
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
clampWeightDelta(axis[
|
|
2040
|
+
// These arrive normalized, so they are offsets already: converting them again
|
|
2041
|
+
// would rescale an offset that legitimately sits inside `300…600`.
|
|
2042
|
+
clampWeightDelta(axis[WEIGHT_FIELD], true) === WEIGHT_UNSET &&
|
|
2043
|
+
clampWeightDelta(axis[WEIGHT_DIALOG_FIELD], true) === WEIGHT_UNSET &&
|
|
2044
|
+
clampWeightDelta(axis[CODE_WEIGHT_FIELD], true) === WEIGHT_UNSET &&
|
|
1911
2045
|
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
1912
2046
|
clampLineOffset(axis[CODE_LINE_HEIGHT_FIELD]) === 0 &&
|
|
1913
2047
|
clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
|
|
@@ -2787,6 +2921,9 @@ var shared = {
|
|
|
2787
2921
|
WEIGHT_FIELD: WEIGHT_FIELD,
|
|
2788
2922
|
WEIGHT_DIALOG_FIELD: WEIGHT_DIALOG_FIELD,
|
|
2789
2923
|
CODE_WEIGHT_FIELD: CODE_WEIGHT_FIELD,
|
|
2924
|
+
WEIGHT_OFFSETS_FIELD: WEIGHT_OFFSETS_FIELD,
|
|
2925
|
+
WEIGHT_FIELDS: WEIGHT_FIELDS,
|
|
2926
|
+
migratePresetWeights: migratePresetWeights,
|
|
2790
2927
|
LINE_HEIGHT_FIELD: LINE_HEIGHT_FIELD,
|
|
2791
2928
|
LINE_HEIGHT_DIALOG_FIELD: LINE_HEIGHT_DIALOG_FIELD,
|
|
2792
2929
|
CODE_LINE_HEIGHT_FIELD: CODE_LINE_HEIGHT_FIELD,
|
|
@@ -2881,6 +3018,7 @@ var shared = {
|
|
|
2881
3018
|
WEIGHT_STEP_CANDIDATES: WEIGHT_STEP_CANDIDATES,
|
|
2882
3019
|
WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
|
|
2883
3020
|
WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
|
|
3021
|
+
WEIGHT_MIN_POSITIONS: WEIGHT_MIN_POSITIONS,
|
|
2884
3022
|
WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
|
|
2885
3023
|
WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
|
|
2886
3024
|
WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
|
|
@@ -2964,6 +3102,7 @@ var NO_SYNTHETIC_BOLD_FIELD = shared.NO_SYNTHETIC_BOLD_FIELD;
|
|
|
2964
3102
|
var PER_THEME_FIELD = shared.PER_THEME_FIELD;
|
|
2965
3103
|
var DARK_VALUES_FIELD = shared.DARK_VALUES_FIELD;
|
|
2966
3104
|
var PRESETS_FIELD = shared.PRESETS_FIELD;
|
|
3105
|
+
var WEIGHT_OFFSETS_FIELD = shared.WEIGHT_OFFSETS_FIELD;
|
|
2967
3106
|
var ACTIVE_PRESET_FIELD = shared.ACTIVE_PRESET_FIELD;
|
|
2968
3107
|
var UI_FOLLOWS_FIELD = shared.UI_FOLLOWS_FIELD;
|
|
2969
3108
|
var VALUE_FIELDS = shared.VALUE_FIELDS;
|
|
@@ -3213,13 +3352,13 @@ var DICTS = {
|
|
|
3213
3352
|
|
|
3214
3353
|
"weight.uiLabel": "Interface font-weight offset",
|
|
3215
3354
|
"weight.uiHint":
|
|
3216
|
-
"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 swaps in a different face for body text and headings alike, and another font offers a different number of steps.",
|
|
3355
|
+
"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 swaps in a different face for body text and headings alike, 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.",
|
|
3217
3356
|
"weight.dialogLabel": "Conversation font-weight offset",
|
|
3218
3357
|
"weight.dialogHint":
|
|
3219
|
-
"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 swaps in a different face for body text and headings alike, and another font offers a different number of steps.",
|
|
3358
|
+
"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 swaps in a different face for body text and headings alike, 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.",
|
|
3220
3359
|
"weight.codeLabel": "Code font-weight offset",
|
|
3221
3360
|
"weight.codeHint":
|
|
3222
|
-
"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 swaps in a different face for body text and headings alike, and another font offers a different number of steps.",
|
|
3361
|
+
"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 swaps in a different face for body text and headings alike, 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.",
|
|
3223
3362
|
|
|
3224
3363
|
"line.dialogLabel": "Conversation line height",
|
|
3225
3364
|
"line.dialogHint":
|
|
@@ -3242,13 +3381,13 @@ var DICTS = {
|
|
|
3242
3381
|
|
|
3243
3382
|
"synth.italic": "No faux italic",
|
|
3244
3383
|
"synth.italicHint":
|
|
3245
|
-
"CJK faces have no italic, so the browser tilts them. On keeps marked text upright.",
|
|
3384
|
+
"CJK faces have no italic, so the browser tilts them. On keeps marked text upright. (Same as faux bold: one extra step at most.)",
|
|
3246
3385
|
"synth.bold": "No faux bold",
|
|
3247
3386
|
"synth.boldHint":
|
|
3248
|
-
"For faces without a real bold: bold text stops being thickened artificially.",
|
|
3387
|
+
"For faces without a real bold: bold text stops being thickened artificially. (Disabling it lets the browser fake a bold face, which adds one extra step for a font that ships none — it cannot invent the middle ones.)",
|
|
3249
3388
|
"synth.simple": "No faux italic / faux bold",
|
|
3250
3389
|
"synth.simpleHint":
|
|
3251
|
-
"Stops synthetic italic and synthetic bold everywhere; switch to Advanced to set them apart.",
|
|
3390
|
+
"Stops synthetic italic and synthetic bold everywhere; switch to Advanced to set them apart. (Both switches: one extra step at most, no middle ones.)",
|
|
3252
3391
|
|
|
3253
3392
|
"section.ui": "Interface",
|
|
3254
3393
|
"section.dialog": "Conversation",
|
|
@@ -3257,7 +3396,8 @@ var DICTS = {
|
|
|
3257
3396
|
"section.close": "Collapse {name}",
|
|
3258
3397
|
|
|
3259
3398
|
"ui.follow": "Follows the conversation",
|
|
3260
|
-
"ui.followHint": "Font and weight",
|
|
3399
|
+
"ui.followHint": "Font and weight. While this is on, the interface section's own values are ignored.",
|
|
3400
|
+
"write.reverted": "The setting did not reach the document, so it fell back to what the document holds.",
|
|
3261
3401
|
"ui.own": "Own values",
|
|
3262
3402
|
"dialog.default": "DSH defaults",
|
|
3263
3403
|
|
|
@@ -3357,13 +3497,13 @@ var DICTS = {
|
|
|
3357
3497
|
|
|
3358
3498
|
"weight.uiLabel": "界面字重偏移",
|
|
3359
3499
|
"weight.uiHint":
|
|
3360
|
-
"在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
3500
|
+
"在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
3361
3501
|
"weight.dialogLabel": "对话字重偏移",
|
|
3362
3502
|
"weight.dialogHint":
|
|
3363
|
-
"在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
3503
|
+
"在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
3364
3504
|
"weight.codeLabel": "代码字重偏移",
|
|
3365
3505
|
"weight.codeHint":
|
|
3366
|
-
"在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH
|
|
3506
|
+
"在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。档距按当前字体实测,其中幅度最小的一档看起来最弱。",
|
|
3367
3507
|
|
|
3368
3508
|
"line.dialogLabel": "对话行高",
|
|
3369
3509
|
"line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
|
|
@@ -3384,12 +3524,12 @@ var DICTS = {
|
|
|
3384
3524
|
|
|
3385
3525
|
"synth.italic": "禁用伪斜体",
|
|
3386
3526
|
"synth.italicHint":
|
|
3387
|
-
"
|
|
3527
|
+
"中文字体没有斜体,浏览器会把标记文本硬掰歪;开启后保持直立。(与伪粗体同理:最多多出一档。)",
|
|
3388
3528
|
"synth.bold": "禁用伪粗体",
|
|
3389
|
-
"synth.boldHint": "
|
|
3529
|
+
"synth.boldHint": "没有真实粗体的字体不再被人为加粗。(关掉它,浏览器会为没有粗体字形的字体自己造粗体:能多出一档,但变不出中间档。)",
|
|
3390
3530
|
"synth.simple": "禁用伪斜体/伪粗体",
|
|
3391
3531
|
"synth.simpleHint":
|
|
3392
|
-
"
|
|
3532
|
+
"整页停用合成斜体与合成粗体;切到高级模式可分开设置。(两项一起关:最多多出一档,变不出中间档。)",
|
|
3393
3533
|
|
|
3394
3534
|
"section.ui": "界面",
|
|
3395
3535
|
"section.dialog": "对话",
|
|
@@ -3399,7 +3539,8 @@ var DICTS = {
|
|
|
3399
3539
|
|
|
3400
3540
|
|
|
3401
3541
|
"ui.follow": "跟随对话设置",
|
|
3402
|
-
"ui.followHint": "
|
|
3542
|
+
"ui.followHint": "字体与字重跟随开启时,界面分区自己的取值会被忽略。",
|
|
3543
|
+
"write.reverted": "设置没写进文档,已恢复为文档里的值。",
|
|
3403
3544
|
"ui.own": "独立数值",
|
|
3404
3545
|
"dialog.default": "DSH 默认",
|
|
3405
3546
|
|
|
@@ -3597,6 +3738,8 @@ var CARD_CSS = [
|
|
|
3597
3738
|
".dfp-slider{flex:1;min-width:0;height:20px;accent-color:var(--dsw-alias-brand-primary)}",
|
|
3598
3739
|
".dfp-value{flex:none;min-width:56px;text-align:right;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;font-variant-numeric:tabular-nums}",
|
|
3599
3740
|
".dfp-scale{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;display:flex;justify-content:space-between}",
|
|
3741
|
+
// a write the document never took: visible, but quieter than the field label
|
|
3742
|
+
".dfp-sliderNotice{margin-top:6px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}",
|
|
3600
3743
|
|
|
3601
3744
|
// the per-section preview: an inline box at the end of the expanded
|
|
3602
3745
|
// section body, showing only that section's own sample
|
|
@@ -4109,6 +4252,9 @@ function Section(props) {
|
|
|
4109
4252
|
*/
|
|
4110
4253
|
function NumberSlider(props) {
|
|
4111
4254
|
var [pending, setPending] = useState(null);
|
|
4255
|
+
// A write the document never took: the readout falls back to the document and
|
|
4256
|
+
// this says so once, instead of the thumb looking like it moved on its own.
|
|
4257
|
+
var [reverted, setReverted] = useState(false);
|
|
4112
4258
|
var step = typeof props.step === "number" && props.step > 0 ? props.step : 1;
|
|
4113
4259
|
// The value the host has not confirmed yet. Between the release and the
|
|
4114
4260
|
// settings round-trip the committed prop is still the OLD number — clearing
|
|
@@ -4141,6 +4287,7 @@ function NumberSlider(props) {
|
|
|
4141
4287
|
);
|
|
4142
4288
|
var commit = function (value) {
|
|
4143
4289
|
awaitingRef.current = value;
|
|
4290
|
+
setReverted(false);
|
|
4144
4291
|
props.onChange(value);
|
|
4145
4292
|
};
|
|
4146
4293
|
/**
|
|
@@ -4160,6 +4307,8 @@ function NumberSlider(props) {
|
|
|
4160
4307
|
var timer = setTimeout(function () {
|
|
4161
4308
|
awaitingRef.current = null;
|
|
4162
4309
|
setLocal(null);
|
|
4310
|
+
// The document is the truth and it never took this value: say so once.
|
|
4311
|
+
setReverted(true);
|
|
4163
4312
|
}, COMMIT_PATIENCE_MS);
|
|
4164
4313
|
return function () {
|
|
4165
4314
|
clearTimeout(timer);
|
|
@@ -4221,6 +4370,9 @@ function NumberSlider(props) {
|
|
|
4221
4370
|
}),
|
|
4222
4371
|
h("span", { className: "dfp-value" }, readout)
|
|
4223
4372
|
),
|
|
4373
|
+
reverted && props.revertedText
|
|
4374
|
+
? h("div", { className: "dfp-sliderNotice" }, props.revertedText)
|
|
4375
|
+
: null,
|
|
4224
4376
|
h(
|
|
4225
4377
|
"div",
|
|
4226
4378
|
{ className: "dfp-scale" },
|
|
@@ -5466,6 +5618,7 @@ function FontCard(props) {
|
|
|
5466
5618
|
value: props.value,
|
|
5467
5619
|
disabled: !writable,
|
|
5468
5620
|
label: t(props.labelKey),
|
|
5621
|
+
revertedText: t("write.reverted"),
|
|
5469
5622
|
readout: props.text + " " + t("size.unit"),
|
|
5470
5623
|
minLabel: SIZE_MIN + " " + t("size.unit"),
|
|
5471
5624
|
maxLabel: "+" + SIZE_MAX + " " + t("size.unit"),
|
|
@@ -5520,6 +5673,7 @@ function FontCard(props) {
|
|
|
5520
5673
|
value: shape.value,
|
|
5521
5674
|
disabled: !writable,
|
|
5522
5675
|
label: t(props.labelKey),
|
|
5676
|
+
revertedText: t("write.reverted"),
|
|
5523
5677
|
// One notch is one family step, so every notch changes what is on screen.
|
|
5524
5678
|
step: 1,
|
|
5525
5679
|
// The readout is the STEP COUNT, not the weight: "+2" is two faces
|
|
@@ -5532,6 +5686,8 @@ function FontCard(props) {
|
|
|
5532
5686
|
// the document confirmed it).
|
|
5533
5687
|
pendingText: text,
|
|
5534
5688
|
onChange: function (count) {
|
|
5689
|
+
// Once per document, before anything is written on the new scale.
|
|
5690
|
+
adoptWeightScale();
|
|
5535
5691
|
// The neutral position means "leave the axis alone" rather than "write
|
|
5536
5692
|
// it on every element".
|
|
5537
5693
|
if (count === 0) resetField(props.field);
|
|
@@ -5562,6 +5718,7 @@ function FontCard(props) {
|
|
|
5562
5718
|
value: props.value,
|
|
5563
5719
|
disabled: !writable,
|
|
5564
5720
|
label: t(props.labelKey),
|
|
5721
|
+
revertedText: t("write.reverted"),
|
|
5565
5722
|
readout: props.value + " " + t("line.unit"),
|
|
5566
5723
|
minLabel: LINE_HEIGHT_MIN + " " + t("line.unit"),
|
|
5567
5724
|
maxLabel: LINE_HEIGHT_MAX + " " + t("line.unit"),
|
|
@@ -5598,6 +5755,7 @@ function FontCard(props) {
|
|
|
5598
5755
|
value: value,
|
|
5599
5756
|
disabled: !writable,
|
|
5600
5757
|
label: t("line.codeLabel"),
|
|
5758
|
+
revertedText: t("write.reverted"),
|
|
5601
5759
|
readout: (value > 0 ? "+" + value : String(value)) + "px",
|
|
5602
5760
|
minLabel: CODE_LINE_HEIGHT_MIN + "px",
|
|
5603
5761
|
maxLabel: "+" + CODE_LINE_HEIGHT_MAX + "px",
|
|
@@ -5742,6 +5900,40 @@ function FontCard(props) {
|
|
|
5742
5900
|
submit(PRESETS_FIELD, JSON.stringify(next));
|
|
5743
5901
|
};
|
|
5744
5902
|
|
|
5903
|
+
/**
|
|
5904
|
+
* Adopt the offset scale, once per document.
|
|
5905
|
+
*
|
|
5906
|
+
* A document written before the weight axes became relative stores absolute
|
|
5907
|
+
* weights inside `300…600`, and once a document is marked a stored `450` means
|
|
5908
|
+
* +450 — the two scales overlap, so the conversion must happen exactly once and
|
|
5909
|
+
* never again. This is that moment: before the first weight this card writes,
|
|
5910
|
+
* convert whatever is still absolute (the three fields and every saved snapshot),
|
|
5911
|
+
* then set the marker.
|
|
5912
|
+
*
|
|
5913
|
+
* Values first, marker last: a stylesheet rebuild between the two still sees
|
|
5914
|
+
* values that render exactly as before, and a marked document is never left
|
|
5915
|
+
* holding an unconverted absolute weight.
|
|
5916
|
+
*
|
|
5917
|
+
* The three fields are read from `config`, not from the expanded `editing` set:
|
|
5918
|
+
* while the interface follows the conversation, `editing` mirrors the
|
|
5919
|
+
* conversation's weight into the interface's own field, and writing that back
|
|
5920
|
+
* would silently freeze the follow.
|
|
5921
|
+
*/
|
|
5922
|
+
var adoptWeightScale = function () {
|
|
5923
|
+
if (config[WEIGHT_OFFSETS_FIELD] === true) return;
|
|
5924
|
+
for (var index = 0; index < shared.WEIGHT_FIELDS.length; index += 1) {
|
|
5925
|
+
var field = shared.WEIGHT_FIELDS[index];
|
|
5926
|
+
var stored = Number(config[field]);
|
|
5927
|
+
if (!isFinite(stored) || stored < shared.WEIGHT_MIN || stored > shared.WEIGHT_MAX) {
|
|
5928
|
+
continue;
|
|
5929
|
+
}
|
|
5930
|
+
submit(field, stored - shared.WEIGHT_BASE);
|
|
5931
|
+
}
|
|
5932
|
+
var migrated = shared.migratePresetWeights(config[PRESETS_FIELD]);
|
|
5933
|
+
if (migrated.changed) writePresets(migrated.presets);
|
|
5934
|
+
submit(WEIGHT_OFFSETS_FIELD, true);
|
|
5935
|
+
};
|
|
5936
|
+
|
|
5745
5937
|
/**
|
|
5746
5938
|
* Switch presets: the entry becomes the auto-save target and its stored
|
|
5747
5939
|
* values are applied over the set currently being edited — every axis the
|
package/lib/index.js
CHANGED
|
@@ -217,6 +217,12 @@ export const Config = volatileWhenSupported(z.object({
|
|
|
217
217
|
.description(
|
|
218
218
|
`Code font-weight offset; added to each code surface's own weight and independent of the interface weight (the slider spans whatever weights the chosen font can render), ${shared.WEIGHT_UNSET} keeps DSH's own weights. A legacy absolute weight (${shared.WEIGHT_MIN}..${shared.WEIGHT_MAX}) is accepted and converted to its offset.`
|
|
219
219
|
),
|
|
220
|
+
[shared.WEIGHT_OFFSETS_FIELD]: z
|
|
221
|
+
.boolean()
|
|
222
|
+
.default(false)
|
|
223
|
+
.description(
|
|
224
|
+
"Scale marker: the three weight fields hold offsets, not legacy absolute weights (set automatically the first time the card writes a weight)"
|
|
225
|
+
),
|
|
220
226
|
[LINE_HEIGHT_FIELD]: z
|
|
221
227
|
.number()
|
|
222
228
|
.min(shared.LINE_HEIGHT_MIN)
|