@nsnanocat/preference-panes 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,7 +29,7 @@ preferences.destroy();
29
29
 
30
30
  前端支持 BoxJS 字段数组、单个 app 和 apps 订阅,并要求输入恰好包含一个模块。模块名、存储根和 Settings 路径都从 `@Root.Module.Settings.key` 字段 ID 推导,不从 app 名称或调用参数补充。
31
31
 
32
- 默认样式随包内置。动态模块 HTML 可通过 `css` 查询参数或优先级更高的 `X-PreferencePanes-CSS` Header 直接加载一个项目 stylesheet;未提供时只使用默认样式。项目 stylesheet 位于默认样式之后,可覆盖 `pp-*` 变量和组件规则。页面脚本不二次下载 CSS,也不识别客户端 User-Agent 或加载客户端 SDK。
32
+ 默认样式随包内置。动态模块 HTML 接受 `json`、`css` 查询参数及优先级更高的 `X-PreferencePanes-JSON`、`X-PreferencePanes-CSS` Header,两个输入值都是资源 URL。未指定 JSON 时默认读取 `/api/{module}`,未指定 CSS 时只使用默认样式。项目 stylesheet 位于默认样式之后,可覆盖 `pp-*` 变量和组件规则。页面脚本不识别客户端 User-Agent,也不加载客户端 SDK。
33
33
 
34
34
  ## 页面与 API
35
35
 
@@ -39,7 +39,7 @@ preferences.destroy();
39
39
  - `GET /settings/assets/index.mjs`
40
40
  - `GET /settings/assets/navigation.mjs`
41
41
 
42
- 模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,通过同源 `/api/{module}` 读取原始 BoxJS,然后调用 `mount(boxjs)`。`web.js` 只在生成 HTML 时解析可选 CSS 地址并写入 `<link data-preference-panes-stylesheet>`;CSS 地址必须解析为 HTTP(S)。页面不接受 JSON 输入或兼容资源别名,也不直接访问 `/configs/**`。
42
+ 模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,读取 HTML 中声明的 BoxJS 资源,然后调用 `mount(boxjs)`。`web.js` 在生成 HTML 时解析 BoxJS 与可选 CSS 地址,分别写入 `<meta name="preference-panes-boxjs">` 和 `<link data-preference-panes-stylesheet>`;两个地址都必须解析为 HTTP(S)。静态页面没有资源声明时回退同源 `/api/{module}`,页面不直接访问 `/configs/**`。
43
43
 
44
44
  业务模块模板直接处理:
45
45
 
@@ -62,7 +62,7 @@ await fetch("/api/set", {
62
62
  });
63
63
  ```
64
64
 
65
- 页面初始化时只执行一次 `POST /api/get` 读取 Settings 子树。写入成功后仅更新当前页面快照;查看 Settings/Caches 时按需读取,清空和重置通过 `/api/delete` 完成。
65
+ 页面初始化时只执行一次 `POST /api/get` 读取 Settings 子树。外部存储中的历史值不会阻断页面加载:当前选项表未定义的值会保留并在对应设置项下提示,无法由控件表示的值会回退到默认值并显示警告;用户的新写入仍执行严格校验。写入成功后仅更新当前页面快照;查看 Settings/Caches 时按需读取,清空和重置通过 `/api/delete` 完成。
66
66
 
67
67
  设置项支持 `type: "url"` 作为只读跳转入口。它的 `val` 必须是带 scheme 的地址,例如 `bilibili://main/top_category`;面板将其渲染为可点击链接,不会发起存储写入。嵌入 `ModuleFrame` 时,宿主可拦截 `open-url` 事件(`detail.url`)并打开链接;未拦截或独立网页中保留链接的默认导航。
68
68
 
@@ -76,7 +76,10 @@ await status.check("/api/Module");
76
76
 
77
77
  const frame = new ModuleFrame("/settings/Module", {
78
78
  signal,
79
- headers: { "X-PreferencePanes-CSS": "https://example.org/theme.css" },
79
+ headers: {
80
+ "X-PreferencePanes-JSON": "https://example.org/Module.boxjs.json",
81
+ "X-PreferencePanes-CSS": "https://example.org/theme.css",
82
+ },
80
83
  });
