@nsnanocat/preference-panes 0.8.1 → 0.9.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.
@@ -125,10 +125,30 @@ function pageInputs(url, headers = {}) {
125
125
  function element(tag, className, text) {
126
126
  const node = document.createElement(tag);
127
127
  node.className = className;
128
+ // 官方 AppSettings 1.1.2 的作用域标记与原版 CSS 一起固定版本。
129
+ // Pin official AppSettings 1.1.2 scope attributes together with its unmodified CSS.
130
+ if (/\bform-row(?:\b|__)/.test(className)) node.setAttribute("data-v-b69aa1ea", "");
131
+ if (/\bform-group(?:\b|__)/.test(className)) node.setAttribute("data-v-e590be47", "");
128
132
  if (text !== undefined) node.textContent = text;
129
133
  return node;
130
134
  }
131
135
 
136
+ /**
137
+ * 搜索、选择和文本控件共用官方 VField 的 DOM 结构。
138
+ * Share the official VField DOM structure across search, select and text controls.
139
+ * @param {HTMLElement} control 已创建的原生控件 / Existing native control.
140
+ * @param {boolean} [multiline] 是否为多行输入 / Whether the control is multiline.
141
+ * @returns {HTMLDivElement} 字段容器 / Field container.
142
+ */
143
+ function fieldControl(control, multiline = false) {
144
+ const field = element("div", `v-field pp-editor${multiline ? " v-field--textarea" : ""}`);
145
+ const body = element("div", "v-field__body");
146
+ control.classList.add("v-field__control");
147
+ body.append(control);
148
+ field.append(body);
149
+ return field;
150
+ }
151
+
132
152
  /**
133
153
  * 元数据地址只允许 HTTP(S) 和相对地址。
134
154
  * Allow only HTTP(S) and relative metadata addresses.
@@ -173,7 +193,25 @@ function errorView(error, retry) {
173
193
  return view;
174
194
  }
175
195
 
176
- 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";
196
+ /**
197
+ * 请求宿主确认;独立网页使用浏览器对话框。
198
+ * Request confirmation from the host, using the browser dialog for standalone pages.
199
+ * @param {Window} host 模块窗口 / Module window.
200
+ * @param {string} message 确认内容 / Confirmation message.
201
+ * @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
202
+ */
203
+ function requestConfirmation(host, message) {
204
+ return new Promise((resolve, reject) => {
205
+ const frame = host.frameElement;
206
+ if (frame) {
207
+ const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
208
+ if (!frame.dispatchEvent(event)) return;
209
+ }
210
+ resolve(host.confirm(message));
211
+ });
212
+ }
213
+
214
+ var defaults = "@import url(\"https://hilo.bilibili.com/h5_common/theme.min.css\") layer(preference-panes);\n@import url(\"https://hilo.bilibili.com/h5_common/b-style.min.css\") layer(preference-panes);\n@import url(\"https://hilo.bilibili.com/app_settings/assets/messageSettingsLayout-ltzQ1gMi.css\") layer(preference-panes);\n@import url(\"https://hilo.bilibili.com/app_settings/assets/message-settings-BD3N1lqQ.css\") layer(preference-panes);\n@layer preference-panes {\n/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。\n * Grouped rows follow the Bilibili settings layout, scoped to the panel. */\n.pp-panel {\n --pp-text: var(--text1);\n --pp-background: var(--bg2);\n --pp-surface: var(--bg1);\n --pp-border: var(--line_regular);\n --pp-muted: var(--text3);\n --pp-accent: var(--brand_pink);\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:not(.v-toggle) {\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) + 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}\n.pp-search {\n width: 100%;\n margin-bottom: 12px;\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-panel .v-toggle {\n border: 0;\n padding: 0;\n flex: none;\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\n}";
177
215
 
178
216
  /**
179
217
  * 标题栏共用三点菜单;Shadow DOM 隔离项目样式,保留继承的主题色。
@@ -795,7 +833,7 @@ function mountPanel(root, catalog) {
795
833
  { id: "clearCaches", label: "清空缓存", destructive: true },
796
834
  { id: "reset", label: "重置模块", destructive: true },
797
835
  ];
798
- const menu = new ActionMenu(id => handlers.get(id)());
836
+ const menu = new ActionMenu(id => runAction(id));
799
837
  const trailing = element("span", "pp-nav-spacer");
800
838
  trailing.append(menu.element);
801
839
  const brand = element("div", "pp-brand");
@@ -825,7 +863,7 @@ function mountPanel(root, catalog) {
825
863
  );
826
864
  };
827
865
  const onAction = event => {
828
- if (!saving && handlers.has(event.detail)) handlers.get(event.detail)();
866
+ if (!saving && handlers.has(event.detail)) runAction(event.detail);
829
867
  };
830
868
  window.frameElement?.addEventListener("preferencepanes:action", onAction);
831
869
  let timer,
@@ -867,6 +905,19 @@ function mountPanel(root, catalog) {
867
905
  }, 2400);
868
906
  };
869
907
  const client = createPreferencesClient({ catalog, notify });
908
+ /**
909
+ * 两种菜单入口共用异步错误处理,包含宿主确认框错误。
910
+ * Share async error handling between both menus, including host-dialog errors.
911
+ * @param {string} id 操作标识 / Action identifier.
912
+ * @returns {Promise<void>} 操作已处理 / Action handled.
913
+ */
914
+ async function runAction(id) {
915
+ try {
916
+ await handlers.get(id)();
917
+ } catch (error) {
918
+ notify({ kind: "error", message: error.message });
919
+ }
920
+ }
870
921
  /**
871
922
  * 打开模块并忽略已过期的异步结果。
872
923
  * Open a module and ignore stale asynchronous results.
@@ -898,6 +949,14 @@ function mountPanel(root, catalog) {
898
949
  const { definition, values } = client.snapshot(active);
899
950
  heading.textContent = definition.metadata?.name || active;
900
951
  const view = element("section", "pp-fields");
952
+ const search = element("input", "");
953
+ search.type = "search";
954
+ search.placeholder = "搜索设置项";
955
+ search.setAttribute("aria-label", "搜索设置");
956
+ const searchField = fieldControl(search);
957
+ searchField.classList.add("pp-search");
958
+ view.append(searchField);
959
+ const searchRows = [];
901
960
  /**
902
961
  * 挂载后执行的多行高度更新
903
962
  * Textarea sizing callbacks run after mounting.
@@ -972,7 +1031,7 @@ function mountPanel(root, catalog) {
972
1031
  const match = /^\[([^\]]+)\]\s*(.*)$/.exec(field.name);
973
1032
  const group = match?.[1] ?? "通用";
974
1033
  if (!groups.has(group)) {
975
- const section = element("section", "form-group");
1034
+ const section = element("section", "form-group form-group--has-title");
976
1035
  const rows = element("div", "form-group__row");
977
1036
  section.append(element("h2", "form-group__title", group), rows);
978
1037
  groups.set(group, rows);
@@ -1000,7 +1059,7 @@ function mountPanel(root, catalog) {
1000
1059
  let eventName = "change";
1001
1060
  switch (true) {
1002
1061
  case Boolean(field.options) && field.type !== "array": {
1003
- const select = element("select", "pp-input");
1062
+ const select = element("select", "");
1004
1063
  select.setAttribute("aria-label", field.name);
1005
1064
  field.options.forEach((option, index) => {
1006
1065
  const item = element("option", "", option.label);
@@ -1010,7 +1069,7 @@ function mountPanel(root, catalog) {
1010
1069
  write = value => {
1011
1070
  select.selectedIndex = field.options.findIndex(option => option.key === value);
1012
1071
  };
1013
- row.append(select);
1072
+ row.append(fieldControl(select));
1014
1073
  read = () => field.options[select.selectedIndex]?.key;
1015
1074
  break;
1016
1075
  }
@@ -1056,9 +1115,27 @@ function mountPanel(root, catalog) {
1056
1115
  };
1057
1116
  break;
1058
1117
  }
1118
+ case field.type === "boolean": {
1119
+ const toggle = element("button", "v-toggle v-toggle--small form-row__toggle");
1120
+ toggle.type = "button";
1121
+ toggle.setAttribute("role", "switch");
1122
+ toggle.setAttribute("aria-label", field.name);
1123
+ toggle.append(element("span", "v-toggle__circle"));
1124
+ write = value => {
1125
+ toggle.setAttribute("aria-checked", String(value === true));
1126
+ toggle.classList.toggle("v-toggle--closed", value !== true);
1127
+ };
1128
+ read = () => toggle.getAttribute("aria-checked") === "true";
1129
+ toggle.onclick = () => {
1130
+ write(!read());
1131
+ toggle.dispatchEvent(new window.Event("change", { bubbles: true }));
1132
+ };
1133
+ row.append(toggle);
1134
+ break;
1135
+ }
1059
1136
  default: {
1060
1137
  const multiline = field.control === "textarea" || field.type === "array";
1061
- const input = element(multiline ? "textarea" : "input", "pp-input");
1138
+ const input = element(multiline ? "textarea" : "input", "");
1062
1139
  if (multiline) row.classList.add("pp-multiline");
1063
1140
  input.setAttribute("aria-label", field.name);
1064
1141
  if (field.placeholder) input.placeholder = field.placeholder;
@@ -1080,33 +1157,23 @@ function mountPanel(root, catalog) {
1080
1157
  input.addEventListener("input", grow);
1081
1158
  growingInputs.push(grow);
1082
1159
  }
1083
- if (field.type === "boolean") {
1084
- input.type = "checkbox";
1085
- input.classList.add("pp-switch");
1086
- input.setAttribute("role", "switch");
1087
- write = value => {
1088
- input.checked = value === true;
1089
- };
1090
- read = () => input.checked;
1091
- } else {
1092
- eventName = "input";
1093
- if (!multiline) input.type = field.type === "number" ? "number" : "text";
1094
- write = value => {
1095
- input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
1096
- grow();
1097
- };
1098
- read = () => {
1099
- switch (field.type) {
1100
- case "array":
1101
- return JSON.parse(input.value);
1102
- case "number":
1103
- return input.value === "" ? Number.NaN : Number(input.value);
1104
- default:
1105
- return input.value;
1106
- }
1107
- };
1108
- }
1109
- row.append(input);
1160
+ eventName = "input";
1161
+ if (!multiline) input.type = field.type === "number" ? "number" : "text";
1162
+ write = value => {
1163
+ input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
1164
+ grow();
1165
+ };
1166
+ read = () => {
1167
+ switch (field.type) {
1168
+ case "array":
1169
+ return JSON.parse(input.value);
1170
+ case "number":
1171
+ return input.value === "" ? Number.NaN : Number(input.value);
1172
+ default:
1173
+ return input.value;
1174
+ }
1175
+ };
1176
+ row.append(fieldControl(input, multiline));
1110
1177
  break;
1111
1178
  }
1112
1179
  }
@@ -1136,7 +1203,18 @@ function mountPanel(root, catalog) {
1136
1203
  });
1137
1204
  if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
1138
1205
  groups.get(group).append(row);
1206
+ searchRows.push({ row, text: [field.name, field.key, field.description, ...(field.options ?? []).map(option => option.label)].join(" ").toLocaleLowerCase() });
1139
1207
  }
1208
+ const empty = element("p", "pp-description", "没有匹配的设置项");
1209
+ empty.hidden = true;
1210
+ empty.setAttribute("role", "status");
1211
+ view.append(empty);
1212
+ search.oninput = () => {
1213
+ const words = search.value.trim().toLocaleLowerCase().split(/\s+/);
1214
+ for (const { row, text } of searchRows) row.hidden = !words.every(word => text.includes(word));
1215
+ for (const rows of groups.values()) rows.parentElement.hidden = [...rows.children].every(row => row.hidden);
1216
+ empty.hidden = searchRows.some(({ row }) => !row.hidden);
1217
+ };
1140
1218
  const cachePage = element("section", "pp-cache-page");
1141
1219
  const output = element("pre", "pp-cache");
1142
1220
  output.textContent = "暂无缓存";
@@ -1161,9 +1239,9 @@ function mountPanel(root, catalog) {
1161
1239
  },
1162
1240
  );
1163
1241
  });
1164
- handlers.set("clearCaches", () => {
1242
+ handlers.set("clearCaches", async () => {
1165
1243
  if (saving) return;
1166
- if (!window.confirm(`清空 ${active} 的全部 Caches?`)) return;
1244
+ if (!(await requestConfirmation(window, `清空 ${active} 的全部 Caches?`)) || destroyed || saving) return;
1167
1245
  return perform(
1168
1246
  () => client.clearCaches(active),
1169
1247
  () => {
@@ -1171,9 +1249,9 @@ function mountPanel(root, catalog) {
1171
1249
  },
1172
1250
  );
1173
1251
  });
1174
- handlers.set("reset", () => {
1252
+ handlers.set("reset", async () => {
1175
1253
  if (saving) return;
1176
- if (!window.confirm(`重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) return;
1254
+ if (!(await requestConfirmation(window, `重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) || destroyed || saving) return;
1177
1255
  return perform(() => client.reset(active), controls);
1178
1256
  });
1179
1257
  navigation?.destroy();
@@ -1213,8 +1291,8 @@ function mountPanel(root, catalog) {
1213
1291
  }
1214
1292
 
1215
1293
  /**
1216
- * 只挂载导入 JSON 对应的模块设置页,默认样式内置,CSS 仅用于该页。
1217
- * Mount only the imported module's settings page with built-in defaults and optional page CSS.
1294
+ * 挂载模块设置页;内置资源只引用官方地址,CSS 输入仅用于该页。
1295
+ * Mount the module page with official resource URLs and optional page-specific CSS.
1218
1296
  * @param {import("../index.js").BoxJSInput} boxjs 单个模块的 BoxJS JSON / BoxJS JSON for one module.
1219
1297
  * @param {string} [css] 可选 CSS 正文 / Optional CSS text.
1220
1298
  * @returns {import("./index.js").MountedPreferences} 模块生命周期句柄 / Module lifecycle handle.
@@ -1239,8 +1317,28 @@ function mount(boxjs, css = "") {
1239
1317
  document.head.append(base, custom);
1240
1318
  const previousTitle = document.title;
1241
1319
  const previousTheme = document.documentElement.dataset.theme;
1242
- const theme = navigator.userAgent.match(/themeId\/(\d+)/)?.[1];
1243
- if (theme) document.documentElement.dataset.theme = theme === "2" ? "dark" : "light";
1320
+ const previousDark = document.documentElement.classList.contains("bili_dark");
1321
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
1322
+ const previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
1323
+ const host = window.frameElement?.ownerDocument.documentElement;
1324
+ /**
1325
+ * 跟随嵌入宿主的通用环境状态,不识别业务 App 或解析其 UA。
1326
+ * Follow generic host appearance without detecting a business app or parsing its user agent.
1327
+ * @returns {void} 已同步主题与键盘避让 / Theme and keyboard clearance synchronized.
1328
+ */
1329
+ const syncAppearance = () => {
1330
+ const theme = host?.dataset.theme ?? previousTheme ?? (systemTheme.matches ? "dark" : "light");
1331
+ document.documentElement.dataset.theme = theme;
1332
+ document.documentElement.classList.toggle("bili_dark", theme === "dark");
1333
+ if (host) document.documentElement.style.setProperty("--pp-keyboard-height", host.style.getPropertyValue("--pp-keyboard-height"));
1334
+ };
1335
+ let observer;
1336
+ syncAppearance();
1337
+ systemTheme.addEventListener("change", syncAppearance);
1338
+ if (host) {
1339
+ observer = new MutationObserver(syncAppearance);
1340
+ observer.observe(host, { attributes: true, attributeFilter: ["data-theme", "style"] });
1341
+ }
1244
1342
  document.title = metadata.name ?? catalog.module.module;
1245
1343
  let panel;
1246
1344
  const view = {
@@ -1250,6 +1348,9 @@ function mount(boxjs, css = "") {
1250
1348
  * @returns {void} 无返回值 / No return value.
1251
1349
  */
1252
1350
  destroy() {
1351
+ observer?.disconnect();
1352
+ systemTheme.removeEventListener("change", syncAppearance);
1353
+ document.documentElement.classList.toggle("bili_dark", previousDark);
1253
1354
  panel?.destroy();
1254
1355
  base.remove();
1255
1356
  custom.remove();
@@ -1258,6 +1359,7 @@ function mount(boxjs, css = "") {
1258
1359
  document.title = previousTitle;
1259
1360
  if (previousTheme === undefined) delete document.documentElement.dataset.theme;
1260
1361
  else document.documentElement.dataset.theme = previousTheme;
1362
+ document.documentElement.style.setProperty("--pp-keyboard-height", previousKeyboard);
1261
1363
  },
1262
1364
  };
1263
1365
  try {
@@ -9,6 +9,6 @@
9
9
  </head>
10
10
  <body>
11
11
  <main id="preferences"></main>
12
- <script type="module" src="/settings/assets/app.mjs?v=0.8.1"></script>
12
+ <script type="module" src="/settings/assets/app.mjs?v=0.9.1"></script>
13
13
  </body>
14
14
  </html>
@@ -146,6 +146,10 @@ class ModuleFrame extends EventTarget {
146
146
  this.#state = { ...event.detail, actions: event.detail.actions ?? [] };
147
147
  this.dispatchEvent(new Event("change"));
148
148
  };
149
+ #confirmation = event => {
150
+ const request = new CustomEvent("confirm", { cancelable: true, detail: event.detail });
151
+ if (!this.dispatchEvent(request)) event.preventDefault();
152
+ };
149
153
 
150
154
  /**
151
155
  * 建立 iframe 与请求输入;调用方挂载 element 后调用 load。
@@ -162,6 +166,7 @@ class ModuleFrame extends EventTarget {
162
166
  this.element.title = `${inputs.module} 设置`;
163
167
  this.element.dataset.preferencePanes = JSON.stringify(inputs);
164
168
  this.element.addEventListener("preferencepanes:change", this.#change);
169
+ this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
165
170
  this.#state = { title: inputs.module, module: inputs.module, busy: false, canGoBack: true, actions: [] };
166
171
  options.signal?.addEventListener("abort", this.#abort, { once: true });
167
172
  }
@@ -222,6 +227,7 @@ class ModuleFrame extends EventTarget {
222
227
  this.#controller.abort();
223
228
  this.#options.signal?.removeEventListener("abort", this.#abort);
224
229
  this.element.removeEventListener("preferencepanes:change", this.#change);
230
+ this.element.removeEventListener("preferencepanes:confirm", this.#confirmation);
225
231
  }
226
232
  }
227
233