@nsnanocat/preference-panes 0.9.15 → 1.0.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.
@@ -1,157 +1,72 @@
1
- import type { ModuleDefinition, SettingsScalar } from "../index.js";
1
+ import type { ModuleDefinition, ModuleModel, SettingsScalar } from "../index.js";
2
2
  /**
3
- * 单键写入或删除的通知事件,携带对应模块与点分键路径。
4
- * Notification for a single-key write or delete, including module and dotted key path.
3
+ * 单键写入或删除的通知事件。
4
+ * Notification for a single-key write or delete.
5
5
  */
6
6
  export interface Notification {
7
- /**
8
- * 结果类别
9
- * Result kind.
10
- */
7
+ /** 结果类别 / Result kind. */
11
8
  kind: "success" | "error";
12
- /**
13
- * 操作类别
14
- * Operation kind.
15
- */
9
+ /** 操作类别 / Operation kind. */
16
10
  operation: "write" | "delete" | "clearCaches" | "reset";
17
- /**
18
- * 模块标识
19
- * Module identifier.
20
- */
11
+ /** 模块标识 / Module identifier. */
21
12
  module: string;
22
- /**
23
- * 含模块名、不含存储根的点分路径
24
- * Dotted path including the module but excluding the storage root.
25
- */
26
- key: string;
27
- /**
28
- * 失败原因,仅错误事件提供
29
- * Failure reason, provided for errors only.
30
- */
13
+ /** 字段路径 / Field path. */
14
+ key?: string;
15
+ /** 失败原因 / Failure reason. */
31
16
  message?: string;
32
17
  }
33
18
  /**
34
- * 浏览器会话客户端选项。
35
- * Options for the browser session client.
19
+ * 浏览器页面客户端选项;模型由 API 提供。
20
+ * Browser page client options; the model is supplied by the API.
36
21
  */
37
22
  export interface PreferencesClientOptions {
38
- /**
39
- * 包内从 BoxJS 推导的目录。
40
- * Internal catalog derived from BoxJS.
41
- */
42
- catalog: { modules: ReadonlyMap<string, { storageKey: string }>; select(module: string): unknown };
43
- /**
44
- * 默认使用浏览器 fetch,可注入同签名传输
45
- * Defaults to browser fetch; an equivalent transport may be supplied.
46
- */
23
+ /** API 返回的模块模型 / Module model returned by the API. */
24
+ model: ModuleModel;
25
+ /** 用于渲染的归一化字段定义 / Normalized field definition for rendering. */
26
+ definition: ModuleDefinition;
27
+ /** 默认使用浏览器 fetch / Defaults to browser fetch. */
47
28
  fetch?: typeof globalThis.fetch;
48
- /**
49
- * 成功写入或失败时调用,不用于读取事件
50
- * Called for successful mutations or failures, not reads.
51
- */
29
+ /** 操作通知 / Mutation notifications. */
52
30
  notify?: (notification: Notification) => void;
53
- /**
54
- * 单次请求超时,单位毫秒,默认 10000
55
- * Per-request timeout in milliseconds; defaults to 10000.
56
- */
31
+ /** 请求超时毫秒数 / Request timeout in milliseconds. */
57
32
  timeout?: number;
58
33
  }
59
34
  /**
60
- * 会话的深拷贝快照,调用方修改不会影响缓存。
61
- * Deep-cloned session snapshot; caller changes cannot alter the cache.
35
+ * 会话的深拷贝快照。
36
+ * Deep-cloned session snapshot.
62
37
  */
63
38
  export interface ModuleSnapshot {
64
- /**
65
- * 当前配置生成的模块定义
66
- * Module definition generated from current config.
67
- */
39
+ /** 当前模块字段定义 / Current module field definition. */
68
40
  definition: ModuleDefinition;
69
- /**
70
- * 点分键到显示值的映射,已包含适用的默认值;持久化 null 原样保留。
71
- * Dotted keys mapped to display values including applicable defaults; persisted null is preserved.
72
- */
41
+ /** 当前页面值 / Current page values. */
73
42
  values: Record<string, SettingsScalar | SettingsScalar[] | null>;
74
43
  }
