@nsnanocat/preference-panes 1.1.3 → 1.2.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.
- package/README.md +7 -4
- package/dist/module/index.html +2 -1
- package/dist/module/index.mjs +50 -39
- package/dist/module/navigation.mjs +7 -7
- package/dist/preference-panes.mjs +50 -39
- package/dist/web.js +42 -2
- package/package.json +1 -1
- package/src/browser/ActionMenu.mjs +2 -2
- package/src/browser/ModuleFrame.mjs +5 -5
- package/src/browser/Navigation.d.mts +4 -4
- package/src/browser/module.html +1 -0
- package/src/browser/panel.css +0 -3
- package/src/browser/panel.mjs +45 -35
- package/src/browser/styles.mjs +2 -1
- package/src/web.mjs +41 -1
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
|
-
|
|
32
|
+
默认样式随包内置。动态模块 HTML 可通过 `css` 查询参数或优先级更高的 `X-PreferencePanes-CSS` Header 直接加载一个项目 stylesheet;未提供时只使用默认样式。项目 stylesheet 位于默认样式之后,可覆盖 `pp-*` 变量和组件规则。页面脚本不二次下载 CSS,也不识别客户端 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)
|
|
42
|
+
模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,通过同源 `/api/{module}` 读取原始 BoxJS,然后调用 `mount(boxjs)`。`web.js` 只在生成 HTML 时解析可选 CSS 地址并写入 `<link data-preference-panes-stylesheet>`;CSS 地址必须解析为 HTTP(S)。页面不接受 JSON 输入或兼容资源别名,也不直接访问 `/configs/**`。
|
|
43
43
|
|
|
44
44
|
业务模块模板直接处理:
|
|
45
45
|
|
|
@@ -72,12 +72,15 @@ import { ModuleFrame, ModuleStatus } from "@nsnanocat/preference-panes/navigatio
|
|
|
72
72
|
const status = new ModuleStatus(statusElement);
|
|
73
73
|
await status.check("/api/Module");
|
|
74
74
|
|
|
75
|
-
const frame = new ModuleFrame("/settings/Module", {
|
|
75
|
+
const frame = new ModuleFrame("/settings/Module", {
|
|
76
|
+
signal,
|
|
77
|
+
headers: { "X-PreferencePanes-CSS": "https://example.org/theme.css" },
|
|
78
|
+
});
|
|
76
79
|
container.append(frame.element);
|
|
77
80
|
await frame.load();
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
`ModuleFrame`
|
|
83
|
+
`ModuleFrame` 接受 `{ signal?: AbortSignal; headers?: HeadersInit }`,使用这些选项发送一次 GET,并将返回 HTML 原样设置为 `iframe.srcdoc`。iframe dataset 只保存模块身份,不保存 CSS 地址。宿主通过 `change`、`confirm`、`notice` 事件同步标题、操作菜单、确认框和提示,不读取或改写 iframe 内部 DOM。项目主页、Bilibili JSBridge、原生导航和视觉样式仍由宿主负责。
|
|
81
84
|
|
|
82
85
|
## 构建与验证
|
|
83
86
|
|
package/dist/module/index.html
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
<meta name="color-scheme" content="light dark">
|
|
7
7
|
<title>Module Preferences</title>
|
|
8
8
|
<style>body { margin: 0; }</style>
|
|
9
|
+
|
|
9
10
|
</head>
|
|
10
11
|
<body>
|
|
11
12
|
<main id="preferences"></main>
|
|
12
|
-
<script type="module" src="/settings/assets/index.mjs?v=1.
|
|
13
|
+
<script type="module" src="/settings/assets/index.mjs?v=1.2.0"></script>
|
|
13
14
|
</body>
|
|
14
15
|
</html>
|
package/dist/module/index.mjs
CHANGED
|
@@ -276,8 +276,8 @@ function requestConfirmation(host, message) {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
279
|
+
* 独立页面的三点按钮和底部操作菜单;嵌入页面由宿主提供对应界面。
|
|
280
|
+
* Overflow trigger and bottom action sheet for standalone pages; embedded pages use host-provided chrome.
|
|
281
281
|
*/
|
|
282
282
|
class ActionMenu {
|
|
283
283
|
#button;
|
|
@@ -817,13 +817,9 @@ class PreferencesPanel {
|
|
|
817
817
|
const title = definition.metadata?.name ?? definition.module;
|
|
818
818
|
const document = root.ownerDocument;
|
|
819
819
|
const window = document.defaultView;
|
|
820
|
+
const frame = window.frameElement?.dataset.preferencePanes ? window.frameElement : undefined;
|
|
820
821
|
const shell = element("div", "pp-panel");
|
|
821
822
|
shell.dataset.module = definition.module;
|
|
822
|
-
const header = element("header", "pp-header");
|
|
823
|
-
const back = element("button", "pp-back", "‹");
|
|
824
|
-
back.setAttribute("aria-label", "返回");
|
|
825
|
-
back.type = "button";
|
|
826
|
-
const heading = element("h1", "pp-title", title);
|
|
827
823
|
const handlers = new Map();
|
|
828
824
|
const menuItems = [
|
|
829
825
|
{ id: "viewSettings", label: "查看设置" },
|
|
@@ -831,37 +827,51 @@ class PreferencesPanel {
|
|
|
831
827
|
{ id: "clearCaches", label: "清空缓存", destructive: true },
|
|
832
828
|
{ id: "reset", label: "重置设置", destructive: true },
|
|
833
829
|
];
|
|
834
|
-
const menu = new ActionMenu(id => runAction(id));
|
|
835
|
-
const trailing = element("span", "pp-nav-spacer");
|
|
836
|
-
trailing.append(menu.element);
|
|
837
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
|
+
}
|
|
838
845
|
let toast;
|
|
839
|
-
header.append(back, heading, trailing);
|
|
840
|
-
shell.append(header, viewport);
|
|
841
846
|
root.append(shell);
|
|
842
847
|
// 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
|
|
843
848
|
// Embedded mode publishes navigation state without host reads or mutations of the module DOM.
|
|
844
849
|
const publishNavigation = () => {
|
|
845
850
|
const actions = handlers.size ? menuItems : [];
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
+
}
|
|
854
862
|
};
|
|
855
863
|
const onAction = event => {
|
|
856
864
|
if (!saving && handlers.has(event.detail)) runAction(event.detail);
|
|
857
865
|
};
|
|
858
|
-
|
|
866
|
+
frame?.addEventListener("preferencepanes:action", onAction);
|
|
859
867
|
let timer,
|
|
860
868
|
navigation,
|
|
861
869
|
generation = 0,
|
|
862
870
|
active = null,
|
|
863
871
|
saving = false,
|
|
864
|
-
destroyed = false
|
|
872
|
+
destroyed = false,
|
|
873
|
+
currentTitle = title,
|
|
874
|
+
canGoBack = window.history.length > 1;
|
|
865
875
|
/**
|
|
866
876
|
* 展示短暂通知,不刷新设置数据。
|
|
867
877
|
* Display a transient notification without refreshing settings.
|
|
@@ -890,7 +900,6 @@ class PreferencesPanel {
|
|
|
890
900
|
}
|
|
891
901
|
// 宿主接管时不创建网页 Toast,也不运行其计时器。
|
|
892
902
|
// A host-owned notice creates no web Toast and starts no local timer.
|
|
893
|
-
const frame = window.frameElement;
|
|
894
903
|
if (frame && !frame.dispatchEvent(new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:notice", { cancelable: true, detail: { kind: event.kind, message } }))) return;
|
|
895
904
|
if (!toast) {
|
|
896
905
|
toast = element("div", "pp-toast");
|
|
@@ -928,8 +937,8 @@ class PreferencesPanel {
|
|
|
928
937
|
async function open(module) {
|
|
929
938
|
const version = ++generation;
|
|
930
939
|
active = module;
|
|
931
|
-
|
|
932
|
-
|
|
940
|
+
currentTitle = module;
|
|
941
|
+
canGoBack = window.history.length > 1;
|
|
933
942
|
publishNavigation();
|
|
934
943
|
viewport.replaceChildren(statusView("读取设置…"));
|
|
935
944
|
try {
|
|
@@ -948,7 +957,7 @@ class PreferencesPanel {
|
|
|
948
957
|
*/
|
|
949
958
|
function controls() {
|
|
950
959
|
const { definition, values } = client.snapshot();
|
|
951
|
-
|
|
960
|
+
currentTitle = definition.metadata?.name || active;
|
|
952
961
|
const view = element("section", "pp-fields");
|
|
953
962
|
/**
|
|
954
963
|
* 挂载后执行的多行高度更新
|
|
@@ -968,8 +977,8 @@ class PreferencesPanel {
|
|
|
968
977
|
*/
|
|
969
978
|
const updateNavigation = () => {
|
|
970
979
|
const editor = editors.get(navigation.current);
|
|
971
|
-
|
|
972
|
-
|
|
980
|
+
currentTitle = editor?.title ?? definition.metadata?.name ?? active;
|
|
981
|
+
canGoBack = !saving && navigation.canGoBack;
|
|
973
982
|
publishNavigation();
|
|
974
983
|
};
|
|
975
984
|
/**
|
|
@@ -983,7 +992,7 @@ class PreferencesPanel {
|
|
|
983
992
|
function perform(action, success, failure = () => {}) {
|
|
984
993
|
pendingWrites++;
|
|
985
994
|
saving = true;
|
|
986
|
-
|
|
995
|
+
canGoBack = false;
|
|
987
996
|
publishNavigation();
|
|
988
997
|
queue = queue
|
|
989
998
|
.then(action)
|
|
@@ -999,7 +1008,7 @@ class PreferencesPanel {
|
|
|
999
1008
|
pendingWrites--;
|
|
1000
1009
|
saving = pendingWrites > 0;
|
|
1001
1010
|
if (destroyed && !saving) client.leave();
|
|
1002
|
-
|
|
1011
|
+
canGoBack = !saving && navigation.canGoBack;
|
|
1003
1012
|
publishNavigation();
|
|
1004
1013
|
});
|
|
1005
1014
|
return queue;
|
|
@@ -1273,16 +1282,17 @@ class PreferencesPanel {
|
|
|
1273
1282
|
* Loaded forms delegate back to navigation; loading views can return to the previous document.
|
|
1274
1283
|
* @returns {void} 无返回值 / No return value.
|
|
1275
1284
|
*/
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1285
|
+
if (back)
|
|
1286
|
+
back.onclick = () => {
|
|
1287
|
+
if (saving) return;
|
|
1288
|
+
if (navigation) navigation.back();
|
|
1289
|
+
else window.history.back();
|
|
1290
|
+
};
|
|
1281
1291
|
open(definition.module);
|
|
1282
1292
|
return () => {
|
|
1283
1293
|
destroyed = true;
|
|
1284
|
-
menu
|
|
1285
|
-
|
|
1294
|
+
menu?.destroy();
|
|
1295
|
+
frame?.removeEventListener("preferencepanes:action", onAction);
|
|
1286
1296
|
navigation?.destroy();
|
|
1287
1297
|
generation++;
|
|
1288
1298
|
if (active && !saving) client.leave();
|
|
@@ -1301,9 +1311,10 @@ class PreferencesPanel {
|
|
|
1301
1311
|
}
|
|
1302
1312
|
}
|
|
1303
1313
|
|
|
1304
|
-
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
|
|
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";
|
|
1305
1315
|
|
|
1306
1316
|
const selector = "style[data-preference-panes-defaults]";
|
|
1317
|
+
const stylesheetSelector = "link[data-preference-panes-stylesheet]";
|
|
1307
1318
|
|
|
1308
1319
|
/**
|
|
1309
1320
|
* 在文档中安装一次默认样式,并标记当前调用方是否拥有该节点。
|
|
@@ -1317,7 +1328,7 @@ function installDefaultStyles(document) {
|
|
|
1317
1328
|
const element = document.createElement("style");
|
|
1318
1329
|
element.dataset.preferencePanesDefaults = "";
|
|
1319
1330
|
element.textContent = defaults;
|
|
1320
|
-
document.head.
|
|
1331
|
+
document.head.insertBefore(element, document.head.querySelector(stylesheetSelector));
|
|
1321
1332
|
return { element, owned: true };
|
|
1322
1333
|
}
|
|
1323
1334
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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;
|
|
@@ -147,8 +147,8 @@ class ActionMenu {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
* Module document container: preserve HTML verbatim and mark only the module identity on the iframe.
|
|
150
|
+
* 模块文档容器:发送 GET 请求并将原始 HTML 交给 iframe,只在元素上标记模块身份。
|
|
151
|
+
* Module document container: send a GET request, preserve HTML verbatim and mark only the module identity on the iframe.
|
|
152
152
|
*/
|
|
153
153
|
class ModuleFrame extends EventTarget {
|
|
154
154
|
#url;
|
|
@@ -173,14 +173,14 @@ class ModuleFrame extends EventTarget {
|
|
|
173
173
|
* 建立 iframe;调用方挂载 element 后调用 load。
|
|
174
174
|
* Create the iframe; callers mount element and then call load.
|
|
175
175
|
* @param {string | URL} url 模块请求地址 / Module request URL.
|
|
176
|
-
* @param {{signal?: AbortSignal}} [options]
|
|
176
|
+
* @param {{signal?: AbortSignal, headers?: HeadersInit}} [options] 请求选项 / Request options.
|
|
177
177
|
*/
|
|
178
178
|
constructor(url, options = {}) {
|
|
179
179
|
super();
|
|
180
180
|
this.#url = new URL(url, document.baseURI);
|
|
181
181
|
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
|
|
182
182
|
if (!match) throw new TypeError("Open a concrete module URL");
|
|
183
|
-
this.#options = options;
|
|
183
|
+
this.#options = { signal: options.signal, headers: options.headers };
|
|
184
184
|
this.element = document.createElement("iframe");
|
|
185
185
|
this.element.title = `${match[1]} 设置`;
|
|
186
186
|
this.element.dataset.preferencePanes = "true";
|
|
@@ -209,7 +209,7 @@ class ModuleFrame extends EventTarget {
|
|
|
209
209
|
if (this.#options.signal?.aborted) this.destroy();
|
|
210
210
|
const timer = setTimeout(() => this.#controller.abort(), 10000);
|
|
211
211
|
try {
|
|
212
|
-
const response = await fetch(this.#url, { cache: "no-store", credentials: "omit", signal: this.#controller.signal });
|
|
212
|
+
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", headers: this.#options.headers, signal: this.#controller.signal });
|
|
213
213
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
214
214
|
const html = await response.text();
|
|
215
215
|
this.#controller.signal.throwIfAborted();
|
|
@@ -276,8 +276,8 @@ function requestConfirmation(host, message) {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
279
|
+
* 独立页面的三点按钮和底部操作菜单;嵌入页面由宿主提供对应界面。
|
|
280
|
+
* Overflow trigger and bottom action sheet for standalone pages; embedded pages use host-provided chrome.
|
|
281
281
|
*/
|
|
282
282
|
class ActionMenu {
|
|
283
283
|
#button;
|
|
@@ -817,13 +817,9 @@ class PreferencesPanel {
|
|
|
817
817
|
const title = definition.metadata?.name ?? definition.module;
|
|
818
818
|
const document = root.ownerDocument;
|
|
819
819
|
const window = document.defaultView;
|
|
820
|
+
const frame = window.frameElement?.dataset.preferencePanes ? window.frameElement : undefined;
|
|
820
821
|
const shell = element("div", "pp-panel");
|
|
821
822
|
shell.dataset.module = definition.module;
|
|
822
|
-
const header = element("header", "pp-header");
|
|
823
|
-
const back = element("button", "pp-back", "‹");
|
|
824
|
-
back.setAttribute("aria-label", "返回");
|
|
825
|
-
back.type = "button";
|
|
826
|
-
const heading = element("h1", "pp-title", title);
|
|
827
823
|
const handlers = new Map();
|
|
828
824
|
const menuItems = [
|
|
829
825
|
{ id: "viewSettings", label: "查看设置" },
|
|
@@ -831,37 +827,51 @@ class PreferencesPanel {
|
|
|
831
827
|
{ id: "clearCaches", label: "清空缓存", destructive: true },
|
|
832
828
|
{ id: "reset", label: "重置设置", destructive: true },
|
|
833
829
|
];
|
|
834
|
-
const menu = new ActionMenu(id => runAction(id));
|
|
835
|
-
const trailing = element("span", "pp-nav-spacer");
|
|
836
|
-
trailing.append(menu.element);
|
|
837
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
|
+
}
|
|
838
845
|
let toast;
|
|
839
|
-
header.append(back, heading, trailing);
|
|
840
|
-
shell.append(header, viewport);
|
|
841
846
|
root.append(shell);
|
|
842
847
|
// 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
|
|
843
848
|
// Embedded mode publishes navigation state without host reads or mutations of the module DOM.
|
|
844
849
|
const publishNavigation = () => {
|
|
845
850
|
const actions = handlers.size ? menuItems : [];
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
+
}
|
|
854
862
|
};
|
|
855
863
|
const onAction = event => {
|
|
856
864
|
if (!saving && handlers.has(event.detail)) runAction(event.detail);
|
|
857
865
|
};
|
|
858
|
-
|
|
866
|
+
frame?.addEventListener("preferencepanes:action", onAction);
|
|
859
867
|
let timer,
|
|
860
868
|
navigation,
|
|
861
869
|
generation = 0,
|
|
862
870
|
active = null,
|
|
863
871
|
saving = false,
|
|
864
|
-
destroyed = false
|
|
872
|
+
destroyed = false,
|
|
873
|
+
currentTitle = title,
|
|
874
|
+
canGoBack = window.history.length > 1;
|
|
865
875
|
/**
|
|
866
876
|
* 展示短暂通知,不刷新设置数据。
|
|
867
877
|
* Display a transient notification without refreshing settings.
|
|
@@ -890,7 +900,6 @@ class PreferencesPanel {
|
|
|
890
900
|
}
|
|
891
901
|
// 宿主接管时不创建网页 Toast,也不运行其计时器。
|
|
892
902
|
// A host-owned notice creates no web Toast and starts no local timer.
|
|
893
|
-
const frame = window.frameElement;
|
|
894
903
|
if (frame && !frame.dispatchEvent(new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:notice", { cancelable: true, detail: { kind: event.kind, message } }))) return;
|
|
895
904
|
if (!toast) {
|
|
896
905
|
toast = element("div", "pp-toast");
|
|
@@ -928,8 +937,8 @@ class PreferencesPanel {
|
|
|
928
937
|
async function open(module) {
|
|
929
938
|
const version = ++generation;
|
|
930
939
|
active = module;
|
|
931
|
-
|
|
932
|
-
|
|
940
|
+
currentTitle = module;
|
|
941
|
+
canGoBack = window.history.length > 1;
|
|
933
942
|
publishNavigation();
|
|
934
943
|
viewport.replaceChildren(statusView("读取设置…"));
|
|
935
944
|
try {
|
|
@@ -948,7 +957,7 @@ class PreferencesPanel {
|
|
|
948
957
|
*/
|
|
949
958
|
function controls() {
|
|
950
959
|
const { definition, values } = client.snapshot();
|
|
951
|
-
|
|
960
|
+
currentTitle = definition.metadata?.name || active;
|
|
952
961
|
const view = element("section", "pp-fields");
|
|
953
962
|
/**
|
|
954
963
|
* 挂载后执行的多行高度更新
|
|
@@ -968,8 +977,8 @@ class PreferencesPanel {
|
|
|
968
977
|
*/
|
|
969
978
|
const updateNavigation = () => {
|
|
970
979
|
const editor = editors.get(navigation.current);
|
|
971
|
-
|
|
972
|
-
|
|
980
|
+
currentTitle = editor?.title ?? definition.metadata?.name ?? active;
|
|
981
|
+
canGoBack = !saving && navigation.canGoBack;
|
|
973
982
|
publishNavigation();
|
|
974
983
|
};
|
|
975
984
|
/**
|
|
@@ -983,7 +992,7 @@ class PreferencesPanel {
|
|
|
983
992
|
function perform(action, success, failure = () => {}) {
|
|
984
993
|
pendingWrites++;
|
|
985
994
|
saving = true;
|
|
986
|
-
|
|
995
|
+
canGoBack = false;
|
|
987
996
|
publishNavigation();
|
|
988
997
|
queue = queue
|
|
989
998
|
.then(action)
|
|
@@ -999,7 +1008,7 @@ class PreferencesPanel {
|
|
|
999
1008
|
pendingWrites--;
|
|
1000
1009
|
saving = pendingWrites > 0;
|
|
1001
1010
|
if (destroyed && !saving) client.leave();
|
|
1002
|
-
|
|
1011
|
+
canGoBack = !saving && navigation.canGoBack;
|
|
1003
1012
|
publishNavigation();
|
|
1004
1013
|
});
|
|
1005
1014
|
return queue;
|
|
@@ -1273,16 +1282,17 @@ class PreferencesPanel {
|
|
|
1273
1282
|
* Loaded forms delegate back to navigation; loading views can return to the previous document.
|
|
1274
1283
|
* @returns {void} 无返回值 / No return value.
|
|
1275
1284
|
*/
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1285
|
+
if (back)
|
|
1286
|
+
back.onclick = () => {
|
|
1287
|
+
if (saving) return;
|
|
1288
|
+
if (navigation) navigation.back();
|
|
1289
|
+
else window.history.back();
|
|
1290
|
+
};
|
|
1281
1291
|
open(definition.module);
|
|
1282
1292
|
return () => {
|
|
1283
1293
|
destroyed = true;
|
|
1284
|
-
menu
|
|
1285
|
-
|
|
1294
|
+
menu?.destroy();
|
|
1295
|
+
frame?.removeEventListener("preferencepanes:action", onAction);
|
|
1286
1296
|
navigation?.destroy();
|
|
1287
1297
|
generation++;
|
|
1288
1298
|
if (active && !saving) client.leave();
|
|
@@ -1301,9 +1311,10 @@ class PreferencesPanel {
|
|
|
1301
1311
|
}
|
|
1302
1312
|
}
|
|
1303
1313
|
|
|
1304
|
-
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
|
|
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";
|
|
1305
1315
|
|
|
1306
1316
|
const selector = "style[data-preference-panes-defaults]";
|
|
1317
|
+
const stylesheetSelector = "link[data-preference-panes-stylesheet]";
|
|
1307
1318
|
|
|
1308
1319
|
/**
|
|
1309
1320
|
* 在文档中安装一次默认样式,并标记当前调用方是否拥有该节点。
|
|
@@ -1317,7 +1328,7 @@ function installDefaultStyles(document) {
|
|
|
1317
1328
|
const element = document.createElement("style");
|
|
1318
1329
|
element.dataset.preferencePanesDefaults = "";
|
|
1319
1330
|
element.textContent = defaults;
|
|
1320
|
-
document.head.
|
|
1331
|
+
document.head.insertBefore(element, document.head.querySelector(stylesheetSelector));
|
|
1321
1332
|
return { element, owned: true };
|
|
1322
1333
|
}
|
|
1323
1334
|
|