dsh-fonttune 0.2.2 → 0.2.4
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 +44 -0
- package/CHANGELOG.md +44 -0
- package/README.en.md +20 -5
- package/README.md +20 -5
- package/lib/client.js +913 -312
- package/lib/index.js +95 -11
- package/lib/shared.cjs +285 -145
- package/package.json +11 -6
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]);
|
|
@@ -1032,17 +1046,6 @@ function isShorthandName(name) {
|
|
|
1032
1046
|
return true;
|
|
1033
1047
|
}
|
|
1034
1048
|
|
|
1035
|
-
/**
|
|
1036
|
-
* Whether one token (name + value) is a `font` shorthand this plugin may
|
|
1037
|
-
* rewrite: a bare `--dsw-font-…` name whose value parses as a shorthand.
|
|
1038
|
-
* @param {string} name - custom property name.
|
|
1039
|
-
* @param {string} value - the token's untouched value.
|
|
1040
|
-
* @returns {boolean}
|
|
1041
|
-
*/
|
|
1042
|
-
function isShorthandToken(name, value) {
|
|
1043
|
-
return isShorthandName(name) && parseShorthand(value) !== null;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
1049
|
/**
|
|
1047
1050
|
* Whether a token is one this plugin reads and re-declares at all.
|
|
1048
1051
|
* @param {string} name - custom property name including the leading dashes.
|
|
@@ -1071,35 +1074,6 @@ function tokenNames(baseTokens) {
|
|
|
1071
1074
|
return names;
|
|
1072
1075
|
}
|
|
1073
1076
|
|
|
1074
|
-
/**
|
|
1075
|
-
* The interface PART size tokens: everything scalable except the code and
|
|
1076
|
-
* markdown families and the delta tokens (which the dialog offset shifts
|
|
1077
|
-
* additively, never multiplicatively).
|
|
1078
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1079
|
-
* @returns {string[]} the names the interface offset re-declares.
|
|
1080
|
-
*/
|
|
1081
|
-
function bodyTokenNames(baseTokens) {
|
|
1082
|
-
return tokenNames(baseTokens).filter(function (name) {
|
|
1083
|
-
return (
|
|
1084
|
-
!isCodeToken(name) &&
|
|
1085
|
-
!isMarkdownToken(name) &&
|
|
1086
|
-
!isDeltaToken(name) &&
|
|
1087
|
-
!isContentToken(name)
|
|
1088
|
-
);
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
/**
|
|
1093
|
-
* The interface line-height PART tokens (non-code, non-markdown).
|
|
1094
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1095
|
-
* @returns {string[]}
|
|
1096
|
-
*/
|
|
1097
|
-
function uiLineTokenNames(baseTokens) {
|
|
1098
|
-
return bodyTokenNames(baseTokens).filter(function (name) {
|
|
1099
|
-
return name.indexOf("-line-height") >= 0;
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
1077
|
/**
|
|
1104
1078
|
* The markdown (dialog) PART tokens: sizes and line heights of the markdown
|
|
1105
1079
|
* family, excluding code.
|
|
@@ -1110,30 +1084,6 @@ function markdownTokenNames(baseTokens) {
|
|
|
1110
1084
|
return tokenNames(baseTokens).filter(isMarkdownToken);
|
|
1111
1085
|
}
|
|
1112
1086
|
|
|
1113
|
-
/**
|
|
1114
|
-
* The markdown content-size DELTA tokens present in a base map.
|
|
1115
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1116
|
-
* @returns {string[]}
|
|
1117
|
-
*/
|
|
1118
|
-
function deltaTokenNames(baseTokens) {
|
|
1119
|
-
if (baseTokens === null || typeof baseTokens !== "object") return [];
|
|
1120
|
-
return Object.keys(baseTokens).filter(isDeltaToken);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
/**
|
|
1124
|
-
* The code PART tokens: the `font` shorthands' size/height parts, when the
|
|
1125
|
-
* live stylesheets declare them. The shorthands themselves are handled by
|
|
1126
|
-
* `codeShorthandTokens`, so they are excluded here to avoid double
|
|
1127
|
-
* declarations.
|
|
1128
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1129
|
-
* @returns {string[]}
|
|
1130
|
-
*/
|
|
1131
|
-
function codeTokenNames(baseTokens) {
|
|
1132
|
-
return tokenNames(baseTokens).filter(function (name) {
|
|
1133
|
-
return isCodeToken(name) && !isShorthandName(name);
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
1087
|
/**
|
|
1138
1088
|
* Every `font` shorthand token in a base map that parses, sorted by name
|
|
1139
1089
|
* length (families before variants, mirroring the part-token order).
|
|
@@ -1166,17 +1116,6 @@ function markdownShorthandTokens(baseTokens) {
|
|
|
1166
1116
|
});
|
|
1167
1117
|
}
|
|
1168
1118
|
|
|
1169
|
-
/**
|
|
1170
|
-
* The interface (non-markdown, non-code) `font` shorthand tokens.
|
|
1171
|
-
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
1172
|
-
* @returns {{name: string, parts: object}[]}
|
|
1173
|
-
*/
|
|
1174
|
-
function uiShorthandTokens(baseTokens) {
|
|
1175
|
-
return shorthandTokens(baseTokens).filter(function (entry) {
|
|
1176
|
-
return !isMarkdownToken(entry.name) && !isCodeToken(entry.name);
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
1119
|
/**
|
|
1181
1120
|
* The code `font` shorthand tokens.
|
|
1182
1121
|
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
@@ -1192,6 +1131,66 @@ function codeShorthandTokens(baseTokens) {
|
|
|
1192
1131
|
* selectors
|
|
1193
1132
|
* ------------------------------------------------------------------ */
|
|
1194
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
|
+
|
|
1195
1194
|
/**
|
|
1196
1195
|
* What counts as code text for the code axes.
|
|
1197
1196
|
*
|
|
@@ -1230,38 +1229,6 @@ var CODE_SELECTOR = [
|
|
|
1230
1229
|
})
|
|
1231
1230
|
.join(",");
|
|
1232
1231
|
|
|
1233
|
-
/**
|
|
1234
|
-
* Everything a code surface must be excluded from a DIALOG-scoped rule:
|
|
1235
|
-
* the code elements themselves AND their descendants (a highlight `<span>`
|
|
1236
|
-
* inside a `<pre>` is not a `pre`, so the exclusion has to reach through
|
|
1237
|
-
* ancestry with complex `:not()` arguments — supported by the Chromium line
|
|
1238
|
-
* DSH targets).
|
|
1239
|
-
*/
|
|
1240
|
-
var CODE_EXCLUDES = [
|
|
1241
|
-
"pre",
|
|
1242
|
-
"pre *",
|
|
1243
|
-
"code",
|
|
1244
|
-
"code *",
|
|
1245
|
-
"kbd",
|
|
1246
|
-
"kbd *",
|
|
1247
|
-
"samp",
|
|
1248
|
-
"samp *",
|
|
1249
|
-
"var",
|
|
1250
|
-
"var *",
|
|
1251
|
-
"tt",
|
|
1252
|
-
"tt *",
|
|
1253
|
-
"textarea",
|
|
1254
|
-
"textarea *",
|
|
1255
|
-
'[class*="code" i]',
|
|
1256
|
-
'[class*="code" i] *',
|
|
1257
|
-
'[class*="terminal" i]',
|
|
1258
|
-
'[class*="terminal" i] *',
|
|
1259
|
-
]
|
|
1260
|
-
.map(function (selector) {
|
|
1261
|
-
return ":not(" + selector + ")";
|
|
1262
|
-
})
|
|
1263
|
-
.join("");
|
|
1264
|
-
|
|
1265
1232
|
/**
|
|
1266
1233
|
* The dialog surface: DSH renders the conversation markdown in a CSS-module
|
|
1267
1234
|
* container whose class hashes per release (`_markdown_<hash>_…`). A
|
|
@@ -1293,11 +1260,57 @@ var DARK_ATTR = "body[data-ds-dark-theme]";
|
|
|
1293
1260
|
* interface's own preview stays inside the rule, and `dfp-previewCode` is a
|
|
1294
1261
|
* code surface like any other.
|
|
1295
1262
|
*/
|
|
1296
|
-
var INTERFACE_EXCLUDES =
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
+
}
|
|
1301
1314
|
|
|
1302
1315
|
/**
|
|
1303
1316
|
* Prefix every selector in a comma list, for the per-theme rules.
|
|
@@ -1306,7 +1319,7 @@ var INTERFACE_EXCLUDES =
|
|
|
1306
1319
|
* @returns {string} the prefixed list.
|
|
1307
1320
|
*/
|
|
1308
1321
|
function prefixSelector(selectorList, prefix) {
|
|
1309
|
-
var parts = selectorList
|
|
1322
|
+
var parts = splitSelectorList(selectorList);
|
|
1310
1323
|
var out = [];
|
|
1311
1324
|
for (var index = 0; index < parts.length; index += 1) {
|
|
1312
1325
|
out.push(prefix + " " + parts[index]);
|
|
@@ -1468,9 +1481,13 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1468
1481
|
*/
|
|
1469
1482
|
function pushScopedCodeFamily() {
|
|
1470
1483
|
var scopedCode = [];
|
|
1471
|
-
var surfaces = CODE_SELECTOR
|
|
1484
|
+
var surfaces = splitSelectorList(CODE_SELECTOR);
|
|
1472
1485
|
for (var cIndex = 0; cIndex < surfaces.length; cIndex += 1) {
|
|
1473
|
-
|
|
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);
|
|
1474
1491
|
}
|
|
1475
1492
|
declarations.push(scopedCode.join(",") + "{font-family:" + mono + " !important}");
|
|
1476
1493
|
}
|
|
@@ -1567,8 +1584,8 @@ function buildAxisCss(axis, baseTokens, isDark) {
|
|
|
1567
1584
|
// (secondary size, both deltas, delta-based heading sizes, heights) at their
|
|
1568
1585
|
// body-computed values. The offset is therefore added once, at the source —
|
|
1569
1586
|
// `--dsh-content-font-size` becomes "official base + offset" — and the whole
|
|
1570
|
-
// ladder follows by derivation, with nothing counted twice. The
|
|
1571
|
-
// 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
|
|
1572
1589
|
// moves only when this axis says so, and a dialog that FOLLOWS the interface
|
|
1573
1590
|
// follows it through this value.
|
|
1574
1591
|
var dialogOffsetValue = axis[SIZE_DIALOG_FIELD];
|
|
@@ -1980,18 +1997,137 @@ var PRESETS = {
|
|
|
1980
1997
|
};
|
|
1981
1998
|
|
|
1982
1999
|
/**
|
|
1983
|
-
*
|
|
1984
|
-
* height inside one simple `size/height <family>` shorthand.
|
|
1985
|
-
* @param {string} value - a value shaped like `11px/19px var(--ds-font-family-code)`.
|
|
1986
|
-
* @param {number} scale - the ratio to apply.
|
|
1987
|
-
* @returns {string|null} the scaled shorthand, or null when the shape differs.
|
|
2000
|
+
* The curated groups, in the order the picker lists them.
|
|
1988
2001
|
*/
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
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;
|
|
1995
2131
|
}
|
|
1996
2132
|
|
|
1997
2133
|
var shared = {
|
|
@@ -2018,6 +2154,8 @@ var shared = {
|
|
|
2018
2154
|
ACTIVE_PRESET_FIELD: ACTIVE_PRESET_FIELD,
|
|
2019
2155
|
UI_FOLLOWS_FIELD: UI_FOLLOWS_FIELD,
|
|
2020
2156
|
VALUE_FIELDS: VALUE_FIELDS,
|
|
2157
|
+
RETIRED_FIELDS: RETIRED_FIELDS,
|
|
2158
|
+
DURABLE_FIELDS: DURABLE_FIELDS,
|
|
2021
2159
|
SIZE_MIN: SIZE_MIN,
|
|
2022
2160
|
SIZE_MAX: SIZE_MAX,
|
|
2023
2161
|
WEIGHT_MIN: WEIGHT_MIN,
|
|
@@ -2030,7 +2168,6 @@ var shared = {
|
|
|
2030
2168
|
LIGATURES_DEFAULT: LIGATURES_DEFAULT,
|
|
2031
2169
|
LIGATURES_ON: LIGATURES_ON,
|
|
2032
2170
|
LIGATURES_OFF: LIGATURES_OFF,
|
|
2033
|
-
MARKER: MARKER,
|
|
2034
2171
|
STYLE_TAG: STYLE_TAG,
|
|
2035
2172
|
CARD_STYLE_TAG: CARD_STYLE_TAG,
|
|
2036
2173
|
DEFAULTS: DEFAULTS,
|
|
@@ -2041,7 +2178,17 @@ var shared = {
|
|
|
2041
2178
|
MAX_PRESET_NAME: MAX_PRESET_NAME,
|
|
2042
2179
|
FALLBACK_TOKENS: FALLBACK_TOKENS,
|
|
2043
2180
|
PRESETS: PRESETS,
|
|
2181
|
+
PRESET_GROUPS: PRESET_GROUPS,
|
|
2182
|
+
MAX_LOCAL_FONTS: MAX_LOCAL_FONTS,
|
|
2183
|
+
pickerGroups: pickerGroups,
|
|
2184
|
+
catalogRefreshDue: catalogRefreshDue,
|
|
2044
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,
|
|
2045
2192
|
DARK_ATTR: DARK_ATTR,
|
|
2046
2193
|
sanitize: sanitize,
|
|
2047
2194
|
sanitizeFamily: sanitizeFamily,
|
|
@@ -2072,19 +2219,12 @@ var shared = {
|
|
|
2072
2219
|
isMarkdownToken: isMarkdownToken,
|
|
2073
2220
|
isDeltaToken: isDeltaToken,
|
|
2074
2221
|
isShorthandName: isShorthandName,
|
|
2075
|
-
isShorthandToken: isShorthandToken,
|
|
2076
2222
|
isScaledToken: isScaledToken,
|
|
2077
2223
|
tokenNames: tokenNames,
|
|
2078
|
-
codeTokenNames: codeTokenNames,
|
|
2079
2224
|
markdownTokenNames: markdownTokenNames,
|
|
2080
|
-
bodyTokenNames: bodyTokenNames,
|
|
2081
|
-
uiLineTokenNames: uiLineTokenNames,
|
|
2082
|
-
deltaTokenNames: deltaTokenNames,
|
|
2083
2225
|
shorthandTokens: shorthandTokens,
|
|
2084
2226
|
markdownShorthandTokens: markdownShorthandTokens,
|
|
2085
|
-
uiShorthandTokens: uiShorthandTokens,
|
|
2086
2227
|
codeShorthandTokens: codeShorthandTokens,
|
|
2087
|
-
scaleFontShorthand: scaleFontShorthand,
|
|
2088
2228
|
isGenericFamilyName: isGenericFamilyName,
|
|
2089
2229
|
isCJKFamilyName: isCJKFamilyName,
|
|
2090
2230
|
setWestEntry: setWestEntry,
|
|
@@ -2126,7 +2266,6 @@ if (typeof module !== "undefined" && module.exports) module.exports = shared;
|
|
|
2126
2266
|
|
|
2127
2267
|
var React = require("react");
|
|
2128
2268
|
var createPortal = require("react-dom").createPortal;
|
|
2129
|
-
var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
2130
2269
|
|
|
2131
2270
|
var h = React.createElement;
|
|
2132
2271
|
var useCallback = React.useCallback;
|
|
@@ -2158,6 +2297,7 @@ var PRESETS_FIELD = shared.PRESETS_FIELD;
|
|
|
2158
2297
|
var ACTIVE_PRESET_FIELD = shared.ACTIVE_PRESET_FIELD;
|
|
2159
2298
|
var UI_FOLLOWS_FIELD = shared.UI_FOLLOWS_FIELD;
|
|
2160
2299
|
var VALUE_FIELDS = shared.VALUE_FIELDS;
|
|
2300
|
+
var RETIRED_FIELDS = shared.RETIRED_FIELDS;
|
|
2161
2301
|
var SIZE_MIN = shared.SIZE_MIN;
|
|
2162
2302
|
var SIZE_MAX = shared.SIZE_MAX;
|
|
2163
2303
|
var WEIGHT_MIN = shared.WEIGHT_MIN;
|
|
@@ -2193,8 +2333,24 @@ var setWestEntry = shared.setWestEntry;
|
|
|
2193
2333
|
var setEastEntry = shared.setEastEntry;
|
|
2194
2334
|
var removeStackEntry = shared.removeStackEntry;
|
|
2195
2335
|
|
|
2196
|
-
/**
|
|
2197
|
-
|
|
2336
|
+
/**
|
|
2337
|
+
* Services this bundle waits for before it applies.
|
|
2338
|
+
*
|
|
2339
|
+
* `settingsScope` is deliberately absent: a host that does not provide a
|
|
2340
|
+
* declared service parks the whole package until it appears, and DSH
|
|
2341
|
+
* 0.1.7-alpha.x replaced that service with `configForms` — a plugin asking for
|
|
2342
|
+
* it never activates there. Both settings dialects are looked up at apply time
|
|
2343
|
+
* with `ctx.get` instead (see `pickSettingsScope`).
|
|
2344
|
+
*/
|
|
2345
|
+
var inject = ["slots", "locale"];
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* The settings scope and translator `apply` bound, for the seat that renders
|
|
2349
|
+
* the card without handing it either: the alpha line's bundle page renders the
|
|
2350
|
+
* registered component with its own props, so the card falls back to these.
|
|
2351
|
+
*/
|
|
2352
|
+
var cardScope = null;
|
|
2353
|
+
var cardT = null;
|
|
2198
2354
|
|
|
2199
2355
|
/** The weight DSH uses for body text; choosing it means "leave it alone". */
|
|
2200
2356
|
var NEUTRAL_WEIGHT = 400;
|
|
@@ -2224,10 +2380,78 @@ var CHEVRON_PATH =
|
|
|
2224
2380
|
var PANEL_HEIGHT = 380;
|
|
2225
2381
|
|
|
2226
2382
|
/**
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2383
|
+
* The translator the card falls back to when the host hands it no `t`: the
|
|
2384
|
+
* built-in English table says something useful where the key alone would not.
|
|
2385
|
+
* @param {string} key - dictionary key.
|
|
2386
|
+
* @param {Record<string, string>} [params] - `{name}` substitutions.
|
|
2387
|
+
* @returns {string} the text.
|
|
2388
|
+
*/
|
|
2389
|
+
function fallbackTranslate(key, params) {
|
|
2390
|
+
return translate("en", key, params);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/**
|
|
2394
|
+
* The settings seat a host without any settings service leaves the card: every
|
|
2395
|
+
* control renders disabled over an empty document, exactly like a read-only
|
|
2396
|
+
* deployment. It keeps the card — and therefore the whole configuration page —
|
|
2397
|
+
* from failing to render on a composition this plugin does not know.
|
|
2398
|
+
*/
|
|
2399
|
+
var MEMORY_SCOPE = {
|
|
2400
|
+
getSnapshot: function () {
|
|
2401
|
+
return {
|
|
2402
|
+
status: "ready",
|
|
2403
|
+
value: {},
|
|
2404
|
+
base: {},
|
|
2405
|
+
user: {},
|
|
2406
|
+
revision: 0,
|
|
2407
|
+
writable: false,
|
|
2408
|
+
mode: "memory",
|
|
2409
|
+
};
|
|
2410
|
+
},
|
|
2411
|
+
subscribe: function () {
|
|
2412
|
+
return function () {};
|
|
2413
|
+
},
|
|
2414
|
+
set: function () {
|
|
2415
|
+
return Promise.reject(new Error("settings are not available in this deployment"));
|
|
2416
|
+
},
|
|
2417
|
+
unset: function () {
|
|
2418
|
+
return Promise.reject(new Error("settings are not available in this deployment"));
|
|
2419
|
+
},
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
/* ------------------------------------------------------------------ *
|
|
2423
|
+
* inline icons
|
|
2424
|
+
* ------------------------------------------------------------------ */
|
|
2425
|
+
|
|
2426
|
+
/**
|
|
2427
|
+
* The 16px "plus" glyph of the "add a font" button.
|
|
2428
|
+
*
|
|
2429
|
+
* Drawn here rather than taken from `@deepseek-ai/dsh-client-ui-primitives`
|
|
2430
|
+
* because that module's icon names are not stable across the host lines this
|
|
2431
|
+
* plugin supports: up to 0.1.5-rc.x the shell exports `IconPlusOutline16`,
|
|
2432
|
+
* while 0.1.7-alpha.x ships the same artwork as `IconPlusOutlineRegular` /
|
|
2433
|
+
* `IconPlusOutlineMedium` — and requiring a name the shell does not have turns
|
|
2434
|
+
* the whole card into a render error. The path is the shipped one, byte for
|
|
2435
|
+
* byte, so the button looks identical on both.
|
|
2436
|
+
* @returns {object} the svg element.
|
|
2229
2437
|
*/
|
|
2230
|
-
|
|
2438
|
+
function plusIcon() {
|
|
2439
|
+
return h(
|
|
2440
|
+
"svg",
|
|
2441
|
+
{
|
|
2442
|
+
width: 16,
|
|
2443
|
+
height: 16,
|
|
2444
|
+
viewBox: "0 0 16 16",
|
|
2445
|
+
fill: "none",
|
|
2446
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
2447
|
+
"aria-hidden": "true",
|
|
2448
|
+
},
|
|
2449
|
+
h("path", {
|
|
2450
|
+
d: "M8.64453 1.5V7.34961H14.5V8.65039H8.64453V14.5H7.34473V8.65039H1.5V7.34961H7.34473V1.5H8.64453Z",
|
|
2451
|
+
fill: "currentColor",
|
|
2452
|
+
})
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2231
2455
|
|
|
2232
2456
|
/* ------------------------------------------------------------------ *
|
|
2233
2457
|
* copy
|
|
@@ -2287,13 +2511,9 @@ var DICTS = {
|
|
|
2287
2511
|
"monoWest.label": "Code · Western",
|
|
2288
2512
|
"monoEast.label": "Code · CJK",
|
|
2289
2513
|
"split.pick": "Choose…",
|
|
2290
|
-
"split.unset": "Not set",
|
|
2291
2514
|
"split.remove": "Remove {name}",
|
|
2292
2515
|
"split.rest": "Other fallbacks (reorder them in Advanced): {names}",
|
|
2293
2516
|
|
|
2294
|
-
"size.bodyLabel": "Interface font size offset",
|
|
2295
|
-
"size.bodyHint":
|
|
2296
|
-
"Adds {offset} to interface text sizes, on top of DSH's own font-size setting. 0 keeps DSH's sizes.",
|
|
2297
2517
|
"size.dialogLabel": "Conversation font size offset",
|
|
2298
2518
|
"size.dialogHint":
|
|
2299
2519
|
"Adds {offset} to the conversation text sizes, on top of DSH's own font-size setting. 0 keeps DSH's sizes.",
|
|
@@ -2312,9 +2532,6 @@ var DICTS = {
|
|
|
2312
2532
|
"weight.codeHint":
|
|
2313
2533
|
"Overrides code blocks, inline code and terminal output only. 400 or unset keeps DSH's own weight.",
|
|
2314
2534
|
|
|
2315
|
-
"line.bodyLabel": "Interface line height",
|
|
2316
|
-
"line.bodyHint":
|
|
2317
|
-
"Scales every interface line height by {ratio}. 100% keeps DSH's own line heights.",
|
|
2318
2535
|
"line.dialogLabel": "Conversation line height",
|
|
2319
2536
|
"line.dialogHint":
|
|
2320
2537
|
"Scales every conversation line height by {ratio}. 100% keeps DSH's own line heights.",
|
|
@@ -2334,7 +2551,6 @@ var DICTS = {
|
|
|
2334
2551
|
"Advanced font-feature-settings value, e.g. \"ss01\" on, \"cv01\" 1. Invalid values are ignored.",
|
|
2335
2552
|
"feat.placeholder": "\"ss01\" on",
|
|
2336
2553
|
|
|
2337
|
-
"synth.label": "Refuse synthetic styles",
|
|
2338
2554
|
"synth.italic": "No faux italic",
|
|
2339
2555
|
"synth.italicHint":
|
|
2340
2556
|
"CJK faces have no italic, so the browser tilts them. On keeps marked text upright.",
|
|
@@ -2348,15 +2564,6 @@ var DICTS = {
|
|
|
2348
2564
|
"section.ui": "Interface",
|
|
2349
2565
|
"section.dialog": "Conversation",
|
|
2350
2566
|
"section.code": "Code",
|
|
2351
|
-
"section.fine": "Global fine-tuning",
|
|
2352
|
-
"section.presets": "Presets",
|
|
2353
|
-
"section.uiHint":
|
|
2354
|
-
"Sidebars, headings, buttons and every other chrome text.",
|
|
2355
|
-
"section.dialogHint":
|
|
2356
|
-
"The conversation markdown: paragraphs, tables and headings.",
|
|
2357
|
-
"section.codeHint": "Code blocks, inline code and terminal output.",
|
|
2358
|
-
"section.fineHint":
|
|
2359
|
-
"Settings that apply to the whole page.",
|
|
2360
2567
|
"section.open": "Expand {name}",
|
|
2361
2568
|
"section.close": "Collapse {name}",
|
|
2362
2569
|
|
|
@@ -2365,13 +2572,6 @@ var DICTS = {
|
|
|
2365
2572
|
"ui.own": "Own values",
|
|
2366
2573
|
"dialog.default": "DSH defaults",
|
|
2367
2574
|
|
|
2368
|
-
"theme.label": "Per-theme values",
|
|
2369
|
-
"theme.on": "Light and dark keep separate values",
|
|
2370
|
-
"theme.edit": "Editing theme",
|
|
2371
|
-
"theme.light": "Light",
|
|
2372
|
-
"theme.dark": "Dark",
|
|
2373
|
-
"theme.mismatch":
|
|
2374
|
-
"The page is in {active} mode; the preview below shows the values being edited.",
|
|
2375
2575
|
|
|
2376
2576
|
"dialog.label": "Conversation font",
|
|
2377
2577
|
"dialog.hint":
|
|
@@ -2379,11 +2579,6 @@ var DICTS = {
|
|
|
2379
2579
|
"dialogWest.label": "Conversation · Western",
|
|
2380
2580
|
"dialogEast.label": "Conversation · CJK",
|
|
2381
2581
|
|
|
2382
|
-
"preset.save": "Save current",
|
|
2383
|
-
"preset.namePlaceholder": "Preset name",
|
|
2384
|
-
"preset.apply": "Apply",
|
|
2385
|
-
"preset.overwrite": "Overwrite",
|
|
2386
|
-
"preset.delete": "Delete",
|
|
2387
2582
|
"preset.export": "Export",
|
|
2388
2583
|
"preset.import": "Import",
|
|
2389
2584
|
"preset.importPlaceholder": "Paste an exported preset JSON here",
|
|
@@ -2394,18 +2589,13 @@ var DICTS = {
|
|
|
2394
2589
|
"preset.renamed": "Renamed to “{name}”.",
|
|
2395
2590
|
"preset.autoSave": "With a preset selected, every change saves into it automatically.",
|
|
2396
2591
|
"preset.nameUsed": "That name is already taken.",
|
|
2397
|
-
"preset.saved": "Saved “{name}”.",
|
|
2398
2592
|
"preset.applied": "Applied “{name}”.",
|
|
2399
|
-
"preset.deleted": "Deleted “{name}”.",
|
|
2400
|
-
"preset.updated": "Overwrote “{name}”.",
|
|
2401
2593
|
"preset.exported": "Presets copied to the clipboard.",
|
|
2402
2594
|
"preset.imported": "Imported {count} preset(s).",
|
|
2403
2595
|
"preset.importBad": "That text is not a valid preset export.",
|
|
2404
|
-
"preset.
|
|
2405
|
-
"preset.
|
|
2406
|
-
"preset.nameTaken": "“{name}” already exists — saving overwrites it.",
|
|
2596
|
+
"preset.exportManual": "Clipboard unavailable — press Ctrl+C to copy the selected text.",
|
|
2597
|
+
"preset.writeFailed": "The setting was not saved: {message}",
|
|
2407
2598
|
|
|
2408
|
-
"preview.label": "Preview",
|
|
2409
2599
|
"preview.sansCaption": "Interface",
|
|
2410
2600
|
"preview.dialogCaption": "Conversation",
|
|
2411
2601
|
"preview.monoCaption": "Code",
|
|
@@ -2413,8 +2603,6 @@ var DICTS = {
|
|
|
2413
2603
|
"The quick brown fox jumps over the lazy dog — 中文排版预览,标点符号,数字 0123456789。",
|
|
2414
2604
|
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
2415
2605
|
|
|
2416
|
-
"footnote.local":
|
|
2417
|
-
"Stored in the Host settings document. Every change applies immediately.",
|
|
2418
2606
|
},
|
|
2419
2607
|
zh: {
|
|
2420
2608
|
"card.title": "字体增强",
|
|
@@ -2467,13 +2655,9 @@ var DICTS = {
|
|
|
2467
2655
|
"monoWest.label": "代码 · 西文字体",
|
|
2468
2656
|
"monoEast.label": "代码 · 中文字体",
|
|
2469
2657
|
"split.pick": "选择…",
|
|
2470
|
-
"split.unset": "未选择",
|
|
2471
2658
|
"split.remove": "移除 {name}",
|
|
2472
2659
|
"split.rest": "其余回退项(在高级模式中排序):{names}",
|
|
2473
2660
|
|
|
2474
|
-
"size.bodyLabel": "界面字号偏移",
|
|
2475
|
-
"size.bodyHint":
|
|
2476
|
-
"给界面文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持原样。",
|
|
2477
2661
|
"size.dialogLabel": "对话字号偏移",
|
|
2478
2662
|
"size.dialogHint":
|
|
2479
2663
|
"给对话文字统一加 {offset},与设置里的「字号大小」叠加;0 表示保持 DSH 原样。",
|
|
@@ -2491,8 +2675,6 @@ var DICTS = {
|
|
|
2491
2675
|
"weight.codeHint":
|
|
2492
2676
|
"只覆盖代码块、行内代码与终端输出;400 或未设置表示保持 DSH 原样。",
|
|
2493
2677
|
|
|
2494
|
-
"line.bodyLabel": "界面行高",
|
|
2495
|
-
"line.bodyHint": "把界面行高整体缩放为 {ratio};100% 表示保持原样。",
|
|
2496
2678
|
"line.dialogLabel": "对话行高",
|
|
2497
2679
|
"line.dialogHint": "把对话行高整体缩放为 {ratio};100% 表示保持 DSH 原样。",
|
|
2498
2680
|
"line.codeLabel": "代码行高",
|
|
@@ -2510,7 +2692,6 @@ var DICTS = {
|
|
|
2510
2692
|
"高级的 font-feature-settings 值,例如 ss01 on、cv01 1;非法值会被忽略。",
|
|
2511
2693
|
"feat.placeholder": "\"ss01\" on",
|
|
2512
2694
|
|
|
2513
|
-
"synth.label": "拒绝合成样式",
|
|
2514
2695
|
"synth.italic": "禁用伪斜体",
|
|
2515
2696
|
"synth.italicHint":
|
|
2516
2697
|
"中文字体没有斜体,浏览器会把标记文本硬掰歪;开启后保持直立。",
|
|
@@ -2523,21 +2704,9 @@ var DICTS = {
|
|
|
2523
2704
|
"section.ui": "界面",
|
|
2524
2705
|
"section.dialog": "对话",
|
|
2525
2706
|
"section.code": "代码",
|
|
2526
|
-
"section.fine": "全局微调",
|
|
2527
|
-
"section.presets": "预设方案",
|
|
2528
|
-
"section.uiHint": "侧栏、标题、按钮等界面文字。",
|
|
2529
|
-
"section.dialogHint": "会话里的 Markdown:段落、表格与标题。",
|
|
2530
|
-
"section.codeHint": "代码块、行内代码与终端输出。",
|
|
2531
|
-
"section.fineHint": "作用于整页的选项。",
|
|
2532
2707
|
"section.open": "展开{name}",
|
|
2533
2708
|
"section.close": "收起{name}",
|
|
2534
2709
|
|
|
2535
|
-
"theme.label": "分主题数值",
|
|
2536
|
-
"theme.on": "深浅色各存一套数值",
|
|
2537
|
-
"theme.edit": "正在编辑",
|
|
2538
|
-
"theme.light": "浅色",
|
|
2539
|
-
"theme.dark": "深色",
|
|
2540
|
-
"theme.mismatch": "当前页面是{active}主题,下方预览显示的是正在编辑的那套数值。",
|
|
2541
2710
|
|
|
2542
2711
|
"ui.follow": "跟随对话设置",
|
|
2543
2712
|
"ui.followHint": "字体与字重",
|
|
@@ -2550,11 +2719,6 @@ var DICTS = {
|
|
|
2550
2719
|
"dialogWest.label": "对话 · 西文字体",
|
|
2551
2720
|
"dialogEast.label": "对话 · 中文字体",
|
|
2552
2721
|
|
|
2553
|
-
"preset.save": "保存当前",
|
|
2554
|
-
"preset.namePlaceholder": "方案名称",
|
|
2555
|
-
"preset.apply": "应用",
|
|
2556
|
-
"preset.overwrite": "覆盖",
|
|
2557
|
-
"preset.delete": "删除",
|
|
2558
2722
|
"preset.export": "导出",
|
|
2559
2723
|
"preset.import": "导入",
|
|
2560
2724
|
"preset.importPlaceholder": "粘贴导出的方案内容",
|
|
@@ -2565,18 +2729,13 @@ var DICTS = {
|
|
|
2565
2729
|
"preset.renamed": "已改名为“{name}”。",
|
|
2566
2730
|
"preset.autoSave": "选中方案后,所有改动自动存入该方案。",
|
|
2567
2731
|
"preset.nameUsed": "这个名字已被占用。",
|
|
2568
|
-
"preset.saved": "已保存“{name}”。",
|
|
2569
2732
|
"preset.applied": "已应用“{name}”。",
|
|
2570
|
-
"preset.deleted": "已删除“{name}”。",
|
|
2571
|
-
"preset.updated": "已覆盖“{name}”。",
|
|
2572
2733
|
"preset.exported": "方案已复制到剪贴板。",
|
|
2573
2734
|
"preset.imported": "已导入 {count} 个方案。",
|
|
2574
2735
|
"preset.importBad": "这段内容不是有效的方案导出。",
|
|
2575
|
-
"preset.
|
|
2576
|
-
"preset.
|
|
2577
|
-
"preset.nameTaken": "“{name}”已存在,保存将覆盖它。",
|
|
2736
|
+
"preset.exportManual": "剪贴板不可用,按 Ctrl+C 复制下方已选中的内容。",
|
|
2737
|
+
"preset.writeFailed": "设置没有保存成功:{message}",
|
|
2578
2738
|
|
|
2579
|
-
"preview.label": "预览",
|
|
2580
2739
|
"preview.sansCaption": "界面",
|
|
2581
2740
|
"preview.dialogCaption": "对话",
|
|
2582
2741
|
"preview.monoCaption": "代码",
|
|
@@ -2584,7 +2743,6 @@ var DICTS = {
|
|
|
2584
2743
|
"The quick brown fox jumps over the lazy dog —— 中文排版预览,标点符号,数字 0123456789。",
|
|
2585
2744
|
"preview.code": "const greet = (name) => `hello ${name}`; // => != >= -> 代码预览",
|
|
2586
2745
|
|
|
2587
|
-
"footnote.local": "保存在宿主设置文档里;每次改动立即生效。",
|
|
2588
2746
|
},
|
|
2589
2747
|
};
|
|
2590
2748
|
|
|
@@ -2687,7 +2845,6 @@ var CARD_CSS = [
|
|
|
2687
2845
|
".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)}",
|
|
2688
2846
|
".dfp-miniButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-specific-sidebar-nav-item-hover)}",
|
|
2689
2847
|
".dfp-miniButton:disabled{opacity:.4;cursor:default}",
|
|
2690
|
-
".dfp-presetForm{align-items:center;gap:8px;margin-top:10px;display:flex}",
|
|
2691
2848
|
".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}",
|
|
2692
2849
|
// The auto-save hint and the transient message share one row: the hint holds
|
|
2693
2850
|
// the left, the message fades in on the right. The row is always mounted and
|
|
@@ -2736,6 +2893,7 @@ var CARD_CSS = [
|
|
|
2736
2893
|
|
|
2737
2894
|
".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}",
|
|
2738
2895
|
".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}",
|
|
2896
|
+
".dfp-exportText{resize:vertical;min-height:54px;font-family:var(--ds-font-family-code,ui-monospace,monospace);font-size:12px;line-height:17px}",
|
|
2739
2897
|
".dfp-note{padding:4px 2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
|
|
2740
2898
|
".dfp-list{flex-direction:column;gap:1px;flex:1;min-height:0;overflow-y:auto;display:flex}",
|
|
2741
2899
|
".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)}",
|
|
@@ -3321,24 +3479,45 @@ function PresetSelect(props) {
|
|
|
3321
3479
|
* ------------------------------------------------------------------ */
|
|
3322
3480
|
|
|
3323
3481
|
/**
|
|
3324
|
-
* @typedef {{status: "unsupported"|"loading"|"denied"|"ready", families: string[]
|
|
3482
|
+
* @typedef {{status: "unsupported"|"loading"|"denied"|"ready", families: string[],
|
|
3483
|
+
* at?: number, permanent?: boolean}} Catalog
|
|
3325
3484
|
*/
|
|
3326
3485
|
|
|
3327
3486
|
/** Enumeration is session-stable, so it is resolved once and kept. */
|
|
3328
3487
|
var catalogCache = null;
|
|
3329
3488
|
|
|
3489
|
+
/**
|
|
3490
|
+
* How long a refused (or empty) enumeration is trusted before the picker asks
|
|
3491
|
+
* again. Answering "denied" and never retrying is wrong in the case that
|
|
3492
|
+
* actually happens: the user grants the permission from the browser's prompt
|
|
3493
|
+
* or site settings and expects the list to fill in — and when the permission
|
|
3494
|
+
* is still blocked the browser rejects the call instantly without prompting,
|
|
3495
|
+
* so the retry costs nothing.
|
|
3496
|
+
*/
|
|
3497
|
+
var CATALOG_RETRY_MS = 20000;
|
|
3498
|
+
|
|
3499
|
+
/** The clock, injectable so the retry window is testable without waiting. */
|
|
3500
|
+
var catalogNow = function () {
|
|
3501
|
+
return Date.now();
|
|
3502
|
+
};
|
|
3503
|
+
|
|
3330
3504
|
/**
|
|
3331
3505
|
* Enumerate installed families; the built-in presets stand in when the browser
|
|
3332
3506
|
* cannot or will not enumerate them.
|
|
3507
|
+
* @param {boolean} [force] - ignore a cached refusal and ask again.
|
|
3333
3508
|
* @returns {Promise<Catalog>} the catalog.
|
|
3334
3509
|
*/
|
|
3335
|
-
async function loadCatalog() {
|
|
3336
|
-
|
|
3510
|
+
async function loadCatalog(force) {
|
|
3511
|
+
var now = catalogNow();
|
|
3512
|
+
if (force !== true && !shared.catalogRefreshDue(catalogCache, now, CATALOG_RETRY_MS)) {
|
|
3513
|
+
return catalogCache;
|
|
3514
|
+
}
|
|
3337
3515
|
var host = /** @type {{queryLocalFonts?: () => Promise<Array<{family: string}>>}} */ (
|
|
3338
3516
|
globalThis
|
|
3339
3517
|
);
|
|
3340
3518
|
if (typeof host.queryLocalFonts !== "function") {
|
|
3341
|
-
|
|
3519
|
+
// No API at all: retrying cannot help, and the picker says so.
|
|
3520
|
+
catalogCache = { status: "unsupported", families: [], at: now, permanent: true };
|
|
3342
3521
|
return catalogCache;
|
|
3343
3522
|
}
|
|
3344
3523
|
try {
|
|
@@ -3358,11 +3537,11 @@ async function loadCatalog() {
|
|
|
3358
3537
|
});
|
|
3359
3538
|
catalogCache =
|
|
3360
3539
|
families.length === 0
|
|
3361
|
-
? { status: "unsupported", families: [] }
|
|
3362
|
-
: { status: "ready", families: families };
|
|
3540
|
+
? { status: "unsupported", families: [], at: now }
|
|
3541
|
+
: { status: "ready", families: families, at: now };
|
|
3363
3542
|
} catch (error) {
|
|
3364
3543
|
// A refused permission is the common case here: `queryLocalFonts` prompts.
|
|
3365
|
-
catalogCache = { status: "denied", families: [] };
|
|
3544
|
+
catalogCache = { status: "denied", families: [], at: now };
|
|
3366
3545
|
}
|
|
3367
3546
|
return catalogCache;
|
|
3368
3547
|
}
|
|
@@ -3425,13 +3604,65 @@ function classifyFamily(name) {
|
|
|
3425
3604
|
return verdict;
|
|
3426
3605
|
}
|
|
3427
3606
|
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3607
|
+
/* ------------------------------------------------------------------ *
|
|
3608
|
+
* clipboard
|
|
3609
|
+
* ------------------------------------------------------------------ */
|
|
3610
|
+
|
|
3611
|
+
/**
|
|
3612
|
+
* Copy through the legacy selection path.
|
|
3613
|
+
*
|
|
3614
|
+
* This is not a museum piece: the async Clipboard API only exists in a secure
|
|
3615
|
+
* context, and the deployment note in this repository's README documents a
|
|
3616
|
+
* remote HTTP origin where `navigator.clipboard` is simply absent. A
|
|
3617
|
+
* selection-based copy still works there, and it also works when the async
|
|
3618
|
+
* call is refused without the clipboard-write permission.
|
|
3619
|
+
* @param {string} text - what to put on the clipboard.
|
|
3620
|
+
* @returns {boolean} whether the copy command reported success.
|
|
3621
|
+
*/
|
|
3622
|
+
function copyBySelection(text) {
|
|
3623
|
+
if (typeof document === "undefined" || !document.body) return false;
|
|
3624
|
+
if (typeof document.execCommand !== "function") return false;
|
|
3625
|
+
var area = null;
|
|
3626
|
+
try {
|
|
3627
|
+
area = document.createElement("textarea");
|
|
3628
|
+
area.value = text;
|
|
3629
|
+
area.setAttribute("readonly", "");
|
|
3630
|
+
// Off-screen rather than hidden: `display:none` cannot hold a selection.
|
|
3631
|
+
area.style.position = "fixed";
|
|
3632
|
+
area.style.top = "-1000px";
|
|
3633
|
+
area.style.opacity = "0";
|
|
3634
|
+
document.body.appendChild(area);
|
|
3635
|
+
area.select();
|
|
3636
|
+
return document.execCommand("copy") === true;
|
|
3637
|
+
} catch (error) {
|
|
3638
|
+
return false;
|
|
3639
|
+
} finally {
|
|
3640
|
+
if (area !== null && area.parentNode) area.parentNode.removeChild(area);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
/**
|
|
3645
|
+
* Copy text, trying the async API first and the selection fallback after it.
|
|
3646
|
+
* @param {string} text - what to put on the clipboard.
|
|
3647
|
+
* @returns {Promise<boolean>} whether the text reached the clipboard.
|
|
3648
|
+
*/
|
|
3649
|
+
async function copyText(text) {
|
|
3650
|
+
var nav = globalThis.navigator;
|
|
3651
|
+
if (
|
|
3652
|
+
nav !== undefined &&
|
|
3653
|
+
nav !== null &&
|
|
3654
|
+
nav.clipboard !== undefined &&
|
|
3655
|
+
typeof nav.clipboard.writeText === "function"
|
|
3656
|
+
) {
|
|
3657
|
+
try {
|
|
3658
|
+
await nav.clipboard.writeText(text);
|
|
3659
|
+
return true;
|
|
3660
|
+
} catch (error) {
|
|
3661
|
+
// No permission, or no clipboard in this context: fall through.
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
return copyBySelection(text);
|
|
3665
|
+
}
|
|
3435
3666
|
|
|
3436
3667
|
/* ------------------------------------------------------------------ *
|
|
3437
3668
|
* the family picker
|
|
@@ -3576,7 +3807,6 @@ function StackPicker(props) {
|
|
|
3576
3807
|
commit(families.concat([name]));
|
|
3577
3808
|
};
|
|
3578
3809
|
|
|
3579
|
-
var needle = query.trim().toLowerCase();
|
|
3580
3810
|
var selectedLower = {};
|
|
3581
3811
|
if (single) {
|
|
3582
3812
|
if (typeof props.value === "string" && props.value !== "") {
|
|
@@ -3587,47 +3817,15 @@ function StackPicker(props) {
|
|
|
3587
3817
|
selectedLower[families[selectedIndex].toLowerCase()] = true;
|
|
3588
3818
|
}
|
|
3589
3819
|
}
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
var options =
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
if (skipSelected === true && selectedLower[name.toLowerCase()] === true) continue;
|
|
3600
|
-
rows.push(name);
|
|
3601
|
-
if (limit !== undefined && rows.length >= limit) break;
|
|
3602
|
-
}
|
|
3603
|
-
if (rows.length > 0) options.push({ label: labelKey, families: rows });
|
|
3604
|
-
};
|
|
3605
|
-
|
|
3606
|
-
var selectedRows = [];
|
|
3607
|
-
for (var familyIndex = 0; familyIndex < families.length; familyIndex += 1) {
|
|
3608
|
-
if (matches(families[familyIndex])) selectedRows.push(families[familyIndex]);
|
|
3609
|
-
}
|
|
3610
|
-
if (selectedRows.length > 0) {
|
|
3611
|
-
options.push({ label: "stack.groupSelected", families: selectedRows });
|
|
3612
|
-
}
|
|
3613
|
-
|
|
3614
|
-
if (catalog.status === "ready") {
|
|
3615
|
-
if (needle === "") {
|
|
3616
|
-
for (var groupIndex = 0; groupIndex < PRESET_GROUPS.length; groupIndex += 1) {
|
|
3617
|
-
pushGroup(PRESET_GROUPS[groupIndex].label, PRESET_GROUPS[groupIndex].families, undefined, !single);
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
pushGroup("stack.groupLocal", catalog.families, MAX_VISIBLE_FONTS, !single);
|
|
3621
|
-
} else {
|
|
3622
|
-
for (var fallbackIndex = 0; fallbackIndex < PRESET_GROUPS.length; fallbackIndex += 1) {
|
|
3623
|
-
pushGroup(
|
|
3624
|
-
PRESET_GROUPS[fallbackIndex].label,
|
|
3625
|
-
PRESET_GROUPS[fallbackIndex].families,
|
|
3626
|
-
undefined,
|
|
3627
|
-
!single
|
|
3628
|
-
);
|
|
3629
|
-
}
|
|
3630
|
-
}
|
|
3820
|
+
// The grouping itself is pure and lives in the shared core, where it is
|
|
3821
|
+
// covered without a browser: which rows appear, in which order, and never
|
|
3822
|
+
// twice.
|
|
3823
|
+
var options = shared.pickerGroups({
|
|
3824
|
+
stack: families,
|
|
3825
|
+
single: single,
|
|
3826
|
+
query: query,
|
|
3827
|
+
catalog: catalog,
|
|
3828
|
+
});
|
|
3631
3829
|
|
|
3632
3830
|
var typed = query.trim();
|
|
3633
3831
|
var typedKnown = false;
|
|
@@ -3904,7 +4102,7 @@ function StackPicker(props) {
|
|
|
3904
4102
|
setOpen(!open);
|
|
3905
4103
|
},
|
|
3906
4104
|
},
|
|
3907
|
-
|
|
4105
|
+
plusIcon(),
|
|
3908
4106
|
h("span", null, t("stack.add"))
|
|
3909
4107
|
)
|
|
3910
4108
|
)
|
|
@@ -4077,6 +4275,24 @@ function firstFamily(stack) {
|
|
|
4077
4275
|
* the card
|
|
4078
4276
|
* ------------------------------------------------------------------ */
|
|
4079
4277
|
|
|
4278
|
+
/**
|
|
4279
|
+
* The card as DSH 0.1.7-alpha.x's Plugins page renders it.
|
|
4280
|
+
*
|
|
4281
|
+
* That page contributes one entry per plugin (`plugins.item`) and asks that one
|
|
4282
|
+
* component for two different views: a one-line `summary` for the card in the
|
|
4283
|
+
* list, and the `page` body once the entry is opened. The summary is answered
|
|
4284
|
+
* with the card's own description, and every other view is the card itself, so
|
|
4285
|
+
* the same UI serves the rc line's configuration cell and the alpha's plugin
|
|
4286
|
+
* page without a second implementation.
|
|
4287
|
+
* @param {object} props - the view asked for plus the injected face.
|
|
4288
|
+
* @returns {object|string} the card, or the one-line summary.
|
|
4289
|
+
*/
|
|
4290
|
+
function FontCardPage(props) {
|
|
4291
|
+
var t = typeof props.t === "function" ? props.t : cardT || fallbackTranslate;
|
|
4292
|
+
if (props.view === "summary") return t("card.description");
|
|
4293
|
+
return h(FontCard, { scope: props.scope || cardScope || MEMORY_SCOPE, t: t });
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4080
4296
|
/**
|
|
4081
4297
|
* Render the plugin's card: the settings section dispatches this component
|
|
4082
4298
|
* under the namespace key, and the host has to serve that namespace for it to
|
|
@@ -4094,8 +4310,11 @@ function firstFamily(stack) {
|
|
|
4094
4310
|
* @returns {object} the card element.
|
|
4095
4311
|
*/
|
|
4096
4312
|
function FontCard(props) {
|
|
4097
|
-
|
|
4098
|
-
|
|
4313
|
+
// The registration hands the card its face on the line that supports
|
|
4314
|
+
// `inject`; the alpha's bundle page renders it with its own props, so the
|
|
4315
|
+
// values `apply` bound are the fallback.
|
|
4316
|
+
var t = typeof props.t === "function" ? props.t : cardT || fallbackTranslate;
|
|
4317
|
+
var scope = props.scope || cardScope || MEMORY_SCOPE;
|
|
4099
4318
|
var [open, setOpen] = useState(false);
|
|
4100
4319
|
var [view, setView] = useState(readViewMode);
|
|
4101
4320
|
var [expanded, setExpanded] = useState(null);
|
|
@@ -4105,6 +4324,8 @@ function FontCard(props) {
|
|
|
4105
4324
|
// effect runs again instead of ignoring the second one.
|
|
4106
4325
|
var [presetStatus, setPresetStatus] = useState({ text: "", id: 0 });
|
|
4107
4326
|
var [statusShown, setStatusShown] = useState(false);
|
|
4327
|
+
/** The exported JSON when the clipboard refused it: shown selected instead. */
|
|
4328
|
+
var [exportText, setExportText] = useState("");
|
|
4108
4329
|
var [renaming, setRenaming] = useState(false);
|
|
4109
4330
|
var [importOpen, setImportOpen] = useState(false);
|
|
4110
4331
|
/** True while the card itself writes a whole preset's values: the edits
|
|
@@ -4191,19 +4412,71 @@ function FontCard(props) {
|
|
|
4191
4412
|
clear(field);
|
|
4192
4413
|
mirrorPreset(field, DEFAULTS[field]);
|
|
4193
4414
|
};
|
|
4194
|
-
|
|
4195
|
-
|
|
4415
|
+
/**
|
|
4416
|
+
* Report a write the settings document refused. A silent failure is the
|
|
4417
|
+
* worst outcome here: the control goes back to showing the stored value and
|
|
4418
|
+
* the user is left believing the change was saved.
|
|
4419
|
+
* @param {unknown} error - the rejection reason, or a boolean refusal.
|
|
4420
|
+
*/
|
|
4421
|
+
var writeFailed = function (error) {
|
|
4422
|
+
var message =
|
|
4423
|
+
error === false || error === undefined
|
|
4424
|
+
? "the host refused the write"
|
|
4425
|
+
: error && error.message
|
|
4426
|
+
? String(error.message)
|
|
4427
|
+
: String(error);
|
|
4428
|
+
if (message.length > 160) message = message.slice(0, 160) + "…";
|
|
4429
|
+
showStatus(t("preset.writeFailed", { message: message }));
|
|
4430
|
+
};
|
|
4431
|
+
/**
|
|
4432
|
+
* Follow one queued settings write.
|
|
4433
|
+
*
|
|
4434
|
+
* The two host lines answer differently: up to 0.1.5-rc.x the scope resolves
|
|
4435
|
+
* a promise that rejects on a transport failure, while 0.1.7-alpha.x resolves
|
|
4436
|
+
* `false` for a write the host refused (a stale revision, say). Both are
|
|
4437
|
+
* reported, and a refused write is never left looking saved.
|
|
4438
|
+
* @param {unknown} result - what the scope returned.
|
|
4439
|
+
*/
|
|
4440
|
+
var followWrite = function (result) {
|
|
4441
|
+
if (result === false) {
|
|
4442
|
+
writeFailed(result);
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
if (result && typeof result.then === "function") {
|
|
4446
|
+
result.then(function (accepted) {
|
|
4447
|
+
if (accepted === false) writeFailed(accepted);
|
|
4448
|
+
}, function (error) {
|
|
4449
|
+
// The scope reloads the Host state itself; the status row is what
|
|
4450
|
+
// keeps the user from thinking the change stuck.
|
|
4451
|
+
writeFailed(error);
|
|
4452
|
+
});
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4196
4455
|
if (result && typeof result.catch === "function") {
|
|
4197
|
-
result.catch(function () {
|
|
4198
|
-
|
|
4456
|
+
result.catch(function (error) {
|
|
4457
|
+
writeFailed(error);
|
|
4199
4458
|
});
|
|
4200
4459
|
}
|
|
4201
4460
|
};
|
|
4461
|
+
var submit = function (field, value) {
|
|
4462
|
+
var result;
|
|
4463
|
+
try {
|
|
4464
|
+
result = scope.set(field, value);
|
|
4465
|
+
} catch (error) {
|
|
4466
|
+
writeFailed(error);
|
|
4467
|
+
return;
|
|
4468
|
+
}
|
|
4469
|
+
followWrite(result);
|
|
4470
|
+
};
|
|
4202
4471
|
var clear = function (field) {
|
|
4203
|
-
var result
|
|
4204
|
-
|
|
4205
|
-
result.
|
|
4472
|
+
var result;
|
|
4473
|
+
try {
|
|
4474
|
+
result = scope.unset(field);
|
|
4475
|
+
} catch (error) {
|
|
4476
|
+
writeFailed(error);
|
|
4477
|
+
return;
|
|
4206
4478
|
}
|
|
4479
|
+
followWrite(result);
|
|
4207
4480
|
};
|
|
4208
4481
|
var overridden = function (field) {
|
|
4209
4482
|
return Object.prototype.hasOwnProperty.call(user, field);
|
|
@@ -4542,21 +4815,23 @@ function FontCard(props) {
|
|
|
4542
4815
|
showStatus(t("preset.renamed", { name: name }));
|
|
4543
4816
|
};
|
|
4544
4817
|
|
|
4818
|
+
/**
|
|
4819
|
+
* Hand the preset list over as JSON: the clipboard when it is available,
|
|
4820
|
+
* and otherwise a selected read-only box with the same text in it. The old
|
|
4821
|
+
* fallback printed the export into the one-line status row, which truncated
|
|
4822
|
+
* the JSON at 120 characters — a message that could not be copied from.
|
|
4823
|
+
*/
|
|
4545
4824
|
var exportPresets = function () {
|
|
4546
4825
|
var text = JSON.stringify(presetList);
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
.
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
});
|
|
4557
|
-
} else {
|
|
4558
|
-
showStatus(text.slice(0, 120) + "…");
|
|
4559
|
-
}
|
|
4826
|
+
void copyText(text).then(function (copied) {
|
|
4827
|
+
if (copied) {
|
|
4828
|
+
setExportText("");
|
|
4829
|
+
showStatus(t("preset.exported"));
|
|
4830
|
+
return;
|
|
4831
|
+
}
|
|
4832
|
+
setExportText(text);
|
|
4833
|
+
showStatus(t("preset.exportManual"));
|
|
4834
|
+
});
|
|
4560
4835
|
};
|
|
4561
4836
|
var importPresets = function (text) {
|
|
4562
4837
|
var parsed = normalizePresets(text);
|
|
@@ -4852,6 +5127,25 @@ function FontCard(props) {
|
|
|
4852
5127
|
if (text !== "") importPresets(text);
|
|
4853
5128
|
},
|
|
4854
5129
|
})
|
|
5130
|
+
: null,
|
|
5131
|
+
// The clipboard-less export: the same JSON, already selected, so
|
|
5132
|
+
// the one thing left to do is press the copy shortcut.
|
|
5133
|
+
exportText !== ""
|
|
5134
|
+
? h("textarea", {
|
|
5135
|
+
className: "dfp-search dfp-exportText",
|
|
5136
|
+
rows: 3,
|
|
5137
|
+
readOnly: true,
|
|
5138
|
+
autoFocus: true,
|
|
5139
|
+
spellCheck: false,
|
|
5140
|
+
"aria-label": t("preset.exportManual"),
|
|
5141
|
+
value: exportText,
|
|
5142
|
+
onFocus: function (event) {
|
|
5143
|
+
event.target.select();
|
|
5144
|
+
},
|
|
5145
|
+
onCopy: function () {
|
|
5146
|
+
setExportText("");
|
|
5147
|
+
},
|
|
5148
|
+
})
|
|
4855
5149
|
: null
|
|
4856
5150
|
),
|
|
4857
5151
|
|
|
@@ -5075,6 +5369,11 @@ function FontCard(props) {
|
|
|
5075
5369
|
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
5076
5370
|
clear(VALUE_FIELDS[index]);
|
|
5077
5371
|
}
|
|
5372
|
+
// The retired axes are cleared too: they render nothing, so a
|
|
5373
|
+
// value left behind would be invisible forever.
|
|
5374
|
+
for (var retired = 0; retired < RETIRED_FIELDS.length; retired += 1) {
|
|
5375
|
+
clear(RETIRED_FIELDS[retired]);
|
|
5376
|
+
}
|
|
5078
5377
|
clear(DARK_VALUES_FIELD);
|
|
5079
5378
|
// Auto-save: the reset IS a change to the active preset, so
|
|
5080
5379
|
// its snapshot follows back to the neutral defaults.
|
|
@@ -5118,7 +5417,231 @@ function apply(ctx) {
|
|
|
5118
5417
|
var applyCss = createStylesheet(function () {
|
|
5119
5418
|
return tokens;
|
|
5120
5419
|
});
|
|
5121
|
-
|
|
5420
|
+
|
|
5421
|
+
/**
|
|
5422
|
+
* Look a service up without declaring it.
|
|
5423
|
+
*
|
|
5424
|
+
* Declaring a name the running host does not provide parks the whole package
|
|
5425
|
+
* until it appears — on DSH 0.1.7-alpha.x a declared `settingsScope` left the
|
|
5426
|
+
* Web UI waiting on a service that no longer exists. Both routes are tried:
|
|
5427
|
+
* `ctx.get(name)` first, then the service property older lines also expose.
|
|
5428
|
+
* @param {string} name - the service name.
|
|
5429
|
+
* @returns {object|null} the service, or null when this host has none.
|
|
5430
|
+
*/
|
|
5431
|
+
var getService = function (name) {
|
|
5432
|
+
try {
|
|
5433
|
+
if (typeof ctx.get === "function") {
|
|
5434
|
+
var found = ctx.get(name);
|
|
5435
|
+
if (found) return found;
|
|
5436
|
+
}
|
|
5437
|
+
} catch (error) {
|
|
5438
|
+
// fall through to the property read
|
|
5439
|
+
}
|
|
5440
|
+
try {
|
|
5441
|
+
return ctx[name] || null;
|
|
5442
|
+
} catch (error) {
|
|
5443
|
+
return null;
|
|
5444
|
+
}
|
|
5445
|
+
};
|
|
5446
|
+
|
|
5447
|
+
/**
|
|
5448
|
+
* Whether one served namespace is THIS plugin's form.
|
|
5449
|
+
*
|
|
5450
|
+
* The alpha keys a form by profile entry id, which the installing profile's
|
|
5451
|
+
* patch decides, so the entry's own fields are what identify us: either the
|
|
5452
|
+
* served schema or the values it carries name them.
|
|
5453
|
+
* @param {object} entry - one namespace from the describe view.
|
|
5454
|
+
* @returns {boolean} true when the entry is this plugin's.
|
|
5455
|
+
*/
|
|
5456
|
+
var isOurNamespace = function (entry) {
|
|
5457
|
+
var schema = "";
|
|
5458
|
+
try {
|
|
5459
|
+
schema = JSON.stringify((entry && entry.schema) || {});
|
|
5460
|
+
} catch (error) {
|
|
5461
|
+
schema = "";
|
|
5462
|
+
}
|
|
5463
|
+
if (schema.indexOf(UI_FOLLOWS_FIELD) >= 0 && schema.indexOf(STACK_DIALOG_FIELD) >= 0) {
|
|
5464
|
+
return true;
|
|
5465
|
+
}
|
|
5466
|
+
var values = (entry && (entry.value || entry.user || entry.base)) || {};
|
|
5467
|
+
return (
|
|
5468
|
+
Object.prototype.hasOwnProperty.call(values, UI_FOLLOWS_FIELD) &&
|
|
5469
|
+
Object.prototype.hasOwnProperty.call(values, STACK_DIALOG_FIELD)
|
|
5470
|
+
);
|
|
5471
|
+
};
|
|
5472
|
+
|
|
5473
|
+
/**
|
|
5474
|
+
* The profile entry id the `configForms` dialect addresses settings by.
|
|
5475
|
+
*
|
|
5476
|
+
* Up to 0.1.5-rc.x a namespace is a name the plugin registers; from
|
|
5477
|
+
* 0.1.7-alpha.x the settings service derives a form per composed entry and
|
|
5478
|
+
* keys it by that entry's id — which the installing profile's patch decides,
|
|
5479
|
+
* so nothing here may hard-code it. The served view is the only honest
|
|
5480
|
+
* answer: while it has not arrived this returns null, and the caller waits
|
|
5481
|
+
* for it instead of guessing a name whose form could never be written.
|
|
5482
|
+
* @param {object} forms - the `configForms` service.
|
|
5483
|
+
* @returns {string|null} the entry id, or null while the host has not served it.
|
|
5484
|
+
*/
|
|
5485
|
+
var formsNamespace = function (forms) {
|
|
5486
|
+
var view = null;
|
|
5487
|
+
try {
|
|
5488
|
+
view = forms.describe ? forms.describe().getSnapshot().view : null;
|
|
5489
|
+
} catch (error) {
|
|
5490
|
+
view = null;
|
|
5491
|
+
}
|
|
5492
|
+
var served = (view && view.namespaces) || [];
|
|
5493
|
+
if (globalThis.__DFP_PROBE__ === true) {
|
|
5494
|
+
console.log(
|
|
5495
|
+
"[dfp-probe] served namespaces " +
|
|
5496
|
+
JSON.stringify(
|
|
5497
|
+
served.map(function (item) {
|
|
5498
|
+
return item && item.ns;
|
|
5499
|
+
})
|
|
5500
|
+
)
|
|
5501
|
+
);
|
|
5502
|
+
}
|
|
5503
|
+
for (var index = 0; index < served.length; index += 1) {
|
|
5504
|
+
if (isOurNamespace(served[index])) {
|
|
5505
|
+
if (globalThis.__DFP_PROBE__ === true) {
|
|
5506
|
+
console.log("[dfp-probe] matched by served schema: " + String(served[index].ns));
|
|
5507
|
+
}
|
|
5508
|
+
return served[index].ns;
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
return null;
|
|
5512
|
+
};
|
|
5513
|
+
|
|
5514
|
+
/**
|
|
5515
|
+
* A scope that adopts the host's scope the moment the host serves it.
|
|
5516
|
+
*
|
|
5517
|
+
* The alpha's `configForms` can be asked for a form before its describe view
|
|
5518
|
+
* has arrived — `get(id)` then answers with a not-ready controller for that
|
|
5519
|
+
* id, and every write through it is refused. This stands in for the scope
|
|
5520
|
+
* until the real one is identifiable, forwards the card's subscription to it
|
|
5521
|
+
* on adoption, and lets the card re-read immediately afterwards.
|
|
5522
|
+
* @returns {object} the pending scope, with an `adopt` seat for the caller.
|
|
5523
|
+
*/
|
|
5524
|
+
var createPendingScope = function () {
|
|
5525
|
+
var target = null;
|
|
5526
|
+
var listeners = [];
|
|
5527
|
+
return {
|
|
5528
|
+
getSnapshot: function () {
|
|
5529
|
+
return target === null
|
|
5530
|
+
? {
|
|
5531
|
+
status: "loading",
|
|
5532
|
+
value: undefined,
|
|
5533
|
+
base: undefined,
|
|
5534
|
+
user: undefined,
|
|
5535
|
+
revision: undefined,
|
|
5536
|
+
writable: false,
|
|
5537
|
+
mode: "pending",
|
|
5538
|
+
}
|
|
5539
|
+
: target.getSnapshot();
|
|
5540
|
+
},
|
|
5541
|
+
subscribe: function (listener) {
|
|
5542
|
+
if (target !== null) return target.subscribe(listener);
|
|
5543
|
+
listeners.push(listener);
|
|
5544
|
+
return function () {
|
|
5545
|
+
var at = listeners.indexOf(listener);
|
|
5546
|
+
if (at >= 0) listeners.splice(at, 1);
|
|
5547
|
+
};
|
|
5548
|
+
},
|
|
5549
|
+
set: function (field, value) {
|
|
5550
|
+
if (target === null) {
|
|
5551
|
+
return Promise.reject(new Error("the settings service is still starting"));
|
|
5552
|
+
}
|
|
5553
|
+
return target.set(field, value);
|
|
5554
|
+
},
|
|
5555
|
+
unset: function (field) {
|
|
5556
|
+
if (target === null) {
|
|
5557
|
+
return Promise.reject(new Error("the settings service is still starting"));
|
|
5558
|
+
}
|
|
5559
|
+
return target.unset(field);
|
|
5560
|
+
},
|
|
5561
|
+
/** Hand over the real scope once it is identifiable; true when taken. */
|
|
5562
|
+
adopt: function (next) {
|
|
5563
|
+
if (target !== null || next === null || next === undefined) return false;
|
|
5564
|
+
target = next;
|
|
5565
|
+
var pending = listeners.slice();
|
|
5566
|
+
listeners.length = 0;
|
|
5567
|
+
for (var index = 0; index < pending.length; index += 1) {
|
|
5568
|
+
target.subscribe(pending[index]);
|
|
5569
|
+
pending[index]();
|
|
5570
|
+
}
|
|
5571
|
+
return true;
|
|
5572
|
+
},
|
|
5573
|
+
};
|
|
5574
|
+
};
|
|
5575
|
+
|
|
5576
|
+
/**
|
|
5577
|
+
* Bind the settings scope in whichever dialect the running host speaks: the
|
|
5578
|
+
* `settingsScope` service up to 0.1.5-rc.x (`bind({namespace})`), the
|
|
5579
|
+
* `configForms` service from 0.1.7-alpha.x (`get(entryId)`). Both expose the
|
|
5580
|
+
* same snapshot/`set`/`unset` face, so one card covers them.
|
|
5581
|
+
* @returns {object|null} the bound scope, or null when neither dialect answered.
|
|
5582
|
+
*/
|
|
5583
|
+
var pickSettingsScope = function () {
|
|
5584
|
+
var settingsScope = getService("settingsScope");
|
|
5585
|
+
if (settingsScope && typeof settingsScope.bind === "function") {
|
|
5586
|
+
try {
|
|
5587
|
+
var bound = settingsScope.bind({ namespace: NAMESPACE });
|
|
5588
|
+
if (bound) return bound;
|
|
5589
|
+
} catch (error) {
|
|
5590
|
+
// fall through to the newer dialect
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
var forms = getService("configForms");
|
|
5594
|
+
if (forms && typeof forms.get === "function") {
|
|
5595
|
+
var bindAlphaForm = function () {
|
|
5596
|
+
var ns = formsNamespace(forms);
|
|
5597
|
+
if (ns === null) return null;
|
|
5598
|
+
try {
|
|
5599
|
+
var form = forms.get(ns);
|
|
5600
|
+
return form && typeof form.getSnapshot === "function" ? form : null;
|
|
5601
|
+
} catch (error) {
|
|
5602
|
+
return null;
|
|
5603
|
+
}
|
|
5604
|
+
};
|
|
5605
|
+
var immediate = bindAlphaForm();
|
|
5606
|
+
if (immediate !== null) return immediate;
|
|
5607
|
+
var pending = createPendingScope();
|
|
5608
|
+
var mirror = null;
|
|
5609
|
+
try {
|
|
5610
|
+
mirror = typeof forms.describe === "function" ? forms.describe() : null;
|
|
5611
|
+
} catch (error) {
|
|
5612
|
+
mirror = null;
|
|
5613
|
+
}
|
|
5614
|
+
var offMirror = null;
|
|
5615
|
+
var adopt = function () {
|
|
5616
|
+
var form = bindAlphaForm();
|
|
5617
|
+
if (form === null) return;
|
|
5618
|
+
if (!pending.adopt(form)) return;
|
|
5619
|
+
if (typeof offMirror === "function") offMirror();
|
|
5620
|
+
};
|
|
5621
|
+
if (mirror && typeof mirror.subscribe === "function") {
|
|
5622
|
+
offMirror = mirror.subscribe(adopt);
|
|
5623
|
+
if (typeof mirror.ensure === "function") mirror.ensure();
|
|
5624
|
+
}
|
|
5625
|
+
adopt();
|
|
5626
|
+
return pending;
|
|
5627
|
+
}
|
|
5628
|
+
return null;
|
|
5629
|
+
};
|
|
5630
|
+
|
|
5631
|
+
var scope = pickSettingsScope() || MEMORY_SCOPE;
|
|
5632
|
+
if (globalThis.__DFP_PROBE__ === true) {
|
|
5633
|
+
var probeSnapshot = scope.getSnapshot ? scope.getSnapshot() : {};
|
|
5634
|
+
console.log(
|
|
5635
|
+
"[dfp-probe] scope " +
|
|
5636
|
+
JSON.stringify({
|
|
5637
|
+
mode: probeSnapshot.mode,
|
|
5638
|
+
status: probeSnapshot.status,
|
|
5639
|
+
revision: probeSnapshot.revision,
|
|
5640
|
+
writable: probeSnapshot.writable,
|
|
5641
|
+
value: probeSnapshot.value,
|
|
5642
|
+
})
|
|
5643
|
+
);
|
|
5644
|
+
}
|
|
5122
5645
|
var appliedOverrides = "";
|
|
5123
5646
|
|
|
5124
5647
|
/**
|
|
@@ -5191,7 +5714,38 @@ function apply(ctx) {
|
|
|
5191
5714
|
function () {
|
|
5192
5715
|
if (typeof document === "undefined") return undefined;
|
|
5193
5716
|
var lastSignature = "";
|
|
5194
|
-
var
|
|
5717
|
+
var lastGate = null;
|
|
5718
|
+
var ticks = 0;
|
|
5719
|
+
// Reading the base tokens walks every property of every rule of every
|
|
5720
|
+
// stylesheet — far too much to repeat every four seconds forever. This
|
|
5721
|
+
// gate covers everything that moves those tokens in DSH: the theme
|
|
5722
|
+
// writes its content font size inline on `body`, a light/dark switch
|
|
5723
|
+
// swaps the class on `html`, and a lazily loaded view adds a stylesheet.
|
|
5724
|
+
// A full sweep still runs every Nth tick, which is what catches an
|
|
5725
|
+
// in-place rewrite inside the CSS-in-JS sheet.
|
|
5726
|
+
var GATE_TICKS_PER_SWEEP = 8;
|
|
5727
|
+
var gate = function () {
|
|
5728
|
+
var inline = "";
|
|
5729
|
+
var root = null;
|
|
5730
|
+
try {
|
|
5731
|
+
inline = document.body ? document.body.getAttribute("style") || "" : "";
|
|
5732
|
+
root = document.documentElement;
|
|
5733
|
+
} catch (error) {
|
|
5734
|
+
inline = "";
|
|
5735
|
+
}
|
|
5736
|
+
return (
|
|
5737
|
+
inline +
|
|
5738
|
+
"|" +
|
|
5739
|
+
(root ? root.className || "" : "") +
|
|
5740
|
+
"|" +
|
|
5741
|
+
document.styleSheets.length
|
|
5742
|
+
);
|
|
5743
|
+
};
|
|
5744
|
+
var recheck = function (force) {
|
|
5745
|
+
ticks += 1;
|
|
5746
|
+
var current = gate();
|
|
5747
|
+
if (force !== true && ticks % GATE_TICKS_PER_SWEEP !== 0 && current === lastGate) return;
|
|
5748
|
+
lastGate = current;
|
|
5195
5749
|
var next = readBaseTokens();
|
|
5196
5750
|
var signature = "";
|
|
5197
5751
|
var name;
|
|
@@ -5208,7 +5762,9 @@ function apply(ctx) {
|
|
|
5208
5762
|
sync();
|
|
5209
5763
|
};
|
|
5210
5764
|
scheduleRecheck = recheck;
|
|
5211
|
-
var timer = globalThis.setTimeout(
|
|
5765
|
+
var timer = globalThis.setTimeout(function () {
|
|
5766
|
+
recheck(true);
|
|
5767
|
+
}, 500);
|
|
5212
5768
|
var interval = globalThis.setInterval(recheck, 4000);
|
|
5213
5769
|
return function () {
|
|
5214
5770
|
globalThis.clearTimeout(timer);
|
|
@@ -5223,7 +5779,8 @@ function apply(ctx) {
|
|
|
5223
5779
|
// this hook; the polling recheck still covers theme changes there.
|
|
5224
5780
|
if (typeof ctx.on !== "function") return undefined;
|
|
5225
5781
|
return ctx.on("theme/change", function () {
|
|
5226
|
-
|
|
5782
|
+
// Forced: the theme may have moved without touching the cheap gate.
|
|
5783
|
+
if (scheduleRecheck !== null) scheduleRecheck(true);
|
|
5227
5784
|
});
|
|
5228
5785
|
},
|
|
5229
5786
|
"dsh-fonttune: theme change adoption"
|
|
@@ -5238,6 +5795,9 @@ function apply(ctx) {
|
|
|
5238
5795
|
}
|
|
5239
5796
|
return translate(locale, key, params);
|
|
5240
5797
|
};
|
|
5798
|
+
// The card reads these when a seat renders it without the injected face.
|
|
5799
|
+
cardScope = scope;
|
|
5800
|
+
cardT = t;
|
|
5241
5801
|
ctx.effect(
|
|
5242
5802
|
function () {
|
|
5243
5803
|
return ctx.locale.register(NAMESPACE, DICTS);
|
|
@@ -5245,18 +5805,59 @@ function apply(ctx) {
|
|
|
5245
5805
|
"dsh-fonttune: dictionaries"
|
|
5246
5806
|
);
|
|
5247
5807
|
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5808
|
+
/**
|
|
5809
|
+
* Offer the card in one slot seat.
|
|
5810
|
+
*
|
|
5811
|
+
* Which seat a host declares is a line difference, not a preference, and the
|
|
5812
|
+
* three are not alternatives to pick from — each is simply inert where a host
|
|
5813
|
+
* never declares it:
|
|
5814
|
+
*
|
|
5815
|
+
* * `settings.plugin.item` — up to 0.1.5-rc.x, a keyed cell under
|
|
5816
|
+
* Settings → Plugins → Plugin configuration (keyed by the namespace);
|
|
5817
|
+
* * `plugins.item` — 0.1.7-alpha.x's own entry in the Plugins page's
|
|
5818
|
+
* official list, which is how EVERY plugin page is contributed there (the
|
|
5819
|
+
* built-in settings pages do the same), and the seat that needs no
|
|
5820
|
+
* installed package behind it;
|
|
5821
|
+
* * `plugins.bundle.config` — the alpha's per-package page, keyed by the npm
|
|
5822
|
+
* package name, for a profile that installed this plugin as a bundle.
|
|
5823
|
+
*
|
|
5824
|
+
* @param {string} seat - the slot name.
|
|
5825
|
+
* @param {object} options - the registration options that seat looks up.
|
|
5826
|
+
* @param {Function} [component] - the component to register; the card itself
|
|
5827
|
+
* unless a seat hands it props the card does not read.
|
|
5828
|
+
*/
|
|
5829
|
+
var offerCard = function (seat, options, component) {
|
|
5830
|
+
ctx.slots.inject(seat, function () {
|
|
5831
|
+
var registration = {
|
|
5832
|
+
name: seat,
|
|
5833
|
+
locale: NAMESPACE,
|
|
5253
5834
|
inject: function () {
|
|
5254
5835
|
return { scope: scope, t: t };
|
|
5255
5836
|
},
|
|
5837
|
+
};
|
|
5838
|
+
for (var field in options) {
|
|
5839
|
+
if (Object.prototype.hasOwnProperty.call(options, field)) {
|
|
5840
|
+
registration[field] = options[field];
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
return ctx.slots.register(registration, component === undefined ? FontCard : component);
|
|
5844
|
+
});
|
|
5845
|
+
};
|
|
5846
|
+
if (scope !== MEMORY_SCOPE) {
|
|
5847
|
+
offerCard("settings.plugin.item", { key: NAMESPACE });
|
|
5848
|
+
offerCard("plugins.bundle.config", { key: "dsh-fonttune" });
|
|
5849
|
+
offerCard(
|
|
5850
|
+
"plugins.item",
|
|
5851
|
+
{
|
|
5852
|
+
id: "fonttune",
|
|
5853
|
+
order: 40,
|
|
5854
|
+
label: function () {
|
|
5855
|
+
return t("card.title");
|
|
5856
|
+
},
|
|
5256
5857
|
},
|
|
5257
|
-
|
|
5858
|
+
FontCardPage
|
|
5258
5859
|
);
|
|
5259
|
-
}
|
|
5860
|
+
}
|
|
5260
5861
|
}
|
|
5261
5862
|
|
|
5262
5863
|
exports.apply = apply;
|