75
44
  /**
76
- * 只在页面存活期间维护模块缓存的通用客户端。
77
- * Generic client maintaining module caches only during the page lifetime.
45
+ * 只调用模块 API 的页面客户端。
46
+ * Page client that only calls the module API.
78
47
  */
79
48
  export interface PreferencesClient {
80
- /**
81
- * 使用已导入的 JSON 替换会话,只读取一次设置值。
82
- * Replace the session from imported JSON and read stored settings once.
83
- * @param module 模块标识 / Module identifier.
84
- * @returns 新会话的独立快照 / Independent snapshot of the new session.
85
- * @throws {Error} 写入进行中、请求或配置无效、会话被替换 / Active write, invalid request or config, or replaced session.
86
- */
87
- open(module: string): Promise<ModuleSnapshot>;
88
- /**
89
- * 获取已打开模块的快照,不发请求。
90
- * Get a snapshot of an open module without network requests.
91
- * @param module 模块标识 / Module identifier.
92
- * @returns 深拷贝快照 / Deep-cloned snapshot.
93
- * @throws {Error} 模块尚未打开 / Module has not been opened.
94
- */
95
- snapshot(module: string): ModuleSnapshot;
96
- /**
97
- * 取消未完成的读取并移除缓存,不撤销已发送的写入。
98
- * Abort pending reads and discard the cache without undoing dispatched writes.
99
- * @param module 模块标识 / Module identifier.
100
- * @returns 无返回值 / No return value.
101
- */
102
- leave(module: string): void;
103
- /**
104
- * POST 单个字段,HTTP 200 后更新缓存,不追加 GET。
105
- * POST one field and update its cache only on HTTP 200, without a follow-up GET.
106
- * @param module 已打开的模块 / Open module.
107
- * @param key 完整点分字段路径 / Complete dotted field path.
108
- * @param value 符合字段类型和选项的值 / Value matching the field type and choices.
109
- * @returns 操作完成 / Completion of the operation.
110
- * @throws {Error} 会话、值、并发写入或网络错误 / Session, value, concurrent-write or network error.
111
- */
112
- set(module: string, key: string, value: SettingsScalar | SettingsScalar[]): Promise<void>;
113
- /**
114
- * POST /api/delete 删除覆盖值,200 后显示默认值,不追加读取。
115
- * POST /api/delete removes an override and displays its default after 200, without rereading.
116
- * @param module 已打开的模块 / Open module.
117
- * @param key 完整点分字段路径 / Complete dotted field path.
118
- * @returns 操作完成 / Completion of the operation.
119
- * @throws {Error} 会话、路径、并发写入或网络错误 / Session, path, concurrent-write or network error.
120
- */
121
- remove(module: string, key: string): Promise<void>;
122
- /**
123
- * 按需重新读取整个模块 Settings,不更新页面会话缓存。
124
- * Reread all module Settings on demand without updating the page-session cache.
125
- * @param module 已打开模块 / Open module.
126
- * @returns 设置 JSON 值,缺失时为 undefined / Settings JSON value, or undefined when absent.
127
- */
128
- readSettings(module: string): Promise<unknown>;
129
- /**
130
- * 按需读取整个模块 Caches,不刷新设置。
131
- * Read all module Caches on demand without refreshing settings.
132
- * @param module 已打开模块 / Open module.
133
- * @returns 缓存 JSON 值,缺失时为 undefined / Cache JSON value, or undefined when absent.
134
- */
135
- readCaches(module: string): Promise<unknown>;
136
- /**
137
- * 删除模块 Caches 并更新相关页面状态,不追加 GET。
138
- * Delete module Caches and update related page state without a follow-up GET.
139
- * @param module 已打开模块 / Open module.
140
- * @returns 清理完成 / Cleanup completion.
141
- */
142
- clearCaches(module: string): Promise<void>;
143
- /**
144
- * 删除整个模块持久化数据,页面使用当前 BoxJS 默认值。
145
- * Delete all module persistence and use current BoxJS defaults on the page.
146
- * @param module 已打开模块 / Open module.
147
- * @returns 重置完成 / Reset completion.
148
- */
149
- reset(module: string): Promise<void>;
49
+ /** 获取页面快照,不发请求 / Get a page snapshot without a request. */
50
+ snapshot(): ModuleSnapshot;
51
+ /** 读取 Settings 子树 / Read the Settings subtree. */
52
+ readSettings(): Promise<unknown>;
53
+ /** 读取 Caches 子树 / Read the Caches subtree. */
54
+ readCaches(): Promise<unknown>;
55
+ /** 删除 Caches / Delete Caches. */
56
+ clearCaches(): Promise<void>;
57
+ /** 删除整个模块数据并恢复默认值 / Delete module data and restore defaults. */
58
+ reset(): Promise<void>;
59
+ /** 取消请求 / Cancel requests. */
60
+ leave(): void;
61
+ /** 写入单个字段 / Write one field. */
62
+ set(key: string, value: SettingsScalar | SettingsScalar[]): Promise<void>;
63
+ /** 删除单个字段覆盖值 / Delete one field override. */
64
+ remove(key: string): Promise<void>;
150
65
  }
