@nsnanocat/preference-panes 0.7.2 → 0.8.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 +25 -76
- package/dist/api.js +1622 -0
- package/dist/module/app.mjs +169 -52
- package/dist/module/index.html +1 -1
- package/dist/module/navigation.mjs +218 -4
- package/dist/preference-panes.mjs +168 -51
- package/package.json +1 -1
- package/src/Store.mjs +40 -53
- package/src/browser/ActionMenu.mjs +115 -0
- package/src/browser/ModuleFrame.mjs +13 -2
- package/src/browser/ModuleStatus.mjs +86 -0
- package/src/browser/Navigation.d.mts +99 -1
- package/src/browser/Navigation.mjs +2 -0
- package/src/browser/client.d.mts +2 -2
- package/src/browser/client.mjs +21 -21
- package/src/browser/panel.css +2 -15
- package/src/browser/panel.mjs +31 -21
- package/src/build.mjs +3 -13
- package/src/index.d.ts +6 -6
- package/src/lib/page-inputs.mjs +1 -1
- package/src/lib/settings-path.mjs +0 -18
- package/src/proxy/handler.mjs +9 -22
- package/dist/preference-panes.config.js +0 -842
- package/dist/preference-panes.proxy.js +0 -1754
- package/src/proxy/config.mjs +0 -14
package/src/browser/panel.css
CHANGED
|
@@ -106,7 +106,8 @@
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
.pp-fields,
|
|
109
|
-
.pp-choice-page
|
|
109
|
+
.pp-choice-page,
|
|
110
|
+
.pp-cache-page {
|
|
110
111
|
position: absolute;
|
|
111
112
|
inset: 0;
|
|
112
113
|
overflow: auto;
|
|
@@ -283,27 +284,13 @@ select.pp-input {
|
|
|
283
284
|
color: inherit;
|
|
284
285
|
text-decoration: underline;
|
|
285
286
|
}
|
|
286
|
-
.pp-maintenance {
|
|
287
|
-
margin-top: 24px;
|
|
288
|
-
}
|
|
289
|
-
.pp-actions {
|
|
290
|
-
display: flex;
|
|
291
|
-
flex-wrap: wrap;
|
|
292
|
-
gap: 8px;
|
|
293
|
-
}
|
|
294
|
-
.pp-actions button,
|
|
295
287
|
.pp-error button {
|
|
296
288
|
min-height: 44px;
|
|
297
289
|
padding: 8px 12px;
|
|
298
290
|
border-radius: 6px;
|
|
299
291
|
background: var(--pp-surface);
|
|
300
292
|
}
|
|
301
|
-
.pp-panel .pp-danger {
|
|
302
|
-
color: #e45656;
|
|
303
|
-
}
|
|
304
293
|
.pp-cache {
|
|
305
|
-
max-height: 320px;
|
|
306
|
-
overflow: auto;
|
|
307
294
|
white-space: pre-wrap;
|
|
308
295
|
overflow-wrap: anywhere;
|
|
309
296
|
}
|
package/src/browser/panel.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ActionMenu } from "./ActionMenu.mjs";
|
|
1
2
|
import { createPreferencesClient } from "./client.mjs";
|
|
2
3
|
import { errorView, icon, element as node, resourceURL } from "./components.mjs";
|
|
3
4
|
import { Navigation } from "./Navigation.mjs";
|
|
@@ -20,6 +21,15 @@ export function mountPanel(root, catalog) {
|
|
|
20
21
|
back.setAttribute("aria-label", "返回");
|
|
21
22
|
back.type = "button";
|
|
22
23
|
const heading = node("h1", "pp-title", title);
|
|
24
|
+
const handlers = new Map();
|
|
25
|
+
const menuItems = [
|
|
26
|
+
{ id: "viewCaches", label: "查看缓存" },
|
|
27
|
+
{ id: "clearCaches", label: "清空缓存", destructive: true },
|
|
28
|
+
{ id: "reset", label: "重置模块", destructive: true },
|
|
29
|
+
];
|
|
30
|
+
const menu = new ActionMenu(id => handlers.get(id)());
|
|
31
|
+
const trailing = node("span", "pp-nav-spacer");
|
|
32
|
+
trailing.append(menu.element);
|
|
23
33
|
const brand = node("div", "pp-brand");
|
|
24
34
|
const logo = node("span", "pp-brand-icon");
|
|
25
35
|
logo.setAttribute("aria-hidden", "true");
|
|
@@ -30,20 +40,26 @@ export function mountPanel(root, catalog) {
|
|
|
30
40
|
const toast = node("div", "pp-toast");
|
|
31
41
|
toast.setAttribute("role", "status");
|
|
32
42
|
toast.hidden = true;
|
|
33
|
-
header.append(back, brand,
|
|
43
|
+
header.append(back, brand, trailing);
|
|
34
44
|
shell.append(header, viewport, toast);
|
|
35
45
|
root.append(shell);
|
|
36
46
|
// 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
|
|
37
47
|
// Embedded mode publishes navigation state without host reads or mutations of the module DOM.
|
|
38
48
|
const publishNavigation = () => {
|
|
49
|
+
const actions = handlers.size ? menuItems : [];
|
|
50
|
+
menu.update(actions, saving);
|
|
39
51
|
const frame = window.frameElement;
|
|
40
52
|
if (!frame?.dataset.preferencePanes) return;
|
|
41
53
|
frame.dispatchEvent(
|
|
42
54
|
new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
|
|
43
|
-
detail: { title: heading.textContent, module: catalog.module.module, busy: saving, canGoBack: !back.disabled },
|
|
55
|
+
detail: { title: heading.textContent, module: catalog.module.module, busy: saving, canGoBack: !back.disabled, actions },
|
|
44
56
|
}),
|
|
45
57
|
);
|
|
46
58
|
};
|
|
59
|
+
const onAction = event => {
|
|
60
|
+
if (!saving && handlers.has(event.detail)) handlers.get(event.detail)();
|
|
61
|
+
};
|
|
62
|
+
window.frameElement?.addEventListener("preferencepanes:action", onAction);
|
|
47
63
|
let timer,
|
|
48
64
|
navigation,
|
|
49
65
|
generation = 0,
|
|
@@ -353,17 +369,13 @@ export function mountPanel(root, catalog) {
|
|
|
353
369
|
if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
|
|
354
370
|
groups.get(group).append(row);
|
|
355
371
|
}
|
|
356
|
-
const
|
|
357
|
-
maintenance.append(node("h2", "pp-title", "模块数据"));
|
|
358
|
-
const actions = node("div", "pp-actions");
|
|
359
|
-
const cacheView = node("button", "", "查看 Caches");
|
|
360
|
-
const cacheClear = node("button", "", "清空 Caches");
|
|
361
|
-
const reset = node("button", "pp-danger", "重置模块");
|
|
372
|
+
const cachePage = node("section", "pp-cache-page");
|
|
362
373
|
const output = node("pre", "pp-cache");
|
|
363
|
-
output.
|
|
374
|
+
output.textContent = "暂无缓存";
|
|
364
375
|
output.setAttribute("aria-label", "Caches 内容");
|
|
365
|
-
|
|
366
|
-
|
|
376
|
+
cachePage.append(output);
|
|
377
|
+
editors.set("$caches", { node: cachePage, title: "缓存" });
|
|
378
|
+
handlers.set("viewCaches", () => {
|
|
367
379
|
if (saving) return;
|
|
368
380
|
let value;
|
|
369
381
|
return perform(
|
|
@@ -377,12 +389,11 @@ export function mountPanel(root, catalog) {
|
|
|
377
389
|
},
|
|
378
390
|
() => {
|
|
379
391
|
output.textContent = value === undefined ? "暂无缓存" : JSON.stringify(value, null, 2);
|
|
380
|
-
|
|
381
|
-
cacheView.textContent = "刷新 Caches";
|
|
392
|
+
navigation.open("$caches");
|
|
382
393
|
},
|
|
383
394
|
);
|
|
384
|
-
};
|
|
385
|
-
|
|
395
|
+
});
|
|
396
|
+
handlers.set("clearCaches", () => {
|
|
386
397
|
if (saving) return;
|
|
387
398
|
if (!window.confirm(`清空 ${active} 的全部 Caches?`)) return;
|
|
388
399
|
return perform(
|
|
@@ -391,15 +402,12 @@ export function mountPanel(root, catalog) {
|
|
|
391
402
|
output.textContent = "暂无缓存";
|
|
392
403
|
},
|
|
393
404
|
);
|
|
394
|
-
};
|
|
395
|
-
reset
|
|
405
|
+
});
|
|
406
|
+
handlers.set("reset", () => {
|
|
396
407
|
if (saving) return;
|
|
397
408
|
if (!window.confirm(`重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) return;
|
|
398
409
|
return perform(() => client.reset(active), controls);
|
|
399
|
-
};
|
|
400
|
-
actions.append(cacheView, cacheClear, reset);
|
|
401
|
-
maintenance.append(actions, output);
|
|
402
|
-
view.append(maintenance);
|
|
410
|
+
});
|
|
403
411
|
navigation?.destroy();
|
|
404
412
|
navigation = new Navigation(viewport, view, key => editors.get(key)?.node);
|
|
405
413
|
navigation.addEventListener("change", updateNavigation);
|
|
@@ -425,6 +433,8 @@ export function mountPanel(root, catalog) {
|
|
|
425
433
|
*/
|
|
426
434
|
destroy() {
|
|
427
435
|
destroyed = true;
|
|
436
|
+
menu.destroy();
|
|
437
|
+
window.frameElement?.removeEventListener("preferencepanes:action", onAction);
|
|
428
438
|
navigation?.destroy();
|
|
429
439
|
generation++;
|
|
430
440
|
if (active && !saving) client.leave(active);
|
package/src/build.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { BoxJS } from "./BoxJS.mjs";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* Build
|
|
5
|
+
* 仅生成模块前端文件,不复制配置或生成绑定业务的读写脚本。
|
|
6
|
+
* Build module frontend files without copying configuration or generating bound storage scripts.
|
|
7
7
|
* @param {unknown} boxjs 仅包含一个模块的 BoxJS JSON / BoxJS JSON containing exactly one module.
|
|
8
8
|
* @param {string} [css] 可选自定义 CSS 正文 / Optional custom CSS text.
|
|
9
9
|
* @returns {Promise<Record<string, string>>} 相对路径到文件正文的映射 / Relative file paths mapped to file contents.
|
|
@@ -12,22 +12,12 @@ export async function build(boxjs, css = "") {
|
|
|
12
12
|
if (typeof css !== "string") throw new TypeError("CSS must be a string");
|
|
13
13
|
const catalog = new BoxJS(boxjs);
|
|
14
14
|
const module = catalog.module.module;
|
|
15
|
-
const [html, app, navigation
|
|
16
|
-
readFile(new URL("../dist/module/index.html", import.meta.url), "utf8"),
|
|
17
|
-
readFile(new URL("../dist/module/app.mjs", import.meta.url), "utf8"),
|
|
18
|
-
readFile(new URL("../dist/module/navigation.mjs", import.meta.url), "utf8"),
|
|
19
|
-
readFile(new URL("../dist/preference-panes.proxy.js", import.meta.url), "utf8"),
|
|
20
|
-
readFile(new URL("../dist/preference-panes.config.js", import.meta.url), "utf8"),
|
|
21
|
-
]);
|
|
22
|
-
const config = JSON.stringify(catalog.select(module));
|
|
15
|
+
const [html, app, navigation] = await Promise.all([readFile(new URL("../dist/module/index.html", import.meta.url), "utf8"), readFile(new URL("../dist/module/app.mjs", import.meta.url), "utf8"), readFile(new URL("../dist/module/navigation.mjs", import.meta.url), "utf8")]);
|
|
23
16
|
return {
|
|
24
17
|
[`settings/${module}/index.html`]: html,
|
|
25
18
|
[`settings/assets/${module}.html`]: html,
|
|
26
19
|
"settings/assets/app.mjs": app,
|
|
27
20
|
"settings/assets/navigation.mjs": navigation,
|
|
28
|
-
[`settings/assets/${module}.boxjs.json`]: config,
|
|
29
21
|
[`settings/assets/${module}.css`]: css,
|
|
30
|
-
[`settings/assets/${module}.request.js`]: `${proxy}\nPreferencePanes.run(${config},${JSON.stringify(css)});\n`,
|
|
31
|
-
[`settings/assets/${module}.config.js`]: `${mock}\nPreferencePanes.mock(${config});\n`,
|
|
32
22
|
};
|
|
33
23
|
}
|
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
|
-
* POST
|
|
96
|
-
* POST contains
|
|
95
|
+
* POST 的正文为一个 form 字段,字段名是完整 @root.path
|
|
96
|
+
* POST contains one form field whose name is the complete @root.path.
|
|
97
97
|
*/
|
|
98
98
|
body?: string;
|
|
99
99
|
}
|
|
@@ -113,8 +113,8 @@ export interface SettingsResponse {
|
|
|
113
113
|
*/
|
|
114
114
|
headers: Record<string, string>;
|
|
115
115
|
/**
|
|
116
|
-
* JSON
|
|
117
|
-
* JSON
|
|
116
|
+
* JSON 或页面资源正文
|
|
117
|
+
* JSON or page resource body.
|
|
118
118
|
*/
|
|
119
119
|
body: string;
|
|
120
120
|
}
|
|
@@ -269,8 +269,8 @@ export interface BoxJSSubscription extends BoxJSMetadata {
|
|
|
269
269
|
*/
|
|
270
270
|
export type BoxJSInput = BoxJSSetting[] | BoxJSApp | BoxJSSubscription;
|
|
271
271
|
/**
|
|
272
|
-
*
|
|
273
|
-
* Build
|
|
272
|
+
* 生成模块前端文件,不复制配置、不生成绑定模块的代理脚本。
|
|
273
|
+
* Build module frontend files without copying configuration or producing bound proxy scripts.
|
|
274
274
|
* @param boxjs 恰好包含一个模块的 BoxJS JSON / BoxJS JSON describing exactly one module.
|
|
275
275
|
* @param css 可选 CSS 正文 / Optional CSS text.
|
|
276
276
|
* @returns 相对路径到文件内容的映射 / Relative paths mapped to file contents.
|
package/src/lib/page-inputs.mjs
CHANGED
|
@@ -11,7 +11,7 @@ export function pageInputs(url, headers = {}) {
|
|
|
11
11
|
const module = match[1];
|
|
12
12
|
const values = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
13
13
|
const json = values["x-preferencepanes-json"] ?? url.searchParams.get("json") ?? `/configs/${module}`;
|
|
14
|
-
const css = values["x-preferencepanes-css"] ?? url.searchParams.get("css") ??
|
|
14
|
+
const css = values["x-preferencepanes-css"] ?? url.searchParams.get("css") ?? "";
|
|
15
15
|
if (!json.trim()) throw new TypeError("JSON resource URL is required");
|
|
16
16
|
return { url: url.href, module, json, css };
|
|
17
17
|
}
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 解析已经取得的 pathname,避免重复构造 URL。
|
|
3
|
-
* Parse an existing pathname without constructing another URL.
|
|
4
|
-
* @param {string} pathname 以 / 开头的 URL pathname / URL pathname beginning with /.
|
|
5
|
-
* @returns {string[] | undefined} 解码后的路径,非 API 路径不处理 / Decoded path, or undefined outside /api/.
|
|
6
|
-
* @throws {TypeError} 转义编码或路径片段非法 / Invalid percent encoding or path segments.
|
|
7
|
-
*/
|
|
8
|
-
export function parseSettingsPathname(pathname) {
|
|
9
|
-
if (!pathname.startsWith("/api/")) return;
|
|
10
|
-
let parts;
|
|
11
|
-
try {
|
|
12
|
-
parts = pathname.slice(5).replace(/\/$/, "").split("/").map(decodeURIComponent);
|
|
13
|
-
} catch {
|
|
14
|
-
throw new TypeError("Invalid encoded key path");
|
|
15
|
-
}
|
|
16
|
-
return validatePathParts(parts);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
1
|
/**
|
|
20
2
|
* 校验原始路径片段,不进行 URL 编码转换。
|
|
21
3
|
* Validate raw path segments without URL encoding conversion.
|
package/src/proxy/handler.mjs
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
1
|
import { URL } from "@nsnanocat/url";
|
|
2
2
|
import assets from "#assets";
|
|
3
|
-
import { BoxJS } from "../BoxJS.mjs";
|
|
4
3
|
import { pageInputs } from "../lib/page-inputs.mjs";
|
|
5
4
|
import { response } from "../lib/response.mjs";
|
|
6
5
|
import { Store } from "../Store.mjs";
|
|
7
6
|
import { complete } from "./response.mjs";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @
|
|
13
|
-
* @param {string} [css] 自定义 CSS 正文 / Custom CSS text.
|
|
14
|
-
* @returns {Promise<void>} 已提交宿主响应 / Delivered host response.
|
|
9
|
+
* 通用代理入口:提供无鉴权 form 存储 API 及模块页面,不含业务配置。
|
|
10
|
+
* Generic proxy entry serving unauthenticated form storage and module pages without business configuration.
|
|
11
|
+
* @returns {Promise<void>} 已交给代理宿主的响应 / Response delivered to the proxy host.
|
|
15
12
|
*/
|
|
16
|
-
|
|
13
|
+
async function run() {
|
|
17
14
|
const request = globalThis.$request;
|
|
18
15
|
let result;
|
|
19
16
|
try {
|
|
20
|
-
if (typeof css !== "string") throw new TypeError("CSS must be a string");
|
|
21
|
-
const catalog = new BoxJS(boxjs);
|
|
22
|
-
const module = catalog.module.module;
|
|
23
17
|
const url = new URL(request.url);
|
|
24
18
|
switch (true) {
|
|
25
19
|
case url.pathname.startsWith("/api/"):
|
|
26
|
-
result = await new Store(
|
|
20
|
+
result = await new Store().handle(request, url);
|
|
27
21
|
break;
|
|
28
|
-
case url.pathname
|
|
29
|
-
break;
|
|
30
|
-
case url.pathname === `/settings/${module}` || url.pathname === `/settings/${module}/`: {
|
|
31
|
-
// Header 由代理传入文档,浏览器再下载 JSON/CSS;代理不获取外部资源。
|
|
32
|
-
// Carry headers into the document; only the browser downloads external JSON/CSS.
|
|
22
|
+
case /^\/settings\/[a-zA-Z0-9_-]+\/?$/.test(url.pathname): {
|
|
33
23
|
const inputs = encodeURIComponent(JSON.stringify(pageInputs(url, request.headers)));
|
|
34
|
-
|
|
35
|
-
result = response(request, 200, html, "text/html");
|
|
24
|
+
result = response(request, 200, assets.page.body.replace("</head>", `<meta name="preference-panes-inputs" content="${inputs}"></head>`), "text/html");
|
|
36
25
|
break;
|
|
37
26
|
}
|
|
38
|
-
case url.pathname === `/settings/assets/${module}.css`:
|
|
39
|
-
result = response(request, 200, css, "text/css");
|
|
40
|
-
break;
|
|
41
27
|
default: {
|
|
42
28
|
const asset = assets[url.pathname];
|
|
43
29
|
if (asset) result = response(request, 200, asset.body, asset.type);
|
|
@@ -46,7 +32,8 @@ export async function run(boxjs, css = "") {
|
|
|
46
32
|
if (result && !url.pathname.startsWith("/api/") && !["GET", "HEAD"].includes(request.method)) result = response(request, 405, { error: "Method not allowed" });
|
|
47
33
|
} catch (error) {
|
|
48
34
|
console.error(`PreferencePanes: ${error.message}`);
|
|
49
|
-
result = response(request, 500, { error:
|
|
35
|
+
result = response(request, 500, { error: error.message });
|
|
50
36
|
}
|
|
51
37
|
complete(result);
|
|
52
38
|
}
|
|
39
|
+
run();
|