@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.
package/README.md CHANGED
@@ -1,74 +1,94 @@
1
1
  # @nsnanocat/preference-panes
2
2
 
3
- PreferencePanes 分别提供具体模块的设置前端和本地持久化 API。`web.js` 只返回模块 HTML 与公共浏览器资源,`api.js` 只处理模块数据和持久化;业务模块自己发布版本对应的 BoxJS JSON。项目主页及其客户端 SDK 调用属于调用方,不进入本包。
3
+ PreferencePanes 提供一个由 BoxJS JSON 驱动的通用设置前端,以及一个独立的代理持久化 API。业务模块只发布自己的 `/configs/{module}` `/api/{module}`;通用 `/settings/**` 前端只需要安装一次。
4
4
 
5
- ## 模块 API
5
+ Biliverse 中,Enhanced 是唯一安装 `web.js` 的模块。Global、Redirect、ADBlock 不携带 `web.js`,它们的设置页仍由同一份通用前端读取各自 BoxJS 后渲染。
6
6
 
7
- 嵌入的模块页跟随宿主根元素的 `data-theme`(light/dark)和 `--pp-keyboard-height`(CSS 长度),退出时释放观察器。独立页面使用网页自身或系统主题;通用包不识别任何客户端 User-Agent。
7
+ ## 浏览器入口
8
8
 
