@nsnanocat/preference-panes 0.3.1 → 0.4.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.
@@ -0,0 +1,134 @@
1
+ /* 通用菜单样式;项目可通过 JSON 引用官方组件样式。
2
+ * Generic menu styles; projects may reference official component styles through JSON. */
3
+ body {
4
+ margin: 0;
5
+ overflow-x: hidden;
6
+ font-family: system-ui, sans-serif;
7
+ background: #f6f7f8;
8
+ color: #18191c;
9
+ }
10
+ .pp-home {
11
+ max-width: 720px;
12
+ margin: auto;
13
+ padding: 32px 16px;
14
+ }
15
+ .brand-logo {
16
+ display: block;
17
+ width: 64px;
18
+ height: 64px;
19
+ margin: auto;
20
+ }
21
+ .brand-logo img {
22
+ display: block;
23
+ width: 100%;
24
+ height: 100%;
25
+ object-fit: contain;
26
+ }
27
+ .pp-home h1 {
28
+ text-align: center;
29
+ font-size: 26px;
30
+ margin: 12px 0 32px;
31
+ }
32
+ .self-panel {
33
+ padding: 12px;
34
+ background: #fff;
35
+ border-radius: 12px;
36
+ }
37
+ .self-panel .header {
38
+ font-size: 16px;
39
+ margin: 0;
40
+ padding: 0 6px;
41
+ }
42
+ .self-panel .container {
43
+ margin-top: 16px;
44
+ }
45
+ .self-panel .self-item {
46
+ width: 25%;
47
+ min-width: 0;
48
+ border: 0;
49
+ background: none;
50
+ padding: 12px 2px;
51
+ color: inherit;
52
+ cursor: pointer;
53
+ }
54
+ .self-panel .self-item .logo {
55
+ display: block;
56
+ width: 56px;
57
+ height: 56px;
58
+ padding: 10px;
59
+ box-sizing: border-box;
60
+ border-radius: 50%;
61
+ background: #f1f2f3;
62
+ }
63
+ .self-panel .logo img {
64
+ display: block;
65
+ width: 100%;
66
+ height: 100%;
67
+ object-fit: contain;
68
+ }
69
+ .self-panel .self-item.is-zh .name {
70
+ font-size: 13px;
71
+ line-height: 20px;
72
+ margin-top: 8px;
73
+ white-space: normal;
74
+ }
75
+ .module-status {
76
+ font-size: 11px;
77
+ line-height: 16px;
78
+ min-height: 16px;
79
+ color: #9499a0;
80
+ }
81
+ .self-item:disabled {
82
+ opacity: 0.45;
83
+ cursor: default;
84
+ }
85
+ .settings-note {
86
+ font-size: 13px;
87
+ line-height: 1.6;
88
+ color: #797f89;
89
+ padding: 16px 0;
90
+ }
91
+ @media (prefers-color-scheme: dark) {
92
+ body {
93
+ background: #101114;
94
+ color: #e3e5e7;
95
+ }
96
+ .self-panel {
97
+ background: #1c1d20;
98
+ }
99
+ .self-panel .self-item .logo {
100
+ background: #303136;
101
+ }
102
+ }
103
+ :root[data-theme="dark"] body {
104
+ background: #101114;
105
+ color: #e3e5e7;
106
+ }
107
+ :root[data-theme="dark"] .self-panel {
108
+ background: #1c1d20;
109
+ }
110
+ :root[data-theme="dark"] .self-panel .self-item .logo {
111
+ background: #303136;
112
+ }
113
+ :root[data-theme="light"] body {
114
+ background: #f6f7f8;
115
+ color: #18191c;
116
+ }
117
+ :root[data-theme="light"] .self-panel {
118
+ background: #fff;
119
+ }
120
+ :root[data-theme="light"] .self-panel .self-item .logo {
121
+ background: #f1f2f3;
122
+ }
123
+
124
+ .self-panel .scroll {
125
+ display: flex;
126
+ }
127
+ .self-panel .self-item {
128
+ display: flex;
129
+ flex-direction: column;
130
+ align-items: center;
131
+ }
132
+ .pp-site-error {
133
+ padding: 16px;
134
+ }
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
+ <meta name="color-scheme" content="light dark">
7
+ <title>Preferences</title>
8
+ <link rel="stylesheet" href="/settings/assets/panel.css?v=0.4.1">
9
+ <link rel="stylesheet" href="/settings/assets/home.css?v=0.4.1">
10
+ </head>
11
+ <body>
12
+ <main id="preferences"></main>
13
+ <script type="module" src="/settings/assets/app.mjs?v=0.4.1"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,325 @@
1
+ /* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。
2
+ * Grouped rows follow the Bilibili settings layout, scoped to the panel. */
3
+ .pp-panel {
4
+ --pp-text: #18191c;
5
+ --pp-background: #f6f7f8;
6
+ --pp-surface: #fff;
7
+ --pp-border: #e3e5e7;
8
+ --pp-muted: #9499a0;
9
+ --pp-accent: #fb7299;
10
+ font:
11
+ 15px / 1.5 -apple-system,
12
+ BlinkMacSystemFont,
13
+ "Segoe UI",
14
+ sans-serif;
15
+ color: var(--pp-text);
16
+ background: var(--pp-background);
17
+ position: relative;
18
+ min-height: 100vh;
19
+ }
20
+ .pp-panel * {
21
+ box-sizing: border-box;
22
+ letter-spacing: 0;
23
+ }
24
+ .pp-header {
25
+ height: calc(52px + env(safe-area-inset-top));
26
+ padding: env(safe-area-inset-top) 12px 0;
27
+ display: flex;
28
+ align-items: center;
29
+ background: var(--pp-surface);
30
+ border-bottom: 1px solid var(--pp-border);
31
+ position: relative;
32
+ }
33
+ .pp-title {
34
+ font-size: 17px;
35
+ font-weight: 500;
36
+ margin: 0;
37
+ min-width: 0;
38
+ overflow-wrap: anywhere;
39
+ }
40
+ .pp-header .pp-title {
41
+ flex: 1;
42
+ text-align: center;
43
+ }
44
+ .pp-nav-spacer {
45
+ width: 44px;
46
+ flex: none;
47
+ }
48
+ .pp-panel button {
49
+ font: inherit;
50
+ cursor: pointer;
51
+ border: 0;
52
+ background: none;
53
+ color: inherit;
54
+ }
55
+ .pp-panel .pp-back {
56
+ width: 44px;
57
+ height: 44px;
58
+ flex: none;
59
+ font-size: 34px;
60
+ line-height: 32px;
61
+ padding: 0;
62
+ }
63
+ .pp-panel button:disabled {
64
+ opacity: 0.5;
65
+ cursor: wait;
66
+ }
67
+ .pp-viewport {
68
+ position: relative;
69
+ height: calc(100vh - 52px - env(safe-area-inset-top));
70
+ overflow: hidden;
71
+ }
72
+ @supports (height: 100dvh) {
73
+ .pp-viewport {
74
+ height: calc(100dvh - 52px - env(safe-area-inset-top));
75
+ }
76
+ }
77
+ .pp-fields,
78
+ .pp-choice-page {
79
+ position: absolute;
80
+ inset: 0;
81
+ overflow: auto;
82
+ padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));
83
+ background: var(--pp-background);
84
+ }
85
+ .pp-panel .form-group {
86
+ margin: 0 0 12px;
87
+ }
88
+ .pp-panel .form-group__title {
89
+ font-size: 12px;
90
+ line-height: 17px;
91
+ font-weight: 400;
92
+ color: var(--pp-muted);
93
+ padding-left: 12px;
94
+ margin: 12px 0 6px;
95
+ }
96
+ .pp-panel .form-group__row {
97
+ border-radius: 8px;
98
+ overflow: hidden;
99
+ background: var(--pp-surface);
100
+ }
101
+ .pp-panel .form-row {
102
+ position: relative;
103
+ display: flex;
104
+ align-items: center;
105
+ width: 100%;
106
+ min-height: 46px;
107
+ padding: 12px;
108
+ border: 0;
109
+ border-bottom: 1px solid var(--pp-border);
110
+ background: var(--pp-surface);
111
+ gap: 12px;
112
+ }
113
+ .pp-panel .form-row:last-child {
114
+ border-bottom: 0;
115
+ }
116
+ .pp-panel .form-row__text {
117
+ flex: 1;
118
+ min-width: 0;
119
+ margin: 0;
120
+ display: flex;
121
+ flex-direction: column;
122
+ }
123
+ .pp-panel .form-row__title {
124
+ font-size: 15px;
125
+ line-height: 22px;
126
+ color: var(--pp-text);
127
+ text-align: left;
128
+ }
129
+ .pp-panel .form-row__subtitle {
130
+ font-size: 12px;
131
+ line-height: 18px;
132
+ color: var(--pp-muted);
133
+ overflow-wrap: anywhere;
134
+ margin-top: 2px;
135
+ }
136
+ .pp-choice-link {
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: flex-end;
140
+ gap: 8px;
141
+ max-width: 45%;
142
+ min-width: 44px;
143
+ min-height: 44px;
144
+ padding: 0;
145
+ text-align: right;
146
+ flex: 1;
147
+ }
148
+ .pp-summary {
149
+ color: var(--pp-muted);
150
+ font-size: 13px;
151
+ line-height: 18px;
152
+ display: -webkit-box;
153
+ -webkit-line-clamp: 2;
154
+ -webkit-box-orient: vertical;
155
+ overflow: hidden;
156
+ overflow-wrap: anywhere;
157
+ }
158
+ .pp-chevron {
159
+ color: var(--pp-muted);
160
+ font-size: 22px;
161
+ flex: none;
162
+ }
163
+ .pp-input {
164
+ font: inherit;
165
+ color: var(--pp-text);
166
+ background: var(--pp-surface);
167
+ border: 1px solid var(--pp-border);
168
+ border-radius: 6px;
169
+ padding: 8px;
170
+ min-width: 0;
171
+ max-width: 45%;
172
+ width: 45%;
173
+ }
174
+ select.pp-input {
175
+ text-overflow: ellipsis;
176
+ font-size: 13px;
177
+ }
178
+ .pp-panel .pp-multiline {
179
+ display: block;
180
+ }
181
+ .pp-multiline .pp-input {
182
+ max-width: 100%;
183
+ width: 100%;
184
+ margin-top: 10px;
185
+ }
186
+ .pp-switch {
187
+ appearance: none;
188
+ -webkit-appearance: none;
189
+ position: relative;
190
+ flex: none;
191
+ width: 32px;
192
+ height: 20px;
193
+ max-width: none;
194
+ border: 0;
195
+ border-radius: 15px;
196
+ padding: 0;
197
+ background: #c9ccd0;
198
+ cursor: pointer;
199
+ transition: background 0.2s;
200
+ }
201
+ .pp-switch::before {
202
+ content: "";
203
+ position: absolute;
204
+ top: 3px;
205
+ left: 3px;
206
+ width: 14px;
207
+ height: 14px;
208
+ border-radius: 50%;
209
+ background: white;
210
+ transition: transform 0.2s;
211
+ }
212
+ .pp-switch:checked {
213
+ background: var(--pp-accent);
214
+ }
215
+ .pp-switch:checked::before {
216
+ transform: translateX(12px);
217
+ }
218
+ .pp-choice {
219
+ justify-content: space-between;
220
+ cursor: pointer;
221
+ }
222
+ .pp-choice input {
223
+ width: 20px;
224
+ height: 20px;
225
+ flex: none;
226
+ accent-color: var(--pp-accent);
227
+ margin: 0;
228
+ }
229
+ .pp-description {
230
+ font-size: 12px;
231
+ line-height: 1.6;
232
+ color: var(--pp-muted);
233
+ white-space: pre-wrap;
234
+ overflow-wrap: anywhere;
235
+ }
236
+ .pp-module-info {
237
+ display: flex;
238
+ gap: 12px;
239
+ margin: 12px 0;
240
+ }
241
+ .pp-module-icon {
242
+ width: 48px;
243
+ height: 48px;
244
+ object-fit: contain;
245
+ flex: none;
246
+ }
247
+ .pp-module-details {
248
+ min-width: 0;
249
+ overflow-wrap: anywhere;
250
+ }
251
+ .pp-module-source {
252
+ color: inherit;
253
+ text-decoration: underline;
254
+ }
255
+ .pp-maintenance {
256
+ margin-top: 24px;
257
+ }
258
+ .pp-actions {
259
+ display: flex;
260
+ flex-wrap: wrap;
261
+ gap: 8px;
262
+ }
263
+ .pp-actions button,
264
+ .pp-error button {
265
+ min-height: 44px;
266
+ padding: 8px 12px;
267
+ border-radius: 6px;
268
+ background: var(--pp-surface);
269
+ }
270
+ .pp-panel .pp-danger {
271
+ color: #e45656;
272
+ }
273
+ .pp-cache {
274
+ max-height: 320px;
275
+ overflow: auto;
276
+ white-space: pre-wrap;
277
+ overflow-wrap: anywhere;
278
+ }
279
+ .pp-toast {
280
+ pointer-events: none;
281
+ position: fixed;
282
+ bottom: calc(30px + env(safe-area-inset-bottom));
283
+ left: 50%;
284
+ transform: translateX(-50%);
285
+ max-width: 90vw;
286
+ padding: 10px 16px;
287
+ border-radius: 8px;
288
+ background: #333e;
289
+ color: white;
290
+ font-size: 13px;
291
+ z-index: 20;
292
+ }
293
+ .pp-toast[data-kind="error"] {
294
+ background: #8d2424;
295
+ }
296
+ .pp-panel :focus-visible {
297
+ outline: 2px solid var(--pp-accent);
298
+ outline-offset: -2px;
299
+ }
300
+ @media (prefers-color-scheme: dark) {
301
+ .pp-panel {
302
+ --pp-text: #e3e5e7;
303
+ --pp-background: #17181a;
304
+ --pp-surface: #232427;
305
+ --pp-border: #343538;
306
+ }
307
+ }
308
+ :root[data-theme="dark"] .pp-panel {
309
+ --pp-text: #e3e5e7;
310
+ --pp-background: #17181a;
311
+ --pp-surface: #232427;
312
+ --pp-border: #343538;
313
+ }
314
+ :root[data-theme="light"] .pp-panel {
315
+ --pp-text: #18191c;
316
+ --pp-background: #f6f7f8;
317
+ --pp-surface: #fff;
318
+ --pp-border: #e3e5e7;
319
+ }
320
+ @media (prefers-reduced-motion: reduce) {
321
+ .pp-panel .pp-switch,
322
+ .pp-panel .pp-switch::before {
323
+ transition: none;
324
+ }
325
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsnanocat/preference-panes",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Shared settings API runtime for JavaScript proxy modules",
5
5
  "author": "VirgilClyne <Virgil@nanocat.me>",
6
6
  "homepage": "https://NSNanoCat.github.io/preference-panes",
@@ -28,7 +28,8 @@
28
28
  },
