dsh-fonttune 0.3.4 → 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/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)
package/lib/shared.cjs CHANGED
@@ -77,6 +77,23 @@ var WEIGHT_DIALOG_FIELD = "weightDialog";
77
77
  /** Field carrying the code font weight (0 = leave DSH alone). */
78
78
  var CODE_WEIGHT_FIELD = "weightCode";
79
79
 
80
+ /**
81
+ * Marks a document whose weight fields are OFFSETS, not absolute weights.
82
+ *
83
+ * Before 0.2.8 the three weight axes stored an absolute CSS weight (`300…600`,
84
+ * `400` = normal) and `clampWeightDelta` converted one on sight. That inference
85
+ * only works while offsets never land inside the absolute window, which capped
86
+ * the offset range at +200 — and +200 is not enough room for a step the eye can
87
+ * see on a family with few faces (measured: one 200-weight step is a +30% change
88
+ * on Noto Serif SC, one 100-weight step only +16%).
89
+ *
90
+ * So the document says which scale it is on, and the inference is borrowed for
91
+ * exactly one write: the first time 0.3.5+ writes a weight, the card converts any
92
+ * remaining absolute value, converts the saved presets the same way, and sets
93
+ * this field. From then on a stored weight is an offset, whatever its size.
94
+ */
95
+ var WEIGHT_OFFSETS_FIELD = "weightOffsets";
96
+
80
97
  /**
81
98
  * Field carrying the interface line-height ratio in percent (100 = untouched).
82
99
  * A ratio rather than a px offset: interface line heights form a design ladder
@@ -208,17 +225,18 @@ var WEIGHT_LADDER_TOP = DIALOG_WEIGHT_GROUPS.reduce(function (top, group) {
208
225
  /**
209
226
  * How far above its base an offset may reach at all.
210
227
  *
211
- * Every axis applies the offset to each element's OWN weight, and the heaviest
212
- * layer of the ladder sits at `WEIGHT_LADDER_TOP`; past `WEIGHT_CSS_MAX` minus
213
- * that, the heaviest layer cannot move any further, so a longer range would only
214
- * add positions that change nothing for the boldest text on screen.
228
+ * The ceiling comes from the layer a notch MUST move: body text sits at
229
+ * `WEIGHT_BASE`, so the range runs as far as the text a reader is reading can
230
+ * still change — measured up to +400 on a family whose faces reach 900. Bold text
231
+ * stops moving at +300 and headings at +200; that is accepted, because a heading
232
+ * already at its heaviest face stays there (see `WEIGHT_LADDER_STRONG`).
215
233
  *
216
- * Staying inside it has a second, load-bearing effect: offsets then never land in
217
- * `WEIGHT_MIN…WEIGHT_MAX`, the range an ABSOLUTE weight from a pre-relative
218
- * document occupies (see `clampWeightDelta`). The two meanings cannot be confused,
219
- * so a stored offset is never rescaled into a different one.
234
+ * It used to stop at `WEIGHT_CSS_MAX - WEIGHT_LADDER_TOP` (= +200) so an offset
235
+ * could never land inside `WEIGHT_MIN…WEIGHT_MAX`, the window an ABSOLUTE weight
236
+ * from a pre-relative document occupies. The document's own marker resolves that
237
+ * window now (`WEIGHT_OFFSETS_FIELD`), so the range is free to grow.
220
238
  */
221
- var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX - WEIGHT_LADDER_TOP;
239
+ var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX - WEIGHT_BASE;
222
240
 
223
241
  /**
224
242
  * The heaviest layer a notch still has to move: bold text.
@@ -314,9 +332,12 @@ var VALUE_FIELDS = [
314
332
  var RETIRED_FIELDS = [SIZE_FIELD, LINE_HEIGHT_FIELD];
315
333
 
316
334
  /**
317
- * Every durable axis field, live and retired: what normalization covers.
335
+ * Every durable field normalization covers: the live axes, the retired pair, and
336
+ * the scale marker (`WEIGHT_OFFSETS_FIELD`, which is deliberately NOT a value
337
+ * axis — a preset must never carry it, or applying that preset would re-label a
338
+ * document still holding absolute weights).
318
339
  */
319
- var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS);
340
+ var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS, [WEIGHT_OFFSETS_FIELD]);
320
341
 