151
66
  /**
152
- * 创建包内客户端,接管请求和会话。
153
- * Create an internal client for requests and sessions.
154
- * @param options 包内目录与运行环境 / Internal catalog and runtime environment.
155
- * @returns 会话客户端 / Session client.
67
+ * 创建只调用模块 API 的页面客户端。
68
+ * Create a page client that only calls the module API.
69
+ * @param options API 模型与运行环境 / API model and runtime.
70
+ * @returns 页面客户端 / Page client.
156
71
  */
157
72
  export function createPreferencesClient(options: PreferencesClientOptions): PreferencesClient;
@@ -1,219 +1,101 @@
1
- import { normalizeBoxJs, normalizeStoredValue, validValue } from "../lib/boxjs.mjs";
2
-
3
1
  /**
4
- * 单个模块的临时会话;离开页面后丢弃。
5
- * Transient module session discarded when leaving the page.
6
- * @typedef {object} ModuleSession
7
- * @property {AbortController} controller 读取请求的取消控制器 / Abort controller for reads.
8
- * @property {import("../index.js").ModuleDefinition | null} definition 加载完成的配置,加载中为 null / Loaded configuration, or null while loading.
9
- * @property {import("./client.mjs").ModuleSnapshot["values"]} values 当前显示值 / Current display values.
10
- * @property {boolean} saving 是否正在写入 / Whether a mutation is in progress.
2
+ * 创建单模块页面客户端;只调用模块 API,不读取或解析 BoxJS。
3
+ * Create a single-module page client that only calls the module API and never reads or parses BoxJS.
4
+ * @param {import("./client.mjs").PreferencesClientOptions} options API 模型、请求与通知 / API model, requests and notifications.
5
+ * @returns {import("./client.mjs").PreferencesClient} 页面客户端 / Page client.
11
6
  */
