@nsnanocat/preference-panes 1.1.0 → 1.1.2

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,4 +1,4 @@
1
- import type { ModuleModel } from "../index.js";
1
+ import type { BoxJSInput } from "../index.js";
2
2
 
3
3
  /**
4
4
  * 具体模块设置页的生命周期句柄。
@@ -9,25 +9,9 @@ export interface MountedPreferences {
9
9
  destroy(): void;
10
10
  }
11
11
  /**
12
- * 管理模块设置视图的模型、样式、主题和面板生命周期。
13
- * Manage model, styles, theme, and panel lifecycle for a module settings view.
14
- */
15
- export class PreferencesView implements MountedPreferences {
16
- /**
17
- * 使用模块 API 返回的模型挂载设置页。
18
- * Mount a settings page from the model returned by the module API.
19
- * @param model 模块 API 模型 / Module API model.
20
- * @param css 可选 CSS 正文 / Optional CSS text.
21
- */
22
- constructor(model: ModuleModel, css?: string);
23
- /** 移除页面、样式和监听器 / Remove page, styles and listeners. */
24
- destroy(): void;
25
- }
26
- /**
27
- * 使用模块 API 返回的模型挂载设置页;CSS 仅覆盖当前模块。
28
- * Mount a settings page from a module API model; CSS only overrides this module.
29
- * @param model 模块 API 模型 / Module API model.
30
- * @param css 可选 CSS 正文 / Optional CSS text.
12
+ * 使用原始 BoxJS JSON 挂载设置页。
13
+ * Mount a settings page from raw BoxJS JSON.
14
+ * @param boxjs 恰好包含一个模块的 BoxJS JSON / BoxJS JSON describing exactly one module.
31
15
  * @returns 模块视图 / Module view.
32
16
  */
33
- export function mount(model: ModuleModel, css?: string): PreferencesView;
17
+ export function mount(boxjs: BoxJSInput): MountedPreferences;
@@ -1,14 +1,13 @@
1
- import { pageInputs } from "../lib/page-inputs.mjs";
1
+ import { normalizeBoxJs } from "./boxjs.mjs";
2
2
  import { statusView } from "./components.mjs";
3
- import { PreferencesView } from "./mount.mjs";
3
+ import { mount } from "./mount.mjs";
4
4
  import { installDefaultStyles } from "./styles.mjs";
5
5
 
6
6
  /**
7
- * 管理模块文档的页面输入、初始请求、重载和错误状态。
8
- * Manage page inputs, initial requests, reloads, and error states for a module document.
7
+ * 管理模块文档的配置请求、重载和错误状态。
8
+ * Manage configuration requests, reloads, and error states for a module document.
9
9
  */