29
29
  "./browser/panel.css": "./src/browser/panel.css",
30
30
  "./dist/preference-panes.mjs": "./dist/preference-panes.mjs",
31
- "./dist/preference-panes.request.js": "./dist/preference-panes.request.js"
31
+ "./dist/preference-panes.request.js": "./dist/preference-panes.request.js",
32
+ "./dist/settings/*": "./dist/settings/*"
32
33
  },
33
34
  "types": "src/index.d.ts",
34
35
  "scripts": {
@@ -0,0 +1,50 @@
1
+ import { URL } from "@nsnanocat/url";
2
+ import { fetch } from "@nsnanocat/util/polyfill/fetch";
3
+ import { SettingsHandler } from "./SettingsHandler.mjs";
4
+
5
+ /**
6
+ * 统一处理存储 API 和无原生 Mock 平台的静态资源请求。
7
+ * Handle storage APIs and static resources on hosts without native Mock support.
8
+ */
9
+ export class PreferencesHandler extends SettingsHandler {
10
+ #origin;
11
+ #resources;
12
+ /**
13
+ * 根据安装 JSON 配置资源映射,不执行项目自定义代码。
14
+ * Configure resource mappings from installation JSON without project-specific code.
15
+ * @param {import("./index.js").PreferencesHandlerOptions} options 安装映射 / Installation mapping.
16
+ */
17
+ constructor(options) {
18
+ super(options);
19
+ this.#origin = new URL(options.origin).origin;
20
+ this.#resources = options.resources.map(({ pattern, source, contentType }) => {
21
+ const url = new URL(source);
22
+ if (url.protocol !== "https:") throw new TypeError("Resource sources must use HTTPS");
23
+ if (typeof contentType !== "string" || /[\r\n]/.test(contentType)) throw new TypeError("Invalid resource content type");
24
+ return { pattern: new RegExp(pattern), source: url.href, contentType };
25
+ });
26
+ }
27
+ /**
28
+ * API 先交给存储桥接,只有资源路由才发出下载请求。
29
+ * Dispatch APIs to storage first; only resource routes perform downloads.
30
+ * @param {import("./index.js").SettingsRequest} request 宿主请求 / Host request.
31
+ * @returns {Promise<import("./index.js").SettingsResponse | undefined>} 响应或非接管请求 / Response or unhandled request.
32
+ */
33
+ async handle(request) {
34
+ const api = await super.handle(request);
35
+ if (api) return api;
36
+ const url = new URL(request.url);
37
+ if (url.origin !== this.#origin) return;
38
+ const resource = this.#resources.find(resource => resource.pattern.test(url.pathname));
39
+ if (!resource) return;
40
+ switch (request.method) {
41
+ case "GET":
42
+ case "HEAD": {
43
+ const response = await fetch({ url: resource.source, method: "GET", headers: { "Cache-Control": "no-cache" }, timeout: 5000 });
44
+ return { status: response.status, headers: { "Content-Type": `${resource.contentType}; charset=utf-8`, "Cache-Control": "no-store" }, body: request.method === "HEAD" ? "" : response.body };
45
+ }
46
+ default:
47
+ return { status: 405, headers: { Allow: "HEAD, GET" }, body: "" };
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,154 @@
1
+ import { validatePathParts } from "../lib/settings-path.mjs";
2
+ import { createPreferencesClient } from "./client.mjs";
3
+ import { mountPreferencePanes } from "./panel.mjs";
4
+
5
+ /**
6
+ * 自包含页面入口,业务名称、图标和入口全部从站点 BoxJS JSON 读取。
7
+ * Self-contained page entry reading all branding, icons and navigation from site BoxJS JSON.
8
+ * @module @nsnanocat/preference-panes/site
9
+ */
10
+ const root = document.querySelector("#preferences");
11
+ const client = createPreferencesClient({ timeout: 3500 });
12
+ const theme = navigator.userAgent.match(/themeId\/(\d+)/)?.[1];
13
+ if (theme) document.documentElement.dataset.theme = theme === "2" ? "dark" : "light";
14
+ let panel,
15
+ menu,
16
+ revision = 0,
17
+ routedPath;
18
+
19
+ /**
20
+ * 创建只含纯文本的元素。
21
+ * Create an element containing plain text only.
22
+ * @param {string} tag 标签 / Tag.
23
+ * @param {string} className 样式 / CSS class.
24
+ * @param {string} [text] 文本 / Text.
25
+ * @returns {HTMLElement} 元素 / Element.
26
+ */
27
+ function node(tag, className, text) {
28
+ const element = document.createElement(tag);
29
+ element.className = className;
30
+ if (text !== undefined) element.textContent = text;
31
+ return element;
32
+ }
33
+
34
+ /**
35
+ * 按站点 JSON 显示图标;iconDark 是明确的暗色扩展,不改变 BoxJS icons 语义。
36
+ * Display configured icons; iconDark is an explicit dark variant without changing BoxJS icons semantics.
37
+ * @param {{icon?:string,iconDark?:string,icons?:string[]}} metadata 图标数据 / Icon data.
38
+ * @param {string} className 样式 / CSS class.
39
+ * @returns {HTMLPictureElement} 图片元素 / Picture element.
40
+ */
41
+ function icon(metadata, className) {
42
+ const picture = node("picture", className),
43
+ image = node("img", "");
44
+ if (metadata.iconDark) {
45
+ const source = node("source", "");
46
+ source.media = theme ? (theme === "2" ? "all" : "not all") : "(prefers-color-scheme: dark)";
47
+ source.srcset = resourceURL(metadata.iconDark);
48
+ picture.append(source);
49
+ }
50
+ const selected = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
51
+ if (selected) image.src = resourceURL(selected);
52
+ image.alt = "";
53
+ picture.append(image);
54
+ return picture;
55
+ }
56
+
57
+ /**
58
+ * 校验配置中的资源 URL。
59
+ * Validate resource URLs in site configuration.
60
+ * @param {string} value 地址 / Address.
61
+ * @returns {string} 完整 URL / Absolute URL.
62
+ */
63
+ function resourceURL(value) {
64
+ const url = new URL(value, location.href);
65
+ if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Resources must use HTTP(S)");
66
+ return url.href;
67
+ }
68
+
69
+ /**
70
+ * 挂载通用设置或站点菜单,每次进入菜单都重新 HEAD 探测。
71
+ * Mount settings or the site menu, repeating HEAD probes on every menu entry.
72
+ * @returns {Promise<void>} 挂载完成 / Mount completion.
73
+ */
74
+ async function render() {
75
+ routedPath = location.pathname;
76
+ const version = ++revision;
77
+ panel?.destroy();
78
+ panel = undefined;
79
+ root.replaceChildren();
80
+ if (routedPath !== "/settings/") {
81
+ document.title = routedPath.split("/")[2] || "Preferences";
82
+ panel = mountPreferencePanes({ element: root });
83
+ return;
84
+ }
85
+ try {
86
+ if (!menu) {
87
+ const response = await fetch("/settings/assets/site.boxjs.json", { cache: "no-store", credentials: "omit" });
88
+ if (response.status !== 200) throw new Error(`Site config HTTP ${response.status}`);
89
+ const data = await response.json();
90
+ if (typeof data.name !== "string" || !Array.isArray(data.apps)) throw new TypeError("Invalid site BoxJS JSON");
91
+ for (const app of data.apps) validatePathParts([app.module]);
92
+ const localStyles = document.head.querySelector('link[rel="stylesheet"]');
93
+ for (const href of data.stylesheets ?? []) {
94
+ const link = document.createElement("link");
95
+ link.rel = "stylesheet";
96
+ link.href = resourceURL(href);
97
+ document.head.insertBefore(link, localStyles);
98
+ }
99
+ menu = data;
100
+ }
101
+ if (version !== revision) return;
102
+ document.title = menu.name;
103
+ const home = node("section", "pp-home");
104
+ home.append(icon(menu, "brand-logo"), node("h1", "", menu.name));
105
+ const section = node("section", "self-panel is-zh");
106
+ section.append(node("h2", "header", menu.sectionTitle ?? "模块"));
107
+ const container = node("div", "container"),
108
+ scrollView = node("div", "scroll-view"),
109
+ rows = node("div", "scroll");
110
+ scrollView.append(rows);
111
+ container.append(scrollView);
112
+ section.append(container);
113
+ home.append(section);
114
+ if (menu.desc) home.append(node("p", "settings-note", menu.desc));
115
+ root.append(home);
116
+ for (const app of menu.apps) {
117
+ const button = node("button", "self-item is-zh"),
118
+ status = node("span", "module-status", "检测中");
119
+ button.type = "button";
120
+ button.disabled = true;
121
+ button.dataset.module = app.module;
122
+ button.append(icon(app, "logo"), node("span", "name", app.name ?? app.module), status);
123
+ rows.append(button);
124
+ button.onclick = () => {
125
+ history.pushState(null, "", `/settings/${app.module}`);
126
+ render();
127
+ if (!matchMedia("(prefers-reduced-motion: reduce)").matches) root.animate([{ transform: "translateX(100%)" }, { transform: "translateX(0)" }], { duration: 260, easing: "ease-out" });
128
+ };
129
+ client.probe(app.module).then(available => {
130
+ if (revision !== version) return;
131
+ button.disabled = !available;
132
+ status.textContent = available ? "" : "未响应";
133
+ });
134
+ }
135
+ } catch (error) {
136
+ if (version === revision) {
137
+ const retry = node("button", "", "重新读取");
138
+ retry.type = "button";
139
+ retry.onclick = render;
140
+ root.replaceChildren(node("p", "pp-site-error", `加载失败:${error.message}`), retry);
141
+ }
142
+ }
143
+ }
144
+
145
+ window.addEventListener("popstate", () => {
146
+ if (routedPath !== location.pathname) {
147
+ render();
148
+ if (location.pathname === "/settings/" && !matchMedia("(prefers-reduced-motion: reduce)").matches) root.animate([{ transform: "translateX(-100%)" }, { transform: "translateX(0)" }], { duration: 260, easing: "ease-out" });
149
+ }
150
+ });
151
+ window.addEventListener("pageshow", event => {
152
+ if (event.persisted && location.pathname === "/settings/") render();
153
+ });
154
+ render();