@nsnanocat/preference-panes 1.3.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsnanocat/preference-panes",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Generic BoxJS settings frontend and persistence API for JavaScript proxy modules",
5
5
  "author": "VirgilClyne <Virgil@nanocat.me>",
6
6
  "homepage": "https://NSNanoCat.github.io/preference-panes",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "./navigation": {
30
30
  "types": "./src/browser/Navigation.d.mts",
31
- "import": "./dist/module/navigation.mjs"
31
+ "import": "./dist/navigation.mjs"
32
32
  }
33
33
  },
34
34
  "types": "src/index.d.ts",
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 独立页面的三点按钮和底部操作菜单;嵌入页面由宿主提供对应界面。
3
- * Overflow trigger and bottom action sheet for standalone pages; embedded pages use host-provided chrome.
2
+ * 共用菜单按钮和底部操作菜单;原生宿主可通过 MORE 调用。
3
+ * Shared menu trigger and bottom action sheet; native hosts may invoke it through MORE.
4
4
  */
5
5
  export class ActionMenu {
6
6
  #button;
@@ -28,13 +28,14 @@ export class ActionMenu {
28
28
  this.element = document.createElement("span");
29
29
  const triggerRoot = this.element.attachShadow({ mode: "open" });
30
30
  triggerRoot.innerHTML = `<style>
31
- :host{display:inline-flex;width:44px;height:44px;color:inherit}
31
+ :host{display:inline-flex;flex:none;width:37px;height:37px;color:inherit}
32
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
+ button{box-sizing:border-box;width:37px;height:37px;padding:8px;border-radius:4px;font:inherit;cursor:pointer;border:0;color:inherit;background:none;transition:background-color .3s}
34
+ button:hover{background:var(--pp-field,#8882)}
34
35
  button:disabled{opacity:.4;cursor:default}
35
36
  button:focus-visible{outline:2px solid currentColor;outline-offset:-3px}
36
- svg{display:block;width:24px;height:24px;fill:currentColor}
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>`;
37
+ svg{display:block;width:21px;height:21px;fill:none}
38
+ </style><button type="button" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false"><svg viewBox="0 0 21 21" aria-hidden="true"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.667" d="M3.645 5.225h13.333M3.645 10.225h13.333M3.645 15.225h13.333"/></svg></button>`;
38
39
  this.#button = triggerRoot.querySelector("button");
39
40
  this.#layer = document.createElement("span");
40
41
  const layerRoot = this.#layer.attachShadow({ mode: "open" });
@@ -1,84 +1,3 @@
1
- /**
2
- * 静态 HTML 的模块 iframe 容器;页面资源输入写入 iframe 数据属性。
3
- * Static HTML module iframe whose resource inputs are written to iframe data attributes.
4
- * confirm 事件可 preventDefault 后通过 detail.resolve/reject 完成宿主确认。
5
- * Prevent default on confirm events and settle host dialogs through detail.resolve/reject.
6
- * notice 事件可 preventDefault 后交由宿主显示,模块不再创建网页 Toast。
7
- * Prevent default on notice events to display them in the host without a module web Toast.
8
- * open-url 事件可 preventDefault 后交由宿主打开 URL,模块不再执行链接默认导航。
9
- * Prevent default on open-url events to open the URL in the host instead of navigating the module.
10
- */
11
- export class ModuleFrame extends EventTarget {
12
- /**
13
- * 创建容器。
14
- * Create a container.
15
- * @param url 模块地址 / Module URL.
16
- * @param options 静态页面请求与资源输入 / Static page request and resource inputs.
17
- */
18
- constructor(url: string | URL, options?: { signal?: AbortSignal; headers?: HeadersInit });
19
- /**
20
- * 宿主挂载节点。
21
- * Host-mounted element.
22
- */
23
- readonly element: HTMLIFrameElement;
24
- /**
25
- * change 事件对应的导航状态。
26
- * Navigation state exposed with change events.
27
- */
28
- readonly state: { title: string; module: string; busy: boolean; canGoBack: boolean; actions: MenuAction[] };
29
- /**
30
- * 获取原始 HTML。
31
- * Fetch unmodified HTML.
32
- * @returns 加载完成 / Load completion.
33
- */
34
- load(): Promise<void>;
35
- /**
36
- * 沿模块历史返回。
37
- * Go back through module history.
38
- * @returns 无返回值 / No return value.
39
- */
40
- back(): void;
41
- /**
42
- * 执行模块提供的菜单操作。
43
- * Perform a module-provided menu action.
44
- * @param id 操作标识 / Action identifier.
45
- * @returns 无返回值 / No return value.
46
- */
47
- perform(id: string): void;
48
- /**
49
- * 取消请求与事件订阅。
50
- * Cancel requests and subscriptions.
51
- * @returns 无返回值 / No return value.
52
- */
53
- destroy(): void;
54
- }
55
-
56
- /**
57
- * 宿主确认事件的数据。
58
- * Host confirmation event detail.
59
- */
60
- export interface ConfirmationRequest {
61
- /**
62
- * 确认内容。
63
- * Confirmation message.
64
- */
65
- message: string;
66
- /**
67
- * 返回选择。
68
- * Return the user's choice.
69
- * @param confirmed 是否确认 / Whether confirmed.
70
- * @returns 无返回值 / No return value.
71
- */
72
- resolve(confirmed: boolean): void;
73
- /**
74
- * 返回失败。
75
- * Return a failure.
76
- * @param error 错误 / Error.
77
- * @returns 无返回值 / No return value.
78
- */
79
- reject(error: Error): void;
80
- }
81
-
82
1
  /**
83
2
  * 操作结果通知。
84
3
  * Operation result notice.
@@ -96,18 +15,6 @@ export interface Notice {
96
15
  message: string;
97
16
  }
98
17
 
99
- /**
100
- * 宿主 URL 导航请求。
101
- * Host URL navigation request.
102
- */
103
- export interface OpenURLRequest {
104
- /**
105
- * 完整目标地址。
106
- * Absolute target URL.
107
- */
108
- url: string;
109
- }
110
-
111
18
  /**
112
19
  * 菜单操作描述。
113
20
  * Menu action descriptor.
@@ -131,8 +38,8 @@ export interface MenuAction {
131
38
  }
132
39
 
133
40
  /**
134
- * 共用三点菜单。
135
- * Shared overflow menu.
41
+ * 共用操作菜单。
42
+ * Shared action menu.
136
43
  */
137
44
  export class ActionMenu {
138
45
  /**
@@ -258,6 +165,8 @@ export class Navigation extends EventTarget {
258
165
  * @returns 无返回值 / No return value.
259
166
  */
260
167
  open(key: string): void;
168
+ /** 更新当前挂载视图,不增加历史 / Refresh the mounted view without adding history. */
169
+ refresh(): void;
261
170
  /**
262
171
  * 沿联合历史后退。
263
172
  * Go back through joint history.
@@ -1,10 +1,9 @@
1
1
  export { ActionMenu } from "./ActionMenu.mjs";
2
- export { ModuleFrame } from "./ModuleFrame.mjs";
3
2
  export { ModuleStatus, probeModule } from "./ModuleStatus.mjs";
4
3
 
5
4
  /**
6
- * 同一文档内的主页/子页导航;iframe 各自的实例通过浏览器联合历史协作。
7
- * Navigate home/detail views within a document; iframe instances cooperate through joint browser history.
5
+ * 同一文档内的首页、模块及字段子页导航。
6
+ * Navigate homepage, module and field views within one document.
8
7
  */
9
8
  export class Navigation extends EventTarget {
10
9
  #container;
@@ -16,7 +15,7 @@ export class Navigation extends EventTarget {
16
15
  #retiring;
17
16
  #controller;
18
17
  #animation;
19
- #scroll = new WeakMap();
18
+ #scroll = new Map();
20
19
  #onHistory = () => this.#route();
21
20
  #onPageShow = event => {
22
21
  if (event.persisted) this.#route(true);
@@ -59,28 +58,37 @@ export class Navigation extends EventTarget {
59
58
  }
60
59
 
61
60
  /**
62
- * 加入子页历史;使用文档自身 URL,避免 srcdoc 按宿主 base URL 跳转。
63
- * Push a detail using the document URL, avoiding srcdoc navigation against the host base URL.
61
+ * 加入模块或字段子页历史,保留路由中的层级分隔符。
62
+ * Push a module or field view while retaining route hierarchy separators.
64
63
  * @param {string} key 子页键 / Detail key.
65
64
  * @returns {void} 无返回值 / No return value.
66
65
  */
67
66
  open(key) {
68
67
  if (key === this.#key) return;
69
68
  const url = new URL(this.#window.location.href);
70
- url.hash = encodeURIComponent(key);
69
+ url.hash = key.split("/").map(encodeURIComponent).join("/");
71
70
  this.#window.history.pushState({ ...this.#window.history.state, preferencePanesRoute: key }, "", url.href);
72
71
  this.#route();
73
72
  }
74
73
 
75
74
  /**
76
- * 沿浏览器联合历史返回,根页可退回宿主或上个文档。
77
- * Go back through joint history, including a host or previous document from home.
75
+ * 沿文档历史返回,根页可退回上个文档。
76
+ * Go back through document history, including the previous document from home.
78
77
  * @returns {void} 无返回值 / No return value.
79
78
  */
80
79
  back() {
81
80
  if (this.canGoBack) this.#window.history.back();
82
81
  }
83
82
 
83
+ /**
84
+ * 更新当前页的挂载视图,保留路由和滚动位置。
85
+ * Refresh the mounted view while retaining route and scroll position.
86
+ * @returns {void} 无返回值 / No return value.
87
+ */
88
+ refresh() {
89
+ this.#route(true);
90
+ }
91
+
84
92
  /**
85
93
  * 解析 URL 并统一处理页面切换、加载取消与动画结束后的释放。
86
94
  * Resolve the URL and coordinate transitions, cancellation and release after animation.
@@ -97,28 +105,42 @@ export class Navigation extends EventTarget {
97
105
  key = "";
98
106
  }
99
107
  if (!reload && key === this.#key) return;
108
+ if (key !== this.#key) this.#scroll.set(this.#key, (this.#view ?? this.#home).scrollTop);
100
109
  this.#controller?.abort();
101
110
  this.#controller = new AbortController();
102
111
  const next = key ? this.#create(key, this.#controller.signal) : undefined;
103
112
  if (!next) key = "";
104
113
  const history = this.#window.history;
105
- // 直接打开子页时建立一次主页历史;刷新不重复堆叠。
106
- // Seed home history once for direct details, without stacking entries on reload.
114
+ // 直接打开字段页时补齐首页和模块历史;刷新不重复堆叠。
115
+ // Seed homepage and module history for direct field links without stacking on reload.
107
116
  if (url.hash && history.state?.preferencePanesRoute !== key) {
108
117
  url.hash = "";
109
118
  history.replaceState({ ...history.state, preferencePanesRoute: "" }, "", url.href);
110
119
  if (key) {
111
- url.hash = encodeURIComponent(key);
120
+ const module = key.split("/")[0];
121
+ if (key.includes("/") && module !== "$link") {
122
+ url.hash = encodeURIComponent(module);
123
+ history.pushState({ ...history.state, preferencePanesRoute: module }, "", url.href);
124
+ }
125
+ url.hash = key.split("/").map(encodeURIComponent).join("/");
112
126
  history.pushState({ ...history.state, preferencePanesRoute: key }, "", url.href);
113
127
  }
114
128
  }
115
129
  const previous = this.#view;
130
+ if (previous === next && this.#key === key) {
131
+ if (previous && this.#scroll.has(key)) {
132
+ previous.scrollTop = this.#scroll.get(key);
133
+ this.#scroll.delete(key);
134
+ }
135
+ this.dispatchEvent(new Event("change"));
136
+ return;
137
+ }
138
+ const scroll = this.#scroll.get(key) ?? (key === this.#key ? (previous?.scrollTop ?? 0) : 0);
116
139
  const position = previous ? this.#window.getComputedStyle(previous).transform : "none";
117
140
  this.#animation?.cancel();
118
141
  this.#retiring?.remove();
119
142
  this.#retiring = previous;
120
143
  if (previous) {
121
- this.#scroll.set(previous, previous.scrollTop);
122
144
  previous.inert = true;
123
145
  }
124
146
  this.#key = key;
@@ -127,7 +149,14 @@ export class Navigation extends EventTarget {
127
149
  if (next) {
128
150
  next.inert = false;
129
151
  this.#container.append(next);
130
- next.scrollTop = this.#scroll.get(next) ?? 0;
152
+ next.scrollTop = scroll;
153
+ // 加载占位页可能无法滚到原位置;配置与控件就绪后的 refresh 再恢复一次。
154
+ // A loading placeholder may clamp scrolling; restore once more when config and controls refresh.
155
+ if (next.scrollTop === scroll) this.#scroll.delete(key);
156
+ }
157
+ if (!next) {
158
+ this.#home.scrollTop = this.#scroll.get("") ?? 0;
159
+ this.#scroll.delete("");
131
160
  }
132
161
  const moving = next ?? previous;
133
162
  if (moving) {
@@ -70,7 +70,7 @@ export class PreferencesClient {
70
70
  clearCaches(): Promise<void>;
71
71
  /** 删除声明的设置路径,默认整个模块数据,并恢复默认值 / Delete declared setting paths, defaulting to module data, and restore defaults. */
72
72
  reset(): Promise<void>;
73
- /** 取消请求 / Cancel requests. */
73
+ /** 取消读取,已开始的写入继续完成 / Cancel reads while allowing started writes to finish. */
74
74
  leave(): void;
75
75
  /** 写入单个字段 / Write one field. */
76
76
  set(key: string, value: SettingsScalar | SettingsScalar[]): Promise<void>;
@@ -113,8 +113,8 @@ export class PreferencesClient {
113
113
  }
114
114
 
115
115
  /**
116
- * 终止当前页面仍在进行的请求。
117
- * Abort requests still owned by the current page.
116
+ * 终止当前页面的读取;已开始的持久化写入继续完成。
117
+ * Abort reads owned by the current page while allowing started persistence writes to finish.
118
118
  * @returns {void} 无返回值 / No return value.
119
119
  */
120
120
  leave() {
@@ -153,8 +153,10 @@ export class PreferencesClient {
153
153
  async #send(action, path, value) {
154
154
  const controller = new AbortController();
155
155
  const abort = () => controller.abort();
156
- if (this.#session.signal.aborted) abort();
157
- this.#session.signal.addEventListener("abort", abort, { once: true });
156
+ if (action === "get") {
157
+ if (this.#session.signal.aborted) abort();
158
+ this.#session.signal.addEventListener("abort", abort, { once: true });
159
+ }
158
160
  const timer = setTimeout(abort, this.#timeout);
159
161
  try {
160
162
  const response = await this.#request(`/api/${action}`, {
@@ -36,18 +36,6 @@ export function fieldControl(control) {
36
36
  return control;
37
37
  }
38
38
 
39
- /**
40
- * 元数据地址只允许 HTTP(S) 和相对地址。
41
- * Allow only HTTP(S) and relative metadata addresses.
42
- * @param {string} value 元数据地址 / Metadata address.
43
- * @returns {string} 完整地址 / Absolute address.
44
- */
45
- export function resourceURL(value) {
46
- const url = new URL(value, document.baseURI);
47
- if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Metadata URLs must use HTTP(S)");
48
- return url.href;
49
- }
50
-
51
39
  /**
52
40
  * 创建覆盖可用内容区的通用读取状态,失败时可附加重试动作。
53
41
  * Create a shared status view that fills the available content area and may include retry.
@@ -70,35 +58,3 @@ export function statusView(message, retry) {
70
58
  }
71
59
  return view;
72
60
  }
73
-
74
- /**
75
- * 请求宿主确认;独立网页使用浏览器对话框。
76
- * Request confirmation from the host, using the browser dialog for standalone pages.
77
- * @param {Window} host 模块窗口 / Module window.
78
- * @param {string} message 确认内容 / Confirmation message.
79
- * @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
80
- */
81
- export function requestConfirmation(host, message) {
82
- return new Promise((resolve, reject) => {
83
- const frame = host.frameElement;
84
- if (frame) {
85
- const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
86
- if (!frame.dispatchEvent(event)) return;
87
- }
88
- resolve(host.confirm(message));
89
- });
90
- }
91
-
92
- /**
93
- * 请求宿主打开 URL;未被接管时保留链接默认导航。
94
- * Request host URL navigation while preserving the link default when unhandled.
95
- * @param {Window} host 模块窗口 / Module window.
96
- * @param {string} url 完整目标地址 / Absolute target URL.
97
- * @returns {boolean} 宿主是否接管导航 / Whether the host accepted navigation ownership.
98
- */
99
- export function requestURLNavigation(host, url) {
100
- const frame = host.frameElement;
101
- if (!frame) return false;
102
- const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:open-url", { cancelable: true, detail: { url } });
103
- return !frame.dispatchEvent(event);
104
- }
@@ -4,6 +4,12 @@ import type { ActionMenu, MenuAction, Notice } from "./Navigation.mjs";
4
4
  export interface HomeState {
5
5
  /** 当前标题 / Current title. */
6
6
  title: string;
7
+ /** 首页导航图片 / Homepage navigation image. */
8
+ logo?: string;
9
+ /** 首页导航文字 / Homepage navigation text. */
10
+ logoText?: string;
11
+ /** 网页导航品牌链接 / Web navigation brand link. */
12
+ logoHref?: string;
7
13
  /** 可用菜单操作 / Available menu actions. */
8
14
  actions: MenuAction[];
9
15
  /** 操作是否正在执行 / Whether an operation is in progress. */
@@ -44,11 +50,17 @@ export type HomeIcon = { src: string; text?: never } | { text: string; src?: nev
44
50
  /** 外链打开方式:带导航的内部框架、当前页面或新页面 / Link opening mode: an internal frame with navigation, the current page or a new page. */
45
51
  export type HomeLinkTarget = "internal" | "_self" | "_blank";
46
52
  /** 首页模块或链接入口 / Homepage module or link entry. */
47
- export type HomeItem = { name: string; icon: HomeIcon; description?: string } & ({ module: string; pageModule?: string; href?: never; target?: never } | { href: string; target?: HomeLinkTarget; module?: never; pageModule?: never });
53
+ export type HomeItem = { name: string; icon: HomeIcon; description?: string } & ({ module: string; href?: never; target?: never } | { href: string; target?: HomeLinkTarget; module?: never });
48
54
  /** 运行时导入的首页配置 / Runtime-imported homepage configuration. */
49
55
  export interface HomeConfig {
56
+ /** 文档和原生导航标题;未提供品牌时也是网页导航文字 / Document and native navigation title; also the web navigation text when no brand is supplied. */
50
57
  title: string;
58
+ /** 网页导航图片,相对于 JSON 地址 / Web navigation image, relative to the JSON URL. */
51
59
  logo?: string;
60
+ /** 网页导航文字,沿用 Rspress 命名 / Web navigation text, following Rspress naming. */
61
+ logoText?: string;
62
+ /** 网页导航品牌链接,相对于 JSON 地址;默认网站首页 / Web navigation brand link, relative to the JSON URL; defaults to the site homepage. */
63
+ logoHref?: string;
52
64
  sections: { title: string; layout?: "grid" | "list"; items: HomeItem[] }[];
53
65
  footer?: string[];
54
66
  }
@@ -9,7 +9,6 @@
9
9
  <div data-preference-panes-pages>
10
10
  <main class="pp-home" data-preference-panes-home></main>
11
11
  </div>
12
- <template data-preference-panes-module><section data-preference-panes-module-page><p data-module-message>正在打开设置…</p></section></template>
13
12
  <script type="module">__PREFERENCE_PANES_HOME_SCRIPT__
14
13
  await startHome();</script>
15
14
  </body>