@nsnanocat/preference-panes 1.2.3 → 1.2.5

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  PreferencePanes 提供一个由 BoxJS JSON 驱动的通用设置前端,以及一个独立的代理持久化 API。业务模块直接将自己的 BoxJS JSON Mock 到 `/api/{module}`;通用 `/settings/**` 前端和固定存储 API 分别只需要安装一次。
4
4
 
5
- 在 Biliverse 中,Enhanced 是唯一安装 `web.js` 的模块。Global、Redirect、ADBlock 不携带 `web.js`,它们的设置页仍由同一份通用前端读取各自 BoxJS 后渲染。
5
+ 在 Biliverse 中,Enhanced 统一映射 PreferencePanes 发布的静态模块 HTML、页面入口和导航组件。Global、Redirect、ADBlock 不重复携带页面资源,它们的设置页仍由同一份通用前端读取各自 BoxJS 后渲染。
6
6
 
7
7
  ## 浏览器入口
8
8
 
@@ -29,17 +29,17 @@ preferences.destroy();
29
29
 
30
30
  前端支持 BoxJS 字段数组、单个 app 和 apps 订阅,并要求输入恰好包含一个模块。模块名、存储根和 Settings 路径都从 `@Root.Module.Settings.key` 字段 ID 推导,不从 app 名称或调用参数补充。
31
31
 
32
- 默认样式随包内置。动态模块 HTML 可通过 `css` 查询参数或优先级更高的 `X-PreferencePanes-CSS` Header 直接加载一个项目 stylesheet;未提供时只使用默认样式。项目 stylesheet 位于默认样式之后,可覆盖 `pp-*` 变量和组件规则。页面脚本不二次下载 CSS,也不识别客户端 User-Agent 或加载客户端 SDK。
32
+ 默认样式随包内置。静态模块页接受 `json`、`css` 查询参数;嵌入 `ModuleFrame` 时也可通过 `X-PreferencePanes-JSON`、`X-PreferencePanes-CSS` 选项传入两个资源 URL。未指定 JSON 时默认读取 `/api/{module}`,未指定 CSS 时只使用默认样式。项目 stylesheet 位于默认样式之后,可覆盖 `pp-*` 变量和组件规则。页面脚本不识别客户端 User-Agent,也不加载客户端 SDK。
33
33
 
34
34
  ## 页面与 API
35
35
 
36
- `web.js` 只返回三类通用资源:
36
+ Release 分别提供三项可直接映射的页面资源:
37
37
 
38
38
  - `GET /settings/{module}`
39
39
  - `GET /settings/assets/index.mjs`
40
40
  - `GET /settings/assets/navigation.mjs`
41
41
 
42
- 模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,通过同源 `/api/{module}` 读取原始 BoxJS,然后调用 `mount(boxjs)`。`web.js` 只在生成 HTML 时解析可选 CSS 地址并写入 `<link data-preference-panes-stylesheet>`;CSS 地址必须解析为 HTTP(S)。页面不接受 JSON 输入或兼容资源别名,也不直接访问 `/configs/**`。
42
+ 模块 HTML、页面入口和导航组件分别对应 Release 的 `index.html`、`index.mjs`、`navigation.mjs`,不得再打包进代理响应脚本。模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,读取 BoxJS 资源后调用 `mount(boxjs)`。BoxJS 与可选 CSS 地址必须解析为 HTTP(S);未声明 BoxJS 时回退同源 `/api/{module}`,页面不直接访问 `/configs/**`。
43
43
 
44
44
  业务模块模板直接处理:
45
45
 
@@ -76,13 +76,16 @@ await status.check("/api/Module");
76
76
 
77
77
  const frame = new ModuleFrame("/settings/Module", {
78
78
  signal,
79
- headers: { "X-PreferencePanes-CSS": "https://example.org/theme.css" },
79
+ headers: {
80
+ "X-PreferencePanes-JSON": "https://example.org/Module.boxjs.json",
81
+ "X-PreferencePanes-CSS": "https://example.org/theme.css",
82
+ },
80
83
  });
