@nsnanocat/preference-panes 1.1.2 → 1.1.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.
@@ -35,6 +35,7 @@ function normalizeBoxJs(config, module) {
35
35
  if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
36
36
  validatePathParts(parts);
37
37
  const name = parts[0];
38
+ if (["get", "set", "delete"].includes(name)) throw new TypeError(`Reserved API module name: ${name}`);
38
39
  let target = modules.get(name);
39
40
  if (!target) {
40
41
  target = { module: name, storageKey, entries: [], owners: new Set() };
@@ -275,8 +276,8 @@ function requestConfirmation(host, message) {
275
276
  }
276
277
 
277
278
  /**
278
- * 共用三点按钮和底部操作菜单;弹层挂载到文档根部,不受标题栏显示状态影响。
279
- * Shared overflow trigger and bottom action sheet; the layer is mounted at document level and remains independent of header visibility.
279
+ * 独立页面的三点按钮和底部操作菜单;嵌入页面由宿主提供对应界面。
280
+ * Overflow trigger and bottom action sheet for standalone pages; embedded pages use host-provided chrome.
280
281
  */
281
282
  class ActionMenu {
282
283
  #button;
@@ -816,13 +817,9 @@ class PreferencesPanel {
816
817
  const title = definition.metadata?.name ?? definition.module;
817
818
  const document = root.ownerDocument;
818
819
  const window = document.defaultView;
820
+ const frame = window.frameElement?.dataset.preferencePanes ? window.frameElement : undefined;
819
821
  const shell = element("div", "pp-panel");
820
822
  shell.dataset.module = definition.module;
821
- const header = element("header", "pp-header");
822
- const back = element("button", "pp-back", "‹");
823
- back.setAttribute("aria-label", "返回");
824
- back.type = "button";
825
- const heading = element("h1", "pp-title", title);
826
823
  const handlers = new Map();
827
824
  const menuItems = [
828
825
  { id: "viewSettings", label: "查看设置" },
@@ -830,37 +827,51 @@ class PreferencesPanel {
830
827
  { id: "clearCaches", label: "清空缓存", destructive: true },
831
828
  { id: "reset", label: "重置设置", destructive: true },
832
829
  ];
833
- const menu = new ActionMenu(id => runAction(id));
834
- const trailing = element("span", "pp-nav-spacer");
835
- trailing.append(menu.element);
836
830
  const viewport = element("div", "pp-viewport");
831
+ let back, heading, menu;
832
+ if (frame) shell.append(viewport);
833
+ else {
834
+ const header = element("header", "pp-header");
835
+ back = element("button", "pp-back", "‹");
836
+ back.setAttribute("aria-label", "返回");
837
+ back.type = "button";
838
+ heading = element("h1", "pp-title", title);
839
+ menu = new ActionMenu(id => runAction(id));
840
+ const trailing = element("span", "pp-nav-spacer");
841
+ trailing.append(menu.element);
842
+ header.append(back, heading, trailing);
843
+ shell.append(header, viewport);
844
+ }
837
845
  let toast;
838
- header.append(back, heading, trailing);
839
- shell.append(header, viewport);
840
846
  root.append(shell);
841
847
  // 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
842
848
  // Embedded mode publishes navigation state without host reads or mutations of the module DOM.
843
849
  const publishNavigation = () => {
844
850
  const actions = handlers.size ? menuItems : [];
845
- menu.update(actions, saving);
846
- const frame = window.frameElement;
847
- if (!frame?.dataset.preferencePanes) return;
848
- frame.dispatchEvent(
849
- new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
850
- detail: { title: heading.textContent, module: definition.module, busy: saving, canGoBack: !back.disabled, actions },
851
- }),
852
- );
851
+ if (frame)
852
+ frame.dispatchEvent(
853
+ new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
854
+ detail: { title: currentTitle, module: definition.module, busy: saving, canGoBack, actions },
855
+ }),
856
+ );
857
+ else {
858
+ heading.textContent = currentTitle;
859
+ back.disabled = !canGoBack;
860
+ menu.update(actions, saving);
861
+ }
853
862
  };
854
863
  const onAction = event => {
855
864
  if (!saving && handlers.has(event.detail)) runAction(event.detail);
856
865
  };
857
- window.frameElement?.addEventListener("preferencepanes:action", onAction);
866
+ frame?.addEventListener("preferencepanes:action", onAction);
858
867
  let timer,
859
868
  navigation,
860
869
  generation = 0,
861
870
  active = null,
862
871
  saving = false,
863
- destroyed = false;
872
+ destroyed = false,
873
+ currentTitle = title,
874
+ canGoBack = window.history.length > 1;
864
875
  /**
865
876
  * 展示短暂通知,不刷新设置数据。
866
877
  * Display a transient notification without refreshing settings.
@@ -889,7 +900,6 @@ class PreferencesPanel {
889
900
  }
890
901
  // 宿主接管时不创建网页 Toast,也不运行其计时器。
891
902
  // A host-owned notice creates no web Toast and starts no local timer.
892
- const frame = window.frameElement;
893
903
  if (frame && !frame.dispatchEvent(new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:notice", { cancelable: true, detail: { kind: event.kind, message } }))) return;
894
904
  if (!toast) {
895
905
  toast = element("div", "pp-toast");
@@ -927,8 +937,8 @@ class PreferencesPanel {
927
937
  async function open(module) {
928
938
  const version = ++generation;
929
939
  active = module;
930
- back.disabled = window.history.length <= 1;
931
- heading.textContent = module;
940
+ currentTitle = module;
941
+ canGoBack = window.history.length > 1;
932
942
  publishNavigation();
933
943
  viewport.replaceChildren(statusView("读取设置…"));
934
944
  try {
@@ -947,7 +957,7 @@ class PreferencesPanel {
947
957
  */
948
958
  function controls() {
949
959
  const { definition, values } = client.snapshot();
950
- heading.textContent = definition.metadata?.name || active;
960
+ currentTitle = definition.metadata?.name || active;
951
961
  const view = element("section", "pp-fields");
952
962
  /**
953
963
  * 挂载后执行的多行高度更新
@@ -967,8 +977,8 @@ class PreferencesPanel {
967
977
  */
968
978
  const updateNavigation = () => {
969
979
  const editor = editors.get(navigation.current);
970
- heading.textContent = editor?.title ?? definition.metadata?.name ?? active;
971
- back.disabled = saving || !navigation.canGoBack;
980
+ currentTitle = editor?.title ?? definition.metadata?.name ?? active;
981
+ canGoBack = !saving && navigation.canGoBack;
972
982
  publishNavigation();
973
983
  };
974
984
  /**
@@ -982,7 +992,7 @@ class PreferencesPanel {
982
992
  function perform(action, success, failure = () => {}) {
983
993
  pendingWrites++;
984
994
  saving = true;
985
- back.disabled = true;
995
+ canGoBack = false;
986
996
  publishNavigation();
987
997
  queue = queue
988
998
  .then(action)
@@ -998,7 +1008,7 @@ class PreferencesPanel {
998
1008
  pendingWrites--;
999
1009
  saving = pendingWrites > 0;
1000
1010
  if (destroyed && !saving) client.leave();
1001
- back.disabled = saving || !navigation.canGoBack;
1011
+ canGoBack = !saving && navigation.canGoBack;
1002
1012
  publishNavigation();
1003
1013
  });
1004
1014
  return queue;
@@ -1272,16 +1282,17 @@ class PreferencesPanel {
1272
1282
  * Loaded forms delegate back to navigation; loading views can return to the previous document.
1273
1283
  * @returns {void} 无返回值 / No return value.
1274
1284
  */
1275
- back.onclick = () => {
1276
- if (saving) return;
1277
- if (navigation) navigation.back();
1278
- else window.history.back();
1279
- };
1285
+ if (back)
1286
+ back.onclick = () => {
1287
+ if (saving) return;
1288
+ if (navigation) navigation.back();
1289
+ else window.history.back();
1290
+ };
1280
1291
  open(definition.module);
1281
1292
  return () => {
1282
1293
  destroyed = true;
1283
- menu.destroy();
1284
- window.frameElement?.removeEventListener("preferencepanes:action", onAction);
1294
+ menu?.destroy();
1295
+ frame?.removeEventListener("preferencepanes:action", onAction);
1285
1296
  navigation?.destroy();
1286
1297
  generation++;
1287
1298
  if (active && !saving) client.leave();
@@ -1300,7 +1311,7 @@ class PreferencesPanel {
1300
1311
  }
1301
1312
  }
1302
1313
 
1303
- 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:root[data-preference-panes-embedded] .pp-header {\n display: none;\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-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";
1314
+ 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-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";
1304
1315
 
1305
1316
  const selector = "style[data-preference-panes-defaults]";
1306
1317
 
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 共用三点按钮和底部操作菜单;弹层挂载到文档根部,不受标题栏显示状态影响。
3
- * Shared overflow trigger and bottom action sheet; the layer is mounted at document level and remains independent of header visibility.
2
+ * 独立页面的三点按钮和底部操作菜单;嵌入页面由宿主提供对应界面。
3
+ * Overflow trigger and bottom action sheet for standalone pages; embedded pages use host-provided chrome.
4
4
  */
5
5
  class ActionMenu {
6
6
  #button;
@@ -331,7 +331,7 @@ class ModuleStatus extends EventTarget {
331
331
  const response = await probeModule(url, { ...options, signal: controller.signal });
332
332
  if (controller !== this.#controller) return response;
333
333
  const version = response.status === 200 ? response.headers.get("X-PreferencePanes-Version")?.trim() || null : null;
334
- this.#render(response.status === 200 ? "installed" : "missing", version);
334
+ this.#render(version ? "installed" : "missing", version);
335
335
  return response;
336
336
  } catch (error) {
337
337
  if (controller !== this.#controller) return;
@@ -35,6 +35,7 @@ function normalizeBoxJs(config, module) {
35
35
  if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
36
36
  validatePathParts(parts);
37
37
  const name = parts[0];
38
+ if (["get", "set", "delete"].includes(name)) throw new TypeError(`Reserved API module name: ${name}`);
38
39
  let target = modules.get(name);
39
40
  if (!target) {
40
41
  target = { module: name, storageKey, entries: [], owners: new Set() };
@@ -275,8 +276,8 @@ function requestConfirmation(host, message) {
275
276
  }
276
277
 
277
278
  /**
278
- * 共用三点按钮和底部操作菜单;弹层挂载到文档根部,不受标题栏显示状态影响。
279
- * Shared overflow trigger and bottom action sheet; the layer is mounted at document level and remains independent of header visibility.
279
+ * 独立页面的三点按钮和底部操作菜单;嵌入页面由宿主提供对应界面。
280
+ * Overflow trigger and bottom action sheet for standalone pages; embedded pages use host-provided chrome.
280
281
  */
281
282
  class ActionMenu {
282
283
  #button;
@@ -816,13 +817,9 @@ class PreferencesPanel {
816
817
  const title = definition.metadata?.name ?? definition.module;
817
818
  const document = root.ownerDocument;
818
819
  const window = document.defaultView;
820
+ const frame = window.frameElement?.dataset.preferencePanes ? window.frameElement : undefined;
819
821
  const shell = element("div", "pp-panel");
820
822
  shell.dataset.module = definition.module;
821
- const header = element("header", "pp-header");
822
- const back = element("button", "pp-back", "‹");
823
- back.setAttribute("aria-label", "返回");
824
- back.type = "button";
825
- const heading = element("h1", "pp-title", title);
826
823
  const handlers = new Map();
827
824
  const menuItems = [
828
825
  { id: "viewSettings", label: "查看设置" },
@@ -830,37 +827,51 @@ class PreferencesPanel {
830
827
  { id: "clearCaches", label: "清空缓存", destructive: true },
831
828
  { id: "reset", label: "重置设置", destructive: true },
832
829
  ];
833
- const menu = new ActionMenu(id => runAction(id));
834
- const trailing = element("span", "pp-nav-spacer");
835
- trailing.append(menu.element);
836
830
  const viewport = element("div", "pp-viewport");
831
+ let back, heading, menu;
832
+ if (frame) shell.append(viewport);
833
+ else {
834
+ const header = element("header", "pp-header");
835
+ back = element("button", "pp-back", "‹");
836
+ back.setAttribute("aria-label", "返回");
837
+ back.type = "button";
838
+ heading = element("h1", "pp-title", title);
839
+ menu = new ActionMenu(id => runAction(id));
840
+ const trailing = element("span", "pp-nav-spacer");
841
+ trailing.append(menu.element);
842
+ header.append(back, heading, trailing);
843
+ shell.append(header, viewport);
844
+ }
837
845
  let toast;
838
- header.append(back, heading, trailing);
839
- shell.append(header, viewport);
840
846
  root.append(shell);
841
847
  // 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
842
848
  // Embedded mode publishes navigation state without host reads or mutations of the module DOM.
843
849
  const publishNavigation = () => {
844
850
  const actions = handlers.size ? menuItems : [];
845
- menu.update(actions, saving);
846
- const frame = window.frameElement;
847
- if (!frame?.dataset.preferencePanes) return;
848
- frame.dispatchEvent(
849
- new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
850
- detail: { title: heading.textContent, module: definition.module, busy: saving, canGoBack: !back.disabled, actions },
851
- }),
852
- );
851
+ if (frame)
852
+ frame.dispatchEvent(
853
+ new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
854
+ detail: { title: currentTitle, module: definition.module, busy: saving, canGoBack, actions },
855
+ }),
856
+ );
857
+ else {
858
+ heading.textContent = currentTitle;
859
+ back.disabled = !canGoBack;
860
+ menu.update(actions, saving);
861
+ }
853
862
  };
854
863
  const onAction = event => {
855
864
  if (!saving && handlers.has(event.detail)) runAction(event.detail);
856
865
  };
857
- window.frameElement?.addEventListener("preferencepanes:action", onAction);
866
+ frame?.addEventListener("preferencepanes:action", onAction);
858
867
  let timer,
859
868
  navigation,
860
869
  generation = 0,
861
870
  active = null,
862
871
  saving = false,
863
- destroyed = false;
872
+ destroyed = false,
873
+ currentTitle = title,
874
+ canGoBack = window.history.length > 1;
864
875
  /**
865
876
  * 展示短暂通知,不刷新设置数据。
866
877
  * Display a transient notification without refreshing settings.
@@ -889,7 +900,6 @@ class PreferencesPanel {
889
900
  }
890
901
  // 宿主接管时不创建网页 Toast,也不运行其计时器。
891
902
  // A host-owned notice creates no web Toast and starts no local timer.
892
- const frame = window.frameElement;
893
903
  if (frame && !frame.dispatchEvent(new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:notice", { cancelable: true, detail: { kind: event.kind, message } }))) return;
894
904
  if (!toast) {
895
905
  toast = element("div", "pp-toast");
@@ -927,8 +937,8 @@ class PreferencesPanel {
927
937
  async function open(module) {
928
938
  const version = ++generation;
929
939
  active = module;
930
- back.disabled = window.history.length <= 1;
931
- heading.textContent = module;
940
+ currentTitle = module;
941
+ canGoBack = window.history.length > 1;
932
942
  publishNavigation();
933
943
  viewport.replaceChildren(statusView("读取设置…"));
934
944
  try {
@@ -947,7 +957,7 @@ class PreferencesPanel {
947
957
  */
948
958
  function controls() {
949
959
  const { definition, values } = client.snapshot();
950
- heading.textContent = definition.metadata?.name || active;
960
+ currentTitle = definition.metadata?.name || active;
951
961
  const view = element("section", "pp-fields");
952
962
  /**
953
963
  * 挂载后执行的多行高度更新
@@ -967,8 +977,8 @@ class PreferencesPanel {
967
977
  */
968
978
  const updateNavigation = () => {
969
979
  const editor = editors.get(navigation.current);
970
- heading.textContent = editor?.title ?? definition.metadata?.name ?? active;
971
- back.disabled = saving || !navigation.canGoBack;
980
+ currentTitle = editor?.title ?? definition.metadata?.name ?? active;
981
+ canGoBack = !saving && navigation.canGoBack;
972
982
  publishNavigation();
973
983
  };
974
984
  /**
@@ -982,7 +992,7 @@ class PreferencesPanel {
982
992
  function perform(action, success, failure = () => {}) {
983
993
  pendingWrites++;
984
994
  saving = true;
985
- back.disabled = true;
995
+ canGoBack = false;
986
996
  publishNavigation();
987
997
  queue = queue
988
998
  .then(action)
@@ -998,7 +1008,7 @@ class PreferencesPanel {
998
1008
  pendingWrites--;
999
1009
  saving = pendingWrites > 0;
1000
1010
  if (destroyed && !saving) client.leave();
1001
- back.disabled = saving || !navigation.canGoBack;
1011
+ canGoBack = !saving && navigation.canGoBack;
1002
1012
  publishNavigation();
1003
1013
  });
1004
1014
  return queue;
@@ -1272,16 +1282,17 @@ class PreferencesPanel {
1272
1282
  * Loaded forms delegate back to navigation; loading views can return to the previous document.
1273
1283
  * @returns {void} 无返回值 / No return value.
1274
1284
  */
1275
- back.onclick = () => {
1276
- if (saving) return;
1277
- if (navigation) navigation.back();
1278
- else window.history.back();
1279
- };
1285
+ if (back)
1286
+ back.onclick = () => {
1287
+ if (saving) return;
1288
+ if (navigation) navigation.back();
1289
+ else window.history.back();
1290
+ };
1280
1291
  open(definition.module);
1281
1292
  return () => {
1282
1293
  destroyed = true;
1283
- menu.destroy();
1284
- window.frameElement?.removeEventListener("preferencepanes:action", onAction);
1294
+ menu?.destroy();
1295
+ frame?.removeEventListener("preferencepanes:action", onAction);
1285
1296
  navigation?.destroy();
1286
1297
  generation++;
1287
1298
  if (active && !saving) client.leave();
@@ -1300,7 +1311,7 @@ class PreferencesPanel {
1300
1311
  }
1301
1312
  }
1302
1313
 
1303
- 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:root[data-preference-panes-embedded] .pp-header {\n display: none;\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-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";
1314
+ 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-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";
1304
1315
 
1305
1316
  const selector = "style[data-preference-panes-defaults]";
1306
1317