@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.
@@ -1,90 +1,70 @@
1
- import { normalizeBoxJs, normalizeStoredValue, validValue } from "./boxjs.mjs";
2
- import { element, resourceURL } from "./components.mjs";
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
- * 挂载模块设置页;默认样式由包提供,可选 CSS 仅作用于当前模块。
8
- * Mount a module page with package defaults and optional module-scoped CSS.
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
- export function mount(model, css = "") {
14
- if (typeof css !== "string") throw new TypeError("CSS must be a string");
15
- const definition = normalizeBoxJs(model.boxjs, model.module);
16
- const values = { ...model.values };
17
- for (const field of definition.fields) {
18
- if (values[field.key] === undefined) continue;
19
- values[field.key] = normalizeStoredValue(field, values[field.key]);
20
- if (!validValue(field, values[field.key])) throw new TypeError(`Invalid stored value: ${field.key}`);
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
- for (const field of definition.fields) if (values[field.key] === undefined && Object.hasOwn(field, "defaultValue")) values[field.key] = structuredClone(field.defaultValue);
23
- const rendered = { ...model, definition, values };
24
- const metadata = definition.metadata ?? {};
25
- const image = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
26
- if (image) resourceURL(image);
27
- if (metadata.repo) resourceURL(metadata.repo);
28
- const existing = document.querySelector("#preferences");
29
- const root = existing ?? element("main", "");
30
- if (!existing) {
31
- root.id = "preferences";
32
- document.body.append(root);
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
- const { element: base, owned: ownsBase } = installDefaultStyles(document);
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
- * 跟随嵌入宿主的通用环境状态,不识别业务 App 或解析其 UA。
45
- * Follow generic host appearance without detecting a business app or parsing its user agent.
46
- * @returns {void} 已同步主题与键盘避让 / Theme and keyboard clearance synchronized.
49
+ * 释放页面视图和页面级监听器。
50
+ * Release the page view and page-level listener.
51
+ * @returns {void} 无返回值 / No return value.
47
52
  */
48
- const syncAppearance = () => {
49
- const theme = host?.dataset.theme ?? previousTheme ?? (systemTheme.matches ? "dark" : "light");
50
- document.documentElement.dataset.theme = theme;
51
- if (host) document.documentElement.style.setProperty("--pp-keyboard-height", host.style.getPropertyValue("--pp-keyboard-height"));
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
- document.title = metadata.name ?? definition.module;
61
- let panel;
62
- const view = {
63
- /**
64
- * 释放模块视图、样式与会话,不操作项目入口页。
65
- * Release the module view, styles and session without operating a project landing page.
66
- * @returns {void} 无返回值 / No return value.
67
- */
68
- destroy() {
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();
@@ -9,6 +9,6 @@
9
9
  </head>
10
10
  <body>
11
11
  <main id="preferences"></main>
12
- <script type="module" src="/settings/assets/app.mjs?v=__VERSION__"></script>
12
+ <script type="module" src="/settings/assets/index.mjs?v=__VERSION__"></script>
13
13
  </body>
14
14
  </html>
@@ -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
+ }