dsh-fonttune 0.1.6 → 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 +29 -0
- package/CHANGELOG.md +29 -0
- package/README.en.md +23 -14
- package/README.md +22 -13
- package/lib/client.js +2855 -525
- 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).
|
|
689
996
|
* @param {string} name - custom property name including the leading dashes.
|
|
690
|
-
* @returns {boolean}
|
|
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.
|
|
1022
|
+
* @param {string} name - custom property name including the leading dashes.
|
|
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
|
+
);
|
|
721
1063
|
});
|
|
722
1064
|
}
|
|
723
1065
|
|
|
724
1066
|
/**
|
|
725
|
-
* The
|
|
726
|
-
*
|
|
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;
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* The markdown (dialog) PART tokens: sizes and line heights of the markdown
|
|
1079
|
+
* family, excluding code.
|
|
1080
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
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.
|
|
727
1102
|
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
728
|
-
* @returns {string[]}
|
|
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
|
+
);
|
|
1368
|
+
}
|
|
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;
|
|
732
1699
|
}
|
|
733
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
1829
|
"--dsw-font-markdown-code-block-small-line-height": "16px",
|
|
793
|
-
|
|
794
|
-
"--dsw-font-markdown-table-line-height": "22px",
|
|
1830
|
+
// content size (the interface chain's anchor, written inline by the theme)
|
|
795
1831
|
"--dsh-content-font-size": "14px",
|
|
796
1832
|
"--dsh-content-font-size-secondary": "13px",
|
|
797
1833
|
};
|
|
798
1834
|
|
|
799
|
-
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
* @param {number} digits - decimals to keep.
|
|
803
|
-
* @returns {number} the rounded number.
|
|
804
|
-
*/
|
|
805
|
-
function round(value, digits) {
|
|
806
|
-
var factor = Math.pow(10, digits);
|
|
807
|
-
return Math.round(value * factor) / factor;
|
|
808
|
-
}
|
|
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;
|
|
@@ -1031,7 +2156,7 @@ var DICTS = {
|
|
|
1031
2156
|
en: {
|
|
1032
2157
|
"card.title": "Font tune",
|
|
1033
2158
|
"card.description":
|
|
1034
|
-
"
|
|
2159
|
+
"Interface and conversation fonts, a size, a weight and a line height for each, code extras and presets",
|
|
1035
2160
|
"card.expand": "Expand",
|
|
1036
2161
|
"card.collapse": "Collapse",
|
|
1037
2162
|
"card.resetAll": "Reset all",
|
|
@@ -1039,8 +2164,10 @@ var DICTS = {
|
|
|
1039
2164
|
|
|
1040
2165
|
"common.overridden": "Changed",
|
|
1041
2166
|
"common.reset": "Reset",
|
|
2167
|
+
"common.on": "On",
|
|
2168
|
+
"common.off": "Off",
|
|
1042
2169
|
|
|
1043
|
-
"sans.label": "
|
|
2170
|
+
"sans.label": "Interface font",
|
|
1044
2171
|
"sans.hint":
|
|
1045
2172
|
"Each family is tried in order: put a Latin face first and CJK faces after it. Empty leaves DSH's own stack.",
|
|
1046
2173
|
"mono.label": "Code font",
|
|
@@ -1074,8 +2201,8 @@ var DICTS = {
|
|
|
1074
2201
|
"mode.advanced": "Advanced",
|
|
1075
2202
|
"mode.simpleHint":
|
|
1076
2203
|
"Manages only the front two slots of the stack — Western, then CJK. Everything you ordered in Advanced stays untouched.",
|
|
1077
|
-
"sansWest.label": "
|
|
1078
|
-
"sansEast.label": "
|
|
2204
|
+
"sansWest.label": "Interface · Western",
|
|
2205
|
+
"sansEast.label": "Interface · CJK",
|
|
1079
2206
|
"monoWest.label": "Code · Western",
|
|
1080
2207
|
"monoEast.label": "Code · CJK",
|
|
1081
2208
|
"split.pick": "Choose…",
|
|
@@ -1083,35 +2210,131 @@ var DICTS = {
|
|
|
1083
2210
|
"split.remove": "Remove {name}",
|
|
1084
2211
|
"split.rest": "Other fallbacks (reorder them in Advanced): {names}",
|
|
1085
2212
|
|
|
1086
|
-
"size.bodyLabel": "
|
|
2213
|
+
"size.bodyLabel": "Interface font size offset",
|
|
1087
2214
|
"size.bodyHint":
|
|
1088
|
-
"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.",
|
|
1089
2219
|
"size.codeLabel": "Code font size offset",
|
|
1090
2220
|
"size.codeHint":
|
|
1091
|
-
"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.",
|
|
1092
2222
|
"size.unit": "px",
|
|
1093
2223
|
|
|
1094
|
-
"weight.
|
|
1095
|
-
"weight.
|
|
1096
|
-
"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.",
|
|
1097
2227
|
"weight.codeLabel": "Code font weight",
|
|
1098
2228
|
"weight.codeHint":
|
|
1099
|
-
"Overrides code blocks, inline code and terminal output only. 400 or unset keeps DSH's own weight
|
|
1100
|
-
|
|
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.",
|
|
1101
2322
|
|
|
1102
2323
|
"preview.label": "Preview",
|
|
1103
|
-
"preview.sansCaption": "
|
|
2324
|
+
"preview.sansCaption": "Interface",
|
|
2325
|
+
"preview.dialogCaption": "Conversation",
|
|
1104
2326
|
"preview.monoCaption": "Code",
|
|
1105
2327
|
"preview.sample":
|
|
1106
2328
|
"The quick brown fox jumps over the lazy dog — 中文排版预览,标点符号,数字 0123456789。",
|
|
1107
|
-
"preview.code": "const greet = (name) => `hello ${name}`; // 代码预览",
|
|
2329
|
+
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
1108
2330
|
|
|
1109
2331
|
"footnote.local":
|
|
1110
2332
|
"Stored in the Host settings document. Every change applies immediately.",
|
|
1111
2333
|
},
|
|
1112
2334
|
zh: {
|
|
1113
2335
|
"card.title": "字体增强",
|
|
1114
|
-
"card.description":
|
|
2336
|
+
"card.description":
|
|
2337
|
+
"界面与对话字体、各自的字号/字重/行高,代码增强与预设方案",
|
|
1115
2338
|
"card.expand": "展开",
|
|
1116
2339
|
"card.collapse": "收起",
|
|
1117
2340
|
"card.resetAll": "全部重置",
|
|
@@ -1119,8 +2342,10 @@ var DICTS = {
|
|
|
1119
2342
|
|
|
1120
2343
|
"common.overridden": "已修改",
|
|
1121
2344
|
"common.reset": "重置",
|
|
2345
|
+
"common.on": "开启",
|
|
2346
|
+
"common.off": "关闭",
|
|
1122
2347
|
|
|
1123
|
-
"sans.label": "
|
|
2348
|
+
"sans.label": "界面字体",
|
|
1124
2349
|
"sans.hint":
|
|
1125
2350
|
"按顺序回退:拉丁字体放前面、中文字体放后面;留空表示沿用 DSH 的字体栈。",
|
|
1126
2351
|
"mono.label": "代码字体",
|
|
@@ -1152,8 +2377,8 @@ var DICTS = {
|
|
|
1152
2377
|
"mode.advanced": "高级",
|
|
1153
2378
|
"mode.simpleHint":
|
|
1154
2379
|
"只管理栈最前面的西文/中文两项;你在高级模式里排好的其余回退原样保留。",
|
|
1155
|
-
"sansWest.label": "
|
|
1156
|
-
"sansEast.label": "
|
|
2380
|
+
"sansWest.label": "界面 · 西文字体",
|
|
2381
|
+
"sansEast.label": "界面 · 中文字体",
|
|
1157
2382
|
"monoWest.label": "代码 · 西文字体",
|
|
1158
2383
|
"monoEast.label": "代码 · 中文字体",
|
|
1159
2384
|
"split.pick": "选择…",
|
|
@@ -1161,28 +2386,114 @@ var DICTS = {
|
|
|
1161
2386
|
"split.remove": "移除 {name}",
|
|
1162
2387
|
"split.rest": "其余回退项(在高级模式中排序):{names}",
|
|
1163
2388
|
|
|
1164
|
-
"size.bodyLabel": "
|
|
2389
|
+
"size.bodyLabel": "界面字号偏移",
|
|
1165
2390
|
"size.bodyHint":
|
|
1166
|
-
"
|
|
2391
|
+
"给界面文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持原样。",
|
|
2392
|
+
"size.dialogLabel": "对话字号偏移",
|
|
2393
|
+
"size.dialogHint":
|
|
2394
|
+
"给对话文字统一加 {offset},与设置里的「字号大小」叠加;未设置时跟随界面偏移。",
|
|
1167
2395
|
"size.codeLabel": "代码字号偏移",
|
|
1168
2396
|
"size.codeHint":
|
|
1169
|
-
"
|
|
2397
|
+
"只作用于代码块与行内代码,与界面字号互不影响;0 表示保持原样。",
|
|
1170
2398
|
"size.unit": "px",
|
|
1171
2399
|
|
|
1172
|
-
"weight.
|
|
1173
|
-
"weight.
|
|
1174
|
-
"覆盖正文与界面文字的粗细(含标题);代码不受它影响。默认字重为400。",
|
|
2400
|
+
"weight.dialogLabel": "对话字重",
|
|
2401
|
+
"weight.dialogHint": "覆盖对话 Markdown 的粗细;未设置时跟随界面字重。",
|
|
1175
2402
|
"weight.codeLabel": "代码字重",
|
|
1176
2403
|
"weight.codeHint":
|
|
1177
|
-
"只覆盖代码块、行内代码与终端输出;400 或未设置表示保持 DSH
|
|
1178
|
-
|
|
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}”已存在,保存将覆盖它。",
|
|
1179
2489
|
|
|
1180
2490
|
"preview.label": "预览",
|
|
1181
|
-
"preview.sansCaption": "
|
|
2491
|
+
"preview.sansCaption": "界面",
|
|
2492
|
+
"preview.dialogCaption": "对话",
|
|
1182
2493
|
"preview.monoCaption": "代码",
|
|
1183
2494
|
"preview.sample":
|
|
1184
2495
|
"The quick brown fox jumps over the lazy dog —— 中文排版预览,标点符号,数字 0123456789。",
|
|
1185
|
-
"preview.code": "const greet = (name) => `hello ${name}`; // 代码预览",
|
|
2496
|
+
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
1186
2497
|
|
|
1187
2498
|
"footnote.local": "保存在宿主设置文档里;每次改动立即生效。",
|
|
1188
2499
|
},
|
|
@@ -1241,15 +2552,64 @@ var CARD_CSS = [
|
|
|
1241
2552
|
".dfp-resetAll:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}",
|
|
1242
2553
|
".dfp-resetAll:disabled{opacity:.4;cursor:default}",
|
|
1243
2554
|
|
|
1244
|
-
".dfp-field{padding:
|
|
2555
|
+
".dfp-field{padding:14px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}",
|
|
1245
2556
|
".dfp-fieldLast{border-bottom:0}",
|
|
1246
2557
|
".dfp-fieldHead{align-items:center;gap:8px;display:flex}",
|
|
1247
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}",
|
|
1248
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}",
|
|
1249
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}",
|
|
1250
2565
|
".dfp-fieldReset:hover{color:var(--dsw-alias-label-primary)}",
|
|
1251
2566
|
".dfp-hint{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:18px}",
|
|
1252
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
|
+
|
|
1253
2613
|
".dfp-chips{align-items:center;flex-wrap:wrap;gap:6px;margin-top:10px;display:flex}",
|
|
1254
2614
|
".dfp-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
1255
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}",
|
|
@@ -1266,10 +2626,7 @@ var CARD_CSS = [
|
|
|
1266
2626
|
|
|
1267
2627
|
// Segmented / toggle controls follow dsh-quick-toc's settings card colours:
|
|
1268
2628
|
// the selected option takes the same --dsw-specific-sidebar-nav-item-active
|
|
1269
|
-
// surface (not just darker text). Shape: one joined pill with a divider
|
|
1270
|
-
// the card had before — quick-toc is being aligned to this shape instead.
|
|
1271
|
-
".dfp-modeRow{align-items:center;gap:10px;margin:14px 0 0;display:flex}",
|
|
1272
|
-
".dfp-modeLabel{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}",
|
|
2629
|
+
// surface (not just darker text). Shape: one joined pill with a divider.
|
|
1273
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}",
|
|
1274
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}",
|
|
1275
2632
|
".dfp-modeButton:first-child{border-left:none}",
|
|
@@ -1284,6 +2641,10 @@ var CARD_CSS = [
|
|
|
1284
2641
|
".dfp-pick:disabled{opacity:.4;cursor:default}",
|
|
1285
2642
|
".dfp-pickEmpty{color:var(--dsw-alias-label-tertiary)}",
|
|
1286
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
|
+
|
|
1287
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}",
|
|
1288
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}",
|
|
1289
2650
|
".dfp-note{padding:4px 2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
|
|
@@ -1300,9 +2661,12 @@ var CARD_CSS = [
|
|
|
1300
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}",
|
|
1301
2662
|
".dfp-scale{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;display:flex;justify-content:space-between}",
|
|
1302
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
|
|
1303
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}",
|
|
1304
2667
|
".dfp-previewCaption{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin-bottom:4px}",
|
|
1305
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}",
|
|
1306
2670
|
].join("");
|
|
1307
2671
|
|
|
1308
2672
|
/**
|
|
@@ -1363,9 +2727,9 @@ function isOwnSheet(sheet) {
|
|
|
1363
2727
|
* rule applies it reports the SCALED values, and feeding those back as bases
|
|
1364
2728
|
* compounds the offset on every refresh.
|
|
1365
2729
|
*
|
|
1366
|
-
*
|
|
1367
|
-
* too: they are
|
|
1368
|
-
*
|
|
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.
|
|
1369
2733
|
*
|
|
1370
2734
|
* @returns {Record<string, string>} token name to its untouched value.
|
|
1371
2735
|
*/
|
|
@@ -1449,6 +2813,24 @@ function createStylesheet(tokens) {
|
|
|
1449
2813
|
};
|
|
1450
2814
|
}
|
|
1451
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
|
+
|
|
1452
2834
|
/* ------------------------------------------------------------------ *
|
|
1453
2835
|
* hooks and controls
|
|
1454
2836
|
* ------------------------------------------------------------------ */
|
|
@@ -1514,24 +2896,67 @@ function FieldShell(props) {
|
|
|
1514
2896
|
},
|
|
1515
2897
|
props.t("common.reset")
|
|
1516
2898
|
)
|
|
1517
|
-
|
|
2899
|
+
: null
|
|
1518
2900
|
),
|
|
1519
2901
|
h("p", { className: "dfp-hint" }, props.hint),
|
|
1520
2902
|
props.children
|
|
1521
2903
|
);
|
|
1522
2904
|
}
|
|
1523
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
|
+
|
|
1524
2948
|
/**
|
|
1525
2949
|
* A slider over a whole-number range with a live value readout.
|
|
1526
2950
|
* Dragging only moves a local value; the change handler runs once the
|
|
1527
2951
|
* pointer is released (or on blur/keyup), so intermediate steps never
|
|
1528
2952
|
* rewrite the settings document — the page does not recompute per pixel.
|
|
1529
|
-
* @param {object} props - range, value, labels, change handler and an
|
|
2953
|
+
* @param {object} props - range, step, value, labels, change handler and an
|
|
1530
2954
|
* optional pendingText(v) formatting the readout while dragging.
|
|
1531
2955
|
* @returns {object} the slider element.
|
|
1532
2956
|
*/
|
|
1533
2957
|
function NumberSlider(props) {
|
|
1534
2958
|
var [pending, setPending] = useState(null);
|
|
2959
|
+
var step = typeof props.step === "number" && props.step > 0 ? props.step : 1;
|
|
1535
2960
|
// The value the host has not confirmed yet. Between the release and the
|
|
1536
2961
|
// settings round-trip the committed prop is still the OLD number — clearing
|
|
1537
2962
|
// the local value right away would show that old number for one frame
|
|
@@ -1592,7 +3017,7 @@ function NumberSlider(props) {
|
|
|
1592
3017
|
className: "dfp-slider",
|
|
1593
3018
|
min: props.min,
|
|
1594
3019
|
max: props.max,
|
|
1595
|
-
step:
|
|
3020
|
+
step: step,
|
|
1596
3021
|
value: shown,
|
|
1597
3022
|
disabled: props.disabled,
|
|
1598
3023
|
"aria-label": props.label,
|
|
@@ -1617,6 +3042,178 @@ function NumberSlider(props) {
|
|
|
1617
3042
|
);
|
|
1618
3043
|
}
|
|
1619
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
|
+
|
|
1620
3217
|
/* ------------------------------------------------------------------ *
|
|
1621
3218
|
* font enumeration
|
|
1622
3219
|
* ------------------------------------------------------------------ */
|
|
@@ -1688,7 +3285,7 @@ function measureCJK(name) {
|
|
|
1688
3285
|
var context = canvas.getContext("2d");
|
|
1689
3286
|
if (context === null) return null;
|
|
1690
3287
|
var probe = "中文字體测试";
|
|
1691
|
-
context.font =
|
|
3288
|
+
context.font = "72px " + quoteFamily(name) + ", monospace";
|
|
1692
3289
|
var withFamily = context.measureText(probe).width;
|
|
1693
3290
|
context.font = "72px monospace";
|
|
1694
3291
|
var without = context.measureText(probe).width;
|
|
@@ -2216,12 +3813,25 @@ function StackPicker(props) {
|
|
|
2216
3813
|
}
|
|
2217
3814
|
|
|
2218
3815
|
/* ------------------------------------------------------------------ *
|
|
2219
|
-
*
|
|
3816
|
+
* card helpers
|
|
2220
3817
|
* ------------------------------------------------------------------ */
|
|
2221
3818
|
|
|
2222
3819
|
/** localStorage key of the card's view mode. */
|
|
2223
3820
|
var MODE_KEY = "dsh-fonttune.mode.v1";
|
|
2224
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
|
+
|
|
2225
3835
|
/**
|
|
2226
3836
|
* Read the view mode the card opens in. Simple is the default; an unreadable
|
|
2227
3837
|
* or missing storage (tests, private modes) must not break the card.
|
|
@@ -2311,10 +3921,72 @@ function SimpleFamilyField(props) {
|
|
|
2311
3921
|
);
|
|
2312
3922
|
}
|
|
2313
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
|
+
|
|
2314
3977
|
/**
|
|
2315
3978
|
* Render the plugin's card: the settings section dispatches this component
|
|
2316
3979
|
* under the namespace key, and the host has to serve that namespace for it to
|
|
2317
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).
|
|
2318
3990
|
* @param {object} props - slot props (the injected face plus the locale seat).
|
|
2319
3991
|
* @returns {object} the card element.
|
|
2320
3992
|
*/
|
|
@@ -2323,10 +3995,50 @@ function FontCard(props) {
|
|
|
2323
3995
|
var scope = props.scope;
|
|
2324
3996
|
var [open, setOpen] = useState(false);
|
|
2325
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);
|
|
2326
4013
|
var snapshot = useScopeSnapshot(scope);
|
|
2327
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);
|
|
2328
4019
|
var user = snapshot.user !== null && typeof snapshot.user === "object" ? snapshot.user : {};
|
|
2329
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;
|
|
2330
4042
|
|
|
2331
4043
|
var changeView = function (next) {
|
|
2332
4044
|
setView(next);
|
|
@@ -2336,28 +4048,47 @@ function FontCard(props) {
|
|
|
2336
4048
|
// Unavailable storage only costs the persistence of the preference.
|
|
2337
4049
|
}
|
|
2338
4050
|
};
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
var
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
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);
|
|
2358
4075
|
};
|
|
2359
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
|
+
*/
|
|
2360
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) {
|
|
2361
4092
|
var result = scope.set(field, value);
|
|
2362
4093
|
if (result && typeof result.catch === "function") {
|
|
2363
4094
|
result.catch(function () {
|
|
@@ -2365,7 +4096,7 @@ function FontCard(props) {
|
|
|
2365
4096
|
});
|
|
2366
4097
|
}
|
|
2367
4098
|
};
|
|
2368
|
-
var
|
|
4099
|
+
var clear = function (field) {
|
|
2369
4100
|
var result = scope.unset(field);
|
|
2370
4101
|
if (result && typeof result.catch === "function") {
|
|
2371
4102
|
result.catch(function () {});
|
|
@@ -2375,42 +4106,114 @@ function FontCard(props) {
|
|
|
2375
4106
|
return Object.prototype.hasOwnProperty.call(user, field);
|
|
2376
4107
|
};
|
|
2377
4108
|
|
|
2378
|
-
|
|
2379
|
-
var
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
var
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
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) {
|
|
2391
4191
|
return h(
|
|
2392
4192
|
FieldShell,
|
|
2393
4193
|
{
|
|
2394
4194
|
t: t,
|
|
2395
4195
|
label: t(props.labelKey),
|
|
2396
|
-
hint: t(props.hintKey, {
|
|
2397
|
-
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,
|
|
2398
4198
|
disabled: !writable,
|
|
2399
4199
|
onReset: function () {
|
|
2400
4200
|
resetField(props.field);
|
|
2401
4201
|
},
|
|
2402
4202
|
},
|
|
2403
4203
|
h(NumberSlider, {
|
|
2404
|
-
min:
|
|
2405
|
-
max:
|
|
4204
|
+
min: LINE_HEIGHT_MIN,
|
|
4205
|
+
max: LINE_HEIGHT_MAX,
|
|
4206
|
+
step: 5,
|
|
2406
4207
|
value: props.value,
|
|
2407
4208
|
disabled: !writable,
|
|
2408
4209
|
label: t(props.labelKey),
|
|
2409
|
-
readout: props.
|
|
2410
|
-
minLabel:
|
|
2411
|
-
maxLabel:
|
|
2412
|
-
|
|
2413
|
-
|
|
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");
|
|
2414
4217
|
},
|
|
2415
4218
|
onChange: function (value) {
|
|
2416
4219
|
setField(props.field, value);
|
|
@@ -2419,43 +4222,356 @@ function FontCard(props) {
|
|
|
2419
4222
|
);
|
|
2420
4223
|
};
|
|
2421
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 ----
|
|
2422
4352
|
/**
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2425
|
-
*
|
|
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.
|
|
2426
4358
|
*/
|
|
2427
|
-
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
|
+
}
|
|
2428
4510
|
return h(
|
|
2429
4511
|
FieldShell,
|
|
2430
4512
|
{
|
|
2431
4513
|
t: t,
|
|
2432
4514
|
label: t(props.labelKey),
|
|
2433
4515
|
hint: t(props.hintKey),
|
|
2434
|
-
overridden: overridden(props.field)
|
|
4516
|
+
overridden: overridden(props.field),
|
|
2435
4517
|
disabled: !writable,
|
|
2436
4518
|
onReset: function () {
|
|
2437
4519
|
resetField(props.field);
|
|
2438
4520
|
},
|
|
2439
4521
|
},
|
|
2440
|
-
h(
|
|
2441
|
-
|
|
2442
|
-
max: WEIGHT_MAX,
|
|
2443
|
-
value: props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value,
|
|
2444
|
-
disabled: !writable,
|
|
4522
|
+
h(StackPicker, {
|
|
4523
|
+
t: t,
|
|
2445
4524
|
label: t(props.labelKey),
|
|
2446
|
-
|
|
2447
|
-
minLabel: String(WEIGHT_MIN),
|
|
2448
|
-
maxLabel: String(WEIGHT_MAX),
|
|
4525
|
+
value: stack,
|
|
2449
4526
|
onChange: function (value) {
|
|
2450
|
-
|
|
2451
|
-
// alone" rather than "write 400 on every element".
|
|
2452
|
-
if (value === NEUTRAL_WEIGHT) resetField(props.field);
|
|
2453
|
-
else setField(props.field, value);
|
|
4527
|
+
setField(props.field, value);
|
|
2454
4528
|
},
|
|
2455
4529
|
})
|
|
2456
4530
|
);
|
|
2457
4531
|
};
|
|
2458
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
|
+
|
|
2459
4575
|
return h(
|
|
2460
4576
|
"li",
|
|
2461
4577
|
{ className: "dfp-card" + (open ? " dfp-cardOpen" : "") },
|
|
@@ -2495,193 +4611,326 @@ function FontCard(props) {
|
|
|
2495
4611
|
{ className: "dfp-body" },
|
|
2496
4612
|
writable ? null : h("p", { className: "dfp-readOnly", role: "status" }, t("card.readOnly")),
|
|
2497
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.
|
|
2498
4641
|
h(
|
|
2499
4642
|
"div",
|
|
2500
|
-
{ className: "dfp-
|
|
2501
|
-
h(
|
|
4643
|
+
{ className: "dfp-field" },
|
|
4644
|
+
h(
|
|
4645
|
+
"div",
|
|
4646
|
+
{ className: "dfp-fieldHead" },
|
|
4647
|
+
h("span", { className: "dfp-fieldLabel" }, t("preset.label"))
|
|
4648
|
+
),
|
|
2502
4649
|
h(
|
|
2503
4650
|
"div",
|
|
2504
|
-
{ 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
|
+
}),
|
|
2505
4684
|
h(
|
|
2506
4685
|
"button",
|
|
2507
4686
|
{
|
|
2508
4687
|
type: "button",
|
|
2509
|
-
className: "dfp-
|
|
2510
|
-
|
|
4688
|
+
className: "dfp-miniButton",
|
|
4689
|
+
disabled: !writable,
|
|
2511
4690
|
onClick: function () {
|
|
2512
|
-
|
|
4691
|
+
setPresetName(activeName);
|
|
4692
|
+
setRenaming(true);
|
|
2513
4693
|
},
|
|
2514
4694
|
},
|
|
2515
|
-
t("
|
|
4695
|
+
t("preset.rename")
|
|
2516
4696
|
),
|
|
2517
4697
|
h(
|
|
2518
4698
|
"button",
|
|
2519
4699
|
{
|
|
2520
4700
|
type: "button",
|
|
2521
|
-
className: "dfp-
|
|
2522
|
-
|
|
4701
|
+
className: "dfp-miniButton",
|
|
4702
|
+
disabled: !writable,
|
|
2523
4703
|
onClick: function () {
|
|
2524
|
-
|
|
4704
|
+
setImportOpen(!importOpen);
|
|
2525
4705
|
},
|
|
2526
4706
|
},
|
|
2527
|
-
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")
|
|
2528
4717
|
)
|
|
2529
|
-
)
|
|
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
|
|
2530
4753
|
),
|
|
2531
4754
|
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
westLabel: "sansWest.label",
|
|
2537
|
-
eastLabel: "sansEast.label",
|
|
2538
|
-
slots: deriveSlots(config[SANS_FIELD]),
|
|
2539
|
-
disabled: !writable,
|
|
2540
|
-
onPickWest: pickSansWest,
|
|
2541
|
-
onPickEast: pickSansEast,
|
|
2542
|
-
onRemoveWest: function () {
|
|
2543
|
-
var slots = deriveSlots(config[SANS_FIELD]);
|
|
2544
|
-
if (slots.west !== null) dropSansEntry(slots.west);
|
|
2545
|
-
},
|
|
2546
|
-
onRemoveEast: function () {
|
|
2547
|
-
var slots = deriveSlots(config[SANS_FIELD]);
|
|
2548
|
-
if (slots.east !== null) dropSansEntry(slots.east);
|
|
2549
|
-
},
|
|
2550
|
-
})
|
|
2551
|
-
: h(
|
|
2552
|
-
FieldShell,
|
|
4755
|
+
// the accordion, conversation first (it owns every axis), then the
|
|
4756
|
+
// interface (which follows it), then code
|
|
4757
|
+
h(
|
|
4758
|
+
Section,
|
|
2553
4759
|
{
|
|
2554
4760
|
t: t,
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
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");
|
|
2561
4766
|
},
|
|
2562
4767
|
},
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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
|
+
)
|
|
2571
4807
|
),
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
field: SIZE_FIELD,
|
|
2575
|
-
labelKey: "size.bodyLabel",
|
|
2576
|
-
hintKey: "size.bodyHint",
|
|
2577
|
-
value: offset,
|
|
2578
|
-
text: offsetText,
|
|
2579
|
-
}),
|
|
2580
|
-
|
|
2581
|
-
weightField({
|
|
2582
|
-
field: WEIGHT_FIELD,
|
|
2583
|
-
labelKey: "weight.bodyLabel",
|
|
2584
|
-
hintKey: "weight.bodyHint",
|
|
2585
|
-
value: weight,
|
|
2586
|
-
}),
|
|
2587
|
-
|
|
2588
|
-
view === "simple"
|
|
2589
|
-
? h(SimpleFamilyField, {
|
|
2590
|
-
t: t,
|
|
2591
|
-
label: t("mono.label"),
|
|
2592
|
-
westLabel: "monoWest.label",
|
|
2593
|
-
eastLabel: "monoEast.label",
|
|
2594
|
-
slots: deriveSlots(config[MONO_FIELD]),
|
|
2595
|
-
disabled: !writable,
|
|
2596
|
-
onPickWest: pickMonoWest,
|
|
2597
|
-
onPickEast: pickMonoEast,
|
|
2598
|
-
onRemoveWest: function () {
|
|
2599
|
-
var slots = deriveSlots(config[MONO_FIELD]);
|
|
2600
|
-
if (slots.west !== null) dropMonoEntry(slots.west);
|
|
2601
|
-
},
|
|
2602
|
-
onRemoveEast: function () {
|
|
2603
|
-
var slots = deriveSlots(config[MONO_FIELD]);
|
|
2604
|
-
if (slots.east !== null) dropMonoEntry(slots.east);
|
|
2605
|
-
},
|
|
2606
|
-
})
|
|
2607
|
-
: h(
|
|
2608
|
-
FieldShell,
|
|
4808
|
+
h(
|
|
4809
|
+
Section,
|
|
2609
4810
|
{
|
|
2610
4811
|
t: t,
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
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");
|
|
2617
4817
|
},
|
|
2618
4818
|
},
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
value: config[MONO_FIELD],
|
|
2623
|
-
onChange: function (value) {
|
|
2624
|
-
setField(MONO_FIELD, value);
|
|
2625
|
-
},
|
|
2626
|
-
})
|
|
2627
|
-
),
|
|
2628
|
-
|
|
2629
|
-
sizeField({
|
|
2630
|
-
field: CODE_SIZE_FIELD,
|
|
2631
|
-
labelKey: "size.codeLabel",
|
|
2632
|
-
hintKey: "size.codeHint",
|
|
2633
|
-
value: codeOffset,
|
|
2634
|
-
text: codeOffsetText,
|
|
2635
|
-
}),
|
|
2636
|
-
|
|
2637
|
-
weightField({
|
|
2638
|
-
field: CODE_WEIGHT_FIELD,
|
|
2639
|
-
labelKey: "weight.codeLabel",
|
|
2640
|
-
hintKey: "weight.codeHint",
|
|
2641
|
-
value: codeWeight,
|
|
2642
|
-
}),
|
|
2643
|
-
|
|
2644
|
-
h(
|
|
2645
|
-
"div",
|
|
2646
|
-
{ className: "dfp-field dfp-fieldLast" },
|
|
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.
|
|
2647
4822
|
h(
|
|
2648
4823
|
"div",
|
|
2649
|
-
{ className: "dfp-
|
|
2650
|
-
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
|
+
)
|
|
2651
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
|
+
}),
|
|
2652
4856
|
h(
|
|
2653
4857
|
"div",
|
|
2654
4858
|
{ className: "dfp-previewBox" },
|
|
2655
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")),
|
|
2656
4904
|
h(
|
|
2657
4905
|
"div",
|
|
2658
|
-
{
|
|
2659
|
-
className: "dfp-previewText",
|
|
2660
|
-
style:
|
|
2661
|
-
config[SANS_FIELD] === ""
|
|
2662
|
-
? undefined
|
|
2663
|
-
: { fontFamily: formatStack(parseStack(config[SANS_FIELD])) },
|
|
2664
|
-
},
|
|
2665
|
-
t("preview.sample")
|
|
2666
|
-
),
|
|
2667
|
-
h(
|
|
2668
|
-
"div",
|
|
2669
|
-
{ className: "dfp-previewCaption", style: { marginTop: 10 } },
|
|
2670
|
-
t("preview.monoCaption")
|
|
2671
|
-
),
|
|
2672
|
-
h(
|
|
2673
|
-
"div",
|
|
2674
|
-
{
|
|
2675
|
-
className: "dfp-previewText dfp-previewCode",
|
|
2676
|
-
style:
|
|
2677
|
-
config[MONO_FIELD] === ""
|
|
2678
|
-
? undefined
|
|
2679
|
-
: { fontFamily: formatStack(parseStack(config[MONO_FIELD])) },
|
|
2680
|
-
},
|
|
4906
|
+
{ className: "dfp-previewText dfp-previewCode", style: previewCodeStyle },
|
|
2681
4907
|
t("preview.code")
|
|
2682
4908
|
)
|
|
2683
4909
|
)
|
|
2684
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
|
+
),
|
|
2685
4934
|
|
|
2686
4935
|
h(
|
|
2687
4936
|
"div",
|
|
@@ -2693,12 +4942,27 @@ function FontCard(props) {
|
|
|
2693
4942
|
className: "dfp-resetAll",
|
|
2694
4943
|
disabled: !writable,
|
|
2695
4944
|
onClick: function () {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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
|
+
}
|
|
2702
4966
|
},
|
|
2703
4967
|
},
|
|
2704
4968
|
t("card.resetAll")
|
|
@@ -2725,11 +4989,58 @@ function apply(ctx) {
|
|
|
2725
4989
|
return tokens;
|
|
2726
4990
|
});
|
|
2727
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
|
+
};
|
|
2728
5034
|
|
|
2729
5035
|
var sync = function () {
|
|
2730
5036
|
var snapshot = scope.getSnapshot();
|
|
2731
5037
|
if (snapshot.value === undefined) return;
|
|
2732
|
-
|
|
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);
|
|
2733
5044
|
};
|
|
2734
5045
|
ctx.effect(
|
|
2735
5046
|
function () {
|
|
@@ -2741,7 +5052,11 @@ function apply(ctx) {
|
|
|
2741
5052
|
|
|
2742
5053
|
// DSH writes its tokens (and its content font size) after this bundle
|
|
2743
5054
|
// activates, and the host's own boot row only covers the first frame; the
|
|
2744
|
-
// size axis therefore re-reads the live values and reapplies when they
|
|
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;
|
|
2745
5060
|
ctx.effect(
|
|
2746
5061
|
function () {
|
|
2747
5062
|
if (typeof document === "undefined") return undefined;
|
|
@@ -2762,6 +5077,7 @@ function apply(ctx) {
|
|
|
2762
5077
|
}
|
|
2763
5078
|
sync();
|
|
2764
5079
|
};
|
|
5080
|
+
scheduleRecheck = recheck;
|
|
2765
5081
|
var timer = globalThis.setTimeout(recheck, 500);
|
|
2766
5082
|
var interval = globalThis.setInterval(recheck, 4000);
|
|
2767
5083
|
return function () {
|
|
@@ -2771,6 +5087,17 @@ function apply(ctx) {
|
|
|
2771
5087
|
},
|
|
2772
5088
|
"dsh-fonttune: token refresh"
|
|
2773
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
|
+
);
|
|
2774
5101
|
|
|
2775
5102
|
var t = function (key, params) {
|
|
2776
5103
|
var locale = "en";
|
|
@@ -2802,6 +5129,9 @@ function apply(ctx) {
|
|
|
2802
5129
|
});
|
|
2803
5130
|
}
|
|
2804
5131
|
|
|
5132
|
+
exports.apply = apply;
|
|
5133
|
+
exports.inject = inject;
|
|
5134
|
+
|
|
2805
5135
|
exports.apply = apply;
|
|
2806
5136
|
exports.inject = inject;
|
|
2807
5137
|
|