81
84
  container.append(frame.element);
82
85
  await frame.load();
83
86
  ```
84
87
 
85
- `ModuleFrame` 接受 `{ signal?: AbortSignal; headers?: HeadersInit }`,使用这些选项发送一次 GET,并将返回 HTML 原样设置为 `iframe.srcdoc`。iframe dataset 只保存模块身份,不保存 CSS 地址。宿主通过 `change`、`confirm`、`notice`、`open-url` 事件同步标题、操作菜单、确认框、提示和链接跳转,不读取或改写 iframe 内部 DOM。项目主页、Bilibili JSBridge、原生导航和视觉样式仍由宿主负责。
88
+ `ModuleFrame` 接受 `{ signal?: AbortSignal; headers?: HeadersInit }`。`X-PreferencePanes-JSON` 与 `X-PreferencePanes-CSS` 只用于校验并写入 iframe 数据属性,不作为网络请求 Header;静态 HTML 仍以普通 GET 获取并原样设置为 `iframe.srcdoc`。宿主通过 `change`、`confirm`、`notice`、`open-url` 事件同步标题、操作菜单、确认框、提示和链接跳转,不读取或改写 iframe 内部 DOM。项目主页、Bilibili JSBridge、原生导航和视觉样式仍由宿主负责。
86
89
 
87
90
  ## 构建与验证
88
91
 
@@ -93,8 +96,16 @@ npm run apifox:check
93
96
  npm pack --dry-run
94
97
  ```
95
98
 
96
- 构建生成 `dist/api.js`、`dist/web.js`、`dist/preference-panes.mjs` 和 `dist/module/` 通用页面资源。包不再提供按模块生成 HTML/CSS 的公开 `build()`。
99
+ 构建生成 `dist/api.js`、`dist/preference-panes.mjs` 和 `dist/module/index.html`、`index.mjs`、`navigation.mjs`。包不提供 `web.js`,也不提供按模块生成 HTML/CSS 的公开 `build()`。
97
100
 
98
- `npm run preview` 启动 JSON 文件导入测试台,使用同一通用页面、同源配置和内存存储验证模块行为。
101
+ ### 内置演示
102
+
103
+ ```sh
104
+ npm run preview
105
+ ```
106
+
107
+ 预览入口会自动加载 `examples/Module.boxjs.json`,覆盖 `boolean`、`selects`、`checkboxes`、`text`、`textarea`、`number` 和 `url` 全部受支持控件。顶端 CSS 选择器默认使用包内置 CSS,也可切换 `examples/theme.css` 或导入本地 CSS 文件。预览服务器直接提供正式构建的静态模块页和 JS 文件,通过 `json`、`css` 查询参数传入资源地址,并使用固定存储 API 与独立内存存储,不维护演示专用渲染器。
108
+
109
+ 内置存储会提供已移除选项和格式错误值,用于直接核对字段级黄色警告及非阻断加载;URL 字段还会携带同路径旧存储值,验证只读链接始终使用 BoxJS `val`。测试台同时保留上传其它 BoxJS JSON 并生成隔离预览的能力。
99
110
 
