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