321
342
  /**
322
343
  * Longest accepted font stack, in characters (mirrored by the host schema).
@@ -435,7 +456,7 @@ var FEATURES_UNSAFE = /[^a-zA-Z0-9"' ,]/g;
435
456
  * ------------------------------------------------------------------ */
436
457
 
437
458
  /** Clamp/validate one field by name (shared by config and value sets). */
438
- function normalizeField(field, value) {
459
+ function normalizeField(field, value, relative) {
439
460
  switch (field) {
440
461
  case SANS_FIELD:
441
462
  case STACK_DIALOG_FIELD:
@@ -448,7 +469,7 @@ function normalizeField(field, value) {
448
469
  case WEIGHT_FIELD:
449
470
  case WEIGHT_DIALOG_FIELD:
450
471
  case CODE_WEIGHT_FIELD:
451
- return clampWeightDelta(value);
472
+ return clampWeightDelta(value, relative);
452
473
  case LINE_HEIGHT_FIELD:
453
474
  case LINE_HEIGHT_DIALOG_FIELD:
454
475
  return clampRatio(value);
@@ -456,6 +477,9 @@ function normalizeField(field, value) {
456
477
  return clampLineOffset(value);
457
478
  case LIGATURES_FIELD:
458
479
  return clampLigatures(value);
480
+ case WEIGHT_OFFSETS_FIELD:
481
+ // The document's own scale marker: false/absent = the pre-relative scale.
482
+ return toBool(value);
459
483
  case FEATURES_FIELD:
460
484
  return sanitizeFeatures(value);
461
485
  case NO_SYNTHETIC_ITALIC_FIELD:
@@ -479,14 +503,17 @@ function normalizeField(field, value) {
479
503
  */
480
504
  function normalizeConfig(value) {
481
505
  var source = value !== null && typeof value === "object" ? value : {};
506
+ // The scale marker is read BEFORE the axes: it decides whether a weight inside
507
+ // `300…600` is a legacy absolute value or an offset (see `clampWeightDelta`).
508
+ var relative = toBool(source[WEIGHT_OFFSETS_FIELD]);
482
509
  var config = {};
483
510
  for (var index = 0; index < DURABLE_FIELDS.length; index += 1) {
484
511
  var field = DURABLE_FIELDS[index];
485
- config[field] = normalizeField(field, source[field]);
512
+ config[field] = normalizeField(field, source[field], relative);
486
513
  }
487
514
  config[PER_THEME_FIELD] = toBool(source[PER_THEME_FIELD]);
488
- config[DARK_VALUES_FIELD] = normalizeDarkValues(source[DARK_VALUES_FIELD]);
489
- config[PRESETS_FIELD] = normalizePresets(source[PRESETS_FIELD]);
515
+ config[DARK_VALUES_FIELD] = normalizeDarkValues(source[DARK_VALUES_FIELD], relative);
516
+ config[PRESETS_FIELD] = normalizePresets(source[PRESETS_FIELD], relative);
490
517
  config[ACTIVE_PRESET_FIELD] = sanitize(source[ACTIVE_PRESET_FIELD]).slice(0, MAX_PRESET_NAME);
491
518
  return config;
492
519
  }
@@ -524,21 +551,21 @@ function clampWeight(value) {
524
551
  * Clamp one RELATIVE weight to its allowed range, migrating an absolute value.
525
552
  *
526
553
  * A document written before the axes became relative stores an absolute weight
527
- * (`300…600`, `400` = normal). That window sits entirely outside the delta
528
- * window, so the conversion is unambiguous and idempotent: a value inside
529
- * `300…600` is old and becomes its distance from normal (`400 → 0`, `560 → +160`,
530
- * `300 → −100`), while a value already inside `−100…200` is a delta and is left
531
- * alone. The user's saved setup therefore keeps meaning "as bold as before"
532
- * instead of jumping to the top of the new scale.
554
+ * (`300…600`, `400` = normal) and has no marker, so a value inside `300…600` is
555
+ * converted to its distance from normal (`400 → 0`, `560 → +160`, `300 → −100`).
556
+ * A marked document is on the offset scale already and is never converted — that
557
+ * is what lets an offset grow past `+200` without being mistaken for one of those
558
+ * absolute values (the two windows overlap; see `WEIGHT_OFFSETS_FIELD`).
533
559
  *
534
560
  * @param {unknown} value - candidate offset, or a pre-0.2.8 absolute weight.
561
+ * @param {boolean} [relative] - true when the document carries the offset marker.
535
562
  * @returns {number} the offset the stylesheet may use.
536
563
  */
537
- function clampWeightDelta(value) {
564
+ function clampWeightDelta(value, relative) {
538
565
  var delta = Number(value);
539
566
  if (!isFinite(delta)) return WEIGHT_UNSET;
540
567
  delta = Math.round(delta);
541
- if (delta >= WEIGHT_MIN && delta <= WEIGHT_MAX) delta -= WEIGHT_BASE;
568
+ if (relative !== true && delta >= WEIGHT_MIN && delta <= WEIGHT_MAX) delta -= WEIGHT_BASE;
542
569
  // The slider's own range comes from the font (see `weightProfileFrom`) and can
543
570
  // reach further than the nominal one, so the hard bounds are the widest range
544
571
  // any font-derived slider may use.
@@ -586,13 +613,13 @@ function clampLigatures(value) {
586
613
  * @param {unknown} values - candidate map.
587
614
  * @returns {Record<string, string|number|boolean>} the safe subset.
588
615
  */
589
- function normalizeValueSet(values) {
616
+ function normalizeValueSet(values, relative) {
590
617
  var out = {};
591
618
  if (values === null || typeof values !== "object" || Array.isArray(values)) return out;
592
619
  for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
593
620
  var field = VALUE_FIELDS[index];
594
621
  if (!Object.prototype.hasOwnProperty.call(values, field)) continue;
595
- out[field] = normalizeField(field, values[field]);
622
+ out[field] = normalizeField(field, values[field], relative);
596
623
  }
597
624
  return out;
598
625
  }
@@ -602,7 +629,7 @@ function normalizeValueSet(values) {
602
629
  * @param {unknown} value - candidate JSON text or object.
603
630
  * @returns {Record<string, string|number|boolean>} the sparse override map.
604
631
  */
605
- function normalizeDarkValues(value) {
632
+ function normalizeDarkValues(value, relative) {
606
633
  if (typeof value === "string") {
607
634
  if (value.trim() === "") return {};
608
635
  try {
@@ -611,7 +638,70 @@ function normalizeDarkValues(value) {
611
638
  return {};
612
639
  }
613
640
  }
614
- return normalizeValueSet(value);
641
+ return normalizeValueSet(value, relative);
642
+ }
643
+
644
+ /** The three axes that share the weight scale (and its marker). */
645
+ var WEIGHT_FIELDS = [WEIGHT_FIELD, WEIGHT_DIALOG_FIELD, CODE_WEIGHT_FIELD];
646
+
647
+ /**
648
+ * Convert the weight fields of saved preset snapshots to offsets.
649
+ *
650
+ * Runs exactly once per document, right before the card sets
651
+ * `WEIGHT_OFFSETS_FIELD`: a snapshot saved before 0.2.8 holds an absolute weight,
652
+ * and once the document is marked a stored weight is an offset, so applying that
653
+ * snapshot later would read `450` as +450 instead of +50. Converting the snapshots
654
+ * in the same breath as the document keeps the two on one scale.
655
+ *
656
+ * Only values inside the legacy absolute window are touched, so a snapshot that
657
+ * already holds offsets (any 0.3.x one) comes back unchanged and `changed` stays
658
+ * false — the caller then writes nothing.
659
+ *
660
+ * @param {unknown} presets - normalized presets, or anything shaped like them.
661
+ * @returns {{presets: object[], changed: boolean}} the converted list.
662
+ */
663
+ function migratePresetWeights(presets) {
664
+ if (!Array.isArray(presets)) return { presets: [], changed: false };
665
+ var changed = false;
666
+ var out = [];
667
+ for (var index = 0; index < presets.length; index += 1) {
668
+ var entry = presets[index];
669
+ if (entry === null || typeof entry !== "object" || entry.values === null) {
670
+ out.push(entry);
671
+ continue;
672
+ }
673
+ var values = entry.values;
674
+ if (typeof values !== "object") {
675
+ out.push(entry);
676
+ continue;
677
+ }
678
+ var converted = null;
679
+ for (var axis = 0; axis < WEIGHT_FIELDS.length; axis += 1) {
680
+ var field = WEIGHT_FIELDS[axis];
681
+ if (!Object.prototype.hasOwnProperty.call(values, field)) continue;
682
+ var stored = Number(values[field]);
683
+ if (!isFinite(stored) || stored < WEIGHT_MIN || stored > WEIGHT_MAX) continue;
684
+ if (converted === null) {
685
+ converted = {};
686
+ for (var key in values) {
687
+ if (Object.prototype.hasOwnProperty.call(values, key)) converted[key] = values[key];
688
+ }
689
+ }
690
+ converted[field] = stored - WEIGHT_BASE;
691
+ }
692
+ if (converted === null) {
693
+ out.push(entry);
694
+ continue;
695
+ }
696
+ changed = true;
697
+ var copy = {};
698
+ for (var name in entry) {
699
+ if (Object.prototype.hasOwnProperty.call(entry, name)) copy[name] = entry[name];
700
+ }
701
+ copy.values = converted;
702
+ out.push(copy);
703
+ }
704
+ return { presets: out, changed: changed };
615
705
  }
616
706
 
617
707
  /**
@@ -619,7 +709,7 @@ function normalizeDarkValues(value) {
619
709
  * @param {unknown} value - candidate JSON text or array.
620
710
  * @returns {{name: string, values: object, savedAt: number}[]}
621
711
  */
622
- function normalizePresets(value) {
712
+ function normalizePresets(value, relative) {
623
713
  var list = value;
624
714
  if (typeof value === "string") {
625
715
  if (value.trim() === "") return [];
@@ -639,7 +729,7 @@ function normalizePresets(value) {
639
729
  var stamp = Math.round(Number(entry.savedAt));
640
730
  out.push({
641
731
  name: name,
642
- values: normalizeValueSet(entry.values),
732
+ values: normalizeValueSet(entry.values, relative),
643
733
  savedAt: isFinite(stamp) && stamp > 0 ? stamp : 0,
644
734
  });
645
735
  }
@@ -1692,6 +1782,17 @@ function resolveAxes(config) {
1692
1782
  */
1693
1783
  var WEIGHT_STEP_CANDIDATES = [1, 5, 10, 25, 50, 100, 200, 300];
1694
1784
 
1785
+ /**
1786
+ * How many positions a slider must still offer before a coarser step is refused.
1787
+ *
1788
+ * Coarseness is the point (see `weightProfileFrom`), but a control with two
1789
+ * positions is not a slider. Four is the smallest count that still lets a reader
1790
+ * go one notch either side of the neutral position and one further up — and on
1791
+ * the measured seven-face family it lands on a 200-weight step, where one notch is
1792
+ * a +30% change in ink, against +16% at 100 (which is why 100 was not enough).
1793
+ */
1794
+ var WEIGHT_MIN_POSITIONS = 4;
1795
+
1695
1796
  /** The weight grid the profile is measured on. */
1696
1797
  var WEIGHT_PROBE_LOW = 100;
1697
1798
  var WEIGHT_PROBE_HIGH = 900;
@@ -1733,19 +1834,28 @@ function everyPositionDiffers(sample, bases, min, max, step) {
1733
1834
  /**
1734
1835
  * The slider a family deserves: how far it can go, and by how much.
1735
1836
  *
1736
- * A font with six faces can be made lighter or heavier only as far as those faces
1737
- * reach, and only in steps between them; a family with a continuous axis gets a
1738
- * fine step and a wide range. Measured on a weight grid:
1837
+ * A font can be made lighter or heavier only as far as its faces reach, and only
1838
+ * in jumps between them. Measured on a weight grid:
1739
1839
  * - the downward reach is the highest weight that still renders as light as the
1740
1840
  * family can (asking for less cannot change anything);
1741
- * - the upward reach is the LOWEST weight at or above the layer a notch has to
1742
- * move that already renders as heavy as the family can, so the range ends where
1743
- * bold text stops changing instead of running past it (see `WEIGHT_DELTA_UP_LIMIT`
1744
- * for why that ceiling is also the widest one allowed);
1745
- * - the step is the finest candidate whose positions all differ at every base a
1746
- * notch has to move, so no notch is a no-op for body text or bold text. A family whose faces are unevenly spaced
1747
- * (a 700 next to a 900) may end up with one repeated position whichever uniform
1748
- * step is used — the coarsest candidate is then the honest answer, never a crash.
1841
+ * - the upward reach is the LOWEST weight at or above body text that already
1842
+ * renders as heavy as the family can, so the range ends where the text a reader
1843
+ * is reading stops changing (see `WEIGHT_DELTA_UP_LIMIT` for the ceiling);
1844
+ * - the step is the COARSEST candidate that still leaves `WEIGHT_MIN_POSITIONS`
1845
+ * positions with body text changing at every one of them. Coarse is the point:
1846
+ * a finer step splits one face into several positions that all render the same,
1847
+ * so the control feels dead (measured on Noto Serif SC, whose faces are
1848
+ * 100-250 / 300 / 400 / 500 / 600 / 700 / 800-900: one 100-weight step is a
1849
+ * +16% change in ink, one 200-weight step +30%).
1850
+ *
1851
+ * Bold text and headings are deliberately NOT required to change at every
1852
+ * position: the heaviest ladder layer sits at 600 and reaches the ceiling at +300,
1853
+ * so no range can move it further. Body text is what a reader reads, so it carries
1854
+ * the requirement, and a heading already at its boldest face stays there.
1855
+ *
1856
+ * Every position is a whole number of steps from 0, so the card's step counts map
1857
+ * back onto exactly the offsets the document stores.
1858
+ *
1749
1859
  * @param {(weight: number) => string|number} sample - renders a probe at one
1750
1860
  * weight and returns a signature; equal signatures mean identical rendering.
1751
1861
  * @param {{candidates?: number[], floor?: number, ceil?: number, top?: number}}
@@ -1793,27 +1903,49 @@ function weightProfileFrom(sample, options) {
1793
1903
  }
1794
1904
  var reachUp = Math.min(ceil, WEIGHT_DELTA_UP_LIMIT, headroom);
1795
1905
  var reachDown = Math.max(floor, Math.min(0, lightest - WEIGHT_BASE));
1796
- var bases = top === WEIGHT_BASE ? [WEIGHT_BASE] : [WEIGHT_BASE, top];
1797
- for (var index = 0; index < candidates.length; index += 1) {
1906
+ var cap = Math.min(reachUp, WEIGHT_DELTA_UP_LIMIT);
1907
+ /**
1908
+ * The shape one candidate step describes.
1909
+ *
1910
+ * Every position is a whole number of steps from 0, so the card's step counts
1911
+ * multiply back into exactly the offsets the document stores. The reach is
1912
+ * snapped to that grid without ever passing the hard floor or the ceiling —
1913
+ * rounding the top UP (which the first version did) put a position past the
1914
+ * widest allowed offset, and rounding the bottom DOWN could pass the floor.
1915
+ */
1916
+ var shapeAt = function (step) {
1917
+ var down = Math.min(ceilTo(-reachDown, step), floorTo(-floor, step));
1918
+ return { min: -down, max: floorTo(cap, step), step: step };
1919
+ };
1920
+ var best = null;
1921
+ var bestCount = 0;
1922
+ // Coarsest candidate first: the coarsest step that still leaves enough visible
1923
+ // positions is the one a reader can actually work with.
1924
+ for (var index = candidates.length - 1; index >= 0; index -= 1) {
1798
1925
  var step = candidates[index];
1799
1926
  if (!(step > 0)) continue;
1800
- var min = floorTo(reachDown, step);
1801
- // Rounding up to the step must not push the range past the limit: the widest
1802
- // offset has to stay out of the absolute-weight window.
1803
- var max = Math.min(ceilTo(reachUp, step), WEIGHT_DELTA_UP_LIMIT);
1804
- // A step wider than the range would leave only one reachable position.
1805
- if (max > min && step > max - min) continue;
1806
- if (everyPositionDiffers(sample, bases, min, max, step)) {
1807
- return { min: min, max: max, step: step };
1927
+ var shape = shapeAt(step);
1928
+ if (shape.max < shape.min) continue;
1929
+ if (!everyPositionDiffers(sample, [WEIGHT_BASE], shape.min, shape.max, step)) continue;
1930
+ var count = (shape.max - shape.min) / step + 1;
1931
+ if (count >= WEIGHT_MIN_POSITIONS) return shape;
1932
+ if (count > bestCount) {
1933
+ best = shape;
1934
+ bestCount = count;
1808
1935
  }
1809
1936
  }
1937
+ if (best !== null) {
1938
+ // Fewer positions than asked for is still usable; one position is the whole
1939
+ // range, and any step describes it, so use the finest and keep the unit small.
1940
+ if (best.max === best.min) return { min: best.min, max: best.max, step: 1 };
1941
+ return best;
1942
+ }
1943
+ // No step kept body text alive: the coarsest one that still fits is the honest
1944
+ // answer, never a step the control cannot reach.
1810
1945
  var coarsest = candidates[candidates.length - 1];
1811
- var min = floorTo(reachDown, coarsest);
1812
- var max = Math.min(ceilTo(reachUp, coarsest), WEIGHT_DELTA_UP_LIMIT);
1813
- // Nothing kept every position alive: the coarsest step that still fits is the
1814
- // honest answer, never a step the control cannot reach.
1815
- var span = max - min;
1816
- return { min: min, max: max, step: span > 0 ? Math.min(coarsest, span) : 1 };
1946
+ var plain = shapeAt(coarsest);
1947
+ var span = plain.max - plain.min;
1948
+ return { min: plain.min, max: plain.max, step: span > 0 ? Math.min(coarsest, span) : 1 };
1817
1949
  }
1818
1950
 
1819
1951
  /**
@@ -1896,9 +2028,11 @@ function isDormant(config) {
1896
2028
  sanitize(axis[MONO_FIELD]) === "" &&
1897
2029
  clampOffset(axis[SIZE_DIALOG_FIELD]) === 0 &&
1898
2030
  clampOffset(axis[CODE_SIZE_FIELD]) === 0 &&
1899
- clampWeightDelta(axis[WEIGHT_FIELD]) === WEIGHT_UNSET &&
1900
- clampWeightDelta(axis[WEIGHT_DIALOG_FIELD]) === WEIGHT_UNSET &&
1901
- clampWeightDelta(axis[CODE_WEIGHT_FIELD]) === WEIGHT_UNSET &&
2031
+ // These arrive normalized, so they are offsets already: converting them again
2032
+ // would rescale an offset that legitimately sits inside `300…600`.
2033
+ clampWeightDelta(axis[WEIGHT_FIELD], true) === WEIGHT_UNSET &&
2034
+ clampWeightDelta(axis[WEIGHT_DIALOG_FIELD], true) === WEIGHT_UNSET &&
2035
+ clampWeightDelta(axis[CODE_WEIGHT_FIELD], true) === WEIGHT_UNSET &&
1902
2036
  clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
1903
2037
  clampLineOffset(axis[CODE_LINE_HEIGHT_FIELD]) === 0 &&
1904
2038
  clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
@@ -2778,6 +2912,9 @@ var shared = {
2778
2912
  WEIGHT_FIELD: WEIGHT_FIELD,
2779
2913
  WEIGHT_DIALOG_FIELD: WEIGHT_DIALOG_FIELD,
2780
2914
  CODE_WEIGHT_FIELD: CODE_WEIGHT_FIELD,
2915
+ WEIGHT_OFFSETS_FIELD: WEIGHT_OFFSETS_FIELD,
2916
+ WEIGHT_FIELDS: WEIGHT_FIELDS,
2917
+ migratePresetWeights: migratePresetWeights,
2781
2918
  LINE_HEIGHT_FIELD: LINE_HEIGHT_FIELD,
2782
2919
  LINE_HEIGHT_DIALOG_FIELD: LINE_HEIGHT_DIALOG_FIELD,
2783
2920
  CODE_LINE_HEIGHT_FIELD: CODE_LINE_HEIGHT_FIELD,
@@ -2872,6 +3009,7 @@ var shared = {
2872
3009
  WEIGHT_STEP_CANDIDATES: WEIGHT_STEP_CANDIDATES,
2873
3010
  WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
2874
3011
  WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
3012
+ WEIGHT_MIN_POSITIONS: WEIGHT_MIN_POSITIONS,
2875
3013
  WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
2876
3014
  WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
2877
3015
  WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fonttune",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",