@nsnanocat/preference-panes 0.9.16 → 1.1.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.
@@ -0,0 +1,119 @@
1
+ import { normalizeBoxJs, normalizeStoredValue, validValue } 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
+ * 管理模块设置视图的模型规范化、样式、主题同步和面板生命周期。
8
+ * Manage model normalization, styles, theme synchronization, and panel lifecycle for a module settings view.
9
+ */
10
+ export class PreferencesView {
11
+ #existing;
12
+ #root;
13
+ #base;
14
+ #ownsBase;
15
+ #custom;
16
+ #previousTitle;
17
+ #previousTheme;
18
+ #systemTheme;
19
+ #previousKeyboard;
20
+ #host;
21
+ #observer;
22
+ #panel;
23
+
24
+ /**
25
+ * 使用模块 API 返回的模型挂载设置页。
26
+ * Mount a settings page from the model returned by the module API.
27
+ * @param {import("../index.js").ModuleModel} model API 返回的模块模型 / Module model returned by the API.
28
+ * @param {string} [css] 可选 CSS 正文 / Optional module-scoped CSS text.
29
+ */
30
+ constructor(model, css = "") {
31
+ if (typeof css !== "string") throw new TypeError("CSS must be a string");
32
+ const definition = normalizeBoxJs(model.boxjs, model.module);
33
+ const values = { ...model.values };
34
+ for (const field of definition.fields) {
35
+ if (values[field.key] === undefined) continue;
36
+ values[field.key] = normalizeStoredValue(field, values[field.key]);
37
+ if (!validValue(field, values[field.key])) throw new TypeError(`Invalid stored value: ${field.key}`);
38
+ }
39
+ for (const field of definition.fields) if (values[field.key] === undefined && Object.hasOwn(field, "defaultValue")) values[field.key] = structuredClone(field.defaultValue);
40
+ const rendered = { ...model, definition, values };
41
+ const metadata = definition.metadata ?? {};
42
+ const image = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
43
+ if (image) resourceURL(image);
44
+ if (metadata.repo) resourceURL(metadata.repo);
45
+
46
+ this.#existing = document.querySelector("#preferences");
47
+ this.#root = this.#existing ?? element("main", "");
48
+ if (!this.#existing) {
49
+ this.#root.id = "preferences";
50
+ document.body.append(this.#root);
51
+ }
52
+ const styles = installDefaultStyles(document);
53
+ this.#base = styles.element;
54
+ this.#ownsBase = styles.owned;
55
+ this.#custom = element("style", "");
56
+ this.#custom.textContent = css;
57
+ document.head.append(this.#custom);
58
+ this.#previousTitle = document.title;
59
+ this.#previousTheme = document.documentElement.dataset.theme;
60
+ this.#systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
61
+ this.#previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
62
+ this.#host = window.frameElement?.ownerDocument.documentElement;
63
+ this.#syncAppearance();
64
+ this.#systemTheme.addEventListener("change", this.#syncAppearance);
65
+ if (this.#host) {
66
+ this.#observer = new MutationObserver(this.#syncAppearance);
67
+ this.#observer.observe(this.#host, { attributes: true, attributeFilter: ["data-theme", "style"] });
68
+ }
69
+ document.title = metadata.name ?? definition.module;
70
+ try {
71
+ this.#root.replaceChildren();
72
+ this.#panel = new PreferencesPanel(this.#root, rendered);
73
+ } catch (error) {
74
+ this.destroy();
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 跟随嵌入宿主的通用环境状态,不识别业务 App 或解析其 UA。
81
+ * Follow generic host appearance without detecting a business App or parsing its UA.
82
+ * @returns {void} 已同步主题与键盘避让 / Theme and keyboard clearance synchronized.
83
+ */
84
+ #syncAppearance = () => {
85
+ const theme = this.#host?.dataset.theme ?? this.#previousTheme ?? (this.#systemTheme.matches ? "dark" : "light");
86
+ document.documentElement.dataset.theme = theme;
87
+ if (this.#host) document.documentElement.style.setProperty("--pp-keyboard-height", this.#host.style.getPropertyValue("--pp-keyboard-height"));
88
+ };
89
+
90
+ /**
91
+ * 释放模块视图、样式与会话,不操作项目入口页。
92
+ * Release the module view, styles, and session without operating a project landing page.
93
+ * @returns {void} 无返回值 / No return value.
94
+ */
95
+ destroy() {
96
+ this.#observer?.disconnect();
97
+ this.#systemTheme.removeEventListener("change", this.#syncAppearance);
98
+ this.#panel?.destroy();
99
+ if (this.#ownsBase) this.#base.remove();
100
+ this.#custom.remove();
101
+ if (this.#existing) this.#root.replaceChildren();
102
+ else this.#root.remove();
103
+ document.title = this.#previousTitle;
104
+ if (this.#previousTheme === undefined) delete document.documentElement.dataset.theme;
105
+ else document.documentElement.dataset.theme = this.#previousTheme;
106
+ document.documentElement.style.setProperty("--pp-keyboard-height", this.#previousKeyboard);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 使用模块 API 返回的模型挂载设置页;CSS 仅覆盖当前模块。
112
+ * Mount a settings page from a module API model; CSS only overrides this module.
113
+ * @param {import("../index.js").ModuleModel} model API 返回的模块模型 / Module model returned by the API.
114
+ * @param {string} [css] 可选 CSS 正文 / Optional module-scoped CSS text.
115
+ * @returns {PreferencesView} 模块视图 / Module view.
116
+ */
117
+ export function mount(model, css = "") {
118
+ return new PreferencesView(model, css);
119
+ }