7
+ export function createPreferencesClient({ model, definition, fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 }) {
8
+ const { module, configURL } = model;
9
+ const session = new AbortController();
10
+ const values = structuredClone(model.values);
11
+ let saving = false;
12
12
 
13
- /**
14
- * 创建页面会话缓存;打开时重读,选项操作仅在 HTTP 200 后更新缓存。
15
- * Create a page-session cache; reload on open and mutate cache only after HTTP 200.
16
- * @param {import("./client.mjs").PreferencesClientOptions} options 包内目录、请求与通知 / Internal catalog, requests and notifications.
17
- * @returns {import("./client.mjs").PreferencesClient} 通用客户端 / Generic client.
18
- */
19
- export function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 }) {
20
13
  /**
21
- * 模块会话表
22
- * Module session map.
23
- * @type {Map<string, ModuleSession>}
14
+ * 向模块 API 发送 JSON 动作。
15
+ * Send a JSON action to the module API.
16
+ * @param {"get" | "set" | "delete"} action 模块动作 / Module action.
17
+ * @param {unknown} payload JSON 请求体 / JSON request body.
18
+ * @returns {Promise<Response>} 原始响应 / Raw response.
24
19
  */
25
- const sessions = new Map();
26
- /**
27
- * 用 form 发送完整存储键;读取 404 交给调用方处理。
28
- * Send a complete storage key as form data; callers handle missing reads.
29
- * @param {string} path 完整 @root.path / Complete @root.path.
30
- * @param {"get" | "set" | "delete"} action 存储操作 / Storage operation.
31
- * @param {unknown} body set 值,其它操作忽略 / Set value, ignored by other operations.
32
- * @param {AbortSignal | undefined} signal 会话取消信号 / Session cancellation signal.
33
- * @returns {Promise<Response>} 未消费正文的响应 / Response with an unread body.
34
- * @throws {Error} 非 200 且非数据 GET 404、超时、取消或网络错误 / Non-200 status except missing-data GETs, timeout, cancellation or network error.
35
- */
36
- async function send(path, action, body, signal) {
20
+ async function send(action, payload) {
37
21
  const controller = new AbortController();
38
22
  const abort = () => controller.abort();
39
- if (signal?.aborted) abort();
40
- signal?.addEventListener("abort", abort, { once: true });
23
+ if (session.signal.aborted) abort();
24
+ session.signal.addEventListener("abort", abort, { once: true });
41
25
  const timer = setTimeout(abort, timeout);
42
26
  try {
43
- const response = await request(`/api/${action}`, {
27
+ const response = await request(`/api/${encodeURIComponent(module)}/${action}`, {
44
28
  method: "POST",
45
29
  credentials: "omit",
46
30
  cache: "no-store",
47
31
  signal: controller.signal,
48
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
49
- body: new URLSearchParams([[path, action === "set" ? JSON.stringify(body) : ""]]).toString(),
32
+ headers: { "Content-Type": "application/json", "X-PreferencePanes-JSON": configURL },
33
+ body: JSON.stringify(payload),
50
34
  });
51
35
  if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
52
36
  return response;
53
37
  } finally {
54
38
  clearTimeout(timer);
55
- signal?.removeEventListener("abort", abort);
39
+ session.signal.removeEventListener("abort", abort);
56
40
  }
57
41
  }
42
+
58
43
  /**
59
- * 获取独立快照,避免调用方修改内部缓存。
60
- * Return an independent snapshot so callers cannot mutate the cache.
61
- * @param {string} module 已打开模块 / Open module.
62
- * @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
63
- * @throws {Error} 模块未完成加载 / Module has not finished loading.
64
- */
65
- const snapshot = module => {
66
- const state = sessions.get(module);
67
- if (!state?.definition) throw new Error("Open the module first");
68
- return structuredClone({ definition: state.definition, values: state.values });
69
- };
70
- /**
71
- * 串行修改单键,仅成功后更新仍存活的会话。
72
- * Serialize single-key mutations and update a still-active session only after success.
73
- * @param {string} module 已打开模块 / Open module.
74
- * @param {string} key 完整点分字段路径 / Complete dotted field path.
75
- * @param {"set" | "delete"} action 写入或删除 / Write or delete.
76
- * @param {unknown} value 写入值,删除时忽略 / Write value, ignored for deletion.
77
- * @param {"write" | "delete" | "clearCaches" | "reset"} [operation] 操作类型 / Operation kind.
44
+ * 执行写入动作;成功后只更新当前页面值。
45
+ * Execute a mutation and update only the current page values after success.
46
+ * @param {"set" | "delete"} action API 动作 / API action.
47
+ * @param {unknown} payload JSON 请求体 / JSON request body.
48
+ * @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
49
+ * @param {string} [key] 字段路径 / Field path.
78
50
  * @returns {Promise<void>} 操作完成 / Operation completion.
79
- * @throws {Error} 会话、字段、值或请求错误 / Session, field, value or request error.
80
51
  */
81
- async function change(module, key, action, value, operation = action === "set" ? "write" : "delete") {
82
- const state = sessions.get(module);
83
- if (!state?.definition) throw new Error("Open the module first");
84
- if (state.saving) throw new Error("A settings write is already in progress");
85
- const field = state.definition.fields.find(field => field.key === key);
86
- state.saving = true;
52
+ async function change(action, payload, operation, key) {
53
+ if (saving) throw new Error("A settings write is already in progress");
54
+ saving = true;
87
55
  try {
88
- if ((operation === "write" || operation === "delete") && (!field || (action === "set" && !validValue(field, value)))) throw new TypeError("Invalid setting value");
89
- await send(`@${state.definition.storageKey}.${key}`, action, value);
90
- if (sessions.get(module) === state) {
91
- switch (operation) {
92
- case "write":
93
- state.values[key] = structuredClone(value);
94
- break;
95
- case "delete":
96
- case "clearCaches":
97
- case "reset":
98
- for (const candidate of state.definition.fields) {
99
- if (candidate.key !== key && !candidate.key.startsWith(`${key}.`)) continue;
100
- delete state.values[candidate.key];
101
- if (Object.hasOwn(candidate, "defaultValue")) state.values[candidate.key] = structuredClone(candidate.defaultValue);
102
- }
103
- break;
56
+ await send(action, payload);
57
+ switch (operation) {
58
+ case "write":
59
+ values[key] = structuredClone(payload.value);
60
+ break;
61
+ case "delete": {
62
+ const field = definition.fields.find(candidate => candidate.key === key);
63
+ delete values[key];
64
+ if (field && Object.hasOwn(field, "defaultValue")) values[key] = structuredClone(field.defaultValue);
65
+ break;
104
66
  }
67
+ case "clearCaches":
68
+ break;
69
+ case "reset":
70
+ for (const field of definition.fields) {
71
+ delete values[field.key];
72
+ if (Object.hasOwn(field, "defaultValue")) values[field.key] = structuredClone(field.defaultValue);
73
+ }
74
+ break;
105
75
  }
106
76
  notify({ kind: "success", operation, module, key });
107
77
  } catch (error) {
108
78
  notify({ kind: "error", operation, module, key, message: error.message });
109
79
  throw error;
110
80
  } finally {
111
- state.saving = false;
81
+ saving = false;
112
82
  }
113
83
  }
84
+
114
85
  return {
115
- /**
116
- * 从已导入的 JSON 创建新会话,只读取一次设置值。
117
- * Create a session from imported JSON and read stored settings once.
118
- * @param {string} module 模块标识 / Module identifier.
119
- * @returns {Promise<import("./client.mjs").ModuleSnapshot>} 新快照 / New snapshot.
120
- * @throws {Error} 读取失败、会话被替换或写入尚未完成 / Read failure, replaced session or unfinished write.
121
- */
122
- async open(module) {
123
- const binding = catalog.modules.get(module);
124
- if (!binding) throw new TypeError(`No BoxJS settings for module: ${module}`);
125
- const previous = sessions.get(module);
126
- if (previous?.saving) throw new Error("Cannot refresh while saving");
127
- previous?.controller.abort();
128
- const state = { controller: new AbortController(), definition: null, values: {}, saving: false };
129
- sessions.set(module, state);
130
- try {
131
- const definition = normalizeBoxJs(catalog, module);
132
- const response = await send(`@${definition.storageKey}.${definition.settingsPath.join(".")}`, "get", undefined, state.controller.signal);
133
- let subtree = response.status === 404 ? {} : await response.json();
134
- if (typeof subtree === "string") subtree = JSON.parse(subtree);
135
- if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
136
- if (sessions.get(module) !== state) throw new Error("Module session was replaced");
137
- state.definition = definition;
138
- for (const field of definition.fields) {
139
- const stored = field.key
140
- .split(".")
141
- .slice(definition.settingsPath.length)
142
- .reduce((parent, part) => Object(parent)[part], subtree);
143
- const value = stored === undefined ? field.defaultValue : stored;
144
- if (value !== undefined) state.values[field.key] = normalizeStoredValue(field, value);
145
- }
146
- return snapshot(module);
147
- } catch (error) {
148
- if (sessions.get(module) === state) sessions.delete(module);
149
- throw error;
150
- }
151
- },
152
- snapshot,
153
- /**
154
- * 按需重新读取模块 Settings,不更新页面会话缓存。
155
- * Reread module Settings on demand without updating the page-session cache.
156
- * @param {string} module 已打开的模块 / Open module.
157
- * @returns {Promise<unknown>} 设置值,缺失为 undefined / Settings value, or undefined when absent.
158
- */
159
- async readSettings(module) {
160
- const state = sessions.get(module);
161
- if (!state?.definition) throw new Error("Open the module first");
162
- const response = await send(`@${state.definition.storageKey}.${state.definition.settingsPath.join(".")}`, "get", undefined, state.controller.signal);
86
+ snapshot: () => structuredClone({ definition, values }),
87
+ async readSettings() {
88
+ const response = await send("get", { scope: "settings" });
163
89
  return response.status === 404 ? undefined : response.json();
164
90
  },
165
- /**
166
- * 按需读取模块 Caches,不自动读取其它设置。
167
- * Read module Caches on demand without refreshing other settings.
168
- * @param {string} module 已打开的模块 / Open module.
169
- * @returns {Promise<unknown>} 缓存值,缺失为 undefined / Cache value, or undefined when absent.
170
- */
171
- async readCaches(module) {
172
- const state = sessions.get(module);
173
- if (!state?.definition) throw new Error("Open the module first");
174
- const response = await send(`@${state.definition.storageKey}.${module}.Caches`, "get", undefined, state.controller.signal);
91
+ async readCaches() {
92
+ const response = await send("get", { scope: "caches" });
175
93
  return response.status === 404 ? undefined : response.json();
176
94
  },
177
- /**
178
- * 删除整个 Caches 节点,成功后不追加 GET。
179
- * Delete the entire Caches node without a follow-up GET.
180
- * @param {string} module 已打开模块 / Open module.
181
- * @returns {Promise<void>} 清理完成 / Cleanup completion.
182
- */
183
- clearCaches: module => change(module, `${module}.Caches`, "delete", undefined, "clearCaches"),
184
- /**
185
- * 删除整个模块持久化节点,以当前 BoxJS 默认值重置页面缓存。
186
- * Delete module persistence and reset the page cache using current BoxJS defaults.
187
- * @param {string} module 已打开模块 / Open module.
188
- * @returns {Promise<void>} 重置完成 / Reset completion.
189
- */
190
- reset: module => change(module, module, "delete", undefined, "reset"),
191
- /**
192
- * 取消读取并清除会话,不撤销已发送的写入。
193
- * Abort reads and clear the session without undoing dispatched writes.
194
- * @param {string} module 模块标识 / Module identifier.
195
- * @returns {void} 无返回值 / No return value.
196
- */
197
- leave(module) {
198
- sessions.get(module)?.controller.abort();
199
- sessions.delete(module);
200
- },
201
- /**
202
- * 写入单键并更新当前会话。
203
- * Write one key and update the current session.
204
- * @param {string} module 已打开模块 / Open module.
205
- * @param {string} key 点分字段路径 / Dotted field path.
206
- * @param {import("../index.js").SettingsScalar | import("../index.js").SettingsScalar[]} value 字段值 / Field value.
207
- * @returns {Promise<void>} 写入完成 / Write completion.
208
- */
209
- set: (module, key, value) => change(module, key, "set", value),
210
- /**
211
- * 删除单键覆盖值并显示默认值。
212
- * Delete one override and display its default value.
213
- * @param {string} module 已打开模块 / Open module.
214
- * @param {string} key 点分字段路径 / Dotted field path.
215
- * @returns {Promise<void>} 删除完成 / Delete completion.
216
- */
217
- remove: (module, key) => change(module, key, "delete"),
95
+ clearCaches: () => change("delete", { scope: "caches" }, "clearCaches"),
96
+ reset: () => change("delete", { scope: "module" }, "reset"),
97
+ leave: () => session.abort(),
98
+ set: (key, value) => change("set", { key, value }, "write", key),
99
+ remove: key => change("delete", { key }, "delete", key),
218
100
  };
219
101
  }
@@ -1,22 +1,18 @@
1
- import type { BoxJSInput } from "../index.js";
1
+ import type { ModuleModel } from "../index.js";
2
2
 
3
3
  /**
4
4
  * 具体模块设置页的生命周期句柄。
5
5
  * Lifecycle handle for a concrete module settings page.
6
6
  */
7
7
  export interface MountedPreferences {
8
- /**
9
- * 移除页面、样式、监听器和临时会话。
10
- * Remove the page, styles, listeners and transient sessions.
11
- * @returns 无返回值 / No return value.
12
- */
8
+ /** 移除页面、样式和监听器 / Remove page, styles and listeners. */
13
9
  destroy(): void;
14
10
  }
15
11
  /**
16
- * 仅以 BoxJS 和可选 CSS 挂载一个模块页,不生成项目主页。
17
- * Mount one module page using BoxJS and optional CSS, without a project landing page.
18
- * @param boxjs 恰好包含一个模块的 BoxJS JSON / BoxJS JSON describing exactly one module.
19
- * @param css 可选 CSS 正文;默认样式始终内置 / Optional CSS text; default styles are built in.
12
+ * 使用模块 API 返回的模型挂载设置页;CSS 仅覆盖当前模块。
13
+ * Mount a settings page from a module API model; CSS only overrides this module.
14
+ * @param model 模块 API 模型 / Module API model.
15
+ * @param css 可选 CSS 正文 / Optional CSS text.
20
16
  * @returns 生命周期句柄 / Lifecycle handle.
21
17
  */
22
- export function mount(boxjs: BoxJSInput, css?: string): MountedPreferences;
18
+ export function mount(model: ModuleModel, css?: string): MountedPreferences;
@@ -1,4 +1,4 @@
1
- import { BoxJS } from "../BoxJS.mjs";
1
+ import { normalizeBoxJs, normalizeStoredValue, validValue } from "./boxjs.mjs";
2
2
  import { element, resourceURL } from "./components.mjs";
3
3
  import { mountPanel } from "./panel.mjs";
4
4
  import { installDefaultStyles } from "./styles.mjs";
@@ -6,14 +6,22 @@ import { installDefaultStyles } from "./styles.mjs";
6
6
  /**
7
7
  * 挂载模块设置页;默认样式由包提供,可选 CSS 仅作用于当前模块。
8
8
  * Mount a module page with package defaults and optional module-scoped CSS.
9
- * @param {import("../index.js").BoxJSInput} boxjs 单个模块的 BoxJS JSON / BoxJS JSON for one module.
9
+ * @param {import("../index.js").ModuleModel} model API 返回的模块模型 / Module model returned by the API.
10
10
  * @param {string} [css] 可选 CSS 正文 / Optional CSS text.
11
11
  * @returns {import("./index.js").MountedPreferences} 模块生命周期句柄 / Module lifecycle handle.
12
12
  */
13
- export function mount(boxjs, css = "") {
13
+ export function mount(model, css = "") {
14
14
  if (typeof css !== "string") throw new TypeError("CSS must be a string");
15
- const catalog = boxjs instanceof BoxJS ? boxjs : new BoxJS(boxjs);
16
- const metadata = catalog.module.metadata;
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}`);
21
+ }
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 ?? {};
17
25
  const image = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
18
26
  if (image) resourceURL(image);
19
27
  if (metadata.repo) resourceURL(metadata.repo);
@@ -49,7 +57,7 @@ export function mount(boxjs, css = "") {
49
57
  observer = new MutationObserver(syncAppearance);
50
58
  observer.observe(host, { attributes: true, attributeFilter: ["data-theme", "style"] });
51
59
  }
52
- document.title = metadata.name ?? catalog.module.module;
60
+ document.title = metadata.name ?? definition.module;
53
61
  let panel;
54
62
  const view = {
55
63
  /**
@@ -73,7 +81,7 @@ export function mount(boxjs, css = "") {
73
81
  };
74
82
  try {
75
83
  root.replaceChildren();
76
- panel = mountPanel(root, catalog);
84
+ panel = mountPanel(root, rendered);
77
85
  return view;
78
86
  } catch (error) {
79
87
  view.destroy();