81
84
  container.append(frame.element);
82
85
  await frame.load();
@@ -95,6 +98,14 @@ npm pack --dry-run
95
98
 
96
99
  构建生成 `dist/api.js`、`dist/web.js`、`dist/preference-panes.mjs` 和 `dist/module/` 通用页面资源。包不再提供按模块生成 HTML/CSS 的公开 `build()`。
97
100
 
98
- `npm run preview` 启动 JSON 文件导入测试台,使用同一通用页面、同源配置和内存存储验证模块行为。
101
+ ### 内置演示
102
+
103
+ ```sh
104
+ npm run preview
105
+ ```
106
+
107
+ 预览入口会自动加载 `examples/Module.boxjs.json`,覆盖 `boolean`、`selects`、`checkboxes`、`text`、`textarea`、`number` 和 `url` 全部受支持控件。顶端 CSS 选择器默认使用包内置 CSS,也可切换 `examples/theme.css` 或导入本地 CSS 文件。页面通过正式构建的 `web.js` 以 `X-PreferencePanes-JSON` 和可选 `X-PreferencePanes-CSS` Header 注入资源地址,并使用固定存储 API 与独立内存存储,不维护演示专用渲染器。
108
+
109
+ 内置存储会提供已移除选项和格式错误值,用于直接核对字段级黄色警告及非阻断加载;URL 字段还会携带同路径旧存储值,验证只读链接始终使用 BoxJS `val`。测试台同时保留上传其它 BoxJS JSON 并生成隔离预览的能力。
99
110
 
