dsh-fonttune 0.1.6 → 0.2.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 +39 -0
- package/CHANGELOG.md +39 -0
- package/README.en.md +27 -15
- package/README.md +26 -14
- package/lib/client.js +2982 -527
- package/lib/index.js +168 -32
- package/lib/shared.cjs +1435 -265
- package/package.json +4 -4
package/lib/client.js
CHANGED
|
@@ -19,6 +19,26 @@ var __dfpShared = (function () {
|
|
|
19
19
|
* configuration produces, and the sanitizing that keeps user-typed font names
|
|
20
20
|
* from breaking out of the injected stylesheet.
|
|
21
21
|
*
|
|
22
|
+
* 0.2.x shape: the axes are grouped into three SURFACES — interface (the UI
|
|
23
|
+
* chrome), dialog (the conversation markdown) and code. The CONVERSATION owns
|
|
24
|
+
* every axis: a family, a size offset, a weight and a line-height ratio. The
|
|
25
|
+
* interface owns a family and a weight, and by default FOLLOWS the conversation
|
|
26
|
+
* on both (the switch turns that off and reveals its own controls); DSH exposes
|
|
27
|
+
* no interface size or line-height hook at all, so those two axes are retired
|
|
28
|
+
* and the durable fields inject nothing. Code carries a family, a size offset,
|
|
29
|
+
* a weight, an additive line-height offset and the ligature/feature controls.
|
|
30
|
+
* `perTheme` enables a second value set for the dark theme, stored as a sparse
|
|
31
|
+
* override map.
|
|
32
|
+
*
|
|
33
|
+
* The interface weight is the one axis that has to reach the WHOLE page (DSH
|
|
34
|
+
* pins its labels with literal weights on class rules, so nothing narrower can
|
|
35
|
+
* move them), which is why it is a `body, body *` rule with an exclusion for
|
|
36
|
+
* the conversation markdown subtree: the conversation's own weight rule then
|
|
37
|
+
* owns that subtree completely and neither axis can disturb the other. That
|
|
38
|
+
* exclusion is what makes the axis safe — measured on a live page (computed
|
|
39
|
+
* styles, `test/weight-verify.mjs`): every text element outside the markdown
|
|
40
|
+
* subtree takes the weight, and not one element inside it moves.
|
|
41
|
+
*
|
|
22
42
|
* @module dsh-fonttune/shared
|
|
23
43
|
*/
|
|
24
44
|
"use strict";
|
|
@@ -26,24 +46,99 @@ var __dfpShared = (function () {
|
|
|
26
46
|
/** Settings namespace registered by the host half. */
|
|
27
47
|
var NAMESPACE = "dsh-fonttune";
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
/* ------------------------------------------------------------------ *
|
|
50
|
+
* durable field names
|
|
51
|
+
* ------------------------------------------------------------------ */
|
|
52
|
+
|
|
53
|
+
/** Field carrying the interface CSS font-family stack (empty = leave DSH). */
|
|
30
54
|
var SANS_FIELD = "sans";
|
|
31
55
|
|
|
32
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Field carrying the dialog stack. The conversation owns the family: empty
|
|
58
|
+
* means DSH's own default family. A document written before 0.2.0 only has the
|
|
59
|
+
* interface family, so an empty dialog takes it (see `expandFollow`) — that is
|
|
60
|
+
* a migration bridge, not a live rule the card describes.
|
|
61
|
+
*/
|
|
62
|
+
var STACK_DIALOG_FIELD = "stackDialog";
|
|
63
|
+
|
|
64
|
+
/** Field carrying the code CSS font-family stack (empty = leave DSH). */
|
|
33
65
|
var MONO_FIELD = "mono";
|
|
34
66
|
|
|
35
|
-
/** Field carrying the
|
|
67
|
+
/** Field carrying the interface font-size offset in px (0 = leave DSH alone). */
|
|
36
68
|
var SIZE_FIELD = "sizeOffset";
|
|
37
69
|
|
|
70
|
+
/** Field carrying the dialog font-size offset in px (0 = DSH's own sizes). */
|
|
71
|
+
var SIZE_DIALOG_FIELD = "sizeOffsetDialog";
|
|
72
|
+
|
|
38
73
|
/** Field carrying the code font-size offset in px (0 = leave DSH alone). */
|
|
39
74
|
var CODE_SIZE_FIELD = "sizeOffsetCode";
|
|
40
75
|
|
|
41
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Field carrying the interface font weight (0 = leave DSH alone). While the
|
|
78
|
+
* interface follows the conversation this is the fallback for a conversation
|
|
79
|
+
* that sets no weight of its own.
|
|
80
|
+
*/
|
|
42
81
|
var WEIGHT_FIELD = "weight";
|
|
43
82
|
|
|
83
|
+
/** Field carrying the dialog font weight (0 = DSH's own weights). */
|
|
84
|
+
var WEIGHT_DIALOG_FIELD = "weightDialog";
|
|
85
|
+
|
|
44
86
|
/** Field carrying the code font weight (0 = leave DSH alone). */
|
|
45
87
|
var CODE_WEIGHT_FIELD = "weightCode";
|
|
46
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Field carrying the interface line-height ratio in percent (100 = untouched).
|
|
91
|
+
* A ratio rather than a px offset: interface line heights form a design ladder
|
|
92
|
+
* (14/18/20/22/24/26/28/32), and a ratio keeps every step proportional.
|
|
93
|
+
*/
|
|
94
|
+
var LINE_HEIGHT_FIELD = "lineHeight";
|
|
95
|
+
|
|
96
|
+
/** Field carrying the dialog line-height ratio in percent (100 = DSH's own). */
|
|
97
|
+
var LINE_HEIGHT_DIALOG_FIELD = "lineHeightDialog";
|
|
98
|
+
|
|
99
|
+
/** Field carrying the code line-height offset in px (0 = leave DSH alone). */
|
|
100
|
+
var CODE_LINE_HEIGHT_FIELD = "lineHeightCode";
|
|
101
|
+
|
|
102
|
+
/** Ligature mode: 0 = DSH default, 1 = forced on, 2 = forced off. */
|
|
103
|
+
var LIGATURES_FIELD = "codeLigatures";
|
|
104
|
+
|
|
105
|
+
/** Advanced `font-feature-settings` value for code (sanitized; empty = none). */
|
|
106
|
+
var FEATURES_FIELD = "codeFeatures";
|
|
107
|
+
|
|
108
|
+
/** Disable synthetic (faux) italic — the "tilted CJK" rendering. */
|
|
109
|
+
var NO_SYNTHETIC_ITALIC_FIELD = "noSyntheticItalic";
|
|
110
|
+
|
|
111
|
+
/** Disable synthetic (faux) bold, for faces without a real bold. */
|
|
112
|
+
var NO_SYNTHETIC_BOLD_FIELD = "noSyntheticBold";
|
|
113
|
+
|
|
114
|
+
/** Whether the dark theme keeps its own value set (sparse overrides). */
|
|
115
|
+
var PER_THEME_FIELD = "perTheme";
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* JSON string of the dark-theme sparse overrides: a map of axis field to
|
|
119
|
+
* value, applied over the single set while the dark theme is active.
|
|
120
|
+
*/
|
|
121
|
+
var DARK_VALUES_FIELD = "darkValues";
|
|
122
|
+
|
|
123
|
+
/** JSON string of saved presets: [{name, values, savedAt}]. */
|
|
124
|
+
var PRESETS_FIELD = "presets";
|
|
125
|
+
|
|
126
|
+
/** Name of the preset the card's edits auto-save into. */
|
|
127
|
+
var ACTIVE_PRESET_FIELD = "activePreset";
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Whether the INTERFACE follows the conversation. The conversation owns every
|
|
131
|
+
* axis; the interface has a family and a weight of its own (DSH exposes no
|
|
132
|
+
* interface size or line-height hook), so on — the default — the conversation's
|
|
133
|
+
* family and weight win and the interface's own values stand as the fallback
|
|
134
|
+
* for an axis the conversation does not set.
|
|
135
|
+
*/
|
|
136
|
+
var UI_FOLLOWS_FIELD = "uiFollowsDialog";
|
|
137
|
+
|
|
138
|
+
/* ------------------------------------------------------------------ *
|
|
139
|
+
* ranges and constants
|
|
140
|
+
* ------------------------------------------------------------------ */
|
|
141
|
+
|
|
47
142
|
/** Allowed font-size offset range. The upper bound stays under a 2x scale. */
|
|
48
143
|
var SIZE_MIN = -3;
|
|
49
144
|
var SIZE_MAX = 6;
|
|
@@ -53,10 +148,23 @@ var WEIGHT_MIN = 300;
|
|
|
53
148
|
var WEIGHT_MAX = 600;
|
|
54
149
|
var WEIGHT_UNSET = 0;
|
|
55
150
|
|
|
151
|
+
/** Allowed line-height ratio range, in percent (100 = untouched). */
|
|
152
|
+
var LINE_HEIGHT_MIN = 100;
|
|
153
|
+
var LINE_HEIGHT_MAX = 160;
|
|
154
|
+
|
|
155
|
+
/** Allowed code line-height offset range, in px. */
|
|
156
|
+
var CODE_LINE_HEIGHT_MIN = -4;
|
|
157
|
+
var CODE_LINE_HEIGHT_MAX = 8;
|
|
158
|
+
|
|
159
|
+
/** Ligature modes. */
|
|
160
|
+
var LIGATURES_DEFAULT = 0;
|
|
161
|
+
var LIGATURES_ON = 1;
|
|
162
|
+
var LIGATURES_OFF = 2;
|
|
163
|
+
|
|
56
164
|
/** Marker on both injected style tags, used for scoping every rule we write. */
|
|
57
165
|
var MARKER = "dfp";
|
|
58
166
|
|
|
59
|
-
/** `data-plugin-css` value of the tag applying
|
|
167
|
+
/** `data-plugin-css` value of the tag applying the configuration. */
|
|
60
168
|
var STYLE_TAG = "dsh-fonttune";
|
|
61
169
|
|
|
62
170
|
/** `data-plugin-css` value of the card's own chrome stylesheet. */
|
|
@@ -65,13 +173,52 @@ var CARD_STYLE_TAG = "dsh-fonttune-card";
|
|
|
65
173
|
/** Composition defaults: every axis dormant, so installing changes nothing. */
|
|
66
174
|
var DEFAULTS = {
|
|
67
175
|
sans: "",
|
|
176
|
+
stackDialog: "",
|
|
68
177
|
mono: "",
|
|
69
178
|
sizeOffset: 0,
|
|
179
|
+
sizeOffsetDialog: 0,
|
|
70
180
|
sizeOffsetCode: 0,
|
|
71
181
|
weight: WEIGHT_UNSET,
|
|
182
|
+
weightDialog: WEIGHT_UNSET,
|
|
72
183
|
weightCode: WEIGHT_UNSET,
|
|
184
|
+
lineHeight: LINE_HEIGHT_MIN,
|
|
185
|
+
lineHeightDialog: LINE_HEIGHT_MIN,
|
|
186
|
+
lineHeightCode: 0,
|
|
187
|
+
codeLigatures: LIGATURES_DEFAULT,
|
|
188
|
+
codeFeatures: "",
|
|
189
|
+
noSyntheticItalic: false,
|
|
190
|
+
noSyntheticBold: false,
|
|
191
|
+
perTheme: false,
|
|
192
|
+
darkValues: "{}",
|
|
193
|
+
presets: "[]",
|
|
194
|
+
activePreset: "",
|
|
195
|
+
uiFollowsDialog: true,
|
|
73
196
|
};
|
|
74
197
|
|
|
198
|
+
/**
|
|
199
|
+
* The axis fields a preset snapshots: everything the card edits, excluding
|
|
200
|
+
* the storage fields themselves.
|
|
201
|
+
*/
|
|
202
|
+
var VALUE_FIELDS = [
|
|
203
|
+
SANS_FIELD,
|
|
204
|
+
STACK_DIALOG_FIELD,
|
|
205
|
+
MONO_FIELD,
|
|
206
|
+
SIZE_FIELD,
|
|
207
|
+
SIZE_DIALOG_FIELD,
|
|
208
|
+
CODE_SIZE_FIELD,
|
|
209
|
+
WEIGHT_FIELD,
|
|
210
|
+
WEIGHT_DIALOG_FIELD,
|
|
211
|
+
CODE_WEIGHT_FIELD,
|
|
212
|
+
LINE_HEIGHT_FIELD,
|
|
213
|
+
LINE_HEIGHT_DIALOG_FIELD,
|
|
214
|
+
CODE_LINE_HEIGHT_FIELD,
|
|
215
|
+
LIGATURES_FIELD,
|
|
216
|
+
FEATURES_FIELD,
|
|
217
|
+
NO_SYNTHETIC_ITALIC_FIELD,
|
|
218
|
+
NO_SYNTHETIC_BOLD_FIELD,
|
|
219
|
+
UI_FOLLOWS_FIELD,
|
|
220
|
+
];
|
|
221
|
+
|
|
75
222
|
/**
|
|
76
223
|
* Longest accepted font stack, in characters (mirrored by the host schema).
|
|
77
224
|
*/
|
|
@@ -80,6 +227,19 @@ var MAX_STACK_LENGTH = 200;
|
|
|
80
227
|
/** Longest accepted single family name, in characters. */
|
|
81
228
|
var MAX_FAMILY_LENGTH = 64;
|
|
82
229
|
|
|
230
|
+
/** Longest accepted `font-feature-settings` value, in characters. */
|
|
231
|
+
var MAX_FEATURES_LENGTH = 120;
|
|
232
|
+
|
|
233
|
+
/** Most presets one configuration may hold. */
|
|
234
|
+
var MAX_PRESETS = 20;
|
|
235
|
+
|
|
236
|
+
/** Longest preset name, in characters. */
|
|
237
|
+
var MAX_PRESET_NAME = 32;
|
|
238
|
+
|
|
239
|
+
/* ------------------------------------------------------------------ *
|
|
240
|
+
* sanitizing
|
|
241
|
+
* ------------------------------------------------------------------ */
|
|
242
|
+
|
|
83
243
|
/**
|
|
84
244
|
* What may survive in a family name.
|
|
85
245
|
*
|
|
@@ -150,28 +310,93 @@ var GENERIC_FAMILIES = [
|
|
|
150
310
|
"blinkmacsystemfont",
|
|
151
311
|
];
|
|
152
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Sanitize an advanced `font-feature-settings` value.
|
|
315
|
+
*
|
|
316
|
+
* Feature tags are letters and digits; entries may carry on/off/number and are
|
|
317
|
+
* comma separated. The allow-list (letters, digits, quotes, spaces, commas)
|
|
318
|
+
* keeps the value from closing the declaration or opening another one, and a
|
|
319
|
+
* coarse shape test rejects anything that is not a tag-entry list.
|
|
320
|
+
* @param {unknown} value - candidate features value.
|
|
321
|
+
* @returns {string} the sanitized value, or "".
|
|
322
|
+
*/
|
|
323
|
+
function sanitizeFeatures(value) {
|
|
324
|
+
if (typeof value !== "string") return "";
|
|
325
|
+
var text = value.replace(FEATURES_UNSAFE, "").slice(0, MAX_FEATURES_LENGTH).trim();
|
|
326
|
+
if (text === "") return "";
|
|
327
|
+
if (!/^(?:"?[a-zA-Z0-9]{1,8}"?(?: (?:on|off|-?\d+))?(?:, ?)?)+$/.test(text)) return "";
|
|
328
|
+
return text;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Characters a `font-feature-settings` value cannot contain. */
|
|
332
|
+
var FEATURES_UNSAFE = /[^a-zA-Z0-9"' ,]/g;
|
|
333
|
+
|
|
334
|
+
/* ------------------------------------------------------------------ *
|
|
335
|
+
* configuration normalization
|
|
336
|
+
* ------------------------------------------------------------------ */
|
|
337
|
+
|
|
338
|
+
/** Clamp/validate one field by name (shared by config and value sets). */
|
|
339
|
+
function normalizeField(field, value) {
|
|
340
|
+
switch (field) {
|
|
341
|
+
case SANS_FIELD:
|
|
342
|
+
case STACK_DIALOG_FIELD:
|
|
343
|
+
case MONO_FIELD:
|
|
344
|
+
return sanitize(value).slice(0, MAX_STACK_LENGTH);
|
|
345
|
+
case SIZE_FIELD:
|
|
346
|
+
case SIZE_DIALOG_FIELD:
|
|
347
|
+
case CODE_SIZE_FIELD:
|
|
348
|
+
return clampOffset(value);
|
|
349
|
+
case WEIGHT_FIELD:
|
|
350
|
+
case WEIGHT_DIALOG_FIELD:
|
|
351
|
+
case CODE_WEIGHT_FIELD:
|
|
352
|
+
return clampWeight(value);
|
|
353
|
+
case LINE_HEIGHT_FIELD:
|
|
354
|
+
case LINE_HEIGHT_DIALOG_FIELD:
|
|
355
|
+
return clampRatio(value);
|
|
356
|
+
case CODE_LINE_HEIGHT_FIELD:
|
|
357
|
+
return clampLineOffset(value);
|
|
358
|
+
case LIGATURES_FIELD:
|
|
359
|
+
return clampLigatures(value);
|
|
360
|
+
case FEATURES_FIELD:
|
|
361
|
+
return sanitizeFeatures(value);
|
|
362
|
+
case NO_SYNTHETIC_ITALIC_FIELD:
|
|
363
|
+
case NO_SYNTHETIC_BOLD_FIELD:
|
|
364
|
+
return toBool(value);
|
|
365
|
+
case UI_FOLLOWS_FIELD:
|
|
366
|
+
// Absent means the default: the interface follows the conversation.
|
|
367
|
+
return value === undefined || value === null || value === "" ? true : toBool(value);
|
|
368
|
+
default:
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
153
373
|
/**
|
|
154
374
|
* Normalize a configuration-shaped object coming from the settings document,
|
|
155
|
-
* a config layer, or a test fixture.
|
|
375
|
+
* a config layer, or a test fixture. Storage fields (`darkValues`,
|
|
376
|
+
* `presets`) are decoded and validated here too, so every consumer sees
|
|
377
|
+
* plain data.
|
|
156
378
|
* @param {unknown} value - candidate configuration.
|
|
157
|
-
* @returns {
|
|
379
|
+
* @returns {Record<string, string|number|boolean|object|array>} the normalized config.
|
|
158
380
|
*/
|
|
159
381
|
function normalizeConfig(value) {
|
|
160
382
|
var source = value !== null && typeof value === "object" ? value : {};
|
|
161
|
-
var size = clampOffset(source[SIZE_FIELD]);
|
|
162
|
-
var codeSize = clampOffset(source[CODE_SIZE_FIELD]);
|
|
163
|
-
var weight = clampWeight(source[WEIGHT_FIELD]);
|
|
164
|
-
var codeWeight = clampWeight(source[CODE_WEIGHT_FIELD]);
|
|
165
383
|
var config = {};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
config[
|
|
171
|
-
config[
|
|
384
|
+
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
385
|
+
var field = VALUE_FIELDS[index];
|
|
386
|
+
config[field] = normalizeField(field, source[field]);
|
|
387
|
+
}
|
|
388
|
+
config[PER_THEME_FIELD] = toBool(source[PER_THEME_FIELD]);
|
|
389
|
+
config[DARK_VALUES_FIELD] = normalizeDarkValues(source[DARK_VALUES_FIELD]);
|
|
390
|
+
config[PRESETS_FIELD] = normalizePresets(source[PRESETS_FIELD]);
|
|
391
|
+
config[ACTIVE_PRESET_FIELD] = sanitize(source[ACTIVE_PRESET_FIELD]).slice(0, MAX_PRESET_NAME);
|
|
172
392
|
return config;
|
|
173
393
|
}
|
|
174
394
|
|
|
395
|
+
/** Coerce a settings value to a strict boolean; anything unusable is false. */
|
|
396
|
+
function toBool(value) {
|
|
397
|
+
return value === true || value === 1 || value === "1" || value === "true";
|
|
398
|
+
}
|
|
399
|
+
|
|
175
400
|
/**
|
|
176
401
|
* Clamp one size offset to the allowed range, with 0 for anything unusable.
|
|
177
402
|
* @param {unknown} value - candidate offset in px.
|
|
@@ -196,6 +421,109 @@ function clampWeight(value) {
|
|
|
196
421
|
return Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
|
|
197
422
|
}
|
|
198
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Clamp one line-height ratio to the allowed percent range.
|
|
426
|
+
* @param {unknown} value - candidate ratio in percent.
|
|
427
|
+
* @returns {number} the integer ratio (100 when unusable).
|
|
428
|
+
*/
|
|
429
|
+
function clampRatio(value) {
|
|
430
|
+
var ratio = Math.round(Number(value));
|
|
431
|
+
if (!isFinite(ratio)) return LINE_HEIGHT_MIN;
|
|
432
|
+
return Math.min(LINE_HEIGHT_MAX, Math.max(LINE_HEIGHT_MIN, ratio));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Clamp one code line-height offset to the allowed px range.
|
|
437
|
+
* @param {unknown} value - candidate offset in px.
|
|
438
|
+
* @returns {number} the integer offset (0 when unusable).
|
|
439
|
+
*/
|
|
440
|
+
function clampLineOffset(value) {
|
|
441
|
+
var offset = Math.round(Number(value));
|
|
442
|
+
if (!isFinite(offset) || offset === 0) return 0;
|
|
443
|
+
return Math.min(CODE_LINE_HEIGHT_MAX, Math.max(CODE_LINE_HEIGHT_MIN, offset));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Clamp one ligature mode to the three known values.
|
|
448
|
+
* @param {unknown} value - candidate mode.
|
|
449
|
+
* @returns {number} 0, 1 or 2.
|
|
450
|
+
*/
|
|
451
|
+
function clampLigatures(value) {
|
|
452
|
+
var mode = Math.round(Number(value));
|
|
453
|
+
if (!isFinite(mode) || mode <= 0) return LIGATURES_DEFAULT;
|
|
454
|
+
return mode >= LIGATURES_OFF ? LIGATURES_OFF : LIGATURES_ON;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Validate and normalize one sparse axis-value map (dark overrides, presets).
|
|
459
|
+
* Only known axis fields survive, each clamped.
|
|
460
|
+
* @param {unknown} values - candidate map.
|
|
461
|
+
* @returns {Record<string, string|number|boolean>} the safe subset.
|
|
462
|
+
*/
|
|
463
|
+
function normalizeValueSet(values) {
|
|
464
|
+
var out = {};
|
|
465
|
+
if (values === null || typeof values !== "object" || Array.isArray(values)) return out;
|
|
466
|
+
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
467
|
+
var field = VALUE_FIELDS[index];
|
|
468
|
+
if (!Object.prototype.hasOwnProperty.call(values, field)) continue;
|
|
469
|
+
out[field] = normalizeField(field, values[field]);
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Parse and validate the durable `darkValues` JSON string.
|
|
476
|
+
* @param {unknown} value - candidate JSON text or object.
|
|
477
|
+
* @returns {Record<string, string|number|boolean>} the sparse override map.
|
|
478
|
+
*/
|
|
479
|
+
function normalizeDarkValues(value) {
|
|
480
|
+
if (typeof value === "string") {
|
|
481
|
+
if (value.trim() === "") return {};
|
|
482
|
+
try {
|
|
483
|
+
value = JSON.parse(value);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
return {};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return normalizeValueSet(value);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Parse and validate the durable `presets` JSON string.
|
|
493
|
+
* @param {unknown} value - candidate JSON text or array.
|
|
494
|
+
* @returns {{name: string, values: object, savedAt: number}[]}
|
|
495
|
+
*/
|
|
496
|
+
function normalizePresets(value) {
|
|
497
|
+
var list = value;
|
|
498
|
+
if (typeof value === "string") {
|
|
499
|
+
if (value.trim() === "") return [];
|
|
500
|
+
try {
|
|
501
|
+
list = JSON.parse(value);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!Array.isArray(list)) return [];
|
|
507
|
+
var out = [];
|
|
508
|
+
for (var index = 0; index < list.length && out.length < MAX_PRESETS; index += 1) {
|
|
509
|
+
var entry = list[index];
|
|
510
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
511
|
+
var name = sanitize(entry.name).slice(0, MAX_PRESET_NAME);
|
|
512
|
+
if (name === "") continue;
|
|
513
|
+
var stamp = Math.round(Number(entry.savedAt));
|
|
514
|
+
out.push({
|
|
515
|
+
name: name,
|
|
516
|
+
values: normalizeValueSet(entry.values),
|
|
517
|
+
savedAt: isFinite(stamp) && stamp > 0 ? stamp : 0,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* ------------------------------------------------------------------ *
|
|
524
|
+
* stacks
|
|
525
|
+
* ------------------------------------------------------------------ */
|
|
526
|
+
|
|
199
527
|
/**
|
|
200
528
|
* Format a family list as a CSS font-family value.
|
|
201
529
|
* @param {readonly string[]} families - family names in precedence order.
|
|
@@ -369,6 +697,10 @@ function removeStackEntry(families, family) {
|
|
|
369
697
|
return next;
|
|
370
698
|
}
|
|
371
699
|
|
|
700
|
+
/* ------------------------------------------------------------------ *
|
|
701
|
+
* scaling helpers
|
|
702
|
+
* ------------------------------------------------------------------ */
|
|
703
|
+
|
|
372
704
|
/**
|
|
373
705
|
* Compute the uniform scale a pixel offset produces over DSH's 16px base.
|
|
374
706
|
*
|
|
@@ -389,152 +721,16 @@ function scaleFor(sizeOffset, base) {
|
|
|
389
721
|
}
|
|
390
722
|
|
|
391
723
|
/**
|
|
392
|
-
*
|
|
393
|
-
* @param {
|
|
394
|
-
* @
|
|
395
|
-
|
|
396
|
-
function isDormant(config) {
|
|
397
|
-
return (
|
|
398
|
-
config[SANS_FIELD] === "" &&
|
|
399
|
-
config[MONO_FIELD] === "" &&
|
|
400
|
-
config[SIZE_FIELD] === 0 &&
|
|
401
|
-
config[CODE_SIZE_FIELD] === 0 &&
|
|
402
|
-
config[WEIGHT_FIELD] === WEIGHT_UNSET &&
|
|
403
|
-
config[CODE_WEIGHT_FIELD] === WEIGHT_UNSET
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Build the stylesheet one configuration applies.
|
|
409
|
-
*
|
|
410
|
-
* Families are declared at their SOURCE: DSH's design tokens chain to two
|
|
411
|
-
* variables (`--dsw-font-family` for every sans token, `--ds-font-family-code`
|
|
412
|
-
* for every code token — verified against the shipped theme), so overriding
|
|
413
|
-
* those variables is what reaches the conversation markdown and the sidebar,
|
|
414
|
-
* whose elements declare `font-family: var(--dsw-font-family)` themselves and
|
|
415
|
-
* would never inherit a plain `body` rule. The explicit `body` / `pre,code`
|
|
416
|
-
* rules stay as a second path for elements that hardcode a family.
|
|
417
|
-
*
|
|
418
|
-
* The size offsets rewrite DSH's own design tokens rather than every element:
|
|
419
|
-
* a token whose name ends in `-font-size` or `-line-height` is re-declared as
|
|
420
|
-
* itself multiplied by one scale factor. `baseTokens` supplies the untouched
|
|
421
|
-
* values (read from the live document, falling back to the embedded map), so
|
|
422
|
-
* the ratio composes with DSH's own font-size slider instead of replacing it.
|
|
423
|
-
*
|
|
424
|
-
* Body and code are two independent axes. Body sizes hang off the content-size
|
|
425
|
-
* chain (`--dsh-content-font-size` and the `--dsw-font-*` steps), while every
|
|
426
|
-
* markdown code token is a bare literal (`--dsw-font-markdown-code-block:
|
|
427
|
-
* 11px/19px …`) that the content chain never touches — so the code axis is the
|
|
428
|
-
* only thing that moves it, and the body axis deliberately leaves those tokens
|
|
429
|
-
* alone. Code sizing is consumed through the `font` SHORTHAND tokens (that is
|
|
430
|
-
* what the shipped stylesheets use), which is why those are re-declared too,
|
|
431
|
-
* not just their `-font-size` / `-line-height` parts.
|
|
432
|
-
*
|
|
433
|
-
* Weight has no token chain to ride: no shipped rule reads a `-font-weight`
|
|
434
|
-
* token (measured — zero `font-weight: var(--dsw-font-…)` in rc.2), the theme
|
|
435
|
-
* writes literal weights on its own class rules, and that is exactly why the
|
|
436
|
-
* body axis has to be a blanket `!important` rule. The code axis is therefore a
|
|
437
|
-
* selector-scoped `!important` rule placed after it, which wins on specificity
|
|
438
|
-
* (`[class*=…]`) or on source order (`pre, …` ties with `body *`).
|
|
439
|
-
*
|
|
440
|
-
* @param {{sans: string, mono: string, sizeOffset: number, sizeOffsetCode: number, weight: number, weightCode: number}} config - normalized config.
|
|
441
|
-
* @param {Record<string, string>} [baseTokens] - token name to untouched value.
|
|
442
|
-
* @returns {string} declarations for one `<style>` element ("" when dormant).
|
|
724
|
+
* Round to a fixed number of decimals so generated CSS stays readable.
|
|
725
|
+
* @param {number} value - the number.
|
|
726
|
+
* @param {number} digits - decimals to keep.
|
|
727
|
+
* @returns {number} the rounded number.
|
|
443
728
|
*/
|
|
444
|
-
function
|
|
445
|
-
var
|
|
446
|
-
|
|
447
|
-
var mono = formatStack(parseStack(normalized[MONO_FIELD]));
|
|
448
|
-
var offset = normalized[SIZE_FIELD];
|
|
449
|
-
var codeOffset = normalized[CODE_SIZE_FIELD];
|
|
450
|
-
var weight = normalized[WEIGHT_FIELD];
|
|
451
|
-
var codeWeight = normalized[CODE_WEIGHT_FIELD];
|
|
452
|
-
if (isDormant(normalized)) return "";
|
|
453
|
-
var declarations = [];
|
|
454
|
-
|
|
455
|
-
if (sans !== "") {
|
|
456
|
-
declarations.push(":root,body{--dsw-font-family:" + sans + " !important}");
|
|
457
|
-
declarations.push("body{font-family:" + sans + " !important}");
|
|
458
|
-
}
|
|
459
|
-
if (mono !== "") {
|
|
460
|
-
// The theme's code tokens chain to `--ds-font-family-code`; the other name
|
|
461
|
-
// is what dsh-ui-font historically wrote and costs nothing to cover.
|
|
462
|
-
declarations.push(
|
|
463
|
-
":root,body{--dsw-font-mono:" + mono + " !important;--ds-font-family-code:" + mono + " !important}"
|
|
464
|
-
);
|
|
465
|
-
// Written after the body rule: equal specificity on a code element means
|
|
466
|
-
// the later declaration wins, so code keeps its own family inside the UI.
|
|
467
|
-
declarations.push(
|
|
468
|
-
"pre,code,kbd,samp,var,tt,textarea,.cm-editor .cm-content{font-family:" +
|
|
469
|
-
mono +
|
|
470
|
-
" !important}"
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
if (offset !== 0) {
|
|
474
|
-
var scaled = scaleTokens(bodyTokenNames(baseTokens), baseTokens, round(scaleFor(offset), 6));
|
|
475
|
-
if (scaled.length > 0) {
|
|
476
|
-
declarations.push(bodyAndDescendants(scaled.join(";")));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (codeOffset !== 0) {
|
|
480
|
-
var scaledCode = scaleCodeTokens(baseTokens, round(scaleFor(codeOffset), 6));
|
|
481
|
-
if (scaledCode.length > 0) {
|
|
482
|
-
declarations.push(bodyAndDescendants(scaledCode.join(";")));
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
if (weight !== WEIGHT_UNSET) {
|
|
486
|
-
// Written as the user chose it: a variable font honors every integer, and
|
|
487
|
-
// a static one rounds to its own nearest step by itself.
|
|
488
|
-
declarations.push(bodyAndDescendants("font-weight:" + weight + " !important"));
|
|
489
|
-
}
|
|
490
|
-
if (codeWeight !== WEIGHT_UNSET) {
|
|
491
|
-
declarations.push(CODE_SELECTOR + "{font-weight:" + codeWeight + " !important}");
|
|
492
|
-
} else if (weight !== WEIGHT_UNSET) {
|
|
493
|
-
// The body axis is a blanket `body, body *` rule, so code would inherit the
|
|
494
|
-
// body weight unless it is explicitly taken back out. `normal` is what DSH
|
|
495
|
-
// itself computes there (the code `font` shorthands carry no weight), so an
|
|
496
|
-
// unset code weight means "code stays exactly as DSH shipped it".
|
|
497
|
-
declarations.push(CODE_SELECTOR + "{font-weight:normal !important}");
|
|
498
|
-
}
|
|
499
|
-
return declarations.join("\n");
|
|
729
|
+
function round(value, digits) {
|
|
730
|
+
var factor = Math.pow(10, digits);
|
|
731
|
+
return Math.round(value * factor) / factor;
|
|
500
732
|
}
|
|
501
733
|
|
|
502
|
-
/**
|
|
503
|
-
* What counts as code text for the weight axis.
|
|
504
|
-
*
|
|
505
|
-
* There is no token to hook here (see `buildFontCss`), so the code axis has to
|
|
506
|
-
* name the elements. Two layers, both measured against rc.2:
|
|
507
|
-
*
|
|
508
|
-
* 1. The tags DSH's own stylesheets size with a code token: markdown `pre` /
|
|
509
|
-
* inline `code`, the tool card's `:where(pre)` block, form controls, and the
|
|
510
|
-
* CodeMirror editor. Their descendants are listed too, because the blanket
|
|
511
|
-
* body rule matches EVERY element — a `<span>` inside a highlighted block
|
|
512
|
-
* would otherwise be pulled back to the body weight.
|
|
513
|
-
* 2. Containers that only a class name identifies: the terminal and tool-code
|
|
514
|
-
* bodies are plain `div`s (`…_terminal`, `…_codeBody`, `md-code-block`).
|
|
515
|
-
*
|
|
516
|
-
* The `i` flag keeps a casing change in a future DSH from silently dropping the
|
|
517
|
-
* rule, and an unmatched token/class simply means that surface follows the body
|
|
518
|
-
* axis — the same degradation as an unknown token name on the size axes.
|
|
519
|
-
*/
|
|
520
|
-
var CODE_SELECTOR = [
|
|
521
|
-
"pre",
|
|
522
|
-
"code",
|
|
523
|
-
"kbd",
|
|
524
|
-
"samp",
|
|
525
|
-
"var",
|
|
526
|
-
"tt",
|
|
527
|
-
"textarea",
|
|
528
|
-
".cm-editor",
|
|
529
|
-
".dfp-previewCode",
|
|
530
|
-
'[class*="code" i]',
|
|
531
|
-
'[class*="terminal" i]',
|
|
532
|
-
]
|
|
533
|
-
.map(function (selector) {
|
|
534
|
-
return selector + "," + selector + " *";
|
|
535
|
-
})
|
|
536
|
-
.join(",");
|
|
537
|
-
|
|
538
734
|
/**
|
|
539
735
|
* Values a ratio may multiply: a number with a unit, a bare number, or an
|
|
540
736
|
* expression. A keyword (`unset`, `inherit`) or an unrecognized shape is left
|
|
@@ -552,17 +748,17 @@ function isScalableValue(value) {
|
|
|
552
748
|
}
|
|
553
749
|
|
|
554
750
|
/**
|
|
555
|
-
* Scale one set of tokens by one ratio.
|
|
751
|
+
* Scale one set of SIZE tokens by one ratio.
|
|
556
752
|
*
|
|
557
753
|
* A base that itself references another token via var() derives from it:
|
|
558
754
|
* scaling the var target already scales this one, so re-scaling here would
|
|
559
755
|
* compound (markdown-base = var(--dsh-content-font-size) would take the ratio
|
|
560
756
|
* twice). DSH's chain does the work instead.
|
|
561
757
|
*
|
|
562
|
-
* `!important` is required: the theme writes `--dsh-content-font-size` INLINE
|
|
563
|
-
* body, and an inline declaration outranks a plain stylesheet one —
|
|
564
|
-
* flag body itself would keep DSH's own size while every
|
|
565
|
-
* splitting the page in two.
|
|
758
|
+
* `!important` is required: the theme writes `--dsh-content-font-size` INLINE
|
|
759
|
+
* on body, and an inline declaration outranks a plain stylesheet one —
|
|
760
|
+
* without the flag body itself would keep DSH's own size while every
|
|
761
|
+
* descendant scales, splitting the page in two.
|
|
566
762
|
*
|
|
567
763
|
* @param {string[]} names - token names to re-declare.
|
|
568
764
|
* @param {Record<string, string>} baseTokens - token name to untouched value.
|
|
@@ -583,66 +779,143 @@ function scaleTokens(names, baseTokens, scale) {
|
|
|
583
779
|
}
|
|
584
780
|
|
|
585
781
|
/**
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* Two shapes exist and both must be covered, because the shipped stylesheets
|
|
589
|
-
* consume the shorthand (`font: var(--dsw-font-markdown-code-block-small)`)
|
|
590
|
-
* while the split tokens exist for anything that asks for a single part:
|
|
591
|
-
* `11px/19px <family>` and a bare `11px`.
|
|
782
|
+
* Scale one set of LINE-HEIGHT tokens by one factor.
|
|
592
783
|
*
|
|
784
|
+
* Unlike sizes, a line height never chains through another line height in
|
|
785
|
+
* DSH's theme (heights are literals or `calc(Npx + size-delta)`), so var()
|
|
786
|
+
* values are wrapped rather than skipped — the wrap keeps the var chain
|
|
787
|
+
* resolving at use time and cannot compound.
|
|
788
|
+
* @param {string[]} names - token names to re-declare.
|
|
593
789
|
* @param {Record<string, string>} baseTokens - token name to untouched value.
|
|
594
|
-
* @param {number}
|
|
790
|
+
* @param {number} factor - the ratio to apply.
|
|
595
791
|
* @returns {string[]} declarations.
|
|
596
792
|
*/
|
|
597
|
-
function
|
|
598
|
-
var names = codeTokenNames(baseTokens);
|
|
793
|
+
function scaleLineTokens(names, baseTokens, factor) {
|
|
599
794
|
var out = [];
|
|
600
795
|
for (var index = 0; index < names.length; index += 1) {
|
|
601
796
|
var name = names[index];
|
|
602
797
|
var base = baseTokens[name];
|
|
603
798
|
if (typeof base !== "string" || base === "") continue;
|
|
604
|
-
var shorthand = scaleFontShorthand(base, scale);
|
|
605
|
-
if (shorthand !== null) {
|
|
606
|
-
out.push(name + ":" + shorthand + " !important");
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
// A part token that derives from another token follows that token's own
|
|
610
|
-
// scaling; re-declaring it here would compound the ratio.
|
|
611
|
-
if (base.indexOf("var(") !== -1) continue;
|
|
612
799
|
if (!isScalableValue(base)) continue;
|
|
613
|
-
out.push(name + ":calc((" + base + ") * " +
|
|
800
|
+
out.push(name + ":calc((" + base + ") * " + factor + ") !important");
|
|
614
801
|
}
|
|
615
802
|
return out;
|
|
616
803
|
}
|
|
617
804
|
|
|
618
805
|
/**
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
* @param {string}
|
|
622
|
-
* @param {number}
|
|
623
|
-
* @returns {string
|
|
806
|
+
* Shift one set of tokens by an additive px amount.
|
|
807
|
+
* @param {string[]} names - token names to re-declare.
|
|
808
|
+
* @param {Record<string, string>} baseTokens - token name to untouched value.
|
|
809
|
+
* @param {number} offset - the px amount to add.
|
|
810
|
+
* @returns {string[]} declarations.
|
|
624
811
|
*/
|
|
625
|
-
function
|
|
626
|
-
var
|
|
812
|
+
function shiftTokens(names, baseTokens, offset) {
|
|
813
|
+
var out = [];
|
|
814
|
+
for (var index = 0; index < names.length; index += 1) {
|
|
815
|
+
var name = names[index];
|
|
816
|
+
var base = baseTokens[name];
|
|
817
|
+
if (typeof base !== "string" || base === "") continue;
|
|
818
|
+
if (!isScalableValue(base)) continue;
|
|
819
|
+
out.push(name + ":calc((" + base + ") + " + offset + "px) !important");
|
|
820
|
+
}
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* ------------------------------------------------------------------ *
|
|
825
|
+
* `font` shorthand parsing
|
|
826
|
+
* ------------------------------------------------------------------ */
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* The family reference every DSH `font` shorthand ends with. Markdown tokens
|
|
830
|
+
* chain to the sans variable, code tokens to the code variable.
|
|
831
|
+
*/
|
|
832
|
+
var FAMILY_REFS = ["var(--dsw-font-family)", "var(--ds-font-family-code)"];
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Parse one `font` shorthand into its parts.
|
|
836
|
+
*
|
|
837
|
+
* DSH's shorthands come in four shapes, all ending in a family var():
|
|
838
|
+
* `11px/19px var(--ds-font-family-code)` (code),
|
|
839
|
+
* `var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)` (base),
|
|
840
|
+
* `700 calc(21px + var(...)) / calc(30px + var(...)) var(--dsw-font-family)` (h1),
|
|
841
|
+
* `italic 600 var(...) / calc(...) var(--dsw-font-family)` (strong-italic).
|
|
842
|
+
*
|
|
843
|
+
* Because the size expression may itself contain `var(...)`, the split keys
|
|
844
|
+
* off the family reference (found from the right) and the `/`, never off
|
|
845
|
+
* whitespace inside `calc()`.
|
|
846
|
+
* @param {string} value - a `font` shorthand value.
|
|
847
|
+
* @returns {{lead: string, size: string, height: string, family: string}|null} the parts, or null when the shape differs.
|
|
848
|
+
*/
|
|
849
|
+
function parseShorthand(value) {
|
|
850
|
+
if (typeof value !== "string") return null;
|
|
851
|
+
var familyAt = -1;
|
|
852
|
+
for (var index = 0; index < FAMILY_REFS.length; index += 1) {
|
|
853
|
+
var at = value.lastIndexOf(FAMILY_REFS[index]);
|
|
854
|
+
if (at > familyAt) familyAt = at;
|
|
855
|
+
}
|
|
856
|
+
if (familyAt <= 0) return null;
|
|
857
|
+
var family = value.slice(familyAt).trim();
|
|
858
|
+
if (family === "") return null;
|
|
859
|
+
var head = value.slice(0, familyAt).trim();
|
|
860
|
+
var slash = head.lastIndexOf("/");
|
|
627
861
|
if (slash <= 0) return null;
|
|
628
|
-
var
|
|
629
|
-
var
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
var
|
|
633
|
-
var
|
|
634
|
-
if (size === "" || height === "" || family === "") return null;
|
|
862
|
+
var sizeChunk = head.slice(0, slash).trim();
|
|
863
|
+
var height = head.slice(slash + 1).trim();
|
|
864
|
+
if (sizeChunk === "" || height === "") return null;
|
|
865
|
+
var chunks = splitTopLevel(sizeChunk);
|
|
866
|
+
var size = chunks.pop();
|
|
867
|
+
var lead = chunks.join(" ");
|
|
635
868
|
if (!isScalableValue(size) || !isScalableValue(height)) return null;
|
|
636
|
-
return
|
|
637
|
-
|
|
638
|
-
|
|
869
|
+
return { lead: lead, size: size, height: height, family: family };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Split a value on whitespace that sits OUTSIDE any parentheses, so a leading
|
|
874
|
+
* `700 calc(21px + var(…))` yields `["700", "calc(21px + var(...))"]`.
|
|
875
|
+
* @param {string} value - the chunk to split.
|
|
876
|
+
* @returns {string[]} the top-level chunks.
|
|
877
|
+
*/
|
|
878
|
+
function splitTopLevel(value) {
|
|
879
|
+
var out = [];
|
|
880
|
+
var current = "";
|
|
881
|
+
var depth = 0;
|
|
882
|
+
for (var index = 0; index < value.length; index += 1) {
|
|
883
|
+
var char = value.charAt(index);
|
|
884
|
+
if (char === "(") depth += 1;
|
|
885
|
+
else if (char === ")") depth -= 1;
|
|
886
|
+
if (depth === 0 && /\s/.test(char)) {
|
|
887
|
+
if (current !== "") {
|
|
888
|
+
out.push(current);
|
|
889
|
+
current = "";
|
|
890
|
+
}
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
current += char;
|
|
894
|
+
}
|
|
895
|
+
if (current !== "") out.push(current);
|
|
896
|
+
return out;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Rebuild one `font` shorthand from parsed parts.
|
|
901
|
+
* @param {{lead: string, size: string, height: string, family: string}} parts - parsed parts.
|
|
902
|
+
* @returns {string} the shorthand value (without `!important`).
|
|
903
|
+
*/
|
|
904
|
+
function rebuildShorthand(parts) {
|
|
905
|
+
var out = "";
|
|
906
|
+
if (parts.lead !== "") out += parts.lead + " ";
|
|
907
|
+
return out + parts.size + " / " + parts.height + " " + parts.family;
|
|
639
908
|
}
|
|
640
909
|
|
|
910
|
+
/* ------------------------------------------------------------------ *
|
|
911
|
+
* token classification
|
|
912
|
+
* ------------------------------------------------------------------ */
|
|
913
|
+
|
|
641
914
|
/**
|
|
642
|
-
* Match DSH's typography tokens: the design system's sizes plus the
|
|
643
|
-
* size the theme plugin writes on `body` (whose secondary variant
|
|
644
|
-
* suffix of its own). Line heights scale with sizes so a token's
|
|
645
|
-
* never disagrees with its parts.
|
|
915
|
+
* Match DSH's typography PART tokens: the design system's sizes plus the
|
|
916
|
+
* content size the theme plugin writes on `body` (whose secondary variant
|
|
917
|
+
* carries a suffix of its own). Line heights scale with sizes so a token's
|
|
918
|
+
* shorthand never disagrees with its parts.
|
|
646
919
|
*
|
|
647
920
|
* Deliberately a shape test rather than an enumeration: DSH generates these
|
|
648
921
|
* tokens at runtime, so whatever a future release names them, a token that
|
|
@@ -650,6 +923,26 @@ function scaleFontShorthand(value, scale) {
|
|
|
650
923
|
*/
|
|
651
924
|
var TOKEN_PATTERN = /^--(?:dsw-font-[a-z0-9-]*|dsh-content-font)-(?:font-size|line-height)(?:-secondary)?$|^--dsh-content-font-(?:size|line-height)(?:-secondary)?$/;
|
|
652
925
|
|
|
926
|
+
/**
|
|
927
|
+
* The content-size DELTA tokens the theme derives on `body`; the dialog size
|
|
928
|
+
* offset shifts these additively, which is what moves every delta-derived
|
|
929
|
+
* markdown size by exactly the offset.
|
|
930
|
+
*/
|
|
931
|
+
var DELTA_PATTERN = /^--dsh-content-font-delta(?:-secondary)?$/;
|
|
932
|
+
|
|
933
|
+
/** The whole official content-size chain (size / line height / delta). */
|
|
934
|
+
var CONTENT_PATTERN = /^--dsh-content-font-(?:size|line-height|delta)(?:-secondary)?$/;
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* The PART-token suffixes DSH appends for single-axis consumers.
|
|
938
|
+
*/
|
|
939
|
+
var PART_SUFFIXES = ["-font-size", "-line-height", "-font-family", "-font-weight", "-font-style"];
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* The bare family variables, which are NOT `font` shorthands.
|
|
943
|
+
*/
|
|
944
|
+
var FAMILY_VARS = { "--dsw-font-family": true, "--dsw-font-mono": true, "--ds-font-family-code": true };
|
|
945
|
+
|
|
653
946
|
/**
|
|
654
947
|
* A `body, body *` selector list, kept in one place because every global
|
|
655
948
|
* declaration needs the same reach (DSH writes some tokens on descendants).
|
|
@@ -661,22 +954,46 @@ function bodyAndDescendants(declarations) {
|
|
|
661
954
|
}
|
|
662
955
|
|
|
663
956
|
/**
|
|
664
|
-
* Whether one
|
|
957
|
+
* Whether one name belongs to the official CONTENT-size chain the conversation
|
|
958
|
+
* owns: `--dsh-content-font-size` / `-line-height` (the official "对话字号"
|
|
959
|
+
* setting writes the former) and the delta tokens derived from it.
|
|
960
|
+
*
|
|
961
|
+
* The interface axis must never scale these. Doing so was the real reason an
|
|
962
|
+
* interface size change moved the conversation: the markdown ladder consumes
|
|
963
|
+
* this chain directly, so scaling it is a conversation edit no matter which
|
|
964
|
+
* slider caused it.
|
|
665
965
|
* @param {string} name - custom property name including the leading dashes.
|
|
666
|
-
* @returns {boolean}
|
|
966
|
+
* @returns {boolean}
|
|
967
|
+
*/
|
|
968
|
+
function isContentToken(name) {
|
|
969
|
+
return typeof name === "string" && CONTENT_PATTERN.test(name);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Whether one custom-property name is a PART token this plugin scales.
|
|
974
|
+
* @param {string} name - custom property name including the leading dashes.
|
|
975
|
+
* @returns {boolean} true when the token carries a size or a line height.
|
|
667
976
|
*/
|
|
668
977
|
function isFontToken(name) {
|
|
669
978
|
return typeof name === "string" && TOKEN_PATTERN.test(name);
|
|
670
979
|
}
|
|
671
980
|
|
|
672
981
|
/**
|
|
673
|
-
*
|
|
982
|
+
* Whether one name is a content-size delta token.
|
|
983
|
+
* @param {string} name - custom property name including the leading dashes.
|
|
984
|
+
* @returns {boolean}
|
|
985
|
+
*/
|
|
986
|
+
function isDeltaToken(name) {
|
|
987
|
+
return typeof name === "string" && DELTA_PATTERN.test(name);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Whether one name is a markdown CODE token: its own sizing axis.
|
|
674
992
|
*
|
|
675
993
|
* The theme declares these as literals the content-size chain never reaches
|
|
676
994
|
* (`--dsw-font-markdown-code:12px/19px …`, `-code-block:11px/19px …`,
|
|
677
995
|
* `-code-block-small:11px/16px …`), and the shipped stylesheets consume them
|
|
678
|
-
* through the `font` shorthand.
|
|
679
|
-
* to the code offset and to nothing else.
|
|
996
|
+
* through the `font` shorthand.
|
|
680
997
|
* @param {string} name - custom property name including the leading dashes.
|
|
681
998
|
* @returns {boolean} true when the token sizes code.
|
|
682
999
|
*/
|
|
@@ -685,23 +1002,63 @@ function isCodeToken(name) {
|
|
|
685
1002
|
}
|
|
686
1003
|
|
|
687
1004
|
/**
|
|
688
|
-
* Whether a token is
|
|
1005
|
+
* Whether one name is a markdown (dialog) token that is NOT code: the token
|
|
1006
|
+
* family the dialog axes own.
|
|
1007
|
+
* @param {string} name - custom property name including the leading dashes.
|
|
1008
|
+
* @returns {boolean}
|
|
1009
|
+
*/
|
|
1010
|
+
function isMarkdownToken(name) {
|
|
1011
|
+
return typeof name === "string" && name.indexOf("--dsw-font-markdown-") === 0 && !isCodeToken(name);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Whether one NAME looks like a `font` shorthand token (a bare
|
|
1016
|
+
* `--dsw-font-…` name that is not a PART token or a family variable).
|
|
1017
|
+
* @param {string} name - custom property name including the leading dashes.
|
|
1018
|
+
* @returns {boolean}
|
|
1019
|
+
*/
|
|
1020
|
+
function isShorthandName(name) {
|
|
1021
|
+
if (typeof name !== "string" || name.indexOf("--dsw-font-") !== 0) return false;
|
|
1022
|
+
if (FAMILY_VARS[name] === true) return false;
|
|
1023
|
+
for (var index = 0; index < PART_SUFFIXES.length; index += 1) {
|
|
1024
|
+
var suffix = PART_SUFFIXES[index];
|
|
1025
|
+
if (name.length > suffix.length && name.slice(-suffix.length) === suffix) return false;
|
|
1026
|
+
}
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Whether one token (name + value) is a `font` shorthand this plugin may
|
|
1032
|
+
* rewrite: a bare `--dsw-font-…` name whose value parses as a shorthand.
|
|
1033
|
+
* @param {string} name - custom property name.
|
|
1034
|
+
* @param {string} value - the token's untouched value.
|
|
1035
|
+
* @returns {boolean}
|
|
1036
|
+
*/
|
|
1037
|
+
function isShorthandToken(name, value) {
|
|
1038
|
+
return isShorthandName(name) && parseShorthand(value) !== null;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Whether a token is one this plugin reads and re-declares at all.
|
|
689
1043
|
* @param {string} name - custom property name including the leading dashes.
|
|
690
|
-
* @returns {boolean} true for
|
|
1044
|
+
* @returns {boolean} true for part tokens, code tokens, shorthand names and
|
|
1045
|
+
* the content-size deltas.
|
|
691
1046
|
*/
|
|
692
1047
|
function isScaledToken(name) {
|
|
693
|
-
return isFontToken(name) || isCodeToken(name);
|
|
1048
|
+
return isFontToken(name) || isCodeToken(name) || isShorthandName(name) || isDeltaToken(name);
|
|
694
1049
|
}
|
|
695
1050
|
|
|
696
1051
|
/**
|
|
697
|
-
* Ordered token names present in a base map, sizes before line heights
|
|
698
|
-
* reader of the stylesheet sees each size next to its height.
|
|
1052
|
+
* Ordered PART-token names present in a base map, sizes before line heights
|
|
1053
|
+
* so a reader of the stylesheet sees each size next to its height.
|
|
699
1054
|
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
700
|
-
* @returns {string[]} the names
|
|
1055
|
+
* @returns {string[]} the names.
|
|
701
1056
|
*/
|
|
702
1057
|
function tokenNames(baseTokens) {
|
|
703
1058
|
if (baseTokens === null || typeof baseTokens !== "object") return [];
|
|
704
|
-
var names = Object.keys(baseTokens).filter(
|
|
1059
|
+
var names = Object.keys(baseTokens).filter(function (name) {
|
|
1060
|
+
return isFontToken(name) || isCodeToken(name) || isDeltaToken(name);
|
|
1061
|
+
});
|
|
705
1062
|
names.sort(function (left, right) {
|
|
706
1063
|
if (left.length !== right.length) return left.length - right.length;
|
|
707
1064
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -710,102 +1067,848 @@ function tokenNames(baseTokens) {
|
|
|
710
1067
|
}
|
|
711
1068
|
|
|
712
1069
|
/**
|
|
713
|
-
* The
|
|
714
|
-
*
|
|
1070
|
+
* The interface PART size tokens: everything scalable except the code and
|
|
1071
|
+
* markdown families and the delta tokens (which the dialog offset shifts
|
|
1072
|
+
* additively, never multiplicatively).
|
|
715
1073
|
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
716
|
-
* @returns {string[]} the names the
|
|
1074
|
+
* @returns {string[]} the names the interface offset re-declares.
|
|
717
1075
|
*/
|
|
718
1076
|
function bodyTokenNames(baseTokens) {
|
|
719
1077
|
return tokenNames(baseTokens).filter(function (name) {
|
|
720
|
-
return
|
|
1078
|
+
return (
|
|
1079
|
+
!isCodeToken(name) &&
|
|
1080
|
+
!isMarkdownToken(name) &&
|
|
1081
|
+
!isDeltaToken(name) &&
|
|
1082
|
+
!isContentToken(name)
|
|
1083
|
+
);
|
|
721
1084
|
});
|
|
722
1085
|
}
|
|
723
1086
|
|
|
724
1087
|
/**
|
|
725
|
-
* The
|
|
726
|
-
*
|
|
1088
|
+
* The interface line-height PART tokens (non-code, non-markdown).
|
|
1089
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1090
|
+
* @returns {string[]}
|
|
1091
|
+
*/
|
|
1092
|
+
function uiLineTokenNames(baseTokens) {
|
|
1093
|
+
return bodyTokenNames(baseTokens).filter(function (name) {
|
|
1094
|
+
return name.indexOf("-line-height") >= 0;
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* The markdown (dialog) PART tokens: sizes and line heights of the markdown
|
|
1100
|
+
* family, excluding code.
|
|
1101
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1102
|
+
* @returns {string[]}
|
|
1103
|
+
*/
|
|
1104
|
+
function markdownTokenNames(baseTokens) {
|
|
1105
|
+
return tokenNames(baseTokens).filter(isMarkdownToken);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* The markdown content-size DELTA tokens present in a base map.
|
|
1110
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1111
|
+
* @returns {string[]}
|
|
1112
|
+
*/
|
|
1113
|
+
function deltaTokenNames(baseTokens) {
|
|
1114
|
+
if (baseTokens === null || typeof baseTokens !== "object") return [];
|
|
1115
|
+
return Object.keys(baseTokens).filter(isDeltaToken);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* The code PART tokens: the `font` shorthands' size/height parts, when the
|
|
1120
|
+
* live stylesheets declare them. The shorthands themselves are handled by
|
|
1121
|
+
* `codeShorthandTokens`, so they are excluded here to avoid double
|
|
1122
|
+
* declarations.
|
|
727
1123
|
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
728
|
-
* @returns {string[]}
|
|
1124
|
+
* @returns {string[]}
|
|
729
1125
|
*/
|
|
730
1126
|
function codeTokenNames(baseTokens) {
|
|
731
|
-
return tokenNames(baseTokens).filter(
|
|
1127
|
+
return tokenNames(baseTokens).filter(function (name) {
|
|
1128
|
+
return isCodeToken(name) && !isShorthandName(name);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Every `font` shorthand token in a base map that parses, sorted by name
|
|
1134
|
+
* length (families before variants, mirroring the part-token order).
|
|
1135
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1136
|
+
* @returns {{name: string, parts: object}[]}
|
|
1137
|
+
*/
|
|
1138
|
+
function shorthandTokens(baseTokens) {
|
|
1139
|
+
if (baseTokens === null || typeof baseTokens !== "object") return [];
|
|
1140
|
+
var out = [];
|
|
1141
|
+
var names = Object.keys(baseTokens).filter(isShorthandName);
|
|
1142
|
+
names.sort(function (left, right) {
|
|
1143
|
+
if (left.length !== right.length) return left.length - right.length;
|
|
1144
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1145
|
+
});
|
|
1146
|
+
for (var index = 0; index < names.length; index += 1) {
|
|
1147
|
+
var parts = parseShorthand(baseTokens[names[index]]);
|
|
1148
|
+
if (parts !== null) out.push({ name: names[index], parts: parts });
|
|
1149
|
+
}
|
|
1150
|
+
return out;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* The markdown (dialog) `font` shorthand tokens, excluding code.
|
|
1155
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1156
|
+
* @returns {{name: string, parts: object}[]}
|
|
1157
|
+
*/
|
|
1158
|
+
function markdownShorthandTokens(baseTokens) {
|
|
1159
|
+
return shorthandTokens(baseTokens).filter(function (entry) {
|
|
1160
|
+
return isMarkdownToken(entry.name);
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* The interface (non-markdown, non-code) `font` shorthand tokens.
|
|
1166
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1167
|
+
* @returns {{name: string, parts: object}[]}
|
|
1168
|
+
*/
|
|
1169
|
+
function uiShorthandTokens(baseTokens) {
|
|
1170
|
+
return shorthandTokens(baseTokens).filter(function (entry) {
|
|
1171
|
+
return !isMarkdownToken(entry.name) && !isCodeToken(entry.name);
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* The code `font` shorthand tokens.
|
|
1177
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1178
|
+
* @returns {{name: string, parts: object}[]}
|
|
1179
|
+
*/
|
|
1180
|
+
function codeShorthandTokens(baseTokens) {
|
|
1181
|
+
return shorthandTokens(baseTokens).filter(function (entry) {
|
|
1182
|
+
return isCodeToken(entry.name);
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/* ------------------------------------------------------------------ *
|
|
1187
|
+
* selectors
|
|
1188
|
+
* ------------------------------------------------------------------ */
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* What counts as code text for the code axes.
|
|
1192
|
+
*
|
|
1193
|
+
* There is no token to hook here (no shipped rule reads a `-font-weight`
|
|
1194
|
+
* token — measured, zero `font-weight: var(--dsw-font-…)` in rc.2), so the
|
|
1195
|
+
* code axes have to name the elements. Two layers, both measured against
|
|
1196
|
+
* rc.2:
|
|
1197
|
+
*
|
|
1198
|
+
* 1. The tags DSH's own stylesheets size with a code token: markdown `pre` /
|
|
1199
|
+
* inline `code`, the tool card's `:where(pre)` block, form controls, and
|
|
1200
|
+
* CodeMirror-like editors. Their descendants are listed too, because the
|
|
1201
|
+
* blanket body rule matches EVERY element — a `<span>` inside a
|
|
1202
|
+
* highlighted block would otherwise be pulled back to the body weight.
|
|
1203
|
+
* 2. Containers that only a class name identifies: the terminal and tool-code
|
|
1204
|
+
* bodies are plain `div`s (`…_terminal`, `…_codeBody`, `md-code-block`).
|
|
1205
|
+
*
|
|
1206
|
+
* The `i` flag keeps a casing change in a future DSH from silently dropping
|
|
1207
|
+
* the rule, and an unmatched token/class simply means that surface follows
|
|
1208
|
+
* the interface axis — the same degradation as an unknown token name.
|
|
1209
|
+
*/
|
|
1210
|
+
var CODE_SELECTOR = [
|
|
1211
|
+
"pre",
|
|
1212
|
+
"code",
|
|
1213
|
+
"kbd",
|
|
1214
|
+
"samp",
|
|
1215
|
+
"var",
|
|
1216
|
+
"tt",
|
|
1217
|
+
"textarea",
|
|
1218
|
+
".cm-editor",
|
|
1219
|
+
".dfp-previewCode",
|
|
1220
|
+
'[class*="code" i]',
|
|
1221
|
+
'[class*="terminal" i]',
|
|
1222
|
+
]
|
|
1223
|
+
.map(function (selector) {
|
|
1224
|
+
return selector + "," + selector + " *";
|
|
1225
|
+
})
|
|
1226
|
+
.join(",");
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Everything a code surface must be excluded from a DIALOG-scoped rule:
|
|
1230
|
+
* the code elements themselves AND their descendants (a highlight `<span>`
|
|
1231
|
+
* inside a `<pre>` is not a `pre`, so the exclusion has to reach through
|
|
1232
|
+
* ancestry with complex `:not()` arguments — supported by the Chromium line
|
|
1233
|
+
* DSH targets).
|
|
1234
|
+
*/
|
|
1235
|
+
var CODE_EXCLUDES = [
|
|
1236
|
+
"pre",
|
|
1237
|
+
"pre *",
|
|
1238
|
+
"code",
|
|
1239
|
+
"code *",
|
|
1240
|
+
"kbd",
|
|
1241
|
+
"kbd *",
|
|
1242
|
+
"samp",
|
|
1243
|
+
"samp *",
|
|
1244
|
+
"var",
|
|
1245
|
+
"var *",
|
|
1246
|
+
"tt",
|
|
1247
|
+
"tt *",
|
|
1248
|
+
"textarea",
|
|
1249
|
+
"textarea *",
|
|
1250
|
+
'[class*="code" i]',
|
|
1251
|
+
'[class*="code" i] *',
|
|
1252
|
+
'[class*="terminal" i]',
|
|
1253
|
+
'[class*="terminal" i] *',
|
|
1254
|
+
]
|
|
1255
|
+
.map(function (selector) {
|
|
1256
|
+
return ":not(" + selector + ")";
|
|
1257
|
+
})
|
|
1258
|
+
.join("");
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* The dialog surface: DSH renders the conversation markdown in a CSS-module
|
|
1262
|
+
* container whose class hashes per release (`_markdown_<hash>_…`). A
|
|
1263
|
+
* class-substring match degrades safely — no match means the dialog follows
|
|
1264
|
+
* the interface axis, the same behavior as an unknown token name.
|
|
1265
|
+
*/
|
|
1266
|
+
var MARKDOWN_SELECTOR =
|
|
1267
|
+
'[class*="_markdown_" i]' +
|
|
1268
|
+
CODE_EXCLUDES +
|
|
1269
|
+
',[class*="_markdown_" i] *' +
|
|
1270
|
+
CODE_EXCLUDES;
|
|
1271
|
+
|
|
1272
|
+
/** The dark-theme attribute DSH's ThemePresenter writes on body. */
|
|
1273
|
+
var DARK_ATTR = "body[data-ds-dark-theme]";
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Everything the INTERFACE weight rule must stay out of.
|
|
1277
|
+
*
|
|
1278
|
+
* A weight can only be expressed as a `body, body *` rule: DSH pins its labels
|
|
1279
|
+
* with literal weights on class rules (`…_label{font-weight:500}`,
|
|
1280
|
+
* `…_heading{font-weight:600}`) that nothing narrower reaches. That rule would
|
|
1281
|
+
* otherwise cover the conversation too, so the conversation markdown subtree is
|
|
1282
|
+
* excluded here — and with it every code surface, because the code axis owns
|
|
1283
|
+
* those and the two rules would otherwise fight over the same elements (the
|
|
1284
|
+
* `:not()` arguments raise this rule's specificity above a single class).
|
|
1285
|
+
*
|
|
1286
|
+
* `dfp-previewDialog` is the card's conversation preview: it carries the
|
|
1287
|
+
* conversation's own weight, so the interface rule must not flatten it. The
|
|
1288
|
+
* interface's own preview stays inside the rule, and `dfp-previewCode` is a
|
|
1289
|
+
* code surface like any other.
|
|
1290
|
+
*/
|
|
1291
|
+
var INTERFACE_EXCLUDES =
|
|
1292
|
+
CODE_EXCLUDES +
|
|
1293
|
+
":not(.cm-editor):not(.cm-editor *):not(.dfp-previewCode):not(.dfp-previewCode *)" +
|
|
1294
|
+
':not([class*="_markdown_" i]):not([class*="_markdown_" i] *)' +
|
|
1295
|
+
":not(.dfp-previewDialog):not(.dfp-previewDialog *)";
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Prefix every selector in a comma list, for the per-theme rules.
|
|
1299
|
+
* @param {string} selectorList - a comma-separated selector list.
|
|
1300
|
+
* @param {string} prefix - the prefix to prepend to each selector.
|
|
1301
|
+
* @returns {string} the prefixed list.
|
|
1302
|
+
*/
|
|
1303
|
+
function prefixSelector(selectorList, prefix) {
|
|
1304
|
+
var parts = selectorList.split(",");
|
|
1305
|
+
var out = [];
|
|
1306
|
+
for (var index = 0; index < parts.length; index += 1) {
|
|
1307
|
+
out.push(prefix + " " + parts[index]);
|
|
1308
|
+
}
|
|
1309
|
+
return out.join(",");
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/* ------------------------------------------------------------------ *
|
|
1313
|
+
* per-theme resolution
|
|
1314
|
+
* ------------------------------------------------------------------ */
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Expand the FOLLOW semantics over one normalized configuration.
|
|
1318
|
+
*
|
|
1319
|
+
* The conversation owns every axis. Follow — the default — makes the interface
|
|
1320
|
+
* take the conversation's family and weight, keeping its own value only for an
|
|
1321
|
+
* axis the conversation leaves unset. Follow off leaves both of the interface's
|
|
1322
|
+
* axes standing on their own; the conversation is never touched either way,
|
|
1323
|
+
* because the interface's rules exclude the markdown subtree.
|
|
1324
|
+
*
|
|
1325
|
+
* The interface's size and line-height axes are retired and render nothing, so
|
|
1326
|
+
* nothing follows for them (see `buildAxisCss` for the measurement).
|
|
1327
|
+
*
|
|
1328
|
+
* @param {Record<string, unknown>} raw - a normalized configuration.
|
|
1329
|
+
* @returns {Record<string, unknown>} the set with the follow rule applied.
|
|
1330
|
+
*/
|
|
1331
|
+
function expandFollow(raw) {
|
|
1332
|
+
var out = {};
|
|
1333
|
+
for (var field in raw) {
|
|
1334
|
+
if (Object.prototype.hasOwnProperty.call(raw, field)) out[field] = raw[field];
|
|
1335
|
+
}
|
|
1336
|
+
// Migration bridge, not a live rule: a document written before 0.2.0 carries
|
|
1337
|
+
// the family on the interface only, so an empty conversation family takes it.
|
|
1338
|
+
// The card never describes an empty field as "follows the interface".
|
|
1339
|
+
out[STACK_DIALOG_FIELD] =
|
|
1340
|
+
raw[STACK_DIALOG_FIELD] !== "" ? raw[STACK_DIALOG_FIELD] : raw[SANS_FIELD];
|
|
1341
|
+
out[WEIGHT_DIALOG_FIELD] = raw[WEIGHT_DIALOG_FIELD];
|
|
1342
|
+
if (raw[UI_FOLLOWS_FIELD] !== false) {
|
|
1343
|
+
out[SANS_FIELD] = out[STACK_DIALOG_FIELD];
|
|
1344
|
+
out[WEIGHT_FIELD] =
|
|
1345
|
+
raw[WEIGHT_DIALOG_FIELD] !== WEIGHT_UNSET ? raw[WEIGHT_DIALOG_FIELD] : raw[WEIGHT_FIELD];
|
|
1346
|
+
}
|
|
1347
|
+
return out;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Resolve the effective light and dark axis sets of one configuration.
|
|
1352
|
+
*
|
|
1353
|
+
* With `perTheme` off the dark set IS the light set; with it on, the sparse
|
|
1354
|
+
* `darkValues` map is applied over the single set first and follow-expansion
|
|
1355
|
+
* runs on the result, so a dark dialog value still tracks the dark interface
|
|
1356
|
+
* value when the interface itself is overridden per theme.
|
|
1357
|
+
* @param {unknown} config - candidate configuration.
|
|
1358
|
+
* @returns {{light: object, dark: object}} the two axis sets.
|
|
1359
|
+
*/
|
|
1360
|
+
function resolveAxes(config) {
|
|
1361
|
+
var raw = normalizeConfig(config);
|
|
1362
|
+
var light = expandFollow(raw);
|
|
1363
|
+
if (raw[PER_THEME_FIELD] !== true) return { light: light, dark: light };
|
|
1364
|
+
// The dark overrides are applied to the RAW values and only then expanded: an
|
|
1365
|
+
// already-expanded set has the follow-borrowed values baked in, so expanding
|
|
1366
|
+
// it again would let a borrowed value shadow the dark override.
|
|
1367
|
+
var merged = {};
|
|
1368
|
+
for (var field in raw) {
|
|
1369
|
+
if (Object.prototype.hasOwnProperty.call(raw, field)) merged[field] = raw[field];
|
|
1370
|
+
}
|
|
1371
|
+
var overrides = raw[DARK_VALUES_FIELD];
|
|
1372
|
+
for (var key in overrides) {
|
|
1373
|
+
if (Object.prototype.hasOwnProperty.call(overrides, key)) merged[key] = overrides[key];
|
|
1374
|
+
}
|
|
1375
|
+
return { light: light, dark: expandFollow(merged) };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/* ------------------------------------------------------------------ *
|
|
1379
|
+
* the stylesheet builder
|
|
1380
|
+
* ------------------------------------------------------------------ */
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Whether a normalized configuration asks for any change at all.
|
|
1384
|
+
* @param {unknown} config - candidate configuration.
|
|
1385
|
+
* @returns {boolean} true when nothing should be injected.
|
|
1386
|
+
*/
|
|
1387
|
+
function isDormant(config) {
|
|
1388
|
+
var axis = config !== null && typeof config === "object" ? config : {};
|
|
1389
|
+
if (axis[PER_THEME_FIELD] === true) {
|
|
1390
|
+
var dark = axis[DARK_VALUES_FIELD];
|
|
1391
|
+
if (dark !== null && typeof dark === "object") {
|
|
1392
|
+
for (var field in dark) {
|
|
1393
|
+
if (Object.prototype.hasOwnProperty.call(dark, field)) return false;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return (
|
|
1398
|
+
// The retired interface size and line-height axes are deliberately absent:
|
|
1399
|
+
// a configuration that only sets them renders nothing, so it is dormant like
|
|
1400
|
+
// an empty one. The interface WEIGHT is not retired — it renders a blanket
|
|
1401
|
+
// rule — so it counts.
|
|
1402
|
+
sanitize(axis[SANS_FIELD]) === "" &&
|
|
1403
|
+
sanitize(axis[STACK_DIALOG_FIELD]) === "" &&
|
|
1404
|
+
sanitize(axis[MONO_FIELD]) === "" &&
|
|
1405
|
+
clampOffset(axis[SIZE_DIALOG_FIELD]) === 0 &&
|
|
1406
|
+
clampOffset(axis[CODE_SIZE_FIELD]) === 0 &&
|
|
1407
|
+
clampWeight(axis[WEIGHT_FIELD]) === WEIGHT_UNSET &&
|
|
1408
|
+
clampWeight(axis[WEIGHT_DIALOG_FIELD]) === WEIGHT_UNSET &&
|
|
1409
|
+
clampWeight(axis[CODE_WEIGHT_FIELD]) === WEIGHT_UNSET &&
|
|
1410
|
+
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
1411
|
+
clampLineOffset(axis[CODE_LINE_HEIGHT_FIELD]) === 0 &&
|
|
1412
|
+
clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
|
|
1413
|
+
sanitizeFeatures(axis[FEATURES_FIELD]) === "" &&
|
|
1414
|
+
toBool(axis[NO_SYNTHETIC_ITALIC_FIELD]) === false &&
|
|
1415
|
+
toBool(axis[NO_SYNTHETIC_BOLD_FIELD]) === false
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Build the stylesheet one THEME VALUE SET applies.
|
|
1421
|
+
*
|
|
1422
|
+
* The dialog axes reach the conversation markdown through a scope rule keyed
|
|
1423
|
+
* on the markdown container (class-substring match) rather than through the
|
|
1424
|
+
* design tokens: the tokens' `font` shorthands inline their parts, so a
|
|
1425
|
+
* token-level dialog override would need a full shorthand rebuild, while a
|
|
1426
|
+
* scoped `!important` rule wins over every non-important declaration the
|
|
1427
|
+
* theme ships and degrades to "follows the interface" when nothing matches.
|
|
1428
|
+
* Code surfaces are excluded from the dialog scope by construction (see
|
|
1429
|
+
* `CODE_EXCLUDES`), so inline code inside a serif conversation stays mono.
|
|
1430
|
+
*
|
|
1431
|
+
* @param {Record<string, unknown>} axis - an expanded (follow-applied) value set.
|
|
1432
|
+
* @param {Record<string, string>} baseTokens - token name to untouched value.
|
|
1433
|
+
* @param {boolean} isDark - prefix every rule with the dark-theme attribute.
|
|
1434
|
+
* @returns {string} declarations for one `<style>` element.
|
|
1435
|
+
*/
|
|
1436
|
+
function buildAxisCss(axis, baseTokens, isDark) {
|
|
1437
|
+
var sans = formatStack(parseStack(axis[SANS_FIELD]));
|
|
1438
|
+
var mono = formatStack(parseStack(axis[MONO_FIELD]));
|
|
1439
|
+
var declarations = [];
|
|
1440
|
+
|
|
1441
|
+
function blanket(declarations, excludes) {
|
|
1442
|
+
// The exclusion reaches the descendants only: `body` itself inherits
|
|
1443
|
+
// nothing, and neither the body element nor its dark attribute can be a
|
|
1444
|
+
// markdown container or a code surface.
|
|
1445
|
+
var suffix = excludes === undefined ? "" : excludes;
|
|
1446
|
+
var selector = isDark
|
|
1447
|
+
? DARK_ATTR + "," + DARK_ATTR + " *" + suffix
|
|
1448
|
+
: "body,body *" + suffix;
|
|
1449
|
+
return selector + "{" + declarations + "}";
|
|
1450
|
+
}
|
|
1451
|
+
function codeRule(declarations) {
|
|
1452
|
+
var selector = isDark ? prefixSelector(CODE_SELECTOR, DARK_ATTR) : CODE_SELECTOR;
|
|
1453
|
+
return selector + "{" + declarations + "}";
|
|
1454
|
+
}
|
|
1455
|
+
function dialogRule(declarations) {
|
|
1456
|
+
var selector = isDark ? prefixSelector(MARKDOWN_SELECTOR, DARK_ATTR) : MARKDOWN_SELECTOR;
|
|
1457
|
+
return selector + "{" + declarations + "}";
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Re-assert the code family inside the dialog: scoped under the markdown
|
|
1461
|
+
* container, after the dialog rule and above it in specificity, so a
|
|
1462
|
+
* serif conversation keeps monospaced code.
|
|
1463
|
+
*/
|
|
1464
|
+
function pushScopedCodeFamily() {
|
|
1465
|
+
var scopedCode = [];
|
|
1466
|
+
var surfaces = CODE_SELECTOR.split(",");
|
|
1467
|
+
for (var cIndex = 0; cIndex < surfaces.length; cIndex += 1) {
|
|
1468
|
+
scopedCode.push('[class*="_markdown_" i] ' + surfaces[cIndex]);
|
|
1469
|
+
}
|
|
1470
|
+
declarations.push(scopedCode.join(",") + "{font-family:" + mono + " !important}");
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// ---- families ----
|
|
1474
|
+
if (sans !== "") {
|
|
1475
|
+
declarations.push(
|
|
1476
|
+
(isDark ? DARK_ATTR : ":root,body") + "{--dsw-font-family:" + sans + " !important}"
|
|
1477
|
+
);
|
|
1478
|
+
declarations.push((isDark ? DARK_ATTR : "body") + "{font-family:" + sans + " !important}");
|
|
1479
|
+
}
|
|
1480
|
+
if (mono !== "") {
|
|
1481
|
+
// The theme's code tokens chain to `--ds-font-family-code`; the other name
|
|
1482
|
+
// is what dsh-ui-font historically wrote and costs nothing to cover.
|
|
1483
|
+
declarations.push(
|
|
1484
|
+
(isDark ? DARK_ATTR : ":root,body") +
|
|
1485
|
+
"{--dsw-font-mono:" +
|
|
1486
|
+
mono +
|
|
1487
|
+
" !important;--ds-font-family-code:" +
|
|
1488
|
+
mono +
|
|
1489
|
+
" !important}"
|
|
1490
|
+
);
|
|
1491
|
+
// Written after the interface family rule: equal specificity on a code
|
|
1492
|
+
// element means the later declaration wins, so code keeps its own family.
|
|
1493
|
+
declarations.push(codeRule("font-family:" + mono + " !important"));
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// ---- interface size / line height: RETIRED, and deliberately so ----
|
|
1497
|
+
// Verified against DSH 0.1.5-rc.2: the official settings expose exactly one
|
|
1498
|
+
// size hook, the CONVERSATION font size (`--dsh-content-font-size`). The
|
|
1499
|
+
// interface ladder tokens (`--dsw-font-xs-13`, `--dsw-font-s-14`, …) are
|
|
1500
|
+
// consumed only by conversation-area widgets (tool cards, trajectory,
|
|
1501
|
+
// attachment chips — 6 client-ui packages), while the settings sheet, the
|
|
1502
|
+
// left workspace/sidebar and the right sidebar declare their text sizes as
|
|
1503
|
+
// literals inside their own CSS modules. There is therefore no interface
|
|
1504
|
+
// size or line-height hook: scaling the ladder from these fields moved
|
|
1505
|
+
// nothing a user could recognise as "interface", and scaling the content
|
|
1506
|
+
// chain from them moved the conversation. The durable fields stay in the
|
|
1507
|
+
// schema (old documents keep parsing, presets keep round-tripping) but inject
|
|
1508
|
+
// nothing at all. The interface axis that DOES work is the family (a
|
|
1509
|
+
// page-wide `font-family` on body plus the theme variable) and the weight
|
|
1510
|
+
// (below).
|
|
1511
|
+
|
|
1512
|
+
// ---- interface weight ----
|
|
1513
|
+
// Measured on rc.2 (live page, computed styles): a `body{font-weight}` rule
|
|
1514
|
+
// only reaches the elements that INHERIT their weight, while `body, body *`
|
|
1515
|
+
// also reaches the labels DSH pins with a literal weight on a class rule —
|
|
1516
|
+
// which is why the axis has to be blanket, and why the rule without the
|
|
1517
|
+
// markdown exclusion drags the conversation along with it. With the
|
|
1518
|
+
// exclusion the interface still moves and the markdown subtree does not.
|
|
1519
|
+
var interfaceWeight = axis[WEIGHT_FIELD];
|
|
1520
|
+
var codeWeight = axis[CODE_WEIGHT_FIELD];
|
|
1521
|
+
if (interfaceWeight !== WEIGHT_UNSET) {
|
|
1522
|
+
declarations.push(
|
|
1523
|
+
blanket("font-weight:" + interfaceWeight + " !important", INTERFACE_EXCLUDES)
|
|
1524
|
+
);
|
|
1525
|
+
if (codeWeight === WEIGHT_UNSET) {
|
|
1526
|
+
// Code surfaces are excluded from the blanket rule, so without this they
|
|
1527
|
+
// would inherit the interface weight from body. `normal` is what DSH
|
|
1528
|
+
// computes for them (the code `font` shorthands carry no weight), so an
|
|
1529
|
+
// unset code weight keeps code exactly as DSH shipped it.
|
|
1530
|
+
declarations.push(codeRule("font-weight:normal !important"));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// ---- dialog family (scoped; skipped when it equals the interface) ----
|
|
1535
|
+
var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
|
|
1536
|
+
if (dialog !== "" && dialog !== sans) {
|
|
1537
|
+
declarations.push(dialogRule("font-family:" + dialog + " !important"));
|
|
1538
|
+
if (mono !== "") {
|
|
1539
|
+
// Keep the code family inside the dialog.
|
|
1540
|
+
pushScopedCodeFamily();
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// ---- dialog weight (uniform inside the conversation) ----
|
|
1545
|
+
var dialogWeight = axis[WEIGHT_DIALOG_FIELD];
|
|
1546
|
+
if (dialogWeight !== WEIGHT_UNSET) {
|
|
1547
|
+
declarations.push(dialogRule("font-weight:" + dialogWeight + " !important"));
|
|
1548
|
+
// The card's conversation preview simulates this surface, so it takes the
|
|
1549
|
+
// same weight (and is excluded from the interface rule above).
|
|
1550
|
+
declarations.push(
|
|
1551
|
+
(isDark ? DARK_ATTR + " " : "") +
|
|
1552
|
+
".dfp-previewDialog{font-weight:" +
|
|
1553
|
+
dialogWeight +
|
|
1554
|
+
" !important}"
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// ---- dialog size offset (additive, on the official content-size source) ----
|
|
1559
|
+
// Every markdown size derives from the official content-size chain, and that
|
|
1560
|
+
// chain is COMPUTED ON BODY: custom properties resolve where they are
|
|
1561
|
+
// declared, so overriding the size deeper down would leave the derived tokens
|
|
1562
|
+
// (secondary size, both deltas, delta-based heading sizes, heights) at their
|
|
1563
|
+
// body-computed values. The offset is therefore added once, at the source —
|
|
1564
|
+
// `--dsh-content-font-size` becomes "official base + offset" — and the whole
|
|
1565
|
+
// ladder follows by derivation, with nothing counted twice. The interface
|
|
1566
|
+
// axis never touches this chain (see `bodyTokenNames`), so the conversation
|
|
1567
|
+
// moves only when this axis says so, and a dialog that FOLLOWS the interface
|
|
1568
|
+
// follows it through this value.
|
|
1569
|
+
var dialogOffsetValue = axis[SIZE_DIALOG_FIELD];
|
|
1570
|
+
if (dialogOffsetValue !== 0) {
|
|
1571
|
+
var contentSources = tokenNames(baseTokens).filter(function (name) {
|
|
1572
|
+
if (!isContentToken(name)) return false;
|
|
1573
|
+
var base = baseTokens[name];
|
|
1574
|
+
// The source token only: the derived ones carry var()/calc() and would
|
|
1575
|
+
// add the offset a second time if they were shifted as well.
|
|
1576
|
+
return (
|
|
1577
|
+
typeof base === "string" &&
|
|
1578
|
+
base !== "" &&
|
|
1579
|
+
base.indexOf("var(") === -1 &&
|
|
1580
|
+
base.indexOf("calc(") === -1
|
|
1581
|
+
);
|
|
1582
|
+
});
|
|
1583
|
+
var contentShift = shiftTokens(contentSources, baseTokens, dialogOffsetValue);
|
|
1584
|
+
if (contentShift.length > 0) declarations.push(blanket(contentShift.join(";")));
|
|
1585
|
+
// The `small` series is the only markdown family with literal sizes: it
|
|
1586
|
+
// does not derive from the chain, so its parts and shorthands shift here.
|
|
1587
|
+
var literals = markdownTokenNames(baseTokens).filter(function (name) {
|
|
1588
|
+
var base = baseTokens[name];
|
|
1589
|
+
return name.indexOf("-font-size") >= 0 && typeof base === "string" && base.indexOf("var(") === -1;
|
|
1590
|
+
});
|
|
1591
|
+
var literalShift = shiftTokens(literals, baseTokens, dialogOffsetValue);
|
|
1592
|
+
if (literalShift.length > 0) declarations.push(blanket(literalShift.join(";")));
|
|
1593
|
+
// The shorthands whose size is a literal (the small series) must ride the
|
|
1594
|
+
// shift too; the delta-derived shorthands flow through the chain above.
|
|
1595
|
+
var sizeShifted = [];
|
|
1596
|
+
var markdownEntries = shorthandTokens(baseTokens).filter(function (entry) {
|
|
1597
|
+
return isMarkdownToken(entry.name) && entry.parts.size.indexOf("var(") === -1;
|
|
1598
|
+
});
|
|
1599
|
+
for (var mIndex = 0; mIndex < markdownEntries.length; mIndex += 1) {
|
|
1600
|
+
var mEntry = markdownEntries[mIndex];
|
|
1601
|
+
var mSize =
|
|
1602
|
+
"calc((" + mEntry.parts.size + ") + " + dialogOffsetValue + "px)";
|
|
1603
|
+
sizeShifted.push(
|
|
1604
|
+
mEntry.name + ":" + rebuildShorthand({ lead: mEntry.parts.lead, size: mSize, height: mEntry.parts.height, family: mEntry.parts.family }) + " !important"
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
if (sizeShifted.length > 0) declarations.push(blanket(sizeShifted.join(";")));
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// ---- dialog line height (ratio, through the shorthand heights) ----
|
|
1611
|
+
var dialogLine = axis[LINE_HEIGHT_DIALOG_FIELD];
|
|
1612
|
+
if (dialogLine !== LINE_HEIGHT_MIN) {
|
|
1613
|
+
var dialogFactor = round(dialogLine / LINE_HEIGHT_MIN, 6);
|
|
1614
|
+
var markdownLines = markdownTokenNames(baseTokens).filter(function (name) {
|
|
1615
|
+
return name.indexOf("-line-height") >= 0;
|
|
1616
|
+
});
|
|
1617
|
+
var dialogLineDecls = scaleLineTokens(markdownLines, baseTokens, dialogFactor);
|
|
1618
|
+
if (dialogLineDecls.length > 0) declarations.push(blanket(dialogLineDecls.join(";")));
|
|
1619
|
+
var dialogShorthandList = markdownShorthandTokens(baseTokens);
|
|
1620
|
+
var dialogShorthandShifted = [];
|
|
1621
|
+
for (var dIndex = 0; dIndex < dialogShorthandList.length; dIndex += 1) {
|
|
1622
|
+
var dEntry = dialogShorthandList[dIndex];
|
|
1623
|
+
dialogShorthandShifted.push(
|
|
1624
|
+
dEntry.name +
|
|
1625
|
+
":" +
|
|
1626
|
+
rebuildShorthand({
|
|
1627
|
+
lead: dEntry.parts.lead,
|
|
1628
|
+
size: dEntry.parts.size,
|
|
1629
|
+
height: "calc((" + dEntry.parts.height + ") * " + dialogFactor + ")",
|
|
1630
|
+
family: dEntry.parts.family,
|
|
1631
|
+
}) +
|
|
1632
|
+
" !important"
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
if (dialogShorthandShifted.length > 0) {
|
|
1636
|
+
declarations.push(blanket(dialogShorthandShifted.join(";")));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// ---- code axes ----
|
|
1641
|
+
var codeSize = axis[CODE_SIZE_FIELD];
|
|
1642
|
+
var codeLine = axis[CODE_LINE_HEIGHT_FIELD];
|
|
1643
|
+
if (codeSize !== 0 || codeLine !== 0) {
|
|
1644
|
+
var codeScaled = scaleCodeTokens(
|
|
1645
|
+
baseTokens,
|
|
1646
|
+
round(scaleFor(codeSize), 6),
|
|
1647
|
+
codeLine
|
|
1648
|
+
);
|
|
1649
|
+
if (codeScaled.length > 0) declarations.push(blanket(codeScaled.join(";")));
|
|
1650
|
+
}
|
|
1651
|
+
var codeWeightRule = axis[CODE_WEIGHT_FIELD];
|
|
1652
|
+
if (codeWeightRule !== WEIGHT_UNSET) {
|
|
1653
|
+
declarations.push(codeRule("font-weight:" + codeWeightRule + " !important"));
|
|
1654
|
+
}
|
|
1655
|
+
var ligatures = axis[LIGATURES_FIELD];
|
|
1656
|
+
if (ligatures === LIGATURES_ON) {
|
|
1657
|
+
declarations.push(codeRule("font-variant-ligatures:contextual !important"));
|
|
1658
|
+
} else if (ligatures === LIGATURES_OFF) {
|
|
1659
|
+
declarations.push(codeRule("font-variant-ligatures:none !important"));
|
|
1660
|
+
}
|
|
1661
|
+
var features = axis[FEATURES_FIELD];
|
|
1662
|
+
if (features !== "") {
|
|
1663
|
+
declarations.push(codeRule("font-feature-settings:" + features + " !important"));
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// ---- synthesis (faux italic / faux bold) ----
|
|
1667
|
+
var noItalic = axis[NO_SYNTHETIC_ITALIC_FIELD] === true;
|
|
1668
|
+
var noBold = axis[NO_SYNTHETIC_BOLD_FIELD] === true;
|
|
1669
|
+
if (noItalic && noBold) {
|
|
1670
|
+
declarations.push(blanket("font-synthesis:none !important"));
|
|
1671
|
+
} else {
|
|
1672
|
+
if (noItalic) declarations.push(blanket("font-synthesis-style:none !important"));
|
|
1673
|
+
if (noBold) declarations.push(blanket("font-synthesis-weight:none !important"));
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return declarations.join("\n");
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Re-declare the markdown code tokens at their own ratio and line offset.
|
|
1681
|
+
*
|
|
1682
|
+
* Two shapes exist and both must be covered, because the shipped stylesheets
|
|
1683
|
+
* consume the shorthand (`font: var(--dsw-font-markdown-code-block-small)`)
|
|
1684
|
+
* while the split tokens exist for anything that asks for a single part:
|
|
1685
|
+
* `11px/19px <family>` and a bare `11px`. The shorthand's height rides BOTH
|
|
1686
|
+
* the ratio and the additive offset; the size rides the ratio only.
|
|
1687
|
+
*
|
|
1688
|
+
* @param {Record<string, string>} baseTokens - token name to untouched value.
|
|
1689
|
+
* @param {number} scale - the size ratio to apply.
|
|
1690
|
+
* @param {number} [lineOffset=0] - additive px on the height.
|
|
1691
|
+
* @returns {string[]} declarations.
|
|
1692
|
+
*/
|
|
1693
|
+
function scaleCodeTokens(baseTokens, scale, lineOffset) {
|
|
1694
|
+
var shift = typeof lineOffset === "number" && lineOffset !== 0 ? lineOffset : 0;
|
|
1695
|
+
// The code axis covers every `--dsw-font-markdown-code*` token exactly once:
|
|
1696
|
+
// a value that parses as a shorthand rides the shorthand path, everything
|
|
1697
|
+
// else the part path.
|
|
1698
|
+
var entries = [];
|
|
1699
|
+
var seen = {};
|
|
1700
|
+
var all = tokenNames(baseTokens).filter(isCodeToken);
|
|
1701
|
+
for (var aIndex = 0; aIndex < all.length; aIndex += 1) {
|
|
1702
|
+
seen[all[aIndex]] = true;
|
|
1703
|
+
entries.push({ name: all[aIndex], value: baseTokens[all[aIndex]] });
|
|
1704
|
+
}
|
|
1705
|
+
var shorthands = shorthandTokens(baseTokens);
|
|
1706
|
+
for (var sIndex = 0; sIndex < shorthands.length; sIndex += 1) {
|
|
1707
|
+
var entry = shorthands[sIndex];
|
|
1708
|
+
if (!isCodeToken(entry.name) || seen[entry.name] === true) continue;
|
|
1709
|
+
seen[entry.name] = true;
|
|
1710
|
+
entries.push({ name: entry.name, value: baseTokens[entry.name] });
|
|
1711
|
+
}
|
|
1712
|
+
var out = [];
|
|
1713
|
+
for (var index = 0; index < entries.length; index += 1) {
|
|
1714
|
+
var name = entries[index].name;
|
|
1715
|
+
var base = entries[index].value;
|
|
1716
|
+
if (typeof base !== "string" || base === "") continue;
|
|
1717
|
+
var parsed = parseShorthand(base);
|
|
1718
|
+
if (parsed !== null) {
|
|
1719
|
+
var size = scale === 1 ? parsed.size : "calc((" + parsed.size + ") * " + scale + ")";
|
|
1720
|
+
var height = "calc((" + parsed.height + ") * " + scale + ")";
|
|
1721
|
+
if (shift !== 0) height = "calc(" + height + " + " + shift + "px)";
|
|
1722
|
+
out.push(
|
|
1723
|
+
name +
|
|
1724
|
+
":" +
|
|
1725
|
+
rebuildShorthand({ lead: parsed.lead, size: size, height: height, family: parsed.family }) +
|
|
1726
|
+
" !important"
|
|
1727
|
+
);
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
// A part token that derives from another token follows that token's own
|
|
1731
|
+
// scaling; re-declaring it here would compound the ratio — except the
|
|
1732
|
+
// line-height parts, which must additionally carry the offset.
|
|
1733
|
+
if (base.indexOf("var(") !== -1 && !(name.indexOf("-line-height") >= 0 && shift !== 0)) continue;
|
|
1734
|
+
if (!isScalableValue(base)) continue;
|
|
1735
|
+
if (name.indexOf("-line-height") >= 0 && shift !== 0) {
|
|
1736
|
+
out.push(
|
|
1737
|
+
name +
|
|
1738
|
+
":calc(((" + base + ") * " + scale + ") + " + shift + "px) !important"
|
|
1739
|
+
);
|
|
1740
|
+
} else {
|
|
1741
|
+
out.push(name + ":calc((" + base + ") * " + scale + ") !important");
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return out;
|
|
732
1745
|
}
|
|
733
1746
|
|
|
1747
|
+
/* ------------------------------------------------------------------ *
|
|
1748
|
+
* entry point
|
|
1749
|
+
* ------------------------------------------------------------------ */
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Build the stylesheet one configuration applies.
|
|
1753
|
+
*
|
|
1754
|
+
* The configuration resolves into a light set and — when `perTheme` is on — a
|
|
1755
|
+
* dark set; the light rules are emitted unprefixed and the dark rules carry
|
|
1756
|
+
* the dark-theme attribute, so the dark set wins by specificity exactly while
|
|
1757
|
+
* that theme is active. When `perTheme` is off, or the dark set equals the
|
|
1758
|
+
* light set, no prefixed rules are emitted at all.
|
|
1759
|
+
*
|
|
1760
|
+
* @param {unknown} config - normalized or raw configuration.
|
|
1761
|
+
* @param {Record<string, string>} [baseTokens] - token name to untouched value.
|
|
1762
|
+
* @returns {string} declarations for one `<style>` element ("" when dormant).
|
|
1763
|
+
*/
|
|
1764
|
+
function buildFontCss(config, baseTokens) {
|
|
1765
|
+
var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
|
|
1766
|
+
var sets = resolveAxes(config);
|
|
1767
|
+
if (isDormant(sets.light) && isDormant(sets.dark)) return "";
|
|
1768
|
+
var light = buildAxisCss(sets.light, tokens, false);
|
|
1769
|
+
if (sameValueSet(sets.light, sets.dark)) return light;
|
|
1770
|
+
var dark = buildAxisCss(sets.dark, tokens, true);
|
|
1771
|
+
if (dark === "") return light;
|
|
1772
|
+
return light + "\n" + dark;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* Whether two axis sets produce the same declarations, compared over the
|
|
1777
|
+
* visual fields only (the storage fields — presets, the dark map itself —
|
|
1778
|
+
* never render).
|
|
1779
|
+
* @param {Record<string, unknown>} left - an expanded axis set.
|
|
1780
|
+
* @param {Record<string, unknown>} right - another expanded axis set.
|
|
1781
|
+
* @returns {boolean}
|
|
1782
|
+
*/
|
|
1783
|
+
function sameValueSet(left, right) {
|
|
1784
|
+
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
1785
|
+
var field = VALUE_FIELDS[index];
|
|
1786
|
+
if (left[field] !== right[field]) return false;
|
|
1787
|
+
}
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/* ------------------------------------------------------------------ *
|
|
1792
|
+
* embedded fallback map
|
|
1793
|
+
* ------------------------------------------------------------------ */
|
|
1794
|
+
|
|
734
1795
|
/**
|
|
735
1796
|
* Embedded fallback for the tokens the theme plugin defines at runtime.
|
|
736
1797
|
*
|
|
737
1798
|
* The live document is always preferred; this map only covers the window
|
|
738
1799
|
* before those declarations exist (and any token a future release renames
|
|
739
|
-
* away from the pattern). Values
|
|
740
|
-
* DSH 0.1.5-rc.2.
|
|
1800
|
+
* away from the pattern). Values are read byte for byte from the theme's own
|
|
1801
|
+
* stylesheet in DSH 0.1.5-rc.2.
|
|
741
1802
|
*/
|
|
742
1803
|
var FALLBACK_TOKENS = {
|
|
1804
|
+
// interface ladder shorthands
|
|
1805
|
+
"--dsw-font-xxxs-11": "11px/14px var(--dsw-font-family)",
|
|
1806
|
+
"--dsw-font-xxxs-strong-11": "500 11px/14px var(--dsw-font-family)",
|
|
1807
|
+
"--dsw-font-xxs-12": "12px/18px var(--dsw-font-family)",
|
|
1808
|
+
"--dsw-font-xxs-strong-12": "500 12px/18px var(--dsw-font-family)",
|
|
1809
|
+
"--dsw-font-xs-13": "13px/20px var(--dsw-font-family)",
|
|
1810
|
+
"--dsw-font-xs-strong-13": "500 13px/20px var(--dsw-font-family)",
|
|
1811
|
+
"--dsw-font-s-14": "14px/22px var(--dsw-font-family)",
|
|
1812
|
+
"--dsw-font-s-strong-14": "500 14px/22px var(--dsw-font-family)",
|
|
1813
|
+
"--dsw-font-base-16": "16px/24px var(--dsw-font-family)",
|
|
1814
|
+
"--dsw-font-base-strong-16": "500 16px/24px var(--dsw-font-family)",
|
|
1815
|
+
"--dsw-font-m-18": "500 16px/28px var(--dsw-font-family)",
|
|
1816
|
+
"--dsw-font-l-20": "500 20px/28px var(--dsw-font-family)",
|
|
1817
|
+
"--dsw-font-xl-24": "600 24px/32px var(--dsw-font-family)",
|
|
1818
|
+
// interface ladder parts
|
|
743
1819
|
"--dsw-font-xxxs-11-font-size": "11px",
|
|
744
|
-
"--dsw-font-xxxs-11-line-height": "
|
|
1820
|
+
"--dsw-font-xxxs-11-line-height": "14px",
|
|
745
1821
|
"--dsw-font-xxxs-strong-11-font-size": "11px",
|
|
746
|
-
"--dsw-font-xxxs-strong-11-line-height": "
|
|
1822
|
+
"--dsw-font-xxxs-strong-11-line-height": "14px",
|
|
747
1823
|
"--dsw-font-xxs-12-font-size": "12px",
|
|
748
|
-
"--dsw-font-xxs-12-line-height": "
|
|
1824
|
+
"--dsw-font-xxs-12-line-height": "18px",
|
|
749
1825
|
"--dsw-font-xxs-strong-12-font-size": "12px",
|
|
750
|
-
"--dsw-font-xxs-strong-12-line-height": "
|
|
1826
|
+
"--dsw-font-xxs-strong-12-line-height": "18px",
|
|
751
1827
|
"--dsw-font-xs-13-font-size": "13px",
|
|
752
|
-
"--dsw-font-xs-13-line-height": "
|
|
1828
|
+
"--dsw-font-xs-13-line-height": "20px",
|
|
753
1829
|
"--dsw-font-xs-strong-13-font-size": "13px",
|
|
754
|
-
"--dsw-font-xs-strong-13-line-height": "
|
|
1830
|
+
"--dsw-font-xs-strong-13-line-height": "20px",
|
|
755
1831
|
"--dsw-font-s-14-font-size": "14px",
|
|
756
|
-
"--dsw-font-s-14-line-height": "
|
|
1832
|
+
"--dsw-font-s-14-line-height": "22px",
|
|
757
1833
|
"--dsw-font-s-strong-14-font-size": "14px",
|
|
758
|
-
"--dsw-font-s-strong-14-line-height": "
|
|
1834
|
+
"--dsw-font-s-strong-14-line-height": "22px",
|
|
759
1835
|
"--dsw-font-base-16-font-size": "16px",
|
|
760
|
-
"--dsw-font-base-16-line-height": "
|
|
1836
|
+
"--dsw-font-base-16-line-height": "24px",
|
|
761
1837
|
"--dsw-font-base-strong-16-font-size": "16px",
|
|
762
|
-
"--dsw-font-base-strong-16-line-height": "
|
|
763
|
-
"--dsw-font-m-18-font-size": "
|
|
1838
|
+
"--dsw-font-base-strong-16-line-height": "24px",
|
|
1839
|
+
"--dsw-font-m-18-font-size": "16px",
|
|
764
1840
|
"--dsw-font-m-18-line-height": "28px",
|
|
765
1841
|
"--dsw-font-l-20-font-size": "20px",
|
|
766
|
-
"--dsw-font-l-20-line-height": "
|
|
1842
|
+
"--dsw-font-l-20-line-height": "28px",
|
|
767
1843
|
"--dsw-font-xl-24-font-size": "24px",
|
|
768
|
-
"--dsw-font-xl-24-line-height": "
|
|
769
|
-
|
|
770
|
-
"--dsw-font-markdown-
|
|
771
|
-
"--dsw-font-markdown-
|
|
772
|
-
"--dsw-font-markdown-
|
|
773
|
-
"--dsw-font-markdown-
|
|
774
|
-
"--dsw-font-markdown-
|
|
775
|
-
"--dsw-font-markdown-
|
|
776
|
-
"--dsw-font-markdown-
|
|
777
|
-
"--dsw-font-markdown-
|
|
778
|
-
"--dsw-font-markdown-
|
|
779
|
-
"--dsw-font-markdown-
|
|
780
|
-
"--dsw-font-markdown-
|
|
781
|
-
|
|
782
|
-
|
|
1844
|
+
"--dsw-font-xl-24-line-height": "32px",
|
|
1845
|
+
// markdown (dialog) shorthands
|
|
1846
|
+
"--dsw-font-markdown-h1": "700 calc(21px + var(--dsh-content-font-delta)) / calc(30px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
|
|
1847
|
+
"--dsw-font-markdown-h2": "700 calc(19px + var(--dsh-content-font-delta)) / calc(28px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
|
|
1848
|
+
"--dsw-font-markdown-h3": "700 calc(18px + var(--dsh-content-font-delta)) / calc(26px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
|
|
1849
|
+
"--dsw-font-markdown-h4": "600 var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
|
|
1850
|
+
"--dsw-font-markdown-base": "var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
|
|
1851
|
+
"--dsw-font-markdown-base-strong": "600 var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
|
|
1852
|
+
"--dsw-font-markdown-base-italic": "italic var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
|
|
1853
|
+
"--dsw-font-markdown-base-strong-italic": "italic 600 var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
|
|
1854
|
+
"--dsw-font-markdown-table": "var(--dsh-content-font-size-secondary,13px)/calc(22px + var(--dsh-content-font-delta-secondary,0px)) var(--dsw-font-family)",
|
|
1855
|
+
"--dsw-font-markdown-table-head": "500 var(--dsh-content-font-size-secondary,13px)/calc(22px + var(--dsh-content-font-delta-secondary,0px)) var(--dsw-font-family)",
|
|
1856
|
+
"--dsw-font-markdown-small": "12px/20px var(--dsw-font-family)",
|
|
1857
|
+
"--dsw-font-markdown-small-strong": "600 12px/20px var(--dsw-font-family)",
|
|
1858
|
+
"--dsw-font-markdown-small-italic": "italic 12px/20px var(--dsw-font-family)",
|
|
1859
|
+
"--dsw-font-markdown-small-strong-italic": "italic 600 12px/20px var(--dsw-font-family)",
|
|
1860
|
+
// markdown small-series literals (the additive dialog offset rewrites these)
|
|
1861
|
+
"--dsw-font-markdown-small-font-size": "12px",
|
|
1862
|
+
"--dsw-font-markdown-small-line-height": "20px",
|
|
1863
|
+
"--dsw-font-markdown-small-strong-font-size": "12px",
|
|
1864
|
+
"--dsw-font-markdown-small-strong-line-height": "20px",
|
|
1865
|
+
"--dsw-font-markdown-small-italic-font-size": "12px",
|
|
1866
|
+
"--dsw-font-markdown-small-italic-line-height": "20px",
|
|
1867
|
+
"--dsw-font-markdown-small-strong-italic-font-size": "12px",
|
|
1868
|
+
"--dsw-font-markdown-small-strong-italic-line-height": "20px",
|
|
1869
|
+
// markdown part tokens (the dialog line-height ratio scales their heights;
|
|
1870
|
+
// their var-derived sizes ride the delta tokens, never the ratio)
|
|
1871
|
+
"--dsw-font-markdown-base-font-size": "var(--dsh-content-font-size,14px)",
|
|
1872
|
+
"--dsw-font-markdown-base-line-height": "calc(24px + var(--dsh-content-font-delta))",
|
|
1873
|
+
"--dsw-font-markdown-base-strong-font-size": "var(--dsh-content-font-size,14px)",
|
|
1874
|
+
"--dsw-font-markdown-base-strong-line-height": "calc(24px + var(--dsh-content-font-delta))",
|
|
1875
|
+
"--dsw-font-markdown-base-italic-font-size": "var(--dsh-content-font-size,14px)",
|
|
1876
|
+
"--dsw-font-markdown-base-italic-line-height": "calc(24px + var(--dsh-content-font-delta))",
|
|
1877
|
+
"--dsw-font-markdown-base-strong-italic-font-size": "var(--dsh-content-font-size,14px)",
|
|
1878
|
+
"--dsw-font-markdown-base-strong-italic-line-height": "calc(24px + var(--dsh-content-font-delta))",
|
|
1879
|
+
"--dsw-font-markdown-h1-font-size": "calc(21px + var(--dsh-content-font-delta))",
|
|
1880
|
+
"--dsw-font-markdown-h1-line-height": "calc(30px + var(--dsh-content-font-delta))",
|
|
1881
|
+
"--dsw-font-markdown-h2-font-size": "calc(19px + var(--dsh-content-font-delta))",
|
|
1882
|
+
"--dsw-font-markdown-h2-line-height": "calc(28px + var(--dsh-content-font-delta))",
|
|
1883
|
+
"--dsw-font-markdown-h3-font-size": "calc(18px + var(--dsh-content-font-delta))",
|
|
1884
|
+
"--dsw-font-markdown-h3-line-height": "calc(26px + var(--dsh-content-font-delta))",
|
|
1885
|
+
"--dsw-font-markdown-h4-font-size": "var(--dsh-content-font-size,14px)",
|
|
1886
|
+
"--dsw-font-markdown-h4-line-height": "calc(24px + var(--dsh-content-font-delta))",
|
|
1887
|
+
"--dsw-font-markdown-table-font-size": "var(--dsh-content-font-size-secondary,13px)",
|
|
1888
|
+
"--dsw-font-markdown-table-line-height": "calc(22px + var(--dsh-content-font-delta-secondary,0px))",
|
|
1889
|
+
"--dsw-font-markdown-table-head-font-size": "var(--dsh-content-font-size-secondary,13px)",
|
|
1890
|
+
"--dsw-font-markdown-table-head-line-height": "calc(22px + var(--dsh-content-font-delta-secondary,0px))",
|
|
1891
|
+
// markdown delta tokens (the dialog offset shifts these additively)
|
|
1892
|
+
"--dsh-content-font-delta": "calc(var(--dsh-content-font-size,14px) - 14px)",
|
|
1893
|
+
"--dsh-content-font-delta-secondary": "calc(var(--dsh-content-font-size-secondary) - 13px)",
|
|
1894
|
+
// the code family: shorthand + parts, at rc.2's values
|
|
783
1895
|
"--dsw-font-markdown-code": "12px/19px var(--ds-font-family-code)",
|
|
784
1896
|
"--dsw-font-markdown-code-font-size": "12px",
|
|
785
1897
|
"--dsw-font-markdown-code-line-height": "19px",
|
|
786
1898
|
"--dsw-font-markdown-code-block": "11px/19px var(--ds-font-family-code)",
|
|
787
|
-
"--dsw-font-markdown-code-block": "11px/19px var(--ds-font-family-code)",
|
|
788
1899
|
"--dsw-font-markdown-code-block-font-size": "11px",
|
|
789
1900
|
"--dsw-font-markdown-code-block-line-height": "19px",
|
|
790
1901
|
"--dsw-font-markdown-code-block-small": "11px/16px var(--ds-font-family-code)",
|
|
791
1902
|
"--dsw-font-markdown-code-block-small-font-size": "11px",
|
|
792
1903
|
"--dsw-font-markdown-code-block-small-line-height": "16px",
|
|
793
|
-
|
|
794
|
-
"--dsw-font-markdown-table-line-height": "22px",
|
|
1904
|
+
// content size (the interface chain's anchor, written inline by the theme)
|
|
795
1905
|
"--dsh-content-font-size": "14px",
|
|
796
1906
|
"--dsh-content-font-size-secondary": "13px",
|
|
797
1907
|
};
|
|
798
1908
|
|
|
799
|
-
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
* @param {number} digits - decimals to keep.
|
|
803
|
-
* @returns {number} the rounded number.
|
|
804
|
-
*/
|
|
805
|
-
function round(value, digits) {
|
|
806
|
-
var factor = Math.pow(10, digits);
|
|
807
|
-
return Math.round(value * factor) / factor;
|
|
808
|
-
}
|
|
1909
|
+
/* ------------------------------------------------------------------ *
|
|
1910
|
+
* curated family lists
|
|
1911
|
+
* ------------------------------------------------------------------ */
|
|
809
1912
|
|
|
810
1913
|
/**
|
|
811
1914
|
* Curated families offered when the browser cannot enumerate local fonts —
|
|
@@ -871,44 +1974,111 @@ var PRESETS = {
|
|
|
871
1974
|
generic: ["system-ui", "sans-serif", "serif"],
|
|
872
1975
|
};
|
|
873
1976
|
|
|
1977
|
+
/**
|
|
1978
|
+
* Legacy single-purpose helper kept for compatibility: scale the size and the
|
|
1979
|
+
* height inside one simple `size/height <family>` shorthand.
|
|
1980
|
+
* @param {string} value - a value shaped like `11px/19px var(--ds-font-family-code)`.
|
|
1981
|
+
* @param {number} scale - the ratio to apply.
|
|
1982
|
+
* @returns {string|null} the scaled shorthand, or null when the shape differs.
|
|
1983
|
+
*/
|
|
1984
|
+
function scaleFontShorthand(value, scale) {
|
|
1985
|
+
var parts = parseShorthand(value);
|
|
1986
|
+
if (parts === null || parts.lead !== "") return null;
|
|
1987
|
+
return (
|
|
1988
|
+
"calc((" + parts.size + ") * " + scale + ")/calc((" + parts.height + ") * " + scale + ") " + parts.family
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
874
1992
|
var shared = {
|
|
875
1993
|
NAMESPACE: NAMESPACE,
|
|
876
1994
|
SANS_FIELD: SANS_FIELD,
|
|
1995
|
+
STACK_DIALOG_FIELD: STACK_DIALOG_FIELD,
|
|
877
1996
|
MONO_FIELD: MONO_FIELD,
|
|
878
1997
|
SIZE_FIELD: SIZE_FIELD,
|
|
1998
|
+
SIZE_DIALOG_FIELD: SIZE_DIALOG_FIELD,
|
|
879
1999
|
CODE_SIZE_FIELD: CODE_SIZE_FIELD,
|
|
880
2000
|
WEIGHT_FIELD: WEIGHT_FIELD,
|
|
2001
|
+
WEIGHT_DIALOG_FIELD: WEIGHT_DIALOG_FIELD,
|
|
881
2002
|
CODE_WEIGHT_FIELD: CODE_WEIGHT_FIELD,
|
|
2003
|
+
LINE_HEIGHT_FIELD: LINE_HEIGHT_FIELD,
|
|
2004
|
+
LINE_HEIGHT_DIALOG_FIELD: LINE_HEIGHT_DIALOG_FIELD,
|
|
2005
|
+
CODE_LINE_HEIGHT_FIELD: CODE_LINE_HEIGHT_FIELD,
|
|
2006
|
+
LIGATURES_FIELD: LIGATURES_FIELD,
|
|
2007
|
+
FEATURES_FIELD: FEATURES_FIELD,
|
|
2008
|
+
NO_SYNTHETIC_ITALIC_FIELD: NO_SYNTHETIC_ITALIC_FIELD,
|
|
2009
|
+
NO_SYNTHETIC_BOLD_FIELD: NO_SYNTHETIC_BOLD_FIELD,
|
|
2010
|
+
PER_THEME_FIELD: PER_THEME_FIELD,
|
|
2011
|
+
DARK_VALUES_FIELD: DARK_VALUES_FIELD,
|
|
2012
|
+
PRESETS_FIELD: PRESETS_FIELD,
|
|
2013
|
+
ACTIVE_PRESET_FIELD: ACTIVE_PRESET_FIELD,
|
|
2014
|
+
UI_FOLLOWS_FIELD: UI_FOLLOWS_FIELD,
|
|
2015
|
+
VALUE_FIELDS: VALUE_FIELDS,
|
|
882
2016
|
SIZE_MIN: SIZE_MIN,
|
|
883
2017
|
SIZE_MAX: SIZE_MAX,
|
|
884
2018
|
WEIGHT_MIN: WEIGHT_MIN,
|
|
885
2019
|
WEIGHT_MAX: WEIGHT_MAX,
|
|
886
2020
|
WEIGHT_UNSET: WEIGHT_UNSET,
|
|
2021
|
+
LINE_HEIGHT_MIN: LINE_HEIGHT_MIN,
|
|
2022
|
+
LINE_HEIGHT_MAX: LINE_HEIGHT_MAX,
|
|
2023
|
+
CODE_LINE_HEIGHT_MIN: CODE_LINE_HEIGHT_MIN,
|
|
2024
|
+
CODE_LINE_HEIGHT_MAX: CODE_LINE_HEIGHT_MAX,
|
|
2025
|
+
LIGATURES_DEFAULT: LIGATURES_DEFAULT,
|
|
2026
|
+
LIGATURES_ON: LIGATURES_ON,
|
|
2027
|
+
LIGATURES_OFF: LIGATURES_OFF,
|
|
887
2028
|
MARKER: MARKER,
|
|
888
2029
|
STYLE_TAG: STYLE_TAG,
|
|
889
2030
|
CARD_STYLE_TAG: CARD_STYLE_TAG,
|
|
890
2031
|
DEFAULTS: DEFAULTS,
|
|
891
2032
|
MAX_STACK_LENGTH: MAX_STACK_LENGTH,
|
|
892
2033
|
MAX_FAMILY_LENGTH: MAX_FAMILY_LENGTH,
|
|
2034
|
+
MAX_FEATURES_LENGTH: MAX_FEATURES_LENGTH,
|
|
2035
|
+
MAX_PRESETS: MAX_PRESETS,
|
|
2036
|
+
MAX_PRESET_NAME: MAX_PRESET_NAME,
|
|
893
2037
|
FALLBACK_TOKENS: FALLBACK_TOKENS,
|
|
894
2038
|
PRESETS: PRESETS,
|
|
2039
|
+
MARKDOWN_SELECTOR: MARKDOWN_SELECTOR,
|
|
2040
|
+
DARK_ATTR: DARK_ATTR,
|
|
895
2041
|
sanitize: sanitize,
|
|
896
2042
|
sanitizeFamily: sanitizeFamily,
|
|
2043
|
+
sanitizeFeatures: sanitizeFeatures,
|
|
897
2044
|
quoteFamily: quoteFamily,
|
|
898
2045
|
normalizeConfig: normalizeConfig,
|
|
2046
|
+
normalizeDarkValues: normalizeDarkValues,
|
|
2047
|
+
normalizePresets: normalizePresets,
|
|
2048
|
+
normalizeValueSet: normalizeValueSet,
|
|
899
2049
|
clampOffset: clampOffset,
|
|
900
2050
|
clampWeight: clampWeight,
|
|
2051
|
+
clampRatio: clampRatio,
|
|
2052
|
+
clampLineOffset: clampLineOffset,
|
|
2053
|
+
clampLigatures: clampLigatures,
|
|
901
2054
|
CODE_SELECTOR: CODE_SELECTOR,
|
|
902
2055
|
formatStack: formatStack,
|
|
903
2056
|
parseStack: parseStack,
|
|
2057
|
+
parseShorthand: parseShorthand,
|
|
2058
|
+
rebuildShorthand: rebuildShorthand,
|
|
904
2059
|
scaleFor: scaleFor,
|
|
905
2060
|
isDormant: isDormant,
|
|
2061
|
+
buildAxisCss: buildAxisCss,
|
|
906
2062
|
buildFontCss: buildFontCss,
|
|
2063
|
+
resolveAxes: resolveAxes,
|
|
2064
|
+
expandFollow: expandFollow,
|
|
907
2065
|
isFontToken: isFontToken,
|
|
908
2066
|
isCodeToken: isCodeToken,
|
|
2067
|
+
isMarkdownToken: isMarkdownToken,
|
|
2068
|
+
isDeltaToken: isDeltaToken,
|
|
2069
|
+
isShorthandName: isShorthandName,
|
|
2070
|
+
isShorthandToken: isShorthandToken,
|
|
909
2071
|
isScaledToken: isScaledToken,
|
|
2072
|
+
tokenNames: tokenNames,
|
|
910
2073
|
codeTokenNames: codeTokenNames,
|
|
2074
|
+
markdownTokenNames: markdownTokenNames,
|
|
911
2075
|
bodyTokenNames: bodyTokenNames,
|
|
2076
|
+
uiLineTokenNames: uiLineTokenNames,
|
|
2077
|
+
deltaTokenNames: deltaTokenNames,
|
|
2078
|
+
shorthandTokens: shorthandTokens,
|
|
2079
|
+
markdownShorthandTokens: markdownShorthandTokens,
|
|
2080
|
+
uiShorthandTokens: uiShorthandTokens,
|
|
2081
|
+
codeShorthandTokens: codeShorthandTokens,
|
|
912
2082
|
scaleFontShorthand: scaleFontShorthand,
|
|
913
2083
|
isGenericFamilyName: isGenericFamilyName,
|
|
914
2084
|
isCJKFamilyName: isCJKFamilyName,
|
|
@@ -928,14 +2098,19 @@ if (typeof module !== "undefined" && module.exports) module.exports = shared;
|
|
|
928
2098
|
* Runs inside the DSH web client as a lazy-CJS module bundle (see `build.mjs`).
|
|
929
2099
|
* It owns one card in Settings -> Plugins -> Plugin configuration, keyed by the
|
|
930
2100
|
* settings namespace the host half registers, and it applies the saved
|
|
931
|
-
* configuration by rewriting a single `<style>` element
|
|
2101
|
+
* configuration by rewriting a single `<style>` element plus the official
|
|
2102
|
+
* `theme.overrideTokens` layer for the family variables.
|
|
932
2103
|
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
2104
|
+
* 0.2.x shape: the card is an accordion — four sections (conversation /
|
|
2105
|
+
* interface / code / global fine-tuning), each collapsed to a title plus a
|
|
2106
|
+
* summary of the current values, at most one open at a time; an expanded
|
|
2107
|
+
* section ends with an inline preview of just that part. The preset bar sits
|
|
2108
|
+
* under the edit mode: a dropdown of the presets plus rename/import/export, and
|
|
2109
|
+
* with a preset selected every edit auto-saves into it. The conversation owns
|
|
2110
|
+
* every axis; the interface's section leads with the follow switch — on (the
|
|
2111
|
+
* default) it takes the conversation's family and weight and shows no controls
|
|
2112
|
+
* of its own, off reveals its own family and weight. Light and dark themes share
|
|
2113
|
+
* one value set; the per-theme editor ships in a later release.
|
|
939
2114
|
*
|
|
940
2115
|
* Module scope stays side-effect free: the loader materializes the factory
|
|
941
2116
|
* only when the plugin is first used, and everything that touches the document
|
|
@@ -959,27 +2134,53 @@ var useSyncExternalStore = React.useSyncExternalStore;
|
|
|
959
2134
|
var shared = __dfpShared;
|
|
960
2135
|
var NAMESPACE = shared.NAMESPACE;
|
|
961
2136
|
var SANS_FIELD = shared.SANS_FIELD;
|
|
2137
|
+
var STACK_DIALOG_FIELD = shared.STACK_DIALOG_FIELD;
|
|
962
2138
|
var MONO_FIELD = shared.MONO_FIELD;
|
|
963
|
-
var
|
|
2139
|
+
var SIZE_DIALOG_FIELD = shared.SIZE_DIALOG_FIELD;
|
|
964
2140
|
var CODE_SIZE_FIELD = shared.CODE_SIZE_FIELD;
|
|
965
2141
|
var WEIGHT_FIELD = shared.WEIGHT_FIELD;
|
|
2142
|
+
var WEIGHT_DIALOG_FIELD = shared.WEIGHT_DIALOG_FIELD;
|
|
966
2143
|
var CODE_WEIGHT_FIELD = shared.CODE_WEIGHT_FIELD;
|
|
2144
|
+
var LINE_HEIGHT_DIALOG_FIELD = shared.LINE_HEIGHT_DIALOG_FIELD;
|
|
2145
|
+
var CODE_LINE_HEIGHT_FIELD = shared.CODE_LINE_HEIGHT_FIELD;
|
|
2146
|
+
var LIGATURES_FIELD = shared.LIGATURES_FIELD;
|
|
2147
|
+
var FEATURES_FIELD = shared.FEATURES_FIELD;
|
|
2148
|
+
var NO_SYNTHETIC_ITALIC_FIELD = shared.NO_SYNTHETIC_ITALIC_FIELD;
|
|
2149
|
+
var NO_SYNTHETIC_BOLD_FIELD = shared.NO_SYNTHETIC_BOLD_FIELD;
|
|
2150
|
+
var PER_THEME_FIELD = shared.PER_THEME_FIELD;
|
|
2151
|
+
var DARK_VALUES_FIELD = shared.DARK_VALUES_FIELD;
|
|
2152
|
+
var PRESETS_FIELD = shared.PRESETS_FIELD;
|
|
2153
|
+
var ACTIVE_PRESET_FIELD = shared.ACTIVE_PRESET_FIELD;
|
|
2154
|
+
var UI_FOLLOWS_FIELD = shared.UI_FOLLOWS_FIELD;
|
|
2155
|
+
var VALUE_FIELDS = shared.VALUE_FIELDS;
|
|
967
2156
|
var SIZE_MIN = shared.SIZE_MIN;
|
|
968
2157
|
var SIZE_MAX = shared.SIZE_MAX;
|
|
969
2158
|
var WEIGHT_MIN = shared.WEIGHT_MIN;
|
|
970
2159
|
var WEIGHT_MAX = shared.WEIGHT_MAX;
|
|
971
2160
|
var WEIGHT_UNSET = shared.WEIGHT_UNSET;
|
|
2161
|
+
var LINE_HEIGHT_MIN = shared.LINE_HEIGHT_MIN;
|
|
2162
|
+
var LINE_HEIGHT_MAX = shared.LINE_HEIGHT_MAX;
|
|
2163
|
+
var CODE_LINE_HEIGHT_MIN = shared.CODE_LINE_HEIGHT_MIN;
|
|
2164
|
+
var CODE_LINE_HEIGHT_MAX = shared.CODE_LINE_HEIGHT_MAX;
|
|
2165
|
+
var LIGATURES_DEFAULT = shared.LIGATURES_DEFAULT;
|
|
2166
|
+
var LIGATURES_ON = shared.LIGATURES_ON;
|
|
2167
|
+
var LIGATURES_OFF = shared.LIGATURES_OFF;
|
|
972
2168
|
var CARD_STYLE_TAG = shared.CARD_STYLE_TAG;
|
|
973
2169
|
var STYLE_TAG = shared.STYLE_TAG;
|
|
974
2170
|
var FALLBACK_TOKENS = shared.FALLBACK_TOKENS;
|
|
975
2171
|
var PRESETS = shared.PRESETS;
|
|
2172
|
+
var DEFAULTS = shared.DEFAULTS;
|
|
976
2173
|
var buildFontCss = shared.buildFontCss;
|
|
977
2174
|
var formatStack = shared.formatStack;
|
|
978
2175
|
var isFontToken = shared.isFontToken;
|
|
979
2176
|
var isScaledToken = shared.isScaledToken;
|
|
980
2177
|
var normalizeConfig = shared.normalizeConfig;
|
|
2178
|
+
var normalizeDarkValues = shared.normalizeDarkValues;
|
|
2179
|
+
var normalizePresets = shared.normalizePresets;
|
|
2180
|
+
var normalizeValueSet = shared.normalizeValueSet;
|
|
981
2181
|
var parseStack = shared.parseStack;
|
|
982
2182
|
var quoteFamily = shared.quoteFamily;
|
|
2183
|
+
var resolveAxes = shared.resolveAxes;
|
|
983
2184
|
var sanitizeFamily = shared.sanitizeFamily;
|
|
984
2185
|
var isGenericFamilyName = shared.isGenericFamilyName;
|
|
985
2186
|
var isCJKFamilyName = shared.isCJKFamilyName;
|
|
@@ -1031,7 +2232,7 @@ var DICTS = {
|
|
|
1031
2232
|
en: {
|
|
1032
2233
|
"card.title": "Font tune",
|
|
1033
2234
|
"card.description":
|
|
1034
|
-
"
|
|
2235
|
+
"Interface and conversation fonts, a size, a weight and a line height for each, code extras and presets",
|
|
1035
2236
|
"card.expand": "Expand",
|
|
1036
2237
|
"card.collapse": "Collapse",
|
|
1037
2238
|
"card.resetAll": "Reset all",
|
|
@@ -1039,8 +2240,10 @@ var DICTS = {
|
|
|
1039
2240
|
|
|
1040
2241
|
"common.overridden": "Changed",
|
|
1041
2242
|
"common.reset": "Reset",
|
|
2243
|
+
"common.on": "On",
|
|
2244
|
+
"common.off": "Off",
|
|
1042
2245
|
|
|
1043
|
-
"sans.label": "
|
|
2246
|
+
"sans.label": "Interface font",
|
|
1044
2247
|
"sans.hint":
|
|
1045
2248
|
"Each family is tried in order: put a Latin face first and CJK faces after it. Empty leaves DSH's own stack.",
|
|
1046
2249
|
"mono.label": "Code font",
|
|
@@ -1074,8 +2277,8 @@ var DICTS = {
|
|
|
1074
2277
|
"mode.advanced": "Advanced",
|
|
1075
2278
|
"mode.simpleHint":
|
|
1076
2279
|
"Manages only the front two slots of the stack — Western, then CJK. Everything you ordered in Advanced stays untouched.",
|
|
1077
|
-
"sansWest.label": "
|
|
1078
|
-
"sansEast.label": "
|
|
2280
|
+
"sansWest.label": "Interface · Western",
|
|
2281
|
+
"sansEast.label": "Interface · CJK",
|
|
1079
2282
|
"monoWest.label": "Code · Western",
|
|
1080
2283
|
"monoEast.label": "Code · CJK",
|
|
1081
2284
|
"split.pick": "Choose…",
|
|
@@ -1083,35 +2286,135 @@ var DICTS = {
|
|
|
1083
2286
|
"split.remove": "Remove {name}",
|
|
1084
2287
|
"split.rest": "Other fallbacks (reorder them in Advanced): {names}",
|
|
1085
2288
|
|
|
1086
|
-
"size.bodyLabel": "
|
|
2289
|
+
"size.bodyLabel": "Interface font size offset",
|
|
1087
2290
|
"size.bodyHint":
|
|
1088
|
-
"Adds {offset} to text
|
|
2291
|
+
"Adds {offset} to interface text sizes, on top of DSH's own font-size setting. 0 keeps DSH's sizes.",
|
|
2292
|
+
"size.dialogLabel": "Conversation font size offset",
|
|
2293
|
+
"size.dialogHint":
|
|
2294
|
+
"Adds {offset} to the conversation text sizes, on top of DSH's own font-size setting. 0 keeps DSH's sizes.",
|
|
1089
2295
|
"size.codeLabel": "Code font size offset",
|
|
1090
2296
|
"size.codeHint":
|
|
1091
|
-
"Adds {offset} to code blocks and inline code only; the
|
|
2297
|
+
"Adds {offset} to code blocks and inline code only; the interface offset does not reach them. 0 keeps DSH's sizes.",
|
|
1092
2298
|
"size.unit": "px",
|
|
1093
2299
|
|
|
1094
|
-
"weight.
|
|
1095
|
-
"weight.
|
|
1096
|
-
"
|
|
2300
|
+
"weight.uiLabel": "Interface font weight",
|
|
2301
|
+
"weight.uiHint":
|
|
2302
|
+
"Sets the weight of the whole interface (sidebars, settings, buttons, headings). The conversation keeps its own.",
|
|
2303
|
+
"weight.dialogLabel": "Conversation font weight",
|
|
2304
|
+
"weight.dialogHint":
|
|
2305
|
+
"Sets the weight of the conversation markdown; unset keeps DSH's own weights.",
|
|
1097
2306
|
"weight.codeLabel": "Code font weight",
|
|
1098
2307
|
"weight.codeHint":
|
|
1099
|
-
"Overrides code blocks, inline code and terminal output only. 400 or unset keeps DSH's own weight
|
|
1100
|
-
|
|
2308
|
+
"Overrides code blocks, inline code and terminal output only. 400 or unset keeps DSH's own weight.",
|
|
2309
|
+
|
|
2310
|
+
"line.bodyLabel": "Interface line height",
|
|
2311
|
+
"line.bodyHint":
|
|
2312
|
+
"Scales every interface line height by {ratio}. 100% keeps DSH's own line heights.",
|
|
2313
|
+
"line.dialogLabel": "Conversation line height",
|
|
2314
|
+
"line.dialogHint":
|
|
2315
|
+
"Scales every conversation line height by {ratio}. 100% keeps DSH's own line heights.",
|
|
2316
|
+
"line.codeLabel": "Code line height",
|
|
2317
|
+
"line.codeHint":
|
|
2318
|
+
"Adds {offset} to code line heights only; 0 keeps DSH's own line heights.",
|
|
2319
|
+
"line.unit": "%",
|
|
2320
|
+
|
|
2321
|
+
"lig.label": "Code ligatures",
|
|
2322
|
+
"lig.hint":
|
|
2323
|
+
"Programming ligatures render multiple characters as one glyph (=> as an arrow, != as ≠). Browsers enable them by default.",
|
|
2324
|
+
"lig.default": "Default",
|
|
2325
|
+
"lig.on": "On",
|
|
2326
|
+
"lig.off": "Off",
|
|
2327
|
+
"feat.label": "Feature settings",
|
|
2328
|
+
"feat.hint":
|
|
2329
|
+
"Advanced font-feature-settings value, e.g. \"ss01\" on, \"cv01\" 1. Invalid values are ignored.",
|
|
2330
|
+
"feat.placeholder": "\"ss01\" on",
|
|
2331
|
+
|
|
2332
|
+
"synth.label": "Refuse synthetic styles",
|
|
2333
|
+
"synth.italic": "No faux italic",
|
|
2334
|
+
"synth.italicHint":
|
|
2335
|
+
"CJK faces have no italic, so the browser tilts them. On keeps marked text upright.",
|
|
2336
|
+
"synth.bold": "No faux bold",
|
|
2337
|
+
"synth.boldHint":
|
|
2338
|
+
"For faces without a real bold: bold text stops being thickened artificially.",
|
|
2339
|
+
"synth.simple": "No faux italic / faux bold",
|
|
2340
|
+
"synth.simpleHint":
|
|
2341
|
+
"Stops synthetic italic and synthetic bold everywhere; switch to Advanced to set them apart.",
|
|
2342
|
+
|
|
2343
|
+
"section.ui": "Interface",
|
|
2344
|
+
"section.dialog": "Conversation",
|
|
2345
|
+
"section.code": "Code",
|
|
2346
|
+
"section.fine": "Global fine-tuning",
|
|
2347
|
+
"section.presets": "Presets",
|
|
2348
|
+
"section.uiHint":
|
|
2349
|
+
"Sidebars, headings, buttons and every other chrome text.",
|
|
2350
|
+
"section.dialogHint":
|
|
2351
|
+
"The conversation markdown: paragraphs, tables and headings.",
|
|
2352
|
+
"section.codeHint": "Code blocks, inline code and terminal output.",
|
|
2353
|
+
"section.fineHint":
|
|
2354
|
+
"Settings that apply to the whole page.",
|
|
2355
|
+
"section.open": "Expand {name}",
|
|
2356
|
+
"section.close": "Collapse {name}",
|
|
2357
|
+
|
|
2358
|
+
"ui.follow": "Follows the conversation",
|
|
2359
|
+
"ui.followHint": "Font and weight",
|
|
2360
|
+
"ui.own": "Own values",
|
|
2361
|
+
"dialog.default": "DSH defaults",
|
|
2362
|
+
|
|
2363
|
+
"theme.label": "Per-theme values",
|
|
2364
|
+
"theme.on": "Light and dark keep separate values",
|
|
2365
|
+
"theme.edit": "Editing theme",
|
|
2366
|
+
"theme.light": "Light",
|
|
2367
|
+
"theme.dark": "Dark",
|
|
2368
|
+
"theme.mismatch":
|
|
2369
|
+
"The page is in {active} mode; the preview below shows the values being edited.",
|
|
2370
|
+
|
|
2371
|
+
"dialog.label": "Conversation font",
|
|
2372
|
+
"dialog.hint":
|
|
2373
|
+
"Applies to the conversation markdown (paragraphs, tables, headings); code surfaces keep the code font.",
|
|
2374
|
+
"dialogWest.label": "Conversation · Western",
|
|
2375
|
+
"dialogEast.label": "Conversation · CJK",
|
|
2376
|
+
|
|
2377
|
+
"preset.save": "Save current",
|
|
2378
|
+
"preset.namePlaceholder": "Preset name",
|
|
2379
|
+
"preset.apply": "Apply",
|
|
2380
|
+
"preset.overwrite": "Overwrite",
|
|
2381
|
+
"preset.delete": "Delete",
|
|
2382
|
+
"preset.export": "Export",
|
|
2383
|
+
"preset.import": "Import",
|
|
2384
|
+
"preset.importPlaceholder": "Paste an exported preset JSON here",
|
|
2385
|
+
"preset.label": "Presets",
|
|
2386
|
+
"preset.select": "Choose a preset",
|
|
2387
|
+
"preset.rename": "Rename",
|
|
2388
|
+
"preset.renamePlaceholder": "New name",
|
|
2389
|
+
"preset.renamed": "Renamed to “{name}”.",
|
|
2390
|
+
"preset.autoSave": "With a preset selected, every change saves into it automatically.",
|
|
2391
|
+
"preset.nameUsed": "That name is already taken.",
|
|
2392
|
+
"preset.saved": "Saved “{name}”.",
|
|
2393
|
+
"preset.applied": "Applied “{name}”.",
|
|
2394
|
+
"preset.deleted": "Deleted “{name}”.",
|
|
2395
|
+
"preset.updated": "Overwrote “{name}”.",
|
|
2396
|
+
"preset.exported": "Presets copied to the clipboard.",
|
|
2397
|
+
"preset.imported": "Imported {count} preset(s).",
|
|
2398
|
+
"preset.importBad": "That text is not a valid preset export.",
|
|
2399
|
+
"preset.full": "The preset list is full ({max}).",
|
|
2400
|
+
"preset.empty": "No presets saved yet.",
|
|
2401
|
+
"preset.nameTaken": "“{name}” already exists — saving overwrites it.",
|
|
1101
2402
|
|
|
1102
2403
|
"preview.label": "Preview",
|
|
1103
|
-
"preview.sansCaption": "
|
|
2404
|
+
"preview.sansCaption": "Interface",
|
|
2405
|
+
"preview.dialogCaption": "Conversation",
|
|
1104
2406
|
"preview.monoCaption": "Code",
|
|
1105
2407
|
"preview.sample":
|
|
1106
2408
|
"The quick brown fox jumps over the lazy dog — 中文排版预览,标点符号,数字 0123456789。",
|
|
1107
|
-
"preview.code": "const greet = (name) => `hello ${name}`; // 代码预览",
|
|
2409
|
+
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
1108
2410
|
|
|
1109
2411
|
"footnote.local":
|
|
1110
2412
|
"Stored in the Host settings document. Every change applies immediately.",
|
|
1111
2413
|
},
|
|
1112
2414
|
zh: {
|
|
1113
2415
|
"card.title": "字体增强",
|
|
1114
|
-
"card.description":
|
|
2416
|
+
"card.description":
|
|
2417
|
+
"界面与对话字体、各自的字号/字重/行高,代码增强与预设方案",
|
|
1115
2418
|
"card.expand": "展开",
|
|
1116
2419
|
"card.collapse": "收起",
|
|
1117
2420
|
"card.resetAll": "全部重置",
|
|
@@ -1119,8 +2422,10 @@ var DICTS = {
|
|
|
1119
2422
|
|
|
1120
2423
|
"common.overridden": "已修改",
|
|
1121
2424
|
"common.reset": "重置",
|
|
2425
|
+
"common.on": "开启",
|
|
2426
|
+
"common.off": "关闭",
|
|
1122
2427
|
|
|
1123
|
-
"sans.label": "
|
|
2428
|
+
"sans.label": "界面字体",
|
|
1124
2429
|
"sans.hint":
|
|
1125
2430
|
"按顺序回退:拉丁字体放前面、中文字体放后面;留空表示沿用 DSH 的字体栈。",
|
|
1126
2431
|
"mono.label": "代码字体",
|
|
@@ -1152,8 +2457,8 @@ var DICTS = {
|
|
|
1152
2457
|
"mode.advanced": "高级",
|
|
1153
2458
|
"mode.simpleHint":
|
|
1154
2459
|
"只管理栈最前面的西文/中文两项;你在高级模式里排好的其余回退原样保留。",
|
|
1155
|
-
"sansWest.label": "
|
|
1156
|
-
"sansEast.label": "
|
|
2460
|
+
"sansWest.label": "界面 · 西文字体",
|
|
2461
|
+
"sansEast.label": "界面 · 中文字体",
|
|
1157
2462
|
"monoWest.label": "代码 · 西文字体",
|
|
1158
2463
|
"monoEast.label": "代码 · 中文字体",
|
|
1159
2464
|
"split.pick": "选择…",
|
|
@@ -1161,28 +2466,118 @@ var DICTS = {
|
|
|
1161
2466
|
"split.remove": "移除 {name}",
|
|
1162
2467
|
"split.rest": "其余回退项(在高级模式中排序):{names}",
|
|
1163
2468
|
|
|
1164
|
-
"size.bodyLabel": "
|
|
2469
|
+
"size.bodyLabel": "界面字号偏移",
|
|
1165
2470
|
"size.bodyHint":
|
|
1166
|
-
"
|
|
2471
|
+
"给界面文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持原样。",
|
|
2472
|
+
"size.dialogLabel": "对话字号偏移",
|
|
2473
|
+
"size.dialogHint":
|
|
2474
|
+
"给对话文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持 DSH 原样。",
|
|
1167
2475
|
"size.codeLabel": "代码字号偏移",
|
|
1168
2476
|
"size.codeHint":
|
|
1169
|
-
"
|
|
2477
|
+
"只作用于代码块与行内代码,与界面字号互不影响;0 表示保持原样。",
|
|
1170
2478
|
"size.unit": "px",
|
|
1171
2479
|
|
|
1172
|
-
"weight.
|
|
1173
|
-
"weight.
|
|
1174
|
-
"
|
|
2480
|
+
"weight.uiLabel": "界面字重",
|
|
2481
|
+
"weight.uiHint":
|
|
2482
|
+
"设置整个界面的字重(侧栏、设置、按钮、标题);对话 Markdown 不受影响。",
|
|
2483
|
+
"weight.dialogLabel": "对话字重",
|
|
2484
|
+
"weight.dialogHint": "设置对话 Markdown 的粗细;未设置时保持 DSH 原本的粗细。",
|
|
1175
2485
|
"weight.codeLabel": "代码字重",
|
|
1176
2486
|
"weight.codeHint":
|
|
1177
|
-
"只覆盖代码块、行内代码与终端输出;400 或未设置表示保持 DSH
|
|
1178
|
-
|
|
2487
|
+
"只覆盖代码块、行内代码与终端输出;400 或未设置表示保持 DSH 原样。",
|
|
2488
|
+
|
|
2489
|
+
"line.bodyLabel": "界面行高",
|
|
2490
|
+
"line.bodyHint": "把界面行高整体缩放为 {ratio};100% 表示保持原样。",
|
|
2491
|
+
"line.dialogLabel": "对话行高",
|
|
2492
|
+
"line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
|
|
2493
|
+
"line.codeLabel": "代码行高",
|
|
2494
|
+
"line.codeHint": "只给代码行高加 {offset}px;0 表示保持原样。",
|
|
2495
|
+
"line.unit": "%",
|
|
2496
|
+
|
|
2497
|
+
"lig.label": "代码连字",
|
|
2498
|
+
"lig.hint":
|
|
2499
|
+
"把多个字符连成一个字形(如 => 变箭头、!= 变 ≠)。浏览器默认就是开启的。",
|
|
2500
|
+
"lig.default": "默认",
|
|
2501
|
+
"lig.on": "开启",
|
|
2502
|
+
"lig.off": "关闭",
|
|
2503
|
+
"feat.label": "特性设置",
|
|
2504
|
+
"feat.hint":
|
|
2505
|
+
"高级的 font-feature-settings 值,例如 ss01 on、cv01 1;非法值会被忽略。",
|
|
2506
|
+
"feat.placeholder": "\"ss01\" on",
|
|
2507
|
+
|
|
2508
|
+
"synth.label": "拒绝合成样式",
|
|
2509
|
+
"synth.italic": "禁用伪斜体",
|
|
2510
|
+
"synth.italicHint":
|
|
2511
|
+
"中文字体没有斜体,浏览器会把标记文本硬掰歪;开启后保持直立。",
|
|
2512
|
+
"synth.bold": "禁用伪粗体",
|
|
2513
|
+
"synth.boldHint": "没有真实粗体的字体不再被人为加粗。",
|
|
2514
|
+
"synth.simple": "禁用伪斜体/伪粗体",
|
|
2515
|
+
"synth.simpleHint":
|
|
2516
|
+
"整页停用合成斜体与合成粗体;切到高级模式可分开设置。",
|
|
2517
|
+
|
|
2518
|
+
"section.ui": "界面",
|
|
2519
|
+
"section.dialog": "对话",
|
|
2520
|
+
"section.code": "代码",
|
|
2521
|
+
"section.fine": "全局微调",
|
|
2522
|
+
"section.presets": "预设方案",
|
|
2523
|
+
"section.uiHint": "侧栏、标题、按钮等界面文字。",
|
|
2524
|
+
"section.dialogHint": "会话里的 Markdown:段落、表格与标题。",
|
|
2525
|
+
"section.codeHint": "代码块、行内代码与终端输出。",
|
|
2526
|
+
"section.fineHint": "作用于整页的选项。",
|
|
2527
|
+
"section.open": "展开{name}",
|
|
2528
|
+
"section.close": "收起{name}",
|
|
2529
|
+
|
|
2530
|
+
"theme.label": "分主题数值",
|
|
2531
|
+
"theme.on": "深浅色各存一套数值",
|
|
2532
|
+
"theme.edit": "正在编辑",
|
|
2533
|
+
"theme.light": "浅色",
|
|
2534
|
+
"theme.dark": "深色",
|
|
2535
|
+
"theme.mismatch": "当前页面是{active}主题,下方预览显示的是正在编辑的那套数值。",
|
|
2536
|
+
|
|
2537
|
+
"ui.follow": "跟随对话设置",
|
|
2538
|
+
"ui.followHint": "字体与字重",
|
|
2539
|
+
"ui.own": "独立数值",
|
|
2540
|
+
"dialog.default": "DSH 默认",
|
|
2541
|
+
|
|
2542
|
+
"dialog.label": "对话字体",
|
|
2543
|
+
"dialog.hint":
|
|
2544
|
+
"作用于会话里的 Markdown(段落、表格、标题);代码表面仍用代码字体。",
|
|
2545
|
+
"dialogWest.label": "对话 · 西文字体",
|
|
2546
|
+
"dialogEast.label": "对话 · 中文字体",
|
|
2547
|
+
|
|
2548
|
+
"preset.save": "保存当前",
|
|
2549
|
+
"preset.namePlaceholder": "方案名称",
|
|
2550
|
+
"preset.apply": "应用",
|
|
2551
|
+
"preset.overwrite": "覆盖",
|
|
2552
|
+
"preset.delete": "删除",
|
|
2553
|
+
"preset.export": "导出",
|
|
2554
|
+
"preset.import": "导入",
|
|
2555
|
+
"preset.importPlaceholder": "粘贴导出的方案内容",
|
|
2556
|
+
"preset.label": "预设方案",
|
|
2557
|
+
"preset.select": "选择方案",
|
|
2558
|
+
"preset.rename": "改名",
|
|
2559
|
+
"preset.renamePlaceholder": "新名字",
|
|
2560
|
+
"preset.renamed": "已改名为“{name}”。",
|
|
2561
|
+
"preset.autoSave": "选中方案后,所有改动自动存入该方案。",
|
|
2562
|
+
"preset.nameUsed": "这个名字已被占用。",
|
|
2563
|
+
"preset.saved": "已保存“{name}”。",
|
|
2564
|
+
"preset.applied": "已应用“{name}”。",
|
|
2565
|
+
"preset.deleted": "已删除“{name}”。",
|
|
2566
|
+
"preset.updated": "已覆盖“{name}”。",
|
|
2567
|
+
"preset.exported": "方案已复制到剪贴板。",
|
|
2568
|
+
"preset.imported": "已导入 {count} 个方案。",
|
|
2569
|
+
"preset.importBad": "这段内容不是有效的方案导出。",
|
|
2570
|
+
"preset.full": "方案列表已满({max})。",
|
|
2571
|
+
"preset.empty": "还没有保存方案。",
|
|
2572
|
+
"preset.nameTaken": "“{name}”已存在,保存将覆盖它。",
|
|
1179
2573
|
|
|
1180
2574
|
"preview.label": "预览",
|
|
1181
|
-
"preview.sansCaption": "
|
|
2575
|
+
"preview.sansCaption": "界面",
|
|
2576
|
+
"preview.dialogCaption": "对话",
|
|
1182
2577
|
"preview.monoCaption": "代码",
|
|
1183
2578
|
"preview.sample":
|
|
1184
2579
|
"The quick brown fox jumps over the lazy dog —— 中文排版预览,标点符号,数字 0123456789。",
|
|
1185
|
-
"preview.code": "const greet = (name) => `hello ${name}`; // 代码预览",
|
|
2580
|
+
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
1186
2581
|
|
|
1187
2582
|
"footnote.local": "保存在宿主设置文档里;每次改动立即生效。",
|
|
1188
2583
|
},
|
|
@@ -1241,15 +2636,64 @@ var CARD_CSS = [
|
|
|
1241
2636
|
".dfp-resetAll:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}",
|
|
1242
2637
|
".dfp-resetAll:disabled{opacity:.4;cursor:default}",
|
|
1243
2638
|
|
|
1244
|
-
".dfp-field{padding:
|
|
2639
|
+
".dfp-field{padding:14px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}",
|
|
1245
2640
|
".dfp-fieldLast{border-bottom:0}",
|
|
1246
2641
|
".dfp-fieldHead{align-items:center;gap:8px;display:flex}",
|
|
1247
2642
|
".dfp-fieldLabel{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}",
|
|
2643
|
+
// The control that sits in the same row as its label, right-aligned — the
|
|
2644
|
+
// same shape as dsh-quick-toc's settings card (`.dqt-pinline`), so the
|
|
2645
|
+
// cards' rows line up row for row.
|
|
2646
|
+
".dfp-inline{flex:1;justify-content:flex-end;align-items:center;gap:10px;display:flex}",
|
|
1248
2647
|
".dfp-overridden{flex:none;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:999px;padding:1px 8px;font-size:11px;line-height:16px}",
|
|
1249
2648
|
".dfp-fieldReset{flex:none;margin-left:auto;appearance:none;font:inherit;cursor:pointer;background:0 0;border:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:0}",
|
|
1250
2649
|
".dfp-fieldReset:hover{color:var(--dsw-alias-label-primary)}",
|
|
1251
2650
|
".dfp-hint{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:18px}",
|
|
1252
2651
|
|
|
2652
|
+
// accordion sections
|
|
2653
|
+
".dfp-section{border-bottom:.5px solid var(--dsw-alias-border-l2)}",
|
|
2654
|
+
".dfp-sectionLast{border-bottom:0}",
|
|
2655
|
+
".dfp-sectionHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;gap:8px;padding:12px 0;display:flex}",
|
|
2656
|
+
".dfp-sectionHead:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
|
|
2657
|
+
".dfp-sectionTitle{flex:none;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}",
|
|
2658
|
+
".dfp-sectionSummary{flex:1;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}",
|
|
2659
|
+
// While a section is open its summary disappears, so without this spacer the
|
|
2660
|
+
// chevron would slide left and sit next to the title; the spacer keeps the
|
|
2661
|
+
// arrow pinned to the far right in both states.
|
|
2662
|
+
".dfp-sectionSpacer{flex:1;min-width:0}",
|
|
2663
|
+
".dfp-sectionBody{padding:0 0 12px}",
|
|
2664
|
+
// The first control may not float away from the section title: the field
|
|
2665
|
+
// rows carry 16px top padding of their own, so the first one is tightened.
|
|
2666
|
+
".dfp-sectionBody>.dfp-field:first-child{padding-top:10px}",
|
|
2667
|
+
".dfp-sectionBody>.dfp-hint:first-child{margin-top:10px}",
|
|
2668
|
+
|
|
2669
|
+
// presets: one bar — a dropdown select plus rename/import/export. The
|
|
2670
|
+
// buttons must never wrap their two-character labels: they keep nowrap and
|
|
2671
|
+
// stay flex:none, so compressing the row eats the SELECT's width instead.
|
|
2672
|
+
".dfp-presetBar{align-items:center;gap:8px;margin-top:8px;display:flex}",
|
|
2673
|
+
".dfp-selectWrap{display:inline-flex;flex:1;min-width:0}",
|
|
2674
|
+
".dfp-select{flex:1;min-width:0;height:30px;box-sizing:border-box;appearance:none;font:inherit;cursor:pointer;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;padding:0 6px 0 10px;display:inline-flex;align-items:center;justify-content:space-between;gap:6px}",
|
|
2675
|
+
".dfp-select:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}",
|
|
2676
|
+
".dfp-select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
|
|
2677
|
+
".dfp-select:disabled{opacity:.4;cursor:default}",
|
|
2678
|
+
".dfp-selectName{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:18px;text-align:left}",
|
|
2679
|
+
".dfp-selectChevron{flex:none;display:block;color:var(--dsw-alias-label-tertiary);transition:transform .16s}",
|
|
2680
|
+
".dfp-selectChevronOpen{transform:rotate(180deg)}",
|
|
2681
|
+
".dfp-drop{position:fixed;z-index:1200;box-sizing:border-box;min-width:160px;max-height:260px;overflow-y:auto;padding:4px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:var(--dsw-shadow-lv3,0 12px 32px #00000024)}",
|
|
2682
|
+
".dfp-miniButton{appearance:none;font:inherit;cursor:pointer;white-space:nowrap;flex:none;border-radius:6px;padding:3px 8px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);background:0 0;border:.5px solid var(--dsw-alias-border-l3)}",
|
|
2683
|
+
".dfp-miniButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-specific-sidebar-nav-item-hover)}",
|
|
2684
|
+
".dfp-miniButton:disabled{opacity:.4;cursor:default}",
|
|
2685
|
+
".dfp-presetForm{align-items:center;gap:8px;margin-top:10px;display:flex}",
|
|
2686
|
+
".dfp-textInput{flex:1;min-width:0;box-sizing:border-box;padding:5px 8px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;font:inherit;font-size:13px;line-height:18px}",
|
|
2687
|
+
// The auto-save hint and the transient message share one row: the hint holds
|
|
2688
|
+
// the left, the message fades in on the right. The row is always mounted and
|
|
2689
|
+
// one line tall, so a message appearing can never push anything down.
|
|
2690
|
+
".dfp-presetMeta{align-items:center;gap:10px;margin-top:6px;display:flex;overflow:hidden}",
|
|
2691
|
+
// The hint never wraps: if a long message squeezes it, it truncates instead
|
|
2692
|
+
// of growing the row (a two-line row would push everything below it down).
|
|
2693
|
+
".dfp-presetHint{flex:1;min-width:0;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
2694
|
+
".dfp-status{flex:none;min-width:0;max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;opacity:0;transition:opacity .24s}",
|
|
2695
|
+
".dfp-statusOn{opacity:1}",
|
|
2696
|
+
|
|
1253
2697
|
".dfp-chips{align-items:center;flex-wrap:wrap;gap:6px;margin-top:10px;display:flex}",
|
|
1254
2698
|
".dfp-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
1255
2699
|
".dfp-chip{align-items:center;gap:2px;height:26px;padding:0 2px 0 4px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;display:inline-flex}",
|
|
@@ -1266,10 +2710,7 @@ var CARD_CSS = [
|
|
|
1266
2710
|
|
|
1267
2711
|
// Segmented / toggle controls follow dsh-quick-toc's settings card colours:
|
|
1268
2712
|
// the selected option takes the same --dsw-specific-sidebar-nav-item-active
|
|
1269
|
-
// surface (not just darker text). Shape: one joined pill with a divider
|
|
1270
|
-
// the card had before — quick-toc is being aligned to this shape instead.
|
|
1271
|
-
".dfp-modeRow{align-items:center;gap:10px;margin:14px 0 0;display:flex}",
|
|
1272
|
-
".dfp-modeLabel{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}",
|
|
2713
|
+
// surface (not just darker text). Shape: one joined pill with a divider.
|
|
1273
2714
|
".dfp-modeSeg{flex:none;display:inline-flex;overflow:hidden;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px}",
|
|
1274
2715
|
".dfp-modeButton{appearance:none;font:inherit;cursor:pointer;height:28px;padding:0 14px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-left:.5px solid var(--dsw-alias-border-l3);font-size:13px;line-height:18px}",
|
|
1275
2716
|
".dfp-modeButton:first-child{border-left:none}",
|
|
@@ -1284,6 +2725,10 @@ var CARD_CSS = [
|
|
|
1284
2725
|
".dfp-pick:disabled{opacity:.4;cursor:default}",
|
|
1285
2726
|
".dfp-pickEmpty{color:var(--dsw-alias-label-tertiary)}",
|
|
1286
2727
|
|
|
2728
|
+
// the per-theme switch is gone (light and dark share one value set); the
|
|
2729
|
+
// on/off controls are all joined segmented controls now, so no pill switch
|
|
2730
|
+
// exists in this card and every row keeps the same 28px control height.
|
|
2731
|
+
|
|
1287
2732
|
".dfp-panel{box-sizing:border-box;position:fixed;z-index:1200;flex-direction:column;width:320px;max-height:380px;padding:8px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:12px;box-shadow:var(--dsw-shadow-lv3,0 12px 32px #00000024);display:flex;gap:6px}",
|
|
1288
2733
|
".dfp-search{width:100%;box-sizing:border-box;padding:5px 8px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;font:inherit;font-size:13px;line-height:18px}",
|
|
1289
2734
|
".dfp-note{padding:4px 2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
|
|
@@ -1300,9 +2745,12 @@ var CARD_CSS = [
|
|
|
1300
2745
|
".dfp-value{flex:none;min-width:56px;text-align:right;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;font-variant-numeric:tabular-nums}",
|
|
1301
2746
|
".dfp-scale{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;display:flex;justify-content:space-between}",
|
|
1302
2747
|
|
|
2748
|
+
// the per-section preview: an inline box at the end of the expanded
|
|
2749
|
+
// section body, showing only that section's own sample
|
|
1303
2750
|
".dfp-previewBox{margin-top:10px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l2);border-radius:10px}",
|
|
1304
2751
|
".dfp-previewCaption{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin-bottom:4px}",
|
|
1305
2752
|
".dfp-previewText{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}",
|
|
2753
|
+
".dfp-previewCode{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}",
|
|
1306
2754
|
].join("");
|
|
1307
2755
|
|
|
1308
2756
|
/**
|
|
@@ -1363,9 +2811,9 @@ function isOwnSheet(sheet) {
|
|
|
1363
2811
|
* rule applies it reports the SCALED values, and feeding those back as bases
|
|
1364
2812
|
* compounds the offset on every refresh.
|
|
1365
2813
|
*
|
|
1366
|
-
*
|
|
1367
|
-
* too: they are
|
|
1368
|
-
*
|
|
2814
|
+
* `font` shorthand tokens (`--dsw-font-markdown-base` and friends) are
|
|
2815
|
+
* collected too: they are what the shipped stylesheets actually consume, and
|
|
2816
|
+
* the line-height axes rebuild their inline heights.
|
|
1369
2817
|
*
|
|
1370
2818
|
* @returns {Record<string, string>} token name to its untouched value.
|
|
1371
2819
|
*/
|
|
@@ -1428,6 +2876,15 @@ function readBaseTokens() {
|
|
|
1428
2876
|
|
|
1429
2877
|
/**
|
|
1430
2878
|
* Create the writer that keeps one `<style>` element in sync.
|
|
2879
|
+
*
|
|
2880
|
+
* The host half renders the same declarations into the served index so the
|
|
2881
|
+
* first frame is already correct, and that element carries this plugin's
|
|
2882
|
+
* `data-plugin-css` stamp. This half ADOPTS it instead of appending a second
|
|
2883
|
+
* copy: the served copy is only rebuilt on the next index render, so two
|
|
2884
|
+
* elements could never stay in sync — a rule a configuration no longer
|
|
2885
|
+
* produces (unset a weight, drop a size offset) would keep applying from the
|
|
2886
|
+
* stale copy until the user reloaded the page.
|
|
2887
|
+
*
|
|
1431
2888
|
* @param {() => Record<string, string>} tokens - reads the current base tokens.
|
|
1432
2889
|
* @returns {(config: unknown) => void} the applier.
|
|
1433
2890
|
*/
|
|
@@ -1437,9 +2894,13 @@ function createStylesheet(tokens) {
|
|
|
1437
2894
|
return function apply(config) {
|
|
1438
2895
|
if (typeof document === "undefined") return;
|
|
1439
2896
|
var css = buildFontCss(config, tokens());
|
|
1440
|
-
if (css === lastCss) return;
|
|
2897
|
+
if (css === lastCss && tag !== null && tag.isConnected) return;
|
|
1441
2898
|
lastCss = css;
|
|
1442
|
-
if (tag === null) {
|
|
2899
|
+
if (tag === null || !tag.isConnected) {
|
|
2900
|
+
// The served row first (it is already in the document), then our own.
|
|
2901
|
+
tag = document.querySelector('style[data-plugin-css="' + STYLE_TAG + '"]');
|
|
2902
|
+
}
|
|
2903
|
+
if (tag === null || !tag.isConnected) {
|
|
1443
2904
|
tag = document.createElement("style");
|
|
1444
2905
|
tag.dataset.plugin = "dsh-fonttune";
|
|
1445
2906
|
tag.dataset.pluginCss = STYLE_TAG;
|
|
@@ -1449,6 +2910,24 @@ function createStylesheet(tokens) {
|
|
|
1449
2910
|
};
|
|
1450
2911
|
}
|
|
1451
2912
|
|
|
2913
|
+
/**
|
|
2914
|
+
* Read the DSH default of one family variable, for the token override pairs.
|
|
2915
|
+
* @param {string} name - custom property name.
|
|
2916
|
+
* @returns {string} the live value, or "" when unreadable.
|
|
2917
|
+
*/
|
|
2918
|
+
function readDefaultFamily(name) {
|
|
2919
|
+
if (typeof document === "undefined") return "";
|
|
2920
|
+
try {
|
|
2921
|
+
var value = document.defaultView
|
|
2922
|
+
.getComputedStyle(document.body)
|
|
2923
|
+
.getPropertyValue(name)
|
|
2924
|
+
.trim();
|
|
2925
|
+
return value === "" ? "" : value;
|
|
2926
|
+
} catch (error) {
|
|
2927
|
+
return "";
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
|
|
1452
2931
|
/* ------------------------------------------------------------------ *
|
|
1453
2932
|
* hooks and controls
|
|
1454
2933
|
* ------------------------------------------------------------------ */
|
|
@@ -1514,24 +2993,67 @@ function FieldShell(props) {
|
|
|
1514
2993
|
},
|
|
1515
2994
|
props.t("common.reset")
|
|
1516
2995
|
)
|
|
1517
|
-
|
|
2996
|
+
: null
|
|
1518
2997
|
),
|
|
1519
2998
|
h("p", { className: "dfp-hint" }, props.hint),
|
|
1520
2999
|
props.children
|
|
1521
3000
|
);
|
|
1522
3001
|
}
|
|
1523
3002
|
|
|
3003
|
+
/**
|
|
3004
|
+
* One accordion section: a title row that reads like a summary line, and the
|
|
3005
|
+
* controls only when expanded. The card keeps at most one section open.
|
|
3006
|
+
* @param {object} props - copy, open state, toggle handler and children.
|
|
3007
|
+
* @returns {object} the section element.
|
|
3008
|
+
*/
|
|
3009
|
+
function Section(props) {
|
|
3010
|
+
return h(
|
|
3011
|
+
"div",
|
|
3012
|
+
{ className: "dfp-section" + (props.last ? " dfp-sectionLast" : ""), ref: props.wrapRef },
|
|
3013
|
+
h(
|
|
3014
|
+
"button",
|
|
3015
|
+
{
|
|
3016
|
+
type: "button",
|
|
3017
|
+
className: "dfp-sectionHead",
|
|
3018
|
+
"aria-expanded": props.open,
|
|
3019
|
+
"aria-label": props.t(props.open ? "section.close" : "section.open", { name: props.title }),
|
|
3020
|
+
onClick: props.onToggle,
|
|
3021
|
+
},
|
|
3022
|
+
h("span", { className: "dfp-sectionTitle" }, props.title),
|
|
3023
|
+
// Collapsed: the summary fills the middle. Expanded: an empty spacer
|
|
3024
|
+
// takes its place so the chevron never leaves the far right edge.
|
|
3025
|
+
props.open
|
|
3026
|
+
? h("span", { className: "dfp-sectionSpacer", "aria-hidden": "true" })
|
|
3027
|
+
: h("span", { className: "dfp-sectionSummary" }, props.summary),
|
|
3028
|
+
h(
|
|
3029
|
+
"svg",
|
|
3030
|
+
{
|
|
3031
|
+
className: "dfp-chevron" + (props.open ? " dfp-chevronOpen" : ""),
|
|
3032
|
+
width: 14,
|
|
3033
|
+
height: 14,
|
|
3034
|
+
viewBox: "0 0 14 14",
|
|
3035
|
+
fill: "none",
|
|
3036
|
+
"aria-hidden": "true",
|
|
3037
|
+
},
|
|
3038
|
+
h("path", { d: CHEVRON_PATH, fill: "currentColor" })
|
|
3039
|
+
)
|
|
3040
|
+
),
|
|
3041
|
+
props.open ? h("div", { className: "dfp-sectionBody" }, props.children) : null
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
1524
3045
|
/**
|
|
1525
3046
|
* A slider over a whole-number range with a live value readout.
|
|
1526
3047
|
* Dragging only moves a local value; the change handler runs once the
|
|
1527
3048
|
* pointer is released (or on blur/keyup), so intermediate steps never
|
|
1528
3049
|
* rewrite the settings document — the page does not recompute per pixel.
|
|
1529
|
-
* @param {object} props - range, value, labels, change handler and an
|
|
3050
|
+
* @param {object} props - range, step, value, labels, change handler and an
|
|
1530
3051
|
* optional pendingText(v) formatting the readout while dragging.
|
|
1531
3052
|
* @returns {object} the slider element.
|
|
1532
3053
|
*/
|
|
1533
3054
|
function NumberSlider(props) {
|
|
1534
3055
|
var [pending, setPending] = useState(null);
|
|
3056
|
+
var step = typeof props.step === "number" && props.step > 0 ? props.step : 1;
|
|
1535
3057
|
// The value the host has not confirmed yet. Between the release and the
|
|
1536
3058
|
// settings round-trip the committed prop is still the OLD number — clearing
|
|
1537
3059
|
// the local value right away would show that old number for one frame
|
|
@@ -1592,7 +3114,7 @@ function NumberSlider(props) {
|
|
|
1592
3114
|
className: "dfp-slider",
|
|
1593
3115
|
min: props.min,
|
|
1594
3116
|
max: props.max,
|
|
1595
|
-
step:
|
|
3117
|
+
step: step,
|
|
1596
3118
|
value: shown,
|
|
1597
3119
|
disabled: props.disabled,
|
|
1598
3120
|
"aria-label": props.label,
|
|
@@ -1617,6 +3139,178 @@ function NumberSlider(props) {
|
|
|
1617
3139
|
);
|
|
1618
3140
|
}
|
|
1619
3141
|
|
|
3142
|
+
/**
|
|
3143
|
+
* One joined segmented control over a small fixed option list.
|
|
3144
|
+
* @param {object} props - options [{value,label}], the current value, the
|
|
3145
|
+
* change handler and an optional aria label.
|
|
3146
|
+
* @returns {object} the segmented control element.
|
|
3147
|
+
*/
|
|
3148
|
+
function Segmented(props) {
|
|
3149
|
+
var options = props.options;
|
|
3150
|
+
return h(
|
|
3151
|
+
"div",
|
|
3152
|
+
{ className: "dfp-modeSeg", role: "group", "aria-label": props.label },
|
|
3153
|
+
options.map(function (option, index) {
|
|
3154
|
+
var active = option.value === props.value;
|
|
3155
|
+
return h(
|
|
3156
|
+
"button",
|
|
3157
|
+
{
|
|
3158
|
+
type: "button",
|
|
3159
|
+
key: option.value,
|
|
3160
|
+
className: "dfp-modeButton" + (active ? " dfp-modeButtonActive" : ""),
|
|
3161
|
+
"aria-pressed": active,
|
|
3162
|
+
disabled: props.disabled,
|
|
3163
|
+
onClick: function () {
|
|
3164
|
+
props.onChange(option.value);
|
|
3165
|
+
},
|
|
3166
|
+
},
|
|
3167
|
+
option.label
|
|
3168
|
+
);
|
|
3169
|
+
})
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* The preset dropdown: a trigger styled like a select box — the active
|
|
3175
|
+
* preset's name with the card's own chevron pointing down inside it on the
|
|
3176
|
+
* right — and a portaled menu listing the presets. Picking an entry applies
|
|
3177
|
+
* it, makes it the auto-save target and closes the menu. The menu is
|
|
3178
|
+
* portaled to `body` because the settings sheet's scroll container would
|
|
3179
|
+
* otherwise clip it.
|
|
3180
|
+
* @param {object} props - copy, the preset list, the active name, the pick
|
|
3181
|
+
* handler and the disabled flag.
|
|
3182
|
+
* @returns {object} the select element.
|
|
3183
|
+
*/
|
|
3184
|
+
function PresetSelect(props) {
|
|
3185
|
+
var t = props.t;
|
|
3186
|
+
var anchorRef = useRef(null);
|
|
3187
|
+
var panelRef = useRef(null);
|
|
3188
|
+
var [open, setOpen] = useState(false);
|
|
3189
|
+
var [position, setPosition] = useState(null);
|
|
3190
|
+
|
|
3191
|
+
useEffect(
|
|
3192
|
+
function () {
|
|
3193
|
+
if (!open) return undefined;
|
|
3194
|
+
var place = function () {
|
|
3195
|
+
var anchor = anchorRef.current;
|
|
3196
|
+
var view = globalThis;
|
|
3197
|
+
if (!anchor || typeof view.innerWidth !== "number") return;
|
|
3198
|
+
var rect = anchor.getBoundingClientRect();
|
|
3199
|
+
var margin = 10;
|
|
3200
|
+
var left = Math.max(margin, Math.min(rect.left, view.innerWidth - rect.width - margin));
|
|
3201
|
+
var below = rect.bottom + 4;
|
|
3202
|
+
var top = below;
|
|
3203
|
+
if (below + 240 > view.innerHeight - margin) {
|
|
3204
|
+
top = Math.max(margin, rect.top - 244);
|
|
3205
|
+
}
|
|
3206
|
+
setPosition({ left: left, top: top, minWidth: Math.max(rect.width, 160) });
|
|
3207
|
+
};
|
|
3208
|
+
place();
|
|
3209
|
+
var view = globalThis;
|
|
3210
|
+
view.addEventListener("resize", place);
|
|
3211
|
+
view.addEventListener("scroll", place, true);
|
|
3212
|
+
return function () {
|
|
3213
|
+
view.removeEventListener("resize", place);
|
|
3214
|
+
view.removeEventListener("scroll", place, true);
|
|
3215
|
+
};
|
|
3216
|
+
},
|
|
3217
|
+
[open]
|
|
3218
|
+
);
|
|
3219
|
+
|
|
3220
|
+
useEffect(
|
|
3221
|
+
function () {
|
|
3222
|
+
if (!open) return undefined;
|
|
3223
|
+
var onPointerDown = function (event) {
|
|
3224
|
+
var anchor = anchorRef.current;
|
|
3225
|
+
var panel = panelRef.current;
|
|
3226
|
+
if (anchor && anchor.contains(event.target)) return;
|
|
3227
|
+
if (panel && panel.contains(event.target)) return;
|
|
3228
|
+
setOpen(false);
|
|
3229
|
+
};
|
|
3230
|
+
var onKeyDown = function (event) {
|
|
3231
|
+
if (event.key === "Escape") setOpen(false);
|
|
3232
|
+
};
|
|
3233
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
3234
|
+
document.addEventListener("keydown", onKeyDown);
|
|
3235
|
+
return function () {
|
|
3236
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
3237
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
3238
|
+
};
|
|
3239
|
+
},
|
|
3240
|
+
[open]
|
|
3241
|
+
);
|
|
3242
|
+
|
|
3243
|
+
var menu = null;
|
|
3244
|
+
if (open) {
|
|
3245
|
+
var items = props.presets.map(function (entry, index) {
|
|
3246
|
+
var active = entry.name === props.value;
|
|
3247
|
+
return h(
|
|
3248
|
+
"button",
|
|
3249
|
+
{
|
|
3250
|
+
type: "button",
|
|
3251
|
+
className: "dfp-option",
|
|
3252
|
+
key: entry.name + ":" + index,
|
|
3253
|
+
role: "option",
|
|
3254
|
+
"aria-selected": active,
|
|
3255
|
+
onClick: function () {
|
|
3256
|
+
setOpen(false);
|
|
3257
|
+
props.onPick(entry);
|
|
3258
|
+
},
|
|
3259
|
+
},
|
|
3260
|
+
h("span", { className: "dfp-optionLabel" }, entry.name),
|
|
3261
|
+
active ? h("span", { className: "dfp-optionCheck" }, "✓") : null
|
|
3262
|
+
);
|
|
3263
|
+
});
|
|
3264
|
+
menu = createPortal(
|
|
3265
|
+
h(
|
|
3266
|
+
"div",
|
|
3267
|
+
{
|
|
3268
|
+
className: "dfp-drop",
|
|
3269
|
+
ref: panelRef,
|
|
3270
|
+
role: "listbox",
|
|
3271
|
+
"aria-label": t("preset.select"),
|
|
3272
|
+
style: position === null ? undefined : { left: position.left, top: position.top, minWidth: position.minWidth },
|
|
3273
|
+
},
|
|
3274
|
+
items
|
|
3275
|
+
),
|
|
3276
|
+
document.body
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
return h(
|
|
3281
|
+
"span",
|
|
3282
|
+
{ className: "dfp-selectWrap", ref: anchorRef },
|
|
3283
|
+
h(
|
|
3284
|
+
"button",
|
|
3285
|
+
{
|
|
3286
|
+
type: "button",
|
|
3287
|
+
className: "dfp-select",
|
|
3288
|
+
"aria-haspopup": "listbox",
|
|
3289
|
+
"aria-expanded": open,
|
|
3290
|
+
"aria-label": t("preset.select"),
|
|
3291
|
+
disabled: props.disabled === true,
|
|
3292
|
+
onClick: function () {
|
|
3293
|
+
setOpen(!open);
|
|
3294
|
+
},
|
|
3295
|
+
},
|
|
3296
|
+
h("span", { className: "dfp-selectName" }, props.value),
|
|
3297
|
+
h(
|
|
3298
|
+
"svg",
|
|
3299
|
+
{
|
|
3300
|
+
className: "dfp-selectChevron" + (open ? " dfp-selectChevronOpen" : ""),
|
|
3301
|
+
width: 14,
|
|
3302
|
+
height: 14,
|
|
3303
|
+
viewBox: "0 0 14 14",
|
|
3304
|
+
fill: "none",
|
|
3305
|
+
"aria-hidden": "true",
|
|
3306
|
+
},
|
|
3307
|
+
h("path", { d: CHEVRON_PATH, fill: "currentColor" })
|
|
3308
|
+
)
|
|
3309
|
+
),
|
|
3310
|
+
menu
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
3313
|
+
|
|
1620
3314
|
/* ------------------------------------------------------------------ *
|
|
1621
3315
|
* font enumeration
|
|
1622
3316
|
* ------------------------------------------------------------------ */
|
|
@@ -1688,7 +3382,7 @@ function measureCJK(name) {
|
|
|
1688
3382
|
var context = canvas.getContext("2d");
|
|
1689
3383
|
if (context === null) return null;
|
|
1690
3384
|
var probe = "中文字體测试";
|
|
1691
|
-
context.font =
|
|
3385
|
+
context.font = "72px " + quoteFamily(name) + ", monospace";
|
|
1692
3386
|
var withFamily = context.measureText(probe).width;
|
|
1693
3387
|
context.font = "72px monospace";
|
|
1694
3388
|
var without = context.measureText(probe).width;
|
|
@@ -2216,12 +3910,25 @@ function StackPicker(props) {
|
|
|
2216
3910
|
}
|
|
2217
3911
|
|
|
2218
3912
|
/* ------------------------------------------------------------------ *
|
|
2219
|
-
*
|
|
3913
|
+
* card helpers
|
|
2220
3914
|
* ------------------------------------------------------------------ */
|
|
2221
3915
|
|
|
2222
3916
|
/** localStorage key of the card's view mode. */
|
|
2223
3917
|
var MODE_KEY = "dsh-fonttune.mode.v1";
|
|
2224
3918
|
|
|
3919
|
+
/**
|
|
3920
|
+
* The five built-in preset slots, shown (and exported) until any preset has
|
|
3921
|
+
* been saved. They are data, not copy: the names stay as-is in every locale
|
|
3922
|
+
* and can be changed with the rename control.
|
|
3923
|
+
*/
|
|
3924
|
+
var DEFAULT_PRESET_NAMES = [
|
|
3925
|
+
"默认配置1",
|
|
3926
|
+
"默认配置2",
|
|
3927
|
+
"默认配置3",
|
|
3928
|
+
"默认配置4",
|
|
3929
|
+
"默认配置5",
|
|
3930
|
+
];
|
|
3931
|
+
|
|
2225
3932
|
/**
|
|
2226
3933
|
* Read the view mode the card opens in. Simple is the default; an unreadable
|
|
2227
3934
|
* or missing storage (tests, private modes) must not break the card.
|
|
@@ -2311,10 +4018,73 @@ function SimpleFamilyField(props) {
|
|
|
2311
4018
|
);
|
|
2312
4019
|
}
|
|
2313
4020
|
|
|
4021
|
+
/**
|
|
4022
|
+
* Summarize one axis set into the short texts the collapsed sections show.
|
|
4023
|
+
* @param {object} axis - the effective (follow-applied) value set.
|
|
4024
|
+
* @param {boolean} uiFollows - whether the interface follows the conversation.
|
|
4025
|
+
* @param {(key: string, params?: object) => string} t - translate seat.
|
|
4026
|
+
* @returns {{ui: string, dialog: string, code: string}}
|
|
4027
|
+
*/
|
|
4028
|
+
function sectionSummaries(axis, uiFollows, t) {
|
|
4029
|
+
var sign = function (value) {
|
|
4030
|
+
return value > 0 ? "+" + value : String(value);
|
|
4031
|
+
};
|
|
4032
|
+
// The conversation owns every axis, so its summary is always its values.
|
|
4033
|
+
var dialogParts = [];
|
|
4034
|
+
if (axis[STACK_DIALOG_FIELD] !== "") dialogParts.push(firstFamily(axis[STACK_DIALOG_FIELD]));
|
|
4035
|
+
if (axis[SIZE_DIALOG_FIELD] !== 0) dialogParts.push(sign(axis[SIZE_DIALOG_FIELD]) + "px");
|
|
4036
|
+
if (axis[LINE_HEIGHT_DIALOG_FIELD] !== LINE_HEIGHT_MIN) {
|
|
4037
|
+
dialogParts.push(axis[LINE_HEIGHT_DIALOG_FIELD] + "%");
|
|
4038
|
+
}
|
|
4039
|
+
if (axis[WEIGHT_DIALOG_FIELD] !== WEIGHT_UNSET) {
|
|
4040
|
+
dialogParts.push(String(axis[WEIGHT_DIALOG_FIELD]));
|
|
4041
|
+
}
|
|
4042
|
+
// The interface either follows (nothing of its own to report) or shows the
|
|
4043
|
+
// two axes it owns.
|
|
4044
|
+
var uiParts = [];
|
|
4045
|
+
if (!uiFollows) {
|
|
4046
|
+
var family = axis[SANS_FIELD] === "" ? null : firstFamily(axis[SANS_FIELD]);
|
|
4047
|
+
if (family !== null) uiParts.push(family);
|
|
4048
|
+
if (axis[WEIGHT_FIELD] !== WEIGHT_UNSET) uiParts.push(String(axis[WEIGHT_FIELD]));
|
|
4049
|
+
}
|
|
4050
|
+
var codeParts = [];
|
|
4051
|
+
if (axis[MONO_FIELD] !== "") codeParts.push(firstFamily(axis[MONO_FIELD]));
|
|
4052
|
+
if (axis[CODE_SIZE_FIELD] !== 0) codeParts.push(sign(axis[CODE_SIZE_FIELD]) + "px");
|
|
4053
|
+
if (axis[CODE_LINE_HEIGHT_FIELD] !== 0) codeParts.push(sign(axis[CODE_LINE_HEIGHT_FIELD]) + "px");
|
|
4054
|
+
if (axis[CODE_WEIGHT_FIELD] !== WEIGHT_UNSET) codeParts.push(String(axis[CODE_WEIGHT_FIELD]));
|
|
4055
|
+
if (axis[LIGATURES_FIELD] !== LIGATURES_DEFAULT) {
|
|
4056
|
+
codeParts.push(t(axis[LIGATURES_FIELD] === LIGATURES_OFF ? "lig.off" : "lig.on"));
|
|
4057
|
+
}
|
|
4058
|
+
return {
|
|
4059
|
+
ui: uiFollows ? t("ui.follow") : uiParts.length > 0 ? uiParts.join(" · ") : t("ui.own"),
|
|
4060
|
+
dialog: dialogParts.length > 0 ? dialogParts.join(" · ") : t("dialog.default"),
|
|
4061
|
+
code: codeParts.join(" · "),
|
|
4062
|
+
};
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
/** The first family of one stack value, or null. */
|
|
4066
|
+
function firstFamily(stack) {
|
|
4067
|
+
var families = parseStack(stack);
|
|
4068
|
+
return families.length > 0 ? families[0] : null;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
/* ------------------------------------------------------------------ *
|
|
4072
|
+
* the card
|
|
4073
|
+
* ------------------------------------------------------------------ */
|
|
4074
|
+
|
|
2314
4075
|
/**
|
|
2315
4076
|
* Render the plugin's card: the settings section dispatches this component
|
|
2316
4077
|
* under the namespace key, and the host has to serve that namespace for it to
|
|
2317
4078
|
* appear at all.
|
|
4079
|
+
*
|
|
4080
|
+
* Structure: a top bar (edit mode), the preset bar (dropdown select +
|
|
4081
|
+
* rename/import/export — with a preset selected every edit auto-saves into
|
|
4082
|
+
* it), and four collapsed accordion sections — conversation / interface /
|
|
4083
|
+
* code / global fine-tuning. Only one section is open at a time, so the card
|
|
4084
|
+
* never sprawls; the interface section leads with the follow switch, and an
|
|
4085
|
+
* expanded section ends with its own inline preview of just that part. Light
|
|
4086
|
+
* and dark themes share one value set (the per-theme editor ships in a later
|
|
4087
|
+
* release).
|
|
2318
4088
|
* @param {object} props - slot props (the injected face plus the locale seat).
|
|
2319
4089
|
* @returns {object} the card element.
|
|
2320
4090
|
*/
|
|
@@ -2323,10 +4093,50 @@ function FontCard(props) {
|
|
|
2323
4093
|
var scope = props.scope;
|
|
2324
4094
|
var [open, setOpen] = useState(false);
|
|
2325
4095
|
var [view, setView] = useState(readViewMode);
|
|
4096
|
+
var [expanded, setExpanded] = useState(null);
|
|
4097
|
+
var [presetName, setPresetName] = useState("");
|
|
4098
|
+
// The transient preset message: `{text, id}` — the id makes two identical
|
|
4099
|
+
// messages (two exports in a row) two distinct events, so the auto-hide
|
|
4100
|
+
// effect runs again instead of ignoring the second one.
|
|
4101
|
+
var [presetStatus, setPresetStatus] = useState({ text: "", id: 0 });
|
|
4102
|
+
var [statusShown, setStatusShown] = useState(false);
|
|
4103
|
+
var [renaming, setRenaming] = useState(false);
|
|
4104
|
+
var [importOpen, setImportOpen] = useState(false);
|
|
4105
|
+
/** True while the card itself writes a whole preset's values: the edits
|
|
4106
|
+
* must not mirror back into the very preset being applied. */
|
|
4107
|
+
var applyingRef = useRef(false);
|
|
4108
|
+
/** The pending auto-hide / fade-out timers of the preset message. */
|
|
4109
|
+
var statusIdRef = useRef(0);
|
|
4110
|
+
var statusTimerRef = useRef(null);
|
|
2326
4111
|
var snapshot = useScopeSnapshot(scope);
|
|
2327
4112
|
var config = normalizeConfig(snapshot.value);
|
|
4113
|
+
// Light and dark share one value set in this release: the stored flag (if
|
|
4114
|
+
// any) is ignored and the dark fields stay dormant in the document.
|
|
4115
|
+
config[PER_THEME_FIELD] = false;
|
|
4116
|
+
var sets = resolveAxes(config);
|
|
2328
4117
|
var user = snapshot.user !== null && typeof snapshot.user === "object" ? snapshot.user : {};
|
|
2329
4118
|
var writable = snapshot.writable === true;
|
|
4119
|
+
var presets = config[PRESETS_FIELD];
|
|
4120
|
+
|
|
4121
|
+
// The preset list the dropdown shows: the stored entries once anything has
|
|
4122
|
+
// been saved, otherwise the five built-in slots. `activePreset` falls back
|
|
4123
|
+
// to the first entry when unset or stale.
|
|
4124
|
+
var presetList =
|
|
4125
|
+
presets.length > 0
|
|
4126
|
+
? presets
|
|
4127
|
+
: DEFAULT_PRESET_NAMES.map(function (name) {
|
|
4128
|
+
return { name: name, values: {}, savedAt: 0 };
|
|
4129
|
+
});
|
|
4130
|
+
var activeName = config[ACTIVE_PRESET_FIELD];
|
|
4131
|
+
var activeKnown = false;
|
|
4132
|
+
for (var presetIndex = 0; presetIndex < presetList.length; presetIndex += 1) {
|
|
4133
|
+
if (presetList[presetIndex].name === activeName) activeKnown = true;
|
|
4134
|
+
}
|
|
4135
|
+
if (!activeKnown) activeName = presetList[0].name;
|
|
4136
|
+
|
|
4137
|
+
// Which value set the controls below edit and display: light and dark share
|
|
4138
|
+
// one set, so it is always the light (flat) fields.
|
|
4139
|
+
var editing = sets.light;
|
|
2330
4140
|
|
|
2331
4141
|
var changeView = function (next) {
|
|
2332
4142
|
setView(next);
|
|
@@ -2336,28 +4146,47 @@ function FontCard(props) {
|
|
|
2336
4146
|
// Unavailable storage only costs the persistence of the preference.
|
|
2337
4147
|
}
|
|
2338
4148
|
};
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
var
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
4149
|
+
|
|
4150
|
+
/**
|
|
4151
|
+
* Mirror one axis value into the active preset's snapshot — the auto-save
|
|
4152
|
+
* contract: with a preset selected, every change the user makes IS that
|
|
4153
|
+
* preset from now on. Skipped while the card itself applies a whole preset.
|
|
4154
|
+
* @param {string} field - an axis field name.
|
|
4155
|
+
* @param {string|number|boolean} value - the value to store.
|
|
4156
|
+
*/
|
|
4157
|
+
var mirrorPreset = function (field, value) {
|
|
4158
|
+
if (!writable || applyingRef.current === true) return;
|
|
4159
|
+
var index = -1;
|
|
4160
|
+
for (var i = 0; i < presetList.length; i += 1) {
|
|
4161
|
+
if (presetList[i].name === activeName) index = i;
|
|
4162
|
+
}
|
|
4163
|
+
if (index < 0) return;
|
|
4164
|
+
var entry = presetList[index];
|
|
4165
|
+
var values = {};
|
|
4166
|
+
for (var key in entry.values) {
|
|
4167
|
+
if (Object.prototype.hasOwnProperty.call(entry.values, key)) values[key] = entry.values[key];
|
|
4168
|
+
}
|
|
4169
|
+
values[field] = value;
|
|
4170
|
+
var next = presetList.slice();
|
|
4171
|
+
next[index] = { name: entry.name, values: values, savedAt: Date.now() };
|
|
4172
|
+
writePresets(next);
|
|
2358
4173
|
};
|
|
2359
4174
|
|
|
4175
|
+
/**
|
|
4176
|
+
* Write one axis field to the durable flat fields, and mirror the same
|
|
4177
|
+
* change into the active preset (the auto-save contract).
|
|
4178
|
+
* @param {string} field - an axis field name.
|
|
4179
|
+
* @param {string|number|boolean} value - the new value.
|
|
4180
|
+
*/
|
|
2360
4181
|
var setField = function (field, value) {
|
|
4182
|
+
submit(field, value);
|
|
4183
|
+
mirrorPreset(field, value);
|
|
4184
|
+
};
|
|
4185
|
+
var resetField = function (field) {
|
|
4186
|
+
clear(field);
|
|
4187
|
+
mirrorPreset(field, DEFAULTS[field]);
|
|
4188
|
+
};
|
|
4189
|
+
var submit = function (field, value) {
|
|
2361
4190
|
var result = scope.set(field, value);
|
|
2362
4191
|
if (result && typeof result.catch === "function") {
|
|
2363
4192
|
result.catch(function () {
|
|
@@ -2365,7 +4194,7 @@ function FontCard(props) {
|
|
|
2365
4194
|
});
|
|
2366
4195
|
}
|
|
2367
4196
|
};
|
|
2368
|
-
var
|
|
4197
|
+
var clear = function (field) {
|
|
2369
4198
|
var result = scope.unset(field);
|
|
2370
4199
|
if (result && typeof result.catch === "function") {
|
|
2371
4200
|
result.catch(function () {});
|
|
@@ -2375,42 +4204,114 @@ function FontCard(props) {
|
|
|
2375
4204
|
return Object.prototype.hasOwnProperty.call(user, field);
|
|
2376
4205
|
};
|
|
2377
4206
|
|
|
2378
|
-
|
|
2379
|
-
var
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
var
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
var sizeField = function (props) {
|
|
4207
|
+
// ---- interface axis handlers (legacy semantics kept) ----
|
|
4208
|
+
var pickWest = function (field, family) {
|
|
4209
|
+
setField(field, formatStack(setWestEntry(parseStack(editing[field]), family, classifyFamily)));
|
|
4210
|
+
};
|
|
4211
|
+
var pickEast = function (field, family) {
|
|
4212
|
+
setField(field, formatStack(setEastEntry(parseStack(edit[field]), family, classifyFamily)));
|
|
4213
|
+
};
|
|
4214
|
+
var dropEntry = function (field, family) {
|
|
4215
|
+
setField(field, formatStack(removeStackEntry(parseStack(edit[field]), family)));
|
|
4216
|
+
};
|
|
4217
|
+
|
|
4218
|
+
// ---- size slider ----
|
|
4219
|
+
var sizeField = function (props) {
|
|
4220
|
+
return h(
|
|
4221
|
+
FieldShell,
|
|
4222
|
+
{
|
|
4223
|
+
t: t,
|
|
4224
|
+
label: t(props.labelKey),
|
|
4225
|
+
hint: t(props.hintKey, { offset: props.text }),
|
|
4226
|
+
overridden: overridden(props.field) && props.value !== 0,
|
|
4227
|
+
disabled: !writable,
|
|
4228
|
+
onReset: function () {
|
|
4229
|
+
resetField(props.field);
|
|
4230
|
+
},
|
|
4231
|
+
},
|
|
4232
|
+
h(NumberSlider, {
|
|
4233
|
+
min: SIZE_MIN,
|
|
4234
|
+
max: SIZE_MAX,
|
|
4235
|
+
value: props.value,
|
|
4236
|
+
disabled: !writable,
|
|
4237
|
+
label: t(props.labelKey),
|
|
4238
|
+
readout: props.text + " " + t("size.unit"),
|
|
4239
|
+
minLabel: SIZE_MIN + " " + t("size.unit"),
|
|
4240
|
+
maxLabel: "+" + SIZE_MAX + " " + t("size.unit"),
|
|
4241
|
+
pendingText: function (value) {
|
|
4242
|
+
return (value > 0 ? "+" + value : String(value)) + " " + t("size.unit");
|
|
4243
|
+
},
|
|
4244
|
+
onChange: function (value) {
|
|
4245
|
+
setField(props.field, value);
|
|
4246
|
+
},
|
|
4247
|
+
})
|
|
4248
|
+
);
|
|
4249
|
+
};
|
|
4250
|
+
|
|
4251
|
+
// ---- weight slider ----
|
|
4252
|
+
var weightField = function (props) {
|
|
4253
|
+
return h(
|
|
4254
|
+
FieldShell,
|
|
4255
|
+
{
|
|
4256
|
+
t: t,
|
|
4257
|
+
label: t(props.labelKey),
|
|
4258
|
+
hint: t(props.hintKey),
|
|
4259
|
+
overridden: overridden(props.field) && props.value !== WEIGHT_UNSET,
|
|
4260
|
+
disabled: !writable,
|
|
4261
|
+
onReset: function () {
|
|
4262
|
+
resetField(props.field);
|
|
4263
|
+
},
|
|
4264
|
+
},
|
|
4265
|
+
h(NumberSlider, {
|
|
4266
|
+
min: WEIGHT_MIN,
|
|
4267
|
+
max: WEIGHT_MAX,
|
|
4268
|
+
value: props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value,
|
|
4269
|
+
disabled: !writable,
|
|
4270
|
+
label: t(props.labelKey),
|
|
4271
|
+
// "Unset" IS 400 here: that is the weight DSH uses for body text, the
|
|
4272
|
+
// slider already sits at 400, and the hint says 400 keeps DSH's own.
|
|
4273
|
+
// Printing "unset" made the readout disagree with the control.
|
|
4274
|
+
readout: String(props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value),
|
|
4275
|
+
minLabel: String(WEIGHT_MIN),
|
|
4276
|
+
maxLabel: String(WEIGHT_MAX),
|
|
4277
|
+
onChange: function (value) {
|
|
4278
|
+
// 400 is DSH's own body weight, so choosing it means "leave the axis
|
|
4279
|
+
// alone" rather than "write 400 on every element".
|
|
4280
|
+
if (value === NEUTRAL_WEIGHT) resetField(props.field);
|
|
4281
|
+
else setField(props.field, value);
|
|
4282
|
+
},
|
|
4283
|
+
})
|
|
4284
|
+
);
|
|
4285
|
+
};
|
|
4286
|
+
|
|
4287
|
+
// ---- interface/dialog line-height slider (percent ratio) ----
|
|
4288
|
+
var lineField = function (props) {
|
|
2391
4289
|
return h(
|
|
2392
4290
|
FieldShell,
|
|
2393
4291
|
{
|
|
2394
4292
|
t: t,
|
|
2395
4293
|
label: t(props.labelKey),
|
|
2396
|
-
hint: t(props.hintKey, {
|
|
2397
|
-
overridden: overridden(props.field) && props.value !==
|
|
4294
|
+
hint: t(props.hintKey, { ratio: (props.value / LINE_HEIGHT_MIN).toFixed(2) + "×" }),
|
|
4295
|
+
overridden: overridden(props.field) && props.value !== LINE_HEIGHT_MIN,
|
|
2398
4296
|
disabled: !writable,
|
|
2399
4297
|
onReset: function () {
|
|
2400
4298
|
resetField(props.field);
|
|
2401
4299
|
},
|
|
2402
4300
|
},
|
|
2403
4301
|
h(NumberSlider, {
|
|
2404
|
-
min:
|
|
2405
|
-
max:
|
|
4302
|
+
min: LINE_HEIGHT_MIN,
|
|
4303
|
+
max: LINE_HEIGHT_MAX,
|
|
4304
|
+
step: 5,
|
|
2406
4305
|
value: props.value,
|
|
2407
4306
|
disabled: !writable,
|
|
2408
4307
|
label: t(props.labelKey),
|
|
2409
|
-
readout: props.
|
|
2410
|
-
minLabel:
|
|
2411
|
-
maxLabel:
|
|
2412
|
-
|
|
2413
|
-
|
|
4308
|
+
readout: props.value + " " + t("line.unit"),
|
|
4309
|
+
minLabel: LINE_HEIGHT_MIN + " " + t("line.unit"),
|
|
4310
|
+
maxLabel: LINE_HEIGHT_MAX + " " + t("line.unit"),
|
|
4311
|
+
// The unit rides the pending readout too: without it the "%" vanished
|
|
4312
|
+
// while dragging and popped back on release.
|
|
4313
|
+
pendingText: function (pending) {
|
|
4314
|
+
return pending + " " + t("line.unit");
|
|
2414
4315
|
},
|
|
2415
4316
|
onChange: function (value) {
|
|
2416
4317
|
setField(props.field, value);
|
|
@@ -2419,43 +4320,356 @@ function FontCard(props) {
|
|
|
2419
4320
|
);
|
|
2420
4321
|
};
|
|
2421
4322
|
|
|
4323
|
+
// ---- code line-height slider (additive px) ----
|
|
4324
|
+
var codeLineField = function () {
|
|
4325
|
+
var value = editing[CODE_LINE_HEIGHT_FIELD];
|
|
4326
|
+
return h(
|
|
4327
|
+
FieldShell,
|
|
4328
|
+
{
|
|
4329
|
+
t: t,
|
|
4330
|
+
label: t("line.codeLabel"),
|
|
4331
|
+
hint: t("line.codeHint", { offset: value > 0 ? "+" + value : String(value) }),
|
|
4332
|
+
overridden: overridden(CODE_LINE_HEIGHT_FIELD) && value !== 0,
|
|
4333
|
+
disabled: !writable,
|
|
4334
|
+
onReset: function () {
|
|
4335
|
+
resetField(CODE_LINE_HEIGHT_FIELD);
|
|
4336
|
+
},
|
|
4337
|
+
},
|
|
4338
|
+
h(NumberSlider, {
|
|
4339
|
+
min: CODE_LINE_HEIGHT_MIN,
|
|
4340
|
+
max: CODE_LINE_HEIGHT_MAX,
|
|
4341
|
+
value: value,
|
|
4342
|
+
disabled: !writable,
|
|
4343
|
+
label: t("line.codeLabel"),
|
|
4344
|
+
readout: (value > 0 ? "+" + value : String(value)) + "px",
|
|
4345
|
+
minLabel: CODE_LINE_HEIGHT_MIN + "px",
|
|
4346
|
+
maxLabel: "+" + CODE_LINE_HEIGHT_MAX + "px",
|
|
4347
|
+
pendingText: function (pending) {
|
|
4348
|
+
return (pending > 0 ? "+" + pending : String(pending)) + "px";
|
|
4349
|
+
},
|
|
4350
|
+
onChange: function (next) {
|
|
4351
|
+
setField(CODE_LINE_HEIGHT_FIELD, next);
|
|
4352
|
+
},
|
|
4353
|
+
})
|
|
4354
|
+
);
|
|
4355
|
+
};
|
|
4356
|
+
|
|
4357
|
+
// ---- ligature segmented control ----
|
|
4358
|
+
var ligatureField = function () {
|
|
4359
|
+
var value = editing[LIGATURES_FIELD];
|
|
4360
|
+
return h(
|
|
4361
|
+
FieldShell,
|
|
4362
|
+
{
|
|
4363
|
+
t: t,
|
|
4364
|
+
label: t("lig.label"),
|
|
4365
|
+
hint: t("lig.hint"),
|
|
4366
|
+
overridden: overridden(LIGATURES_FIELD) && value !== LIGATURES_DEFAULT,
|
|
4367
|
+
disabled: !writable,
|
|
4368
|
+
onReset: function () {
|
|
4369
|
+
resetField(LIGATURES_FIELD);
|
|
4370
|
+
},
|
|
4371
|
+
},
|
|
4372
|
+
h(Segmented, {
|
|
4373
|
+
label: t("lig.label"),
|
|
4374
|
+
value: value,
|
|
4375
|
+
options: [
|
|
4376
|
+
{ value: LIGATURES_DEFAULT, label: t("lig.default") },
|
|
4377
|
+
{ value: LIGATURES_ON, label: t("lig.on") },
|
|
4378
|
+
{ value: LIGATURES_OFF, label: t("lig.off") },
|
|
4379
|
+
],
|
|
4380
|
+
onChange: function (next) {
|
|
4381
|
+
setField(LIGATURES_FIELD, next);
|
|
4382
|
+
},
|
|
4383
|
+
})
|
|
4384
|
+
);
|
|
4385
|
+
};
|
|
4386
|
+
|
|
4387
|
+
// ---- advanced feature-settings text field ----
|
|
4388
|
+
var featuresField = function () {
|
|
4389
|
+
var value = editing[FEATURES_FIELD];
|
|
4390
|
+
return h(
|
|
4391
|
+
FieldShell,
|
|
4392
|
+
{
|
|
4393
|
+
t: t,
|
|
4394
|
+
label: t("feat.label"),
|
|
4395
|
+
hint: t("feat.hint"),
|
|
4396
|
+
overridden: overridden(FEATURES_FIELD) && value !== "",
|
|
4397
|
+
disabled: !writable,
|
|
4398
|
+
onReset: function () {
|
|
4399
|
+
resetField(FEATURES_FIELD);
|
|
4400
|
+
},
|
|
4401
|
+
},
|
|
4402
|
+
h("input", {
|
|
4403
|
+
type: "text",
|
|
4404
|
+
className: "dfp-search",
|
|
4405
|
+
spellCheck: false,
|
|
4406
|
+
value: value,
|
|
4407
|
+
disabled: !writable,
|
|
4408
|
+
"aria-label": t("feat.label"),
|
|
4409
|
+
placeholder: t("feat.placeholder"),
|
|
4410
|
+
onChange: function (event) {
|
|
4411
|
+
setField(FEATURES_FIELD, event.target.value);
|
|
4412
|
+
},
|
|
4413
|
+
})
|
|
4414
|
+
);
|
|
4415
|
+
};
|
|
4416
|
+
|
|
4417
|
+
// ---- synthesis switches ----
|
|
4418
|
+
var synthesisField = function (props) {
|
|
4419
|
+
var value = editing[props.field] === true;
|
|
4420
|
+
return h(
|
|
4421
|
+
"div",
|
|
4422
|
+
{ className: "dfp-field" },
|
|
4423
|
+
h(
|
|
4424
|
+
"div",
|
|
4425
|
+
{ className: "dfp-fieldHead", role: "group", "aria-label": t(props.labelKey) },
|
|
4426
|
+
h("span", { className: "dfp-fieldLabel" }, t(props.labelKey)),
|
|
4427
|
+
h(
|
|
4428
|
+
"div",
|
|
4429
|
+
{ className: "dfp-inline" },
|
|
4430
|
+
h(Segmented, {
|
|
4431
|
+
label: t(props.labelKey),
|
|
4432
|
+
value: value ? "on" : "off",
|
|
4433
|
+
options: [
|
|
4434
|
+
{ value: "on", label: t("common.on") },
|
|
4435
|
+
{ value: "off", label: t("common.off") },
|
|
4436
|
+
],
|
|
4437
|
+
disabled: !writable,
|
|
4438
|
+
onChange: function (next) {
|
|
4439
|
+
if (next === "on") setField(props.field, true);
|
|
4440
|
+
else resetField(props.field);
|
|
4441
|
+
},
|
|
4442
|
+
})
|
|
4443
|
+
)
|
|
4444
|
+
),
|
|
4445
|
+
h("p", { className: "dfp-hint" }, t(props.hintKey))
|
|
4446
|
+
);
|
|
4447
|
+
};
|
|
4448
|
+
|
|
4449
|
+
// ---- presets ----
|
|
2422
4450
|
/**
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2425
|
-
*
|
|
4451
|
+
* Show a transient preset message: it fades in on the same row as the
|
|
4452
|
+
* auto-save hint and fades out by itself after a moment, so it can never
|
|
4453
|
+
* pile up on screen and never pushes the sections below it down (the row it
|
|
4454
|
+
* lives in is always mounted, only its opacity changes).
|
|
4455
|
+
* @param {string} text - the message.
|
|
2426
4456
|
*/
|
|
2427
|
-
var
|
|
4457
|
+
var showStatus = function (text) {
|
|
4458
|
+
statusIdRef.current += 1;
|
|
4459
|
+
setPresetStatus({ text: text, id: statusIdRef.current });
|
|
4460
|
+
};
|
|
4461
|
+
useEffect(
|
|
4462
|
+
function () {
|
|
4463
|
+
if (presetStatus.text === "") return undefined;
|
|
4464
|
+
setStatusShown(true);
|
|
4465
|
+
var hide = globalThis.setTimeout(function () {
|
|
4466
|
+
setStatusShown(false); // the CSS transition fades it out
|
|
4467
|
+
statusTimerRef.current = globalThis.setTimeout(function () {
|
|
4468
|
+
statusTimerRef.current = null;
|
|
4469
|
+
setPresetStatus({ text: "", id: statusIdRef.current });
|
|
4470
|
+
}, 240);
|
|
4471
|
+
}, 2600);
|
|
4472
|
+
return function () {
|
|
4473
|
+
globalThis.clearTimeout(hide);
|
|
4474
|
+
if (statusTimerRef.current !== null) {
|
|
4475
|
+
globalThis.clearTimeout(statusTimerRef.current);
|
|
4476
|
+
statusTimerRef.current = null;
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
},
|
|
4480
|
+
[presetStatus.id]
|
|
4481
|
+
);
|
|
4482
|
+
|
|
4483
|
+
var writePresets = function (next) {
|
|
4484
|
+
if (next.length > shared.MAX_PRESETS) next = next.slice(0, shared.MAX_PRESETS);
|
|
4485
|
+
submit(PRESETS_FIELD, JSON.stringify(next));
|
|
4486
|
+
};
|
|
4487
|
+
|
|
4488
|
+
/**
|
|
4489
|
+
* Switch presets: the entry becomes the auto-save target and its stored
|
|
4490
|
+
* values are applied over the set currently being edited — every axis the
|
|
4491
|
+
* entry does not store falls back to its neutral default, so a slot switch
|
|
4492
|
+
* is exact.
|
|
4493
|
+
* @param {{name: string, values: object}} entry - the picked preset.
|
|
4494
|
+
*/
|
|
4495
|
+
var switchPreset = function (entry) {
|
|
4496
|
+
if (!writable || entry.name === activeName) return;
|
|
4497
|
+
applyingRef.current = true;
|
|
4498
|
+
try {
|
|
4499
|
+
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
4500
|
+
var field = VALUE_FIELDS[index];
|
|
4501
|
+
var value = Object.prototype.hasOwnProperty.call(entry.values, field)
|
|
4502
|
+
? entry.values[field]
|
|
4503
|
+
: DEFAULTS[field];
|
|
4504
|
+
setField(field, value);
|
|
4505
|
+
}
|
|
4506
|
+
submit(ACTIVE_PRESET_FIELD, entry.name);
|
|
4507
|
+
} finally {
|
|
4508
|
+
applyingRef.current = false;
|
|
4509
|
+
}
|
|
4510
|
+
showStatus(t("preset.applied", { name: entry.name }));
|
|
4511
|
+
};
|
|
4512
|
+
|
|
4513
|
+
/** Rename the active preset in place; its stored values ride along. */
|
|
4514
|
+
var renamePreset = function () {
|
|
4515
|
+
setRenaming(false);
|
|
4516
|
+
var name = presetName.trim().slice(0, shared.MAX_PRESET_NAME);
|
|
4517
|
+
setPresetName("");
|
|
4518
|
+
if (name === "" || !writable || name === activeName) return;
|
|
4519
|
+
var taken = false;
|
|
4520
|
+
var activeIndex = -1;
|
|
4521
|
+
for (var index = 0; index < presetList.length; index += 1) {
|
|
4522
|
+
if (presetList[index].name.toLowerCase() === name.toLowerCase()) taken = true;
|
|
4523
|
+
if (presetList[index].name === activeName) activeIndex = index;
|
|
4524
|
+
}
|
|
4525
|
+
if (taken || activeIndex < 0) {
|
|
4526
|
+
showStatus(t("preset.nameUsed"));
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
var next = presetList.slice();
|
|
4530
|
+
next[activeIndex] = {
|
|
4531
|
+
name: name,
|
|
4532
|
+
values: presetList[activeIndex].values,
|
|
4533
|
+
savedAt: presetList[activeIndex].savedAt,
|
|
4534
|
+
};
|
|
4535
|
+
writePresets(next);
|
|
4536
|
+
submit(ACTIVE_PRESET_FIELD, name);
|
|
4537
|
+
showStatus(t("preset.renamed", { name: name }));
|
|
4538
|
+
};
|
|
4539
|
+
|
|
4540
|
+
var exportPresets = function () {
|
|
4541
|
+
var text = JSON.stringify(presetList);
|
|
4542
|
+
var navigator = globalThis.navigator;
|
|
4543
|
+
if (navigator !== undefined && navigator.clipboard !== undefined) {
|
|
4544
|
+
navigator.clipboard
|
|
4545
|
+
.writeText(text)
|
|
4546
|
+
.then(function () {
|
|
4547
|
+
showStatus(t("preset.exported"));
|
|
4548
|
+
})
|
|
4549
|
+
.catch(function () {
|
|
4550
|
+
showStatus(text.slice(0, 200) + "…");
|
|
4551
|
+
});
|
|
4552
|
+
} else {
|
|
4553
|
+
showStatus(text.slice(0, 120) + "…");
|
|
4554
|
+
}
|
|
4555
|
+
};
|
|
4556
|
+
var importPresets = function (text) {
|
|
4557
|
+
var parsed = normalizePresets(text);
|
|
4558
|
+
if (parsed.length === 0) {
|
|
4559
|
+
showStatus(t("preset.importBad"));
|
|
4560
|
+
return;
|
|
4561
|
+
}
|
|
4562
|
+
// Imported presets merge into the list by name, imported values win.
|
|
4563
|
+
var merged = presetList.slice();
|
|
4564
|
+
for (var entryIndex = 0; entryIndex < parsed.length; entryIndex += 1) {
|
|
4565
|
+
var entry = parsed[entryIndex];
|
|
4566
|
+
var replaced = false;
|
|
4567
|
+
for (var existingIndex = 0; existingIndex < merged.length; existingIndex += 1) {
|
|
4568
|
+
if (merged[existingIndex].name.toLowerCase() === entry.name.toLowerCase()) {
|
|
4569
|
+
merged[existingIndex] = entry;
|
|
4570
|
+
replaced = true;
|
|
4571
|
+
break;
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
if (!replaced) merged.push(entry);
|
|
4575
|
+
}
|
|
4576
|
+
writePresets(merged);
|
|
4577
|
+
setImportOpen(false);
|
|
4578
|
+
showStatus(t("preset.imported", { count: parsed.length }));
|
|
4579
|
+
};
|
|
4580
|
+
|
|
4581
|
+
// the family field, in either mode
|
|
4582
|
+
var familyField = function (props) {
|
|
4583
|
+
var stack = editing[props.field];
|
|
4584
|
+
if (view === "simple") {
|
|
4585
|
+
return h(SimpleFamilyField, {
|
|
4586
|
+
t: t,
|
|
4587
|
+
label: t(props.labelKey),
|
|
4588
|
+
westLabel: props.westLabel,
|
|
4589
|
+
eastLabel: props.eastLabel,
|
|
4590
|
+
slots: deriveSlots(stack),
|
|
4591
|
+
disabled: !writable,
|
|
4592
|
+
onPickWest: function (family) {
|
|
4593
|
+
pickWest(props.field, family);
|
|
4594
|
+
},
|
|
4595
|
+
onPickEast: function (family) {
|
|
4596
|
+
pickEast(props.field, family);
|
|
4597
|
+
},
|
|
4598
|
+
onRemoveWest: function () {
|
|
4599
|
+
var slots = deriveSlots(stack);
|
|
4600
|
+
if (slots.west !== null) dropEntry(props.field, slots.west);
|
|
4601
|
+
},
|
|
4602
|
+
onRemoveEast: function () {
|
|
4603
|
+
var slots = deriveSlots(stack);
|
|
4604
|
+
if (slots.east !== null) dropEntry(props.field, slots.east);
|
|
4605
|
+
},
|
|
4606
|
+
});
|
|
4607
|
+
}
|
|
2428
4608
|
return h(
|
|
2429
4609
|
FieldShell,
|
|
2430
4610
|
{
|
|
2431
4611
|
t: t,
|
|
2432
4612
|
label: t(props.labelKey),
|
|
2433
4613
|
hint: t(props.hintKey),
|
|
2434
|
-
overridden: overridden(props.field)
|
|
4614
|
+
overridden: overridden(props.field),
|
|
2435
4615
|
disabled: !writable,
|
|
2436
4616
|
onReset: function () {
|
|
2437
4617
|
resetField(props.field);
|
|
2438
4618
|
},
|
|
2439
4619
|
},
|
|
2440
|
-
h(
|
|
2441
|
-
|
|
2442
|
-
max: WEIGHT_MAX,
|
|
2443
|
-
value: props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value,
|
|
2444
|
-
disabled: !writable,
|
|
4620
|
+
h(StackPicker, {
|
|
4621
|
+
t: t,
|
|
2445
4622
|
label: t(props.labelKey),
|
|
2446
|
-
|
|
2447
|
-
minLabel: String(WEIGHT_MIN),
|
|
2448
|
-
maxLabel: String(WEIGHT_MAX),
|
|
4623
|
+
value: stack,
|
|
2449
4624
|
onChange: function (value) {
|
|
2450
|
-
|
|
2451
|
-
// alone" rather than "write 400 on every element".
|
|
2452
|
-
if (value === NEUTRAL_WEIGHT) resetField(props.field);
|
|
2453
|
-
else setField(props.field, value);
|
|
4625
|
+
setField(props.field, value);
|
|
2454
4626
|
},
|
|
2455
4627
|
})
|
|
2456
4628
|
);
|
|
2457
4629
|
};
|
|
2458
4630
|
|
|
4631
|
+
// The interface follows the conversation (default): of the two axes the
|
|
4632
|
+
// interface owns, the conversation's value wins and its own stays the
|
|
4633
|
+
// fallback. The switch lives in the interface section.
|
|
4634
|
+
var uiFollows = config[UI_FOLLOWS_FIELD] !== false;
|
|
4635
|
+
|
|
4636
|
+
var summaries = sectionSummaries(editing, uiFollows, t);
|
|
4637
|
+
|
|
4638
|
+
// The interface owns one axis now (its family); its preview shows that and
|
|
4639
|
+
// nothing else — the weight, size and line height all come from the
|
|
4640
|
+
// conversation section.
|
|
4641
|
+
var previewUiStyle = {};
|
|
4642
|
+
if (editing[SANS_FIELD] !== "") {
|
|
4643
|
+
previewUiStyle.fontFamily = formatStack(parseStack(editing[SANS_FIELD]));
|
|
4644
|
+
}
|
|
4645
|
+
var previewDialogStyle = {};
|
|
4646
|
+
if (editing[STACK_DIALOG_FIELD] !== "") {
|
|
4647
|
+
previewDialogStyle.fontFamily = formatStack(parseStack(editing[STACK_DIALOG_FIELD]));
|
|
4648
|
+
} else {
|
|
4649
|
+
// Independent and unset: the conversation keeps DSH's own family, so the
|
|
4650
|
+
// preview shows the theme's default rather than the interface stack the
|
|
4651
|
+
// page rule would otherwise paint into this box.
|
|
4652
|
+
var dialogFallback = FALLBACK_TOKENS["--dsw-font-family"];
|
|
4653
|
+
if (typeof dialogFallback === "string" && dialogFallback !== "") {
|
|
4654
|
+
previewDialogStyle.fontFamily = dialogFallback;
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
previewDialogStyle.fontSize = 13 + editing[SIZE_DIALOG_FIELD] + "px";
|
|
4658
|
+
if (editing[LINE_HEIGHT_DIALOG_FIELD] !== LINE_HEIGHT_MIN) {
|
|
4659
|
+
previewDialogStyle.lineHeight = String(editing[LINE_HEIGHT_DIALOG_FIELD] / 100);
|
|
4660
|
+
}
|
|
4661
|
+
if (editing[WEIGHT_DIALOG_FIELD] !== WEIGHT_UNSET) {
|
|
4662
|
+
previewDialogStyle.fontWeight = editing[WEIGHT_DIALOG_FIELD];
|
|
4663
|
+
}
|
|
4664
|
+
var previewCodeStyle = {};
|
|
4665
|
+
if (editing[MONO_FIELD] !== "") {
|
|
4666
|
+
previewCodeStyle.fontFamily = formatStack(parseStack(editing[MONO_FIELD]));
|
|
4667
|
+
}
|
|
4668
|
+
previewCodeStyle.fontSize = 13 + editing[CODE_SIZE_FIELD] + "px";
|
|
4669
|
+
if (editing[CODE_LINE_HEIGHT_FIELD] !== 0) {
|
|
4670
|
+
previewCodeStyle.lineHeight = "calc(20px + " + editing[CODE_LINE_HEIGHT_FIELD] + "px)";
|
|
4671
|
+
}
|
|
4672
|
+
|
|
2459
4673
|
return h(
|
|
2460
4674
|
"li",
|
|
2461
4675
|
{ className: "dfp-card" + (open ? " dfp-cardOpen" : "") },
|
|
@@ -2495,193 +4709,353 @@ function FontCard(props) {
|
|
|
2495
4709
|
{ className: "dfp-body" },
|
|
2496
4710
|
writable ? null : h("p", { className: "dfp-readOnly", role: "status" }, t("card.readOnly")),
|
|
2497
4711
|
|
|
4712
|
+
// edit mode
|
|
4713
|
+
h(
|
|
4714
|
+
"div",
|
|
4715
|
+
{ className: "dfp-field" },
|
|
4716
|
+
h(
|
|
4717
|
+
"div",
|
|
4718
|
+
{ className: "dfp-fieldHead", role: "group", "aria-label": t("mode.label") },
|
|
4719
|
+
h("span", { className: "dfp-fieldLabel" }, t("mode.label")),
|
|
4720
|
+
h(
|
|
4721
|
+
"div",
|
|
4722
|
+
{ className: "dfp-inline" },
|
|
4723
|
+
h(Segmented, {
|
|
4724
|
+
label: t("mode.label"),
|
|
4725
|
+
value: view,
|
|
4726
|
+
options: [
|
|
4727
|
+
{ value: "simple", label: t("mode.simple") },
|
|
4728
|
+
{ value: "advanced", label: t("mode.advanced") },
|
|
4729
|
+
],
|
|
4730
|
+
onChange: changeView,
|
|
4731
|
+
})
|
|
4732
|
+
)
|
|
4733
|
+
)
|
|
4734
|
+
),
|
|
4735
|
+
|
|
4736
|
+
// the preset bar: a dropdown select plus rename/import/export.
|
|
4737
|
+
// With a preset selected every edit auto-saves into it, so there is
|
|
4738
|
+
// no separate save step — the bar sits right under the edit mode.
|
|
2498
4739
|
h(
|
|
2499
4740
|
"div",
|
|
2500
|
-
{ className: "dfp-
|
|
2501
|
-
h(
|
|
4741
|
+
{ className: "dfp-field" },
|
|
4742
|
+
h(
|
|
4743
|
+
"div",
|
|
4744
|
+
{ className: "dfp-fieldHead" },
|
|
4745
|
+
h("span", { className: "dfp-fieldLabel" }, t("preset.label"))
|
|
4746
|
+
),
|
|
2502
4747
|
h(
|
|
2503
4748
|
"div",
|
|
2504
|
-
{ className: "dfp-
|
|
4749
|
+
{ className: "dfp-presetBar" },
|
|
4750
|
+
renaming
|
|
4751
|
+
? h("input", {
|
|
4752
|
+
type: "text",
|
|
4753
|
+
className: "dfp-textInput",
|
|
4754
|
+
autoFocus: true,
|
|
4755
|
+
spellCheck: false,
|
|
4756
|
+
value: presetName,
|
|
4757
|
+
disabled: !writable,
|
|
4758
|
+
"aria-label": t("preset.rename"),
|
|
4759
|
+
placeholder: t("preset.renamePlaceholder"),
|
|
4760
|
+
onChange: function (event) {
|
|
4761
|
+
setPresetName(event.target.value);
|
|
4762
|
+
},
|
|
4763
|
+
onKeyDown: function (event) {
|
|
4764
|
+
if (event.key === "Enter") renamePreset();
|
|
4765
|
+
if (event.key === "Escape") {
|
|
4766
|
+
setRenaming(false);
|
|
4767
|
+
setPresetName("");
|
|
4768
|
+
}
|
|
4769
|
+
},
|
|
4770
|
+
onBlur: function () {
|
|
4771
|
+
if (presetName.trim() !== "") renamePreset();
|
|
4772
|
+
else setRenaming(false);
|
|
4773
|
+
},
|
|
4774
|
+
})
|
|
4775
|
+
: h(PresetSelect, {
|
|
4776
|
+
t: t,
|
|
4777
|
+
presets: presetList,
|
|
4778
|
+
value: activeName,
|
|
4779
|
+
disabled: !writable,
|
|
4780
|
+
onPick: switchPreset,
|
|
4781
|
+
}),
|
|
2505
4782
|
h(
|
|
2506
4783
|
"button",
|
|
2507
4784
|
{
|
|
2508
4785
|
type: "button",
|
|
2509
|
-
className: "dfp-
|
|
2510
|
-
|
|
4786
|
+
className: "dfp-miniButton",
|
|
4787
|
+
disabled: !writable,
|
|
2511
4788
|
onClick: function () {
|
|
2512
|
-
|
|
4789
|
+
setPresetName(activeName);
|
|
4790
|
+
setRenaming(true);
|
|
2513
4791
|
},
|
|
2514
4792
|
},
|
|
2515
|
-
t("
|
|
4793
|
+
t("preset.rename")
|
|
2516
4794
|
),
|
|
2517
4795
|
h(
|
|
2518
4796
|
"button",
|
|
2519
4797
|
{
|
|
2520
4798
|
type: "button",
|
|
2521
|
-
className: "dfp-
|
|
2522
|
-
|
|
4799
|
+
className: "dfp-miniButton",
|
|
4800
|
+
disabled: !writable,
|
|
2523
4801
|
onClick: function () {
|
|
2524
|
-
|
|
4802
|
+
setImportOpen(!importOpen);
|
|
2525
4803
|
},
|
|
2526
4804
|
},
|
|
2527
|
-
t("
|
|
4805
|
+
t("preset.import")
|
|
4806
|
+
),
|
|
4807
|
+
h(
|
|
4808
|
+
"button",
|
|
4809
|
+
{
|
|
4810
|
+
type: "button",
|
|
4811
|
+
className: "dfp-miniButton",
|
|
4812
|
+
onClick: exportPresets,
|
|
4813
|
+
},
|
|
4814
|
+
t("preset.export")
|
|
2528
4815
|
)
|
|
2529
|
-
)
|
|
4816
|
+
),
|
|
4817
|
+
// the hint and the transient message share one always-mounted row:
|
|
4818
|
+
// the message fades in on the right and never changes the row's
|
|
4819
|
+
// height, so nothing below it moves
|
|
4820
|
+
h(
|
|
4821
|
+
"div",
|
|
4822
|
+
{ className: "dfp-presetMeta" },
|
|
4823
|
+
h("p", { className: "dfp-hint dfp-presetHint" }, t("preset.autoSave")),
|
|
4824
|
+
h(
|
|
4825
|
+
"span",
|
|
4826
|
+
{
|
|
4827
|
+
className: "dfp-status" + (statusShown ? " dfp-statusOn" : ""),
|
|
4828
|
+
role: "status",
|
|
4829
|
+
"aria-live": "polite",
|
|
4830
|
+
// The one-line clamp is deliberate; the full text (a long
|
|
4831
|
+
// clipboard fallback, say) stays readable on hover.
|
|
4832
|
+
title: presetStatus.text,
|
|
4833
|
+
},
|
|
4834
|
+
presetStatus.text
|
|
4835
|
+
)
|
|
4836
|
+
),
|
|
4837
|
+
importOpen
|
|
4838
|
+
? h("textarea", {
|
|
4839
|
+
className: "dfp-search",
|
|
4840
|
+
rows: 3,
|
|
4841
|
+
"aria-label": t("preset.import"),
|
|
4842
|
+
placeholder: t("preset.importPlaceholder"),
|
|
4843
|
+
onBlur: function (event) {
|
|
4844
|
+
// Import on blur, not per keystroke: typing a partial
|
|
4845
|
+
// paste must not be parsed as a bad export.
|
|
4846
|
+
var text = event.target.value.trim();
|
|
4847
|
+
if (text !== "") importPresets(text);
|
|
4848
|
+
},
|
|
4849
|
+
})
|
|
4850
|
+
: null
|
|
2530
4851
|
),
|
|
2531
4852
|
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
westLabel: "sansWest.label",
|
|
2537
|
-
eastLabel: "sansEast.label",
|
|
2538
|
-
slots: deriveSlots(config[SANS_FIELD]),
|
|
2539
|
-
disabled: !writable,
|
|
2540
|
-
onPickWest: pickSansWest,
|
|
2541
|
-
onPickEast: pickSansEast,
|
|
2542
|
-
onRemoveWest: function () {
|
|
2543
|
-
var slots = deriveSlots(config[SANS_FIELD]);
|
|
2544
|
-
if (slots.west !== null) dropSansEntry(slots.west);
|
|
2545
|
-
},
|
|
2546
|
-
onRemoveEast: function () {
|
|
2547
|
-
var slots = deriveSlots(config[SANS_FIELD]);
|
|
2548
|
-
if (slots.east !== null) dropSansEntry(slots.east);
|
|
2549
|
-
},
|
|
2550
|
-
})
|
|
2551
|
-
: h(
|
|
2552
|
-
FieldShell,
|
|
4853
|
+
// the accordion, conversation first (it owns every axis), then the
|
|
4854
|
+
// interface (which follows it), then code
|
|
4855
|
+
h(
|
|
4856
|
+
Section,
|
|
2553
4857
|
{
|
|
2554
4858
|
t: t,
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
resetField(SANS_FIELD);
|
|
4859
|
+
title: t("section.dialog"),
|
|
4860
|
+
summary: summaries.dialog,
|
|
4861
|
+
open: expanded === "dialog",
|
|
4862
|
+
onToggle: function () {
|
|
4863
|
+
setExpanded(expanded === "dialog" ? null : "dialog");
|
|
2561
4864
|
},
|
|
2562
4865
|
},
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
4866
|
+
familyField({
|
|
4867
|
+
field: STACK_DIALOG_FIELD,
|
|
4868
|
+
labelKey: "dialog.label",
|
|
4869
|
+
hintKey: "dialog.hint",
|
|
4870
|
+
westLabel: "dialogWest.label",
|
|
4871
|
+
eastLabel: "dialogEast.label",
|
|
4872
|
+
}),
|
|
4873
|
+
sizeField({
|
|
4874
|
+
field: SIZE_DIALOG_FIELD,
|
|
4875
|
+
labelKey: "size.dialogLabel",
|
|
4876
|
+
hintKey: "size.dialogHint",
|
|
4877
|
+
value: editing[SIZE_DIALOG_FIELD],
|
|
4878
|
+
text:
|
|
4879
|
+
editing[SIZE_DIALOG_FIELD] > 0
|
|
4880
|
+
? "+" + editing[SIZE_DIALOG_FIELD]
|
|
4881
|
+
: String(editing[SIZE_DIALOG_FIELD]),
|
|
4882
|
+
}),
|
|
4883
|
+
lineField({
|
|
4884
|
+
field: LINE_HEIGHT_DIALOG_FIELD,
|
|
4885
|
+
labelKey: "line.dialogLabel",
|
|
4886
|
+
hintKey: "line.dialogHint",
|
|
4887
|
+
value: editing[LINE_HEIGHT_DIALOG_FIELD],
|
|
4888
|
+
}),
|
|
4889
|
+
weightField({
|
|
4890
|
+
field: WEIGHT_DIALOG_FIELD,
|
|
4891
|
+
labelKey: "weight.dialogLabel",
|
|
4892
|
+
hintKey: "weight.dialogHint",
|
|
4893
|
+
value: editing[WEIGHT_DIALOG_FIELD],
|
|
4894
|
+
}),
|
|
4895
|
+
h(
|
|
4896
|
+
"div",
|
|
4897
|
+
{ className: "dfp-previewBox" },
|
|
4898
|
+
h("div", { className: "dfp-previewCaption" }, t("preview.dialogCaption")),
|
|
4899
|
+
h(
|
|
4900
|
+
"div",
|
|
4901
|
+
{ className: "dfp-previewText dfp-previewDialog", style: previewDialogStyle },
|
|
4902
|
+
t("preview.sample")
|
|
4903
|
+
)
|
|
4904
|
+
)
|
|
2571
4905
|
),
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
field: SIZE_FIELD,
|
|
2575
|
-
labelKey: "size.bodyLabel",
|
|
2576
|
-
hintKey: "size.bodyHint",
|
|
2577
|
-
value: offset,
|
|
2578
|
-
text: offsetText,
|
|
2579
|
-
}),
|
|
2580
|
-
|
|
2581
|
-
weightField({
|
|
2582
|
-
field: WEIGHT_FIELD,
|
|
2583
|
-
labelKey: "weight.bodyLabel",
|
|
2584
|
-
hintKey: "weight.bodyHint",
|
|
2585
|
-
value: weight,
|
|
2586
|
-
}),
|
|
2587
|
-
|
|
2588
|
-
view === "simple"
|
|
2589
|
-
? h(SimpleFamilyField, {
|
|
2590
|
-
t: t,
|
|
2591
|
-
label: t("mono.label"),
|
|
2592
|
-
westLabel: "monoWest.label",
|
|
2593
|
-
eastLabel: "monoEast.label",
|
|
2594
|
-
slots: deriveSlots(config[MONO_FIELD]),
|
|
2595
|
-
disabled: !writable,
|
|
2596
|
-
onPickWest: pickMonoWest,
|
|
2597
|
-
onPickEast: pickMonoEast,
|
|
2598
|
-
onRemoveWest: function () {
|
|
2599
|
-
var slots = deriveSlots(config[MONO_FIELD]);
|
|
2600
|
-
if (slots.west !== null) dropMonoEntry(slots.west);
|
|
2601
|
-
},
|
|
2602
|
-
onRemoveEast: function () {
|
|
2603
|
-
var slots = deriveSlots(config[MONO_FIELD]);
|
|
2604
|
-
if (slots.east !== null) dropMonoEntry(slots.east);
|
|
2605
|
-
},
|
|
2606
|
-
})
|
|
2607
|
-
: h(
|
|
2608
|
-
FieldShell,
|
|
4906
|
+
h(
|
|
4907
|
+
Section,
|
|
2609
4908
|
{
|
|
2610
4909
|
t: t,
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
resetField(MONO_FIELD);
|
|
4910
|
+
title: t("section.ui"),
|
|
4911
|
+
summary: summaries.ui,
|
|
4912
|
+
open: expanded === "ui",
|
|
4913
|
+
onToggle: function () {
|
|
4914
|
+
setExpanded(expanded === "ui" ? null : "ui");
|
|
2617
4915
|
},
|
|
2618
4916
|
},
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
value: config[MONO_FIELD],
|
|
2623
|
-
onChange: function (value) {
|
|
2624
|
-
setField(MONO_FIELD, value);
|
|
2625
|
-
},
|
|
2626
|
-
})
|
|
2627
|
-
),
|
|
2628
|
-
|
|
2629
|
-
sizeField({
|
|
2630
|
-
field: CODE_SIZE_FIELD,
|
|
2631
|
-
labelKey: "size.codeLabel",
|
|
2632
|
-
hintKey: "size.codeHint",
|
|
2633
|
-
value: codeOffset,
|
|
2634
|
-
text: codeOffsetText,
|
|
2635
|
-
}),
|
|
2636
|
-
|
|
2637
|
-
weightField({
|
|
2638
|
-
field: CODE_WEIGHT_FIELD,
|
|
2639
|
-
labelKey: "weight.codeLabel",
|
|
2640
|
-
hintKey: "weight.codeHint",
|
|
2641
|
-
value: codeWeight,
|
|
2642
|
-
}),
|
|
2643
|
-
|
|
2644
|
-
h(
|
|
2645
|
-
"div",
|
|
2646
|
-
{ className: "dfp-field dfp-fieldLast" },
|
|
4917
|
+
// The interface owns a family and a weight. While it follows the
|
|
4918
|
+
// conversation both come from the section above and only this row
|
|
4919
|
+
// shows; with the switch off its own two controls appear.
|
|
2647
4920
|
h(
|
|
2648
4921
|
"div",
|
|
2649
|
-
{ className: "dfp-
|
|
2650
|
-
h(
|
|
4922
|
+
{ className: "dfp-field" + (uiFollows ? " dfp-fieldLast" : "") },
|
|
4923
|
+
h(
|
|
4924
|
+
"div",
|
|
4925
|
+
{ className: "dfp-fieldHead", role: "group", "aria-label": t("ui.follow") },
|
|
4926
|
+
h("span", { className: "dfp-fieldLabel" }, t("ui.follow")),
|
|
4927
|
+
h(
|
|
4928
|
+
"div",
|
|
4929
|
+
{ className: "dfp-inline" },
|
|
4930
|
+
h(Segmented, {
|
|
4931
|
+
label: t("ui.follow"),
|
|
4932
|
+
value: uiFollows ? "on" : "off",
|
|
4933
|
+
options: [
|
|
4934
|
+
{ value: "on", label: t("common.on") },
|
|
4935
|
+
{ value: "off", label: t("common.off") },
|
|
4936
|
+
],
|
|
4937
|
+
disabled: !writable,
|
|
4938
|
+
onChange: function (next) {
|
|
4939
|
+
if (next === "on") {
|
|
4940
|
+
setField(UI_FOLLOWS_FIELD, true);
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4943
|
+
// Turning the switch off must not change what the page
|
|
4944
|
+
// shows: the interface keeps the values it was following,
|
|
4945
|
+
// written into its own two fields, and the sliders start
|
|
4946
|
+
// from there. An axis the conversation does not set
|
|
4947
|
+
// (empty family, unset weight) clears the interface's own
|
|
4948
|
+
// field instead, so it stays on DSH's defaults.
|
|
4949
|
+
if (editing[SANS_FIELD] === "") resetField(SANS_FIELD);
|
|
4950
|
+
else if (editing[SANS_FIELD] !== config[SANS_FIELD]) {
|
|
4951
|
+
setField(SANS_FIELD, editing[SANS_FIELD]);
|
|
4952
|
+
}
|
|
4953
|
+
if (editing[WEIGHT_FIELD] === WEIGHT_UNSET) resetField(WEIGHT_FIELD);
|
|
4954
|
+
else if (editing[WEIGHT_FIELD] !== config[WEIGHT_FIELD]) {
|
|
4955
|
+
setField(WEIGHT_FIELD, editing[WEIGHT_FIELD]);
|
|
4956
|
+
}
|
|
4957
|
+
setField(UI_FOLLOWS_FIELD, false);
|
|
4958
|
+
},
|
|
4959
|
+
})
|
|
4960
|
+
)
|
|
4961
|
+
),
|
|
4962
|
+
h("p", { className: "dfp-hint" }, t("ui.followHint"))
|
|
2651
4963
|
),
|
|
4964
|
+
uiFollows
|
|
4965
|
+
? null
|
|
4966
|
+
: familyField({
|
|
4967
|
+
field: SANS_FIELD,
|
|
4968
|
+
labelKey: "sans.label",
|
|
4969
|
+
hintKey: "sans.hint",
|
|
4970
|
+
westLabel: "sansWest.label",
|
|
4971
|
+
eastLabel: "sansEast.label",
|
|
4972
|
+
}),
|
|
4973
|
+
uiFollows
|
|
4974
|
+
? null
|
|
4975
|
+
: weightField({
|
|
4976
|
+
field: WEIGHT_FIELD,
|
|
4977
|
+
labelKey: "weight.uiLabel",
|
|
4978
|
+
hintKey: "weight.uiHint",
|
|
4979
|
+
value: editing[WEIGHT_FIELD],
|
|
4980
|
+
}),
|
|
2652
4981
|
h(
|
|
2653
4982
|
"div",
|
|
2654
4983
|
{ className: "dfp-previewBox" },
|
|
2655
4984
|
h("div", { className: "dfp-previewCaption" }, t("preview.sansCaption")),
|
|
4985
|
+
h("div", { className: "dfp-previewText", style: previewUiStyle }, t("preview.sample"))
|
|
4986
|
+
)
|
|
4987
|
+
),
|
|
4988
|
+
h(
|
|
4989
|
+
Section,
|
|
4990
|
+
{
|
|
4991
|
+
t: t,
|
|
4992
|
+
title: t("section.code"),
|
|
4993
|
+
summary: summaries.code,
|
|
4994
|
+
open: expanded === "code",
|
|
4995
|
+
onToggle: function () {
|
|
4996
|
+
setExpanded(expanded === "code" ? null : "code");
|
|
4997
|
+
},
|
|
4998
|
+
},
|
|
4999
|
+
familyField({
|
|
5000
|
+
field: MONO_FIELD,
|
|
5001
|
+
labelKey: "mono.label",
|
|
5002
|
+
hintKey: "mono.hint",
|
|
5003
|
+
westLabel: "monoWest.label",
|
|
5004
|
+
eastLabel: "monoEast.label",
|
|
5005
|
+
}),
|
|
5006
|
+
sizeField({
|
|
5007
|
+
field: CODE_SIZE_FIELD,
|
|
5008
|
+
labelKey: "size.codeLabel",
|
|
5009
|
+
hintKey: "size.codeHint",
|
|
5010
|
+
value: editing[CODE_SIZE_FIELD],
|
|
5011
|
+
text:
|
|
5012
|
+
editing[CODE_SIZE_FIELD] > 0
|
|
5013
|
+
? "+" + editing[CODE_SIZE_FIELD]
|
|
5014
|
+
: String(editing[CODE_SIZE_FIELD]),
|
|
5015
|
+
}),
|
|
5016
|
+
codeLineField(),
|
|
5017
|
+
weightField({
|
|
5018
|
+
field: CODE_WEIGHT_FIELD,
|
|
5019
|
+
labelKey: "weight.codeLabel",
|
|
5020
|
+
hintKey: "weight.codeHint",
|
|
5021
|
+
value: editing[CODE_WEIGHT_FIELD],
|
|
5022
|
+
}),
|
|
5023
|
+
ligatureField(),
|
|
5024
|
+
view === "advanced" ? featuresField() : null,
|
|
5025
|
+
h(
|
|
5026
|
+
"div",
|
|
5027
|
+
{ className: "dfp-previewBox" },
|
|
5028
|
+
h("div", { className: "dfp-previewCaption" }, t("preview.monoCaption")),
|
|
2656
5029
|
h(
|
|
2657
5030
|
"div",
|
|
2658
|
-
{
|
|
2659
|
-
className: "dfp-previewText",
|
|
2660
|
-
style:
|
|
2661
|
-
config[SANS_FIELD] === ""
|
|
2662
|
-
? undefined
|
|
2663
|
-
: { fontFamily: formatStack(parseStack(config[SANS_FIELD])) },
|
|
2664
|
-
},
|
|
2665
|
-
t("preview.sample")
|
|
2666
|
-
),
|
|
2667
|
-
h(
|
|
2668
|
-
"div",
|
|
2669
|
-
{ className: "dfp-previewCaption", style: { marginTop: 10 } },
|
|
2670
|
-
t("preview.monoCaption")
|
|
2671
|
-
),
|
|
2672
|
-
h(
|
|
2673
|
-
"div",
|
|
2674
|
-
{
|
|
2675
|
-
className: "dfp-previewText dfp-previewCode",
|
|
2676
|
-
style:
|
|
2677
|
-
config[MONO_FIELD] === ""
|
|
2678
|
-
? undefined
|
|
2679
|
-
: { fontFamily: formatStack(parseStack(config[MONO_FIELD])) },
|
|
2680
|
-
},
|
|
5031
|
+
{ className: "dfp-previewText dfp-previewCode", style: previewCodeStyle },
|
|
2681
5032
|
t("preview.code")
|
|
2682
5033
|
)
|
|
2683
5034
|
)
|
|
2684
5035
|
),
|
|
5036
|
+
// global fine-tuning, without an accordion: it is one switch (two in
|
|
5037
|
+
// advanced mode), so it sits directly below the three font sections
|
|
5038
|
+
view === "simple"
|
|
5039
|
+
? synthesisField({
|
|
5040
|
+
field: NO_SYNTHETIC_ITALIC_FIELD,
|
|
5041
|
+
labelKey: "synth.simple",
|
|
5042
|
+
hintKey: "synth.simpleHint",
|
|
5043
|
+
simple: true,
|
|
5044
|
+
})
|
|
5045
|
+
: h(
|
|
5046
|
+
"div",
|
|
5047
|
+
null,
|
|
5048
|
+
synthesisField({
|
|
5049
|
+
field: NO_SYNTHETIC_ITALIC_FIELD,
|
|
5050
|
+
labelKey: "synth.italic",
|
|
5051
|
+
hintKey: "synth.italicHint",
|
|
5052
|
+
}),
|
|
5053
|
+
synthesisField({
|
|
5054
|
+
field: NO_SYNTHETIC_BOLD_FIELD,
|
|
5055
|
+
labelKey: "synth.bold",
|
|
5056
|
+
hintKey: "synth.boldHint",
|
|
5057
|
+
})
|
|
5058
|
+
),
|
|
2685
5059
|
|
|
2686
5060
|
h(
|
|
2687
5061
|
"div",
|
|
@@ -2693,12 +5067,27 @@ function FontCard(props) {
|
|
|
2693
5067
|
className: "dfp-resetAll",
|
|
2694
5068
|
disabled: !writable,
|
|
2695
5069
|
onClick: function () {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
5070
|
+
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
5071
|
+
clear(VALUE_FIELDS[index]);
|
|
5072
|
+
}
|
|
5073
|
+
clear(DARK_VALUES_FIELD);
|
|
5074
|
+
// Auto-save: the reset IS a change to the active preset, so
|
|
5075
|
+
// its snapshot follows back to the neutral defaults.
|
|
5076
|
+
if (writable && applyingRef.current !== true) {
|
|
5077
|
+
var activeIndex = -1;
|
|
5078
|
+
for (var i = 0; i < presetList.length; i += 1) {
|
|
5079
|
+
if (presetList[i].name === activeName) activeIndex = i;
|
|
5080
|
+
}
|
|
5081
|
+
if (activeIndex >= 0) {
|
|
5082
|
+
var next = presetList.slice();
|
|
5083
|
+
next[activeIndex] = {
|
|
5084
|
+
name: presetList[activeIndex].name,
|
|
5085
|
+
values: {},
|
|
5086
|
+
savedAt: Date.now(),
|
|
5087
|
+
};
|
|
5088
|
+
writePresets(next);
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
2702
5091
|
},
|
|
2703
5092
|
},
|
|
2704
5093
|
t("card.resetAll")
|
|
@@ -2725,11 +5114,58 @@ function apply(ctx) {
|
|
|
2725
5114
|
return tokens;
|
|
2726
5115
|
});
|
|
2727
5116
|
var scope = ctx.settingsScope.bind({ namespace: NAMESPACE });
|
|
5117
|
+
var appliedOverrides = "";
|
|
5118
|
+
|
|
5119
|
+
/**
|
|
5120
|
+
* Hand the family variables to the official theme layer, so the presenter
|
|
5121
|
+
* owns them on body's inline style and nothing — theme switches included —
|
|
5122
|
+
* can delete them. Only the two family pairs ride this; the size/weight
|
|
5123
|
+
* axes stay on the stylesheet, where their `!important` rules already win.
|
|
5124
|
+
* @param {unknown} config - the current configuration.
|
|
5125
|
+
*/
|
|
5126
|
+
var applyTokenOverrides = function (config) {
|
|
5127
|
+
if (typeof ctx.get !== "function") return;
|
|
5128
|
+
var theme = ctx.get("theme");
|
|
5129
|
+
if (theme === undefined || theme === null) return;
|
|
5130
|
+
if (typeof theme.overrideTokens !== "function") return;
|
|
5131
|
+
var sets = resolveAxes(config);
|
|
5132
|
+
var tokens = {};
|
|
5133
|
+
var uiLight = formatStack(parseStack(sets.light[SANS_FIELD]));
|
|
5134
|
+
var uiDark = formatStack(parseStack(sets.dark[SANS_FIELD]));
|
|
5135
|
+
if (uiLight !== "" && uiDark !== "") {
|
|
5136
|
+
tokens["--dsw-font-family"] = { light: uiLight, dark: uiDark };
|
|
5137
|
+
}
|
|
5138
|
+
var monoLight = formatStack(parseStack(sets.light[MONO_FIELD]));
|
|
5139
|
+
var monoDark = formatStack(parseStack(sets.dark[MONO_FIELD]));
|
|
5140
|
+
if (monoLight !== "" || monoDark !== "") {
|
|
5141
|
+
var fallback = readDefaultFamily("--ds-font-family-code");
|
|
5142
|
+
var light = monoLight !== "" ? monoLight : fallback;
|
|
5143
|
+
var dark = monoDark !== "" ? monoDark : fallback;
|
|
5144
|
+
if (light !== "" && dark !== "") {
|
|
5145
|
+
tokens["--dsw-font-mono"] = { light: light, dark: dark };
|
|
5146
|
+
tokens["--ds-font-family-code"] = { light: light, dark: dark };
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
var signature = JSON.stringify(tokens);
|
|
5150
|
+
if (signature === appliedOverrides) return;
|
|
5151
|
+
appliedOverrides = signature;
|
|
5152
|
+
try {
|
|
5153
|
+
theme.overrideTokens("dsh-fonttune", tokens);
|
|
5154
|
+
} catch (error) {
|
|
5155
|
+
// A teaching error (a bad pair shape) must not break the page; the
|
|
5156
|
+
// stylesheet rules below still apply the same values.
|
|
5157
|
+
}
|
|
5158
|
+
};
|
|
2728
5159
|
|
|
2729
5160
|
var sync = function () {
|
|
2730
5161
|
var snapshot = scope.getSnapshot();
|
|
2731
5162
|
if (snapshot.value === undefined) return;
|
|
2732
|
-
|
|
5163
|
+
// Light and dark share one value set in this release: the stored flag (if
|
|
5164
|
+
// any) is ignored, so no prefixed dark rules are ever emitted.
|
|
5165
|
+
var unified = normalizeConfig(snapshot.value);
|
|
5166
|
+
unified[PER_THEME_FIELD] = false;
|
|
5167
|
+
applyCss(unified);
|
|
5168
|
+
applyTokenOverrides(unified);
|
|
2733
5169
|
};
|
|
2734
5170
|
ctx.effect(
|
|
2735
5171
|
function () {
|
|
@@ -2741,7 +5177,11 @@ function apply(ctx) {
|
|
|
2741
5177
|
|
|
2742
5178
|
// DSH writes its tokens (and its content font size) after this bundle
|
|
2743
5179
|
// activates, and the host's own boot row only covers the first frame; the
|
|
2744
|
-
// size axis therefore re-reads the live values and reapplies when they
|
|
5180
|
+
// size axis therefore re-reads the live values and reapplies when they
|
|
5181
|
+
// moved. The theme/change event makes that immediate: the official
|
|
5182
|
+
// font-size preference flows through it, so a slider change is followed
|
|
5183
|
+
// within a frame instead of the polling interval.
|
|
5184
|
+
var scheduleRecheck = null;
|
|
2745
5185
|
ctx.effect(
|
|
2746
5186
|
function () {
|
|
2747
5187
|
if (typeof document === "undefined") return undefined;
|
|
@@ -2762,6 +5202,7 @@ function apply(ctx) {
|
|
|
2762
5202
|
}
|
|
2763
5203
|
sync();
|
|
2764
5204
|
};
|
|
5205
|
+
scheduleRecheck = recheck;
|
|
2765
5206
|
var timer = globalThis.setTimeout(recheck, 500);
|
|
2766
5207
|
var interval = globalThis.setInterval(recheck, 4000);
|
|
2767
5208
|
return function () {
|
|
@@ -2771,6 +5212,17 @@ function apply(ctx) {
|
|
|
2771
5212
|
},
|
|
2772
5213
|
"dsh-fonttune: token refresh"
|
|
2773
5214
|
);
|
|
5215
|
+
ctx.effect(
|
|
5216
|
+
function () {
|
|
5217
|
+
// Compositions without an event seat (the offline stand-ins) simply skip
|
|
5218
|
+
// this hook; the polling recheck still covers theme changes there.
|
|
5219
|
+
if (typeof ctx.on !== "function") return undefined;
|
|
5220
|
+
return ctx.on("theme/change", function () {
|
|
5221
|
+
if (scheduleRecheck !== null) scheduleRecheck();
|
|
5222
|
+
});
|
|
5223
|
+
},
|
|
5224
|
+
"dsh-fonttune: theme change adoption"
|
|
5225
|
+
);
|
|
2774
5226
|
|
|
2775
5227
|
var t = function (key, params) {
|
|
2776
5228
|
var locale = "en";
|
|
@@ -2802,6 +5254,9 @@ function apply(ctx) {
|
|
|
2802
5254
|
});
|
|
2803
5255
|
}
|
|
2804
5256
|
|
|
5257
|
+
exports.apply = apply;
|
|
5258
|
+
exports.inject = inject;
|
|
5259
|
+
|
|
2805
5260
|
exports.apply = apply;
|
|
2806
5261
|
exports.inject = inject;
|
|
2807
5262
|
|