@nsnanocat/preference-panes 1.2.1 → 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 +3 -3
- package/dist/module/index.html +1 -1
- package/dist/module/index.mjs +105 -12
- package/dist/module/navigation.mjs +8 -8
- package/dist/preference-panes.mjs +105 -12
- package/dist/web.js +1 -1
- package/package.json +1 -1
- package/src/browser/ModuleFrame.mjs +8 -8
- package/src/browser/Navigation.d.mts +14 -0
- 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/components.mjs +14 -0
- package/src/browser/panel.css +8 -0
- package/src/browser/panel.mjs +31 -1
package/README.md
CHANGED
|
@@ -62,9 +62,9 @@ await fetch("/api/set", {
|
|
|
62
62
|
});
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
页面初始化时只执行一次 `POST /api/get` 读取 Settings
|
|
65
|
+
页面初始化时只执行一次 `POST /api/get` 读取 Settings 子树。外部存储中的历史值不会阻断页面加载:当前选项表未定义的值会保留并在对应设置项下提示,无法由控件表示的值会回退到默认值并显示警告;用户的新写入仍执行严格校验。写入成功后仅更新当前页面快照;查看 Settings/Caches 时按需读取,清空和重置通过 `/api/delete` 完成。
|
|
66
66
|
|
|
67
|
-
设置项支持 `type: "url"` 作为只读跳转入口。它的 `val` 必须是带 scheme 的地址,例如 `bilibili://main/top_category
|
|
67
|
+
设置项支持 `type: "url"` 作为只读跳转入口。它的 `val` 必须是带 scheme 的地址,例如 `bilibili://main/top_category`;面板将其渲染为可点击链接,不会发起存储写入。嵌入 `ModuleFrame` 时,宿主可拦截 `open-url` 事件(`detail.url`)并打开链接;未拦截或独立网页中保留链接的默认导航。
|
|
68
68
|
|
|
69
69
|
## 宿主集成
|
|
70
70
|
|
|
@@ -82,7 +82,7 @@ container.append(frame.element);
|
|
|
82
82
|
await frame.load();
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
`ModuleFrame` 接受 `{ signal?: AbortSignal; headers?: HeadersInit }`,使用这些选项发送一次 GET,并将返回 HTML 原样设置为 `iframe.srcdoc`。iframe dataset 只保存模块身份,不保存 CSS 地址。宿主通过 `change`、`confirm`、`notice`
|
|
85
|
+
`ModuleFrame` 接受 `{ signal?: AbortSignal; headers?: HeadersInit }`,使用这些选项发送一次 GET,并将返回 HTML 原样设置为 `iframe.srcdoc`。iframe dataset 只保存模块身份,不保存 CSS 地址。宿主通过 `change`、`confirm`、`notice`、`open-url` 事件同步标题、操作菜单、确认框、提示和链接跳转,不读取或改写 iframe 内部 DOM。项目主页、Bilibili JSBridge、原生导航和视觉样式仍由宿主负责。
|
|
86
86
|
|
|
87
87
|
## 构建与验证
|
|
88
88
|
|
package/dist/module/index.html
CHANGED
package/dist/module/index.mjs
CHANGED
|
@@ -172,18 +172,62 @@ function scalar(value) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
176
|
-
* Check value type
|
|
175
|
+
* 检查值类型与数组结构,不检查选项是否仍在当前配置中。
|
|
176
|
+
* Check value type and array structure without requiring current option membership.
|
|
177
177
|
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
178
|
-
* @param {unknown} value
|
|
179
|
-
* @returns {boolean}
|
|
178
|
+
* @param {unknown} value 待检查值 / Value to inspect.
|
|
179
|
+
* @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
|
|
180
180
|
*/
|
|
181
|
-
function
|
|
181
|
+
function validValueShape(field, value) {
|
|
182
182
|
if (field.type === "array") {
|
|
183
183
|
if (!Array.isArray(value) || value.some(item => !scalar(item)) || new Set(value).size !== value.length) return false;
|
|
184
184
|
} else if (typeof value !== field.type || !scalar(value)) return false;
|
|
185
185
|
if (field.control === "url" && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(?:javascript|data|vbscript):/i.test(value))) return false;
|
|
186
|
-
return
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 返回当前配置未声明的选项值。
|
|
191
|
+
* Return option values absent from the current definition.
|
|
192
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
193
|
+
* @param {unknown} value 已确认结构有效的值 / Value with a valid shape.
|
|
194
|
+
* @returns {unknown[]} 未定义值 / Undefined values.
|
|
195
|
+
*/
|
|
196
|
+
function undefinedOptions(field, value) {
|
|
197
|
+
if (!field.options) return [];
|
|
198
|
+
return (field.type === "array" ? value : [value]).filter(item => !field.options.some(option => option.key === item));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 将外部存储值解析为可渲染值,并把兼容性问题作为字段级警告返回。
|
|
203
|
+
* Resolve external storage into a renderable value and return compatibility issues as field warnings.
|
|
204
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
205
|
+
* @param {unknown} stored 外部存储值 / External stored value.
|
|
206
|
+
* @returns {{value?: unknown, warning?: {kind: "invalid-value" | "undefined-options", values: unknown[]}}} 解析结果 / Resolution result.
|
|
207
|
+
*/
|
|
208
|
+
function resolveStoredValue(field, stored) {
|
|
209
|
+
if (field.control === "url") return { value: field.defaultValue };
|
|
210
|
+
const value = normalizeStoredValue(field, stored === undefined ? field.defaultValue : stored);
|
|
211
|
+
if (value === undefined) return {};
|
|
212
|
+
if (!validValueShape(field, value)) {
|
|
213
|
+
return {
|
|
214
|
+
...(Object.hasOwn(field, "defaultValue") ? { value: field.defaultValue } : {}),
|
|
215
|
+
warning: { kind: "invalid-value", values: [stored] },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const undefinedValues = undefinedOptions(field, value);
|
|
219
|
+
return { value, ...(undefinedValues.length ? { warning: { kind: "undefined-options", values: undefinedValues } } : {}) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 严格检查写入值的类型、数组唯一性及声明选项。
|
|
224
|
+
* Strictly check a write value's type, array uniqueness, and declared options.
|
|
225
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
226
|
+
* @param {unknown} value 待写入的 JSON 值 / JSON value to write.
|
|
227
|
+
* @returns {boolean} 是否允许写入 / Whether the value may be written.
|
|
228
|
+
*/
|
|
229
|
+
function validValue(field, value) {
|
|
230
|
+
return validValueShape(field, value) && undefinedOptions(field, value).length === 0;
|
|
187
231
|
}
|
|
188
232
|
|
|
189
233
|
/**
|
|
@@ -277,6 +321,20 @@ function requestConfirmation(host, message) {
|
|
|
277
321
|
});
|
|
278
322
|
}
|
|
279
323
|
|
|
324
|
+
/**
|
|
325
|
+
* 请求宿主打开 URL;未被接管时保留链接默认导航。
|
|
326
|
+
* Request host URL navigation while preserving the link default when unhandled.
|
|
327
|
+
* @param {Window} host 模块窗口 / Module window.
|
|
328
|
+
* @param {string} url 完整目标地址 / Absolute target URL.
|
|
329
|
+
* @returns {boolean} 宿主是否接管导航 / Whether the host accepted navigation ownership.
|
|
330
|
+
*/
|
|
331
|
+
function requestURLNavigation(host, url) {
|
|
332
|
+
const frame = host.frameElement;
|
|
333
|
+
if (!frame) return false;
|
|
334
|
+
const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:open-url", { cancelable: true, detail: { url } });
|
|
335
|
+
return !frame.dispatchEvent(event);
|
|
336
|
+
}
|
|
337
|
+
|
|
280
338
|
/**
|
|
281
339
|
* 独立页面的三点按钮和底部操作菜单;嵌入页面由宿主提供对应界面。
|
|
282
340
|
* Overflow trigger and bottom action sheet for standalone pages; embedded pages use host-provided chrome.
|
|
@@ -437,6 +495,7 @@ class PreferencesClient {
|
|
|
437
495
|
#timeout;
|
|
438
496
|
#session = new AbortController();
|
|
439
497
|
#values = {};
|
|
498
|
+
#warnings = {};
|
|
440
499
|
#saving = false;
|
|
441
500
|
|
|
442
501
|
/**
|
|
@@ -463,17 +522,18 @@ class PreferencesClient {
|
|
|
463
522
|
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
464
523
|
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
465
524
|
const values = {};
|
|
525
|
+
const warnings = {};
|
|
466
526
|
for (const field of this.#definition.fields) {
|
|
467
527
|
const stored = field.key
|
|
468
528
|
.split(".")
|
|
469
529
|
.slice(this.#definition.settingsPath.length)
|
|
470
530
|
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
471
|
-
const
|
|
472
|
-
if (value
|
|
473
|
-
if (
|
|
474
|
-
values[field.key] = value;
|
|
531
|
+
const resolved = resolveStoredValue(field, stored);
|
|
532
|
+
if (Object.hasOwn(resolved, "value")) values[field.key] = resolved.value;
|
|
533
|
+
if (resolved.warning) warnings[field.key] = resolved.warning;
|
|
475
534
|
}
|
|
476
535
|
this.#values = values;
|
|
536
|
+
this.#warnings = warnings;
|
|
477
537
|
return this.snapshot();
|
|
478
538
|
}
|
|
479
539
|
|
|
@@ -483,7 +543,7 @@ class PreferencesClient {
|
|
|
483
543
|
* @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
|
|
484
544
|
*/
|
|
485
545
|
snapshot() {
|
|
486
|
-
return structuredClone({ definition: this.#definition, values: this.#values });
|
|
546
|
+
return structuredClone({ definition: this.#definition, values: this.#values, warnings: this.#warnings });
|
|
487
547
|
}
|
|
488
548
|
|
|
489
549
|
/**
|
|
@@ -607,15 +667,18 @@ class PreferencesClient {
|
|
|
607
667
|
switch (operation) {
|
|
608
668
|
case "write":
|
|
609
669
|
this.#values[key] = structuredClone(value);
|
|
670
|
+
delete this.#warnings[key];
|
|
610
671
|
break;
|
|
611
672
|
case "delete": {
|
|
612
673
|
delete this.#values[key];
|
|
674
|
+
delete this.#warnings[key];
|
|
613
675
|
if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
|
|
614
676
|
break;
|
|
615
677
|
}
|
|
616
678
|
case "clearCaches":
|
|
617
679
|
break;
|
|
618
680
|
case "reset":
|
|
681
|
+
this.#warnings = {};
|
|
619
682
|
for (const field of this.#definition.fields) {
|
|
620
683
|
delete this.#values[field.key];
|
|
621
684
|
if (Object.hasOwn(field, "defaultValue")) this.#values[field.key] = structuredClone(field.defaultValue);
|
|
@@ -791,6 +854,22 @@ class Navigation extends EventTarget {
|
|
|
791
854
|
}
|
|
792
855
|
}
|
|
793
856
|
|
|
857
|
+
/**
|
|
858
|
+
* 将字段级存储诊断转换为简短提示。
|
|
859
|
+
* Convert a field-level storage diagnostic into concise display text.
|
|
860
|
+
* @param {import("./client.mjs").StoredValueWarning} warning 存储警告 / Storage warning.
|
|
861
|
+
* @returns {string} 用户可见提示 / User-visible warning.
|
|
862
|
+
*/
|
|
863
|
+
function warningMessage(warning) {
|
|
864
|
+
const values = warning.values
|
|
865
|
+
.map(value => {
|
|
866
|
+
if (typeof value === "string") return value;
|
|
867
|
+
return JSON.stringify(value) ?? String(value);
|
|
868
|
+
})
|
|
869
|
+
.join("、");
|
|
870
|
+
return warning.kind === "undefined-options" ? `当前配置未定义值:${values}` : `当前存储值格式不受支持:${values}`;
|
|
871
|
+
}
|
|
872
|
+
|
|
794
873
|
/**
|
|
795
874
|
* 管理模块表单、导航、操作队列和短暂通知。
|
|
796
875
|
* Manage the module form, navigation, operation queue, and transient notifications.
|
|
@@ -969,6 +1048,7 @@ class PreferencesPanel {
|
|
|
969
1048
|
const growingInputs = [];
|
|
970
1049
|
const editors = new Map();
|
|
971
1050
|
const summaries = [];
|
|
1051
|
+
const warningRefreshers = [];
|
|
972
1052
|
const groups = new Map();
|
|
973
1053
|
let queue = Promise.resolve(),
|
|
974
1054
|
pendingWrites = 0;
|
|
@@ -1045,6 +1125,15 @@ class PreferencesPanel {
|
|
|
1045
1125
|
const label = element("div", "pp-label");
|
|
1046
1126
|
label.append(element("span", "pp-field-name", match?.[2] ?? field.name));
|
|
1047
1127
|
if (field.description) label.append(element("span", "pp-field-description", field.description));
|
|
1128
|
+
const warning = element("span", "pp-field-warning");
|
|
1129
|
+
const refreshWarning = () => {
|
|
1130
|
+
const issue = client.snapshot().warnings[field.key];
|
|
1131
|
+
warning.hidden = !issue;
|
|
1132
|
+
warning.textContent = issue ? warningMessage(issue) : "";
|
|
1133
|
+
};
|
|
1134
|
+
warningRefreshers.push(refreshWarning);
|
|
1135
|
+
refreshWarning();
|
|
1136
|
+
label.append(warning);
|
|
1048
1137
|
row.append(label);
|
|
1049
1138
|
const value = values[field.key];
|
|
1050
1139
|
/**
|
|
@@ -1069,6 +1158,9 @@ class PreferencesPanel {
|
|
|
1069
1158
|
write = value => {
|
|
1070
1159
|
link.href = value;
|
|
1071
1160
|
};
|
|
1161
|
+
link.addEventListener("click", event => {
|
|
1162
|
+
if (requestURLNavigation(window, link.href)) event.preventDefault();
|
|
1163
|
+
});
|
|
1072
1164
|
row.append(link);
|
|
1073
1165
|
row.addEventListener("click", event => {
|
|
1074
1166
|
if (!link.contains(event.target)) link.click();
|
|
@@ -1217,6 +1309,7 @@ class PreferencesPanel {
|
|
|
1217
1309
|
},
|
|
1218
1310
|
() => {
|
|
1219
1311
|
for (const refresh of summaries) refresh();
|
|
1312
|
+
for (const refresh of warningRefreshers) refresh();
|
|
1220
1313
|
},
|
|
1221
1314
|
restore,
|
|
1222
1315
|
);
|
|
@@ -1326,7 +1419,7 @@ class PreferencesPanel {
|
|
|
1326
1419
|
}
|
|
1327
1420
|
}
|
|
1328
1421
|
|
|
1329
|
-
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-settings-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-panel a.pp-choice-link {\n color: var(--pp-accent);\n text-decoration: none;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-status {\n position: fixed;\n inset: 0;\n display: grid;\n place-content: center;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n margin: 0;\n padding: 24px;\n color: var(--pp-muted, GrayText);\n text-align: center;\n background: var(--pp-background, Canvas);\n}\n.pp-viewport > .pp-status {\n position: absolute;\n}\n.pp-status-spinner {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n border: 3px solid color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: var(--pp-accent, AccentColor);\n border-radius: 50%;\n animation: pp-status-spin 0.8s linear infinite;\n}\n.pp-status-message {\n max-width: 100%;\n margin: 0;\n overflow-wrap: anywhere;\n}\n.pp-status-action {\n min-width: 96px;\n min-height: 44px;\n padding: 8px 16px;\n border: 0;\n border-radius: 6px;\n color: var(--pp-text, ButtonText);\n font: inherit;\n cursor: pointer;\n background: var(--pp-surface, ButtonFace);\n}\n@keyframes pp-status-spin {\n to {\n transform: rotate(1turn);\n }\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n";
|
|
1422
|
+
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n --pp-warning: #9a6700;\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n --pp-warning: #f0b849;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-settings-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-panel a.pp-choice-link {\n color: var(--pp-accent);\n text-decoration: none;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-field-warning {\n margin-top: 4px;\n color: var(--pp-warning);\n font-size: 12px;\n overflow-wrap: anywhere;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-status {\n position: fixed;\n inset: 0;\n display: grid;\n place-content: center;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n margin: 0;\n padding: 24px;\n color: var(--pp-muted, GrayText);\n text-align: center;\n background: var(--pp-background, Canvas);\n}\n.pp-viewport > .pp-status {\n position: absolute;\n}\n.pp-status-spinner {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n border: 3px solid color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: var(--pp-accent, AccentColor);\n border-radius: 50%;\n animation: pp-status-spin 0.8s linear infinite;\n}\n.pp-status-message {\n max-width: 100%;\n margin: 0;\n overflow-wrap: anywhere;\n}\n.pp-status-action {\n min-width: 96px;\n min-height: 44px;\n padding: 8px 16px;\n border: 0;\n border-radius: 6px;\n color: var(--pp-text, ButtonText);\n font: inherit;\n cursor: pointer;\n background: var(--pp-surface, ButtonFace);\n}\n@keyframes pp-status-spin {\n to {\n transform: rotate(1turn);\n }\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n";
|
|
1330
1423
|
|
|
1331
1424
|
const selector = "style[data-preference-panes-defaults]";
|
|
1332
1425
|
const stylesheetSelector = "link[data-preference-panes-stylesheet]";
|
|
@@ -160,14 +160,12 @@ class ModuleFrame extends EventTarget {
|
|
|
160
160
|
this.#state = { ...event.detail, actions: event.detail.actions ?? [] };
|
|
161
161
|
this.dispatchEvent(new Event("change"));
|
|
162
162
|
};
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
#notice = event =>
|
|
168
|
-
|
|
169
|
-
if (!this.dispatchEvent(notice)) event.preventDefault();
|
|
170
|
-
};
|
|
163
|
+
#forward(type, event) {
|
|
164
|
+
if (!this.dispatchEvent(new CustomEvent(type, { cancelable: true, detail: event.detail }))) event.preventDefault();
|
|
165
|
+
}
|
|
166
|
+
#confirmation = event => this.#forward("confirm", event);
|
|
167
|
+
#notice = event => this.#forward("notice", event);
|
|
168
|
+
#openURL = event => this.#forward("open-url", event);
|
|
171
169
|
|
|
172
170
|
/**
|
|
173
171
|
* 建立 iframe;调用方挂载 element 后调用 load。
|
|
@@ -188,6 +186,7 @@ class ModuleFrame extends EventTarget {
|
|
|
188
186
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
189
187
|
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
190
188
|
this.element.addEventListener("preferencepanes:notice", this.#notice);
|
|
189
|
+
this.element.addEventListener("preferencepanes:open-url", this.#openURL);
|
|
191
190
|
this.#state = { title: match[1], module: match[1], busy: false, canGoBack: true, actions: [] };
|
|
192
191
|
options.signal?.addEventListener("abort", this.#abort, { once: true });
|
|
193
192
|
}
|
|
@@ -250,6 +249,7 @@ class ModuleFrame extends EventTarget {
|
|
|
250
249
|
this.element.removeEventListener("preferencepanes:change", this.#change);
|
|
251
250
|
this.element.removeEventListener("preferencepanes:confirm", this.#confirmation);
|
|
252
251
|
this.element.removeEventListener("preferencepanes:notice", this.#notice);
|
|
252
|
+
this.element.removeEventListener("preferencepanes:open-url", this.#openURL);
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -172,18 +172,62 @@ function scalar(value) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
176
|
-
* Check value type
|
|
175
|
+
* 检查值类型与数组结构,不检查选项是否仍在当前配置中。
|
|
176
|
+
* Check value type and array structure without requiring current option membership.
|
|
177
177
|
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
178
|
-
* @param {unknown} value
|
|
179
|
-
* @returns {boolean}
|
|
178
|
+
* @param {unknown} value 待检查值 / Value to inspect.
|
|
179
|
+
* @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
|
|
180
180
|
*/
|
|
181
|
-
function
|
|
181
|
+
function validValueShape(field, value) {
|
|
182
182
|
if (field.type === "array") {
|
|
183
183
|
if (!Array.isArray(value) || value.some(item => !scalar(item)) || new Set(value).size !== value.length) return false;
|
|
184
184
|
} else if (typeof value !== field.type || !scalar(value)) return false;
|
|
185
185
|
if (field.control === "url" && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(?:javascript|data|vbscript):/i.test(value))) return false;
|
|
186
|
-
return
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 返回当前配置未声明的选项值。
|
|
191
|
+
* Return option values absent from the current definition.
|
|
192
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
193
|
+
* @param {unknown} value 已确认结构有效的值 / Value with a valid shape.
|
|
194
|
+
* @returns {unknown[]} 未定义值 / Undefined values.
|
|
195
|
+
*/
|
|
196
|
+
function undefinedOptions(field, value) {
|
|
197
|
+
if (!field.options) return [];
|
|
198
|
+
return (field.type === "array" ? value : [value]).filter(item => !field.options.some(option => option.key === item));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 将外部存储值解析为可渲染值,并把兼容性问题作为字段级警告返回。
|
|
203
|
+
* Resolve external storage into a renderable value and return compatibility issues as field warnings.
|
|
204
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
205
|
+
* @param {unknown} stored 外部存储值 / External stored value.
|
|
206
|
+
* @returns {{value?: unknown, warning?: {kind: "invalid-value" | "undefined-options", values: unknown[]}}} 解析结果 / Resolution result.
|
|
207
|
+
*/
|
|
208
|
+
function resolveStoredValue(field, stored) {
|
|
209
|
+
if (field.control === "url") return { value: field.defaultValue };
|
|
210
|
+
const value = normalizeStoredValue(field, stored === undefined ? field.defaultValue : stored);
|
|
211
|
+
if (value === undefined) return {};
|
|
212
|
+
if (!validValueShape(field, value)) {
|
|
213
|
+
return {
|
|
214
|
+
...(Object.hasOwn(field, "defaultValue") ? { value: field.defaultValue } : {}),
|
|
215
|
+
warning: { kind: "invalid-value", values: [stored] },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const undefinedValues = undefinedOptions(field, value);
|
|
219
|
+
return { value, ...(undefinedValues.length ? { warning: { kind: "undefined-options", values: undefinedValues } } : {}) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 严格检查写入值的类型、数组唯一性及声明选项。
|
|
224
|
+
* Strictly check a write value's type, array uniqueness, and declared options.
|
|
225
|
+
* @param {import("../index.js").SettingsField} field 前端归一化字段 / Normalized frontend field.
|
|
226
|
+
* @param {unknown} value 待写入的 JSON 值 / JSON value to write.
|
|
227
|
+
* @returns {boolean} 是否允许写入 / Whether the value may be written.
|
|
228
|
+
*/
|
|
229
|
+
function validValue(field, value) {
|
|
230
|
+
return validValueShape(field, value) && undefinedOptions(field, value).length === 0;
|
|
187
231
|
}
|
|
188
232
|
|
|
189
233
|
/**
|
|
@@ -277,6 +321,20 @@ function requestConfirmation(host, message) {
|
|
|
277
321
|
});
|
|
278
322
|
}
|
|
279
323
|
|
|
324
|
+
/**
|
|
325
|
+
* 请求宿主打开 URL;未被接管时保留链接默认导航。
|
|
326
|
+
* Request host URL navigation while preserving the link default when unhandled.
|
|
327
|
+
* @param {Window} host 模块窗口 / Module window.
|
|
328
|
+
* @param {string} url 完整目标地址 / Absolute target URL.
|
|
329
|
+
* @returns {boolean} 宿主是否接管导航 / Whether the host accepted navigation ownership.
|
|
330
|
+
*/
|
|
331
|
+
function requestURLNavigation(host, url) {
|
|
332
|
+
const frame = host.frameElement;
|
|
333
|
+
if (!frame) return false;
|
|
334
|
+
const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:open-url", { cancelable: true, detail: { url } });
|
|
335
|
+
return !frame.dispatchEvent(event);
|
|
336
|
+
}
|
|
337
|
+
|
|
280
338
|
/**
|
|
281
339
|
* 独立页面的三点按钮和底部操作菜单;嵌入页面由宿主提供对应界面。
|
|
282
340
|
* Overflow trigger and bottom action sheet for standalone pages; embedded pages use host-provided chrome.
|
|
@@ -437,6 +495,7 @@ class PreferencesClient {
|
|
|
437
495
|
#timeout;
|
|
438
496
|
#session = new AbortController();
|
|
439
497
|
#values = {};
|
|
498
|
+
#warnings = {};
|
|
440
499
|
#saving = false;
|
|
441
500
|
|
|
442
501
|
/**
|
|
@@ -463,17 +522,18 @@ class PreferencesClient {
|
|
|
463
522
|
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
464
523
|
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
465
524
|
const values = {};
|
|
525
|
+
const warnings = {};
|
|
466
526
|
for (const field of this.#definition.fields) {
|
|
467
527
|
const stored = field.key
|
|
468
528
|
.split(".")
|
|
469
529
|
.slice(this.#definition.settingsPath.length)
|
|
470
530
|
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
471
|
-
const
|
|
472
|
-
if (value
|
|
473
|
-
if (
|
|
474
|
-
values[field.key] = value;
|
|
531
|
+
const resolved = resolveStoredValue(field, stored);
|
|
532
|
+
if (Object.hasOwn(resolved, "value")) values[field.key] = resolved.value;
|
|
533
|
+
if (resolved.warning) warnings[field.key] = resolved.warning;
|
|
475
534
|
}
|
|
476
535
|
this.#values = values;
|
|
536
|
+
this.#warnings = warnings;
|
|
477
537
|
return this.snapshot();
|
|
478
538
|
}
|
|
479
539
|
|
|
@@ -483,7 +543,7 @@ class PreferencesClient {
|
|
|
483
543
|
* @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
|
|
484
544
|
*/
|
|
485
545
|
snapshot() {
|
|
486
|
-
return structuredClone({ definition: this.#definition, values: this.#values });
|
|
546
|
+
return structuredClone({ definition: this.#definition, values: this.#values, warnings: this.#warnings });
|
|
487
547
|
}
|
|
488
548
|
|
|
489
549
|
/**
|
|
@@ -607,15 +667,18 @@ class PreferencesClient {
|
|
|
607
667
|
switch (operation) {
|
|
608
668
|
case "write":
|
|
609
669
|
this.#values[key] = structuredClone(value);
|
|
670
|
+
delete this.#warnings[key];
|
|
610
671
|
break;
|
|
611
672
|
case "delete": {
|
|
612
673
|
delete this.#values[key];
|
|
674
|
+
delete this.#warnings[key];
|
|
613
675
|
if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
|
|
614
676
|
break;
|
|
615
677
|
}
|
|
616
678
|
case "clearCaches":
|
|
617
679
|
break;
|
|
618
680
|
case "reset":
|
|
681
|
+
this.#warnings = {};
|
|
619
682
|
for (const field of this.#definition.fields) {
|
|
620
683
|
delete this.#values[field.key];
|
|
621
684
|
if (Object.hasOwn(field, "defaultValue")) this.#values[field.key] = structuredClone(field.defaultValue);
|
|
@@ -791,6 +854,22 @@ class Navigation extends EventTarget {
|
|
|
791
854
|
}
|
|
792
855
|
}
|
|
793
856
|
|
|
857
|
+
/**
|
|
858
|
+
* 将字段级存储诊断转换为简短提示。
|
|
859
|
+
* Convert a field-level storage diagnostic into concise display text.
|
|
860
|
+
* @param {import("./client.mjs").StoredValueWarning} warning 存储警告 / Storage warning.
|
|
861
|
+
* @returns {string} 用户可见提示 / User-visible warning.
|
|
862
|
+
*/
|
|
863
|
+
function warningMessage(warning) {
|
|
864
|
+
const values = warning.values
|
|
865
|
+
.map(value => {
|
|
866
|
+
if (typeof value === "string") return value;
|
|
867
|
+
return JSON.stringify(value) ?? String(value);
|
|
868
|
+
})
|
|
869
|
+
.join("、");
|
|
870
|
+
return warning.kind === "undefined-options" ? `当前配置未定义值:${values}` : `当前存储值格式不受支持:${values}`;
|
|
871
|
+
}
|
|
872
|
+
|
|
794
873
|
/**
|
|
795
874
|
* 管理模块表单、导航、操作队列和短暂通知。
|
|
796
875
|
* Manage the module form, navigation, operation queue, and transient notifications.
|
|
@@ -969,6 +1048,7 @@ class PreferencesPanel {
|
|
|
969
1048
|
const growingInputs = [];
|
|
970
1049
|
const editors = new Map();
|
|
971
1050
|
const summaries = [];
|
|
1051
|
+
const warningRefreshers = [];
|
|
972
1052
|
const groups = new Map();
|
|
973
1053
|
let queue = Promise.resolve(),
|
|
974
1054
|
pendingWrites = 0;
|
|
@@ -1045,6 +1125,15 @@ class PreferencesPanel {
|
|
|
1045
1125
|
const label = element("div", "pp-label");
|
|
1046
1126
|
label.append(element("span", "pp-field-name", match?.[2] ?? field.name));
|
|
1047
1127
|
if (field.description) label.append(element("span", "pp-field-description", field.description));
|
|
1128
|
+
const warning = element("span", "pp-field-warning");
|
|
1129
|
+
const refreshWarning = () => {
|
|
1130
|
+
const issue = client.snapshot().warnings[field.key];
|
|
1131
|
+
warning.hidden = !issue;
|
|
1132
|
+
warning.textContent = issue ? warningMessage(issue) : "";
|
|
1133
|
+
};
|
|
1134
|
+
warningRefreshers.push(refreshWarning);
|
|
1135
|
+
refreshWarning();
|
|
1136
|
+
label.append(warning);
|
|
1048
1137
|
row.append(label);
|
|
1049
1138
|
const value = values[field.key];
|
|
1050
1139
|
/**
|
|
@@ -1069,6 +1158,9 @@ class PreferencesPanel {
|
|
|
1069
1158
|
write = value => {
|
|
1070
1159
|
link.href = value;
|
|
1071
1160
|
};
|
|
1161
|
+
link.addEventListener("click", event => {
|
|
1162
|
+
if (requestURLNavigation(window, link.href)) event.preventDefault();
|
|
1163
|
+
});
|
|
1072
1164
|
row.append(link);
|
|
1073
1165
|
row.addEventListener("click", event => {
|
|
1074
1166
|
if (!link.contains(event.target)) link.click();
|
|
@@ -1217,6 +1309,7 @@ class PreferencesPanel {
|
|
|
1217
1309
|
},
|
|
1218
1310
|
() => {
|
|
1219
1311
|
for (const refresh of summaries) refresh();
|
|
1312
|
+
for (const refresh of warningRefreshers) refresh();
|
|
1220
1313
|
},
|
|
1221
1314
|
restore,
|
|
1222
1315
|
);
|
|
@@ -1326,7 +1419,7 @@ class PreferencesPanel {
|
|
|
1326
1419
|
}
|
|
1327
1420
|
}
|
|
1328
1421
|
|
|
1329
|
-
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-settings-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-panel a.pp-choice-link {\n color: var(--pp-accent);\n text-decoration: none;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-status {\n position: fixed;\n inset: 0;\n display: grid;\n place-content: center;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n margin: 0;\n padding: 24px;\n color: var(--pp-muted, GrayText);\n text-align: center;\n background: var(--pp-background, Canvas);\n}\n.pp-viewport > .pp-status {\n position: absolute;\n}\n.pp-status-spinner {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n border: 3px solid color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: var(--pp-accent, AccentColor);\n border-radius: 50%;\n animation: pp-status-spin 0.8s linear infinite;\n}\n.pp-status-message {\n max-width: 100%;\n margin: 0;\n overflow-wrap: anywhere;\n}\n.pp-status-action {\n min-width: 96px;\n min-height: 44px;\n padding: 8px 16px;\n border: 0;\n border-radius: 6px;\n color: var(--pp-text, ButtonText);\n font: inherit;\n cursor: pointer;\n background: var(--pp-surface, ButtonFace);\n}\n@keyframes pp-status-spin {\n to {\n transform: rotate(1turn);\n }\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n";
|
|
1422
|
+
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n --pp-warning: #9a6700;\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n --pp-warning: #f0b849;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-settings-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-panel a.pp-choice-link {\n color: var(--pp-accent);\n text-decoration: none;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-field-warning {\n margin-top: 4px;\n color: var(--pp-warning);\n font-size: 12px;\n overflow-wrap: anywhere;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-status {\n position: fixed;\n inset: 0;\n display: grid;\n place-content: center;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n margin: 0;\n padding: 24px;\n color: var(--pp-muted, GrayText);\n text-align: center;\n background: var(--pp-background, Canvas);\n}\n.pp-viewport > .pp-status {\n position: absolute;\n}\n.pp-status-spinner {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n border: 3px solid color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: var(--pp-accent, AccentColor);\n border-radius: 50%;\n animation: pp-status-spin 0.8s linear infinite;\n}\n.pp-status-message {\n max-width: 100%;\n margin: 0;\n overflow-wrap: anywhere;\n}\n.pp-status-action {\n min-width: 96px;\n min-height: 44px;\n padding: 8px 16px;\n border: 0;\n border-radius: 6px;\n color: var(--pp-text, ButtonText);\n font: inherit;\n cursor: pointer;\n background: var(--pp-surface, ButtonFace);\n}\n@keyframes pp-status-spin {\n to {\n transform: rotate(1turn);\n }\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n";
|
|
1330
1423
|
|
|
1331
1424
|
const selector = "style[data-preference-panes-defaults]";
|
|
1332
1425
|
const stylesheetSelector = "link[data-preference-panes-stylesheet]";
|