9
- 默认样式完全由包内 `pp-*` 类和 CSS 变量提供,不请求任何项目或客户端资源。调用方可通过 CSS 输入覆盖变量、组件,或在该 CSS 中自行引用所属客户端的官方样式。开关采用标准 checkbox 的 switch 属性及 switch 语义,由浏览器呈现原生控件([Safari/iOS 17.4 起显示开关](https://webkit.org/blog/15054/an-html-switch-control/),其他浏览器保留可操作的复选框);单选使用 select,多选保留二级页面。BoxJS 动态生成及修改即保存保持不变。
9
+ 浏览器包只公开一个入口:
10
10
 
11
- 宿主可监听 ModuleFrame 的 `confirm` 事件,调用 `preventDefault()` 接管确认框,再以 `event.detail.resolve(boolean)` 或 `reject(error)` 完成。未接管的独立网页使用浏览器对话框;模块离开后到达的确认结果不会继续写入。
11
+ ```ts
12
+ function mount(boxjs: BoxJSInput): MountedPreferences;
13
+ interface MountedPreferences { destroy(): void }
14
+ ```
15
+
16
+ ```js
17
+ import { mount } from "@nsnanocat/preference-panes/browser";
18
+
19
+ const boxjs = await fetch("/configs/Module", {
20
+ cache: "no-store",
21
+ credentials: "omit",
22
+ }).then(response => response.json());
23
+
24
+ const preferences = mount(boxjs);
25
+ preferences.destroy();
26
+ ```
27
+
28
+ `mount()` 同步建立页面生命周期。BoxJS 规范化、默认值、已存值校验、加载状态和失败重试都由内部流程管理;不公开 API Model、视图 class、构建器或 CSS 第二参数。
29
+
30
+ 前端支持 BoxJS 字段数组、单个 app 和 apps 订阅,并要求输入恰好包含一个模块。模块名、存储根和 Settings 路径都从 `@Root.Module.Settings.key` 字段 ID 推导,不从 app 名称或调用参数补充。
31
+
32
+ 默认样式随包内置。嵌入页面只跟随宿主根元素的 `data-theme` 和 `--pp-keyboard-height`,不识别客户端 User-Agent,也不加载项目 CSS 或客户端 SDK。
33
+
34
+ ## 页面与 API
35
+
36
+ `web.js` 只返回三类通用资源:
12
37
 
13
- 宿主也可监听 `notice` 事件,通过 `preventDefault()` 接管 `{kind, message}` 提示;被接管时模块不创建网页 Toast、不启用提示计时器。独立使用的通用面板仍提供默认通知。
38
+ - `GET /settings/{module}`
39
+ - `GET /settings/assets/index.mjs`
40
+ - `GET /settings/assets/navigation.mjs`
14
41
 
15
- 业务模块分别映射同一 Release 的两个产物:`api.js` 只匹配 `/api/{module}` 及其动作,`web.js` 只匹配 `/settings/{module}`、`/settings/assets/app.mjs` 和 `/settings/assets/navigation.mjs`。两者都不包含业务配置,也不需要额外安装独立设置插件。
42
+ 模块页面从 URL `ModuleFrame` 的模块标记取得模块名,直接读取同源 `/configs/{module}`,然后调用 `mount(boxjs)`。页面不接受 JSON/CSS 查询参数、私有请求头或兼容资源别名。
16
43
 
17
- 网页只调用模块 API:`HEAD /api/{module}` 探测模块,`GET /api/{module}` 取得原始 BoxJS 与当前已存值,`POST /api/{module}/get|set|delete` 执行持久化操作。`api.js` 负责取得 BoxJS、确认字段 ID 并把完整 `@root.path` 直接交给 util `Storage`;网页不直接请求配置 Mock,也不提交存储根。
44
+ `api.js` 只处理:
45
+
46
+ - `HEAD /api/{module}`:探测同源 `/configs/{module}`,透传状态与 `X-PreferencePanes-Version`。
47
+ - `POST /api/{module}/get`:读取声明字段或 Settings、Caches、module 子树。
48
+ - `POST /api/{module}/set`:写入当前 BoxJS 声明的字段。
49
+ - `POST /api/{module}/delete`:删除声明字段或模块子树。
50
+
51
+ `GET /api/{module}` 和其它未规定方法返回 `405`。API 每次动作都从 `/configs/{module}` 建立字段目录,只负责字段授权与 util `Storage` 读写,不解释控件、选项或默认值。
18
52
 
19
53
  ```js
20
54
  await fetch("/api/Enhanced/set", {
21
55
  method: "POST",
22
- headers: {
23
- "Content-Type": "application/json",
24
- "X-PreferencePanes-JSON": "/configs/Enhanced",
25
- },
26
- body: JSON.stringify({ key: "Enhanced.Settings.Home.Top_left", value: "mine" }),
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify({
58
+ key: "Enhanced.Settings.Home.Top_left",
59
+ value: "mine",
60
+ }),
27
61
  });
28
62
  ```
29
63
 
30
- 模块 API 返回的 `boxjs` 保持上游原样,`values` 只含实际已存值,不补默认值或解释控件类型。Web `mount()` 独占控件、选项、默认值、展示元数据和已存值校验,再生成页面。写入成功后只更新当前页面快照,不追加读取。旧 `/api/get|set|delete` form 接口和 `/api/module/{module}` 草稿路径均移除。
64
+ 页面初始化时只执行一次 `POST /api/{module}/get` 读取 Settings 子树。写入成功后仅更新当前页面快照;查看 Settings/Caches 时按需读取,清空和重置通过 delete 动作完成。
31
65
 
32
- ## 页面与输入
66
+ ## 宿主集成
33
67
 
34
68
  ```js
35
- import { mount } from "@nsnanocat/preference-panes/browser";
36
- const model = await fetch("/api/Module", {
37
- headers: { "X-PreferencePanes-JSON": "/configs/Module" },
38
- }).then(response => response.json());
39
- const page = mount(model, ".pp-panel { --pp-accent: #16866a; }");
40
- page.destroy();
41
- ```
42
-
43
- API 支持字段数组、单 app 和 apps 订阅,只扫描 `@Root.Module.Settings.key` 形式的字段 ID。浏览器从 API 返回的同一份 BoxJS 解析控件、名称、图标和默认值;省略 CSS 使用内置样式。
69
+ import { ModuleFrame, ModuleStatus } from "@nsnanocat/preference-panes/navigation";
44
70
 
45
- `/settings/{module}` 由独立 `web.js` 返回模块文档。页面可通过 json/css 查询参数或 X-PreferencePanes-JSON/CSS Header 指定资源 URL;默认 JSON 是 /configs/{module},CSS 默认空。Header 分别优先。`web.js` 不访问网络或存储,浏览器执行其中的 `app.mjs` 后才调用 `api.js`。
71
+ const status = new ModuleStatus(statusElement);
72
+ await status.check("/api/Module");
46
73
 
47
- ```js
48
- import { ModuleFrame, Navigation } from "@nsnanocat/preference-panes/navigation";
49
- const frame = new ModuleFrame("/settings/Module", {
50
- headers: { "X-PreferencePanes-JSON": "/configs/Module", "X-PreferencePanes-CSS": "/theme.css" },
51
- signal,
52
- });
74
+ const frame = new ModuleFrame("/settings/Module", { signal });
53
75
  container.append(frame.element);
54
76
  await frame.load();
55
- frame.addEventListener("change", () => { title.textContent = frame.state.title; });
56
- back.onclick = () => frame.back();
57
- frame.destroy();
58
77
  ```
59
78
 
60
- ModuleFrame iframe 元素上保存原请求上下文,HTML 原样加载,不从 about:srcdoc 猜模块、不注入临时 CSS。框架自身管理嵌入模式,通过事件发布标题、忙碌状态和返回能力。Navigation 统一管理 fragment 历史、滑动、滚动保留、加载取消及动画结束后释放;项目提供根页、子页工厂和布局。
61
-
62
- 项目主页可按需使用导出的 `probeModule`、ModuleStatus、ModuleFrame 和 Navigation,也可以自行实现入口。`probeModule("/api/Module", { json: "/configs/Module" })` 只对模块 API 发送一次 HEAD,并原样返回浏览器 `Response`;代理 API 负责探测 BoxJS 上游并透传状态和 `X-PreferencePanes-Version`。ModuleStatus 仅负责把同一响应渲染到状态行。PreferencePanes 不提供主页运行脚本,不识别具体 App,不加载或调用任何客户端 Bridge SDK。调用方若运行在原生 WebView,应在自己的 HTML/页面脚本中直接接入该客户端的官方 SDK,再把 ModuleFrame 事件映射到原生界面。
63
-
64
- 每次进入模块通过 API 取得 JSON 和设置一次,同时读取可选 CSS。二级多选返回复用内存缓存,修改经 Web 校验后提交 API;成功提示、失败回滚由公共组件处理。Caches 按需通过 API 查看/清空,重置只删除指定模块子树。
65
-
66
- 标题栏只显示文字;嵌入模式隐藏模块自身标题栏,由宿主显示原生标题或自己的导航。模块数据操作位于标题栏右侧三点菜单,页面不再平铺维护按钮。`ActionMenu` 是共用的底部操作菜单:独立网页由其三点按钮打开;网页宿主或只有原生按钮、没有原生菜单的 WebView 宿主,将 `frame.state.actions` 传给 `menu.update(actions, busy)`,在宿主按钮点击时调用 `menu.open()`,选择后调用 `frame.perform(id)`。查看设置和缓存都会通过 `POST /api/{module}/get` 按需读取最新子树,再进入可返回的 JSON 详情子页;返回详情页不会触发额外读取,清空和重置设置通过 `/delete` 且仍要求确认。菜单组件提供遮罩、取消按钮、外部点击、Escape 与方向键操作,弹层挂载在文档根部,不依赖模块标题栏是否显示;宿主不访问 iframe 内部 DOM。
79
+ `ModuleFrame` 只携带模块身份和取消信号。宿主通过 `change`、`confirm`、`notice` 事件同步标题、操作菜单、确认框和提示,不读取或改写 iframe 内部 DOM。项目主页、Bilibili JSBridge、原生导航和视觉样式仍由宿主负责。
67
80
 
68
81
  ## 构建与验证
69
82
 
70
- `npm run build` 生成无业务配置的 `dist/api.js`、`dist/web.js` 和公共前端模块;Release 工作流分别上传后端 API 与前端响应脚本。`build(boxjs, css?)` 仅输出 `settings/{module}/index.html`、公共 `settings/assets/app.mjs` 和可选模块 CSS,不复制配置、导航组件或重复 HTML。
83
+ ```sh
84
+ npm run build
85
+ npm run check
86
+ npm run apifox:check
87
+ npm pack --dry-run
88
+ ```
89
+
90
+ 构建生成 `dist/api.js`、`dist/web.js`、`dist/preference-panes.mjs` 和 `dist/module/` 通用页面资源。包不再提供按模块生成 HTML/CSS 的公开 `build()`。
71
91
 
72
- `npm run preview` 提供文件导入测试台,上传 JSON/CSS 后在隔离 iframe 预览;测试存储只在内存中。`npm run check` 检查代码、类型、行为;`npm run apifox:generate` 和 `npm run apifox:check` 维护原生接口文档。
92
+ `npm run preview` 启动 JSON 文件导入测试台,使用同一通用页面、同源配置和内存存储验证模块行为。
73
93
 
74
- [接口规范](apifox/Specification.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
94
+ [接口规范](apifox/Specification.md) · [宿主集成](apifox/HostIntegration.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
package/dist/api.js CHANGED
@@ -1957,31 +1957,21 @@
1957
1957
  const match = /^\/api\/([a-zA-Z0-9_-]+)(?:\/(get|set|delete))?\/?$/.exec(url.pathname);
1958
1958
  if (!match) return;
1959
1959
  const [, module, action] = match;
1960
- const configURL = this.#configURL(request, url, module);
1960
+ const configuration = `${url.origin}/configs/${module}`;
1961
1961
  switch (true) {
1962
1962
  case !action && request.method === "HEAD":
1963
- return this.#probe(request, configURL);
1964
- case !action && request.method === "GET":
1965
- return this.#model(request, module, configURL);
1963
+ return this.#probe(request, configuration);
1966
1964
  case Boolean(action) && request.method === "POST":
1967
- return this.#action(request, module, action, configURL);
1965
+ return this.#action(request, module, action, configuration);
1968
1966
  default:
1969
- return this.#response(request, 405, { error: "Use GET or HEAD for module reads, POST for module actions" });
1967
+ return this.#response(request, 405, { error: "Use HEAD for module probes and POST for module actions" });
1970
1968
  }
1971
1969
  }
1972
1970
 
1973
- #configURL(request, url, module) {
1974
- const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
1975
- const source = headers["x-preferencepanes-json"] ?? `/configs/${module}`;
1976
- if (/^https?:\/\//i.test(source)) return source;
1977
- if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(source)) throw Object.assign(new TypeError("BoxJS resources must use HTTP(S) URLs"), { status: 400 });
1978
- return `${url.origin}/${source.replace(/^\/+/, "")}`;
1979
- }
1980
-
1981
- async #probe(request, configURL) {
1971
+ async #probe(request, configuration) {
1982
1972
  let result;
1983
1973
  try {
1984
- result = await fetch({ url: configURL, method: "HEAD", timeout: 5000, headers: { Accept: "application/json" } });
1974
+ result = await fetch({ url: configuration, method: "HEAD", timeout: 5000, headers: { Accept: "application/json" } });
1985
1975
  } catch (error) {
1986
1976
  return this.#response(request, 502, { error: error.message });
1987
1977
  }
@@ -1989,19 +1979,9 @@
1989
1979
  return this.#response(request, result.statusCode ?? result.status, undefined, version ? { "X-PreferencePanes-Version": version } : {});
1990
1980
  }
1991
1981
 
1992
- async #model(request, module, configURL) {
1993
- const loaded = await this.#load(module, configURL);
1994
- const values = {};
1995
- for (const entry of loaded.entries) {
1996
- const value = Storage.getItem(entry.id, MISSING);
1997
- if (value !== MISSING) values[entry.id.slice(loaded.storageKey.length + 2)] = value;
1998
- }
1999
- return this.#response(request, 200, { module, boxjs: loaded.boxjs, values, configURL }, loaded.version ? { "X-PreferencePanes-Version": loaded.version } : {});
2000
- }
2001
-
2002
- async #action(request, module, action, configURL) {
1982
+ async #action(request, module, action, configuration) {
2003
1983
  const payload = this.#jsonBody(request);
