@nsnanocat/preference-panes 0.9.8 → 0.9.10
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 +2 -4
- package/dist/api.js +1 -1
- package/dist/module/app.mjs +81 -108
- package/dist/module/index.html +1 -1
- package/dist/module/navigation.mjs +67 -36
- package/dist/preference-panes.mjs +78 -105
- package/package.json +1 -1
- package/src/BoxJS.mjs +4 -18
- package/src/browser/ActionMenu.mjs +67 -36
- package/src/browser/Navigation.d.mts +6 -0
- package/src/browser/app.mjs +3 -3
- package/src/browser/client.mjs +1 -1
- package/src/browser/components.mjs +0 -16
- package/src/browser/index.mjs +1 -1
- package/src/browser/panel.css +9 -30
- package/src/browser/panel.mjs +2 -31
- package/src/build.mjs +1 -3
- package/src/lib/boxjs.mjs +3 -2
package/dist/module/app.mjs
CHANGED
|
@@ -22,8 +22,8 @@ class BoxJS {
|
|
|
22
22
|
*/
|
|
23
23
|
constructor(input) {
|
|
24
24
|
if (!input || typeof input !== "object") throw new TypeError("Expected BoxJS JSON");
|
|
25
|
-
|
|
26
|
-
const apps = Array.isArray(
|
|
25
|
+
const document = JSON.parse(JSON.stringify(input));
|
|
26
|
+
const apps = Array.isArray(document) ? [{ settings: document }] : (document.apps ?? [document]);
|
|
27
27
|
if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
|
|
28
28
|
this.modules = new Map();
|
|
29
29
|
for (const app of apps) {
|
|
@@ -31,7 +31,7 @@ class BoxJS {
|
|
|
31
31
|
for (const entry of app.settings) {
|
|
32
32
|
if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
|
|
33
33
|
if (!entry.id.startsWith("@")) {
|
|
34
|
-
if (Array.isArray(
|
|
34
|
+
if (Array.isArray(document)) throw new TypeError("BoxJS settings require @root.path IDs");
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
37
|
const [storageKey, ...parts] = entry.id.slice(1).split(".");
|
|
@@ -48,24 +48,10 @@ class BoxJS {
|
|
|
48
48
|
target.owners.add(app);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
this.metadata = metadata(Array.isArray(
|
|
51
|
+
this.metadata = metadata(Array.isArray(document) ? {} : document);
|
|
52
52
|
for (const target of this.modules.values()) target.metadata = target.owners.size === 1 ? metadata([...target.owners][0]) : {};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
* 提取一个模块的原生 BoxJS,保留所属 app 的元数据。
|
|
57
|
-
* Select a module's native BoxJS while retaining owning-app metadata.
|
|
58
|
-
* @param {string} module 模块标识 / Module identifier.
|
|
59
|
-
* @returns {unknown} 可直接用作配置 Mock 的 JSON / JSON suitable for a configuration Mock.
|
|
60
|
-
*/
|
|
61
|
-
select(module) {
|
|
62
|
-
const target = this.modules.get(module);
|
|
63
|
-
if (!target) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
64
|
-
if (Array.isArray(this.document)) return target.entries;
|
|
65
|
-
const apps = [...target.owners].map(app => ({ ...app, settings: app.settings.filter(entry => target.entries.includes(entry)) }));
|
|
66
|
-
return this.document.apps ? { ...this.document, apps } : apps[0];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
55
|
/**
|
|
70
56
|
* 取得本次导入的唯一模块,避免把模块数据变成项目目录。
|
|
71
57
|
* Get the single imported module without turning module data into a project directory.
|
|
@@ -163,22 +149,6 @@ function resourceURL(value) {
|
|
|
163
149
|
return url.href;
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
/**
|
|
167
|
-
* 展示标准 BoxJS 图标;icons 保持透明/彩色语义,不解释为亮暗版本。
|
|
168
|
-
* Display standard BoxJS icons, preserving transparent/color rather than light/dark semantics.
|
|
169
|
-
* @param {import("../index.js").BoxJSMetadata} metadata 展示信息 / Presentation metadata.
|
|
170
|
-
* @param {string} className 样式 / CSS class.
|
|
171
|
-
* @returns {HTMLImageElement | null} 图标或无图标 / Icon or no icon.
|
|
172
|
-
*/
|
|
173
|
-
function icon(metadata, className) {
|
|
174
|
-
const source = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
|
|
175
|
-
if (!source) return null;
|
|
176
|
-
const image = element("img", className);
|
|
177
|
-
image.src = resourceURL(source);
|
|
178
|
-
image.alt = "";
|
|
179
|
-
return image;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
152
|
/**
|
|
183
153
|
* 共享加载失败视图,不创建配置表单或数据读取。
|
|
184
154
|
* Share a load-error view without creating controls or reading settings.
|
|
@@ -213,20 +183,21 @@ function requestConfirmation(host, message) {
|
|
|
213
183
|
});
|
|
214
184
|
}
|
|
215
185
|
|
|
216
|
-
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n height: 100vh;\n}\n\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n
|
|
186
|
+
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n:root[data-preference-panes-embedded] .pp-header {\n display: none;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-error button {\n min-height: 44px;\n padding: 8px 12px;\n border-radius: 6px;\n background: var(--pp-surface);\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n";
|
|
217
187
|
|
|
218
188
|
/**
|
|
219
|
-
*
|
|
220
|
-
* Shared
|
|
189
|
+
* 共用三点按钮和底部操作菜单;弹层挂载到文档根部,不受标题栏显示状态影响。
|
|
190
|
+
* Shared overflow trigger and bottom action sheet; the layer is mounted at document level and remains independent of header visibility.
|
|
221
191
|
*/
|
|
222
192
|
class ActionMenu {
|
|
223
193
|
#button;
|
|
224
|
-
#
|
|
225
|
-
#
|
|
194
|
+
#layer;
|
|
195
|
+
#items;
|
|
226
196
|
#select;
|
|
227
197
|
#document;
|
|
198
|
+
#disabled = true;
|
|
228
199
|
#key = event => {
|
|
229
|
-
if (event.key === "Escape" && !this.#
|
|
200
|
+
if (event.key === "Escape" && !this.#layer.hidden) {
|
|
230
201
|
event.preventDefault();
|
|
231
202
|
this.close();
|
|
232
203
|
this.#button.focus();
|
|
@@ -242,44 +213,55 @@ class ActionMenu {
|
|
|
242
213
|
this.#document = document;
|
|
243
214
|
this.#select = select;
|
|
244
215
|
this.element = document.createElement("span");
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
:host{display:inline-flex;
|
|
248
|
-
:host([hidden])
|
|
249
|
-
button{font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
216
|
+
const triggerRoot = this.element.attachShadow({ mode: "open" });
|
|
217
|
+
triggerRoot.innerHTML = `<style>
|
|
218
|
+
:host{display:inline-flex;width:44px;height:44px;color:inherit}
|
|
219
|
+
:host([hidden]){display:none!important}
|
|
220
|
+
button{width:44px;height:44px;padding:10px;font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
250
221
|
button:disabled{opacity:.4;cursor:default}
|
|
251
222
|
button:focus-visible{outline:2px solid currentColor;outline-offset:-3px}
|
|
252
|
-
#trigger{width:44px;height:44px;padding:10px;position:relative;z-index:3}
|
|
253
223
|
svg{display:block;width:24px;height:24px;fill:currentColor}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
224
|
+
</style><button type="button" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="4" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="20" cy="12" r="2"/></svg></button>`;
|
|
225
|
+
this.#button = triggerRoot.querySelector("button");
|
|
226
|
+
this.#layer = document.createElement("span");
|
|
227
|
+
const layerRoot = this.#layer.attachShadow({ mode: "open" });
|
|
228
|
+
layerRoot.innerHTML = `<style>
|
|
229
|
+
:host{position:fixed;inset:0;z-index:2147483647;color:var(--pp-text,CanvasText);font:16px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
230
|
+
:host([hidden]){display:none!important}
|
|
231
|
+
button{font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
232
|
+
button:focus-visible{outline:2px solid var(--pp-accent,Highlight);outline-offset:-3px}
|
|
233
|
+
#backdrop{position:absolute;inset:0;width:100%;height:100%;padding:0;background:#0008;animation:pp-fade-in .18s ease-out}
|
|
234
|
+
#sheet{position:absolute;z-index:1;left:0;right:0;bottom:0;width:min(100%,540px);max-height:calc(100% - 24px);margin:auto;padding:8px 8px calc(8px + env(safe-area-inset-bottom));animation:pp-sheet-in .22s cubic-bezier(.2,.8,.2,1)}
|
|
235
|
+
#items,#cancel{overflow:hidden;background:var(--pp-surface,Canvas);border:1px solid var(--pp-border,#8884);border-radius:14px;box-shadow:0 8px 28px #0004}
|
|
236
|
+
#items{max-height:calc(100vh - 116px - env(safe-area-inset-bottom));overflow-y:auto;-webkit-overflow-scrolling:touch}
|
|
237
|
+
#items button,#cancel{display:block;width:100%;min-height:54px;padding:14px 18px;text-align:center}
|
|
238
|
+
#items button+button{border-top:1px solid var(--pp-border,#8884)}
|
|
239
|
+
#items button[data-danger]{color:var(--pp-danger,#e45656)}
|
|
240
|
+
#cancel{margin-top:8px;color:var(--pp-accent,Highlight);font-weight:600}
|
|
241
|
+
@keyframes pp-fade-in{from{opacity:0}}
|
|
242
|
+
@keyframes pp-sheet-in{from{transform:translateY(100%)}}
|
|
243
|
+
@media (prefers-reduced-motion:reduce){#backdrop,#sheet{animation:none}}
|
|
244
|
+
</style><button id="backdrop" type="button" tabindex="-1" aria-label="关闭菜单"></button><section id="sheet" role="dialog" aria-modal="true" aria-label="更多操作"><div id="items" role="menu"></div><button id="cancel" type="button">取消</button></section>`;
|
|
245
|
+
this.#items = layerRoot.querySelector("#items");
|
|
246
|
+
this.#button.onclick = () => (this.#layer.hidden ? this.open() : this.close());
|
|
247
|
+
layerRoot.querySelector("#backdrop").onclick = () => {
|
|
248
|
+
this.close();
|
|
249
|
+
this.#button.focus();
|
|
268
250
|
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
const index = items.indexOf(root.activeElement);
|
|
251
|
+
layerRoot.querySelector("#cancel").onclick = () => {
|
|
252
|
+
this.close();
|
|
253
|
+
this.#button.focus();
|
|
254
|
+
};
|
|
255
|
+
this.#items.onkeydown = event => {
|
|
256
|
+
const items = [...this.#items.children];
|
|
257
|
+
const index = items.indexOf(layerRoot.activeElement);
|
|
277
258
|
const offsets = { ArrowDown: 1, ArrowUp: -1 };
|
|
278
259
|
if (event.key in offsets) {
|
|
279
260
|
event.preventDefault();
|
|
280
261
|
items[(index + offsets[event.key] + items.length) % items.length].focus();
|
|
281
262
|
}
|
|
282
263
|
};
|
|
264
|
+
document.body.append(this.#layer);
|
|
283
265
|
document.addEventListener("keydown", this.#key);
|
|
284
266
|
this.update([]);
|
|
285
267
|
}
|
|
@@ -293,8 +275,9 @@ class ActionMenu {
|
|
|
293
275
|
*/
|
|
294
276
|
update(items, disabled = false) {
|
|
295
277
|
this.close();
|
|
296
|
-
this.#
|
|
297
|
-
this.#
|
|
278
|
+
this.#disabled = disabled || items.length === 0;
|
|
279
|
+
this.#button.disabled = this.#disabled;
|
|
280
|
+
this.#items.replaceChildren(
|
|
298
281
|
...items.map(item => {
|
|
299
282
|
const button = this.#document.createElement("button");
|
|
300
283
|
button.type = "button";
|
|
@@ -310,13 +293,30 @@ class ActionMenu {
|
|
|
310
293
|
);
|
|
311
294
|
}
|
|
312
295
|
|
|
296
|
+
/**
|
|
297
|
+
* 打开当前操作菜单。
|
|
298
|
+
* Open the current action sheet.
|
|
299
|
+
* @returns {void} 无返回值 / No return value.
|
|
300
|
+
*/
|
|
301
|
+
open() {
|
|
302
|
+
if (this.#disabled) return;
|
|
303
|
+
const style = getComputedStyle(this.element);
|
|
304
|
+
for (const property of ["--pp-text", "--pp-surface", "--pp-border", "--pp-accent", "--pp-danger"]) {
|
|
305
|
+
const value = style.getPropertyValue(property);
|
|
306
|
+
if (value) this.#layer.style.setProperty(property, value);
|
|
307
|
+
}
|
|
308
|
+
this.#layer.hidden = false;
|
|
309
|
+
this.#button.setAttribute("aria-expanded", "true");
|
|
310
|
+
this.#items.firstElementChild.focus();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
313
|
/**
|
|
314
314
|
* 关闭菜单。
|
|
315
315
|
* Close the menu.
|
|
316
316
|
* @returns {void} 无返回值 / No return value.
|
|
317
317
|
*/
|
|
318
318
|
close() {
|
|
319
|
-
this.#
|
|
319
|
+
this.#layer.hidden = true;
|
|
320
320
|
this.#button.setAttribute("aria-expanded", "false");
|
|
321
321
|
}
|
|
322
322
|
|
|
@@ -327,6 +327,7 @@ class ActionMenu {
|
|
|
327
327
|
*/
|
|
328
328
|
destroy() {
|
|
329
329
|
this.#document.removeEventListener("keydown", this.#key);
|
|
330
|
+
this.#layer.remove();
|
|
330
331
|
this.element.remove();
|
|
331
332
|
}
|
|
332
333
|
}
|
|
@@ -334,14 +335,15 @@ class ActionMenu {
|
|
|
334
335
|
/**
|
|
335
336
|
* 将 BoxJS 数组、app 或订阅转换为模块字段,保留原文件为唯一字段来源。
|
|
336
337
|
* Normalize a BoxJS array, app or subscription using the source JSON as the field authority.
|
|
337
|
-
* @param {unknown} config BoxJS JSON / BoxJS document.
|
|
338
|
+
* @param {unknown | BoxJS} config BoxJS JSON 或已解析目录 / BoxJS document or parsed catalog.
|
|
338
339
|
* @param {string} module API 第一段模块名 / First API path segment.
|
|
339
340
|
* @returns {import("../index.js").ModuleDefinition} 存储根和字段 / Storage root and fields.
|
|
340
341
|
* @throws {TypeError} 配置结构、字段路径、默认值或展示属性无效 / Invalid configuration, field path, default or presentation attribute.
|
|
341
342
|
*/
|
|
342
343
|
function normalizeBoxJs(config, module) {
|
|
343
344
|
validatePathParts([module]);
|
|
344
|
-
const
|
|
345
|
+
const catalog = config instanceof BoxJS ? config : new BoxJS(config);
|
|
346
|
+
const target = catalog.modules.get(module);
|
|
345
347
|
if (!target) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
346
348
|
const { entries, storageKey, metadata } = target;
|
|
347
349
|
const fields = [];
|
|
@@ -574,7 +576,7 @@ function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bi
|
|
|
574
576
|
const state = { controller: new AbortController(), definition: null, values: {}, saving: false };
|
|
575
577
|
sessions.set(module, state);
|
|
576
578
|
try {
|
|
577
|
-
const definition = normalizeBoxJs(catalog
|
|
579
|
+
const definition = normalizeBoxJs(catalog, module);
|
|
578
580
|
const response = await send(`@${definition.storageKey}.${definition.settingsPath.join(".")}`, "get", undefined, state.controller.signal);
|
|
579
581
|
let subtree = response.status === 404 ? {} : await response.json();
|
|
580
582
|
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
@@ -838,17 +840,10 @@ function mountPanel(root, catalog) {
|
|
|
838
840
|
const menu = new ActionMenu(id => runAction(id));
|
|
839
841
|
const trailing = element("span", "pp-nav-spacer");
|
|
840
842
|
trailing.append(menu.element);
|
|
841
|
-
const logo = element("span", "pp-module-logo");
|
|
842
|
-
logo.setAttribute("aria-hidden", "true");
|
|
843
|
-
const image = icon(catalog.module.metadata, "");
|
|
844
|
-
if (image) logo.append(image);
|
|
845
|
-
const toolbar = element("div", "pp-toolbar");
|
|
846
|
-
toolbar.setAttribute("role", "search");
|
|
847
|
-
toolbar.hidden = true;
|
|
848
843
|
const viewport = element("div", "pp-viewport");
|
|
849
844
|
let toast;
|
|
850
845
|
header.append(back, heading, trailing);
|
|
851
|
-
shell.append(header,
|
|
846
|
+
shell.append(header, viewport);
|
|
852
847
|
root.append(shell);
|
|
853
848
|
// 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
|
|
854
849
|
// Embedded mode publishes navigation state without host reads or mutations of the module DOM.
|
|
@@ -939,7 +934,6 @@ function mountPanel(root, catalog) {
|
|
|
939
934
|
async function open(module) {
|
|
940
935
|
const version = ++generation;
|
|
941
936
|
active = module;
|
|
942
|
-
toolbar.hidden = true;
|
|
943
937
|
back.disabled = window.history.length <= 1;
|
|
944
938
|
heading.textContent = module;
|
|
945
939
|
publishNavigation();
|
|
@@ -962,15 +956,6 @@ function mountPanel(root, catalog) {
|
|
|
962
956
|
const { definition, values } = client.snapshot(active);
|
|
963
957
|
heading.textContent = definition.metadata?.name || active;
|
|
964
958
|
const view = element("section", "pp-fields");
|
|
965
|
-
view.append(logo);
|
|
966
|
-
const search = element("input", "");
|
|
967
|
-
search.type = "search";
|
|
968
|
-
search.placeholder = "搜索设置项";
|
|
969
|
-
search.setAttribute("aria-label", "搜索设置");
|
|
970
|
-
const searchField = fieldControl(search);
|
|
971
|
-
searchField.classList.add("pp-search");
|
|
972
|
-
toolbar.replaceChildren(searchField);
|
|
973
|
-
const searchRows = [];
|
|
974
959
|
/**
|
|
975
960
|
* 挂载后执行的多行高度更新
|
|
976
961
|
* Textarea sizing callbacks run after mounting.
|
|
@@ -989,7 +974,6 @@ function mountPanel(root, catalog) {
|
|
|
989
974
|
*/
|
|
990
975
|
const updateNavigation = () => {
|
|
991
976
|
const editor = editors.get(navigation.current);
|
|
992
|
-
toolbar.hidden = Boolean(navigation.current);
|
|
993
977
|
heading.textContent = editor?.title ?? definition.metadata?.name ?? active;
|
|
994
978
|
back.disabled = saving || !navigation.canGoBack;
|
|
995
979
|
publishNavigation();
|
|
@@ -1214,18 +1198,7 @@ function mountPanel(root, catalog) {
|
|
|
1214
1198
|
});
|
|
1215
1199
|
if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
|
|
1216
1200
|
groups.get(group).append(row);
|
|
1217
|
-
searchRows.push({ row, text: [field.name, field.key, field.description, ...(field.options ?? []).map(option => option.label)].join(" ").toLocaleLowerCase() });
|
|
1218
1201
|
}
|
|
1219
|
-
const empty = element("p", "pp-description", "没有匹配的设置项");
|
|
1220
|
-
empty.hidden = true;
|
|
1221
|
-
empty.setAttribute("role", "status");
|
|
1222
|
-
view.append(empty);
|
|
1223
|
-
search.oninput = () => {
|
|
1224
|
-
const words = search.value.trim().toLocaleLowerCase().split(/\s+/);
|
|
1225
|
-
for (const { row, text } of searchRows) row.hidden = !words.every(word => text.includes(word));
|
|
1226
|
-
for (const rows of groups.values()) rows.parentElement.hidden = [...rows.children].every(row => row.hidden);
|
|
1227
|
-
empty.hidden = searchRows.some(({ row }) => !row.hidden);
|
|
1228
|
-
};
|
|
1229
1202
|
const cachePage = element("section", "pp-cache-page");
|
|
1230
1203
|
const output = element("pre", "pp-cache");
|
|
1231
1204
|
output.textContent = "暂无缓存";
|
|
@@ -1310,7 +1283,7 @@ function mountPanel(root, catalog) {
|
|
|
1310
1283
|
*/
|
|
1311
1284
|
function mount(boxjs, css = "") {
|
|
1312
1285
|
if (typeof css !== "string") throw new TypeError("CSS must be a string");
|
|
1313
|
-
const catalog = new BoxJS(boxjs);
|
|
1286
|
+
const catalog = boxjs instanceof BoxJS ? boxjs : new BoxJS(boxjs);
|
|
1314
1287
|
const metadata = catalog.module.metadata;
|
|
1315
1288
|
const image = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
|
|
1316
1289
|
if (image) resourceURL(image);
|
|
@@ -1412,9 +1385,9 @@ async function start() {
|
|
|
1412
1385
|
});
|
|
1413
1386
|
const [data, style] = await Promise.all(resources.map(url => (url ? fetch(url, { cache: "no-store", credentials: "omit" }) : null)));
|
|
1414
1387
|
if (data.status !== 200 || (style && style.status !== 200)) throw new Error(`HTTP ${data.status !== 200 ? data.status : style.status}`);
|
|
1415
|
-
const
|
|
1416
|
-
if (
|
|
1417
|
-
view = mount(
|
|
1388
|
+
const catalog = new BoxJS(await data.json());
|
|
1389
|
+
if (catalog.module.module !== inputs.module) throw new Error("Imported JSON does not match the module URL");
|
|
1390
|
+
view = mount(catalog, style ? await style.text() : "");
|
|
1418
1391
|
} catch (error) {
|
|
1419
1392
|
document.querySelector("#preferences").replaceChildren(errorView(error, start));
|
|
1420
1393
|
}
|
package/dist/module/index.html
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Shared
|
|
2
|
+
* 共用三点按钮和底部操作菜单;弹层挂载到文档根部,不受标题栏显示状态影响。
|
|
3
|
+
* Shared overflow trigger and bottom action sheet; the layer is mounted at document level and remains independent of header visibility.
|
|
4
4
|
*/
|
|
5
5
|
class ActionMenu {
|
|
6
6
|
#button;
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
#layer;
|
|
8
|
+
#items;
|
|
9
9
|
#select;
|
|
10
10
|
#document;
|
|
11
|
+
#disabled = true;
|
|
11
12
|
#key = event => {
|
|
12
|
-
if (event.key === "Escape" && !this.#
|
|
13
|
+
if (event.key === "Escape" && !this.#layer.hidden) {
|
|
13
14
|
event.preventDefault();
|
|
14
15
|
this.close();
|
|
15
16
|
this.#button.focus();
|
|
@@ -25,44 +26,55 @@ class ActionMenu {
|
|
|
25
26
|
this.#document = document;
|
|
26
27
|
this.#select = select;
|
|
27
28
|
this.element = document.createElement("span");
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
:host{display:inline-flex;
|
|
31
|
-
:host([hidden])
|
|
32
|
-
button{font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
29
|
+
const triggerRoot = this.element.attachShadow({ mode: "open" });
|
|
30
|
+
triggerRoot.innerHTML = `<style>
|
|
31
|
+
:host{display:inline-flex;width:44px;height:44px;color:inherit}
|
|
32
|
+
:host([hidden]){display:none!important}
|
|
33
|
+
button{width:44px;height:44px;padding:10px;font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
33
34
|
button:disabled{opacity:.4;cursor:default}
|
|
34
35
|
button:focus-visible{outline:2px solid currentColor;outline-offset:-3px}
|
|
35
|
-
#trigger{width:44px;height:44px;padding:10px;position:relative;z-index:3}
|
|
36
36
|
svg{display:block;width:24px;height:24px;fill:currentColor}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
</style><button type="button" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="4" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="20" cy="12" r="2"/></svg></button>`;
|
|
38
|
+
this.#button = triggerRoot.querySelector("button");
|
|
39
|
+
this.#layer = document.createElement("span");
|
|
40
|
+
const layerRoot = this.#layer.attachShadow({ mode: "open" });
|
|
41
|
+
layerRoot.innerHTML = `<style>
|
|
42
|
+
:host{position:fixed;inset:0;z-index:2147483647;color:var(--pp-text,CanvasText);font:16px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
43
|
+
:host([hidden]){display:none!important}
|
|
44
|
+
button{font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
45
|
+
button:focus-visible{outline:2px solid var(--pp-accent,Highlight);outline-offset:-3px}
|
|
46
|
+
#backdrop{position:absolute;inset:0;width:100%;height:100%;padding:0;background:#0008;animation:pp-fade-in .18s ease-out}
|
|
47
|
+
#sheet{position:absolute;z-index:1;left:0;right:0;bottom:0;width:min(100%,540px);max-height:calc(100% - 24px);margin:auto;padding:8px 8px calc(8px + env(safe-area-inset-bottom));animation:pp-sheet-in .22s cubic-bezier(.2,.8,.2,1)}
|
|
48
|
+
#items,#cancel{overflow:hidden;background:var(--pp-surface,Canvas);border:1px solid var(--pp-border,#8884);border-radius:14px;box-shadow:0 8px 28px #0004}
|
|
49
|
+
#items{max-height:calc(100vh - 116px - env(safe-area-inset-bottom));overflow-y:auto;-webkit-overflow-scrolling:touch}
|
|
50
|
+
#items button,#cancel{display:block;width:100%;min-height:54px;padding:14px 18px;text-align:center}
|
|
51
|
+
#items button+button{border-top:1px solid var(--pp-border,#8884)}
|
|
52
|
+
#items button[data-danger]{color:var(--pp-danger,#e45656)}
|
|
53
|
+
#cancel{margin-top:8px;color:var(--pp-accent,Highlight);font-weight:600}
|
|
54
|
+
@keyframes pp-fade-in{from{opacity:0}}
|
|
55
|
+
@keyframes pp-sheet-in{from{transform:translateY(100%)}}
|
|
56
|
+
@media (prefers-reduced-motion:reduce){#backdrop,#sheet{animation:none}}
|
|
57
|
+
</style><button id="backdrop" type="button" tabindex="-1" aria-label="关闭菜单"></button><section id="sheet" role="dialog" aria-modal="true" aria-label="更多操作"><div id="items" role="menu"></div><button id="cancel" type="button">取消</button></section>`;
|
|
58
|
+
this.#items = layerRoot.querySelector("#items");
|
|
59
|
+
this.#button.onclick = () => (this.#layer.hidden ? this.open() : this.close());
|
|
60
|
+
layerRoot.querySelector("#backdrop").onclick = () => {
|
|
61
|
+
this.close();
|
|
62
|
+
this.#button.focus();
|
|
51
63
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
const index = items.indexOf(root.activeElement);
|
|
64
|
+
layerRoot.querySelector("#cancel").onclick = () => {
|
|
65
|
+
this.close();
|
|
66
|
+
this.#button.focus();
|
|
67
|
+
};
|
|
68
|
+
this.#items.onkeydown = event => {
|
|
69
|
+
const items = [...this.#items.children];
|
|
70
|
+
const index = items.indexOf(layerRoot.activeElement);
|
|
60
71
|
const offsets = { ArrowDown: 1, ArrowUp: -1 };
|
|
61
72
|
if (event.key in offsets) {
|
|
62
73
|
event.preventDefault();
|
|
63
74
|
items[(index + offsets[event.key] + items.length) % items.length].focus();
|
|
64
75
|
}
|
|
65
76
|
};
|
|
77
|
+
document.body.append(this.#layer);
|
|
66
78
|
document.addEventListener("keydown", this.#key);
|
|
67
79
|
this.update([]);
|
|
68
80
|
}
|
|
@@ -76,8 +88,9 @@ class ActionMenu {
|
|
|
76
88
|
*/
|
|
77
89
|
update(items, disabled = false) {
|
|
78
90
|
this.close();
|
|
79
|
-
this.#
|
|
80
|
-
this.#
|
|
91
|
+
this.#disabled = disabled || items.length === 0;
|
|
92
|
+
this.#button.disabled = this.#disabled;
|
|
93
|
+
this.#items.replaceChildren(
|
|
81
94
|
...items.map(item => {
|
|
82
95
|
const button = this.#document.createElement("button");
|
|
83
96
|
button.type = "button";
|
|
@@ -93,13 +106,30 @@ class ActionMenu {
|
|
|
93
106
|
);
|
|
94
107
|
}
|
|
95
108
|
|
|
109
|
+
/**
|
|
110
|
+
* 打开当前操作菜单。
|
|
111
|
+
* Open the current action sheet.
|
|
112
|
+
* @returns {void} 无返回值 / No return value.
|
|
113
|
+
*/
|
|
114
|
+
open() {
|
|
115
|
+
if (this.#disabled) return;
|
|
116
|
+
const style = getComputedStyle(this.element);
|
|
117
|
+
for (const property of ["--pp-text", "--pp-surface", "--pp-border", "--pp-accent", "--pp-danger"]) {
|
|
118
|
+
const value = style.getPropertyValue(property);
|
|
119
|
+
if (value) this.#layer.style.setProperty(property, value);
|
|
120
|
+
}
|
|
121
|
+
this.#layer.hidden = false;
|
|
122
|
+
this.#button.setAttribute("aria-expanded", "true");
|
|
123
|
+
this.#items.firstElementChild.focus();
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
/**
|
|
97
127
|
* 关闭菜单。
|
|
98
128
|
* Close the menu.
|
|
99
129
|
* @returns {void} 无返回值 / No return value.
|
|
100
130
|
*/
|
|
101
131
|
close() {
|
|
102
|
-
this.#
|
|
132
|
+
this.#layer.hidden = true;
|
|
103
133
|
this.#button.setAttribute("aria-expanded", "false");
|
|
104
134
|
}
|
|
105
135
|
|
|
@@ -110,6 +140,7 @@ class ActionMenu {
|
|
|
110
140
|
*/
|
|
111
141
|
destroy() {
|
|
112
142
|
this.#document.removeEventListener("keydown", this.#key);
|
|
143
|
+
this.#layer.remove();
|
|
113
144
|
this.element.remove();
|
|
114
145
|
}
|
|
115
146
|
}
|