dsh-fonttune 0.2.1 → 0.2.3
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 +38 -0
- package/CHANGELOG.md +38 -0
- package/README.en.md +10 -6
- package/README.md +10 -6
- package/lib/client.js +506 -303
- package/lib/shared.cjs +291 -146
- package/package.json +7 -2
package/lib/client.js
CHANGED
|
@@ -161,9 +161,6 @@ var LIGATURES_DEFAULT = 0;
|
|
|
161
161
|
var LIGATURES_ON = 1;
|
|
162
162
|
var LIGATURES_OFF = 2;
|
|
163
163
|
|
|
164
|
-
/** Marker on both injected style tags, used for scoping every rule we write. */
|
|
165
|
-
var MARKER = "dfp";
|
|
166
|
-
|
|
167
164
|
/** `data-plugin-css` value of the tag applying the configuration. */
|
|
168
165
|
var STYLE_TAG = "dsh-fonttune";
|
|
169
166
|
|
|
@@ -196,20 +193,18 @@ var DEFAULTS = {
|
|
|
196
193
|
};
|
|
197
194
|
|
|
198
195
|
/**
|
|
199
|
-
* The axis fields a preset snapshots
|
|
200
|
-
* the storage fields themselves.
|
|
196
|
+
* The axis fields a preset snapshots and the light/dark sets are compared
|
|
197
|
+
* over: everything the card edits, excluding the storage fields themselves.
|
|
201
198
|
*/
|
|
202
199
|
var VALUE_FIELDS = [
|
|
203
200
|
SANS_FIELD,
|
|
204
201
|
STACK_DIALOG_FIELD,
|
|
205
202
|
MONO_FIELD,
|
|
206
|
-
SIZE_FIELD,
|
|
207
203
|
SIZE_DIALOG_FIELD,
|
|
208
204
|
CODE_SIZE_FIELD,
|
|
209
205
|
WEIGHT_FIELD,
|
|
210
206
|
WEIGHT_DIALOG_FIELD,
|
|
211
207
|
CODE_WEIGHT_FIELD,
|
|
212
|
-
LINE_HEIGHT_FIELD,
|
|
213
208
|
LINE_HEIGHT_DIALOG_FIELD,
|
|
214
209
|
CODE_LINE_HEIGHT_FIELD,
|
|
215
210
|
LIGATURES_FIELD,
|
|
@@ -219,6 +214,25 @@ var VALUE_FIELDS = [
|
|
|
219
214
|
UI_FOLLOWS_FIELD,
|
|
220
215
|
];
|
|
221
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Axes that are still stored but no longer render: the interface's own size
|
|
219
|
+
* and line-height.
|
|
220
|
+
*
|
|
221
|
+
* DSH 0.1.5-rc.2 publishes no interface size or line-height hook at all (the
|
|
222
|
+
* measurement lives in `buildAxisCss`), so these two fields inject nothing.
|
|
223
|
+
* A document written by 0.1.x still carries them, so they keep their schema
|
|
224
|
+
* entries and stay part of normalization — but they are not value axes: a
|
|
225
|
+
* preset no longer snapshots a dead field, and a light/dark pair that differs
|
|
226
|
+
* only here no longer counts as two sets. The card's "reset every axis" clears
|
|
227
|
+
* them too, so a value nothing shows cannot hide in a document forever.
|
|
228
|
+
*/
|
|
229
|
+
var RETIRED_FIELDS = [SIZE_FIELD, LINE_HEIGHT_FIELD];
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Every durable axis field, live and retired: what normalization covers.
|
|
233
|
+
*/
|
|
234
|
+
var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS);
|
|
235
|
+
|
|
222
236
|
/**
|
|
223
237
|
* Longest accepted font stack, in characters (mirrored by the host schema).
|
|
224
238
|
*/
|
|
@@ -381,8 +395,8 @@ function normalizeField(field, value) {
|
|
|
381
395
|
function normalizeConfig(value) {
|
|
382
396
|
var source = value !== null && typeof value === "object" ? value : {};
|
|
383
397
|
var config = {};
|
|
384
|
-
for (var index = 0; index <
|
|
385
|
-
var field =
|
|
398
|
+
for (var index = 0; index < DURABLE_FIELDS.length; index += 1) {
|
|
399
|
+
var field = DURABLE_FIELDS[index];
|
|
386
400
|
config[field] = normalizeField(field, source[field]);
|
|
387
401
|
}
|
|
388
402
|
config[PER_THEME_FIELD] = toBool(source[PER_THEME_FIELD]);
|
|
@@ -617,7 +631,7 @@ function isGenericFamilyName(name) {
|
|
|
617
631
|
function isCJKFamilyName(name) {
|
|
618
632
|
var lower = name.toLowerCase();
|
|
619
633
|
if (
|
|
620
|
-
/(?:yahei|jhenghei|pingfang|hiragino|simsun|simhei|nsimsun|kaiti|fangsong|meiryo|yu ?goth|yu ?minch|ms ?gothic|
|
|
634
|
+
/(?:yahei|jhenghei|pingfang|hiragino|simsun|simhei|nsimsun|kaiti|fangsong|meiryo|yu ?goth|yu ?minch|ms ?p?(?:gothic|mincho)|noto (?:sans|serif) (?:sc|tc|cjk|jp|kr|hk)|source han|sourcehansc|sourcehanserifc|sarasa|misans|harmonyos|wenquanyi|lxgw|unifont|dengxian)/.test(
|
|
621
635
|
lower
|
|
622
636
|
)
|
|
623
637
|
) {
|
|
@@ -811,6 +825,11 @@ function scaleLineTokens(names, baseTokens, factor) {
|
|
|
811
825
|
*/
|
|
812
826
|
function shiftTokens(names, baseTokens, offset) {
|
|
813
827
|
var out = [];
|
|
828
|
+
// Zero is the neutral value of every additive axis: emitting `calc((14px) + 0px)`
|
|
829
|
+
// would be a real declaration that disagrees with `isDormant` and with an
|
|
830
|
+
// otherwise identical configuration, so the guard lives here too, not only at
|
|
831
|
+
// the call sites.
|
|
832
|
+
if (offset === 0) return out;
|
|
814
833
|
for (var index = 0; index < names.length; index += 1) {
|
|
815
834
|
var name = names[index];
|
|
816
835
|
var base = baseTokens[name];
|
|
@@ -1027,17 +1046,6 @@ function isShorthandName(name) {
|
|
|
1027
1046
|
return true;
|
|
1028
1047
|
}
|
|
1029
1048
|
|
|
1030
|
-
/**
|
|
1031
|
-
* Whether one token (name + value) is a `font` shorthand this plugin may
|
|
1032
|
-
* rewrite: a bare `--dsw-font-…` name whose value parses as a shorthand.
|
|
1033
|
-
* @param {string} name - custom property name.
|
|
1034
|
-
* @param {string} value - the token's untouched value.
|
|
1035
|
-
* @returns {boolean}
|
|
1036
|
-
*/
|
|
1037
|
-
function isShorthandToken(name, value) {
|
|
1038
|
-
return isShorthandName(name) && parseShorthand(value) !== null;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
1049
|
/**
|
|
1042
1050
|
* Whether a token is one this plugin reads and re-declares at all.
|
|
1043
1051
|
* @param {string} name - custom property name including the leading dashes.
|
|
@@ -1066,35 +1074,6 @@ function tokenNames(baseTokens) {
|
|
|
1066
1074
|
return names;
|
|
1067
1075
|
}
|
|
1068
1076
|
|
|
1069
|
-
/**
|
|
1070
|
-
* The interface PART size tokens: everything scalable except the code and
|
|
1071
|
-
* markdown families and the delta tokens (which the dialog offset shifts
|
|
1072
|
-
* additively, never multiplicatively).
|
|
1073
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1074
|
-
* @returns {string[]} the names the interface offset re-declares.
|
|
1075
|
-
*/
|
|
1076
|
-
function bodyTokenNames(baseTokens) {
|
|
1077
|
-
return tokenNames(baseTokens).filter(function (name) {
|
|
1078
|
-
return (
|
|
1079
|
-
!isCodeToken(name) &&
|
|
1080
|
-
!isMarkdownToken(name) &&
|
|
1081
|
-
!isDeltaToken(name) &&
|
|
1082
|
-
!isContentToken(name)
|
|
1083
|
-
);
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
/**
|
|
1088
|
-
* The interface line-height PART tokens (non-code, non-markdown).
|
|
1089
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1090
|
-
* @returns {string[]}
|
|
1091
|
-
*/
|
|
1092
|
-
function uiLineTokenNames(baseTokens) {
|
|
1093
|
-
return bodyTokenNames(baseTokens).filter(function (name) {
|
|
1094
|
-
return name.indexOf("-line-height") >= 0;
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
1077
|
/**
|
|
1099
1078
|
* The markdown (dialog) PART tokens: sizes and line heights of the markdown
|
|
1100
1079
|
* family, excluding code.
|
|
@@ -1105,30 +1084,6 @@ function markdownTokenNames(baseTokens) {
|
|
|
1105
1084
|
return tokenNames(baseTokens).filter(isMarkdownToken);
|
|
1106
1085
|
}
|
|
1107
1086
|
|
|
1108
|
-
/**
|
|
1109
|
-
* The markdown content-size DELTA tokens present in a base map.
|
|
1110
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1111
|
-
* @returns {string[]}
|
|
1112
|
-
*/
|
|
1113
|
-
function deltaTokenNames(baseTokens) {
|
|
1114
|
-
if (baseTokens === null || typeof baseTokens !== "object") return [];
|
|
1115
|
-
return Object.keys(baseTokens).filter(isDeltaToken);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
/**
|
|
1119
|
-
* The code PART tokens: the `font` shorthands' size/height parts, when the
|
|
1120
|
-
* live stylesheets declare them. The shorthands themselves are handled by
|
|
1121
|
-
* `codeShorthandTokens`, so they are excluded here to avoid double
|
|
1122
|
-
* declarations.
|
|
1123
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1124
|
-
* @returns {string[]}
|
|
1125
|
-
*/
|
|
1126
|
-
function codeTokenNames(baseTokens) {
|
|
1127
|
-
return tokenNames(baseTokens).filter(function (name) {
|
|
1128
|
-
return isCodeToken(name) && !isShorthandName(name);
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
1087
|
/**
|
|
1133
1088
|
* Every `font` shorthand token in a base map that parses, sorted by name
|
|
1134
1089
|
* length (families before variants, mirroring the part-token order).
|
|
@@ -1161,17 +1116,6 @@ function markdownShorthandTokens(baseTokens) {
|
|
|
1161
1116
|
});
|
|
1162
1117
|
}
|
|
1163
1118
|
|
|
1164
|
-
/**
|
|
1165
|
-
* The interface (non-markdown, non-code) `font` shorthand tokens.
|
|
1166
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1167
|
-
* @returns {{name: string, parts: object}[]}
|
|
1168
|
-
*/
|
|
1169
|
-
function uiShorthandTokens(baseTokens) {
|
|
1170
|
-
return shorthandTokens(baseTokens).filter(function (entry) {
|
|
1171
|
-
return !isMarkdownToken(entry.name) && !isCodeToken(entry.name);
|
|
1172
|
-
});
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
1119
|
/**
|
|
1176
1120
|
* The code `font` shorthand tokens.
|
|
1177
1121
|
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
@@ -1187,6 +1131,66 @@ function codeShorthandTokens(baseTokens) {
|
|
|
1187
1131
|
* selectors
|
|
1188
1132
|
* ------------------------------------------------------------------ */
|
|
1189
1133
|
|
|
1134
|
+
/**
|
|
1135
|
+
* Build one exclusion that keeps a surface out of a rule.
|
|
1136
|
+
*
|
|
1137
|
+
* The whole list goes into a single `:not(…)`. The long form —
|
|
1138
|
+
* `:not(pre):not(pre *)…` — repeats `:not(` and `)` per argument, and that
|
|
1139
|
+
* chain is copied into both dialog alternatives and the interface rule, so the
|
|
1140
|
+
* compact form saves roughly 120 bytes per copy.
|
|
1141
|
+
*
|
|
1142
|
+
* The trade is specificity: `:not(a):not(b)` SUMS its arguments while
|
|
1143
|
+
* `:not(a, b)` takes the most specific one. Nothing here depends on that sum.
|
|
1144
|
+
* Every rule below carries `!important`, which already beats the theme's
|
|
1145
|
+
* non-important class rules whatever the specificity; the shipped DSH 0.1.5
|
|
1146
|
+
* assets hold exactly one `!important` typography declaration
|
|
1147
|
+
* (`font-size` on inline markdown code, and no size declaration is ever
|
|
1148
|
+
* emitted from here); and the rules that could collide with each other are
|
|
1149
|
+
* held apart by these very exclusions rather than by their weight. Verified
|
|
1150
|
+
* against the shipped bundle and every installed plugin: no other
|
|
1151
|
+
* `!important` `font-weight`/`font-family` rule exists to lose to.
|
|
1152
|
+
*
|
|
1153
|
+
* @param {string[]} selectors - the selectors to exclude.
|
|
1154
|
+
* @returns {string} one `:not(…)` carrying the whole list.
|
|
1155
|
+
*/
|
|
1156
|
+
function excludeAll(selectors) {
|
|
1157
|
+
return ":not(" + selectors.join(",") + ")";
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* A code surface, and its descendants: a highlight `<span>` inside a `<pre>`
|
|
1162
|
+
* is not a `pre`, so an exclusion has to reach through ancestry.
|
|
1163
|
+
*/
|
|
1164
|
+
var CODE_SURFACE_SELECTORS = [
|
|
1165
|
+
"pre",
|
|
1166
|
+
"pre *",
|
|
1167
|
+
"code",
|
|
1168
|
+
"code *",
|
|
1169
|
+
"kbd",
|
|
1170
|
+
"kbd *",
|
|
1171
|
+
"samp",
|
|
1172
|
+
"samp *",
|
|
1173
|
+
"var",
|
|
1174
|
+
"var *",
|
|
1175
|
+
"tt",
|
|
1176
|
+
"tt *",
|
|
1177
|
+
"textarea",
|
|
1178
|
+
"textarea *",
|
|
1179
|
+
'[class*="code" i]',
|
|
1180
|
+
'[class*="code" i] *',
|
|
1181
|
+
'[class*="terminal" i]',
|
|
1182
|
+
'[class*="terminal" i] *',
|
|
1183
|
+
];
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Everything a code surface must be excluded from a DIALOG-scoped rule: the
|
|
1187
|
+
* code elements themselves AND their descendants (a highlight `<span>` inside
|
|
1188
|
+
* a `<pre>` is not a `pre`, so the exclusion has to reach through ancestry
|
|
1189
|
+
* with a complex `:not()` argument — supported by the Chromium line DSH
|
|
1190
|
+
* targets).
|
|
1191
|
+
*/
|
|
1192
|
+
var CODE_EXCLUDES = excludeAll(CODE_SURFACE_SELECTORS);
|
|
1193
|
+
|
|
1190
1194
|
/**
|
|
1191
1195
|
* What counts as code text for the code axes.
|
|
1192
1196
|
*
|
|
@@ -1225,38 +1229,6 @@ var CODE_SELECTOR = [
|
|
|
1225
1229
|
})
|
|
1226
1230
|
.join(",");
|
|
1227
1231
|
|
|
1228
|
-
/**
|
|
1229
|
-
* Everything a code surface must be excluded from a DIALOG-scoped rule:
|
|
1230
|
-
* the code elements themselves AND their descendants (a highlight `<span>`
|
|
1231
|
-
* inside a `<pre>` is not a `pre`, so the exclusion has to reach through
|
|
1232
|
-
* ancestry with complex `:not()` arguments — supported by the Chromium line
|
|
1233
|
-
* DSH targets).
|
|
1234
|
-
*/
|
|
1235
|
-
var CODE_EXCLUDES = [
|
|
1236
|
-
"pre",
|
|
1237
|
-
"pre *",
|
|
1238
|
-
"code",
|
|
1239
|
-
"code *",
|
|
1240
|
-
"kbd",
|
|
1241
|
-
"kbd *",
|
|
1242
|
-
"samp",
|
|
1243
|
-
"samp *",
|
|
1244
|
-
"var",
|
|
1245
|
-
"var *",
|
|
1246
|
-
"tt",
|
|
1247
|
-
"tt *",
|
|
1248
|
-
"textarea",
|
|
1249
|
-
"textarea *",
|
|
1250
|
-
'[class*="code" i]',
|
|
1251
|
-
'[class*="code" i] *',
|
|
1252
|
-
'[class*="terminal" i]',
|
|
1253
|
-
'[class*="terminal" i] *',
|
|
1254
|
-
]
|
|
1255
|
-
.map(function (selector) {
|
|
1256
|
-
return ":not(" + selector + ")";
|
|
1257
|
-
})
|
|
1258
|
-
.join("");
|
|
1259
|
-
|
|
1260
1232
|
/**
|
|
1261
1233
|
* The dialog surface: DSH renders the conversation markdown in a CSS-module
|
|
1262
1234
|
* container whose class hashes per release (`_markdown_<hash>_…`). A
|
|
@@ -1288,11 +1260,57 @@ var DARK_ATTR = "body[data-ds-dark-theme]";
|
|
|
1288
1260
|
* interface's own preview stays inside the rule, and `dfp-previewCode` is a
|
|
1289
1261
|
* code surface like any other.
|
|
1290
1262
|
*/
|
|
1291
|
-
var INTERFACE_EXCLUDES =
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1263
|
+
var INTERFACE_EXCLUDES = excludeAll(
|
|
1264
|
+
CODE_SURFACE_SELECTORS.concat([
|
|
1265
|
+
".cm-editor",
|
|
1266
|
+
".cm-editor *",
|
|
1267
|
+
".dfp-previewCode",
|
|
1268
|
+
".dfp-previewCode *",
|
|
1269
|
+
'[class*="_markdown_" i]',
|
|
1270
|
+
'[class*="_markdown_" i] *',
|
|
1271
|
+
".dfp-previewDialog",
|
|
1272
|
+
".dfp-previewDialog *",
|
|
1273
|
+
])
|
|
1274
|
+
);
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Split a selector list on its top-level commas only.
|
|
1278
|
+
*
|
|
1279
|
+
* The exclusions hold a selector LIST inside `:not(…)`, so a plain
|
|
1280
|
+
* `split(",")` would cut a selector in half — and the per-theme rules prefix
|
|
1281
|
+
* every selector in the list.
|
|
1282
|
+
* @param {string} selectorList - a comma-separated selector list.
|
|
1283
|
+
* @returns {string[]} the top-level parts, verbatim.
|
|
1284
|
+
*/
|
|
1285
|
+
function splitSelectorList(selectorList) {
|
|
1286
|
+
var parts = [];
|
|
1287
|
+
var current = "";
|
|
1288
|
+
var depth = 0;
|
|
1289
|
+
var quote = "";
|
|
1290
|
+
for (var index = 0; index < selectorList.length; index += 1) {
|
|
1291
|
+
var character = selectorList[index];
|
|
1292
|
+
if (quote !== "") {
|
|
1293
|
+
current += character;
|
|
1294
|
+
if (character === quote) quote = "";
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (character === '"' || character === "'") {
|
|
1298
|
+
quote = character;
|
|
1299
|
+
current += character;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
if (character === "(" || character === "[") depth += 1;
|
|
1303
|
+
if (character === ")" || character === "]") depth -= 1;
|
|
1304
|
+
if (character === "," && depth === 0) {
|
|
1305
|
+
parts.push(current);
|
|
1306
|
+
current = "";
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
current += character;
|
|
1310
|
+
}
|
|
1311
|
+
parts.push(current);
|
|
1312
|
+
return parts;
|
|
1313
|
+
}
|
|
1296
1314
|
|
|
1297
1315
|
/**
|
|
1298
1316
|
* Prefix every selector in a comma list, for the per-theme rules.
|
|
@@ -1301,7 +1319,7 @@ var INTERFACE_EXCLUDES =
|
|
|
1301
1319
|
* @returns {string} the prefixed list.
|
|
1302
1320
|
*/
|
|
1303
1321
|
function prefixSelector(selectorList, prefix) {
|
|
1304
|
-
var parts = selectorList
|
|
1322
|
+
var parts = splitSelectorList(selectorList);
|
|
1305
1323
|
var out = [];
|
|
1306
1324
|
for (var index = 0; index < parts.length; index += 1) {
|
|
1307
1325
|
out.push(prefix + " " + parts[index]);
|
|
@@ -1463,9 +1481,13 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1463
1481
|
*/
|
|
1464
1482
|
function pushScopedCodeFamily() {
|
|
1465
1483
|
var scopedCode = [];
|
|
1466
|
-
var surfaces = CODE_SELECTOR
|
|
1484
|
+
var surfaces = splitSelectorList(CODE_SELECTOR);
|
|
1467
1485
|
for (var cIndex = 0; cIndex < surfaces.length; cIndex += 1) {
|
|
1468
|
-
|
|
1486
|
+
var scoped = '[class*="_markdown_" i] ' + surfaces[cIndex];
|
|
1487
|
+
// The dark variant has to carry the theme attribute like every other
|
|
1488
|
+
// rule here, or a per-theme set would re-assert the LIGHT code family
|
|
1489
|
+
// inside the dark dialog.
|
|
1490
|
+
scopedCode.push(isDark ? DARK_ATTR + " " + scoped : scoped);
|
|
1469
1491
|
}
|
|
1470
1492
|
declarations.push(scopedCode.join(",") + "{font-family:" + mono + " !important}");
|
|
1471
1493
|
}
|
|
@@ -1562,8 +1584,8 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1562
1584
|
// (secondary size, both deltas, delta-based heading sizes, heights) at their
|
|
1563
1585
|
// body-computed values. The offset is therefore added once, at the source —
|
|
1564
1586
|
// `--dsh-content-font-size` becomes "official base + offset" — and the whole
|
|
1565
|
-
// ladder follows by derivation, with nothing counted twice. The
|
|
1566
|
-
// axis never touches this chain
|
|
1587
|
+
// ladder follows by derivation, with nothing counted twice. The retired
|
|
1588
|
+
// interface size axis never touches this chain at all, so the conversation
|
|
1567
1589
|
// moves only when this axis says so, and a dialog that FOLLOWS the interface
|
|
1568
1590
|
// follows it through this value.
|
|
1569
1591
|
var dialogOffsetValue = axis[SIZE_DIALOG_FIELD];
|
|
@@ -1975,18 +1997,137 @@ var PRESETS = {
|
|
|
1975
1997
|
};
|
|
1976
1998
|
|
|
1977
1999
|
/**
|
|
1978
|
-
*
|
|
1979
|
-
* height inside one simple `size/height <family>` shorthand.
|
|
1980
|
-
* @param {string} value - a value shaped like `11px/19px var(--ds-font-family-code)`.
|
|
1981
|
-
* @param {number} scale - the ratio to apply.
|
|
1982
|
-
* @returns {string|null} the scaled shorthand, or null when the shape differs.
|
|
2000
|
+
* The curated groups, in the order the picker lists them.
|
|
1983
2001
|
*/
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2002
|
+
var PRESET_GROUPS = [
|
|
2003
|
+
{ label: "stack.groupMono", families: PRESETS.mono },
|
|
2004
|
+
{ label: "stack.groupCjk", families: PRESETS.cjk },
|
|
2005
|
+
{ label: "stack.groupLatin", families: PRESETS.latin },
|
|
2006
|
+
{ label: "stack.groupGeneric", families: PRESETS.generic },
|
|
2007
|
+
];
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* How many enumerated families the "installed on this machine" group shows.
|
|
2011
|
+
* The curated groups are the short list; this one is the complete one, so the
|
|
2012
|
+
* cap is only a guard against a machine with an absurd number of families.
|
|
2013
|
+
*/
|
|
2014
|
+
var MAX_LOCAL_FONTS = 240;
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Build the family picker's groups.
|
|
2018
|
+
*
|
|
2019
|
+
* The curated groups are ALWAYS listed: whether or not the browser enumerated
|
|
2020
|
+
* local fonts, and whether or not a search is running. The old shape skipped
|
|
2021
|
+
* them whenever a working catalog met a non-empty query, so a curated family
|
|
2022
|
+
* this machine has not installed could not be found by name at all. Every
|
|
2023
|
+
* family is listed in exactly one group — the local enumeration gives way to
|
|
2024
|
+
* the curated headings rather than repeating what they already offer.
|
|
2025
|
+
*
|
|
2026
|
+
* @param {object} input - the picker's state.
|
|
2027
|
+
* @param {string[]} [input.stack] - the families the stored value names.
|
|
2028
|
+
* @param {boolean} [input.single] - single-pick mode (a West/CJK slot).
|
|
2029
|
+
* @param {string} [input.query] - the search text.
|
|
2030
|
+
* @param {{status: string, families: string[]}} [input.catalog] - enumeration.
|
|
2031
|
+
* @param {number} [input.maxLocal] - cap on the local group's rows.
|
|
2032
|
+
* @returns {Array<{label: string, families: string[]}>} the groups, in the
|
|
2033
|
+
* order the picker displays them.
|
|
2034
|
+
*/
|
|
2035
|
+
function pickerGroups(input) {
|
|
2036
|
+
var stack = Array.isArray(input.stack) ? input.stack : [];
|
|
2037
|
+
var single = input.single === true;
|
|
2038
|
+
var query = typeof input.query === "string" ? input.query : "";
|
|
2039
|
+
var catalog =
|
|
2040
|
+
input.catalog && typeof input.catalog === "object"
|
|
2041
|
+
? input.catalog
|
|
2042
|
+
: { status: "loading", families: [] };
|
|
2043
|
+
var maxLocal = typeof input.maxLocal === "number" ? input.maxLocal : MAX_LOCAL_FONTS;
|
|
2044
|
+
var needle = query.trim().toLowerCase();
|
|
2045
|
+
var index;
|
|
2046
|
+
var matches = function (name) {
|
|
2047
|
+
return needle === "" || String(name).toLowerCase().indexOf(needle) >= 0;
|
|
2048
|
+
};
|
|
2049
|
+
|
|
2050
|
+
// Which families the stack already names. In single mode the stack IS the
|
|
2051
|
+
// one pick, and it stays visible in the curated groups (with its check mark)
|
|
2052
|
+
// so the reader can see where it sits among its own kind.
|
|
2053
|
+
var inStack = {};
|
|
2054
|
+
if (!single) {
|
|
2055
|
+
for (index = 0; index < stack.length; index += 1) {
|
|
2056
|
+
inStack[String(stack[index]).toLowerCase()] = true;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// The curated groups are built FIRST, and the local list gives way to them:
|
|
2061
|
+
// they are the short list the reader scans, and on a real Windows machine
|
|
2062
|
+
// most of the Latin group is installed, so letting the local list win would
|
|
2063
|
+
// empty the curated headings. Everything lands in exactly one group.
|
|
2064
|
+
var curatedLower = {};
|
|
2065
|
+
var curatedGroups = [];
|
|
2066
|
+
for (index = 0; index < PRESET_GROUPS.length; index += 1) {
|
|
2067
|
+
var curated = PRESET_GROUPS[index];
|
|
2068
|
+
var rows = [];
|
|
2069
|
+
for (var nameIndex = 0; nameIndex < curated.families.length; nameIndex += 1) {
|
|
2070
|
+
var name = sanitizeFamily(curated.families[nameIndex]);
|
|
2071
|
+
if (name === "" || !matches(name)) continue;
|
|
2072
|
+
var key = name.toLowerCase();
|
|
2073
|
+
if (inStack[key] === true || curatedLower[key] === true) continue;
|
|
2074
|
+
curatedLower[key] = true;
|
|
2075
|
+
rows.push(name);
|
|
2076
|
+
}
|
|
2077
|
+
if (rows.length > 0) curatedGroups.push({ label: curated.label, families: rows });
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// Then the enumeration: the complete list, minus everything already offered
|
|
2081
|
+
// by a heading above it.
|
|
2082
|
+
var localRows = [];
|
|
2083
|
+
if (catalog.status === "ready") {
|
|
2084
|
+
var locals = Array.isArray(catalog.families) ? catalog.families : [];
|
|
2085
|
+
for (index = 0; index < locals.length; index += 1) {
|
|
2086
|
+
var localName = sanitizeFamily(locals[index]);
|
|
2087
|
+
if (localName === "" || !matches(localName)) continue;
|
|
2088
|
+
var localKey = localName.toLowerCase();
|
|
2089
|
+
if (inStack[localKey] === true || curatedLower[localKey] === true) continue;
|
|
2090
|
+
curatedLower[localKey] = true;
|
|
2091
|
+
localRows.push(localName);
|
|
2092
|
+
if (localRows.length >= maxLocal) break;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
var groups = [];
|
|
2097
|
+
var selectedRows = [];
|
|
2098
|
+
for (index = 0; index < stack.length; index += 1) {
|
|
2099
|
+
if (matches(stack[index])) selectedRows.push(stack[index]);
|
|
2100
|
+
}
|
|
2101
|
+
if (selectedRows.length > 0) {
|
|
2102
|
+
groups.push({ label: "stack.groupSelected", families: selectedRows });
|
|
2103
|
+
}
|
|
2104
|
+
for (index = 0; index < curatedGroups.length; index += 1) {
|
|
2105
|
+
groups.push(curatedGroups[index]);
|
|
2106
|
+
}
|
|
2107
|
+
if (localRows.length > 0) {
|
|
2108
|
+
groups.push({ label: "stack.groupLocal", families: localRows });
|
|
2109
|
+
}
|
|
2110
|
+
return groups;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* Whether a cached font-enumeration verdict may be asked for again.
|
|
2115
|
+
*
|
|
2116
|
+
* A working enumeration is final for the session, and a browser that has no
|
|
2117
|
+
* such API at all will never grow one — but a refusal is not: the user can
|
|
2118
|
+
* grant the font permission from the browser's prompt or its site settings.
|
|
2119
|
+
* Retrying is cheap, because a still-blocked permission is rejected instantly
|
|
2120
|
+
* instead of prompting again.
|
|
2121
|
+
*
|
|
2122
|
+
* @param {{status?: string, at?: number, permanent?: boolean}|null} cache - the cached verdict.
|
|
2123
|
+
* @param {number} now - the current time, in milliseconds.
|
|
2124
|
+
* @param {number} retryMs - how long a refusal is trusted.
|
|
2125
|
+
* @returns {boolean} true when the enumeration should run again.
|
|
2126
|
+
*/
|
|
2127
|
+
function catalogRefreshDue(cache, now, retryMs) {
|
|
2128
|
+
if (cache === null || cache === undefined) return true;
|
|
2129
|
+
if (cache.permanent === true || cache.status === "ready") return false;
|
|
2130
|
+
return now - (cache.at || 0) >= retryMs;
|
|
1990
2131
|
}
|
|
1991
2132
|
|
|
1992
2133
|
var shared = {
|
|
@@ -2013,6 +2154,8 @@ var shared = {
|
|
|
2013
2154
|
ACTIVE_PRESET_FIELD: ACTIVE_PRESET_FIELD,
|
|
2014
2155
|
UI_FOLLOWS_FIELD: UI_FOLLOWS_FIELD,
|
|
2015
2156
|
VALUE_FIELDS: VALUE_FIELDS,
|
|
2157
|
+
RETIRED_FIELDS: RETIRED_FIELDS,
|
|
2158
|
+
DURABLE_FIELDS: DURABLE_FIELDS,
|
|
2016
2159
|
SIZE_MIN: SIZE_MIN,
|
|
2017
2160
|
SIZE_MAX: SIZE_MAX,
|
|
2018
2161
|
WEIGHT_MIN: WEIGHT_MIN,
|
|
@@ -2025,7 +2168,6 @@ var shared = {
|
|
|
2025
2168
|
LIGATURES_DEFAULT: LIGATURES_DEFAULT,
|
|
2026
2169
|
LIGATURES_ON: LIGATURES_ON,
|
|
2027
2170
|
LIGATURES_OFF: LIGATURES_OFF,
|
|
2028
|
-
MARKER: MARKER,
|
|
2029
2171
|
STYLE_TAG: STYLE_TAG,
|
|
2030
2172
|
CARD_STYLE_TAG: CARD_STYLE_TAG,
|
|
2031
2173
|
DEFAULTS: DEFAULTS,
|
|
@@ -2036,7 +2178,17 @@ var shared = {
|
|
|
2036
2178
|
MAX_PRESET_NAME: MAX_PRESET_NAME,
|
|
2037
2179
|
FALLBACK_TOKENS: FALLBACK_TOKENS,
|
|
2038
2180
|
PRESETS: PRESETS,
|
|
2181
|
+
PRESET_GROUPS: PRESET_GROUPS,
|
|
2182
|
+
MAX_LOCAL_FONTS: MAX_LOCAL_FONTS,
|
|
2183
|
+
pickerGroups: pickerGroups,
|
|
2184
|
+
catalogRefreshDue: catalogRefreshDue,
|
|
2039
2185
|
MARKDOWN_SELECTOR: MARKDOWN_SELECTOR,
|
|
2186
|
+
CODE_SURFACE_SELECTORS: CODE_SURFACE_SELECTORS,
|
|
2187
|
+
CODE_EXCLUDES: CODE_EXCLUDES,
|
|
2188
|
+
INTERFACE_EXCLUDES: INTERFACE_EXCLUDES,
|
|
2189
|
+
excludeAll: excludeAll,
|
|
2190
|
+
splitSelectorList: splitSelectorList,
|
|
2191
|
+
prefixSelector: prefixSelector,
|
|
2040
2192
|
DARK_ATTR: DARK_ATTR,
|
|
2041
2193
|
sanitize: sanitize,
|
|
2042
2194
|
sanitizeFamily: sanitizeFamily,
|
|
@@ -2067,19 +2219,12 @@ var shared = {
|
|
|
2067
2219
|
isMarkdownToken: isMarkdownToken,
|
|
2068
2220
|
isDeltaToken: isDeltaToken,
|
|
2069
2221
|
isShorthandName: isShorthandName,
|
|
2070
|
-
isShorthandToken: isShorthandToken,
|
|
2071
2222
|
isScaledToken: isScaledToken,
|
|
2072
2223
|
tokenNames: tokenNames,
|
|
2073
|
-
codeTokenNames: codeTokenNames,
|
|
2074
2224
|
markdownTokenNames: markdownTokenNames,
|
|
2075
|
-
bodyTokenNames: bodyTokenNames,
|
|
2076
|
-
uiLineTokenNames: uiLineTokenNames,
|
|
2077
|
-
deltaTokenNames: deltaTokenNames,
|
|
2078
2225
|
shorthandTokens: shorthandTokens,
|
|
2079
2226
|
markdownShorthandTokens: markdownShorthandTokens,
|
|
2080
|
-
uiShorthandTokens: uiShorthandTokens,
|
|
2081
2227
|
codeShorthandTokens: codeShorthandTokens,
|
|
2082
|
-
scaleFontShorthand: scaleFontShorthand,
|
|
2083
2228
|
isGenericFamilyName: isGenericFamilyName,
|
|
2084
2229
|
isCJKFamilyName: isCJKFamilyName,
|
|
2085
2230
|
setWestEntry: setWestEntry,
|
|
@@ -2153,6 +2298,7 @@ var PRESETS_FIELD = shared.PRESETS_FIELD;
|
|
|
2153
2298
|
var ACTIVE_PRESET_FIELD = shared.ACTIVE_PRESET_FIELD;
|
|
2154
2299
|
var UI_FOLLOWS_FIELD = shared.UI_FOLLOWS_FIELD;
|
|
2155
2300
|
var VALUE_FIELDS = shared.VALUE_FIELDS;
|
|
2301
|
+
var RETIRED_FIELDS = shared.RETIRED_FIELDS;
|
|
2156
2302
|
var SIZE_MIN = shared.SIZE_MIN;
|
|
2157
2303
|
var SIZE_MAX = shared.SIZE_MAX;
|
|
2158
2304
|
var WEIGHT_MIN = shared.WEIGHT_MIN;
|
|
@@ -2218,12 +2364,6 @@ var CHEVRON_PATH =
|
|
|
2218
2364
|
/** Panel height cap, used to decide whether it opens up or down. */
|
|
2219
2365
|
var PANEL_HEIGHT = 380;
|
|
2220
2366
|
|
|
2221
|
-
/**
|
|
2222
|
-
* Cap on how many enumerated families the picker lists at once: a machine can
|
|
2223
|
-
* report well over a thousand, which would make the panel useless.
|
|
2224
|
-
*/
|
|
2225
|
-
var MAX_VISIBLE_FONTS = 240;
|
|
2226
|
-
|
|
2227
2367
|
/* ------------------------------------------------------------------ *
|
|
2228
2368
|
* copy
|
|
2229
2369
|
* ------------------------------------------------------------------ */
|
|
@@ -2282,13 +2422,9 @@ var DICTS = {
|
|
|
2282
2422
|
"monoWest.label": "Code · Western",
|
|
2283
2423
|
"monoEast.label": "Code · CJK",
|
|
2284
2424
|
"split.pick": "Choose…",
|
|
2285
|
-
"split.unset": "Not set",
|
|
2286
2425
|
"split.remove": "Remove {name}",
|
|
2287
2426
|
"split.rest": "Other fallbacks (reorder them in Advanced): {names}",
|
|
2288
2427
|
|
|
2289
|
-
"size.bodyLabel": "Interface font size offset",
|
|
2290
|
-
"size.bodyHint":
|
|
2291
|
-
"Adds {offset} to interface text sizes, on top of DSH's own font-size setting. 0 keeps DSH's sizes.",
|
|
2292
2428
|
"size.dialogLabel": "Conversation font size offset",
|
|
2293
2429
|
"size.dialogHint":
|
|
2294
2430
|
"Adds {offset} to the conversation text sizes, on top of DSH's own font-size setting. 0 keeps DSH's sizes.",
|
|
@@ -2307,9 +2443,6 @@ var DICTS = {
|
|
|
2307
2443
|
"weight.codeHint":
|
|
2308
2444
|
"Overrides code blocks, inline code and terminal output only. 400 or unset keeps DSH's own weight.",
|
|
2309
2445
|
|
|
2310
|
-
"line.bodyLabel": "Interface line height",
|
|
2311
|
-
"line.bodyHint":
|
|
2312
|
-
"Scales every interface line height by {ratio}. 100% keeps DSH's own line heights.",
|
|
2313
2446
|
"line.dialogLabel": "Conversation line height",
|
|
2314
2447
|
"line.dialogHint":
|
|
2315
2448
|
"Scales every conversation line height by {ratio}. 100% keeps DSH's own line heights.",
|
|
@@ -2329,7 +2462,6 @@ var DICTS = {
|
|
|
2329
2462
|
"Advanced font-feature-settings value, e.g. \"ss01\" on, \"cv01\" 1. Invalid values are ignored.",
|
|
2330
2463
|
"feat.placeholder": "\"ss01\" on",
|
|
2331
2464
|
|
|
2332
|
-
"synth.label": "Refuse synthetic styles",
|
|
2333
2465
|
"synth.italic": "No faux italic",
|
|
2334
2466
|
"synth.italicHint":
|
|
2335
2467
|
"CJK faces have no italic, so the browser tilts them. On keeps marked text upright.",
|
|
@@ -2343,15 +2475,6 @@ var DICTS = {
|
|
|
2343
2475
|
"section.ui": "Interface",
|
|
2344
2476
|
"section.dialog": "Conversation",
|
|
2345
2477
|
"section.code": "Code",
|
|
2346
|
-
"section.fine": "Global fine-tuning",
|
|
2347
|
-
"section.presets": "Presets",
|
|
2348
|
-
"section.uiHint":
|
|
2349
|
-
"Sidebars, headings, buttons and every other chrome text.",
|
|
2350
|
-
"section.dialogHint":
|
|
2351
|
-
"The conversation markdown: paragraphs, tables and headings.",
|
|
2352
|
-
"section.codeHint": "Code blocks, inline code and terminal output.",
|
|
2353
|
-
"section.fineHint":
|
|
2354
|
-
"Settings that apply to the whole page.",
|
|
2355
2478
|
"section.open": "Expand {name}",
|
|
2356
2479
|
"section.close": "Collapse {name}",
|
|
2357
2480
|
|
|
@@ -2360,13 +2483,6 @@ var DICTS = {
|
|
|
2360
2483
|
"ui.own": "Own values",
|
|
2361
2484
|
"dialog.default": "DSH defaults",
|
|
2362
2485
|
|
|
2363
|
-
"theme.label": "Per-theme values",
|
|
2364
|
-
"theme.on": "Light and dark keep separate values",
|
|
2365
|
-
"theme.edit": "Editing theme",
|
|
2366
|
-
"theme.light": "Light",
|
|
2367
|
-
"theme.dark": "Dark",
|
|
2368
|
-
"theme.mismatch":
|
|
2369
|
-
"The page is in {active} mode; the preview below shows the values being edited.",
|
|
2370
2486
|
|
|
2371
2487
|
"dialog.label": "Conversation font",
|
|
2372
2488
|
"dialog.hint":
|
|
@@ -2374,11 +2490,6 @@ var DICTS = {
|
|
|
2374
2490
|
"dialogWest.label": "Conversation · Western",
|
|
2375
2491
|
"dialogEast.label": "Conversation · CJK",
|
|
2376
2492
|
|
|
2377
|
-
"preset.save": "Save current",
|
|
2378
|
-
"preset.namePlaceholder": "Preset name",
|
|
2379
|
-
"preset.apply": "Apply",
|
|
2380
|
-
"preset.overwrite": "Overwrite",
|
|
2381
|
-
"preset.delete": "Delete",
|
|
2382
2493
|
"preset.export": "Export",
|
|
2383
2494
|
"preset.import": "Import",
|
|
2384
2495
|
"preset.importPlaceholder": "Paste an exported preset JSON here",
|
|
@@ -2389,18 +2500,13 @@ var DICTS = {
|
|
|
2389
2500
|
"preset.renamed": "Renamed to “{name}”.",
|
|
2390
2501
|
"preset.autoSave": "With a preset selected, every change saves into it automatically.",
|
|
2391
2502
|
"preset.nameUsed": "That name is already taken.",
|
|
2392
|
-
"preset.saved": "Saved “{name}”.",
|
|
2393
2503
|
"preset.applied": "Applied “{name}”.",
|
|
2394
|
-
"preset.deleted": "Deleted “{name}”.",
|
|
2395
|
-
"preset.updated": "Overwrote “{name}”.",
|
|
2396
2504
|
"preset.exported": "Presets copied to the clipboard.",
|
|
2397
2505
|
"preset.imported": "Imported {count} preset(s).",
|
|
2398
2506
|
"preset.importBad": "That text is not a valid preset export.",
|
|
2399
|
-
"preset.
|
|
2400
|
-
"preset.
|
|
2401
|
-
"preset.nameTaken": "“{name}” already exists — saving overwrites it.",
|
|
2507
|
+
"preset.exportManual": "Clipboard unavailable — press Ctrl+C to copy the selected text.",
|
|
2508
|
+
"preset.writeFailed": "The setting was not saved: {message}",
|
|
2402
2509
|
|
|
2403
|
-
"preview.label": "Preview",
|
|
2404
2510
|
"preview.sansCaption": "Interface",
|
|
2405
2511
|
"preview.dialogCaption": "Conversation",
|
|
2406
2512
|
"preview.monoCaption": "Code",
|
|
@@ -2408,8 +2514,6 @@ var DICTS = {
|
|
|
2408
2514
|
"The quick brown fox jumps over the lazy dog — 中文排版预览,标点符号,数字 0123456789。",
|
|
2409
2515
|
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
2410
2516
|
|
|
2411
|
-
"footnote.local":
|
|
2412
|
-
"Stored in the Host settings document. Every change applies immediately.",
|
|
2413
2517
|
},
|
|
2414
2518
|
zh: {
|
|
2415
2519
|
"card.title": "字体增强",
|
|
@@ -2462,13 +2566,9 @@ var DICTS = {
|
|
|
2462
2566
|
"monoWest.label": "代码 · 西文字体",
|
|
2463
2567
|
"monoEast.label": "代码 · 中文字体",
|
|
2464
2568
|
"split.pick": "选择…",
|
|
2465
|
-
"split.unset": "未选择",
|
|
2466
2569
|
"split.remove": "移除 {name}",
|
|
2467
2570
|
"split.rest": "其余回退项(在高级模式中排序):{names}",
|
|
2468
2571
|
|
|
2469
|
-
"size.bodyLabel": "界面字号偏移",
|
|
2470
|
-
"size.bodyHint":
|
|
2471
|
-
"给界面文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持原样。",
|
|
2472
2572
|
"size.dialogLabel": "对话字号偏移",
|
|
2473
2573
|
"size.dialogHint":
|
|
2474
2574
|
"给对话文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持 DSH 原样。",
|
|
@@ -2486,8 +2586,6 @@ var DICTS = {
|
|
|
2486
2586
|
"weight.codeHint":
|
|
2487
2587
|
"只覆盖代码块、行内代码与终端输出;400 或未设置表示保持 DSH 原样。",
|
|
2488
2588
|
|
|
2489
|
-
"line.bodyLabel": "界面行高",
|
|
2490
|
-
"line.bodyHint": "把界面行高整体缩放为 {ratio};100% 表示保持原样。",
|
|
2491
2589
|
"line.dialogLabel": "对话行高",
|
|
2492
2590
|
"line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
|
|
2493
2591
|
"line.codeLabel": "代码行高",
|
|
@@ -2505,7 +2603,6 @@ var DICTS = {
|
|
|
2505
2603
|
"高级的 font-feature-settings 值,例如 ss01 on、cv01 1;非法值会被忽略。",
|
|
2506
2604
|
"feat.placeholder": "\"ss01\" on",
|
|
2507
2605
|
|
|
2508
|
-
"synth.label": "拒绝合成样式",
|
|
2509
2606
|
"synth.italic": "禁用伪斜体",
|
|
2510
2607
|
"synth.italicHint":
|
|
2511
2608
|
"中文字体没有斜体,浏览器会把标记文本硬掰歪;开启后保持直立。",
|
|
@@ -2518,21 +2615,9 @@ var DICTS = {
|
|
|
2518
2615
|
"section.ui": "界面",
|
|
2519
2616
|
"section.dialog": "对话",
|
|
2520
2617
|
"section.code": "代码",
|
|
2521
|
-
"section.fine": "全局微调",
|
|
2522
|
-
"section.presets": "预设方案",
|
|
2523
|
-
"section.uiHint": "侧栏、标题、按钮等界面文字。",
|
|
2524
|
-
"section.dialogHint": "会话里的 Markdown:段落、表格与标题。",
|
|
2525
|
-
"section.codeHint": "代码块、行内代码与终端输出。",
|
|
2526
|
-
"section.fineHint": "作用于整页的选项。",
|
|
2527
2618
|
"section.open": "展开{name}",
|
|
2528
2619
|
"section.close": "收起{name}",
|
|
2529
2620
|
|
|
2530
|
-
"theme.label": "分主题数值",
|
|
2531
|
-
"theme.on": "深浅色各存一套数值",
|
|
2532
|
-
"theme.edit": "正在编辑",
|
|
2533
|
-
"theme.light": "浅色",
|
|
2534
|
-
"theme.dark": "深色",
|
|
2535
|
-
"theme.mismatch": "当前页面是{active}主题,下方预览显示的是正在编辑的那套数值。",
|
|
2536
2621
|
|
|
2537
2622
|
"ui.follow": "跟随对话设置",
|
|
2538
2623
|
"ui.followHint": "字体与字重",
|
|
@@ -2545,11 +2630,6 @@ var DICTS = {
|
|
|
2545
2630
|
"dialogWest.label": "对话 · 西文字体",
|
|
2546
2631
|
"dialogEast.label": "对话 · 中文字体",
|
|
2547
2632
|
|
|
2548
|
-
"preset.save": "保存当前",
|
|
2549
|
-
"preset.namePlaceholder": "方案名称",
|
|
2550
|
-
"preset.apply": "应用",
|
|
2551
|
-
"preset.overwrite": "覆盖",
|
|
2552
|
-
"preset.delete": "删除",
|
|
2553
2633
|
"preset.export": "导出",
|
|
2554
2634
|
"preset.import": "导入",
|
|
2555
2635
|
"preset.importPlaceholder": "粘贴导出的方案内容",
|
|
@@ -2560,18 +2640,13 @@ var DICTS = {
|
|
|
2560
2640
|
"preset.renamed": "已改名为“{name}”。",
|
|
2561
2641
|
"preset.autoSave": "选中方案后,所有改动自动存入该方案。",
|
|
2562
2642
|
"preset.nameUsed": "这个名字已被占用。",
|
|
2563
|
-
"preset.saved": "已保存“{name}”。",
|
|
2564
2643
|
"preset.applied": "已应用“{name}”。",
|
|
2565
|
-
"preset.deleted": "已删除“{name}”。",
|
|
2566
|
-
"preset.updated": "已覆盖“{name}”。",
|
|
2567
2644
|
"preset.exported": "方案已复制到剪贴板。",
|
|
2568
2645
|
"preset.imported": "已导入 {count} 个方案。",
|
|
2569
2646
|
"preset.importBad": "这段内容不是有效的方案导出。",
|
|
2570
|
-
"preset.
|
|
2571
|
-
"preset.
|
|
2572
|
-
"preset.nameTaken": "“{name}”已存在,保存将覆盖它。",
|
|
2647
|
+
"preset.exportManual": "剪贴板不可用,按 Ctrl+C 复制下方已选中的内容。",
|
|
2648
|
+
"preset.writeFailed": "设置没有保存成功:{message}",
|
|
2573
2649
|
|
|
2574
|
-
"preview.label": "预览",
|
|
2575
2650
|
"preview.sansCaption": "界面",
|
|
2576
2651
|
"preview.dialogCaption": "对话",
|
|
2577
2652
|
"preview.monoCaption": "代码",
|
|
@@ -2579,7 +2654,6 @@ var DICTS = {
|
|
|
2579
2654
|
"The quick brown fox jumps over the lazy dog —— 中文排版预览,标点符号,数字 0123456789。",
|
|
2580
2655
|
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
2581
2656
|
|
|
2582
|
-
"footnote.local": "保存在宿主设置文档里;每次改动立即生效。",
|
|
2583
2657
|
},
|
|
2584
2658
|
};
|
|
2585
2659
|
|
|
@@ -2682,7 +2756,6 @@ var CARD_CSS = [
|
|
|
2682
2756
|
".dfp-miniButton{appearance:none;font:inherit;cursor:pointer;white-space:nowrap;flex:none;border-radius:6px;padding:3px 8px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);background:0 0;border:.5px solid var(--dsw-alias-border-l3)}",
|
|
2683
2757
|
".dfp-miniButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-specific-sidebar-nav-item-hover)}",
|
|
2684
2758
|
".dfp-miniButton:disabled{opacity:.4;cursor:default}",
|
|
2685
|
-
".dfp-presetForm{align-items:center;gap:8px;margin-top:10px;display:flex}",
|
|
2686
2759
|
".dfp-textInput{flex:1;min-width:0;box-sizing:border-box;padding:5px 8px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;font:inherit;font-size:13px;line-height:18px}",
|
|
2687
2760
|
// The auto-save hint and the transient message share one row: the hint holds
|
|
2688
2761
|
// the left, the message fades in on the right. The row is always mounted and
|
|
@@ -2731,6 +2804,7 @@ var CARD_CSS = [
|
|
|
2731
2804
|
|
|
2732
2805
|
".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}",
|
|
2733
2806
|
".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}",
|
|
2807
|
+
".dfp-exportText{resize:vertical;min-height:54px;font-family:var(--ds-font-family-code,ui-monospace,monospace);font-size:12px;line-height:17px}",
|
|
2734
2808
|
".dfp-note{padding:4px 2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
|
|
2735
2809
|
".dfp-list{flex-direction:column;gap:1px;flex:1;min-height:0;overflow-y:auto;display:flex}",
|
|
2736
2810
|
".dfp-group{padding:6px 6px 2px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;position:sticky;top:0;background:var(--dsw-alias-bg-layer-2)}",
|
|
@@ -3316,24 +3390,45 @@ function PresetSelect(props) {
|
|
|
3316
3390
|
* ------------------------------------------------------------------ */
|
|
3317
3391
|
|
|
3318
3392
|
/**
|
|
3319
|
-
* @typedef {{status: "unsupported"|"loading"|"denied"|"ready", families: string[]
|
|
3393
|
+
* @typedef {{status: "unsupported"|"loading"|"denied"|"ready", families: string[],
|
|
3394
|
+
* at?: number, permanent?: boolean}} Catalog
|
|
3320
3395
|
*/
|
|
3321
3396
|
|
|
3322
3397
|
/** Enumeration is session-stable, so it is resolved once and kept. */
|
|
3323
3398
|
var catalogCache = null;
|
|
3324
3399
|
|
|
3400
|
+
/**
|
|
3401
|
+
* How long a refused (or empty) enumeration is trusted before the picker asks
|
|
3402
|
+
* again. Answering "denied" and never retrying is wrong in the case that
|
|
3403
|
+
* actually happens: the user grants the permission from the browser's prompt
|
|
3404
|
+
* or site settings and expects the list to fill in — and when the permission
|
|
3405
|
+
* is still blocked the browser rejects the call instantly without prompting,
|
|
3406
|
+
* so the retry costs nothing.
|
|
3407
|
+
*/
|
|
3408
|
+
var CATALOG_RETRY_MS = 20000;
|
|
3409
|
+
|
|
3410
|
+
/** The clock, injectable so the retry window is testable without waiting. */
|
|
3411
|
+
var catalogNow = function () {
|
|
3412
|
+
return Date.now();
|
|
3413
|
+
};
|
|
3414
|
+
|
|
3325
3415
|
/**
|
|
3326
3416
|
* Enumerate installed families; the built-in presets stand in when the browser
|
|
3327
3417
|
* cannot or will not enumerate them.
|
|
3418
|
+
* @param {boolean} [force] - ignore a cached refusal and ask again.
|
|
3328
3419
|
* @returns {Promise<Catalog>} the catalog.
|
|
3329
3420
|
*/
|
|
3330
|
-
async function loadCatalog() {
|
|
3331
|
-
|
|
3421
|
+
async function loadCatalog(force) {
|
|
3422
|
+
var now = catalogNow();
|
|
3423
|
+
if (force !== true && !shared.catalogRefreshDue(catalogCache, now, CATALOG_RETRY_MS)) {
|
|
3424
|
+
return catalogCache;
|
|
3425
|
+
}
|
|
3332
3426
|
var host = /** @type {{queryLocalFonts?: () => Promise<Array<{family: string}>>}} */ (
|
|
3333
3427
|
globalThis
|
|
3334
3428
|
);
|
|
3335
3429
|
if (typeof host.queryLocalFonts !== "function") {
|
|
3336
|
-
|
|
3430
|
+
// No API at all: retrying cannot help, and the picker says so.
|
|
3431
|
+
catalogCache = { status: "unsupported", families: [], at: now, permanent: true };
|
|
3337
3432
|
return catalogCache;
|
|
3338
3433
|
}
|
|
3339
3434
|
try {
|
|
@@ -3353,11 +3448,11 @@ async function loadCatalog() {
|
|
|
3353
3448
|
});
|
|
3354
3449
|
catalogCache =
|
|
3355
3450
|
families.length === 0
|
|
3356
|
-
? { status: "unsupported", families: [] }
|
|
3357
|
-
: { status: "ready", families: families };
|
|
3451
|
+
? { status: "unsupported", families: [], at: now }
|
|
3452
|
+
: { status: "ready", families: families, at: now };
|
|
3358
3453
|
} catch (error) {
|
|
3359
3454
|
// A refused permission is the common case here: `queryLocalFonts` prompts.
|
|
3360
|
-
catalogCache = { status: "denied", families: [] };
|
|
3455
|
+
catalogCache = { status: "denied", families: [], at: now };
|
|
3361
3456
|
}
|
|
3362
3457
|
return catalogCache;
|
|
3363
3458
|
}
|
|
@@ -3420,13 +3515,65 @@ function classifyFamily(name) {
|
|
|
3420
3515
|
return verdict;
|
|
3421
3516
|
}
|
|
3422
3517
|
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3518
|
+
/* ------------------------------------------------------------------ *
|
|
3519
|
+
* clipboard
|
|
3520
|
+
* ------------------------------------------------------------------ */
|
|
3521
|
+
|
|
3522
|
+
/**
|
|
3523
|
+
* Copy through the legacy selection path.
|
|
3524
|
+
*
|
|
3525
|
+
* This is not a museum piece: the async Clipboard API only exists in a secure
|
|
3526
|
+
* context, and the deployment note in this repository's README documents a
|
|
3527
|
+
* remote HTTP origin where `navigator.clipboard` is simply absent. A
|
|
3528
|
+
* selection-based copy still works there, and it also works when the async
|
|
3529
|
+
* call is refused without the clipboard-write permission.
|
|
3530
|
+
* @param {string} text - what to put on the clipboard.
|
|
3531
|
+
* @returns {boolean} whether the copy command reported success.
|
|
3532
|
+
*/
|
|
3533
|
+
function copyBySelection(text) {
|
|
3534
|
+
if (typeof document === "undefined" || !document.body) return false;
|
|
3535
|
+
if (typeof document.execCommand !== "function") return false;
|
|
3536
|
+
var area = null;
|
|
3537
|
+
try {
|
|
3538
|
+
area = document.createElement("textarea");
|
|
3539
|
+
area.value = text;
|
|
3540
|
+
area.setAttribute("readonly", "");
|
|
3541
|
+
// Off-screen rather than hidden: `display:none` cannot hold a selection.
|
|
3542
|
+
area.style.position = "fixed";
|
|
3543
|
+
area.style.top = "-1000px";
|
|
3544
|
+
area.style.opacity = "0";
|
|
3545
|
+
document.body.appendChild(area);
|
|
3546
|
+
area.select();
|
|
3547
|
+
return document.execCommand("copy") === true;
|
|
3548
|
+
} catch (error) {
|
|
3549
|
+
return false;
|
|
3550
|
+
} finally {
|
|
3551
|
+
if (area !== null && area.parentNode) area.parentNode.removeChild(area);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
/**
|
|
3556
|
+
* Copy text, trying the async API first and the selection fallback after it.
|
|
3557
|
+
* @param {string} text - what to put on the clipboard.
|
|
3558
|
+
* @returns {Promise<boolean>} whether the text reached the clipboard.
|
|
3559
|
+
*/
|
|
3560
|
+
async function copyText(text) {
|
|
3561
|
+
var nav = globalThis.navigator;
|
|
3562
|
+
if (
|
|
3563
|
+
nav !== undefined &&
|
|
3564
|
+
nav !== null &&
|
|
3565
|
+
nav.clipboard !== undefined &&
|
|
3566
|
+
typeof nav.clipboard.writeText === "function"
|
|
3567
|
+
) {
|
|
3568
|
+
try {
|
|
3569
|
+
await nav.clipboard.writeText(text);
|
|
3570
|
+
return true;
|
|
3571
|
+
} catch (error) {
|
|
3572
|
+
// No permission, or no clipboard in this context: fall through.
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
return copyBySelection(text);
|
|
3576
|
+
}
|
|
3430
3577
|
|
|
3431
3578
|
/* ------------------------------------------------------------------ *
|
|
3432
3579
|
* the family picker
|
|
@@ -3571,7 +3718,6 @@ function StackPicker(props) {
|
|
|
3571
3718
|
commit(families.concat([name]));
|
|
3572
3719
|
};
|
|
3573
3720
|
|
|
3574
|
-
var needle = query.trim().toLowerCase();
|
|
3575
3721
|
var selectedLower = {};
|
|
3576
3722
|
if (single) {
|
|
3577
3723
|
if (typeof props.value === "string" && props.value !== "") {
|
|
@@ -3582,47 +3728,15 @@ function StackPicker(props) {
|
|
|
3582
3728
|
selectedLower[families[selectedIndex].toLowerCase()] = true;
|
|
3583
3729
|
}
|
|
3584
3730
|
}
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
var options =
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
if (skipSelected === true && selectedLower[name.toLowerCase()] === true) continue;
|
|
3595
|
-
rows.push(name);
|
|
3596
|
-
if (limit !== undefined && rows.length >= limit) break;
|
|
3597
|
-
}
|
|
3598
|
-
if (rows.length > 0) options.push({ label: labelKey, families: rows });
|
|
3599
|
-
};
|
|
3600
|
-
|
|
3601
|
-
var selectedRows = [];
|
|
3602
|
-
for (var familyIndex = 0; familyIndex < families.length; familyIndex += 1) {
|
|
3603
|
-
if (matches(families[familyIndex])) selectedRows.push(families[familyIndex]);
|
|
3604
|
-
}
|
|
3605
|
-
if (selectedRows.length > 0) {
|
|
3606
|
-
options.push({ label: "stack.groupSelected", families: selectedRows });
|
|
3607
|
-
}
|
|
3608
|
-
|
|
3609
|
-
if (catalog.status === "ready") {
|
|
3610
|
-
if (needle === "") {
|
|
3611
|
-
for (var groupIndex = 0; groupIndex < PRESET_GROUPS.length; groupIndex += 1) {
|
|
3612
|
-
pushGroup(PRESET_GROUPS[groupIndex].label, PRESET_GROUPS[groupIndex].families, undefined, !single);
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
pushGroup("stack.groupLocal", catalog.families, MAX_VISIBLE_FONTS, !single);
|
|
3616
|
-
} else {
|
|
3617
|
-
for (var fallbackIndex = 0; fallbackIndex < PRESET_GROUPS.length; fallbackIndex += 1) {
|
|
3618
|
-
pushGroup(
|
|
3619
|
-
PRESET_GROUPS[fallbackIndex].label,
|
|
3620
|
-
PRESET_GROUPS[fallbackIndex].families,
|
|
3621
|
-
undefined,
|
|
3622
|
-
!single
|
|
3623
|
-
);
|
|
3624
|
-
}
|
|
3625
|
-
}
|
|
3731
|
+
// The grouping itself is pure and lives in the shared core, where it is
|
|
3732
|
+
// covered without a browser: which rows appear, in which order, and never
|
|
3733
|
+
// twice.
|
|
3734
|
+
var options = shared.pickerGroups({
|
|
3735
|
+
stack: families,
|
|
3736
|
+
single: single,
|
|
3737
|
+
query: query,
|
|
3738
|
+
catalog: catalog,
|
|
3739
|
+
});
|
|
3626
3740
|
|
|
3627
3741
|
var typed = query.trim();
|
|
3628
3742
|
var typedKnown = false;
|
|
@@ -4100,6 +4214,8 @@ function FontCard(props) {
|
|
|
4100
4214
|
// effect runs again instead of ignoring the second one.
|
|
4101
4215
|
var [presetStatus, setPresetStatus] = useState({ text: "", id: 0 });
|
|
4102
4216
|
var [statusShown, setStatusShown] = useState(false);
|
|
4217
|
+
/** The exported JSON when the clipboard refused it: shown selected instead. */
|
|
4218
|
+
var [exportText, setExportText] = useState("");
|
|
4103
4219
|
var [renaming, setRenaming] = useState(false);
|
|
4104
4220
|
var [importOpen, setImportOpen] = useState(false);
|
|
4105
4221
|
/** True while the card itself writes a whole preset's values: the edits
|
|
@@ -4186,33 +4302,60 @@ function FontCard(props) {
|
|
|
4186
4302
|
clear(field);
|
|
4187
4303
|
mirrorPreset(field, DEFAULTS[field]);
|
|
4188
4304
|
};
|
|
4305
|
+
/**
|
|
4306
|
+
* Report a write the settings document refused. A silent failure is the
|
|
4307
|
+
* worst outcome here: the control goes back to showing the stored value and
|
|
4308
|
+
* the user is left believing the change was saved.
|
|
4309
|
+
* @param {unknown} error - the rejection reason.
|
|
4310
|
+
*/
|
|
4311
|
+
var writeFailed = function (error) {
|
|
4312
|
+
var message = error && error.message ? String(error.message) : String(error);
|
|
4313
|
+
if (message.length > 160) message = message.slice(0, 160) + "…";
|
|
4314
|
+
showStatus(t("preset.writeFailed", { message: message }));
|
|
4315
|
+
};
|
|
4189
4316
|
var submit = function (field, value) {
|
|
4190
|
-
var result
|
|
4317
|
+
var result;
|
|
4318
|
+
try {
|
|
4319
|
+
result = scope.set(field, value);
|
|
4320
|
+
} catch (error) {
|
|
4321
|
+
writeFailed(error);
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4191
4324
|
if (result && typeof result.catch === "function") {
|
|
4192
|
-
result.catch(function () {
|
|
4193
|
-
//
|
|
4325
|
+
result.catch(function (error) {
|
|
4326
|
+
// The scope reloads the Host state itself; the status row is what
|
|
4327
|
+
// keeps the user from thinking the change stuck.
|
|
4328
|
+
writeFailed(error);
|
|
4194
4329
|
});
|
|
4195
4330
|
}
|
|
4196
4331
|
};
|
|
4197
4332
|
var clear = function (field) {
|
|
4198
|
-
var result
|
|
4333
|
+
var result;
|
|
4334
|
+
try {
|
|
4335
|
+
result = scope.unset(field);
|
|
4336
|
+
} catch (error) {
|
|
4337
|
+
writeFailed(error);
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
4199
4340
|
if (result && typeof result.catch === "function") {
|
|
4200
|
-
result.catch(function () {
|
|
4341
|
+
result.catch(function (error) {
|
|
4342
|
+
writeFailed(error);
|
|
4343
|
+
});
|
|
4201
4344
|
}
|
|
4202
4345
|
};
|
|
4203
4346
|
var overridden = function (field) {
|
|
4204
4347
|
return Object.prototype.hasOwnProperty.call(user, field);
|
|
4205
4348
|
};
|
|
4206
4349
|
|
|
4207
|
-
// ----
|
|
4350
|
+
// ---- family stack handlers ----
|
|
4208
4351
|
var pickWest = function (field, family) {
|
|
4209
4352
|
setField(field, formatStack(setWestEntry(parseStack(editing[field]), family, classifyFamily)));
|
|
4210
4353
|
};
|
|
4211
4354
|
var pickEast = function (field, family) {
|
|
4212
|
-
setField(field, formatStack(setEastEntry(parseStack(
|
|
4355
|
+
setField(field, formatStack(setEastEntry(parseStack(editing[field]), family, classifyFamily)));
|
|
4213
4356
|
};
|
|
4214
4357
|
var dropEntry = function (field, family) {
|
|
4215
|
-
setField(field, formatStack(removeStackEntry(parseStack(
|
|
4358
|
+
setField(field, formatStack(removeStackEntry(parseStack(editing[field]), family)));
|
|
4216
4359
|
};
|
|
4217
4360
|
|
|
4218
4361
|
// ---- size slider ----
|
|
@@ -4537,21 +4680,23 @@ function FontCard(props) {
|
|
|
4537
4680
|
showStatus(t("preset.renamed", { name: name }));
|
|
4538
4681
|
};
|
|
4539
4682
|
|
|
4683
|
+
/**
|
|
4684
|
+
* Hand the preset list over as JSON: the clipboard when it is available,
|
|
4685
|
+
* and otherwise a selected read-only box with the same text in it. The old
|
|
4686
|
+
* fallback printed the export into the one-line status row, which truncated
|
|
4687
|
+
* the JSON at 120 characters — a message that could not be copied from.
|
|
4688
|
+
*/
|
|
4540
4689
|
var exportPresets = function () {
|
|
4541
4690
|
var text = JSON.stringify(presetList);
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
.
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
});
|
|
4552
|
-
} else {
|
|
4553
|
-
showStatus(text.slice(0, 120) + "…");
|
|
4554
|
-
}
|
|
4691
|
+
void copyText(text).then(function (copied) {
|
|
4692
|
+
if (copied) {
|
|
4693
|
+
setExportText("");
|
|
4694
|
+
showStatus(t("preset.exported"));
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4697
|
+
setExportText(text);
|
|
4698
|
+
showStatus(t("preset.exportManual"));
|
|
4699
|
+
});
|
|
4555
4700
|
};
|
|
4556
4701
|
var importPresets = function (text) {
|
|
4557
4702
|
var parsed = normalizePresets(text);
|
|
@@ -4847,6 +4992,25 @@ function FontCard(props) {
|
|
|
4847
4992
|
if (text !== "") importPresets(text);
|
|
4848
4993
|
},
|
|
4849
4994
|
})
|
|
4995
|
+
: null,
|
|
4996
|
+
// The clipboard-less export: the same JSON, already selected, so
|
|
4997
|
+
// the one thing left to do is press the copy shortcut.
|
|
4998
|
+
exportText !== ""
|
|
4999
|
+
? h("textarea", {
|
|
5000
|
+
className: "dfp-search dfp-exportText",
|
|
5001
|
+
rows: 3,
|
|
5002
|
+
readOnly: true,
|
|
5003
|
+
autoFocus: true,
|
|
5004
|
+
spellCheck: false,
|
|
5005
|
+
"aria-label": t("preset.exportManual"),
|
|
5006
|
+
value: exportText,
|
|
5007
|
+
onFocus: function (event) {
|
|
5008
|
+
event.target.select();
|
|
5009
|
+
},
|
|
5010
|
+
onCopy: function () {
|
|
5011
|
+
setExportText("");
|
|
5012
|
+
},
|
|
5013
|
+
})
|
|
4850
5014
|
: null
|
|
4851
5015
|
),
|
|
4852
5016
|
|
|
@@ -5070,6 +5234,11 @@ function FontCard(props) {
|
|
|
5070
5234
|
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
5071
5235
|
clear(VALUE_FIELDS[index]);
|
|
5072
5236
|
}
|
|
5237
|
+
// The retired axes are cleared too: they render nothing, so a
|
|
5238
|
+
// value left behind would be invisible forever.
|
|
5239
|
+
for (var retired = 0; retired < RETIRED_FIELDS.length; retired += 1) {
|
|
5240
|
+
clear(RETIRED_FIELDS[retired]);
|
|
5241
|
+
}
|
|
5073
5242
|
clear(DARK_VALUES_FIELD);
|
|
5074
5243
|
// Auto-save: the reset IS a change to the active preset, so
|
|
5075
5244
|
// its snapshot follows back to the neutral defaults.
|
|
@@ -5186,7 +5355,38 @@ function apply(ctx) {
|
|
|
5186
5355
|
function () {
|
|
5187
5356
|
if (typeof document === "undefined") return undefined;
|
|
5188
5357
|
var lastSignature = "";
|
|
5189
|
-
var
|
|
5358
|
+
var lastGate = null;
|
|
5359
|
+
var ticks = 0;
|
|
5360
|
+
// Reading the base tokens walks every property of every rule of every
|
|
5361
|
+
// stylesheet — far too much to repeat every four seconds forever. This
|
|
5362
|
+
// gate covers everything that moves those tokens in DSH: the theme
|
|
5363
|
+
// writes its content font size inline on `body`, a light/dark switch
|
|
5364
|
+
// swaps the class on `html`, and a lazily loaded view adds a stylesheet.
|
|
5365
|
+
// A full sweep still runs every Nth tick, which is what catches an
|
|
5366
|
+
// in-place rewrite inside the CSS-in-JS sheet.
|
|
5367
|
+
var GATE_TICKS_PER_SWEEP = 8;
|
|
5368
|
+
var gate = function () {
|
|
5369
|
+
var inline = "";
|
|
5370
|
+
var root = null;
|
|
5371
|
+
try {
|
|
5372
|
+
inline = document.body ? document.body.getAttribute("style") || "" : "";
|
|
5373
|
+
root = document.documentElement;
|
|
5374
|
+
} catch (error) {
|
|
5375
|
+
inline = "";
|
|
5376
|
+
}
|
|
5377
|
+
return (
|
|
5378
|
+
inline +
|
|
5379
|
+
"|" +
|
|
5380
|
+
(root ? root.className || "" : "") +
|
|
5381
|
+
"|" +
|
|
5382
|
+
document.styleSheets.length
|
|
5383
|
+
);
|
|
5384
|
+
};
|
|
5385
|
+
var recheck = function (force) {
|
|
5386
|
+
ticks += 1;
|
|
5387
|
+
var current = gate();
|
|
5388
|
+
if (force !== true && ticks % GATE_TICKS_PER_SWEEP !== 0 && current === lastGate) return;
|
|
5389
|
+
lastGate = current;
|
|
5190
5390
|
var next = readBaseTokens();
|
|
5191
5391
|
var signature = "";
|
|
5192
5392
|
var name;
|
|
@@ -5203,7 +5403,9 @@ function apply(ctx) {
|
|
|
5203
5403
|
sync();
|
|
5204
5404
|
};
|
|
5205
5405
|
scheduleRecheck = recheck;
|
|
5206
|
-
var timer = globalThis.setTimeout(
|
|
5406
|
+
var timer = globalThis.setTimeout(function () {
|
|
5407
|
+
recheck(true);
|
|
5408
|
+
}, 500);
|
|
5207
5409
|
var interval = globalThis.setInterval(recheck, 4000);
|
|
5208
5410
|
return function () {
|
|
5209
5411
|
globalThis.clearTimeout(timer);
|
|
@@ -5218,7 +5420,8 @@ function apply(ctx) {
|
|
|
5218
5420
|
// this hook; the polling recheck still covers theme changes there.
|
|
5219
5421
|
if (typeof ctx.on !== "function") return undefined;
|
|
5220
5422
|
return ctx.on("theme/change", function () {
|
|
5221
|
-
|
|
5423
|
+
// Forced: the theme may have moved without touching the cheap gate.
|
|
5424
|
+
if (scheduleRecheck !== null) scheduleRecheck(true);
|
|
5222
5425
|
});
|
|
5223
5426
|
},
|
|
5224
5427
|
"dsh-fonttune: theme change adoption"
|