10
- export class ModulePage {
11
- #document;
10
+ class ModulePage {
12
11
  #window;
13
12
  #root;
14
13
  #view;
@@ -19,7 +18,6 @@ export class ModulePage {
19
18
  * @param {Document} document 模块文档 / Module document.
20
19
  */
21
20
  constructor(document) {
22
- this.#document = document;
23
21
  this.#window = document.defaultView;
24
22
  this.#root = document.querySelector("#preferences");
25
23
  installDefaultStyles(document);
@@ -27,8 +25,8 @@ export class ModulePage {
27
25
  }
28
26
 
29
27
  /**
30
- * URL 或代理传递的 Header 导入 JSON/CSS,支持独立文档与 srcdoc。
31
- * Import JSON/CSS from the URL or proxy-carried headers in standalone and srcdoc documents.
28
+ * 通过模块 API 读取 BoxJS JSON 并挂载通用前端。
29
+ * Read BoxJS JSON through the module API and mount the generic frontend.
32
30
  * @returns {Promise<void>} 启动完成 / Startup completion.
33
31
  */
34
32
  async start() {
@@ -36,12 +34,15 @@ export class ModulePage {
36
34
  this.#view?.destroy();
37
35
  this.#view = undefined;
38
36
  this.#root.replaceChildren(statusView("读取设置…"));
39
- const inputs = this.#readInputs();
40
- const apiURL = new URL(`/api/${encodeURIComponent(inputs.module)}`, inputs.url).href;
41
- const styleURL = this.#resourceURL(inputs.css, inputs.url);
42
- const [style, modelResponse] = await Promise.all([styleURL ? fetch(styleURL, { cache: "no-store", credentials: "omit" }) : null, fetch(apiURL, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json", "X-PreferencePanes-JSON": inputs.json } })]);
43
- if ((style && style.status !== 200) || modelResponse.status !== 200) throw new Error(`HTTP ${modelResponse.status !== 200 ? modelResponse.status : style.status}`);
44
- this.#view = new PreferencesView(await modelResponse.json(), style ? await style.text() : "");
37
+ const embedded = this.#window.frameElement?.dataset.preferencePanesModule;
38
+ const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
39
+ const module = embedded ?? match?.[1];
40
+ if (!module) throw new TypeError("Open a concrete module URL");
41
+ const response = await fetch(`/api/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
42
+ if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
43
+ const boxjs = await response.json();
44
+ normalizeBoxJs(boxjs, module);
45
+ this.#view = mount(boxjs);
45
46
  } catch (error) {
46
47
  this.#root.replaceChildren(statusView(`加载失败:${error.message}`, () => this.start()));
47
48
  }
@@ -58,39 +59,6 @@ export class ModulePage {
58
59
  this.#view = undefined;
59
60
  }
60
61
 
61
- /**
62
- * 读取嵌入参数、文档元数据或当前 URL 输入。
63
- * Read embedded parameters, document metadata, or current URL inputs.
64
- * @returns {ReturnType<typeof pageInputs>} 页面输入 / Page inputs.
65
- */
66
- #readInputs() {
67
- const context = this.#document.querySelector('meta[name="preference-panes-inputs"]');
68
- const embedded = this.#window.frameElement?.dataset.preferencePanes;
69
- switch (true) {
70
- case embedded !== undefined:
71
- this.#document.documentElement.dataset.preferencePanesEmbedded = "";
72
- return JSON.parse(embedded);
73
- case context !== null:
74
- return JSON.parse(decodeURIComponent(context.content));
75
- default:
76
- return pageInputs(new URL(this.#window.location.href));
77
- }
78
- }
79
-
80
- /**
81
- * 将可选页面资源限制为 HTTP(S) 地址。
82
- * Restrict an optional page resource to an HTTP(S) URL.
83
- * @param {string | undefined} source 资源地址 / Resource location.
84
- * @param {string} baseURL 页面基准地址 / Page base URL.
85
- * @returns {string | null} 绝对资源地址 / Absolute resource URL.
86
- */
87
- #resourceURL(source, baseURL) {
88
- if (!source) return null;
89
- const url = new URL(source, baseURL);
90
- if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Resources must use HTTP(S) URLs");
91
- return url.href;
92
- }
93
-
94
62
  /**
95
63
  * 从前进后退缓存恢复时重新加载模块。
96
64
  * Reload the module when restored from the back-forward cache.
@@ -1,18 +1,17 @@
1
- import { normalizeBoxJs, normalizeStoredValue, validValue } from "./boxjs.mjs";
1
+ import { normalizeBoxJs } from "./boxjs.mjs";
2
2
  import { element, resourceURL } from "./components.mjs";
3
3
  import { PreferencesPanel } from "./panel.mjs";
4
4
  import { installDefaultStyles } from "./styles.mjs";
5
5
 
6
6
  /**
7
- * 管理模块设置视图的模型规范化、样式、主题同步和面板生命周期。
8
- * Manage model normalization, styles, theme synchronization, and panel lifecycle for a module settings view.
7
+ * 管理 BoxJS 规范化、主题同步和面板生命周期。
8
+ * Manage BoxJS normalization, theme synchronization, and panel lifecycle.
9
9
  */
10
- export class PreferencesView {
10
+ class PreferencesView {
11
11
  #existing;
12
12
  #root;
13
13
  #base;
14
14
  #ownsBase;
15
- #custom;
16
15
  #previousTitle;
17
16
  #previousTheme;
18
17
  #systemTheme;
@@ -22,22 +21,12 @@ export class PreferencesView {
22
21
  #panel;
23
22
 
24
23
  /**
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.
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.
29
27
  */
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 };
28
+ constructor(boxjs) {
29
+ const definition = normalizeBoxJs(boxjs);
41
30
  const metadata = definition.metadata ?? {};
42
31
  const image = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
43
32
  if (image) resourceURL(image);
@@ -52,9 +41,6 @@ export class PreferencesView {
52
41
  const styles = installDefaultStyles(document);
53
42
  this.#base = styles.element;
54
43
  this.#ownsBase = styles.owned;
55
- this.#custom = element("style", "");
56
- this.#custom.textContent = css;
57
- document.head.append(this.#custom);
58
44
  this.#previousTitle = document.title;
59
45
  this.#previousTheme = document.documentElement.dataset.theme;
60
46
  this.#systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
@@ -69,7 +55,7 @@ export class PreferencesView {
69
55
  document.title = metadata.name ?? definition.module;
70
56
  try {
71
57
  this.#root.replaceChildren();
72
- this.#panel = new PreferencesPanel(this.#root, rendered);
58
+ this.#panel = new PreferencesPanel(this.#root, definition);
73
59
  } catch (error) {
74
60
  this.destroy();
75
61
  throw error;
@@ -97,7 +83,6 @@ export class PreferencesView {
97
83
  this.#systemTheme.removeEventListener("change", this.#syncAppearance);
98
84
  this.#panel?.destroy();
99
85
  if (this.#ownsBase) this.#base.remove();
100
- this.#custom.remove();
101
86
  if (this.#existing) this.#root.replaceChildren();
102
87
  else this.#root.remove();
103
88
  document.title = this.#previousTitle;
@@ -108,12 +93,11 @@ export class PreferencesView {
108
93
  }
109
94
 
110
95
  /**
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.
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.
116
100
  */
117
- export function mount(model, css = "") {
118
- return new PreferencesView(model, css);
101
+ export function mount(boxjs) {
102
+ return new PreferencesView(boxjs);
119
103
  }
@@ -12,24 +12,23 @@ export class PreferencesPanel {
12
12
  #release;
13
13
 
14
14
  /**
15
- * 挂载 API 返回的模块模型表单。
16
- * Mount the module form returned by the API.
15
+ * 挂载 BoxJS 定义对应的模块表单。
16
+ * Mount the module form described by a BoxJS definition.
17
17
  * @param {HTMLElement} root 包内挂载元素 / Internal mount element.
18
- * @param {import("../index.js").ModuleModel & {definition: import("../index.js").ModuleDefinition}} model 已规范化模块模型 / Normalized module model.
18
+ * @param {import("../index.js").ModuleDefinition} definition 已规范化字段定义 / Normalized field definition.
19
19
  */
20
- constructor(root, model) {
21
- this.#release = this.#mount(root, model);
20
+ constructor(root, definition) {
21
+ this.#release = this.#mount(root, definition);
22
22
  }
23
23
 
24
24
  /**
25
25
  * 建立面板 DOM、交互和会话,并返回其释放操作。
26
26
  * Build panel DOM, interactions, and session, then return its release operation.
27
27
  * @param {HTMLElement} root 包内挂载元素 / Internal mount element.
28
- * @param {import("../index.js").ModuleModel & {definition: import("../index.js").ModuleDefinition}} model 已规范化模块模型 / Normalized module model.
28
+ * @param {import("../index.js").ModuleDefinition} definition 已规范化字段定义 / Normalized field definition.
29
29
  * @returns {() => void} 释放操作 / Release operation.
30
30
  */
31
- #mount(root, model) {
32
- const { definition } = model;
31
+ #mount(root, definition) {
33
32
  const title = definition.metadata?.name ?? definition.module;
34
33
  const document = root.ownerDocument;
35
34
  const window = document.defaultView;
@@ -121,7 +120,7 @@ export class PreferencesPanel {
121
120
  toast.hidden = true;
122
121
  }, 2400);
123
122
  };
124
- const client = new PreferencesClient({ model, definition, notify });
123
+ const client = new PreferencesClient({ definition, notify });
125
124
  /**
126
125
  * 两种菜单入口共用异步错误处理,包含宿主确认框错误。
127
126
  * Share async error handling between both menus, including host-dialog errors.
@@ -149,6 +148,7 @@ export class PreferencesPanel {
149
148
  publishNavigation();
150
149
  viewport.replaceChildren(statusView("读取设置…"));
151
150
  try {
151
+ await client.open();
152
152
  if (version === generation) controls();
153
153
  } catch (error) {
154
154
  if (version !== generation) return;
package/src/index.d.ts CHANGED
@@ -92,8 +92,8 @@ export interface SettingsRequest {
92
92
  */
93
93
  headers?: Record<string, string | undefined>;
94
94
  /**
95
- * 模块动作的 JSON 正文
96
- * JSON body for a module action.
95
+ * 存储动作的 form 正文
96
+ * Form body for a storage action.
97
97
  */
98
98
  body?: string;
99
99
  }
@@ -268,25 +268,3 @@ export interface BoxJSSubscription extends BoxJSMetadata {
268
268
  * The sole data configuration input.
269
269
  */
270
270
  export type BoxJSInput = BoxJSSetting[] | BoxJSApp | BoxJSSubscription;
271
- /**
272
- * 模块 API 返回的原始 BoxJS 与当前值模型。
273
- * Raw BoxJS and current-value model returned by the module API.
274
- */
275
- export interface ModuleModel {
276
- /** 模块路径段 / Module path segment. */
277
- module: string;
278
- /** API 获取的原始 BoxJS JSON / Raw BoxJS JSON fetched by the API. */
279
- boxjs: BoxJSInput;
280
- /** API 读取到的原始已保存字段值 / Raw persisted field values read by the API. */
281
- values: Record<string, JsonValue>;
282
- /** 后续 API 动作使用的 BoxJS JSON 地址 / BoxJS JSON URL used by later API actions. */
283
- configURL: string;
284
- }
285
- /**
286
- * 生成模块前端文件,不复制配置、不生成绑定模块的代理脚本。
287
- * Build module frontend files without copying configuration or producing bound proxy scripts.
288
- * @param boxjs 恰好包含一个模块的 BoxJS JSON / BoxJS JSON describing exactly one module.
289
- * @param css 可选 CSS 正文 / Optional CSS text.
290
- * @returns 相对路径到文件内容的映射 / Relative paths mapped to file contents.
291
- */
292
- export function build(boxjs: BoxJSInput, css?: string): Promise<Record<string, string>>;
package/src/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 仅接收 BoxJS JSON 与可选 CSS 的构建入口。
3
- * Build entry accepting only BoxJS JSON and optional CSS.
2
+ * PreferencePanes 公共类型根入口;浏览器运行时由子路径导出。
3
+ * PreferencePanes public type root; browser runtimes are exported from subpaths.
4
4
  * @module @nsnanocat/preference-panes
5
5
  */
6
- export { build } from "./build.mjs";
6
+ export {};
package/src/web.mjs CHANGED
@@ -2,7 +2,6 @@ import { URL } from "@nsnanocat/url";
2
2
  import { $app } from "@nsnanocat/util/lib/app.mjs";
3
3
  import { done } from "@nsnanocat/util/lib/done.mjs";
4
4
  import assets from "#assets";
5
- import { pageInputs } from "./lib/page-inputs.mjs";
6
5
 
7
6
  /**
8
7
  * 返回模块页面及其公共浏览器资源,不处理 API、网络或持久化。
@@ -16,10 +15,7 @@ function run() {
16
15
  const url = new URL(request.url);
17
16
  if (/^\/settings\/[a-zA-Z0-9_-]+\/?$/.test(url.pathname)) {
18
17
  if (!["GET", "HEAD"].includes(request.method)) result = response(request, 405, { error: "Method not allowed" });
19
- else {
20
- const inputs = encodeURIComponent(JSON.stringify(pageInputs(url, request.headers)));
21
- result = response(request, 200, assets.page.body.replace("</head>", `<meta name="preference-panes-inputs" content="${inputs}"></head>`), "text/html");
22
- }
18
+ else result = response(request, 200, assets.page.body, "text/html");
23
19
  } else {
24
20
  const asset = assets[url.pathname];
25
21
  if (asset) result = ["GET", "HEAD"].includes(request.method) ? response(request, 200, asset.body, asset.type) : response(request, 405, { error: "Method not allowed" });
package/src/build.mjs DELETED
@@ -1,20 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { normalizeBoxJs } from "./browser/boxjs.mjs";
3
-
4
- /**
5
- * 仅生成模块前端文件,不复制配置或生成绑定业务的读写脚本。
6
- * Build module frontend files without copying configuration or generating bound storage scripts.
7
- * @param {unknown} boxjs 仅包含一个模块的 BoxJS JSON / BoxJS JSON containing exactly one module.
8
- * @param {string} [css] 可选自定义 CSS 正文 / Optional custom CSS text.
9
- * @returns {Promise<Record<string, string>>} 相对路径到文件正文的映射 / Relative file paths mapped to file contents.
10
- */
11
- export async function build(boxjs, css = "") {
12
- if (typeof css !== "string") throw new TypeError("CSS must be a string");
13
- const module = normalizeBoxJs(boxjs).module;
14
- const [html, index] = await Promise.all([readFile(new URL("../dist/module/index.html", import.meta.url), "utf8"), readFile(new URL("../dist/module/index.mjs", import.meta.url), "utf8")]);
15
- return {
16
- [`settings/${module}/index.html`]: html,
17
- "settings/assets/index.mjs": index,
18
- [`settings/assets/${module}.css`]: css,
19
- };
20
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * 统一解析模块页的资源地址:Header 优先于查询参数,再使用模块约定。
3
- * Resolve module resource locations: headers override query parameters and module conventions.
4
- * @param {URL} url 已解析的页面请求地址 / Parsed page request URL.
5
- * @param {Record<string, string | undefined>} [headers] 请求头,名称不区分大小写 / Case-insensitive request headers.
6
- * @returns {{url: string, module: string, json: string, css: string}} 页面上下文与两个资源输入 / Page context and two resource inputs.
7
- */
8
- export function pageInputs(url, headers = {}) {
9
- const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(url.pathname);
10
- if (!match) throw new TypeError("Open a concrete module URL");
11
- const module = match[1];
12
- const values = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
13
- const json = values["x-preferencepanes-json"] ?? url.searchParams.get("json") ?? `/configs/${module}`;
14
- const css = values["x-preferencepanes-css"] ?? url.searchParams.get("css") ?? "";
15
- if (!json.trim()) throw new TypeError("JSON resource URL is required");
16
- return { url: url.href, module, json, css };
17
- }