100
111
  [接口规范](apifox/Specification.md) · [宿主集成](apifox/HostIntegration.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
@@ -4,12 +4,13 @@
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
  <title>Module Preferences</title>
8
9
  <style>body { margin: 0; }</style>
9
10
 
10
11
  </head>
11
12
  <body>
12
13
  <main id="preferences"></main>
13
- <script type="module" src="/settings/assets/index.mjs?v=1.2.2"></script>
14
+ <script type="module" src="/settings/assets/index.mjs?v=1.2.4"></script>
14
15
  </body>
15
16
  </html>
@@ -172,18 +172,62 @@ function scalar(value) {
172
172
  }
173
173
 
174
174
  /**
175
- * 检查值类型、数组唯一性及声明的选项,不进行转换。
176
- * Check value type, array uniqueness and declared choices without coercion.
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 待写入的 JSON 值 / JSON value to write.
179
- * @returns {boolean} 是否符合字段约束 / Whether the value satisfies field constraints.
178
+ * @param {unknown} value 待检查值 / Value to inspect.
179
+ * @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
180
180
  */
181
- function validValue(field, value) {
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 !field.options || (field.type === "array" ? value : [value]).every(item => field.options.some(option => option.key === item));
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
  /**
@@ -451,6 +495,7 @@ class PreferencesClient {
451
495
  #timeout;
452
496
  #session = new AbortController();
453
497
  #values = {};
498
+ #warnings = {};
454
499
  #saving = false;
455
500
 
456
501
  /**
@@ -477,17 +522,18 @@ class PreferencesClient {
477
522
  if (typeof subtree === "string") subtree = JSON.parse(subtree);
478
523
  if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
479
524
  const values = {};
525
+ const warnings = {};
480
526
  for (const field of this.#definition.fields) {
481
527
  const stored = field.key
482
528
  .split(".")
483
529
  .slice(this.#definition.settingsPath.length)
484
530
  .reduce((parent, part) => Object(parent)[part], subtree);
485
- const value = normalizeStoredValue(field, stored === undefined ? field.defaultValue : stored);
486
- if (value === undefined) continue;
487
- if (!validValue(field, value)) throw new TypeError(`Invalid stored value: ${field.key}`);
488
- 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;
489
534
  }
490
535
  this.#values = values;
536
+ this.#warnings = warnings;
491
537
  return this.snapshot();
492
538
  }
493
539
 
@@ -497,7 +543,7 @@ class PreferencesClient {
497
543
  * @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
498
544
  */
499
545
  snapshot() {
500
- return structuredClone({ definition: this.#definition, values: this.#values });
546
+ return structuredClone({ definition: this.#definition, values: this.#values, warnings: this.#warnings });
501
547
  }
502
548
 
503
549
  /**
@@ -621,15 +667,18 @@ class PreferencesClient {
621
667
  switch (operation) {
622
668
  case "write":
623
669
  this.#values[key] = structuredClone(value);
670
+ delete this.#warnings[key];
624
671
  break;
625
672
  case "delete": {
626
673
  delete this.#values[key];
674
+ delete this.#warnings[key];
627
675
  if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
628
676
  break;
629
677
  }
630
678
  case "clearCaches":
631
679
  break;
632
680
  case "reset":
681
+ this.#warnings = {};
633
682
  for (const field of this.#definition.fields) {
634
683
  delete this.#values[field.key];
635
684
  if (Object.hasOwn(field, "defaultValue")) this.#values[field.key] = structuredClone(field.defaultValue);
@@ -805,6 +854,22 @@ class Navigation extends EventTarget {
805
854
  }
806
855
  }
807
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
+
808
873
  /**
809
874
  * 管理模块表单、导航、操作队列和短暂通知。
810
875
  * Manage the module form, navigation, operation queue, and transient notifications.
@@ -983,6 +1048,7 @@ class PreferencesPanel {
983
1048
  const growingInputs = [];
984
1049
  const editors = new Map();
985
1050
  const summaries = [];
1051
+ const warningRefreshers = [];
986
1052
  const groups = new Map();
987
1053
  let queue = Promise.resolve(),
988
1054
  pendingWrites = 0;
@@ -1059,6 +1125,15 @@ class PreferencesPanel {
1059
1125
  const label = element("div", "pp-label");
1060
1126
  label.append(element("span", "pp-field-name", match?.[2] ?? field.name));
1061
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);
1062
1137
  row.append(label);
1063
1138
  const value = values[field.key];
1064
1139
  /**
@@ -1234,6 +1309,7 @@ class PreferencesPanel {
1234
1309
  },
1235
1310
  () => {
1236
1311
  for (const refresh of summaries) refresh();
1312
+ for (const refresh of warningRefreshers) refresh();
1237
1313
  },
1238
1314
  restore,
1239
1315
  );
@@ -1343,7 +1419,7 @@ class PreferencesPanel {
1343
1419
  }
1344
1420
  }
1345
1421
 
1346
- 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";
1347
1423
 
1348
1424
  const selector = "style[data-preference-panes-defaults]";
1349
1425
  const stylesheetSelector = "link[data-preference-panes-stylesheet]";
@@ -1485,8 +1561,8 @@ class ModulePage {
1485
1561
  }
1486
1562
 
1487
1563
  /**
1488
- * 通过模块 API 读取 BoxJS JSON 并挂载通用前端。
1489
- * Read BoxJS JSON through the module API and mount the generic frontend.
1564
+ * 读取页面声明的 BoxJS JSON 并挂载通用前端。
1565
+ * Read the BoxJS JSON declared by the page and mount the generic frontend.
1490
1566
  * @returns {Promise<void>} 启动完成 / Startup completion.
1491
1567
  */
1492
1568
  async start() {
@@ -1498,7 +1574,8 @@ class ModulePage {
1498
1574
  const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
1499
1575
  const module = embedded ?? match?.[1];
1500
1576
  if (!module) throw new TypeError("Open a concrete module URL");
1501
- const response = await fetch(`/api/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
1577
+ const source = this.#window.document.querySelector('meta[name="preference-panes-boxjs"]')?.content || `/api/${encodeURIComponent(module)}`;
1578
+ const response = await fetch(source, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
1502
1579
  if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
1503
1580
  const boxjs = await response.json();
1504
1581
  normalizeBoxJs(boxjs, module);
@@ -172,18 +172,62 @@ function scalar(value) {
172
172
  }
173
173
 
174
174
  /**
175
- * 检查值类型、数组唯一性及声明的选项,不进行转换。
176
- * Check value type, array uniqueness and declared choices without coercion.
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 待写入的 JSON 值 / JSON value to write.
179
- * @returns {boolean} 是否符合字段约束 / Whether the value satisfies field constraints.
178
+ * @param {unknown} value 待检查值 / Value to inspect.
179
+ * @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
180
180
  */
181
- function validValue(field, value) {
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 !field.options || (field.type === "array" ? value : [value]).every(item => field.options.some(option => option.key === item));
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
  /**
@@ -451,6 +495,7 @@ class PreferencesClient {
451
495
  #timeout;
452
496
  #session = new AbortController();
453
497
  #values = {};
498
+ #warnings = {};
454
499
  #saving = false;
455
500
 
456
501
  /**
@@ -477,17 +522,18 @@ class PreferencesClient {
477
522
  if (typeof subtree === "string") subtree = JSON.parse(subtree);
478
523
  if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
479
524
  const values = {};
525
+ const warnings = {};
480
526
  for (const field of this.#definition.fields) {
481
527
  const stored = field.key
482
528
  .split(".")
483
529
  .slice(this.#definition.settingsPath.length)
484
530
  .reduce((parent, part) => Object(parent)[part], subtree);
485
- const value = normalizeStoredValue(field, stored === undefined ? field.defaultValue : stored);
486
- if (value === undefined) continue;
487
- if (!validValue(field, value)) throw new TypeError(`Invalid stored value: ${field.key}`);
488
- 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;
489
534
  }
490
535
  this.#values = values;
536
+ this.#warnings = warnings;
491
537
  return this.snapshot();
492
538
  }
493
539
 
@@ -497,7 +543,7 @@ class PreferencesClient {
497
543
  * @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
498
544
  */
499
545
  snapshot() {
500
- return structuredClone({ definition: this.#definition, values: this.#values });
546
+ return structuredClone({ definition: this.#definition, values: this.#values, warnings: this.#warnings });
501
547
  }
502
548
 
503
549
  /**
@@ -621,15 +667,18 @@ class PreferencesClient {
621
667
  switch (operation) {
622
668
  case "write":
623
669
  this.#values[key] = structuredClone(value);
670
+ delete this.#warnings[key];
624
671
  break;
625
672
  case "delete": {
626
673
  delete this.#values[key];
674
+ delete this.#warnings[key];
627
675
  if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
628
676
  break;
629
677
  }
630
678
  case "clearCaches":
631
679
  break;
632
680
  case "reset":
681
+ this.#warnings = {};
633
682
  for (const field of this.#definition.fields) {
634
683
  delete this.#values[field.key];
635
684
  if (Object.hasOwn(field, "defaultValue")) this.#values[field.key] = structuredClone(field.defaultValue);
@@ -805,6 +854,22 @@ class Navigation extends EventTarget {
805
854
  }
806
855
  }
807
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
+
808
873
  /**
809
874
  * 管理模块表单、导航、操作队列和短暂通知。
810
875
  * Manage the module form, navigation, operation queue, and transient notifications.
@@ -983,6 +1048,7 @@ class PreferencesPanel {
983
1048
  const growingInputs = [];
984
1049
  const editors = new Map();
985
1050
  const summaries = [];
1051
+ const warningRefreshers = [];
986
1052
  const groups = new Map();
987
1053
  let queue = Promise.resolve(),
988
1054
  pendingWrites = 0;
@@ -1059,6 +1125,15 @@ class PreferencesPanel {
1059
1125
  const label = element("div", "pp-label");
1060
1126
  label.append(element("span", "pp-field-name", match?.[2] ?? field.name));
1061
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);
1062
1137
  row.append(label);
1063
1138
  const value = values[field.key];
1064
1139
  /**
@@ -1234,6 +1309,7 @@ class PreferencesPanel {
1234
1309
  },
1235
1310
  () => {
1236
1311
  for (const refresh of summaries) refresh();
1312
+ for (const refresh of warningRefreshers) refresh();
1237
1313
  },
1238
1314
  restore,
1239
1315
  );
@@ -1343,7 +1419,7 @@ class PreferencesPanel {
1343
1419
  }
1344
1420
  }
1345
1421
 
1346
- 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";
1347
1423
 
1348
1424
  const selector = "style[data-preference-panes-defaults]";
1349
1425
  const stylesheetSelector = "link[data-preference-panes-stylesheet]";