@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.
@@ -1,12 +1,12 @@
1
1
  /* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。
2
2
  * Grouped rows follow the Bilibili settings layout, scoped to the panel. */
3
3
  .pp-panel {
4
- --pp-text: #18191c;
5
- --pp-background: #f6f7f8;
6
- --pp-surface: #fff;
7
- --pp-border: #e3e5e7;
8
- --pp-muted: #9499a0;
9
- --pp-accent: #fb7299;
4
+ --pp-text: var(--text1);
5
+ --pp-background: var(--bg2);
6
+ --pp-surface: var(--bg1);
7
+ --pp-border: var(--line_regular);
8
+ --pp-muted: var(--text3);
9
+ --pp-accent: var(--brand_pink);
10
10
  font:
11
11
  15px / 1.5 -apple-system,
12
12
  BlinkMacSystemFont,
@@ -67,7 +67,7 @@
67
67
  width: 44px;
68
68
  flex: none;
69
69
  }
70
- .pp-panel button {
70
+ .pp-panel button:not(.v-toggle) {
71
71
  font: inherit;
72
72
  cursor: pointer;
73
73
  border: 0;
@@ -111,60 +111,10 @@
111
111
  position: absolute;
112
112
  inset: 0;
113
113
  overflow: auto;
114
- padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));
114
+ padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));
115
+ scroll-padding-bottom: var(--pp-keyboard-height, 0px);
115
116
  background: var(--pp-background);
116
117
  }
117
- .pp-panel .form-group {
118
- margin: 0 0 12px;
119
- }
120
- .pp-panel .form-group__title {
121
- font-size: 12px;
122
- line-height: 17px;
123
- font-weight: 400;
124
- color: var(--pp-muted);
125
- padding-left: 12px;
126
- margin: 12px 0 6px;
127
- }
128
- .pp-panel .form-group__row {
129
- border-radius: 8px;
130
- overflow: hidden;
131
- background: var(--pp-surface);
132
- }
133
- .pp-panel .form-row {
134
- position: relative;
135
- display: flex;
136
- align-items: center;
137
- width: 100%;
138
- min-height: 46px;
139
- padding: 12px;
140
- border: 0;
141
- border-bottom: 1px solid var(--pp-border);
142
- background: var(--pp-surface);
143
- gap: 12px;
144
- }
145
- .pp-panel .form-row:last-child {
146
- border-bottom: 0;
147
- }
148
- .pp-panel .form-row__text {
149
- flex: 1;
150
- min-width: 0;
151
- margin: 0;
152
- display: flex;
153
- flex-direction: column;
154
- }
155
- .pp-panel .form-row__title {
156
- font-size: 15px;
157
- line-height: 22px;
158
- color: var(--pp-text);
159
- text-align: left;
160
- }
161
- .pp-panel .form-row__subtitle {
162
- font-size: 12px;
163
- line-height: 18px;
164
- color: var(--pp-muted);
165
- overflow-wrap: anywhere;
166
- margin-top: 2px;
167
- }
168
118
  .pp-choice-link {
169
119
  display: flex;
170
120
  align-items: center;
@@ -192,60 +142,29 @@
192
142
  font-size: 22px;
193
143
  flex: none;
194
144
  }
