@nsnanocat/preference-panes 1.0.0 → 1.1.1
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 +64 -44
- package/dist/api.js +11 -31
- package/dist/module/index.html +1 -1
- package/dist/module/{app.mjs → index.mjs} +764 -623
- package/dist/module/navigation.mjs +15 -32
- package/dist/preference-panes.mjs +703 -566
- package/dist/web.js +2 -23
- package/package.json +2 -2
- package/src/api.mjs +11 -31
- package/src/browser/ModuleFrame.mjs +13 -13
- package/src/browser/ModuleStatus.mjs +2 -3
- package/src/browser/Navigation.d.mts +2 -4
- package/src/browser/client.d.mts +10 -15
- package/src/browser/client.mjs +154 -48
- package/src/browser/index.d.ts +6 -7
- package/src/browser/index.mjs +59 -79
- package/src/browser/module.html +1 -1
- package/src/browser/mount.mjs +103 -0
- package/src/browser/panel.mjs +464 -444
- package/src/index.d.ts +0 -22
- package/src/index.mjs +3 -3
- package/src/web.mjs +1 -5
- package/src/browser/app.mjs +0 -49
- package/src/build.mjs +0 -20
- package/src/lib/page-inputs.mjs +0 -17
package/src/browser/index.mjs
CHANGED
|
@@ -1,90 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { mountPanel } from "./panel.mjs";
|
|
1
|
+
import { statusView } from "./components.mjs";
|
|
2
|
+
import { mount } from "./mount.mjs";
|
|
4
3
|
import { installDefaultStyles } from "./styles.mjs";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @param {import("../index.js").ModuleModel} model API 返回的模块模型 / Module model returned by the API.
|
|
10
|
-
* @param {string} [css] 可选 CSS 正文 / Optional CSS text.
|
|
11
|
-
* @returns {import("./index.js").MountedPreferences} 模块生命周期句柄 / Module lifecycle handle.
|
|
6
|
+
* 管理模块文档的配置请求、重载和错误状态。
|
|
7
|
+
* Manage configuration requests, reloads, and error states for a module document.
|
|
12
8
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
class ModulePage {
|
|
10
|
+
#window;
|
|
11
|
+
#root;
|
|
12
|
+
#view;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 创建模块页面控制器并安装基础样式。
|
|
16
|
+
* Create the module page controller and install base styles.
|
|
17
|
+
* @param {Document} document 模块文档 / Module document.
|
|
18
|
+
*/
|
|
19
|
+
constructor(document) {
|
|
20
|
+
this.#window = document.defaultView;
|
|
21
|
+
this.#root = document.querySelector("#preferences");
|
|
22
|
+
installDefaultStyles(document);
|
|
23
|
+
this.#window.addEventListener("pageshow", this.#show);
|
|
21
24
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 从规范模块路径读取 BoxJS JSON 并挂载通用前端。
|
|
28
|
+
* Read BoxJS JSON from the conventional module path and mount the generic frontend.
|
|
29
|
+
* @returns {Promise<void>} 启动完成 / Startup completion.
|
|
30
|
+
*/
|
|
31
|
+
async start() {
|
|
32
|
+
try {
|
|
33
|
+
this.#view?.destroy();
|
|
34
|
+
this.#view = undefined;
|
|
35
|
+
this.#root.replaceChildren(statusView("读取设置…"));
|
|
36
|
+
const embedded = this.#window.frameElement?.dataset.preferencePanesModule;
|
|
37
|
+
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
|
|
38
|
+
const module = embedded ?? match?.[1];
|
|
39
|
+
if (!module) throw new TypeError("Open a concrete module URL");
|
|
40
|
+
const response = await fetch(`/configs/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
|
|
41
|
+
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
42
|
+
this.#view = mount(await response.json());
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.#root.replaceChildren(statusView(`加载失败:${error.message}`, () => this.start()));
|
|
45
|
+
}
|
|
33
46
|
}
|
|
34
|
-
|
|
35
|
-
const custom = element("style", "");
|
|
36
|
-
custom.textContent = css;
|
|
37
|
-
document.head.append(custom);
|
|
38
|
-
const previousTitle = document.title;
|
|
39
|
-
const previousTheme = document.documentElement.dataset.theme;
|
|
40
|
-
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
41
|
-
const previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
|
|
42
|
-
const host = window.frameElement?.ownerDocument.documentElement;
|
|
47
|
+
|
|
43
48
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @returns {void}
|
|
49
|
+
* 释放页面视图和页面级监听器。
|
|
50
|
+
* Release the page view and page-level listener.
|
|
51
|
+
* @returns {void} 无返回值 / No return value.
|
|
47
52
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
let observer;
|
|
54
|
-
syncAppearance();
|
|
55
|
-
systemTheme.addEventListener("change", syncAppearance);
|
|
56
|
-
if (host) {
|
|
57
|
-
observer = new MutationObserver(syncAppearance);
|
|
58
|
-
observer.observe(host, { attributes: true, attributeFilter: ["data-theme", "style"] });
|
|
53
|
+
destroy() {
|
|
54
|
+
this.#window.removeEventListener("pageshow", this.#show);
|
|
55
|
+
this.#view?.destroy();
|
|
56
|
+
this.#view = undefined;
|
|
59
57
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
observer?.disconnect();
|
|
70
|
-
systemTheme.removeEventListener("change", syncAppearance);
|
|
71
|
-
panel?.destroy();
|
|
72
|
-
if (ownsBase) base.remove();
|
|
73
|
-
custom.remove();
|
|
74
|
-
if (existing) root.replaceChildren();
|
|
75
|
-
else root.remove();
|
|
76
|
-
document.title = previousTitle;
|
|
77
|
-
if (previousTheme === undefined) delete document.documentElement.dataset.theme;
|
|
78
|
-
else document.documentElement.dataset.theme = previousTheme;
|
|
79
|
-
document.documentElement.style.setProperty("--pp-keyboard-height", previousKeyboard);
|
|
80
|
-
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从前进后退缓存恢复时重新加载模块。
|
|
61
|
+
* Reload the module when restored from the back-forward cache.
|
|
62
|
+
* @param {PageTransitionEvent} event 页面显示事件 / Page show event.
|
|
63
|
+
* @returns {void} 无返回值 / No return value.
|
|
64
|
+
*/
|
|
65
|
+
#show = event => {
|
|
66
|
+
if (event.persisted) this.start();
|
|
81
67
|
};
|
|
82
|
-
try {
|
|
83
|
-
root.replaceChildren();
|
|
84
|
-
panel = mountPanel(root, rendered);
|
|
85
|
-
return view;
|
|
86
|
-
} catch (error) {
|
|
87
|
-
view.destroy();
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
68
|
}
|
|
69
|
+
|
|
70
|
+
new ModulePage(document).start();
|
package/src/browser/module.html
CHANGED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { normalizeBoxJs } from "./boxjs.mjs";
|
|
2
|
+
import { element, resourceURL } from "./components.mjs";
|
|
3
|
+
import { PreferencesPanel } from "./panel.mjs";
|
|
4
|
+
import { installDefaultStyles } from "./styles.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 管理 BoxJS 规范化、主题同步和面板生命周期。
|
|
8
|
+
* Manage BoxJS normalization, theme synchronization, and panel lifecycle.
|
|
9
|
+
*/
|
|
10
|
+
class PreferencesView {
|
|
11
|
+
#existing;
|
|
12
|
+
#root;
|
|
13
|
+
#base;
|
|
14
|
+
#ownsBase;
|
|
15
|
+
#previousTitle;
|
|
16
|
+
#previousTheme;
|
|
17
|
+
#systemTheme;
|
|
18
|
+
#previousKeyboard;
|
|
19
|
+
#host;
|
|
20
|
+
#observer;
|
|
21
|
+
#panel;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 使用原始 BoxJS JSON 挂载设置页。
|
|
25
|
+
* Mount a settings page from raw BoxJS JSON.
|
|
26
|
+
* @param {import("../index.js").BoxJSInput} boxjs 单模块 BoxJS JSON / Single-module BoxJS JSON.
|
|
27
|
+
*/
|
|
28
|
+
constructor(boxjs) {
|
|
29
|
+
const definition = normalizeBoxJs(boxjs);
|
|
30
|
+
const metadata = definition.metadata ?? {};
|
|
31
|
+
const image = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
|
|
32
|
+
if (image) resourceURL(image);
|
|
33
|
+
if (metadata.repo) resourceURL(metadata.repo);
|
|
34
|
+
|
|
35
|
+
this.#existing = document.querySelector("#preferences");
|
|
36
|
+
this.#root = this.#existing ?? element("main", "");
|
|
37
|
+
if (!this.#existing) {
|
|
38
|
+
this.#root.id = "preferences";
|
|
39
|
+
document.body.append(this.#root);
|
|
40
|
+
}
|
|
41
|
+
const styles = installDefaultStyles(document);
|
|
42
|
+
this.#base = styles.element;
|
|
43
|
+
this.#ownsBase = styles.owned;
|
|
44
|
+
this.#previousTitle = document.title;
|
|
45
|
+
this.#previousTheme = document.documentElement.dataset.theme;
|
|
46
|
+
this.#systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
47
|
+
this.#previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
|
|
48
|
+
this.#host = window.frameElement?.ownerDocument.documentElement;
|
|
49
|
+
this.#syncAppearance();
|
|
50
|
+
this.#systemTheme.addEventListener("change", this.#syncAppearance);
|
|
51
|
+
if (this.#host) {
|
|
52
|
+
this.#observer = new MutationObserver(this.#syncAppearance);
|
|
53
|
+
this.#observer.observe(this.#host, { attributes: true, attributeFilter: ["data-theme", "style"] });
|
|
54
|
+
}
|
|
55
|
+
document.title = metadata.name ?? definition.module;
|
|
56
|
+
try {
|
|
57
|
+
this.#root.replaceChildren();
|
|
58
|
+
this.#panel = new PreferencesPanel(this.#root, definition);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.destroy();
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 跟随嵌入宿主的通用环境状态,不识别业务 App 或解析其 UA。
|
|
67
|
+
* Follow generic host appearance without detecting a business App or parsing its UA.
|
|
68
|
+
* @returns {void} 已同步主题与键盘避让 / Theme and keyboard clearance synchronized.
|
|
69
|
+
*/
|
|
70
|
+
#syncAppearance = () => {
|
|
71
|
+
const theme = this.#host?.dataset.theme ?? this.#previousTheme ?? (this.#systemTheme.matches ? "dark" : "light");
|
|
72
|
+
document.documentElement.dataset.theme = theme;
|
|
73
|
+
if (this.#host) document.documentElement.style.setProperty("--pp-keyboard-height", this.#host.style.getPropertyValue("--pp-keyboard-height"));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 释放模块视图、样式与会话,不操作项目入口页。
|
|
78
|
+
* Release the module view, styles, and session without operating a project landing page.
|
|
79
|
+
* @returns {void} 无返回值 / No return value.
|
|
80
|
+
*/
|
|
81
|
+
destroy() {
|
|
82
|
+
this.#observer?.disconnect();
|
|
83
|
+
this.#systemTheme.removeEventListener("change", this.#syncAppearance);
|
|
84
|
+
this.#panel?.destroy();
|
|
85
|
+
if (this.#ownsBase) this.#base.remove();
|
|
86
|
+
if (this.#existing) this.#root.replaceChildren();
|
|
87
|
+
else this.#root.remove();
|
|
88
|
+
document.title = this.#previousTitle;
|
|
89
|
+
if (this.#previousTheme === undefined) delete document.documentElement.dataset.theme;
|
|
90
|
+
else document.documentElement.dataset.theme = this.#previousTheme;
|
|
91
|
+
document.documentElement.style.setProperty("--pp-keyboard-height", this.#previousKeyboard);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 使用原始 BoxJS JSON 挂载设置页。
|
|
97
|
+
* Mount a settings page from raw BoxJS JSON.
|
|
98
|
+
* @param {import("../index.js").BoxJSInput} boxjs 单模块 BoxJS JSON / Single-module BoxJS JSON.
|
|
99
|
+
* @returns {import("./index.js").MountedPreferences} 模块视图 / Module view.
|
|
100
|
+
*/
|
|
101
|
+
export function mount(boxjs) {
|
|
102
|
+
return new PreferencesView(boxjs);
|
|
103
|
+
}
|