100
111
  [接口规范](apifox/Specification.md) · [宿主集成](apifox/HostIntegration.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
@@ -4,12 +4,13 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
6
  <meta name="color-scheme" content="light dark">
7
+
7
8
  <title>Module Preferences</title>
8
9
  <style>body { margin: 0; }</style>
9
10
 
10
11
  </head>
11
12
  <body>
12
13
  <main id="preferences"></main>
13
- <script type="module" src="/settings/assets/index.mjs?v=1.2.3"></script>
14
+ <script type="module" src="/settings/assets/index.mjs?v=1.2.5"></script>
14
15
  </body>
15
16
  </html>
@@ -1561,8 +1561,8 @@ class ModulePage {
1561
1561
  }
1562
1562
 
1563
1563
  /**
1564
- * 通过模块 API 读取 BoxJS JSON 并挂载通用前端。
1565
- * Read BoxJS JSON through the module API and mount the generic frontend.
1564
+ * 读取页面声明的 BoxJS JSON 并挂载通用前端。
1565
+ * Read the BoxJS JSON declared by the page and mount the generic frontend.
1566
1566
  * @returns {Promise<void>} 启动完成 / Startup completion.
1567
1567
  */
1568
1568
  async start() {
@@ -1574,7 +1574,23 @@ class ModulePage {
1574
1574
  const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
1575
1575
  const module = embedded ?? match?.[1];
1576
1576
  if (!module) throw new TypeError("Open a concrete module URL");
1577
- const response = await fetch(`/api/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
1577
+ const frame = this.#window.frameElement?.dataset;
1578
+ const base = frame?.preferencePanesBase ?? this.#window.location.href;
1579
+ this.#window.document.querySelector("link[data-preference-panes-stylesheet]")?.remove();
1580
+ const stylesheetSource = frame?.preferencePanesStylesheet ?? new URLSearchParams(this.#window.location.search).get("css")?.trim();
1581
+ if (stylesheetSource) {
1582
+ const stylesheet = new URL(stylesheetSource, base);
1583
+ if (!["http:", "https:"].includes(stylesheet.protocol)) throw new TypeError("CSS resource must use HTTP(S)");
1584
+ const link = this.#window.document.createElement("link");
1585
+ link.dataset.preferencePanesStylesheet = "true";
1586
+ link.rel = "stylesheet";
1587
+ link.href = stylesheet.href;
1588
+ this.#window.document.head.append(link);
1589
+ }
1590
+ const jsonSource = frame?.preferencePanesJson ?? new URLSearchParams(this.#window.location.search).get("json")?.trim();
1591
+ const source = new URL(jsonSource || `/api/${encodeURIComponent(module)}`, base);
1592
+ if (!["http:", "https:"].includes(source.protocol)) throw new TypeError("BoxJS resource must use HTTP(S)");
1593
+ const response = await fetch(source, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
1578
1594
  if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
1579
1595
  const boxjs = await response.json();
1580
1596
  normalizeBoxJs(boxjs, module);
@@ -147,8 +147,8 @@ class ActionMenu {
147
147
  }
148
148
 
149
149
  /**
150
- * 模块文档容器:发送 GET 请求并将原始 HTML 交给 iframe,只在元素上标记模块身份。
151
- * Module document container: send a GET request, preserve HTML verbatim and mark only the module identity on the iframe.
150
+ * 模块文档容器:获取静态 HTML,并通过 iframe 数据属性传递页面资源输入。
151
+ * Module document container: fetch static HTML and pass page resource inputs through iframe data attributes.
152
152
  */
153
153
  class ModuleFrame extends EventTarget {
154
154
  #url;
@@ -178,11 +178,27 @@ class ModuleFrame extends EventTarget {
178
178
  this.#url = new URL(url, document.baseURI);
179
179
  const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
180
180
  if (!match) throw new TypeError("Open a concrete module URL");
181
- this.#options = { signal: options.signal, headers: options.headers };
181
+ this.#options = { signal: options.signal };
182
182
  this.element = document.createElement("iframe");
183
183
  this.element.title = `${match[1]} 设置`;
184
184
  this.element.dataset.preferencePanes = "true";
185
185
  this.element.dataset.preferencePanesModule = match[1];
186
+ this.element.dataset.preferencePanesBase = this.#url.href;
187
+ const headers = new Headers(options.headers);
188
+ for (const [header, property, kind] of [
189
+ ["X-PreferencePanes-JSON", "preferencePanesJson", "BoxJS"],
190
+ ["X-PreferencePanes-CSS", "preferencePanesStylesheet", "CSS"],
191
+ ]) {
192
+ if (!headers.has(header)) continue;
193
+ const source = headers.get(header)?.trim();
194
+ if (!source) {
195
+ if (kind === "BoxJS") throw new TypeError("BoxJS resource URL is required");
196
+ continue;
197
+ }
198
+ const resource = new URL(source, this.#url);
199
+ if (!["http:", "https:"].includes(resource.protocol)) throw new TypeError(`${kind} resource must use HTTP(S)`);
200
+ this.element.dataset[property] = resource.href;
201
+ }
186
202
  this.element.addEventListener("preferencepanes:change", this.#change);
187
203
  this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
188
204
  this.element.addEventListener("preferencepanes:notice", this.#notice);
@@ -208,7 +224,7 @@ class ModuleFrame extends EventTarget {
208
224
  if (this.#options.signal?.aborted) this.destroy();
209
225
  const timer = setTimeout(() => this.#controller.abort(), 10000);
210
226
  try {
211
- const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", headers: this.#options.headers, signal: this.#controller.signal });
227
+ const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", signal: this.#controller.signal });
212
228
  if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
213
229
  const html = await response.text();
214
230
  this.#controller.signal.throwIfAborted();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsnanocat/preference-panes",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Generic BoxJS settings frontend and persistence API for JavaScript proxy modules",
5
5
  "author": "VirgilClyne <Virgil@nanocat.me>",
6
6
  "homepage": "https://NSNanoCat.github.io/preference-panes",
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 模块文档容器:发送 GET 请求并将原始 HTML 交给 iframe,只在元素上标记模块身份。
3
- * Module document container: send a GET request, preserve HTML verbatim and mark only the module identity on the iframe.
2
+ * 模块文档容器:获取静态 HTML,并通过 iframe 数据属性传递页面资源输入。
3
+ * Module document container: fetch static HTML and pass page resource inputs through iframe data attributes.
4
4
  */
5
5
  export class ModuleFrame extends EventTarget {
6
6
  #url;
@@ -30,11 +30,27 @@ export class ModuleFrame extends EventTarget {
30
30
  this.#url = new URL(url, document.baseURI);
31
31
  const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
32
32
  if (!match) throw new TypeError("Open a concrete module URL");
33
- this.#options = { signal: options.signal, headers: options.headers };
33
+ this.#options = { signal: options.signal };
34
34
  this.element = document.createElement("iframe");
35
35
  this.element.title = `${match[1]} 设置`;
36
36
  this.element.dataset.preferencePanes = "true";
37
37
  this.element.dataset.preferencePanesModule = match[1];
38
+ this.element.dataset.preferencePanesBase = this.#url.href;
39
+ const headers = new Headers(options.headers);
40
+ for (const [header, property, kind] of [
41
+ ["X-PreferencePanes-JSON", "preferencePanesJson", "BoxJS"],
42
+ ["X-PreferencePanes-CSS", "preferencePanesStylesheet", "CSS"],
43
+ ]) {
44
+ if (!headers.has(header)) continue;
45
+ const source = headers.get(header)?.trim();
46
+ if (!source) {
47
+ if (kind === "BoxJS") throw new TypeError("BoxJS resource URL is required");
48
+ continue;
49
+ }
50
+ const resource = new URL(source, this.#url);
51
+ if (!["http:", "https:"].includes(resource.protocol)) throw new TypeError(`${kind} resource must use HTTP(S)`);
52
+ this.element.dataset[property] = resource.href;
53
+ }
38
54
  this.element.addEventListener("preferencepanes:change", this.#change);
39
55
  this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
40
56
  this.element.addEventListener("preferencepanes:notice", this.#notice);
@@ -60,7 +76,7 @@ export class ModuleFrame extends EventTarget {
60
76
  if (this.#options.signal?.aborted) this.destroy();
61
77
  const timer = setTimeout(() => this.#controller.abort(), 10000);
62
78
  try {
63
- const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", headers: this.#options.headers, signal: this.#controller.signal });
79
+ const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", signal: this.#controller.signal });
64
80
  if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
65
81
  const html = await response.text();
66
82
  this.#controller.signal.throwIfAborted();
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 原始 HTML 的模块 iframe 容器;请求选项不写入元素上下文。
3
- * Iframe container preserving module HTML without writing request options to element context.
2
+ * 静态 HTML 的模块 iframe 容器;页面资源输入写入 iframe 数据属性。
3
+ * Static HTML module iframe whose resource inputs are written to iframe data attributes.
4
4
  * confirm 事件可 preventDefault 后通过 detail.resolve/reject 完成宿主确认。
5
5
  * Prevent default on confirm events and settle host dialogs through detail.resolve/reject.
6
6
  * notice 事件可 preventDefault 后交由宿主显示,模块不再创建网页 Toast。
@@ -13,7 +13,7 @@ export class ModuleFrame extends EventTarget {
13
13
  * 创建容器。
14
14
  * Create a container.
15
15
  * @param url 模块地址 / Module URL.
16
- * @param options GET 请求选项 / GET request options.
16
+ * @param options 静态页面请求与资源输入 / Static page request and resource inputs.
17
17
  */
18
18
  constructor(url: string | URL, options?: { signal?: AbortSignal; headers?: HeadersInit });
19
19
  /**
@@ -25,8 +25,8 @@ class ModulePage {
25
25
  }
26
26
 
27
27
  /**
28
- * 通过模块 API 读取 BoxJS JSON 并挂载通用前端。
29
- * Read BoxJS JSON through the module API and mount the generic frontend.
28
+ * 读取页面声明的 BoxJS JSON 并挂载通用前端。
29
+ * Read the BoxJS JSON declared by the page and mount the generic frontend.
30
30
  * @returns {Promise<void>} 启动完成 / Startup completion.
31
31
  */
32
32
  async start() {
@@ -38,7 +38,23 @@ class ModulePage {
38
38
  const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
39
39
  const module = embedded ?? match?.[1];
40
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" } });
41
+ const frame = this.#window.frameElement?.dataset;
42
+ const base = frame?.preferencePanesBase ?? this.#window.location.href;
43
+ this.#window.document.querySelector("link[data-preference-panes-stylesheet]")?.remove();
44
+ const stylesheetSource = frame?.preferencePanesStylesheet ?? new URLSearchParams(this.#window.location.search).get("css")?.trim();
45
+ if (stylesheetSource) {
46
+ const stylesheet = new URL(stylesheetSource, base);
47
+ if (!["http:", "https:"].includes(stylesheet.protocol)) throw new TypeError("CSS resource must use HTTP(S)");
48
+ const link = this.#window.document.createElement("link");
49
+ link.dataset.preferencePanesStylesheet = "true";
50
+ link.rel = "stylesheet";
51
+ link.href = stylesheet.href;
52
+ this.#window.document.head.append(link);
53
+ }
54
+ const jsonSource = frame?.preferencePanesJson ?? new URLSearchParams(this.#window.location.search).get("json")?.trim();
55
+ const source = new URL(jsonSource || `/api/${encodeURIComponent(module)}`, base);
56
+ if (!["http:", "https:"].includes(source.protocol)) throw new TypeError("BoxJS resource must use HTTP(S)");
57
+ const response = await fetch(source, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
42
58
  if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
43
59
  const boxjs = await response.json();
44
60
  normalizeBoxJs(boxjs, module);
@@ -4,6 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
6
  <meta name="color-scheme" content="light dark">
7
+ <!--__PREFERENCE_PANES_JSON__-->
7
8
  <title>Module Preferences</title>
8
9
  <style>body { margin: 0; }</style>
9
10
  <!--__PREFERENCE_PANES_STYLESHEET__-->