2004
- const target = await this.#load(module, configURL);
1984
+ const target = await this.#load(module, configuration);
2005
1985
  switch (action) {
2006
1986
  case "get": {
2007
1987
  const value = Storage.getItem(payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key), MISSING);
@@ -2019,10 +1999,10 @@
2019
1999
  }
2020
2000
  }
2021
2001
 
2022
- async #load(module, configURL) {
2002
+ async #load(module, configuration) {
2023
2003
  let result;
2024
2004
  try {
2025
- result = await fetch({ url: configURL, method: "GET", timeout: 5000, headers: { Accept: "application/json" } });
2005
+ result = await fetch({ url: configuration, method: "GET", timeout: 5000, headers: { Accept: "application/json" } });
2026
2006
  } catch (error) {
2027
2007
  throw Object.assign(new Error(`Configuration request failed: ${error.message}`), { status: 502 });
2028
2008
  }
@@ -2053,7 +2033,7 @@
2053
2033
  }
2054
2034
  }
2055
2035
  if (!entries.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
2056
- return { boxjs, entries, module, storageKey, version: this.#header(result.headers, "x-preferencepanes-version") };
2036
+ return { entries, module, storageKey, version: this.#header(result.headers, "x-preferencepanes-version") };
2057
2037
  } catch (error) {
2058
2038
  throw Object.assign(new Error(`Invalid BoxJS: ${error.message}`), { status: 422 });
2059
2039
  }
@@ -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=1.0.0"></script>
12
+ <script type="module" src="/settings/assets/index.mjs?v=1.1.1"></script>
13
13
  </body>
14
14
  </html>