195
- .pp-input {
196
- font: inherit;
197
- color: var(--pp-text);
198
- background: var(--pp-surface);
199
- border: 1px solid var(--pp-border);
200
- border-radius: 6px;
201
- padding: 8px;
202
- min-width: 0;
203
- max-width: 45%;
145
+ .pp-editor {
146
+ flex: none;
204
147
  width: 45%;
148
+ min-width: 0;
205
149
  }
206
- select.pp-input {
207
- text-overflow: ellipsis;
208
- font-size: 13px;
150
+ .pp-search {
151
+ width: 100%;
152
+ margin-bottom: 12px;
209
153
  }
210
154
  .pp-panel .pp-multiline {
211
155
  display: block;
212
156
  }
213
- .pp-multiline .pp-input {
214
- max-width: 100%;
157
+ .pp-multiline .pp-editor {
215
158
  width: 100%;
216
159
  margin-top: 10px;
217
160
  }
218
- .pp-switch {
219
- appearance: none;
220
- -webkit-appearance: none;
221
- position: relative;
222
- flex: none;
223
- width: 32px;
224
- height: 20px;
225
- max-width: none;
161
+ .pp-panel [hidden] {
162
+ display: none !important;
163
+ }
164
+ .pp-panel .v-toggle {
226
165
  border: 0;
227
- border-radius: 15px;
228
166
  padding: 0;
229
- background: #c9ccd0;
230
- cursor: pointer;
231
- transition: background 0.2s;
232
- }
233
- .pp-switch::before {
234
- content: "";
235
- position: absolute;
236
- top: 3px;
237
- left: 3px;
238
- width: 14px;
239
- height: 14px;
240
- border-radius: 50%;
241
- background: white;
242
- transition: transform 0.2s;
243
- }
244
- .pp-switch:checked {
245
- background: var(--pp-accent);
246
- }
247
- .pp-switch:checked::before {
248
- transform: translateX(12px);
167
+ flex: none;
249
168
  }
250
169
  .pp-choice {
251
170
  justify-content: space-between;
@@ -315,29 +234,3 @@ select.pp-input {
315
234
  outline: 2px solid var(--pp-accent);
316
235
  outline-offset: -2px;
317
236
  }
318
- @media (prefers-color-scheme: dark) {
319
- .pp-panel {
320
- --pp-text: #e3e5e7;
321
- --pp-background: #17181a;
322
- --pp-surface: #232427;
323
- --pp-border: #343538;
324
- }
325
- }
326
- :root[data-theme="dark"] .pp-panel {
327
- --pp-text: #e3e5e7;
328
- --pp-background: #17181a;
329
- --pp-surface: #232427;
330
- --pp-border: #343538;
331
- }
332
- :root[data-theme="light"] .pp-panel {
333
- --pp-text: #18191c;
334
- --pp-background: #f6f7f8;
335
- --pp-surface: #fff;
336
- --pp-border: #e3e5e7;
337
- }
338
- @media (prefers-reduced-motion: reduce) {
339
- .pp-panel .pp-switch,
340
- .pp-panel .pp-switch::before {
341
- transition: none;
342
- }
343
- }
@@ -1,6 +1,6 @@
1
1
  import { ActionMenu } from "./ActionMenu.mjs";
2
2
  import { createPreferencesClient } from "./client.mjs";
3
- import { errorView, icon, element as node, resourceURL } from "./components.mjs";
3
+ import { errorView, fieldControl, icon, element as node, requestConfirmation, resourceURL } from "./components.mjs";
4
4
  import { Navigation } from "./Navigation.mjs";
5
5
 
6
6
  /**
@@ -27,7 +27,7 @@ export function mountPanel(root, catalog) {
27
27
  { id: "clearCaches", label: "清空缓存", destructive: true },
28
28
  { id: "reset", label: "重置模块", destructive: true },
29
29
  ];
30
- const menu = new ActionMenu(id => handlers.get(id)());
30
+ const menu = new ActionMenu(id => runAction(id));
31
31
  const trailing = node("span", "pp-nav-spacer");
32
32
  trailing.append(menu.element);
33
33
  const brand = node("div", "pp-brand");
@@ -57,7 +57,7 @@ export function mountPanel(root, catalog) {
57
57
  );
58
58
  };
59
59
  const onAction = event => {
60
- if (!saving && handlers.has(event.detail)) handlers.get(event.detail)();
60
+ if (!saving && handlers.has(event.detail)) runAction(event.detail);
61
61
  };
62
62
  window.frameElement?.addEventListener("preferencepanes:action", onAction);
63
63
  let timer,
@@ -99,6 +99,19 @@ export function mountPanel(root, catalog) {
99
99
  }, 2400);
100
100
  };
101
101
  const client = createPreferencesClient({ catalog, notify });
102
+ /**
103
+ * 两种菜单入口共用异步错误处理,包含宿主确认框错误。
104
+ * Share async error handling between both menus, including host-dialog errors.
105
+ * @param {string} id 操作标识 / Action identifier.
106
+ * @returns {Promise<void>} 操作已处理 / Action handled.
107
+ */
108
+ async function runAction(id) {
109
+ try {
110
+ await handlers.get(id)();
111
+ } catch (error) {
112
+ notify({ kind: "error", message: error.message });
113
+ }
114
+ }
102
115
  /**
103
116
  * 打开模块并忽略已过期的异步结果。
104
117
  * Open a module and ignore stale asynchronous results.
@@ -130,6 +143,14 @@ export function mountPanel(root, catalog) {
130
143
  const { definition, values } = client.snapshot(active);
131
144
  heading.textContent = definition.metadata?.name || active;
132
145
  const view = node("section", "pp-fields");
146
+ const search = node("input", "");
147
+ search.type = "search";
148
+ search.placeholder = "搜索设置项";
149
+ search.setAttribute("aria-label", "搜索设置");
150
+ const searchField = fieldControl(search);
151
+ searchField.classList.add("pp-search");
152
+ view.append(searchField);
153
+ const searchRows = [];
133
154
  /**
134
155
  * 挂载后执行的多行高度更新
135
156
  * Textarea sizing callbacks run after mounting.
@@ -204,7 +225,7 @@ export function mountPanel(root, catalog) {
204
225
  const match = /^\[([^\]]+)\]\s*(.*)$/.exec(field.name);
205
226
  const group = match?.[1] ?? "通用";
206
227
  if (!groups.has(group)) {
207
- const section = node("section", "form-group");
228
+ const section = node("section", "form-group form-group--has-title");
208
229
  const rows = node("div", "form-group__row");
209
230
  section.append(node("h2", "form-group__title", group), rows);
210
231
  groups.set(group, rows);
@@ -232,7 +253,7 @@ export function mountPanel(root, catalog) {
232
253
  let eventName = "change";
233
254
  switch (true) {
234
255
  case Boolean(field.options) && field.type !== "array": {
235
- const select = node("select", "pp-input");
256
+ const select = node("select", "");
236
257
  select.setAttribute("aria-label", field.name);
237
258
  field.options.forEach((option, index) => {
238
259
  const item = node("option", "", option.label);
@@ -242,7 +263,7 @@ export function mountPanel(root, catalog) {
242
263
  write = value => {
243
264
  select.selectedIndex = field.options.findIndex(option => option.key === value);
244
265
  };
245
- row.append(select);
266
+ row.append(fieldControl(select));
246
267
  read = () => field.options[select.selectedIndex]?.key;
247
268
  break;
248
269
  }
@@ -288,9 +309,27 @@ export function mountPanel(root, catalog) {
288
309
  };
289
310
  break;
290
311
  }
312
+ case field.type === "boolean": {
313
+ const toggle = node("button", "v-toggle v-toggle--small form-row__toggle");
314
+ toggle.type = "button";
315
+ toggle.setAttribute("role", "switch");
316
+ toggle.setAttribute("aria-label", field.name);
317
+ toggle.append(node("span", "v-toggle__circle"));
318
+ write = value => {
319
+ toggle.setAttribute("aria-checked", String(value === true));
320
+ toggle.classList.toggle("v-toggle--closed", value !== true);
321
+ };
322
+ read = () => toggle.getAttribute("aria-checked") === "true";
323
+ toggle.onclick = () => {
324
+ write(!read());
325
+ toggle.dispatchEvent(new window.Event("change", { bubbles: true }));
326
+ };
327
+ row.append(toggle);
328
+ break;
329
+ }
291
330
  default: {
292
331
  const multiline = field.control === "textarea" || field.type === "array";
293
- const input = node(multiline ? "textarea" : "input", "pp-input");
332
+ const input = node(multiline ? "textarea" : "input", "");
294
333
  if (multiline) row.classList.add("pp-multiline");
295
334
  input.setAttribute("aria-label", field.name);
296
335
  if (field.placeholder) input.placeholder = field.placeholder;
@@ -312,33 +351,23 @@ export function mountPanel(root, catalog) {
312
351
  input.addEventListener("input", grow);
313
352
  growingInputs.push(grow);
314
353
  }
315
- if (field.type === "boolean") {
316
- input.type = "checkbox";
317
- input.classList.add("pp-switch");
318
- input.setAttribute("role", "switch");
319
- write = value => {
320
- input.checked = value === true;
321
- };
322
- read = () => input.checked;
323
- } else {
324
- eventName = "input";
325
- if (!multiline) input.type = field.type === "number" ? "number" : "text";
326
- write = value => {
327
- input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
328
- grow();
329
- };
330
- read = () => {
331
- switch (field.type) {
332
- case "array":
333
- return JSON.parse(input.value);
334
- case "number":
335
- return input.value === "" ? Number.NaN : Number(input.value);
336
- default:
337
- return input.value;
338
- }
339
- };
340
- }
341
- row.append(input);
354
+ eventName = "input";
355
+ if (!multiline) input.type = field.type === "number" ? "number" : "text";
356
+ write = value => {
357
+ input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
358
+ grow();
359
+ };
360
+ read = () => {
361
+ switch (field.type) {
362
+ case "array":
363
+ return JSON.parse(input.value);
364
+ case "number":
365
+ return input.value === "" ? Number.NaN : Number(input.value);
366
+ default:
367
+ return input.value;
368
+ }
369
+ };
370
+ row.append(fieldControl(input, multiline));
342
371
  break;
343
372
  }
344
373
  }
@@ -368,7 +397,18 @@ export function mountPanel(root, catalog) {
368
397
  });
369
398
  if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
370
399
  groups.get(group).append(row);
400
+ searchRows.push({ row, text: [field.name, field.key, field.description, ...(field.options ?? []).map(option => option.label)].join(" ").toLocaleLowerCase() });
371
401
  }
402
+ const empty = node("p", "pp-description", "没有匹配的设置项");
403
+ empty.hidden = true;
404
+ empty.setAttribute("role", "status");
405
+ view.append(empty);
406
+ search.oninput = () => {
407
+ const words = search.value.trim().toLocaleLowerCase().split(/\s+/);
408
+ for (const { row, text } of searchRows) row.hidden = !words.every(word => text.includes(word));
409
+ for (const rows of groups.values()) rows.parentElement.hidden = [...rows.children].every(row => row.hidden);
410
+ empty.hidden = searchRows.some(({ row }) => !row.hidden);
411
+ };
372
412
  const cachePage = node("section", "pp-cache-page");
373
413
  const output = node("pre", "pp-cache");
374
414
  output.textContent = "暂无缓存";
@@ -393,9 +433,9 @@ export function mountPanel(root, catalog) {
393
433
  },
394
434
  );
395
435
  });
396
- handlers.set("clearCaches", () => {
436
+ handlers.set("clearCaches", async () => {
397
437
  if (saving) return;
398
- if (!window.confirm(`清空 ${active} 的全部 Caches?`)) return;
438
+ if (!(await requestConfirmation(window, `清空 ${active} 的全部 Caches?`)) || destroyed || saving) return;
399
439
  return perform(
400
440
  () => client.clearCaches(active),
401
441
  () => {
@@ -403,9 +443,9 @@ export function mountPanel(root, catalog) {
403
443
  },
404
444
  );
405
445
  });
406
- handlers.set("reset", () => {
446
+ handlers.set("reset", async () => {
407
447
  if (saving) return;
408
- if (!window.confirm(`重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) return;
448
+ if (!(await requestConfirmation(window, `重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) || destroyed || saving) return;
409
449
  return perform(() => client.reset(active), controls);
410
450
  });
411
451
  navigation?.destroy();