dsh-fonttune 0.2.7 → 0.3.1
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 +22 -0
- package/CHANGELOG.md +22 -0
- package/README.en.md +11 -8
- package/README.md +11 -8
- package/lib/client.js +911 -132
- package/lib/index.js +6 -6
- package/lib/shared.cjs +551 -70
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -143,11 +143,105 @@ var UI_FOLLOWS_FIELD = "uiFollowsDialog";
|
|
|
143
143
|
var SIZE_MIN = -3;
|
|
144
144
|
var SIZE_MAX = 6;
|
|
145
145
|
|
|
146
|
-
/** Allowed
|
|
146
|
+
/** Allowed CODE weight range (an absolute value), and "do not touch". */
|
|
147
147
|
var WEIGHT_MIN = 300;
|
|
148
148
|
var WEIGHT_MAX = 600;
|
|
149
149
|
var WEIGHT_UNSET = 0;
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Allowed range of the two RELATIVE weight axes, in steps of 100-weight.
|
|
153
|
+
*
|
|
154
|
+
* The conversation and interface weight axes are offsets on each element's own
|
|
155
|
+
* weight, not replacements: a flat `font-weight` over the markdown subtree
|
|
156
|
+
* erased the heading hierarchy (measured on 0.1.7-rc.2, where every markdown
|
|
157
|
+
* weight lives inside its `font` shorthand — `._markdown_ h2{font:var(--dsw-font-markdown-h2)}`
|
|
158
|
+
* with `700` in it — and a long-handed `font-weight` with `!important` beat the
|
|
159
|
+
* shorthand). The delta window is the old absolute window shifted by −400, so
|
|
160
|
+
* `300…600` maps onto `−100…+200` one-to-one and `400` (normal) becomes 0, which
|
|
161
|
+
* is the value meaning "do not touch".
|
|
162
|
+
*/
|
|
163
|
+
var WEIGHT_DELTA_MIN = -100;
|
|
164
|
+
var WEIGHT_DELTA_MAX = 200;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The widest range a font-derived slider may use, either way.
|
|
168
|
+
*
|
|
169
|
+
* The nominal window above is what the axes can always express; a family with
|
|
170
|
+
* faces outside it (a black 900 cut, a hairline 100) earns a wider slider — see
|
|
171
|
+
* `weightProfileFrom` — and these are the hard bounds that keeps that in check.
|
|
172
|
+
*/
|
|
173
|
+
var WEIGHT_DELTA_FLOOR = -300;
|
|
174
|
+
var WEIGHT_DELTA_CEIL = 600;
|
|
175
|
+
|
|
176
|
+
/** The weight DSH gives an element that declares none of its own. */
|
|
177
|
+
var WEIGHT_BASE = 400;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The weight range CSS accepts at all.
|
|
181
|
+
*
|
|
182
|
+
* Named because it is easy to confuse with `WEIGHT_MIN…WEIGHT_MAX`, which is the
|
|
183
|
+
* window an ABSOLUTE weight from a pre-relative document occupies (300…600) and
|
|
184
|
+
* has nothing to do with what a font can render.
|
|
185
|
+
*/
|
|
186
|
+
var WEIGHT_CSS_MIN = 100;
|
|
187
|
+
var WEIGHT_CSS_MAX = 900;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The weight ladder of the conversation: each group carries the weight DSH's
|
|
191
|
+
* own stylesheet gives those elements, so an offset lands ON TOP of it.
|
|
192
|
+
*
|
|
193
|
+
* The bases are the measured ones (`--dsw-font-markdown-h*-font-weight` 700 /
|
|
194
|
+
* 600, `-base-strong-font-weight` 600, `-table-head-font-weight` 500, everything
|
|
195
|
+
* else 400).
|
|
196
|
+
*/
|
|
197
|
+
var DIALOG_WEIGHT_GROUPS = [
|
|
198
|
+
{ base: 700, elements: "h1,h2,h3" },
|
|
199
|
+
{ base: 600, elements: "h4,h5,h6" },
|
|
200
|
+
{ base: 600, elements: "strong,b" },
|
|
201
|
+
{ base: 500, elements: "th" },
|
|
202
|
+
{
|
|
203
|
+
base: 400,
|
|
204
|
+
elements: "p,li,td,dt,dd,blockquote,summary,em,i,a,label,small,div,span",
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The heaviest base in the conversation ladder — the layer with the least room.
|
|
210
|
+
*
|
|
211
|
+
* Derived from the ladder so the two cannot drift apart.
|
|
212
|
+
*/
|
|
213
|
+
var WEIGHT_LADDER_TOP = DIALOG_WEIGHT_GROUPS.reduce(function (top, group) {
|
|
214
|
+
return group.base > top ? group.base : top;
|
|
215
|
+
}, WEIGHT_BASE);
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* How far above its base an offset may reach at all.
|
|
219
|
+
*
|
|
220
|
+
* Every axis applies the offset to each element's OWN weight, and the heaviest
|
|
221
|
+
* layer of the ladder sits at `WEIGHT_LADDER_TOP`; past `WEIGHT_CSS_MAX` minus
|
|
222
|
+
* that, the heaviest layer cannot move any further, so a longer range would only
|
|
223
|
+
* add positions that change nothing for the boldest text on screen.
|
|
224
|
+
*
|
|
225
|
+
* Staying inside it has a second, load-bearing effect: offsets then never land in
|
|
226
|
+
* `WEIGHT_MIN…WEIGHT_MAX`, the range an ABSOLUTE weight from a pre-relative
|
|
227
|
+
* document occupies (see `clampWeightDelta`). The two meanings cannot be confused,
|
|
228
|
+
* so a stored offset is never rescaled into a different one.
|
|
229
|
+
*/
|
|
230
|
+
var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX - WEIGHT_LADDER_TOP;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The heaviest layer a notch still has to move: bold text.
|
|
234
|
+
*
|
|
235
|
+
* The heading layer above it is deliberately left out of that requirement. A heavy
|
|
236
|
+
* family spaces its top faces far apart (a 700 cut next to a 900), so headings
|
|
237
|
+
* reach their heaviest rendering part-way up the range and would otherwise hold the
|
|
238
|
+
* whole control down to one wide step. Every notch must move the text you read and
|
|
239
|
+
* the text you emphasise; a heading that is already at its boldest face stays there.
|
|
240
|
+
*/
|
|
241
|
+
var WEIGHT_LADDER_STRONG = DIALOG_WEIGHT_GROUPS.reduce(function (strong, group) {
|
|
242
|
+
return group.base < WEIGHT_LADDER_TOP && group.base > strong ? group.base : strong;
|
|
243
|
+
}, WEIGHT_BASE);
|
|
244
|
+
|
|
151
245
|
/** Allowed line-height ratio range, in percent (100 = untouched). */
|
|
152
246
|
var LINE_HEIGHT_MIN = 100;
|
|
153
247
|
var LINE_HEIGHT_MAX = 160;
|
|
@@ -363,7 +457,7 @@ function normalizeField(field, value) {
|
|
|
363
457
|
case WEIGHT_FIELD:
|
|
364
458
|
case WEIGHT_DIALOG_FIELD:
|
|
365
459
|
case CODE_WEIGHT_FIELD:
|
|
366
|
-
return
|
|
460
|
+
return clampWeightDelta(value);
|
|
367
461
|
case LINE_HEIGHT_FIELD:
|
|
368
462
|
case LINE_HEIGHT_DIALOG_FIELD:
|
|
369
463
|
return clampRatio(value);
|
|
@@ -435,6 +529,33 @@ function clampWeight(value) {
|
|
|
435
529
|
return Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
|
|
436
530
|
}
|
|
437
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Clamp one RELATIVE weight to its allowed range, migrating an absolute value.
|
|
534
|
+
*
|
|
535
|
+
* A document written before the axes became relative stores an absolute weight
|
|
536
|
+
* (`300…600`, `400` = normal). That window sits entirely outside the delta
|
|
537
|
+
* window, so the conversion is unambiguous and idempotent: a value inside
|
|
538
|
+
* `300…600` is old and becomes its distance from normal (`400 → 0`, `560 → +160`,
|
|
539
|
+
* `300 → −100`), while a value already inside `−100…200` is a delta and is left
|
|
540
|
+
* alone. The user's saved setup therefore keeps meaning "as bold as before"
|
|
541
|
+
* instead of jumping to the top of the new scale.
|
|
542
|
+
*
|
|
543
|
+
* @param {unknown} value - candidate offset, or a pre-0.2.8 absolute weight.
|
|
544
|
+
* @returns {number} the offset the stylesheet may use.
|
|
545
|
+
*/
|
|
546
|
+
function clampWeightDelta(value) {
|
|
547
|
+
var delta = Number(value);
|
|
548
|
+
if (!isFinite(delta)) return WEIGHT_UNSET;
|
|
549
|
+
delta = Math.round(delta);
|
|
550
|
+
if (delta >= WEIGHT_MIN && delta <= WEIGHT_MAX) delta -= WEIGHT_BASE;
|
|
551
|
+
// The slider's own range comes from the font (see `weightProfileFrom`) and can
|
|
552
|
+
// reach further than the nominal one, so the hard bounds are the widest range
|
|
553
|
+
// any font-derived slider may use.
|
|
554
|
+
if (delta > WEIGHT_DELTA_CEIL) return WEIGHT_DELTA_CEIL;
|
|
555
|
+
if (delta < WEIGHT_DELTA_FLOOR) return WEIGHT_DELTA_FLOOR;
|
|
556
|
+
return delta;
|
|
557
|
+
}
|
|
558
|
+
|
|
438
559
|
/**
|
|
439
560
|
* Clamp one line-height ratio to the allowed percent range.
|
|
440
561
|
* @param {unknown} value - candidate ratio in percent.
|
|
@@ -952,6 +1073,28 @@ var DELTA_PATTERN = /^--dsh-content-font-delta(?:-secondary)?$/;
|
|
|
952
1073
|
/** The whole official content-size chain (size / line height / delta). */
|
|
953
1074
|
var CONTENT_PATTERN = /^--dsh-content-font-(?:size|line-height|delta)(?:-secondary)?$/;
|
|
954
1075
|
|
|
1076
|
+
/**
|
|
1077
|
+
* The one property the conversation size offset is added to: the official
|
|
1078
|
+
* "对话字号" source, which the app writes INLINE on `body`.
|
|
1079
|
+
*
|
|
1080
|
+
* Every other markdown size is derived from it (through
|
|
1081
|
+
* `--dsh-content-font-delta` and the markdown shorthands), and custom properties
|
|
1082
|
+
* resolve where they are DECLARED — so the offset has to land here, once, or a
|
|
1083
|
+
* deeper override would leave the derived tokens at their body-computed values.
|
|
1084
|
+
*/
|
|
1085
|
+
var CONTENT_SIZE_SOURCE = "--dsh-content-font-size";
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Whether one declared value is a plain literal this plugin may wrap in
|
|
1089
|
+
* `calc(… + Npx)`. A `var()` reference is NOT: it is either derived from a
|
|
1090
|
+
* property this plugin already shifted, or it belongs to another axis.
|
|
1091
|
+
* @param {unknown} value - a declared custom-property value.
|
|
1092
|
+
* @returns {boolean}
|
|
1093
|
+
*/
|
|
1094
|
+
function isLiteralTokenValue(value) {
|
|
1095
|
+
return typeof value === "string" && value !== "" && value.indexOf("var(") === -1;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
955
1098
|
/**
|
|
956
1099
|
* The PART-token suffixes DSH appends for single-axis consumers.
|
|
957
1100
|
*/
|
|
@@ -1030,6 +1173,30 @@ function isMarkdownToken(name) {
|
|
|
1030
1173
|
return typeof name === "string" && name.indexOf("--dsw-font-markdown-") === 0 && !isCodeToken(name);
|
|
1031
1174
|
}
|
|
1032
1175
|
|
|
1176
|
+
/**
|
|
1177
|
+
* The one markdown family whose sizes are LITERALS instead of following the
|
|
1178
|
+
* official content-size chain: the `small` series (`12px/20px`, measured on
|
|
1179
|
+
* 0.1.7-rc.2).
|
|
1180
|
+
*
|
|
1181
|
+
* Every other markdown size derives from `--dsh-content-font-size` through
|
|
1182
|
+
* `--dsh-content-font-delta`, so shifting them as well applies the offset a
|
|
1183
|
+
* second time. The value cannot tell the families apart: the page also carries
|
|
1184
|
+
* `--dsw-font-markdown-h1` with its var() already substituted
|
|
1185
|
+
* (`700 calc(21px + -2px)/…`), which reads exactly like a literal and made
|
|
1186
|
+
* H1/H2 shrink while H3 stayed right (the asymmetric report that started this).
|
|
1187
|
+
* The NAME is the only reliable discriminator, so this is a whitelist.
|
|
1188
|
+
*/
|
|
1189
|
+
var MARKDOWN_LITERAL_PREFIX = "--dsw-font-markdown-small";
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Whether a token belongs to the literal (non-derived) markdown family.
|
|
1193
|
+
* @param {string} name - custom property name including the leading dashes.
|
|
1194
|
+
* @returns {boolean}
|
|
1195
|
+
*/
|
|
1196
|
+
function isMarkdownLiteralToken(name) {
|
|
1197
|
+
return typeof name === "string" && name.indexOf(MARKDOWN_LITERAL_PREFIX) === 0;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1033
1200
|
/**
|
|
1034
1201
|
* Whether one NAME looks like a `font` shorthand token (a bare
|
|
1035
1202
|
* `--dsw-font-…` name that is not a PART token or a family variable).
|
|
@@ -1267,8 +1434,7 @@ function codeSurfaceList() {
|
|
|
1267
1434
|
* not free: an ancestor-walking selector costs about 0.03 s per 50-step drag on
|
|
1268
1435
|
* 0.1.5-rc.3 (measured: stripping the ` *` forms from these two rules took the
|
|
1269
1436
|
* drag from 0.89 s to 0.52 s of style recalculation). The weight family keeps
|
|
1270
|
-
* the code subtree right through `--dfp-
|
|
1271
|
-
* `weightRule`.
|
|
1437
|
+
* the code subtree right through `--dfp-wdelta` instead — see `weightRule`.
|
|
1272
1438
|
*
|
|
1273
1439
|
* @param {{code?: string[], terminal?: string[]}|null} [hints] - unused; kept so
|
|
1274
1440
|
* the browser half can keep passing one harvest object to `buildFontCss`.
|
|
@@ -1360,16 +1526,19 @@ var WEIGHT_MARKDOWN_GUARD =
|
|
|
1360
1526
|
"))";
|
|
1361
1527
|
|
|
1362
1528
|
/**
|
|
1363
|
-
* The elements that carry a weight of their own in
|
|
1529
|
+
* The elements that carry a weight of their own in the INTERFACE (the chrome).
|
|
1364
1530
|
*
|
|
1365
1531
|
* The weight is the one property that cannot ride inheritance alone: an element
|
|
1366
1532
|
* whose own rule declares a weight ignores the value it would inherit, whatever
|
|
1367
|
-
* that value's importance. Measured on 0.1.5-rc.3, the
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1533
|
+
* that value's importance. Measured on 0.1.5-rc.3, the chrome's pinned elements
|
|
1534
|
+
* are `button` / `span` / `div`; the rest of the list is the inline markup that
|
|
1535
|
+
* could plausibly join them. This replaces the old `body,body *` reach, which
|
|
1536
|
+
* matched 1892 elements on every recalculation — the whole point of the table is
|
|
1537
|
+
* that it matches a fraction of that while still covering every pinned element.
|
|
1538
|
+
*
|
|
1539
|
+
* The CONVERSATION is not covered here: it has its own ladder with per-element
|
|
1540
|
+
* bases (`DIALOG_WEIGHT_GROUPS`), because one flat weight cannot preserve a
|
|
1541
|
+
* heading hierarchy.
|
|
1373
1542
|
*/
|
|
1374
1543
|
var WEIGHT_ELEMENT_TABLE = [
|
|
1375
1544
|
"button",
|
|
@@ -1521,6 +1690,196 @@ function resolveAxes(config) {
|
|
|
1521
1690
|
* the stylesheet builder
|
|
1522
1691
|
* ------------------------------------------------------------------ */
|
|
1523
1692
|
|
|
1693
|
+
/**
|
|
1694
|
+
* The steps a weight slider may use, finest first.
|
|
1695
|
+
*
|
|
1696
|
+
* A family ships a fixed set of faces and the browser rounds any requested
|
|
1697
|
+
* weight to the nearest one, so a slider that runs in single units spends long
|
|
1698
|
+
* stretches on one face. The control is therefore built from what the family can
|
|
1699
|
+
* actually render: its range runs from the lightest face it has to the heaviest,
|
|
1700
|
+
* and its notch is the family's own granularity.
|
|
1701
|
+
*/
|
|
1702
|
+
var WEIGHT_STEP_CANDIDATES = [1, 5, 10, 25, 50, 100, 200, 300];
|
|
1703
|
+
|
|
1704
|
+
/** The weight grid the profile is measured on. */
|
|
1705
|
+
var WEIGHT_PROBE_LOW = 100;
|
|
1706
|
+
var WEIGHT_PROBE_HIGH = 900;
|
|
1707
|
+
var WEIGHT_PROBE_STEP = 10;
|
|
1708
|
+
|
|
1709
|
+
/** Round down/up to a multiple of `step`. */
|
|
1710
|
+
function floorTo(value, step) {
|
|
1711
|
+
return Math.floor(value / step) * step;
|
|
1712
|
+
}
|
|
1713
|
+
function ceilTo(value, step) {
|
|
1714
|
+
return Math.ceil(value / step) * step;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Does every position of `[min..max]` in `step` increments render differently?
|
|
1719
|
+
*
|
|
1720
|
+
* Checked at every base the offset is added to: a step that is fine for body text
|
|
1721
|
+
* can be a no-op one layer up, where the faces are further apart.
|
|
1722
|
+
* @param {(weight: number) => string|number} sample - probe.
|
|
1723
|
+
* @param {number[]} bases - the weights the offsets are added to.
|
|
1724
|
+
* @param {number} min - first offset.
|
|
1725
|
+
* @param {number} max - last offset.
|
|
1726
|
+
* @param {number} step - offset increment.
|
|
1727
|
+
* @returns {boolean} true when no two neighbours render the same at any base.
|
|
1728
|
+
*/
|
|
1729
|
+
function everyPositionDiffers(sample, bases, min, max, step) {
|
|
1730
|
+
for (var index = 0; index < bases.length; index += 1) {
|
|
1731
|
+
var base = bases[index];
|
|
1732
|
+
var previous = null;
|
|
1733
|
+
for (var position = min; position <= max; position += step) {
|
|
1734
|
+
var signature = sample(base + position);
|
|
1735
|
+
if (previous !== null && signature === previous) return false;
|
|
1736
|
+
previous = signature;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return true;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* The slider a family deserves: how far it can go, and by how much.
|
|
1744
|
+
*
|
|
1745
|
+
* A font with six faces can be made lighter or heavier only as far as those faces
|
|
1746
|
+
* reach, and only in steps between them; a family with a continuous axis gets a
|
|
1747
|
+
* fine step and a wide range. Measured on a weight grid:
|
|
1748
|
+
* - the downward reach is the highest weight that still renders as light as the
|
|
1749
|
+
* family can (asking for less cannot change anything);
|
|
1750
|
+
* - the upward reach is the LOWEST weight at or above the layer a notch has to
|
|
1751
|
+
* move that already renders as heavy as the family can, so the range ends where
|
|
1752
|
+
* bold text stops changing instead of running past it (see `WEIGHT_DELTA_UP_LIMIT`
|
|
1753
|
+
* for why that ceiling is also the widest one allowed);
|
|
1754
|
+
* - the step is the finest candidate whose positions all differ at every base a
|
|
1755
|
+
* notch has to move, so no notch is a no-op for body text or bold text. A family whose faces are unevenly spaced
|
|
1756
|
+
* (a 700 next to a 900) may end up with one repeated position whichever uniform
|
|
1757
|
+
* step is used — the coarsest candidate is then the honest answer, never a crash.
|
|
1758
|
+
* @param {(weight: number) => string|number} sample - renders a probe at one
|
|
1759
|
+
* weight and returns a signature; equal signatures mean identical rendering.
|
|
1760
|
+
* @param {{candidates?: number[], floor?: number, ceil?: number, top?: number}}
|
|
1761
|
+
* [options] overrides for the candidates, the widest allowed range and the
|
|
1762
|
+
* heaviest base the offset is added to (the ladder's, or the axis's own).
|
|
1763
|
+
* @returns {{min: number, max: number, step: number}} the slider's shape; the
|
|
1764
|
+
* range always contains 0, which is the "leave DSH alone" position.
|
|
1765
|
+
*/
|
|
1766
|
+
function weightProfileFrom(sample, options) {
|
|
1767
|
+
var settings = options || {};
|
|
1768
|
+
var candidates =
|
|
1769
|
+
Array.isArray(settings.candidates) && settings.candidates.length > 0
|
|
1770
|
+
? settings.candidates
|
|
1771
|
+
: WEIGHT_STEP_CANDIDATES;
|
|
1772
|
+
var floor = typeof settings.floor === "number" ? settings.floor : WEIGHT_DELTA_FLOOR;
|
|
1773
|
+
var ceil = typeof settings.ceil === "number" ? settings.ceil : WEIGHT_DELTA_CEIL;
|
|
1774
|
+
var top = typeof settings.top === "number" ? settings.top : WEIGHT_LADDER_STRONG;
|
|
1775
|
+
var topSignature = sample(WEIGHT_PROBE_HIGH);
|
|
1776
|
+
var bottomSignature = sample(WEIGHT_PROBE_LOW);
|
|
1777
|
+
var heaviest = WEIGHT_PROBE_HIGH;
|
|
1778
|
+
for (var weight = WEIGHT_PROBE_LOW; weight <= WEIGHT_PROBE_HIGH; weight += WEIGHT_PROBE_STEP) {
|
|
1779
|
+
if (sample(weight) === topSignature) {
|
|
1780
|
+
heaviest = weight;
|
|
1781
|
+
break;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
var lightest = WEIGHT_PROBE_LOW;
|
|
1785
|
+
for (var back = WEIGHT_PROBE_HIGH; back >= WEIGHT_PROBE_LOW; back -= WEIGHT_PROBE_STEP) {
|
|
1786
|
+
if (sample(back) === bottomSignature) {
|
|
1787
|
+
lightest = back;
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
// How far the offset can go before NO layer that must move can move any more:
|
|
1792
|
+
// the widest horizon among those bases, since a layer that could still change
|
|
1793
|
+
// must not be cut off by one that already sits at its heaviest face.
|
|
1794
|
+
var headroom = Math.max(0, heaviest - WEIGHT_BASE);
|
|
1795
|
+
if (top !== WEIGHT_BASE) {
|
|
1796
|
+
for (var up = top; up <= WEIGHT_PROBE_HIGH; up += WEIGHT_PROBE_STEP) {
|
|
1797
|
+
if (sample(up) === topSignature) {
|
|
1798
|
+
headroom = Math.max(headroom, up - top);
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
var reachUp = Math.min(ceil, WEIGHT_DELTA_UP_LIMIT, headroom);
|
|
1804
|
+
var reachDown = Math.max(floor, Math.min(0, lightest - WEIGHT_BASE));
|
|
1805
|
+
var bases = top === WEIGHT_BASE ? [WEIGHT_BASE] : [WEIGHT_BASE, top];
|
|
1806
|
+
for (var index = 0; index < candidates.length; index += 1) {
|
|
1807
|
+
var step = candidates[index];
|
|
1808
|
+
if (!(step > 0)) continue;
|
|
1809
|
+
var min = floorTo(reachDown, step);
|
|
1810
|
+
// Rounding up to the step must not push the range past the limit: the widest
|
|
1811
|
+
// offset has to stay out of the absolute-weight window.
|
|
1812
|
+
var max = Math.min(ceilTo(reachUp, step), WEIGHT_DELTA_UP_LIMIT);
|
|
1813
|
+
// A step wider than the range would leave only one reachable position.
|
|
1814
|
+
if (max > min && step > max - min) continue;
|
|
1815
|
+
if (everyPositionDiffers(sample, bases, min, max, step)) {
|
|
1816
|
+
return { min: min, max: max, step: step };
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
var coarsest = candidates[candidates.length - 1];
|
|
1820
|
+
var min = floorTo(reachDown, coarsest);
|
|
1821
|
+
var max = Math.min(ceilTo(reachUp, coarsest), WEIGHT_DELTA_UP_LIMIT);
|
|
1822
|
+
// Nothing kept every position alive: the coarsest step that still fits is the
|
|
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 };
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Turn a measured profile into the slider the card shows.
|
|
1830
|
+
*
|
|
1831
|
+
* The document keeps storing a weight offset (so nothing written before has to be
|
|
1832
|
+
* migrated), but the CONTROL counts the family's own steps: a six-face family
|
|
1833
|
+
* offers "-2 … +4" instead of "-200 … +400", which is what a reader can actually
|
|
1834
|
+
* see. `unit` is how many weight units one step is worth.
|
|
1835
|
+
* @param {{min: number, max: number, step: number}} profile - measured shape.
|
|
1836
|
+
* @param {number} value - the stored offset, in weight units.
|
|
1837
|
+
* @returns {{min: number, max: number, unit: number, value: number}} the slider
|
|
1838
|
+
* in step counts; a stored value outside the measured range widens it, so it is
|
|
1839
|
+
* never unreachable.
|
|
1840
|
+
*/
|
|
1841
|
+
function weightStepRange(profile, value) {
|
|
1842
|
+
var unit = profile && profile.step > 0 ? profile.step : 1;
|
|
1843
|
+
var count = Math.round(Number(value) / unit);
|
|
1844
|
+
if (!isFinite(count)) count = 0;
|
|
1845
|
+
var min = Math.floor(profile.min / unit);
|
|
1846
|
+
var max = Math.ceil(profile.max / unit);
|
|
1847
|
+
if (count < min) min = count;
|
|
1848
|
+
if (count > max) max = count;
|
|
1849
|
+
return { min: min, max: max, unit: unit, value: count };
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* What a slider's local (unconfirmed) value should do when a value arrives from
|
|
1854
|
+
* the settings document.
|
|
1855
|
+
*
|
|
1856
|
+
* A slider keeps the value the user released until the document echoes it back;
|
|
1857
|
+
* dropping it immediately would show the old number for a frame (the "bounce
|
|
1858
|
+
* back, then settle" flicker). Two cases must NOT drop it:
|
|
1859
|
+
* - a confirmation for the PREVIOUS commit arriving after the user already
|
|
1860
|
+
* started a new drag — the document write is debounced and can land seconds
|
|
1861
|
+
* later (measured on 0.1.7-rc.2: 0.7–5 s), and clearing there snapped the
|
|
1862
|
+
* thumb back to the old value mid-drag;
|
|
1863
|
+
* - any unrelated outside value (another page, a reset) arriving mid-drag.
|
|
1864
|
+
*
|
|
1865
|
+
* @param {{pending: number|null, awaiting: number|null, confirmed: number}} state
|
|
1866
|
+
* the local value, the value a commit is waiting for (null when idle), and the
|
|
1867
|
+
* value the document currently reports.
|
|
1868
|
+
* @returns {{pending: number|null, awaiting: number|null}} the state to keep.
|
|
1869
|
+
*/
|
|
1870
|
+
function reconcileSliderValue(state) {
|
|
1871
|
+
var pending = state.pending;
|
|
1872
|
+
var awaiting = state.awaiting;
|
|
1873
|
+
var confirmed = state.confirmed;
|
|
1874
|
+
if (awaiting !== null && confirmed === awaiting) {
|
|
1875
|
+
awaiting = null;
|
|
1876
|
+
if (pending === confirmed) pending = null;
|
|
1877
|
+
return { pending: pending, awaiting: awaiting };
|
|
1878
|
+
}
|
|
1879
|
+
if (awaiting === null && pending !== null && pending === confirmed) pending = null;
|
|
1880
|
+
return { pending: pending, awaiting: awaiting };
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1524
1883
|
/**
|
|
1525
1884
|
* Whether a normalized configuration asks for any change at all.
|
|
1526
1885
|
* @param {unknown} config - candidate configuration.
|
|
@@ -1546,9 +1905,9 @@ function isDormant(config) {
|
|
|
1546
1905
|
sanitize(axis[MONO_FIELD]) === "" &&
|
|
1547
1906
|
clampOffset(axis[SIZE_DIALOG_FIELD]) === 0 &&
|
|
1548
1907
|
clampOffset(axis[CODE_SIZE_FIELD]) === 0 &&
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1908
|
+
clampWeightDelta(axis[WEIGHT_FIELD]) === WEIGHT_UNSET &&
|
|
1909
|
+
clampWeightDelta(axis[WEIGHT_DIALOG_FIELD]) === WEIGHT_UNSET &&
|
|
1910
|
+
clampWeightDelta(axis[CODE_WEIGHT_FIELD]) === WEIGHT_UNSET &&
|
|
1552
1911
|
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
1553
1912
|
clampLineOffset(axis[CODE_LINE_HEIGHT_FIELD]) === 0 &&
|
|
1554
1913
|
clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
|
|
@@ -1581,8 +1940,9 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
1581
1940
|
var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
|
|
1582
1941
|
// Normalised once: an expanded axis always carries a number, but a caller
|
|
1583
1942
|
// handing in a raw object may leave the field out, and "absent" has to mean
|
|
1584
|
-
// "unset" rather than "write the string undefined".
|
|
1585
|
-
|
|
1943
|
+
// "unset" rather than "write the string undefined". The conversation's value
|
|
1944
|
+
// is an OFFSET (see `clampWeightDelta`), not a weight.
|
|
1945
|
+
var dialogDelta = typeof axis[WEIGHT_DIALOG_FIELD] === "number" ? axis[WEIGHT_DIALOG_FIELD] : WEIGHT_UNSET;
|
|
1586
1946
|
var codeSurfaces = codeSelector(hints);
|
|
1587
1947
|
var dialogSurfaces = dialogSelector(hints);
|
|
1588
1948
|
var declarations = [];
|
|
@@ -1619,24 +1979,86 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
1619
1979
|
* plus an element table for the tags DSH pins a weight on of their own (see
|
|
1620
1980
|
* `WEIGHT_ELEMENT_TABLE`).
|
|
1621
1981
|
*
|
|
1622
|
-
* The
|
|
1982
|
+
* The offset rides a custom property (`--dfp-wdelta`, declared on
|
|
1623
1983
|
* `:root,body` by the caller) rather than a literal. That is what keeps the
|
|
1624
1984
|
* code subtree right without a descendant selector: a highlight `<span>`
|
|
1625
1985
|
* inside a `<pre>` IS matched by this rule's table, but it resolves the
|
|
1626
|
-
* variable its code ancestor set (see the code weight
|
|
1627
|
-
* code
|
|
1986
|
+
* variable its code ancestor set (see the code weight rules), so it takes the
|
|
1987
|
+
* code offset instead of the interface one. Naming `pre *` and friends here
|
|
1628
1988
|
* or in the code rules instead cost 0.03 s per selector per drag.
|
|
1629
1989
|
*
|
|
1630
|
-
* The markdown guard only rides along when the conversation has NO
|
|
1631
|
-
* its own — with one set, the class-scoped
|
|
1632
|
-
* inside markdown anyway.
|
|
1990
|
+
* The markdown guard only rides along when the conversation has NO offset of
|
|
1991
|
+
* its own — with one set, the class-scoped conversation ladder out-ranks this
|
|
1992
|
+
* rule inside markdown anyway.
|
|
1633
1993
|
*/
|
|
1634
1994
|
function weightRule(declarations) {
|
|
1635
1995
|
var base = isDark ? DARK_ATTR : "body";
|
|
1636
1996
|
var selector = base + "," + base + " :where(" + WEIGHT_ELEMENT_TABLE + ")";
|
|
1637
|
-
if (
|
|
1997
|
+
if (dialogDelta === WEIGHT_UNSET) selector += WEIGHT_MARKDOWN_GUARD;
|
|
1638
1998
|
return selector + "{" + declarations + "}";
|
|
1639
1999
|
}
|
|
2000
|
+
/**
|
|
2001
|
+
* The weight one element group resolves to: its own base plus the offset that
|
|
2002
|
+
* is in force where the element sits, clamped to the legal CSS range so an
|
|
2003
|
+
* extreme offset cannot leave it (a weight below 100 is invalid, above 900 is
|
|
2004
|
+
* ignored by the font).
|
|
2005
|
+
* @param {number} base - the weight DSH gives this group.
|
|
2006
|
+
* @returns {string} the `font-weight` value.
|
|
2007
|
+
*/
|
|
2008
|
+
function ladderWeight(base) {
|
|
2009
|
+
return (
|
|
2010
|
+
"clamp(" + WEIGHT_CSS_MIN + ",calc(" + base + " + var(--dfp-wdelta,0))," + WEIGHT_CSS_MAX + ")"
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* The scope the conversation WEIGHT rules hang on: the cheap markdown wrapper
|
|
2015
|
+
* (the harvested hint when the page has one, the substring form otherwise).
|
|
2016
|
+
*
|
|
2017
|
+
* Not the family rule's `dialogSurfaces` list: that one has to reach every
|
|
2018
|
+
* descendant because it overrides an inherited family, while the ladder names
|
|
2019
|
+
* its own elements — and reusing that list would multiply its ancestor-walking
|
|
2020
|
+
* `… *` selectors by five groups, which is exactly the cost the element table
|
|
2021
|
+
* was introduced to remove. Code surfaces need no exclusion here either: the
|
|
2022
|
+
* code rules pin `--dfp-wdelta` on themselves, so an element inside one adds
|
|
2023
|
+
* the offset in force THERE (the code's), which is the value it should get.
|
|
2024
|
+
* @returns {string} one selector.
|
|
2025
|
+
*/
|
|
2026
|
+
function weightScopeSelector() {
|
|
2027
|
+
return hints !== null && typeof hints === "object" && typeof hints.markdown === "string"
|
|
2028
|
+
? hints.markdown
|
|
2029
|
+
: '[class*="_markdown_" i]';
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* A rule for the conversation scope itself, so text sitting directly in the
|
|
2033
|
+
* container moves with the axis (and so `--dfp-wdelta` is pinned for the
|
|
2034
|
+
* ladder below before any child resolves it).
|
|
2035
|
+
* @param {string} declarations - the declarations to write.
|
|
2036
|
+
* @returns {string} the rule.
|
|
2037
|
+
*/
|
|
2038
|
+
function weightScopeRule(declarations) {
|
|
2039
|
+
var scope = weightScopeSelector();
|
|
2040
|
+
return (isDark ? DARK_ATTR + " " : "") + scope + "{" + declarations + "}";
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* The conversation ladder: one rule per group in `DIALOG_WEIGHT_GROUPS`, each
|
|
2044
|
+
* scoped under the conversation.
|
|
2045
|
+
* @param {string} surfaceList - a comma-separated selector list.
|
|
2046
|
+
* @returns {string[]} one rule per group.
|
|
2047
|
+
*/
|
|
2048
|
+
function weightLadderRules(surfaceList) {
|
|
2049
|
+
var rules = [];
|
|
2050
|
+
var surfaces = splitSelectorList(surfaceList);
|
|
2051
|
+
for (var groupIndex = 0; groupIndex < DIALOG_WEIGHT_GROUPS.length; groupIndex += 1) {
|
|
2052
|
+
var group = DIALOG_WEIGHT_GROUPS[groupIndex];
|
|
2053
|
+
var scoped = [];
|
|
2054
|
+
for (var surfaceIndex = 0; surfaceIndex < surfaces.length; surfaceIndex += 1) {
|
|
2055
|
+
var one = surfaces[surfaceIndex] + " :is(" + group.elements + ")";
|
|
2056
|
+
scoped.push(isDark ? DARK_ATTR + " " + one : one);
|
|
2057
|
+
}
|
|
2058
|
+
rules.push(scoped.join(",") + "{font-weight:" + ladderWeight(group.base) + " !important}");
|
|
2059
|
+
}
|
|
2060
|
+
return rules;
|
|
2061
|
+
}
|
|
1640
2062
|
/**
|
|
1641
2063
|
* Re-assert the code family inside the dialog: scoped under the markdown
|
|
1642
2064
|
* container, after the dialog rule and above it in specificity, so a
|
|
@@ -1694,39 +2116,35 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
1694
2116
|
// page-wide `font-family` on body plus the theme variable) and the weight
|
|
1695
2117
|
// (below).
|
|
1696
2118
|
|
|
1697
|
-
// ---- interface weight ----
|
|
2119
|
+
// ---- interface weight (a relative offset) ----
|
|
1698
2120
|
// A `body{font-weight}` rule only reaches the elements that INHERIT their
|
|
1699
2121
|
// weight; the chrome's `button` / `span` / `div` class rules pin their own
|
|
1700
2122
|
// (measured on 0.1.5-rc.3), which is why the second reach exists. The
|
|
1701
2123
|
// conversation is held apart by `weightRule`'s guard (or by the class-scoped
|
|
1702
|
-
//
|
|
1703
|
-
|
|
2124
|
+
// conversation ladder when the conversation has an offset of its own).
|
|
2125
|
+
//
|
|
2126
|
+
// The value is an OFFSET on the element's own weight, never a replacement, so
|
|
2127
|
+
// raising the interface weight cannot flatten anything that shipped heavier
|
|
2128
|
+
// than the base. The offset rides `--dfp-wdelta` so the code rules can pin it
|
|
2129
|
+
// back to zero (or to the code offset) on their surfaces without any
|
|
2130
|
+
// descendant selector.
|
|
2131
|
+
var interfaceDelta = typeof axis[WEIGHT_FIELD] === "number" ? axis[WEIGHT_FIELD] : WEIGHT_UNSET;
|
|
1704
2132
|
var codeWeight = typeof axis[CODE_WEIGHT_FIELD] === "number" ? axis[CODE_WEIGHT_FIELD] : WEIGHT_UNSET;
|
|
1705
|
-
if (
|
|
1706
|
-
// The variable the weight rule resolves; the code rules below override it
|
|
1707
|
-
// on their surfaces so everything inside them inherits the code weight.
|
|
1708
|
-
declarations.push(
|
|
1709
|
-
(isDark ? DARK_ATTR : ":root,body") +
|
|
1710
|
-
"{--dfp-interface-weight:" +
|
|
1711
|
-
interfaceWeight +
|
|
1712
|
-
"}"
|
|
1713
|
-
);
|
|
2133
|
+
if (interfaceDelta !== WEIGHT_UNSET) {
|
|
1714
2134
|
declarations.push(
|
|
1715
|
-
|
|
1716
|
-
"font-weight:var(--dfp-interface-weight," + interfaceWeight + ") !important"
|
|
1717
|
-
)
|
|
2135
|
+
(isDark ? DARK_ATTR : ":root,body") + "{--dfp-wdelta:" + interfaceDelta + "}"
|
|
1718
2136
|
);
|
|
2137
|
+
declarations.push(weightRule("font-weight:" + ladderWeight(WEIGHT_BASE) + " !important"));
|
|
1719
2138
|
if (codeWeight === WEIGHT_UNSET) {
|
|
1720
2139
|
// The code rules come after this one and win on their own (class hooks by
|
|
1721
2140
|
// specificity, element selectors by order), so code surfaces — which
|
|
1722
2141
|
// inherit nothing but start from the body weight — are reset here.
|
|
1723
2142
|
// `normal` is what DSH computes for them (the code `font` shorthands
|
|
1724
2143
|
// carry no weight), so an unset code weight keeps code exactly as DSH
|
|
1725
|
-
// shipped it. The
|
|
1726
|
-
// surface (which the interface table matches) lands on `normal`
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
);
|
|
2144
|
+
// shipped it. The offset is pinned to zero alongside so an element INSIDE
|
|
2145
|
+
// a code surface (which the interface table matches) lands on `normal`
|
|
2146
|
+
// too.
|
|
2147
|
+
declarations.push(codeRule("font-weight:normal !important;--dfp-wdelta:0"));
|
|
1730
2148
|
}
|
|
1731
2149
|
}
|
|
1732
2150
|
|
|
@@ -1739,17 +2157,55 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
1739
2157
|
}
|
|
1740
2158
|
}
|
|
1741
2159
|
|
|
1742
|
-
// ---- dialog weight (
|
|
1743
|
-
|
|
1744
|
-
|
|
2160
|
+
// ---- dialog weight (a relative offset, applied element by element) ----
|
|
2161
|
+
// The conversation cannot take one flat weight: DSH gives h1..h3 700,
|
|
2162
|
+
// h4..h6/strong 600, th 500 and the rest 400, all inside `font` shorthands, so
|
|
2163
|
+
// a single `font-weight: <value> !important` on the markdown scope replaced
|
|
2164
|
+
// every one of them with that value — setting the axis at all flattened the
|
|
2165
|
+
// headings (measured on 0.1.7-rc.2: `##` came out at the body weight). The
|
|
2166
|
+
// ladder below re-states each group's own base PLUS the offset, so the
|
|
2167
|
+
// hierarchy survives any offset, in both directions, and `d = 0` injects
|
|
2168
|
+
// nothing at all.
|
|
2169
|
+
if (dialogDelta === WEIGHT_UNSET && interfaceDelta !== WEIGHT_UNSET) {
|
|
2170
|
+
// The conversation has NO offset of its own — which is exactly why it has to
|
|
2171
|
+
// say so. The interface axis declares `--dfp-wdelta` on `:root,body`, so an
|
|
2172
|
+
// unset conversation INHERITED it: with the interface at +80 the card showed 0
|
|
2173
|
+
// for the conversation while the page rendered +80, and the first notch above
|
|
2174
|
+
// it only added 20 units — a step nobody can see (measured on 0.1.7-rc.2:
|
|
2175
|
+
// `delta` read 80 at the "0" position and 100 at "+1"). Pinning zero keeps
|
|
2176
|
+
// "0 = leave DSH alone" true no matter what the other axes are set to.
|
|
2177
|
+
declarations.push(weightScopeRule("--dfp-wdelta:0"));
|
|
2178
|
+
}
|
|
2179
|
+
if (dialogDelta !== WEIGHT_UNSET) {
|
|
2180
|
+
// Every ladder rule reads `--dfp-wdelta`, so the conversation pins it to its
|
|
2181
|
+
// own offset: the interface's value must not leak in, and an element inside
|
|
2182
|
+
// a code surface inside the conversation still resolves the code offset that
|
|
2183
|
+
// the code rules pin on their own surfaces. The scope itself takes the base
|
|
2184
|
+
// group's weight, so text sitting directly in the container moves with the
|
|
2185
|
+
// axis exactly as it did when the axis was absolute.
|
|
2186
|
+
declarations.push(
|
|
2187
|
+
weightScopeRule(
|
|
2188
|
+
"--dfp-wdelta:" + dialogDelta + ";font-weight:" + ladderWeight(WEIGHT_BASE) + " !important"
|
|
2189
|
+
)
|
|
2190
|
+
);
|
|
2191
|
+
var dialogLadder = weightLadderRules(weightScopeSelector());
|
|
2192
|
+
for (var ladderIndex = 0; ladderIndex < dialogLadder.length; ladderIndex += 1) {
|
|
2193
|
+
declarations.push(dialogLadder[ladderIndex]);
|
|
2194
|
+
}
|
|
1745
2195
|
// The card's conversation preview simulates this surface, so it takes the
|
|
1746
|
-
// same
|
|
2196
|
+
// same ladder (and is excluded from the interface rule above).
|
|
1747
2197
|
declarations.push(
|
|
1748
2198
|
(isDark ? DARK_ATTR + " " : "") +
|
|
1749
|
-
".dfp-previewDialog{
|
|
1750
|
-
|
|
2199
|
+
".dfp-previewDialog{--dfp-wdelta:" +
|
|
2200
|
+
dialogDelta +
|
|
2201
|
+
";font-weight:" +
|
|
2202
|
+
ladderWeight(WEIGHT_BASE) +
|
|
1751
2203
|
" !important}"
|
|
1752
2204
|
);
|
|
2205
|
+
var previewLadder = weightLadderRules(".dfp-previewDialog");
|
|
2206
|
+
for (var previewIndex = 0; previewIndex < previewLadder.length; previewIndex += 1) {
|
|
2207
|
+
declarations.push(previewLadder[previewIndex]);
|
|
2208
|
+
}
|
|
1753
2209
|
}
|
|
1754
2210
|
|
|
1755
2211
|
// ---- dialog size offset (additive, on the official content-size source) ----
|
|
@@ -1766,32 +2222,33 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
1766
2222
|
var dialogOffsetValue = axis[SIZE_DIALOG_FIELD];
|
|
1767
2223
|
if (dialogOffsetValue !== 0) {
|
|
1768
2224
|
var contentSources = tokenNames(baseTokens).filter(function (name) {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
return
|
|
1774
|
-
|
|
1775
|
-
base !== "" &&
|
|
1776
|
-
base.indexOf("var(") === -1 &&
|
|
1777
|
-
base.indexOf("calc(") === -1
|
|
1778
|
-
);
|
|
2225
|
+
// The source property, by NAME: `--dsh-content-font-size`. The derived
|
|
2226
|
+
// members of the chain (`-delta`, `-size-secondary`, the markdown
|
|
2227
|
+
// shorthands) all read it, so shifting them as well would count the offset
|
|
2228
|
+
// twice.
|
|
2229
|
+
if (name !== CONTENT_SIZE_SOURCE) return false;
|
|
2230
|
+
return isLiteralTokenValue(baseTokens[name]);
|
|
1779
2231
|
});
|
|
1780
2232
|
var contentShift = shiftTokens(contentSources, baseTokens, dialogOffsetValue);
|
|
1781
2233
|
if (contentShift.length > 0) declarations.push(rootRule(contentShift.join(";")));
|
|
1782
|
-
// The `small` series is the only markdown family with literal sizes: it
|
|
1783
|
-
//
|
|
2234
|
+
// The `small` series is the only markdown family with literal sizes: it does
|
|
2235
|
+
// not derive from the chain, so its parts and shorthands shift here — and
|
|
2236
|
+
// ONLY it, by name. A value test cannot stand in for the name: the page also
|
|
2237
|
+
// carries `--dsw-font-markdown-h1` with its var() already substituted, which
|
|
2238
|
+
// looks like a literal and used to take the offset a second time (H1/H2
|
|
2239
|
+
// shrank, H3 did not).
|
|
1784
2240
|
var literals = markdownTokenNames(baseTokens).filter(function (name) {
|
|
1785
|
-
|
|
1786
|
-
|
|
2241
|
+
return (
|
|
2242
|
+
isMarkdownLiteralToken(name) &&
|
|
2243
|
+
name.indexOf("-font-size") >= 0 &&
|
|
2244
|
+
isLiteralTokenValue(baseTokens[name])
|
|
2245
|
+
);
|
|
1787
2246
|
});
|
|
1788
2247
|
var literalShift = shiftTokens(literals, baseTokens, dialogOffsetValue);
|
|
1789
2248
|
if (literalShift.length > 0) declarations.push(rootRule(literalShift.join(";")));
|
|
1790
|
-
// The shorthands whose size is a literal (the small series) must ride the
|
|
1791
|
-
// shift too; the delta-derived shorthands flow through the chain above.
|
|
1792
2249
|
var sizeShifted = [];
|
|
1793
2250
|
var markdownEntries = shorthandTokens(baseTokens).filter(function (entry) {
|
|
1794
|
-
return
|
|
2251
|
+
return isMarkdownLiteralToken(entry.name) && isLiteralTokenValue(entry.parts.size);
|
|
1795
2252
|
});
|
|
1796
2253
|
for (var mIndex = 0; mIndex < markdownEntries.length; mIndex += 1) {
|
|
1797
2254
|
var mEntry = markdownEntries[mIndex];
|
|
@@ -1846,12 +2303,16 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
1846
2303
|
if (codeScaled.length > 0) declarations.push(rootRule(codeScaled.join(";")));
|
|
1847
2304
|
}
|
|
1848
2305
|
if (codeWeight !== WEIGHT_UNSET) {
|
|
1849
|
-
// The
|
|
1850
|
-
//
|
|
1851
|
-
// instead of
|
|
2306
|
+
// The code axis is a relative offset too (all three weight axes share one
|
|
2307
|
+
// scale, so the card shows one kind of control). Pinning the offset here
|
|
2308
|
+
// instead of writing a flat `font-weight` keeps whatever weight a code
|
|
2309
|
+
// surface ships with AND is what makes the elements INSIDE a code surface —
|
|
2310
|
+
// which the interface table and the conversation ladder both match — resolve
|
|
2311
|
+
// the code offset rather than the surrounding one, without any `… *`
|
|
2312
|
+
// selector. `WEIGHT_BASE` is what DSH's code `font` shorthands carry.
|
|
1852
2313
|
declarations.push(
|
|
1853
2314
|
codeRule(
|
|
1854
|
-
"font-weight:" +
|
|
2315
|
+
"font-weight:" + ladderWeight(WEIGHT_BASE) + " !important;--dfp-wdelta:" + codeWeight
|
|
1855
2316
|
)
|
|
1856
2317
|
);
|
|
1857
2318
|
}
|
|
@@ -2406,7 +2867,27 @@ var shared = {
|
|
|
2406
2867
|
isFontToken: isFontToken,
|
|
2407
2868
|
isCodeToken: isCodeToken,
|
|
2408
2869
|
isMarkdownToken: isMarkdownToken,
|
|
2870
|
+
isMarkdownLiteralToken: isMarkdownLiteralToken,
|
|
2871
|
+
MARKDOWN_LITERAL_PREFIX: MARKDOWN_LITERAL_PREFIX,
|
|
2872
|
+
CONTENT_SIZE_SOURCE: CONTENT_SIZE_SOURCE,
|
|
2873
|
+
isLiteralTokenValue: isLiteralTokenValue,
|
|
2874
|
+
clampWeightDelta: clampWeightDelta,
|
|
2875
|
+
WEIGHT_DELTA_MIN: WEIGHT_DELTA_MIN,
|
|
2876
|
+
WEIGHT_DELTA_MAX: WEIGHT_DELTA_MAX,
|
|
2877
|
+
WEIGHT_BASE: WEIGHT_BASE,
|
|
2878
|
+
DIALOG_WEIGHT_GROUPS: DIALOG_WEIGHT_GROUPS,
|
|
2409
2879
|
isDeltaToken: isDeltaToken,
|
|
2880
|
+
reconcileSliderValue: reconcileSliderValue,
|
|
2881
|
+
WEIGHT_STEP_CANDIDATES: WEIGHT_STEP_CANDIDATES,
|
|
2882
|
+
WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
|
|
2883
|
+
WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
|
|
2884
|
+
WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
|
|
2885
|
+
WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
|
|
2886
|
+
WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
|
|
2887
|
+
WEIGHT_CSS_MIN: WEIGHT_CSS_MIN,
|
|
2888
|
+
WEIGHT_CSS_MAX: WEIGHT_CSS_MAX,
|
|
2889
|
+
weightProfileFrom: weightProfileFrom,
|
|
2890
|
+
weightStepRange: weightStepRange,
|
|
2410
2891
|
isShorthandName: isShorthandName,
|
|
2411
2892
|
isScaledToken: isScaledToken,
|
|
2412
2893
|
tokenNames: tokenNames,
|
|
@@ -2492,6 +2973,8 @@ var SIZE_MAX = shared.SIZE_MAX;
|
|
|
2492
2973
|
var WEIGHT_MIN = shared.WEIGHT_MIN;
|
|
2493
2974
|
var WEIGHT_MAX = shared.WEIGHT_MAX;
|
|
2494
2975
|
var WEIGHT_UNSET = shared.WEIGHT_UNSET;
|
|
2976
|
+
var WEIGHT_DELTA_MIN = shared.WEIGHT_DELTA_MIN;
|
|
2977
|
+
var WEIGHT_DELTA_MAX = shared.WEIGHT_DELTA_MAX;
|
|
2495
2978
|
var LINE_HEIGHT_MIN = shared.LINE_HEIGHT_MIN;
|
|
2496
2979
|
var LINE_HEIGHT_MAX = shared.LINE_HEIGHT_MAX;
|
|
2497
2980
|
var CODE_LINE_HEIGHT_MIN = shared.CODE_LINE_HEIGHT_MIN;
|
|
@@ -2514,6 +2997,9 @@ var normalizePresets = shared.normalizePresets;
|
|
|
2514
2997
|
var normalizeValueSet = shared.normalizeValueSet;
|
|
2515
2998
|
var parseStack = shared.parseStack;
|
|
2516
2999
|
var quoteFamily = shared.quoteFamily;
|
|
3000
|
+
var reconcileSliderValue = shared.reconcileSliderValue;
|
|
3001
|
+
var weightProfileFrom = shared.weightProfileFrom;
|
|
3002
|
+
var weightStepRange = shared.weightStepRange;
|
|
2517
3003
|
var resolveAxes = shared.resolveAxes;
|
|
2518
3004
|
var sanitizeFamily = shared.sanitizeFamily;
|
|
2519
3005
|
var isGenericFamilyName = shared.isGenericFamilyName;
|
|
@@ -2541,8 +3027,22 @@ var inject = ["slots", "locale"];
|
|
|
2541
3027
|
var cardScope = null;
|
|
2542
3028
|
var cardT = null;
|
|
2543
3029
|
|
|
2544
|
-
/**
|
|
2545
|
-
|
|
3030
|
+
/**
|
|
3031
|
+
* The neutral value of the two RELATIVE weight axes: an offset of zero, which
|
|
3032
|
+
* means "leave every element at its own weight" and is what the slider's middle
|
|
3033
|
+
* position and the "unset" state share.
|
|
3034
|
+
*/
|
|
3035
|
+
var NEUTRAL_WEIGHT = 0;
|
|
3036
|
+
|
|
3037
|
+
/**
|
|
3038
|
+
* Print a weight offset the way the slider reads it: a signed number, so `+60`
|
|
3039
|
+
* and `-40` are distinguishable at a glance (plain `String(0)` for neutral).
|
|
3040
|
+
* @param {number} value - the offset.
|
|
3041
|
+
* @returns {string}
|
|
3042
|
+
*/
|
|
3043
|
+
function weightOffsetText(value) {
|
|
3044
|
+
return value > 0 ? "+" + value : String(value);
|
|
3045
|
+
}
|
|
2546
3046
|
|
|
2547
3047
|
/** Panel width, kept in one place because positioning reads it too. */
|
|
2548
3048
|
var PANEL_WIDTH = 320;
|
|
@@ -2711,15 +3211,15 @@ var DICTS = {
|
|
|
2711
3211
|
"Adds {offset} to code blocks and inline code only; the interface offset does not reach them. 0 keeps DSH's sizes.",
|
|
2712
3212
|
"size.unit": "px",
|
|
2713
3213
|
|
|
2714
|
-
"weight.uiLabel": "Interface font
|
|
3214
|
+
"weight.uiLabel": "Interface font-weight offset",
|
|
2715
3215
|
"weight.uiHint":
|
|
2716
|
-
"
|
|
2717
|
-
"weight.dialogLabel": "Conversation font
|
|
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.",
|
|
3217
|
+
"weight.dialogLabel": "Conversation font-weight offset",
|
|
2718
3218
|
"weight.dialogHint":
|
|
2719
|
-
"
|
|
2720
|
-
"weight.codeLabel": "Code font
|
|
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.",
|
|
3220
|
+
"weight.codeLabel": "Code font-weight offset",
|
|
2721
3221
|
"weight.codeHint":
|
|
2722
|
-
"
|
|
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.",
|
|
2723
3223
|
|
|
2724
3224
|
"line.dialogLabel": "Conversation line height",
|
|
2725
3225
|
"line.dialogHint":
|
|
@@ -2855,14 +3355,15 @@ var DICTS = {
|
|
|
2855
3355
|
"只作用于代码块与行内代码,与界面字号互不影响;0 表示保持原样。",
|
|
2856
3356
|
"size.unit": "px",
|
|
2857
3357
|
|
|
2858
|
-
"weight.uiLabel": "
|
|
3358
|
+
"weight.uiLabel": "界面字重偏移",
|
|
2859
3359
|
"weight.uiHint":
|
|
2860
|
-
"
|
|
2861
|
-
"weight.dialogLabel": "
|
|
2862
|
-
"weight.dialogHint":
|
|
2863
|
-
|
|
3360
|
+
"在整个界面自身的字重上叠加(侧栏、设置、按钮、标题):正数更粗、负数更细;对话 Markdown 不受影响。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。",
|
|
3361
|
+
"weight.dialogLabel": "对话字重偏移",
|
|
3362
|
+
"weight.dialogHint":
|
|
3363
|
+
"在每个元素自身的字重上叠加,所以标题依旧比正文粗、加粗文字依旧加粗;未设置时保持 DSH 原本的粗细。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。",
|
|
3364
|
+
"weight.codeLabel": "代码字重偏移",
|
|
2864
3365
|
"weight.codeHint":
|
|
2865
|
-
"
|
|
3366
|
+
"在每个代码面自身的字重上叠加(代码块、行内代码、终端输出);0 或未设置表示保持 DSH 原样。滑块按当前字体实际能渲染的粗细分档,读数是档数(0 = 保持 DSH 原样):每挪一格,正文与标题都会换一张字形;换个字体,档数也会跟着变。",
|
|
2866
3367
|
|
|
2867
3368
|
"line.dialogLabel": "对话行高",
|
|
2868
3369
|
"line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
|
|
@@ -3309,6 +3810,180 @@ function readDefaultFamily(name) {
|
|
|
3309
3810
|
}
|
|
3310
3811
|
}
|
|
3311
3812
|
|
|
3813
|
+
/* ------------------------------------------------------------------ *
|
|
3814
|
+
* weight granularity
|
|
3815
|
+
* ------------------------------------------------------------------ */
|
|
3816
|
+
|
|
3817
|
+
/** Sample drawn to compare two weights; mixed scripts, like the UI. */
|
|
3818
|
+
var WEIGHT_SAMPLE_TEXT = "对话 Aa 字重 0189";
|
|
3819
|
+
|
|
3820
|
+
/** Canvas used for the granularity probe, created on first use. */
|
|
3821
|
+
var weightProbeCanvas = null;
|
|
3822
|
+
|
|
3823
|
+
/** Cache of measured slider shapes, keyed by the resolved family stack. */
|
|
3824
|
+
var weightProfileCache = {};
|
|
3825
|
+
|
|
3826
|
+
/**
|
|
3827
|
+
* Render the sample at one weight and describe the result.
|
|
3828
|
+
*
|
|
3829
|
+
* The signature is the total ink (summed alpha) plus the advance width at high
|
|
3830
|
+
* precision. Counting only fully-covered pixels would miss what a variable axis
|
|
3831
|
+
* does between two named instances — a couple of units of extra alpha per glyph
|
|
3832
|
+
* — and report a coarse family where the weights are in fact continuous.
|
|
3833
|
+
* @param {string} stack - resolved `font-family` value.
|
|
3834
|
+
* @param {number} weight - CSS weight to draw with.
|
|
3835
|
+
* @returns {string} the signature; equal signatures mean identical rendering.
|
|
3836
|
+
*/
|
|
3837
|
+
function sampleWeightSignature(stack, weight) {
|
|
3838
|
+
if (weightProbeCanvas === null) {
|
|
3839
|
+
weightProbeCanvas = document.createElement("canvas");
|
|
3840
|
+
weightProbeCanvas.width = 260;
|
|
3841
|
+
weightProbeCanvas.height = 56;
|
|
3842
|
+
}
|
|
3843
|
+
// `willReadFrequently` matters here: the profile probe reads the canvas back once
|
|
3844
|
+
// per sampled weight (~80 times on the first measurement), and without it the
|
|
3845
|
+
// browser keeps a GPU-backed surface and warns about the readbacks — which is part
|
|
3846
|
+
// of why dragging the weight felt heavy.
|
|
3847
|
+
var context = weightProbeCanvas.getContext("2d", { willReadFrequently: true });
|
|
3848
|
+
context.clearRect(0, 0, weightProbeCanvas.width, weightProbeCanvas.height);
|
|
3849
|
+
context.font = weight + " 32px " + stack;
|
|
3850
|
+
context.textBaseline = "top";
|
|
3851
|
+
context.fillStyle = "#000";
|
|
3852
|
+
context.fillText(WEIGHT_SAMPLE_TEXT, 2, 6);
|
|
3853
|
+
var advance = Math.round(context.measureText(WEIGHT_SAMPLE_TEXT).width * 1000);
|
|
3854
|
+
var pixels = context.getImageData(0, 0, weightProbeCanvas.width, weightProbeCanvas.height).data;
|
|
3855
|
+
var ink = 0;
|
|
3856
|
+
for (var index = 3; index < pixels.length; index += 4) ink += pixels[index];
|
|
3857
|
+
return ink + "/" + advance;
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
/**
|
|
3861
|
+
* The slider shape to use for one family: its range and its notch.
|
|
3862
|
+
*
|
|
3863
|
+
* A family can only be made as light or as heavy as the faces it ships, so the
|
|
3864
|
+
* range is measured rather than fixed (`weightProfileFrom`), which also makes
|
|
3865
|
+
* every notch a change — for every layer of the ladder, so the measurement is
|
|
3866
|
+
* told which base is the heaviest one the offset lands on. Measured once per
|
|
3867
|
+
* family and base and cached; any failure (no canvas, no 2D context) falls back
|
|
3868
|
+
* to the nominal window with single units.
|
|
3869
|
+
* @param {string} stack - resolved `font-family` value, "" for the page default.
|
|
3870
|
+
* @param {number} [top] - the heaviest base the offset is added to; the ladder's
|
|
3871
|
+
* own top for the conversation and interface axes, the axis's base for code.
|
|
3872
|
+
* @returns {{min: number, max: number, step: number}} the slider shape.
|
|
3873
|
+
*/
|
|
3874
|
+
function weightProfileFor(stack, top) {
|
|
3875
|
+
var base = typeof top === "number" ? top : shared.WEIGHT_LADDER_TOP;
|
|
3876
|
+
var key = String(stack) + "\u0000" + base;
|
|
3877
|
+
if (Object.prototype.hasOwnProperty.call(weightProfileCache, key)) {
|
|
3878
|
+
return weightProfileCache[key];
|
|
3879
|
+
}
|
|
3880
|
+
var profile = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
3881
|
+
try {
|
|
3882
|
+
var resolved = String(stack) === "" ? liveFamily() : String(stack);
|
|
3883
|
+
profile = weightProfileFrom(function (weight) {
|
|
3884
|
+
return sampleWeightSignature(resolved, weight);
|
|
3885
|
+
}, { top: base });
|
|
3886
|
+
} catch (error) {
|
|
3887
|
+
profile = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
3888
|
+
}
|
|
3889
|
+
weightProfileCache[key] = profile;
|
|
3890
|
+
return profile;
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
/**
|
|
3894
|
+
* The family the page actually renders with, as a `font-family` value.
|
|
3895
|
+
* @returns {string} the computed family, or "" when unreadable.
|
|
3896
|
+
*/
|
|
3897
|
+
function liveFamily() {
|
|
3898
|
+
if (typeof document === "undefined") return "";
|
|
3899
|
+
try {
|
|
3900
|
+
return document.defaultView.getComputedStyle(document.body).fontFamily || "";
|
|
3901
|
+
} catch (error) {
|
|
3902
|
+
return "";
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
/**
|
|
3907
|
+
* The conversation's own line-height ratio, measured once.
|
|
3908
|
+
*
|
|
3909
|
+
* The dialog line-height axis is a percentage OF DSH's own height (`28px × 1.05`),
|
|
3910
|
+
* so a preview that used the percentage as an ABSOLUTE ratio was wrong twice over:
|
|
3911
|
+
* at 100% it fell back to DSH's real (taller) default, while 105% asked for 1.05 —
|
|
3912
|
+
* shorter than that default and out of order with the notches above it.
|
|
3913
|
+
*
|
|
3914
|
+
* The base is read from a hidden node carrying the markdown class whose rule
|
|
3915
|
+
* consumes DSH's markdown shorthand, so it is the same ratio the page multiplies.
|
|
3916
|
+
* @returns {number} line height ÷ font size; a plain default when unmeasurable.
|
|
3917
|
+
*/
|
|
3918
|
+
var DIALOG_LINE_RATIO_FALLBACK = 1.5;
|
|
3919
|
+
var dialogLineRatioCache = null;
|
|
3920
|
+
function dialogLineRatio() {
|
|
3921
|
+
if (typeof dialogLineRatioCache === "number") return dialogLineRatioCache;
|
|
3922
|
+
if (typeof document === "undefined" || typeof document.defaultView === "undefined") {
|
|
3923
|
+
return DIALOG_LINE_RATIO_FALLBACK;
|
|
3924
|
+
}
|
|
3925
|
+
try {
|
|
3926
|
+
var className = "";
|
|
3927
|
+
var sheets = document.styleSheets || [];
|
|
3928
|
+
for (var index = 0; index < sheets.length && className === ""; index += 1) {
|
|
3929
|
+
var rules = null;
|
|
3930
|
+
try {
|
|
3931
|
+
rules = sheets[index].cssRules;
|
|
3932
|
+
} catch (error) {
|
|
3933
|
+
rules = null;
|
|
3934
|
+
}
|
|
3935
|
+
if (!rules) continue;
|
|
3936
|
+
for (var ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
|
|
3937
|
+
var selector = rules[ruleIndex].selectorText || "";
|
|
3938
|
+
var body = rules[ruleIndex].style ? rules[ruleIndex].style.cssText : "";
|
|
3939
|
+
if (selector.indexOf("_markdown_") < 0) continue;
|
|
3940
|
+
if (body.indexOf("--dsw-font-markdown-base") < 0) continue;
|
|
3941
|
+
var found = selector.match(/\.(_markdown_[A-Za-z0-9_-]+)/);
|
|
3942
|
+
if (found) className = found[1];
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
if (className !== "") {
|
|
3946
|
+
var host = document.createElement("div");
|
|
3947
|
+
host.className = className;
|
|
3948
|
+
host.setAttribute("data-dss-prose", "");
|
|
3949
|
+
host.style.position = "absolute";
|
|
3950
|
+
host.style.visibility = "hidden";
|
|
3951
|
+
host.style.left = "-9999px";
|
|
3952
|
+
var probe = document.createElement("p");
|
|
3953
|
+
probe.textContent = "line";
|
|
3954
|
+
probe.style.margin = "0";
|
|
3955
|
+
host.appendChild(probe);
|
|
3956
|
+
document.body.appendChild(host);
|
|
3957
|
+
var style = document.defaultView.getComputedStyle(probe);
|
|
3958
|
+
var size = parseFloat(style.fontSize);
|
|
3959
|
+
var height = parseFloat(style.lineHeight);
|
|
3960
|
+
if (isFinite(size) && size > 0 && isFinite(height) && height > 0) {
|
|
3961
|
+
dialogLineRatioCache = Math.round((height / size) * 10000) / 10000;
|
|
3962
|
+
}
|
|
3963
|
+
if (host.parentNode) host.parentNode.removeChild(host);
|
|
3964
|
+
}
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
/* the fallback stands */
|
|
3967
|
+
}
|
|
3968
|
+
if (typeof dialogLineRatioCache !== "number") dialogLineRatioCache = DIALOG_LINE_RATIO_FALLBACK;
|
|
3969
|
+
return dialogLineRatioCache;
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
/**
|
|
3973
|
+
* The family code surfaces render with when the code axis sets none.
|
|
3974
|
+
* @returns {string} the computed family, or "" when unreadable.
|
|
3975
|
+
*/
|
|
3976
|
+
function liveCodeFamily() {
|
|
3977
|
+
if (typeof document === "undefined") return "";
|
|
3978
|
+
try {
|
|
3979
|
+
var sample = document.querySelector("pre, code, kbd, samp");
|
|
3980
|
+
if (sample === null) return liveFamily();
|
|
3981
|
+
return document.defaultView.getComputedStyle(sample).fontFamily || liveFamily();
|
|
3982
|
+
} catch (error) {
|
|
3983
|
+
return "";
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3312
3987
|
/* ------------------------------------------------------------------ *
|
|
3313
3988
|
* hooks and controls
|
|
3314
3989
|
* ------------------------------------------------------------------ */
|
|
@@ -3441,17 +4116,25 @@ function NumberSlider(props) {
|
|
|
3441
4116
|
// (the "bounce back, then settle" the user saw), so the local value stays
|
|
3442
4117
|
// on screen until the confirmed value arrives.
|
|
3443
4118
|
var awaitingRef = useRef(null);
|
|
4119
|
+
// The same value as `pending`, readable without a render: the reconciliation
|
|
4120
|
+
// below runs from an effect that only depends on the incoming value.
|
|
4121
|
+
var pendingRef = useRef(null);
|
|
4122
|
+
var setLocal = function (value) {
|
|
4123
|
+
pendingRef.current = value;
|
|
4124
|
+
setPending(value);
|
|
4125
|
+
};
|
|
3444
4126
|
useEffect(
|
|
3445
4127
|
function () {
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
4128
|
+
// `reconcileSliderValue` owns the decision (and is unit-tested): keeping
|
|
4129
|
+
// the local value while the user is mid-drag is what stops a late
|
|
4130
|
+
// confirmation from snapping the thumb back to the previous value.
|
|
4131
|
+
var next = reconcileSliderValue({
|
|
4132
|
+
pending: pendingRef.current,
|
|
4133
|
+
awaiting: awaitingRef.current,
|
|
4134
|
+
confirmed: props.value,
|
|
4135
|
+
});
|
|
4136
|
+
awaitingRef.current = next.awaiting;
|
|
4137
|
+
setLocal(next.pending);
|
|
3455
4138
|
return undefined;
|
|
3456
4139
|
},
|
|
3457
4140
|
[props.value]
|
|
@@ -3460,12 +4143,39 @@ function NumberSlider(props) {
|
|
|
3460
4143
|
awaitingRef.current = value;
|
|
3461
4144
|
props.onChange(value);
|
|
3462
4145
|
};
|
|
4146
|
+
/**
|
|
4147
|
+
* Stop holding a value the host never confirmed.
|
|
4148
|
+
*
|
|
4149
|
+
* The local value is kept until the document echoes it back, which is what
|
|
4150
|
+
* stops the "bounce back, then settle" flicker — but a write the host REFUSES
|
|
4151
|
+
* (an out-of-range value against an older schema, a read-only deployment) never
|
|
4152
|
+
* echoes, and the slider would then sit on a value the document does not have,
|
|
4153
|
+
* i.e. it would look like it jumped somewhere else and stayed there. The
|
|
4154
|
+
* document is the truth, so after a few seconds the local value yields to it.
|
|
4155
|
+
*/
|
|
4156
|
+
var COMMIT_PATIENCE_MS = 6000;
|
|
4157
|
+
useEffect(
|
|
4158
|
+
function () {
|
|
4159
|
+
if (awaitingRef.current === null) return undefined;
|
|
4160
|
+
var timer = setTimeout(function () {
|
|
4161
|
+
awaitingRef.current = null;
|
|
4162
|
+
setLocal(null);
|
|
4163
|
+
}, COMMIT_PATIENCE_MS);
|
|
4164
|
+
return function () {
|
|
4165
|
+
clearTimeout(timer);
|
|
4166
|
+
};
|
|
4167
|
+
},
|
|
4168
|
+
[pending]
|
|
4169
|
+
);
|
|
3463
4170
|
useEffect(
|
|
3464
4171
|
function () {
|
|
3465
4172
|
if (pending === null) return undefined;
|
|
3466
4173
|
var release = function () {
|
|
3467
|
-
//
|
|
3468
|
-
|
|
4174
|
+
// One release per drag: a late pointerup must not rewrite the SAME value.
|
|
4175
|
+
// The guard compares values instead of "is anything outstanding" — a
|
|
4176
|
+
// commit still waiting for the document must not swallow the next drag's
|
|
4177
|
+
// release, which silently dropped that adjustment on a slow host.
|
|
4178
|
+
if (awaitingRef.current === pending) return;
|
|
3469
4179
|
commit(pending);
|
|
3470
4180
|
};
|
|
3471
4181
|
window.addEventListener("pointerup", release, true);
|
|
@@ -4445,11 +5155,19 @@ function SimpleFamilyField(props) {
|
|
|
4445
5155
|
* @param {boolean} uiFollows - whether the interface follows the conversation.
|
|
4446
5156
|
* @param {(key: string, params?: object) => string} t - translate seat.
|
|
4447
5157
|
* @returns {{ui: string, dialog: string, code: string}}
|
|
5158
|
+
* @param {(field: string, value: number) => number} [weightCount] - turns a
|
|
5159
|
+
* stored weight offset into the number of font steps the sliders show, so a
|
|
5160
|
+
* collapsed section reads "+2" exactly like the open slider does.
|
|
4448
5161
|
*/
|
|
4449
|
-
function sectionSummaries(axis, uiFollows, t) {
|
|
5162
|
+
function sectionSummaries(axis, uiFollows, t, weightCount) {
|
|
4450
5163
|
var sign = function (value) {
|
|
4451
5164
|
return value > 0 ? "+" + value : String(value);
|
|
4452
5165
|
};
|
|
5166
|
+
var weight = function (field, value) {
|
|
5167
|
+
return typeof weightCount === "function"
|
|
5168
|
+
? weightOffsetText(weightCount(field, value))
|
|
5169
|
+
: weightOffsetText(value);
|
|
5170
|
+
};
|
|
4453
5171
|
// The conversation owns every axis, so its summary is always its values.
|
|
4454
5172
|
var dialogParts = [];
|
|
4455
5173
|
if (axis[STACK_DIALOG_FIELD] !== "") dialogParts.push(firstFamily(axis[STACK_DIALOG_FIELD]));
|
|
@@ -4458,7 +5176,7 @@ function sectionSummaries(axis, uiFollows, t) {
|
|
|
4458
5176
|
dialogParts.push(axis[LINE_HEIGHT_DIALOG_FIELD] + "%");
|
|
4459
5177
|
}
|
|
4460
5178
|
if (axis[WEIGHT_DIALOG_FIELD] !== WEIGHT_UNSET) {
|
|
4461
|
-
dialogParts.push(
|
|
5179
|
+
dialogParts.push(weight(WEIGHT_DIALOG_FIELD, axis[WEIGHT_DIALOG_FIELD]));
|
|
4462
5180
|
}
|
|
4463
5181
|
// The interface either follows (nothing of its own to report) or shows the
|
|
4464
5182
|
// two axes it owns.
|
|
@@ -4466,13 +5184,15 @@ function sectionSummaries(axis, uiFollows, t) {
|
|
|
4466
5184
|
if (!uiFollows) {
|
|
4467
5185
|
var family = axis[SANS_FIELD] === "" ? null : firstFamily(axis[SANS_FIELD]);
|
|
4468
5186
|
if (family !== null) uiParts.push(family);
|
|
4469
|
-
if (axis[WEIGHT_FIELD] !== WEIGHT_UNSET) uiParts.push(
|
|
5187
|
+
if (axis[WEIGHT_FIELD] !== WEIGHT_UNSET) uiParts.push(weight(WEIGHT_FIELD, axis[WEIGHT_FIELD]));
|
|
4470
5188
|
}
|
|
4471
5189
|
var codeParts = [];
|
|
4472
5190
|
if (axis[MONO_FIELD] !== "") codeParts.push(firstFamily(axis[MONO_FIELD]));
|
|
4473
5191
|
if (axis[CODE_SIZE_FIELD] !== 0) codeParts.push(sign(axis[CODE_SIZE_FIELD]) + "px");
|
|
4474
5192
|
if (axis[CODE_LINE_HEIGHT_FIELD] !== 0) codeParts.push(sign(axis[CODE_LINE_HEIGHT_FIELD]) + "px");
|
|
4475
|
-
if (axis[CODE_WEIGHT_FIELD] !== WEIGHT_UNSET)
|
|
5193
|
+
if (axis[CODE_WEIGHT_FIELD] !== WEIGHT_UNSET) {
|
|
5194
|
+
codeParts.push(weight(CODE_WEIGHT_FIELD, axis[CODE_WEIGHT_FIELD]));
|
|
5195
|
+
}
|
|
4476
5196
|
if (axis[LIGATURES_FIELD] !== LIGATURES_DEFAULT) {
|
|
4477
5197
|
codeParts.push(t(axis[LIGATURES_FIELD] === LIGATURES_OFF ? "lig.off" : "lig.on"));
|
|
4478
5198
|
}
|
|
@@ -4760,7 +5480,28 @@ function FontCard(props) {
|
|
|
4760
5480
|
};
|
|
4761
5481
|
|
|
4762
5482
|
// ---- weight slider ----
|
|
5483
|
+
// All three weight axes share one shape: an offset added to each element's own
|
|
5484
|
+
// weight, centred on zero (code included — its surfaces ship their own weight
|
|
5485
|
+
// too, and an offset keeps any bold element inside them bold).
|
|
5486
|
+
//
|
|
5487
|
+
// The CONTROL counts the family's own steps ("-2 -1 0 +1 +2 +3 +4"), because a
|
|
5488
|
+
// family can only be made as light or as heavy as the faces it ships and only
|
|
5489
|
+
// in steps between them: how many positions there are is a property of the font,
|
|
5490
|
+
// measured on the page. The VALUE written to the document stays a weight offset,
|
|
5491
|
+
// so nothing stored before has to be migrated and the stylesheet is unchanged.
|
|
4763
5492
|
var weightField = function (props) {
|
|
5493
|
+
var profile = weightProfileFor(
|
|
5494
|
+
props.stack === undefined ? "" : props.stack,
|
|
5495
|
+
props.top === undefined ? shared.WEIGHT_LADDER_STRONG : props.top
|
|
5496
|
+
);
|
|
5497
|
+
var stored = props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value;
|
|
5498
|
+
// A stored offset outside the measured range (the font changed since it was
|
|
5499
|
+
// set) widens the control, so it is never unreachable.
|
|
5500
|
+
var shape = weightStepRange(
|
|
5501
|
+
{ min: Math.min(profile.min, stored), max: Math.max(profile.max, stored), step: profile.step },
|
|
5502
|
+
stored
|
|
5503
|
+
);
|
|
5504
|
+
var text = weightOffsetText;
|
|
4764
5505
|
return h(
|
|
4765
5506
|
FieldShell,
|
|
4766
5507
|
{
|
|
@@ -4774,22 +5515,27 @@ function FontCard(props) {
|
|
|
4774
5515
|
},
|
|
4775
5516
|
},
|
|
4776
5517
|
h(NumberSlider, {
|
|
4777
|
-
min:
|
|
4778
|
-
max:
|
|
4779
|
-
value:
|
|
5518
|
+
min: shape.min,
|
|
5519
|
+
max: shape.max,
|
|
5520
|
+
value: shape.value,
|
|
4780
5521
|
disabled: !writable,
|
|
4781
5522
|
label: t(props.labelKey),
|
|
4782
|
-
//
|
|
4783
|
-
|
|
4784
|
-
//
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
5523
|
+
// One notch is one family step, so every notch changes what is on screen.
|
|
5524
|
+
step: 1,
|
|
5525
|
+
// The readout is the STEP COUNT, not the weight: "+2" is two faces
|
|
5526
|
+
// bolder, and the numbers stay small whatever the family is.
|
|
5527
|
+
readout: text(shape.value),
|
|
5528
|
+
minLabel: text(shape.min),
|
|
5529
|
+
maxLabel: text(shape.max),
|
|
5530
|
+
// The pending readout goes through the same formatter: without it the
|
|
5531
|
+
// sign only appeared on release ("80" while dragging, then "+80" once
|
|
5532
|
+
// the document confirmed it).
|
|
5533
|
+
pendingText: text,
|
|
5534
|
+
onChange: function (count) {
|
|
5535
|
+
// The neutral position means "leave the axis alone" rather than "write
|
|
5536
|
+
// it on every element".
|
|
5537
|
+
if (count === 0) resetField(props.field);
|
|
5538
|
+
else setField(props.field, count * shape.unit);
|
|
4793
5539
|
},
|
|
4794
5540
|
})
|
|
4795
5541
|
);
|
|
@@ -5146,33 +5892,59 @@ function FontCard(props) {
|
|
|
5146
5892
|
// fallback. The switch lives in the interface section.
|
|
5147
5893
|
var uiFollows = config[UI_FOLLOWS_FIELD] !== false;
|
|
5148
5894
|
|
|
5149
|
-
|
|
5895
|
+
// The family each weight axis is measured against: the configured stack when
|
|
5896
|
+
// there is one, the page's own family otherwise. Resolved once, so the
|
|
5897
|
+
// collapsed summaries, the sliders and the previews all agree on the count.
|
|
5898
|
+
var uiStack =
|
|
5899
|
+
editing[SANS_FIELD] !== "" ? formatStack(parseStack(editing[SANS_FIELD])) : "";
|
|
5900
|
+
var dialogFallbackFamily =
|
|
5901
|
+
typeof FALLBACK_TOKENS["--dsw-font-family"] === "string" ? FALLBACK_TOKENS["--dsw-font-family"] : "";
|
|
5902
|
+
var dialogStack =
|
|
5903
|
+
editing[STACK_DIALOG_FIELD] !== ""
|
|
5904
|
+
? formatStack(parseStack(editing[STACK_DIALOG_FIELD]))
|
|
5905
|
+
: dialogFallbackFamily;
|
|
5906
|
+
var codeStack =
|
|
5907
|
+
editing[MONO_FIELD] !== "" ? formatStack(parseStack(editing[MONO_FIELD])) : liveCodeFamily();
|
|
5908
|
+
/**
|
|
5909
|
+
* One weight offset, as the number of family steps the reader sees.
|
|
5910
|
+
* @param {string} field - which weight axis.
|
|
5911
|
+
* @param {number} value - the stored offset.
|
|
5912
|
+
* @returns {number} the step count.
|
|
5913
|
+
*/
|
|
5914
|
+
var weightCount = function (field, value) {
|
|
5915
|
+
var isCode = field === CODE_WEIGHT_FIELD;
|
|
5916
|
+
var stack =
|
|
5917
|
+
field === WEIGHT_DIALOG_FIELD ? dialogStack : isCode ? codeStack : uiStack;
|
|
5918
|
+
var profile = weightProfileFor(stack, isCode ? shared.WEIGHT_BASE : shared.WEIGHT_LADDER_STRONG);
|
|
5919
|
+
return weightStepRange(profile, value === WEIGHT_UNSET ? 0 : value).value;
|
|
5920
|
+
};
|
|
5921
|
+
|
|
5922
|
+
var summaries = sectionSummaries(editing, uiFollows, t, weightCount);
|
|
5150
5923
|
|
|
5151
5924
|
// The interface owns one axis now (its family); its preview shows that and
|
|
5152
5925
|
// nothing else — the weight, size and line height all come from the
|
|
5153
5926
|
// conversation section.
|
|
5154
5927
|
var previewUiStyle = {};
|
|
5155
|
-
if (
|
|
5156
|
-
previewUiStyle.fontFamily = formatStack(parseStack(editing[SANS_FIELD]));
|
|
5157
|
-
}
|
|
5928
|
+
if (uiStack !== "") previewUiStyle.fontFamily = uiStack;
|
|
5158
5929
|
var previewDialogStyle = {};
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
// preview shows the theme's default rather than the interface stack the
|
|
5164
|
-
// page rule would otherwise paint into this box.
|
|
5165
|
-
var dialogFallback = FALLBACK_TOKENS["--dsw-font-family"];
|
|
5166
|
-
if (typeof dialogFallback === "string" && dialogFallback !== "") {
|
|
5167
|
-
previewDialogStyle.fontFamily = dialogFallback;
|
|
5168
|
-
}
|
|
5169
|
-
}
|
|
5930
|
+
// Independent and unset: the conversation keeps DSH's own family, so the
|
|
5931
|
+
// preview shows the theme's default rather than the interface stack the page
|
|
5932
|
+
// rule would otherwise paint into this box.
|
|
5933
|
+
if (dialogStack !== "") previewDialogStyle.fontFamily = dialogStack;
|
|
5170
5934
|
previewDialogStyle.fontSize = 13 + editing[SIZE_DIALOG_FIELD] + "px";
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5935
|
+
// The axis is a percentage OF DSH's own line height, so the preview multiplies the
|
|
5936
|
+
// measured base by the same percentage — and does it at every notch including 100,
|
|
5937
|
+
// or the preview would show a shorter line at 105% than at 100%.
|
|
5938
|
+
previewDialogStyle.lineHeight = String(
|
|
5939
|
+
Math.round(dialogLineRatio() * (editing[LINE_HEIGHT_DIALOG_FIELD] / LINE_HEIGHT_MIN) * 10000) /
|
|
5940
|
+
10000
|
|
5941
|
+
);
|
|
5174
5942
|
if (editing[WEIGHT_DIALOG_FIELD] !== WEIGHT_UNSET) {
|
|
5175
|
-
|
|
5943
|
+
// The offset, not a weight: the injected stylesheet's conversation ladder
|
|
5944
|
+
// reads `--dfp-wdelta` on this very class and re-states each element's own
|
|
5945
|
+
// base plus the offset, so the preview shows the same hierarchy the
|
|
5946
|
+
// conversation does instead of one flat weight.
|
|
5947
|
+
previewDialogStyle["--dfp-wdelta"] = editing[WEIGHT_DIALOG_FIELD];
|
|
5176
5948
|
}
|
|
5177
5949
|
var previewCodeStyle = {};
|
|
5178
5950
|
if (editing[MONO_FIELD] !== "") {
|
|
@@ -5423,6 +6195,7 @@ function FontCard(props) {
|
|
|
5423
6195
|
labelKey: "weight.dialogLabel",
|
|
5424
6196
|
hintKey: "weight.dialogHint",
|
|
5425
6197
|
value: editing[WEIGHT_DIALOG_FIELD],
|
|
6198
|
+
stack: dialogStack,
|
|
5426
6199
|
}),
|
|
5427
6200
|
h(
|
|
5428
6201
|
"div",
|
|
@@ -5509,6 +6282,7 @@ function FontCard(props) {
|
|
|
5509
6282
|
labelKey: "weight.uiLabel",
|
|
5510
6283
|
hintKey: "weight.uiHint",
|
|
5511
6284
|
value: editing[WEIGHT_FIELD],
|
|
6285
|
+
stack: uiStack,
|
|
5512
6286
|
}),
|
|
5513
6287
|
h(
|
|
5514
6288
|
"div",
|
|
@@ -5551,6 +6325,11 @@ function FontCard(props) {
|
|
|
5551
6325
|
labelKey: "weight.codeLabel",
|
|
5552
6326
|
hintKey: "weight.codeHint",
|
|
5553
6327
|
value: editing[CODE_WEIGHT_FIELD],
|
|
6328
|
+
// The code axis puts one weight on the whole surface, so its own base
|
|
6329
|
+
// is the heaviest layer there is: any lower base would cap the range
|
|
6330
|
+
// short of what code text can actually reach.
|
|
6331
|
+
top: shared.WEIGHT_BASE,
|
|
6332
|
+
stack: codeStack,
|
|
5554
6333
|
}),
|
|
5555
6334
|
ligatureField(),
|
|
5556
6335
|
view === "advanced" ? featuresField() : null,
|