@nsnanocat/preference-panes 0.8.0 → 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 +4 -0
- package/dist/api.js +1 -1
- package/dist/module/app.mjs +147 -22
- package/dist/module/index.html +1 -1
- package/dist/module/navigation.mjs +217 -3
- package/dist/preference-panes.mjs +147 -22
- package/package.json +1 -1
- 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/panel.css +2 -15
- package/src/browser/panel.mjs +31 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var defaults = "/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。\n * Grouped rows follow the Bilibili settings layout, scoped to the panel. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-border: #e3e5e7;\n --pp-muted: #9499a0;\n --pp-accent: #fb7299;\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 min-height: 100vh;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\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: sticky;\n top: 0;\n z-index: 1;\n}\n.pp-title {\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-brand {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n text-align: center;\n}\n.pp-brand-icon {\n display: none;\n flex: none;\n width: 28px;\n height: 28px;\n}\n.pp-brand-icon:not(:empty) {\n display: block;\n}\n.pp-brand-icon img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\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 height: calc(100vh - 52px - env(safe-area-inset-top));\n overflow: hidden;\n}\n:root[data-preference-panes-embedded] .pp-header {\n display: none;\n}\n:root[data-preference-panes-embedded] .pp-viewport {\n height: 100vh;\n}\n@supports (height: 100dvh) {\n .pp-viewport {\n height: calc(100dvh - 52px - env(safe-area-inset-top));\n }\n :root[data-preference-panes-embedded] .pp-viewport {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page {\n position: absolute;\n inset: 0;\n overflow: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));\n background: var(--pp-background);\n}\n.pp-panel .form-group {\n margin: 0 0 12px;\n}\n.pp-panel .form-group__title {\n font-size: 12px;\n line-height: 17px;\n font-weight: 400;\n color: var(--pp-muted);\n padding-left: 12px;\n margin: 12px 0 6px;\n}\n.pp-panel .form-group__row {\n border-radius: 8px;\n overflow: hidden;\n background: var(--pp-surface);\n}\n.pp-panel .form-row {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 46px;\n padding: 12px;\n border: 0;\n border-bottom: 1px solid var(--pp-border);\n background: var(--pp-surface);\n gap: 12px;\n}\n.pp-panel .form-row:last-child {\n border-bottom: 0;\n}\n.pp-panel .form-row__text {\n flex: 1;\n min-width: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n}\n.pp-panel .form-row__title {\n font-size: 15px;\n line-height: 22px;\n color: var(--pp-text);\n text-align: left;\n}\n.pp-panel .form-row__subtitle {\n font-size: 12px;\n line-height: 18px;\n color: var(--pp-muted);\n overflow-wrap: anywhere;\n margin-top: 2px;\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-input {\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-surface);\n border: 1px solid var(--pp-border);\n border-radius: 6px;\n padding: 8px;\n min-width: 0;\n max-width: 45%;\n width: 45%;\n}\nselect.pp-input {\n text-overflow: ellipsis;\n font-size: 13px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-input {\n max-width: 100%;\n width: 100%;\n margin-top: 10px;\n}\n.pp-switch {\n appearance: none;\n -webkit-appearance: none;\n position: relative;\n flex: none;\n width: 32px;\n height: 20px;\n max-width: none;\n border: 0;\n border-radius: 15px;\n padding: 0;\n background: #c9ccd0;\n cursor: pointer;\n transition: background 0.2s;\n}\n.pp-switch::before {\n content: \"\";\n position: absolute;\n top: 3px;\n left: 3px;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: white;\n transition: transform 0.2s;\n}\n.pp-switch:checked {\n background: var(--pp-accent);\n}\n.pp-switch:checked::before {\n transform: translateX(12px);\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-icon {\n width: 48px;\n height: 48px;\n object-fit: contain;\n flex: none;\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-
|
|
1
|
+
var defaults = "/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。\n * Grouped rows follow the Bilibili settings layout, scoped to the panel. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-border: #e3e5e7;\n --pp-muted: #9499a0;\n --pp-accent: #fb7299;\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 min-height: 100vh;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\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: sticky;\n top: 0;\n z-index: 1;\n}\n.pp-title {\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-brand {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n text-align: center;\n}\n.pp-brand-icon {\n display: none;\n flex: none;\n width: 28px;\n height: 28px;\n}\n.pp-brand-icon:not(:empty) {\n display: block;\n}\n.pp-brand-icon img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\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 height: calc(100vh - 52px - env(safe-area-inset-top));\n overflow: hidden;\n}\n:root[data-preference-panes-embedded] .pp-header {\n display: none;\n}\n:root[data-preference-panes-embedded] .pp-viewport {\n height: 100vh;\n}\n@supports (height: 100dvh) {\n .pp-viewport {\n height: calc(100dvh - 52px - env(safe-area-inset-top));\n }\n :root[data-preference-panes-embedded] .pp-viewport {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n overflow: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));\n background: var(--pp-background);\n}\n.pp-panel .form-group {\n margin: 0 0 12px;\n}\n.pp-panel .form-group__title {\n font-size: 12px;\n line-height: 17px;\n font-weight: 400;\n color: var(--pp-muted);\n padding-left: 12px;\n margin: 12px 0 6px;\n}\n.pp-panel .form-group__row {\n border-radius: 8px;\n overflow: hidden;\n background: var(--pp-surface);\n}\n.pp-panel .form-row {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 46px;\n padding: 12px;\n border: 0;\n border-bottom: 1px solid var(--pp-border);\n background: var(--pp-surface);\n gap: 12px;\n}\n.pp-panel .form-row:last-child {\n border-bottom: 0;\n}\n.pp-panel .form-row__text {\n flex: 1;\n min-width: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n}\n.pp-panel .form-row__title {\n font-size: 15px;\n line-height: 22px;\n color: var(--pp-text);\n text-align: left;\n}\n.pp-panel .form-row__subtitle {\n font-size: 12px;\n line-height: 18px;\n color: var(--pp-muted);\n overflow-wrap: anywhere;\n margin-top: 2px;\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-input {\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-surface);\n border: 1px solid var(--pp-border);\n border-radius: 6px;\n padding: 8px;\n min-width: 0;\n max-width: 45%;\n width: 45%;\n}\nselect.pp-input {\n text-overflow: ellipsis;\n font-size: 13px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-input {\n max-width: 100%;\n width: 100%;\n margin-top: 10px;\n}\n.pp-switch {\n appearance: none;\n -webkit-appearance: none;\n position: relative;\n flex: none;\n width: 32px;\n height: 20px;\n max-width: none;\n border: 0;\n border-radius: 15px;\n padding: 0;\n background: #c9ccd0;\n cursor: pointer;\n transition: background 0.2s;\n}\n.pp-switch::before {\n content: \"\";\n position: absolute;\n top: 3px;\n left: 3px;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: white;\n transition: transform 0.2s;\n}\n.pp-switch:checked {\n background: var(--pp-accent);\n}\n.pp-switch:checked::before {\n transform: translateX(12px);\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-icon {\n width: 48px;\n height: 48px;\n object-fit: contain;\n flex: none;\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@media (prefers-color-scheme: dark) {\n .pp-panel {\n --pp-text: #e3e5e7;\n --pp-background: #17181a;\n --pp-surface: #232427;\n --pp-border: #343538;\n }\n}\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #e3e5e7;\n --pp-background: #17181a;\n --pp-surface: #232427;\n --pp-border: #343538;\n}\n:root[data-theme=\"light\"] .pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-border: #e3e5e7;\n}\n@media (prefers-reduced-motion: reduce) {\n .pp-panel .pp-switch,\n .pp-panel .pp-switch::before {\n transition: none;\n }\n}\n";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 校验原始路径片段,不进行 URL 编码转换。
|
|
@@ -157,6 +157,122 @@ function errorView(error, retry) {
|
|
|
157
157
|
return view;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* 标题栏共用三点菜单;Shadow DOM 隔离项目样式,保留继承的主题色。
|
|
162
|
+
* Shared title-bar overflow menu; Shadow DOM isolates layout while inheriting theme colors.
|
|
163
|
+
*/
|
|
164
|
+
class ActionMenu {
|
|
165
|
+
#button;
|
|
166
|
+
#popup;
|
|
167
|
+
#backdrop;
|
|
168
|
+
#select;
|
|
169
|
+
#document;
|
|
170
|
+
#key = event => {
|
|
171
|
+
if (event.key === "Escape" && !this.#popup.hidden) {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
this.close();
|
|
174
|
+
this.#button.focus();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 创建菜单,操作逻辑由调用方提供。
|
|
180
|
+
* Create a menu whose actions are handled by the caller.
|
|
181
|
+
* @param {(id: string) => void} select 菜单选择回调 / Selection callback.
|
|
182
|
+
*/
|
|
183
|
+
constructor(select) {
|
|
184
|
+
this.#document = document;
|
|
185
|
+
this.#select = select;
|
|
186
|
+
this.element = document.createElement("span");
|
|
187
|
+
const root = this.element.attachShadow({ mode: "open" });
|
|
188
|
+
root.innerHTML = `<style>
|
|
189
|
+
:host{display:inline-flex;position:relative;width:44px;height:44px;color:inherit}
|
|
190
|
+
:host([hidden]),[hidden]{display:none!important}
|
|
191
|
+
button{font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
192
|
+
button:disabled{opacity:.4;cursor:default}
|
|
193
|
+
button:focus-visible{outline:2px solid currentColor;outline-offset:-3px}
|
|
194
|
+
#trigger{width:44px;height:44px;padding:10px;position:relative;z-index:3}
|
|
195
|
+
svg{display:block;width:24px;height:24px;fill:currentColor}
|
|
196
|
+
#backdrop{position:fixed;inset:0;z-index:1}
|
|
197
|
+
#items{position:absolute;right:0;top:46px;z-index:2;min-width:160px;padding:6px;background:var(--pp-surface,Canvas);color:var(--pp-text,CanvasText);border:1px solid var(--pp-border,#8884);border-radius:12px;box-shadow:0 8px 28px #0003}
|
|
198
|
+
#items button{display:block;text-align:left;white-space:nowrap;width:100%;padding:11px 14px;border-radius:8px;font:14px/1.4 system-ui,sans-serif}
|
|
199
|
+
#items button:hover{background:#8882}
|
|
200
|
+
#items button[data-danger]{color:#e45656}
|
|
201
|
+
</style><button id="trigger" type="button" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false" aria-controls="items"><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><button id="backdrop" type="button" tabindex="-1" aria-label="关闭菜单" hidden></button><div id="items" role="menu" hidden></div>`;
|
|
202
|
+
this.#button = root.querySelector("#trigger");
|
|
203
|
+
this.#popup = root.querySelector("#items");
|
|
204
|
+
this.#backdrop = root.querySelector("#backdrop");
|
|
205
|
+
this.#button.onclick = () => {
|
|
206
|
+
const open = this.#popup.hidden;
|
|
207
|
+
this.#popup.hidden = this.#backdrop.hidden = !open;
|
|
208
|
+
this.#button.setAttribute("aria-expanded", String(open));
|
|
209
|
+
if (open) this.#popup.firstElementChild.focus();
|
|
210
|
+
};
|
|
211
|
+
this.#backdrop.onclick = () => this.close();
|
|
212
|
+
this.#popup.onkeydown = event => {
|
|
213
|
+
if (event.key === "Tab") {
|
|
214
|
+
this.close();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const items = [...this.#popup.children];
|
|
218
|
+
const index = items.indexOf(root.activeElement);
|
|
219
|
+
const offsets = { ArrowDown: 1, ArrowUp: -1 };
|
|
220
|
+
if (event.key in offsets) {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
items[(index + offsets[event.key] + items.length) % items.length].focus();
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
document.addEventListener("keydown", this.#key);
|
|
226
|
+
this.update([]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 同步可用操作和忙碌状态,不重建菜单触发按钮。
|
|
231
|
+
* Update actions and busy state without replacing the trigger button.
|
|
232
|
+
* @param {Array<{id: string, label: string, destructive?: boolean}>} items 操作列表 / Actions.
|
|
233
|
+
* @param {boolean} [disabled] 是否忙碌 / Whether operations are busy.
|
|
234
|
+
* @returns {void} 无返回值 / No return value.
|
|
235
|
+
*/
|
|
236
|
+
update(items, disabled = false) {
|
|
237
|
+
this.close();
|
|
238
|
+
this.#button.disabled = disabled || items.length === 0;
|
|
239
|
+
this.#popup.replaceChildren(
|
|
240
|
+
...items.map(item => {
|
|
241
|
+
const button = this.#document.createElement("button");
|
|
242
|
+
button.type = "button";
|
|
243
|
+
button.setAttribute("role", "menuitem");
|
|
244
|
+
button.textContent = item.label;
|
|
245
|
+
button.toggleAttribute("data-danger", Boolean(item.destructive));
|
|
246
|
+
button.onclick = () => {
|
|
247
|
+
this.close();
|
|
248
|
+
this.#select(item.id);
|
|
249
|
+
};
|
|
250
|
+
return button;
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 关闭菜单。
|
|
257
|
+
* Close the menu.
|
|
258
|
+
* @returns {void} 无返回值 / No return value.
|
|
259
|
+
*/
|
|
260
|
+
close() {
|
|
261
|
+
this.#popup.hidden = this.#backdrop.hidden = true;
|
|
262
|
+
this.#button.setAttribute("aria-expanded", "false");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 移除监听器与节点。
|
|
267
|
+
* Remove listeners and elements.
|
|
268
|
+
* @returns {void} 无返回值 / No return value.
|
|
269
|
+
*/
|
|
270
|
+
destroy() {
|
|
271
|
+
this.#document.removeEventListener("keydown", this.#key);
|
|
272
|
+
this.element.remove();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
160
276
|
/**
|
|
161
277
|
* 将 BoxJS 数组、app 或订阅转换为模块字段,保留原文件为唯一字段来源。
|
|
162
278
|
* Normalize a BoxJS array, app or subscription using the source JSON as the field authority.
|
|
@@ -655,6 +771,15 @@ function mountPanel(root, catalog) {
|
|
|
655
771
|
back.setAttribute("aria-label", "返回");
|
|
656
772
|
back.type = "button";
|
|
657
773
|
const heading = element("h1", "pp-title", title);
|
|
774
|
+
const handlers = new Map();
|
|
775
|
+
const menuItems = [
|
|
776
|
+
{ id: "viewCaches", label: "查看缓存" },
|
|
777
|
+
{ id: "clearCaches", label: "清空缓存", destructive: true },
|
|
778
|
+
{ id: "reset", label: "重置模块", destructive: true },
|
|
779
|
+
];
|
|
780
|
+
const menu = new ActionMenu(id => handlers.get(id)());
|
|
781
|
+
const trailing = element("span", "pp-nav-spacer");
|
|
782
|
+
trailing.append(menu.element);
|
|
658
783
|
const brand = element("div", "pp-brand");
|
|
659
784
|
const logo = element("span", "pp-brand-icon");
|
|
660
785
|
logo.setAttribute("aria-hidden", "true");
|
|
@@ -665,20 +790,26 @@ function mountPanel(root, catalog) {
|
|
|
665
790
|
const toast = element("div", "pp-toast");
|
|
666
791
|
toast.setAttribute("role", "status");
|
|
667
792
|
toast.hidden = true;
|
|
668
|
-
header.append(back, brand,
|
|
793
|
+
header.append(back, brand, trailing);
|
|
669
794
|
shell.append(header, viewport, toast);
|
|
670
795
|
root.append(shell);
|
|
671
796
|
// 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
|
|
672
797
|
// Embedded mode publishes navigation state without host reads or mutations of the module DOM.
|
|
673
798
|
const publishNavigation = () => {
|
|
799
|
+
const actions = handlers.size ? menuItems : [];
|
|
800
|
+
menu.update(actions, saving);
|
|
674
801
|
const frame = window.frameElement;
|
|
675
802
|
if (!frame?.dataset.preferencePanes) return;
|
|
676
803
|
frame.dispatchEvent(
|
|
677
804
|
new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
|
|
678
|
-
detail: { title: heading.textContent, module: catalog.module.module, busy: saving, canGoBack: !back.disabled },
|
|
805
|
+
detail: { title: heading.textContent, module: catalog.module.module, busy: saving, canGoBack: !back.disabled, actions },
|
|
679
806
|
}),
|
|
680
807
|
);
|
|
681
808
|
};
|
|
809
|
+
const onAction = event => {
|
|
810
|
+
if (!saving && handlers.has(event.detail)) handlers.get(event.detail)();
|
|
811
|
+
};
|
|
812
|
+
window.frameElement?.addEventListener("preferencepanes:action", onAction);
|
|
682
813
|
let timer,
|
|
683
814
|
navigation,
|
|
684
815
|
generation = 0,
|
|
@@ -988,17 +1119,13 @@ function mountPanel(root, catalog) {
|
|
|
988
1119
|
if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
|
|
989
1120
|
groups.get(group).append(row);
|
|
990
1121
|
}
|
|
991
|
-
const
|
|
992
|
-
maintenance.append(element("h2", "pp-title", "模块数据"));
|
|
993
|
-
const actions = element("div", "pp-actions");
|
|
994
|
-
const cacheView = element("button", "", "查看 Caches");
|
|
995
|
-
const cacheClear = element("button", "", "清空 Caches");
|
|
996
|
-
const reset = element("button", "pp-danger", "重置模块");
|
|
1122
|
+
const cachePage = element("section", "pp-cache-page");
|
|
997
1123
|
const output = element("pre", "pp-cache");
|
|
998
|
-
output.
|
|
1124
|
+
output.textContent = "暂无缓存";
|
|
999
1125
|
output.setAttribute("aria-label", "Caches 内容");
|
|
1000
|
-
|
|
1001
|
-
|
|
1126
|
+
cachePage.append(output);
|
|
1127
|
+
editors.set("$caches", { node: cachePage, title: "缓存" });
|
|
1128
|
+
handlers.set("viewCaches", () => {
|
|
1002
1129
|
if (saving) return;
|
|
1003
1130
|
let value;
|
|
1004
1131
|
return perform(
|
|
@@ -1012,12 +1139,11 @@ function mountPanel(root, catalog) {
|
|
|
1012
1139
|
},
|
|
1013
1140
|
() => {
|
|
1014
1141
|
output.textContent = value === undefined ? "暂无缓存" : JSON.stringify(value, null, 2);
|
|
1015
|
-
|
|
1016
|
-
cacheView.textContent = "刷新 Caches";
|
|
1142
|
+
navigation.open("$caches");
|
|
1017
1143
|
},
|
|
1018
1144
|
);
|
|
1019
|
-
};
|
|
1020
|
-
|
|
1145
|
+
});
|
|
1146
|
+
handlers.set("clearCaches", () => {
|
|
1021
1147
|
if (saving) return;
|
|
1022
1148
|
if (!window.confirm(`清空 ${active} 的全部 Caches?`)) return;
|
|
1023
1149
|
return perform(
|
|
@@ -1026,15 +1152,12 @@ function mountPanel(root, catalog) {
|
|
|
1026
1152
|
output.textContent = "暂无缓存";
|
|
1027
1153
|
},
|
|
1028
1154
|
);
|
|
1029
|
-
};
|
|
1030
|
-
reset
|
|
1155
|
+
});
|
|
1156
|
+
handlers.set("reset", () => {
|
|
1031
1157
|
if (saving) return;
|
|
1032
1158
|
if (!window.confirm(`重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) return;
|
|
1033
1159
|
return perform(() => client.reset(active), controls);
|
|
1034
|
-
};
|
|
1035
|
-
actions.append(cacheView, cacheClear, reset);
|
|
1036
|
-
maintenance.append(actions, output);
|
|
1037
|
-
view.append(maintenance);
|
|
1160
|
+
});
|
|
1038
1161
|
navigation?.destroy();
|
|
1039
1162
|
navigation = new Navigation(viewport, view, key => editors.get(key)?.node);
|
|
1040
1163
|
navigation.addEventListener("change", updateNavigation);
|
|
@@ -1060,6 +1183,8 @@ function mountPanel(root, catalog) {
|
|
|
1060
1183
|
*/
|
|
1061
1184
|
destroy() {
|
|
1062
1185
|
destroyed = true;
|
|
1186
|
+
menu.destroy();
|
|
1187
|
+
window.frameElement?.removeEventListener("preferencepanes:action", onAction);
|
|
1063
1188
|
navigation?.destroy();
|
|
1064
1189
|
generation++;
|
|
1065
1190
|
if (active && !saving) client.leave(active);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nsnanocat/preference-panes",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 标题栏共用三点菜单;Shadow DOM 隔离项目样式,保留继承的主题色。
|
|
3
|
+
* Shared title-bar overflow menu; Shadow DOM isolates layout while inheriting theme colors.
|
|
4
|
+
*/
|
|
5
|
+
export class ActionMenu {
|
|
6
|
+
#button;
|
|
7
|
+
#popup;
|
|
8
|
+
#backdrop;
|
|
9
|
+
#select;
|
|
10
|
+
#document;
|
|
11
|
+
#key = event => {
|
|
12
|
+
if (event.key === "Escape" && !this.#popup.hidden) {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
this.close();
|
|
15
|
+
this.#button.focus();
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 创建菜单,操作逻辑由调用方提供。
|
|
21
|
+
* Create a menu whose actions are handled by the caller.
|
|
22
|
+
* @param {(id: string) => void} select 菜单选择回调 / Selection callback.
|
|
23
|
+
*/
|
|
24
|
+
constructor(select) {
|
|
25
|
+
this.#document = document;
|
|
26
|
+
this.#select = select;
|
|
27
|
+
this.element = document.createElement("span");
|
|
28
|
+
const root = this.element.attachShadow({ mode: "open" });
|
|
29
|
+
root.innerHTML = `<style>
|
|
30
|
+
:host{display:inline-flex;position:relative;width:44px;height:44px;color:inherit}
|
|
31
|
+
:host([hidden]),[hidden]{display:none!important}
|
|
32
|
+
button{font:inherit;cursor:pointer;border:0;color:inherit;background:none}
|
|
33
|
+
button:disabled{opacity:.4;cursor:default}
|
|
34
|
+
button:focus-visible{outline:2px solid currentColor;outline-offset:-3px}
|
|
35
|
+
#trigger{width:44px;height:44px;padding:10px;position:relative;z-index:3}
|
|
36
|
+
svg{display:block;width:24px;height:24px;fill:currentColor}
|
|
37
|
+
#backdrop{position:fixed;inset:0;z-index:1}
|
|
38
|
+
#items{position:absolute;right:0;top:46px;z-index:2;min-width:160px;padding:6px;background:var(--pp-surface,Canvas);color:var(--pp-text,CanvasText);border:1px solid var(--pp-border,#8884);border-radius:12px;box-shadow:0 8px 28px #0003}
|
|
39
|
+
#items button{display:block;text-align:left;white-space:nowrap;width:100%;padding:11px 14px;border-radius:8px;font:14px/1.4 system-ui,sans-serif}
|
|
40
|
+
#items button:hover{background:#8882}
|
|
41
|
+
#items button[data-danger]{color:#e45656}
|
|
42
|
+
</style><button id="trigger" type="button" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false" aria-controls="items"><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><button id="backdrop" type="button" tabindex="-1" aria-label="关闭菜单" hidden></button><div id="items" role="menu" hidden></div>`;
|
|
43
|
+
this.#button = root.querySelector("#trigger");
|
|
44
|
+
this.#popup = root.querySelector("#items");
|
|
45
|
+
this.#backdrop = root.querySelector("#backdrop");
|
|
46
|
+
this.#button.onclick = () => {
|
|
47
|
+
const open = this.#popup.hidden;
|
|
48
|
+
this.#popup.hidden = this.#backdrop.hidden = !open;
|
|
49
|
+
this.#button.setAttribute("aria-expanded", String(open));
|
|
50
|
+
if (open) this.#popup.firstElementChild.focus();
|
|
51
|
+
};
|
|
52
|
+
this.#backdrop.onclick = () => this.close();
|
|
53
|
+
this.#popup.onkeydown = event => {
|
|
54
|
+
if (event.key === "Tab") {
|
|
55
|
+
this.close();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const items = [...this.#popup.children];
|
|
59
|
+
const index = items.indexOf(root.activeElement);
|
|
60
|
+
const offsets = { ArrowDown: 1, ArrowUp: -1 };
|
|
61
|
+
if (event.key in offsets) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
items[(index + offsets[event.key] + items.length) % items.length].focus();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener("keydown", this.#key);
|
|
67
|
+
this.update([]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 同步可用操作和忙碌状态,不重建菜单触发按钮。
|
|
72
|
+
* Update actions and busy state without replacing the trigger button.
|
|
73
|
+
* @param {Array<{id: string, label: string, destructive?: boolean}>} items 操作列表 / Actions.
|
|
74
|
+
* @param {boolean} [disabled] 是否忙碌 / Whether operations are busy.
|
|
75
|
+
* @returns {void} 无返回值 / No return value.
|
|
76
|
+
*/
|
|
77
|
+
update(items, disabled = false) {
|
|
78
|
+
this.close();
|
|
79
|
+
this.#button.disabled = disabled || items.length === 0;
|
|
80
|
+
this.#popup.replaceChildren(
|
|
81
|
+
...items.map(item => {
|
|
82
|
+
const button = this.#document.createElement("button");
|
|
83
|
+
button.type = "button";
|
|
84
|
+
button.setAttribute("role", "menuitem");
|
|
85
|
+
button.textContent = item.label;
|
|
86
|
+
button.toggleAttribute("data-danger", Boolean(item.destructive));
|
|
87
|
+
button.onclick = () => {
|
|
88
|
+
this.close();
|
|
89
|
+
this.#select(item.id);
|
|
90
|
+
};
|
|
91
|
+
return button;
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 关闭菜单。
|
|
98
|
+
* Close the menu.
|
|
99
|
+
* @returns {void} 无返回值 / No return value.
|
|
100
|
+
*/
|
|
101
|
+
close() {
|
|
102
|
+
this.#popup.hidden = this.#backdrop.hidden = true;
|
|
103
|
+
this.#button.setAttribute("aria-expanded", "false");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 移除监听器与节点。
|
|
108
|
+
* Remove listeners and elements.
|
|
109
|
+
* @returns {void} 无返回值 / No return value.
|
|
110
|
+
*/
|
|
111
|
+
destroy() {
|
|
112
|
+
this.#document.removeEventListener("keydown", this.#key);
|
|
113
|
+
this.element.remove();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -11,7 +11,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
11
11
|
#abort = () => this.destroy();
|
|
12
12
|
#state;
|
|
13
13
|
#change = event => {
|
|
14
|
-
this.#state = event.detail;
|
|
14
|
+
this.#state = { ...event.detail, actions: event.detail.actions ?? [] };
|
|
15
15
|
this.dispatchEvent(new Event("change"));
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -30,7 +30,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
30
30
|
this.element.title = `${inputs.module} 设置`;
|
|
31
31
|
this.element.dataset.preferencePanes = JSON.stringify(inputs);
|
|
32
32
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
33
|
-
this.#state = { title: inputs.module, module: inputs.module, busy: false, canGoBack: true };
|
|
33
|
+
this.#state = { title: inputs.module, module: inputs.module, busy: false, canGoBack: true, actions: [] };
|
|
34
34
|
options.signal?.addEventListener("abort", this.#abort, { once: true });
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -70,6 +70,17 @@ export class ModuleFrame extends EventTarget {
|
|
|
70
70
|
if (!this.#state.busy && this.#state.canGoBack) this.element.contentWindow.history.back();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 向模块发送菜单操作,不让宿主访问内部 DOM 或存储客户端。
|
|
75
|
+
* Dispatch a menu action without host access to internal DOM or the storage client.
|
|
76
|
+
* @param {string} id 当前可用操作 / Available action identifier.
|
|
77
|
+
* @returns {void} 无返回值 / No return value.
|
|
78
|
+
*/
|
|
79
|
+
perform(id) {
|
|
80
|
+
if (this.#state.busy || !this.#state.actions.some(action => action.id === id)) throw new Error("Action is not available");
|
|
81
|
+
this.element.dispatchEvent(new CustomEvent("preferencepanes:action", { detail: id }));
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
/**
|
|
74
85
|
* 取消加载与事件订阅;节点保留到 Navigation 的退出动画结束。
|
|
75
86
|
* Cancel loading and subscriptions; Navigation retains the node until its exit animation ends.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块入口的固定状态行,只通过 HEAD 探测安装状态和业务版本。
|
|
3
|
+
* Fixed module status row, probing installation and business version with HEAD only.
|
|
4
|
+
*/
|
|
5
|
+
export class ModuleStatus extends EventTarget {
|
|
6
|
+
#element;
|
|
7
|
+
#controller;
|
|
8
|
+
#state = { status: "checking", version: null };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 绑定调用方提供的状态行。
|
|
12
|
+
* Bind a caller-owned status row.
|
|
13
|
+
* @param {HTMLElement} element 状态文字容器 / Status text container.
|
|
14
|
+
*/
|
|
15
|
+
constructor(element) {
|
|
16
|
+
super();
|
|
17
|
+
this.#element = element;
|
|
18
|
+
this.#render("checking");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 当前安装状态与业务版本。
|
|
23
|
+
* Current installation state and business version.
|
|
24
|
+
*/
|
|
25
|
+
get state() {
|
|
26
|
+
return { ...this.#state };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 每次进入重新探测,取消旧请求并忽略其迟到结果。
|
|
31
|
+
* Reprobe on entry, cancelling old requests and ignoring late results.
|
|
32
|
+
* @param {string | URL} url 配置 Mock 地址 / Configuration Mock URL.
|
|
33
|
+
* @returns {Promise<void>} 探测完成 / Probe completion.
|
|
34
|
+
*/
|
|
35
|
+
async check(url) {
|
|
36
|
+
this.#controller?.abort();
|
|
37
|
+
const controller = (this.#controller = new AbortController());
|
|
38
|
+
this.#render("checking");
|
|
39
|
+
const timer = setTimeout(() => controller.abort(), 3500);
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(url, { method: "HEAD", cache: "no-store", credentials: "omit", signal: controller.signal });
|
|
42
|
+
if (controller !== this.#controller) return;
|
|
43
|
+
const version = response.headers.get("X-PreferencePanes-Version")?.trim() || null;
|
|
44
|
+
this.#render(response.status === 200 ? "installed" : "missing", version);
|
|
45
|
+
} catch {
|
|
46
|
+
if (controller === this.#controller) this.#render("missing");
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 更新状态标签,缺少版本时不伪造版本号。
|
|
54
|
+
* Render the label without inventing a missing version.
|
|
55
|
+
* @param {"checking" | "installed" | "missing"} status 状态 / State.
|
|
56
|
+
* @param {string | null} [version] 业务版本 / Business version.
|
|
57
|
+
* @returns {void} 无返回值 / No return value.
|
|
58
|
+
*/
|
|
59
|
+
#render(status, version = null) {
|
|
60
|
+
this.#state = { status, version: status === "installed" ? version : null };
|
|
61
|
+
switch (status) {
|
|
62
|
+
case "checking":
|
|
63
|
+
this.#element.textContent = "检测中";
|
|
64
|
+
break;
|
|
65
|
+
case "installed":
|
|
66
|
+
this.#element.textContent = version ?? "版本未知";
|
|
67
|
+
break;
|
|
68
|
+
case "missing":
|
|
69
|
+
this.#element.textContent = "未安装";
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
this.#element.dataset.state = status;
|
|
73
|
+
this.#element.title = this.#element.textContent;
|
|
74
|
+
this.dispatchEvent(new Event("change"));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 释放尚未完成的探测。
|
|
79
|
+
* Release pending probes.
|
|
80
|
+
* @returns {void} 无返回值 / No return value.
|
|
81
|
+
*/
|
|
82
|
+
destroy() {
|
|
83
|
+
this.#controller?.abort();
|
|
84
|
+
this.#controller = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -19,7 +19,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
19
19
|
* change 事件对应的导航状态。
|
|
20
20
|
* Navigation state exposed with change events.
|
|
21
21
|
*/
|
|
22
|
-
readonly state: { title: string; module: string; busy: boolean; canGoBack: boolean };
|
|
22
|
+
readonly state: { title: string; module: string; busy: boolean; canGoBack: boolean; actions: MenuAction[] };
|
|
23
23
|
/**
|
|
24
24
|
* 获取原始 HTML。
|
|
25
25
|
* Fetch unmodified HTML.
|
|
@@ -32,6 +32,13 @@ export class ModuleFrame extends EventTarget {
|
|
|
32
32
|
* @returns 无返回值 / No return value.
|
|
33
33
|
*/
|
|
34
34
|
back(): void;
|
|
35
|
+
/**
|
|
36
|
+
* 执行模块提供的菜单操作。
|
|
37
|
+
* Perform a module-provided menu action.
|
|
38
|
+
* @param id 操作标识 / Action identifier.
|
|
39
|
+
* @returns 无返回值 / No return value.
|
|
40
|
+
*/
|
|
41
|
+
perform(id: string): void;
|
|
35
42
|
/**
|
|
36
43
|
* 取消请求与事件订阅。
|
|
37
44
|
* Cancel requests and subscriptions.
|
|
@@ -40,6 +47,97 @@ export class ModuleFrame extends EventTarget {
|
|
|
40
47
|
destroy(): void;
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
/**
|
|
51
|
+
* 菜单操作描述。
|
|
52
|
+
* Menu action descriptor.
|
|
53
|
+
*/
|
|
54
|
+
export interface MenuAction {
|
|
55
|
+
/**
|
|
56
|
+
* 操作标识。
|
|
57
|
+
* Action identifier.
|
|
58
|
+
*/
|
|
59
|
+
id: string;
|
|
60
|
+
/**
|
|
61
|
+
* 显示文字。
|
|
62
|
+
* Display text.
|
|
63
|
+
*/
|
|
64
|
+
label: string;
|
|
65
|
+
/**
|
|
66
|
+
* 危险操作样式。
|
|
67
|
+
* Destructive action style.
|
|
68
|
+
*/
|
|
69
|
+
destructive?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 共用三点菜单。
|
|
74
|
+
* Shared overflow menu.
|
|
75
|
+
*/
|
|
76
|
+
export class ActionMenu {
|
|
77
|
+
/**
|
|
78
|
+
* 创建菜单。
|
|
79
|
+
* Create a menu.
|
|
80
|
+
* @param select 选择回调 / Selection callback.
|
|
81
|
+
*/
|
|
82
|
+
constructor(select: (id: string) => void);
|
|
83
|
+
/**
|
|
84
|
+
* 菜单节点。
|
|
85
|
+
* Menu element.
|
|
86
|
+
*/
|
|
87
|
+
readonly element: HTMLElement;
|
|
88
|
+
/**
|
|
89
|
+
* 更新操作列表。
|
|
90
|
+
* Update available actions.
|
|
91
|
+
* @param items 操作列表 / Actions.
|
|
92
|
+
* @param disabled 是否忙碌 / Busy state.
|
|
93
|
+
* @returns 无返回值 / No return value.
|
|
94
|
+
*/
|
|
95
|
+
update(items: MenuAction[], disabled?: boolean): void;
|
|
96
|
+
/**
|
|
97
|
+
* 关闭菜单。
|
|
98
|
+
* Close the menu.
|
|
99
|
+
* @returns 无返回值 / No return value.
|
|
100
|
+
*/
|
|
101
|
+
close(): void;
|
|
102
|
+
/**
|
|
103
|
+
* 释放组件。
|
|
104
|
+
* Release the component.
|
|
105
|
+
* @returns 无返回值 / No return value.
|
|
106
|
+
*/
|
|
107
|
+
destroy(): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* HEAD 探测的固定模块状态行。
|
|
112
|
+
* Fixed module status row backed by HEAD probes.
|
|
113
|
+
*/
|
|
114
|
+
export class ModuleStatus extends EventTarget {
|
|
115
|
+
/**
|
|
116
|
+
* 绑定状态行。
|
|
117
|
+
* Bind a status row.
|
|
118
|
+
* @param element 状态行节点 / Status row node.
|
|
119
|
+
*/
|
|
120
|
+
constructor(element: HTMLElement);
|
|
121
|
+
/**
|
|
122
|
+
* 当前状态及模块版本。
|
|
123
|
+
* Current state and module version.
|
|
124
|
+
*/
|
|
125
|
+
readonly state: { status: "checking" | "installed" | "missing"; version: string | null };
|
|
126
|
+
/**
|
|
127
|
+
* 探测配置 Mock。
|
|
128
|
+
* Probe a configuration Mock.
|
|
129
|
+
* @param url 配置地址 / Configuration URL.
|
|
130
|
+
* @returns 探测完成 / Probe completion.
|
|
131
|
+
*/
|
|
132
|
+
check(url: string | URL): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* 取消探测。
|
|
135
|
+
* Cancel probes.
|
|
136
|
+
* @returns 无返回值 / No return value.
|
|
137
|
+
*/
|
|
138
|
+
destroy(): void;
|
|
139
|
+
}
|
|
140
|
+
|
|
43
141
|
/**
|
|
44
142
|
* 同一文档的根页/子页导航,不定义页面布局或模块业务。
|
|
45
143
|
* Home/detail navigation within a document, without layout or module business rules.
|