@nsnanocat/preference-panes 1.2.2 → 1.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/README.md +1 -1
- package/dist/module/index.html +1 -1
- package/dist/module/index.mjs +88 -12
- package/dist/preference-panes.mjs +88 -12
- package/dist/web.js +1 -1
- package/package.json +1 -1
- package/src/browser/boxjs.mjs +50 -6
- package/src/browser/client.d.mts +12 -0
- package/src/browser/client.mjs +11 -6
- package/src/browser/panel.css +8 -0
- package/src/browser/panel.mjs +27 -0
package/src/browser/boxjs.mjs
CHANGED
|
@@ -164,16 +164,60 @@ function scalar(value) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
-
*
|
|
168
|
-
* Check value type
|
|
167
|
+
* 检查值类型与数组结构,不检查选项是否仍在当前配置中。
|
|
168
|
+
* Check value type and array structure without requiring current option membership.
|
|
169
169
|
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
170
|
-
* @param {unknown} value
|
|
171
|
-
* @returns {boolean}
|
|
170
|
+
* @param {unknown} value 待检查值 / Value to inspect.
|
|
171
|
+
* @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
|
|
172
172
|
*/
|
|
173
|
-
|
|
173
|
+
function validValueShape(field, value) {
|
|
174
174
|
if (field.type === "array") {
|
|
175
175
|
if (!Array.isArray(value) || value.some(item => !scalar(item)) || new Set(value).size !== value.length) return false;
|
|
176
176
|
} else if (typeof value !== field.type || !scalar(value)) return false;
|
|
177
177
|
if (field.control === "url" && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(?:javascript|data|vbscript):/i.test(value))) return false;
|
|
178
|
-
return
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 返回当前配置未声明的选项值。
|
|
183
|
+
* Return option values absent from the current definition.
|
|
184
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
185
|
+
* @param {unknown} value 已确认结构有效的值 / Value with a valid shape.
|
|
186
|
+
* @returns {unknown[]} 未定义值 / Undefined values.
|
|
187
|
+
*/
|
|
188
|
+
function undefinedOptions(field, value) {
|
|
189
|
+
if (!field.options) return [];
|
|
190
|
+
return (field.type === "array" ? value : [value]).filter(item => !field.options.some(option => option.key === item));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 将外部存储值解析为可渲染值,并把兼容性问题作为字段级警告返回。
|
|
195
|
+
* Resolve external storage into a renderable value and return compatibility issues as field warnings.
|
|
196
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
197
|
+
* @param {unknown} stored 外部存储值 / External stored value.
|
|
198
|
+
* @returns {{value?: unknown, warning?: {kind: "invalid-value" | "undefined-options", values: unknown[]}}} 解析结果 / Resolution result.
|
|
199
|
+
*/
|
|
200
|
+
export function resolveStoredValue(field, stored) {
|
|
201
|
+
if (field.control === "url") return { value: field.defaultValue };
|
|
202
|
+
const value = normalizeStoredValue(field, stored === undefined ? field.defaultValue : stored);
|
|
203
|
+
if (value === undefined) return {};
|
|
204
|
+
if (!validValueShape(field, value)) {
|
|
205
|
+
return {
|
|
206
|
+
...(Object.hasOwn(field, "defaultValue") ? { value: field.defaultValue } : {}),
|
|
207
|
+
warning: { kind: "invalid-value", values: [stored] },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const undefinedValues = undefinedOptions(field, value);
|
|
211
|
+
return { value, ...(undefinedValues.length ? { warning: { kind: "undefined-options", values: undefinedValues } } : {}) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 严格检查写入值的类型、数组唯一性及声明选项。
|
|
216
|
+
* Strictly check a write value's type, array uniqueness, and declared options.
|
|
217
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
218
|
+
* @param {unknown} value 待写入的 JSON 值 / JSON value to write.
|
|
219
|
+
* @returns {boolean} 是否允许写入 / Whether the value may be written.
|
|
220
|
+
*/
|
|
221
|
+
export function validValue(field, value) {
|
|
222
|
+
return validValueShape(field, value) && undefinedOptions(field, value).length === 0;
|
|
179
223
|
}
|
package/src/browser/client.d.mts
CHANGED
|
@@ -29,6 +29,16 @@ export interface PreferencesClientOptions {
|
|
|
29
29
|
/** 请求超时毫秒数 / Request timeout in milliseconds. */
|
|
30
30
|
timeout?: number;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* 外部存储值与当前字段定义不完全兼容时的非阻断诊断。
|
|
34
|
+
* Non-blocking diagnostic for external storage that no longer fully matches a field definition.
|
|
35
|
+
*/
|
|
36
|
+
export interface StoredValueWarning {
|
|
37
|
+
/** 警告类别 / Warning kind. */
|
|
38
|
+
kind: "invalid-value" | "undefined-options";
|
|
39
|
+
/** 触发警告的原始值或未定义选项 / Raw value or undefined options that triggered the warning. */
|
|
40
|
+
values: unknown[];
|
|
41
|
+
}
|
|
32
42
|
/**
|
|
33
43
|
* 会话的深拷贝快照。
|
|
34
44
|
* Deep-cloned session snapshot.
|
|
@@ -38,6 +48,8 @@ export interface ModuleSnapshot {
|
|
|
38
48
|
definition: ModuleDefinition;
|
|
39
49
|
/** 当前页面值 / Current page values. */
|
|
40
50
|
values: Record<string, SettingsScalar | SettingsScalar[] | null>;
|
|
51
|
+
/** 按字段路径索引的非阻断存储警告 / Non-blocking storage warnings keyed by field path. */
|
|
52
|
+
warnings: Record<string, StoredValueWarning>;
|
|
41
53
|
}
|
|
42
54
|
/**
|
|
43
55
|
* 管理单模块页面的 API 请求、值快照和会话终止。
|
package/src/browser/client.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveStoredValue, validValue } from "./boxjs.mjs";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 管理单模块页面的 API 请求、值快照和会话终止。
|
|
@@ -12,6 +12,7 @@ export class PreferencesClient {
|
|
|
12
12
|
#timeout;
|
|
13
13
|
#session = new AbortController();
|
|
14
14
|
#values = {};
|
|
15
|
+
#warnings = {};
|
|
15
16
|
#saving = false;
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -38,17 +39,18 @@ export class PreferencesClient {
|
|
|
38
39
|
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
39
40
|
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
40
41
|
const values = {};
|
|
42
|
+
const warnings = {};
|
|
41
43
|
for (const field of this.#definition.fields) {
|
|
42
44
|
const stored = field.key
|
|
43
45
|
.split(".")
|
|
44
46
|
.slice(this.#definition.settingsPath.length)
|
|
45
47
|
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
46
|
-
const
|
|
47
|
-
if (value
|
|
48
|
-
if (
|
|
49
|
-
values[field.key] = value;
|
|
48
|
+
const resolved = resolveStoredValue(field, stored);
|
|
49
|
+
if (Object.hasOwn(resolved, "value")) values[field.key] = resolved.value;
|
|
50
|
+
if (resolved.warning) warnings[field.key] = resolved.warning;
|
|
50
51
|
}
|
|
51
52
|
this.#values = values;
|
|
53
|
+
this.#warnings = warnings;
|
|
52
54
|
return this.snapshot();
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -58,7 +60,7 @@ export class PreferencesClient {
|
|
|
58
60
|
* @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
|
|
59
61
|
*/
|
|
60
62
|
snapshot() {
|
|
61
|
-
return structuredClone({ definition: this.#definition, values: this.#values });
|
|
63
|
+
return structuredClone({ definition: this.#definition, values: this.#values, warnings: this.#warnings });
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
@@ -182,15 +184,18 @@ export class PreferencesClient {
|
|
|
182
184
|
switch (operation) {
|
|
183
185
|
case "write":
|
|
184
186
|
this.#values[key] = structuredClone(value);
|
|
187
|
+
delete this.#warnings[key];
|
|
185
188
|
break;
|
|
186
189
|
case "delete": {
|
|
187
190
|
delete this.#values[key];
|
|
191
|
+
delete this.#warnings[key];
|
|
188
192
|
if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
|
|
189
193
|
break;
|
|
190
194
|
}
|
|
191
195
|
case "clearCaches":
|
|
192
196
|
break;
|
|
193
197
|
case "reset":
|
|
198
|
+
this.#warnings = {};
|
|
194
199
|
for (const field of this.#definition.fields) {
|
|
195
200
|
delete this.#values[field.key];
|
|
196
201
|
if (Object.hasOwn(field, "defaultValue")) this.#values[field.key] = structuredClone(field.defaultValue);
|
package/src/browser/panel.css
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
--pp-border: #e3e5e7;
|
|
9
9
|
--pp-muted: #797f87;
|
|
10
10
|
--pp-accent: #1677ff;
|
|
11
|
+
--pp-warning: #9a6700;
|
|
11
12
|
font:
|
|
12
13
|
15px / 1.5 -apple-system,
|
|
13
14
|
BlinkMacSystemFont,
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
--pp-field: #2f3238;
|
|
33
34
|
--pp-border: #2f3238;
|
|
34
35
|
--pp-muted: #9499a0;
|
|
36
|
+
--pp-warning: #f0b849;
|
|
35
37
|
}
|
|
36
38
|
.pp-panel * {
|
|
37
39
|
box-sizing: border-box;
|
|
@@ -175,6 +177,12 @@
|
|
|
175
177
|
color: var(--pp-muted);
|
|
176
178
|
font-size: 12px;
|
|
177
179
|
}
|
|
180
|
+
.pp-field-warning {
|
|
181
|
+
margin-top: 4px;
|
|
182
|
+
color: var(--pp-warning);
|
|
183
|
+
font-size: 12px;
|
|
184
|
+
overflow-wrap: anywhere;
|
|
185
|
+
}
|
|
178
186
|
.pp-group {
|
|
179
187
|
margin-top: 16px;
|
|
180
188
|
}
|
package/src/browser/panel.mjs
CHANGED
|
@@ -4,6 +4,22 @@ import { PreferencesClient } from "./client.mjs";
|
|
|
4
4
|
import { fieldControl, element as node, requestConfirmation, requestURLNavigation, resourceURL, settingRow, statusView } from "./components.mjs";
|
|
5
5
|
import { Navigation } from "./Navigation.mjs";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 将字段级存储诊断转换为简短提示。
|
|
9
|
+
* Convert a field-level storage diagnostic into concise display text.
|
|
10
|
+
* @param {import("./client.mjs").StoredValueWarning} warning 存储警告 / Storage warning.
|
|
11
|
+
* @returns {string} 用户可见提示 / User-visible warning.
|
|
12
|
+
*/
|
|
13
|
+
function warningMessage(warning) {
|
|
14
|
+
const values = warning.values
|
|
15
|
+
.map(value => {
|
|
16
|
+
if (typeof value === "string") return value;
|
|
17
|
+
return JSON.stringify(value) ?? String(value);
|
|
18
|
+
})
|
|
19
|
+
.join("、");
|
|
20
|
+
return warning.kind === "undefined-options" ? `当前配置未定义值:${values}` : `当前存储值格式不受支持:${values}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
/**
|
|
8
24
|
* 管理模块表单、导航、操作队列和短暂通知。
|
|
9
25
|
* Manage the module form, navigation, operation queue, and transient notifications.
|
|
@@ -182,6 +198,7 @@ export class PreferencesPanel {
|
|
|
182
198
|
const growingInputs = [];
|
|
183
199
|
const editors = new Map();
|
|
184
200
|
const summaries = [];
|
|
201
|
+
const warningRefreshers = [];
|
|
185
202
|
const groups = new Map();
|
|
186
203
|
let queue = Promise.resolve(),
|
|
187
204
|
pendingWrites = 0;
|
|
@@ -258,6 +275,15 @@ export class PreferencesPanel {
|
|
|
258
275
|
const label = node("div", "pp-label");
|
|
259
276
|
label.append(node("span", "pp-field-name", match?.[2] ?? field.name));
|
|
260
277
|
if (field.description) label.append(node("span", "pp-field-description", field.description));
|
|
278
|
+
const warning = node("span", "pp-field-warning");
|
|
279
|
+
const refreshWarning = () => {
|
|
280
|
+
const issue = client.snapshot().warnings[field.key];
|
|
281
|
+
warning.hidden = !issue;
|
|
282
|
+
warning.textContent = issue ? warningMessage(issue) : "";
|
|
283
|
+
};
|
|
284
|
+
warningRefreshers.push(refreshWarning);
|
|
285
|
+
refreshWarning();
|
|
286
|
+
label.append(warning);
|
|
261
287
|
row.append(label);
|
|
262
288
|
const value = values[field.key];
|
|
263
289
|
/**
|
|
@@ -434,6 +460,7 @@ export class PreferencesPanel {
|
|
|
434
460
|
},
|
|
435
461
|
() => {
|
|
436
462
|
for (const refresh of summaries) refresh();
|
|
463
|
+
for (const refresh of warningRefreshers) refresh();
|
|
437
464
|
},
|
|
438
465
|
restore,
|
|
439
466
|
);
|