@nsnanocat/preference-panes 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,15 @@
1
1
  <!doctype html>
2
- <html lang="zh-CN">
2
+ <html lang="zh-CN" class="pp-page">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
6
  <meta name="color-scheme" content="light dark">
7
7
 
8
8
  <title>Module Preferences</title>
9
- <style>body { margin: 0; }</style>
10
9
 
11
10
  </head>
12
11
  <body>
13
12
  <main id="preferences"></main>
14
- <script type="module" src="/settings/assets/index.mjs?v=1.2.5"></script>
13
+ <script type="module" src="/settings/assets/index.mjs?v=1.3.0"></script>
15
14
  </body>
16
15
  </html>
@@ -27,6 +27,7 @@ function normalizeBoxJs(config, module) {
27
27
  if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
28
28
  for (const entry of app.settings) {
29
29
  if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
30
+ entry.id = entry.id.replace(/\[(0|[1-9]\d*)\]$/, ".$1");
30
31
  if (!entry.id.startsWith("@")) {
31
32
  if (Array.isArray(document)) throw new TypeError("BoxJS settings require @root.path IDs");
32
33
  continue;
@@ -68,7 +69,7 @@ function normalizeBoxJs(config, module) {
68
69
  };
69
70
  if (type === "select" && !["string", "number", "boolean"].includes(field.type)) throw new TypeError(`Select requires a scalar val: ${entry.id}`);
70
71
  if (entry.items) field.options = entry.items.map(item => ({ key: item.key, label: item.label }));
71
- if (Object.hasOwn(entry, "val")) field.defaultValue = normalizeStoredValue(field, entry.val);
72
+ if (Object.prototype.hasOwnProperty.call(entry, "val")) field.defaultValue = normalizeStoredValue(field, entry.val);
72
73
  if (
73
74
  typeof field.name !== "string" ||
74
75
  (field.placeholder !== undefined && typeof field.placeholder !== "string") ||
@@ -78,18 +79,42 @@ function normalizeBoxJs(config, module) {
78
79
  )
79
80
  throw new TypeError(`Invalid or overlapping BoxJS field: ${entry.id}`);
80
81
  if (field.options && (new Set(field.options.map(item => item.key)).size !== field.options.length || field.options.some(item => !scalar(item.key) || typeof item.label !== "string"))) throw new TypeError(`Invalid options: ${entry.id}`);
81
- if (Object.hasOwn(field, "defaultValue") && !validValue(field, field.defaultValue)) throw new TypeError(`Invalid BoxJS val: ${entry.id}`);
82
- if (field.control === "url" && !Object.hasOwn(field, "defaultValue")) throw new TypeError(`URL BoxJS settings require a val: ${entry.id}`);
82
+ if (Object.prototype.hasOwnProperty.call(field, "defaultValue") && !validValue(field, field.defaultValue)) throw new TypeError(`Invalid BoxJS val: ${entry.id}`);
83
+ if (field.control === "url" && !Object.prototype.hasOwnProperty.call(field, "defaultValue")) throw new TypeError(`URL BoxJS settings require a val: ${entry.id}`);
83
84
  fields.push(field);
84
85
  }
85
86
  if (!fields.length) throw new TypeError(`No BoxJS settings for module: ${target.module}`);
86
- const common = fields[0].key.split(".").slice(0, -1);
87
+ for (const field of fields) {
88
+ const indexed = /^(.*)\.(\d+)$/.exec(field.key);
89
+ if (!indexed) continue;
90
+ const siblings = fields.filter(candidate => candidate.key.startsWith(`${indexed[1]}.`) && /^\d+$/.test(candidate.key.slice(indexed[1].length + 1)));
91
+ if (fields.some(candidate => candidate.key.startsWith(`${indexed[1]}.`) && !siblings.includes(candidate))) throw new TypeError(`Indexed settings cannot mix array entries and named children: ${indexed[1]}`);
92
+ if (siblings.some(candidate => !Object.prototype.hasOwnProperty.call(candidate, "defaultValue")) || !siblings.some(candidate => candidate.key === `${indexed[1]}.${siblings.length - 1}`) || siblings.some(candidate => Number(candidate.key.slice(indexed[1].length + 1)) >= siblings.length))
93
+ throw new TypeError(`Indexed settings require contiguous indices and defaults: ${indexed[1]}`);
94
+ }
95
+ const owner = target.owners.size === 1 ? [...target.owners][0] : undefined;
96
+ const cachePath = owner?.cachePath ?? `@${target.storageKey}.${target.module}.Caches`;
97
+ if (typeof cachePath !== "string" || !cachePath.startsWith(`@${target.storageKey}.`)) throw new TypeError("cachePath must use the module storage root");
98
+ const cacheParts = validatePathParts(cachePath.slice(target.storageKey.length + 2).split("."));
99
+ if (cacheParts.length !== 2 || cacheParts[1] !== "Caches") throw new TypeError("cachePath must identify a module Caches subtree");
100
+ if (owner?.resetPaths !== undefined) {
101
+ if (!Array.isArray(owner.resetPaths) || !owner.resetPaths.length) throw new TypeError("resetPaths must be a nonempty array");
102
+ for (const path of owner.resetPaths) {
103
+ if (typeof path !== "string" || !path.startsWith(`@${target.storageKey}.${target.module}.Settings.`)) throw new TypeError("resetPaths must stay in this module's Settings");
104
+ validatePathParts(path.slice(target.storageKey.length + 2).split("."));
105
+ }
106
+ if (fields.some(field => !owner.resetPaths.some(path => `@${target.storageKey}.${field.key}` === path || `@${target.storageKey}.${field.key}`.startsWith(`${path}.`)))) throw new TypeError("resetPaths must cover the page settings");
107
+ }
108
+ const firstIndexed = /^(.*)\.(\d+)$/.exec(fields[0].key);
109
+ const common = (firstIndexed ? firstIndexed[1] : fields[0].key).split(".").slice(0, -1);
87
110
  for (const field of fields) while (!field.key.startsWith(`${common.join(".")}.`)) common.pop();
88
111
  return {
89
112
  module: target.module,
90
113
  storageKey: target.storageKey,
91
114
  fields,
92
115
  settingsPath: common,
116
+ ...(owner?.cachePath === undefined ? {} : { cachePath }),
117
+ ...(owner?.resetPaths === undefined ? {} : { resetPaths: [...owner.resetPaths] }),
93
118
  ...(Object.keys(metadata).length ? { metadata } : {}),
94
119
  };
95
120
  }
@@ -179,9 +204,14 @@ function scalar(value) {
179
204
  * @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
180
205
  */
181
206
  function validValueShape(field, value) {
182
- if (field.type === "array") {
183
- if (!Array.isArray(value) || value.some(item => !scalar(item)) || new Set(value).size !== value.length) return false;
184
- } else if (typeof value !== field.type || !scalar(value)) return false;
207
+ switch (field.type) {
208
+ case "array":
209
+ if (!Array.isArray(value) || value.some(item => !scalar(item)) || new Set(value).size !== value.length) return false;
210
+ break;
211
+ default:
212
+ if (typeof value !== field.type || !scalar(value)) return false;
213
+ break;
214
+ }
185
215
  if (field.control === "url" && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(?:javascript|data|vbscript):/i.test(value))) return false;
186
216
  return true;
187
217
  }
@@ -211,7 +241,7 @@ function resolveStoredValue(field, stored) {
211
241
  if (value === undefined) return {};
212
242
  if (!validValueShape(field, value)) {
213
243
  return {
214
- ...(Object.hasOwn(field, "defaultValue") ? { value: field.defaultValue } : {}),
244
+ ...(Object.prototype.hasOwnProperty.call(field, "defaultValue") ? { value: field.defaultValue } : {}),
215
245
  warning: { kind: "invalid-value", values: [stored] },
216
246
  };
217
247
  }
@@ -497,6 +527,7 @@ class PreferencesClient {
497
527
  #values = {};
498
528
  #warnings = {};
499
529
  #saving = false;
530
+ #arrays = new Map();
500
531
 
501
532
  /**
502
533
  * 创建从 BoxJS 定义读取和持久化设置的页面客户端。
@@ -523,13 +554,22 @@ class PreferencesClient {
523
554
  if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
524
555
  const values = {};
525
556
  const warnings = {};
557
+ this.#arrays.clear();
526
558
  for (const field of this.#definition.fields) {
559
+ const indexed = /^(.*)\.(\d+)$/.exec(field.key);
560
+ if (indexed && !this.#arrays.has(indexed[1])) {
561
+ const storedArray = indexed[1]
562
+ .split(".")
563
+ .slice(this.#definition.settingsPath.length)
564
+ .reduce((parent, part) => Object(parent)[part], subtree);
565
+ this.#arrays.set(indexed[1], Array.isArray(storedArray) ? [...storedArray] : []);
566
+ }
527
567
  const stored = field.key
528
568
  .split(".")
529
569
  .slice(this.#definition.settingsPath.length)
530
570
  .reduce((parent, part) => Object(parent)[part], subtree);
531
571
  const resolved = resolveStoredValue(field, stored);
532
- if (Object.hasOwn(resolved, "value")) values[field.key] = resolved.value;
572
+ if (Object.prototype.hasOwnProperty.call(resolved, "value")) values[field.key] = resolved.value;
533
573
  if (resolved.warning) warnings[field.key] = resolved.warning;
534
574
  }
535
575
  this.#values = values;
@@ -543,7 +583,7 @@ class PreferencesClient {
543
583
  * @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
544
584
  */
545
585
  snapshot() {
546
- return structuredClone({ definition: this.#definition, values: this.#values, warnings: this.#warnings });
586
+ return JSON.parse(JSON.stringify({ definition: this.#definition, values: this.#values, warnings: this.#warnings }));
547
587
  }
548
588
 
549
589
  /**
@@ -562,22 +602,23 @@ class PreferencesClient {
562
602
  * @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
563
603
  */
564
604
  async readCaches() {
565
- const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
605
+ const response = await this.#send("get", this.#definition.cachePath ?? `@${this.#definition.storageKey}.${this.#module}.Caches`);
566
606
  return response.status === 404 ? undefined : response.json();
567
607
  }
568
608
 
569
609
  /**
570
- * 删除当前模块的 Caches 子树。
571
- * Delete the current module Caches subtree.
610
+ * 删除声明的缓存子树,默认当前模块 Caches。
611
+ * Delete the declared cache subtree, defaulting to the current module Caches.
572
612
  * @returns {Promise<void>} 操作完成 / Operation completion.
573
613
  */
574
614
  clearCaches() {
575
- return this.#change("delete", `${this.#module}.Caches`, undefined, "clearCaches");
615
+ const path = this.#definition.cachePath?.slice(this.#definition.storageKey.length + 2) ?? `${this.#module}.Caches`;
616
+ return this.#change("delete", path, undefined, "clearCaches");
576
617
  }
577
618
 
578
619
  /**
579
- * 删除当前模块数据并恢复页面默认值。
580
- * Delete current module data and restore page defaults.
620
+ * 删除声明的设置子树,默认删除当前模块数据,并恢复页面默认值。
621
+ * Delete declared settings subtrees, defaulting to current module data, and restore page defaults.
581
622
  * @returns {Promise<void>} 操作完成 / Operation completion.
582
623
  */
583
624
  reset() {
@@ -658,30 +699,49 @@ class PreferencesClient {
658
699
  if (this.#saving) throw new Error("A settings write is already in progress");
659
700
  this.#saving = true;
660
701
  try {
661
- let field;
662
- if (operation === "write" || operation === "delete") {
663
- field = this.#definition.fields.find(candidate => candidate.key === key);
664
- if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
665
- }
666
- await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
667
702
  switch (operation) {
668
703
  case "write":
669
- this.#values[key] = structuredClone(value);
670
- delete this.#warnings[key];
671
- break;
672
704
  case "delete": {
673
- delete this.#values[key];
674
- delete this.#warnings[key];
675
- if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
705
+ const field = this.#definition.fields.find(candidate => candidate.key === key);
706
+ if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
707
+ const indexed = /^(.*)\.(\d+)$/.exec(key);
708
+ if (indexed) {
709
+ const array = [...(this.#arrays.get(indexed[1]) ?? [])];
710
+ for (const sibling of this.#definition.fields) {
711
+ if (sibling.key.startsWith(`${indexed[1]}.`) && /^\d+$/.test(sibling.key.slice(indexed[1].length + 1))) {
712
+ const index = Number(sibling.key.slice(indexed[1].length + 1));
713
+ if (array[index] === undefined) array[index] = this.#values[sibling.key] ?? sibling.defaultValue;
714
+ }
715
+ }
716
+ array[Number(indexed[2])] = operation === "delete" ? field.defaultValue : value;
717
+ await this.#send("set", `@${this.#definition.storageKey}.${indexed[1]}`, array);
718
+ this.#arrays.set(indexed[1], array);
719
+ } else await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
720
+ switch (operation) {
721
+ case "write":
722
+ this.#values[key] = JSON.parse(JSON.stringify(value));
723
+ delete this.#warnings[key];
724
+ break;
725
+ case "delete":
726
+ delete this.#values[key];
727
+ delete this.#warnings[key];
728
+ if (Object.prototype.hasOwnProperty.call(field, "defaultValue")) this.#values[key] = JSON.parse(JSON.stringify(field.defaultValue));
729
+ break;
730
+ }
676
731
  break;
677
732
  }
678
733
  case "clearCaches":
734
+ await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
679
735
  break;
680
736
  case "reset":
737
+ if (this.#definition.resetPaths) {
738
+ for (const path of this.#definition.resetPaths) await this.#send("delete", path);
739
+ } else await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
740
+ this.#arrays.clear();
681
741
  this.#warnings = {};
682
742
  for (const field of this.#definition.fields) {
683
743
  delete this.#values[field.key];
684
- if (Object.hasOwn(field, "defaultValue")) this.#values[field.key] = structuredClone(field.defaultValue);
744
+ if (Object.prototype.hasOwnProperty.call(field, "defaultValue")) this.#values[field.key] = JSON.parse(JSON.stringify(field.defaultValue));
685
745
  }
686
746
  break;
687
747
  }
@@ -962,17 +1022,17 @@ class PreferencesPanel {
962
1022
  const notify = event => {
963
1023
  if (destroyed) return;
964
1024
  let message;
965
- switch (true) {
966
- case event.kind === "error":
1025
+ switch (event.kind === "error" ? "error" : event.operation) {
1026
+ case "error":
967
1027
  message = `操作失败:${event.message}`;
968
1028
  break;
969
- case event.operation === "delete":
1029
+ case "delete":
970
1030
  message = "删除成功";
971
1031
  break;
972
- case event.operation === "clearCaches":
1032
+ case "clearCaches":
973
1033
  message = "Caches 已清空";
974
1034
  break;
975
- case event.operation === "reset":
1035
+ case "reset":
976
1036
  message = "设置已重置";
977
1037
  break;
978
1038
  default:
@@ -1095,21 +1155,6 @@ class PreferencesPanel {
1095
1155
  });
1096
1156
  return queue;
1097
1157
  }
1098
- const metadata = definition.metadata;
1099
- if (metadata) {
1100
- const info = element("div", "pp-module-info");
1101
- const details = element("div", "pp-module-details");
1102
- for (const description of [metadata.author, metadata.desc ?? metadata.description, ...(metadata.descs ?? [])]) if (description) details.append(element("p", "pp-description", description));
1103
- if (metadata.repo) {
1104
- const link = element("a", "pp-module-source", "项目主页");
1105
- link.href = resourceURL(metadata.repo);
1106
- link.target = "_blank";
1107
- link.rel = "noopener noreferrer";
1108
- details.append(link);
1109
- }
1110
- info.append(details);
1111
- view.append(info);
1112
- }
1113
1158
  for (const field of definition.fields) {
1114
1159
  const match = /^\[([^\]]+)\]\s*(.*)$/.exec(field.name);
1115
1160
  const group = match?.[1] ?? "通用";
@@ -1150,8 +1195,8 @@ class PreferencesPanel {
1150
1195
  let write;
1151
1196
  let inputContainer = row;
1152
1197
  let eventName = "change";
1153
- switch (true) {
1154
- case field.control === "url": {
1198
+ switch (field.control === "url" ? "url" : field.options ? (field.type === "array" ? "checkboxes" : "selects") : field.type) {
1199
+ case "url": {
1155
1200
  const link = element("a", "pp-choice-link", "打开");
1156
1201
  link.setAttribute("aria-label", field.name);
1157
1202
  link.rel = "noopener noreferrer";
@@ -1167,7 +1212,7 @@ class PreferencesPanel {
1167
1212
  });
1168
1213
  break;
1169
1214
  }
1170
- case Boolean(field.options) && field.type !== "array": {
1215
+ case "selects": {
1171
1216
  const select = element("select", "");
1172
1217
  select.setAttribute("aria-label", field.name);
1173
1218
  field.options.forEach((option, index) => {
@@ -1182,7 +1227,7 @@ class PreferencesPanel {
1182
1227
  read = () => field.options[select.selectedIndex]?.key;
1183
1228
  break;
1184
1229
  }
1185
- case field.type === "array" && Boolean(field.options): {
1230
+ case "checkboxes": {
1186
1231
  const page = element("section", "pp-choice-page");
1187
1232
  if (field.description) page.append(element("p", "pp-description", field.description));
1188
1233
  const choices = element("div", "pp-rows");
@@ -1226,7 +1271,7 @@ class PreferencesPanel {
1226
1271
  };
1227
1272
  break;
1228
1273
  }
1229
- case field.type === "boolean": {
1274
+ case "boolean": {
1230
1275
  const toggle = element("input", "pp-switch");
1231
1276
  toggle.type = "checkbox";
1232
1277
  toggle.setAttribute("switch", "");
@@ -1264,7 +1309,12 @@ class PreferencesPanel {
1264
1309
  growingInputs.push(grow);
1265
1310
  }
1266
1311
  eventName = "input";
1267
- if (!multiline) input.type = field.type === "number" ? "number" : "text";
1312
+ if (!multiline) {
1313
+ input.type = field.type === "number" ? "number" : "text";
1314
+ input.autocapitalize = "off";
1315
+ input.setAttribute("autocorrect", "off");
1316
+ input.spellcheck = false;
1317
+ }
1268
1318
  write = value => {
1269
1319
  input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
1270
1320
  grow();
@@ -1366,7 +1416,9 @@ class PreferencesPanel {
1366
1416
  });
1367
1417
  handlers.set("clearCaches", async () => {
1368
1418
  if (saving) return;
1369
- if (!(await requestConfirmation(window, `清空 ${active} 的全部 Caches?`)) || destroyed || saving) return;
1419
+ const shared = definition.cachePath && definition.cachePath !== `@${definition.storageKey}.${active}.Caches`;
1420
+ const message = shared ? `清空共享缓存 ${definition.cachePath}?使用该缓存的其它模块也会受影响。` : `清空 ${active} 的全部 Caches?`;
1421
+ if (!(await requestConfirmation(window, message)) || destroyed || saving) return;
1370
1422
  return perform(
1371
1423
  () => client.clearCaches(),
1372
1424
  () => {
@@ -1376,7 +1428,8 @@ class PreferencesPanel {
1376
1428
  });
1377
1429
  handlers.set("reset", async () => {
1378
1430
  if (saving) return;
1379
- if (!(await requestConfirmation(window, `重置 ${active} 的设置?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) || destroyed || saving) return;
1431
+ const message = definition.resetPaths ? `重置 ${definition.metadata?.name ?? active} 的设置?只删除本页面的设置,保留共享存储中的其它设置和缓存。` : `重置 ${active} 的设置?这将删除该模块的 Settings、Caches 和其它持久化数据。`;
1432
+ if (!(await requestConfirmation(window, message)) || destroyed || saving) return;
1380
1433
  return perform(() => client.reset(), controls);
1381
1434
  });
1382
1435
  navigation?.destroy();
@@ -1419,7 +1472,7 @@ class PreferencesPanel {
1419
1472
  }
1420
1473
  }
1421
1474
 
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";
1475
+ var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n:root {\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 --pp-host-height: 0px;\n --pp-keyboard-height: 0px;\n}\n.pp-panel {\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\"] {\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 font-size: 16px;\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 var(--pp-border, #8884);\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\n/* 首页沿用表单的分组与说明样式,入口按屏幕宽度自动换行。\n * The landing page reuses form groups and descriptions, with entries wrapping by viewport width. */\n:root.pp-page {\n color-scheme: light dark;\n}\n.pp-page body {\n margin: 0;\n font:\n 16px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n background: var(--pp-background, Canvas);\n color: var(--pp-text, CanvasText);\n}\n[data-preference-panes-pages] {\n position: relative;\n min-height: calc(100vh - var(--pp-host-height));\n background: inherit;\n}\n[data-preference-panes-module-page],\n[data-preference-panes-link-page] {\n position: fixed;\n inset: var(--pp-host-height) 0 0;\n background: inherit;\n z-index: 1;\n box-shadow: -8px 0 24px #00000014;\n}\n[data-preference-panes-module-page] iframe,\n[data-preference-panes-link-page] iframe {\n display: block;\n border: 0;\n width: 100%;\n height: 100%;\n background: inherit;\n}\n[data-module-message] {\n position: absolute;\n inset: 0;\n margin: 0;\n padding: 20px;\n background: inherit;\n}\n[data-module-message][hidden] {\n display: none;\n}\n.pp-host-header {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: 44px minmax(0, 1fr) 44px;\n align-items: center;\n min-height: 44px;\n padding: env(safe-area-inset-top) max(12px, env(safe-area-inset-right)) 0 max(12px, env(safe-area-inset-left));\n border-bottom: 1px solid var(--pp-border, #8884);\n background: var(--pp-surface, Canvas);\n color: var(--pp-accent, Highlight);\n font-size: 17px;\n}\n.pp-host-title {\n text-align: center;\n color: var(--pp-text, CanvasText);\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.pp-host-back {\n border: 0;\n background: none;\n color: inherit;\n width: 44px;\n height: 44px;\n padding: 8px;\n font:\n 32px / 1 -apple-system,\n sans-serif;\n cursor: pointer;\n}\n.pp-host-back[hidden] {\n visibility: hidden;\n display: block;\n}\n.pp-host-toast {\n position: fixed;\n z-index: 3;\n bottom: calc(24px + env(safe-area-inset-bottom) + var(--pp-keyboard-height));\n left: 50%;\n transform: translateX(-50%);\n max-width: calc(100% - 48px);\n padding: 10px 18px;\n border-radius: 12px;\n background: #222e;\n color: #fff;\n font-size: 14px;\n text-align: center;\n}\n.pp-host-toast[hidden] {\n display: none;\n}\n.pp-host-error {\n margin: 24px;\n padding: 16px;\n border: 1px solid var(--pp-danger, #e45656);\n border-radius: 12px;\n font-size: 16px;\n overflow-wrap: anywhere;\n}\n\n.pp-home {\n max-width: 1000px;\n margin: auto;\n padding: 24px max(16px, env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));\n}\n.pp-home-logo {\n display: block;\n width: 64px;\n height: 64px;\n margin: 0 auto 24px;\n object-fit: contain;\n}\n.pp-home-entries {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-around;\n row-gap: 16px;\n padding: 16px 0;\n background: var(--pp-surface);\n}\n.pp-home-item {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 25%;\n min-width: 0;\n padding: 0 4px;\n border: 0;\n background: none;\n color: var(--pp-text);\n font:\n 12px / 1.5 -apple-system,\n sans-serif;\n cursor: pointer;\n text-decoration: none;\n}\n@media (min-width: 500px) {\n .pp-home-item {\n width: calc(100% / 6);\n }\n}\n@media (min-width: 700px) {\n .pp-home-item {\n width: 12.5%;\n }\n}\n.pp-home-icon {\n box-sizing: border-box;\n width: 56px;\n height: 56px;\n padding: 8px;\n border-radius: 50%;\n background: var(--pp-field);\n font-size: 32px;\n line-height: 40px;\n}\n.pp-home-icon img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n.pp-home-name {\n margin-top: 8px;\n}\n.pp-home-content {\n min-width: 0;\n width: 100%;\n}\n.pp-home-content > span {\n display: block;\n}\n.pp-home-name,\n.pp-home-item [data-module-status] {\n width: 100%;\n overflow: hidden;\n text-align: center;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.pp-home-item [data-module-status] {\n min-height: 20px;\n}\n.pp-home-item:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.pp-home-entries[data-layout=\"list\"] {\n display: block;\n padding: 0;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-item {\n flex-direction: row;\n gap: 12px;\n width: 100%;\n padding: 12px 16px;\n font-size: 15px;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-item + .pp-home-item {\n border-top: 1px solid var(--pp-border);\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-icon {\n flex: none;\n width: 40px;\n height: 40px;\n padding: 4px;\n font-size: 24px;\n line-height: 32px;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-name {\n margin-top: 0;\n text-align: left;\n}\n.pp-home-arrow {\n color: var(--pp-muted);\n font-size: 24px;\n}\n.pp-home-footer {\n margin-top: 16px;\n text-align: center;\n white-space: normal;\n}\n.pp-home-footer p {\n margin: 8px 0 0;\n}\n";
1423
1476
 
1424
1477
  const selector = "style[data-preference-panes-defaults]";
1425
1478
  const stylesheetSelector = "link[data-preference-panes-stylesheet]";
@@ -227,7 +227,7 @@ class ModuleFrame extends EventTarget {
227
227
  const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", signal: this.#controller.signal });
228
228
  if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
229
229
  const html = await response.text();
230
- this.#controller.signal.throwIfAborted();
230
+ if (this.#controller.signal.aborted) throw new DOMException("The operation was aborted", "AbortError");
231
231
  this.element.srcdoc = html;
232
232
  } finally {
233
233
  clearTimeout(timer);