@huaqiu/dsh-tool-plugin-update 0.0.0-stage → 0.5.22

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/lib/client.js ADDED
@@ -0,0 +1,1869 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@huaqiu/dsh-tool-plugin-update",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region ../dsh-sidebar-entry/lib/index.mjs
8
+ /** Find the sidebar shell root element, or undefined while not yet mounted. */
9
+ function sidebarRoot() {
10
+ const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
11
+ if (column === null) return void 0;
12
+ return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
13
+ }
14
+ /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
15
+ function newSessionButton(root) {
16
+ const nested = root.querySelector("button[class*=\"newSession\"]");
17
+ if (nested !== null) return nested;
18
+ for (const child of root.children) if (child.tagName === "BUTTON") return child;
19
+ }
20
+ /** Build the entry row (a detached button; insert once the shell is up). */
21
+ function createEntry(options) {
22
+ const entry = document.createElement("button");
23
+ entry.type = "button";
24
+ entry.setAttribute(options.rowAttribute, "");
25
+ if (options.plugin !== void 0) {
26
+ entry.setAttribute("data-dsh-plugin", options.plugin);
27
+ entry.setAttribute("data-dsh-part", "sidebar-entry");
28
+ }
29
+ entry.className = options.css["entry"] ?? "";
30
+ const labelSpan = document.createElement("span");
31
+ labelSpan.className = options.css["entryLabel"] ?? "";
32
+ const iconSpan = document.createElement("span");
33
+ iconSpan.className = options.css["entryIcon"] ?? "";
34
+ iconSpan.innerHTML = options.icon;
35
+ entry.append(iconSpan, labelSpan);
36
+ let badgeSpan = null;
37
+ const applyBadge = () => {
38
+ if (options.badge === void 0) return;
39
+ const count = options.badge.count();
40
+ if (count <= 0) {
41
+ badgeSpan?.remove();
42
+ badgeSpan = null;
43
+ return;
44
+ }
45
+ if (badgeSpan === null) {
46
+ badgeSpan = document.createElement("span");
47
+ badgeSpan.className = options.css["entryBadge"] ?? "";
48
+ entry.append(badgeSpan);
49
+ }
50
+ badgeSpan.textContent = String(count);
51
+ entry.setAttribute("data-has-badge", "");
52
+ };
53
+ const applyLabel = () => {
54
+ entry.setAttribute("aria-label", options.label());
55
+ if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
56
+ labelSpan.textContent = options.label();
57
+ };
58
+ applyLabel();
59
+ applyBadge();
60
+ entry.addEventListener("click", options.onToggle);
61
+ return {
62
+ entry,
63
+ applyLabel,
64
+ applyBadge
65
+ };
66
+ }
67
+ /** Re-insert the entry after the New Session row (before the browser region). */
68
+ function placeEntry(root, entry, options) {
69
+ const button = newSessionButton(root);
70
+ if (button === void 0) return false;
71
+ if (entry.parentElement !== root) {
72
+ const row = button.closest("[class*=\"logoRow\"]");
73
+ const base = row !== null && row.parentElement === root ? row : button;
74
+ const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
75
+ const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
76
+ root.insertBefore(entry, anchor ?? null);
77
+ }
78
+ return true;
79
+ }
80
+ /**
81
+ * Mount the sidebar entry, waiting for the shell to render and self-healing
82
+ * on later React re-renders.
83
+ * @param options - the row's attribute/icon/copy/action/ordering configuration.
84
+ * @returns disposer removing the entry and its observers.
85
+ */
86
+ function mountSidebarEntry(options) {
87
+ if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
88
+ const { entry, applyLabel, applyBadge } = createEntry(options);
89
+ let root;
90
+ let placed = false;
91
+ let unsubscribeRefresh;
92
+ if (options.refresh !== void 0) try {
93
+ unsubscribeRefresh = options.refresh.subscribe(applyLabel);
94
+ } catch {}
95
+ let unsubscribeBadge;
96
+ if (options.badge !== void 0) try {
97
+ unsubscribeBadge = options.badge.subscribe(applyBadge);
98
+ } catch {}
99
+ const tryPlace = () => {
100
+ if (root !== void 0 && !root.isConnected) {
101
+ rootObserver.disconnect();
102
+ root = void 0;
103
+ placed = false;
104
+ }
105
+ if (placed) {
106
+ if (document.body.contains(entry)) return;
107
+ rootObserver.disconnect();
108
+ root = void 0;
109
+ placed = false;
110
+ }
111
+ root ??= sidebarRoot();
112
+ if (root === void 0) return;
113
+ placed = placeEntry(root, entry, options);
114
+ if (placed) rootObserver.observe(root, {
115
+ childList: true,
116
+ subtree: true
117
+ });
118
+ };
119
+ const waitObserver = new MutationObserver(() => {
120
+ tryPlace();
121
+ });
122
+ waitObserver.observe(document.body, {
123
+ childList: true,
124
+ subtree: true
125
+ });
126
+ const rootObserver = new MutationObserver(() => {
127
+ if (root === void 0 || !root.isConnected) {
128
+ placed = false;
129
+ tryPlace();
130
+ return;
131
+ }
132
+ if (!root.contains(entry)) placed = placeEntry(root, entry, options);
133
+ });
134
+ options.active === void 0 || (() => {
135
+ const syncActive = () => {
136
+ if (options.active.isOpen()) entry.dataset.active = "true";
137
+ else delete entry.dataset.active;
138
+ };
139
+ const unsubscribe = options.active.subscribe(syncActive);
140
+ syncActive();
141
+ return unsubscribe;
142
+ })();
143
+ tryPlace();
144
+ return () => {
145
+ waitObserver.disconnect();
146
+ rootObserver.disconnect();
147
+ unsubscribeRefresh?.();
148
+ unsubscribeBadge?.();
149
+ entry.remove();
150
+ };
151
+ }
152
+ /**
153
+ * Every row selector in the Huaqiu sidebar family, in intended visual order.
154
+ *
155
+ * Family ordering is only stable when every participating plugin passes the
156
+ * SAME list to {@link mountSidebarEntry} — the core positions a row relative to
157
+ * the family block, never relative to transient shell geometry. Adding a new
158
+ * family row means adding its selector here (and to no plugin-local list).
159
+ */
160
+ const HUAQIU_SIDEBAR_ENTRY_FAMILY = [
161
+ "[data-hqcg-footprint-entry]",
162
+ "[data-hqcg-symbol-entry]",
163
+ "[data-hqcg-schematic-entry]",
164
+ "[data-hq-plugin-update-entry]"
165
+ ];
166
+ /**
167
+ * Read the host UI language from `<html lang>`.
168
+ *
169
+ * `dsh-client-locale` rewrites the attribute on every language change (dsh
170
+ * web) and HQ Edge mirrors it, so this is the one source every family plugin
171
+ * can read without importing a `locale` service.
172
+ * @returns 'zh' / 'en', or undefined when the tag is absent or unsupported
173
+ * (callers fall back to their own default).
174
+ */
175
+ function detectHostLocale() {
176
+ if (typeof document === "undefined") return void 0;
177
+ const tag = document.documentElement?.getAttribute("lang");
178
+ if (tag === null || tag === void 0 || tag === "") return void 0;
179
+ const primary = tag.toLowerCase().split("-")[0];
180
+ return primary === "zh" || primary === "en" ? primary : void 0;
181
+ }
182
+ /**
183
+ * Watch `<html lang>` and report every resolved change.
184
+ *
185
+ * Idempotent-safe: repeated calls share one observer and each caller gets its
186
+ * own disposer; the observer disconnects when the last listener leaves.
187
+ * @param listener - called with the newly resolved language.
188
+ * @returns disposer removing this listener.
189
+ */
190
+ /** Shared observer/listener set backing {@link observeHostLocale}. */
191
+ const localeListeners = /* @__PURE__ */ new Set();
192
+ let localeObserver = null;
193
+ function observeHostLocale(listener) {
194
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined" || document.documentElement === null) return () => {};
195
+ let current = detectHostLocale();
196
+ const listeners = localeListeners;
197
+ listeners.add(listener);
198
+ if (localeObserver === null) {
199
+ localeObserver = new MutationObserver(() => {
200
+ const next = detectHostLocale();
201
+ if (next === current) return;
202
+ current = next;
203
+ for (const fn of [...listeners]) try {
204
+ fn(next);
205
+ } catch {}
206
+ });
207
+ localeObserver.observe(document.documentElement, {
208
+ attributes: true,
209
+ attributeFilter: ["lang"]
210
+ });
211
+ }
212
+ return () => {
213
+ listeners.delete(listener);
214
+ if (listeners.size === 0 && localeObserver !== null) {
215
+ localeObserver.disconnect();
216
+ localeObserver = null;
217
+ }
218
+ };
219
+ }
220
+ //#endregion
221
+ //#region src/client/api.ts
222
+ /**
223
+ * Plugin-update API client (browser half).
224
+ *
225
+ * Every call is a same-origin fetch against the DSH webServer prefix route
226
+ * (`/api/v1/huaqiu/plugin-update/…`) which the node half forwards to hq-edge.
227
+ * Response shapes mirror hq-edge `apps/server/src/routes/plugin-updates.ts`.
228
+ *
229
+ * @module
230
+ */
231
+ const UPDATE_BASE = "/api/v1/huaqiu/plugin-update";
232
+ async function request(path, init) {
233
+ const res = await fetch(`${UPDATE_BASE}${path}`, {
234
+ cache: "no-store",
235
+ ...init,
236
+ headers: {
237
+ "content-type": "application/json; charset=utf-8",
238
+ ...init?.headers ?? {}
239
+ }
240
+ });
241
+ const body = await res.json().catch(() => null);
242
+ if (!res.ok) {
243
+ const err = body?.error ?? `HTTP ${res.status}`;
244
+ throw new Error(err);
245
+ }
246
+ if (body === null) throw new Error("empty response");
247
+ return body;
248
+ }
249
+ /** GET the cached (or empty-shell) plugin-update report. */
250
+ function fetchReport() {
251
+ return request("/report");
252
+ }
253
+ /** POST an explicit refresh; returns the freshly computed report. */
254
+ function checkUpdates() {
255
+ return request("/check", { method: "POST" });
256
+ }
257
+ /** POST the install transaction for the confirmed set version. */
258
+ function installSet(setVersion) {
259
+ return request("/install", {
260
+ method: "POST",
261
+ body: JSON.stringify({ setVersion })
262
+ });
263
+ }
264
+ /** POST ignore for the current offer (set-scoped, persists across restarts). */
265
+ function ignoreOffer(setVersion) {
266
+ return request("/ignore", {
267
+ method: "POST",
268
+ body: JSON.stringify({ setVersion })
269
+ });
270
+ }
271
+ /** POST unignore for the set version (reverses an ignore). */
272
+ function unignoreOffer(setVersion) {
273
+ return request("/unignore", {
274
+ method: "POST",
275
+ body: JSON.stringify({ setVersion })
276
+ });
277
+ }
278
+ const COPY = {
279
+ zh: {
280
+ entryLabel: "插件更新",
281
+ entryTooltip: "查看可用的插件更新",
282
+ viewTitle: "插件更新",
283
+ viewIntro: "检查并安装可用的插件更新",
284
+ viewInfo: "在这里查看插件集的更新。标注为「被阻止」的插件因主机或 HQ Edge 版本不兼容而无法安装;已是最新的插件无需操作。",
285
+ refresh: "刷新",
286
+ close: "关闭",
287
+ loading: "正在读取插件更新…",
288
+ empty: "没有可用的插件更新",
289
+ checkFailed: "检查更新失败,请重试",
290
+ installAll: (count) => `安装 ${count} 项更新`,
291
+ installOne: "安装",
292
+ installing: "正在安装…(DSH 将自动重启)",
293
+ nothingToInstall: "暂无可安装的更新",
294
+ ignore: "忽略此版本",
295
+ unignore: "恢复提醒",
296
+ ignoredOffer: "已忽略当前插件集更新",
297
+ groupCount: (count) => `${count} 个插件`,
298
+ group: {
299
+ actionable: "待处理",
300
+ blocked: "无法安装",
301
+ ignored: "已忽略",
302
+ current: "已是最新"
303
+ },
304
+ state: {
305
+ current: "当前",
306
+ update: "可更新",
307
+ install: "待安装",
308
+ ignored: "已忽略",
309
+ blocked: "被阻止"
310
+ },
311
+ blockedReason: {
312
+ host: "不适用于当前 EDA 主机",
313
+ "hq-edge-version": "当前 HQ Edge 版本不兼容",
314
+ "runtime-unknown": "HQ Edge 版本未知,无法校验"
315
+ },
316
+ runtime: {
317
+ title: "HQ Edge 运行时",
318
+ current: "当前版本",
319
+ latest: "最新版本",
320
+ set: "发布插件集",
321
+ checkedAt: "上次检查",
322
+ source: "更新源",
323
+ unknown: "版本未知",
324
+ notConfigured: "未配置更新源",
325
+ note: "运行时更新由 EDA 安装器处理"
326
+ },
327
+ lastError: "上次检查失败",
328
+ installError: "安装失败",
329
+ installSuccess: "更新已安装,正在重启 DSH…"
330
+ },
331
+ en: {
332
+ entryLabel: "Plugin Updates",
333
+ entryTooltip: "Review available plugin updates",
334
+ viewTitle: "Plugin Updates",
335
+ viewIntro: "Check for and install available plugin updates",
336
+ viewInfo: "Review the plugin set updates here. A plugin marked Blocked cannot be installed because its host or HQ Edge version is incompatible; a plugin marked Current needs no action.",
337
+ refresh: "Refresh",
338
+ close: "Close",
339
+ loading: "Reading plugin updates…",
340
+ empty: "No plugin updates available",
341
+ checkFailed: "Failed to check for updates — please retry",
342
+ installAll: (count) => `Install ${count} update${count === 1 ? "" : "s"}`,
343
+ installOne: "Install",
344
+ installing: "Installing… (DSH will restart)",
345
+ nothingToInstall: "Nothing to install right now",
346
+ ignore: "Ignore this release",
347
+ unignore: "Show again",
348
+ ignoredOffer: "The current plugin-set update is ignored",
349
+ groupCount: (count) => `${count} plugin${count === 1 ? "" : "s"}`,
350
+ group: {
351
+ actionable: "Actionable",
352
+ blocked: "Blocked",
353
+ ignored: "Ignored",
354
+ current: "Up to date"
355
+ },
356
+ state: {
357
+ current: "Current",
358
+ update: "Update",
359
+ install: "Install",
360
+ ignored: "Ignored",
361
+ blocked: "Blocked"
362
+ },
363
+ blockedReason: {
364
+ host: "Not applicable to the current EDA host",
365
+ "hq-edge-version": "Incompatible with the current HQ Edge version",
366
+ "runtime-unknown": "HQ Edge version unknown — cannot verify"
367
+ },
368
+ runtime: {
369
+ title: "HQ Edge Runtime",
370
+ current: "Current version",
371
+ latest: "Latest version",
372
+ set: "Release set",
373
+ checkedAt: "Last checked",
374
+ source: "Update source",
375
+ unknown: "Version unknown",
376
+ notConfigured: "No update source configured",
377
+ note: "Runtime updates are handled by the EDA installer"
378
+ },
379
+ lastError: "Last check failed",
380
+ installError: "Install failed",
381
+ installSuccess: "Update installed — restarting DSH…"
382
+ }
383
+ };
384
+ //#endregion
385
+ //#region src/client/icons.ts
386
+ /**
387
+ * Official DSH icon artwork, inlined as SVG strings.
388
+ *
389
+ * The paths are copied verbatim from `@deepseek-ai/dsh-client-ui-primitives`
390
+ * (deepseek-harness `packages/client/ui-primitives/src/icons/index.tsx`) so the
391
+ * Huaqiu plugin-update view draws the same glyphs as the shell's own plugin
392
+ * manager. Only `currentColor` is used, so every glyph inherits the label ink
393
+ * of whatever surface it sits on — the same contract the primitives rely on.
394
+ *
395
+ * The plugin renders plain DOM (never a React tree), so the artwork is exposed
396
+ * as markup strings rather than components.
397
+ *
398
+ * @module
399
+ */
400
+ /** Stroke width of the primitives' "Regular" weight. */
401
+ const REGULAR = 1;
402
+ /** Stroke width of the primitives' "Medium" weight. */
403
+ const MEDIUM = 1.3;
404
+ /** Wrap paths in a 16×16 stroked glyph. */
405
+ function glyph(size, strokeWidth, paths, viewBox = "0 0 16 16") {
406
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${viewBox}" fill="none" aria-hidden="true" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round">${paths.map((d) => `<path d="${d}" stroke="currentColor" />`).join("")}</svg>`;
407
+ }
408
+ /** Wrap fill-only paths (the check glyph) in their own viewBox. */
409
+ function fillGlyph(size, paths, viewBox) {
410
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${viewBox}" fill="none" aria-hidden="true">${paths.map((d) => `<path d="${d}" fill="currentColor" />`).join("")}</svg>`;
411
+ }
412
+ /**
413
+ * The plugin pinwheel — the glyph the shell's own Plugins sidebar entry uses
414
+ * (`IconPluginPinwheelOutlineRegular`). Shared by the sidebar row and the
415
+ * package cards so both read as the same feature.
416
+ */
417
+ const PLUGIN_ICON = (size = 16) => glyph(size, REGULAR, [
418
+ "M7.84457 5.06199C11.6605 4.93876 14.7962 6.14848 14.8484 7.76397C14.8875 8.97461 13.1838 10.0696 10.7215 10.5942",
419
+ "M5.12742 8.07731C5.00419 4.26138 6.21391 1.12568 7.8294 1.07351C9.04004 1.03441 10.135 2.73808 10.6596 5.20037",
420
+ "M8.02457 10.6802C4.20865 10.8034 1.07294 9.5937 1.02077 7.97821C0.981678 6.76758 2.68535 5.67262 5.14763 5.14798",
421
+ "M10.7476 7.89535C10.8708 11.7113 9.66109 14.847 8.0456 14.8991C6.83496 14.9382 5.74 13.2346 5.21536 10.7723"
422
+ ]);
423
+ /** The toolbar's refresh action (`IconRefreshOutlineRegular`). */
424
+ const REFRESH_ICON = (size = 16) => glyph(size, REGULAR, ["M14.5001 8C14.5 9.28552 14.1188 10.5422 13.4045 11.611C12.6903 12.6799 11.6752 13.5129 10.4875 14.0049C9.29982 14.4968 7.99295 14.6255 6.73212 14.3747C5.4713 14.124 4.31314 13.505 3.4041 12.596C2.49514 11.687 1.87614 10.5288 1.62537 9.26798C1.37459 8.00716 1.50331 6.70028 1.99525 5.51261C2.48719 4.32494 3.32025 3.30981 4.3891 2.59557C5.45795 1.88134 6.71458 1.50008 8.0001 1.5C9.9001 1.5 11.7001 2.3 13.0001 3.6L14.5001 5.1", "M14.4999 1.5V5.1H10.8999"]);
425
+ /** The panel's close action (`IconCloseOutlineMedium`). */
426
+ const CLOSE_ICON = (size = 16) => glyph(size, MEDIUM, ["M2.5 2.5L13.5 13.5", "M13.5 2.5L2.5 13.5"]);
427
+ /** The notice glyph (`IconWarningOutlineRegular`). */
428
+ const WARNING_ICON = (size = 16) => glyph(size, REGULAR, [
429
+ "M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z",
430
+ "M8 4.29199V9.79199",
431
+ "M8 10.708V11.708"
432
+ ]);
433
+ /** The settled-state glyph (`IconCheckCircleFillRegular`). */
434
+ const CHECK_CIRCLE_ICON = (size = 16) => fillGlyph(size, ["M28.1936 14.6936L19.8066 23.0806C19.2373 23.65 18.7159 24.1742 18.24 24.5571C17.7389 24.9602 17.1365 25.3359 16.3657 25.458C15.9581 25.5225 15.5428 25.5225 15.1353 25.458C14.3645 25.3359 13.7621 24.9602 13.261 24.5571C12.7851 24.1742 12.2637 23.65 11.6943 23.0806L7.80737 19.1936L10.1936 16.8074L14.0806 20.6943C14.7033 21.317 15.0763 21.6873 15.377 21.9292C15.6523 22.1507 15.7109 22.1325 15.6626 22.1248C15.7208 22.1339 15.7802 22.1339 15.8384 22.1248C15.7901 22.1325 15.8486 22.1507 16.124 21.9292C16.4247 21.6873 16.7977 21.317 17.4204 20.6943L25.8074 12.3074L28.1936 14.6936Z", "M32.8496 18.0005C32.8496 9.79906 26.2019 3.15137 18.0005 3.15137C9.79906 3.15137 3.15137 9.79906 3.15137 18.0005C3.15137 26.2019 9.79906 32.8496 18.0005 32.8496C26.2019 32.8496 32.8496 26.2019 32.8496 18.0005ZM35.7764 18.0005C35.7764 27.8173 27.8173 35.7764 18.0005 35.7764C8.18363 35.7764 0.224609 27.8173 0.224609 18.0005C0.224609 8.18363 8.18363 0.224609 18.0005 0.224609C27.8173 0.224609 35.7764 8.18363 35.7764 18.0005Z"], "0 0 36 36");
435
+ /** The install action (`IconDownloadOutlineRegular`). */
436
+ const DOWNLOAD_ICON = (size = 16) => glyph(size, REGULAR, [
437
+ "M8 1.95317V10.0469",
438
+ "M4.25 6.29688L8 10.0469L11.75 6.29688",
439
+ "M1.5 10.0469V13.158C1.5 13.3937 1.60536 13.6198 1.79289 13.7865C1.98043 13.9532 2.23478 14.0469 2.5 14.0469H13.5C13.7652 14.0469 14.0196 13.9532 14.2071 13.7865C14.3946 13.6198 14.5 13.3937 14.5 13.158V10.0469"
440
+ ]);
441
+ /** The head's info affordance (`IconInfoOutlineRegular`, fill-only artwork). */
442
+ const INFO_ICON = (size = 11) => fillGlyph(size, [
443
+ "M12.5757 7.00012C12.5757 3.92085 10.0794 1.42463 7.00012 1.42456C3.9208 1.42456 1.42456 3.9208 1.42456 7.00012C1.42463 10.0794 3.92085 12.5757 7.00012 12.5757C10.0793 12.5756 12.5756 10.0793 12.5757 7.00012ZM13.8002 7.00012C13.8001 10.7559 10.7559 13.8001 7.00012 13.8002C3.2443 13.8002 0.199291 10.7559 0.199219 7.00012C0.199219 3.24426 3.24426 0.199219 7.00012 0.199219C10.7559 0.199291 13.8002 3.2443 13.8002 7.00012Z",
444
+ "M7.6127 3.18921V4.55986H6.38735V3.18921H7.6127Z",
445
+ "M7.6127 5.68921V10.8109H6.38735V5.68921H7.6127Z"
446
+ ], "0 0 14 14");
447
+ //#endregion
448
+ //#region src/client/update-view.ts
449
+ /**
450
+ * Plugin-update review view (browser half).
451
+ *
452
+ * Plain-DOM panel (no React tree) so it can never disturb the DSH shell's
453
+ * reconciliation — same rule as the sidebar entry rows. Rendered content:
454
+ *
455
+ * - head: title + intro + refresh toolbar (the shell plugin-manager head row)
456
+ * - state-grouped sections, each a group head + package cards
457
+ * (48px hairline-framed glyph, title row with a state tag, one-line
458
+ * version transition, end actions) — the shell's card geometry
459
+ * - a facts group for the independent HQ Edge runtime report
460
+ * - footer actions (install / ignore / show again), with a toast that
461
+ * reports the outcome of an install the way the shell's Toast does
462
+ *
463
+ * Every surface rides a `--dsw-alias-*` design token (see VIEW_CSS_TEXT), so
464
+ * the panel follows the shell's light/dark theme and its focus-ring contract.
465
+ *
466
+ * @module
467
+ */
468
+ /** How long the outcome toast stays up, in milliseconds. */
469
+ const TOAST_HOLD_MS = 4e3;
470
+ function el(tag, className = "", text) {
471
+ const node = document.createElement(tag);
472
+ if (className.length > 0) node.className = className;
473
+ if (text !== void 0) node.textContent = text;
474
+ return node;
475
+ }
476
+ /** A button carrying inline SVG artwork (the icon modules emit markup). */
477
+ function iconButton(className, markup, label, onClick) {
478
+ const button = el("button", className);
479
+ button.type = "button";
480
+ button.setAttribute("aria-label", label);
481
+ button.title = label;
482
+ button.innerHTML = markup;
483
+ button.addEventListener("click", onClick);
484
+ return button;
485
+ }
486
+ /**
487
+ * Compact a package name to what a person calls it — the shell plugin
488
+ * manager's own rule (`presentation.ts` `shortName`), so both UIs read the
489
+ * same plugin by the same name.
490
+ */
491
+ function shortName(name) {
492
+ return (name.startsWith("@") ? name.slice(name.indexOf("/") + 1) : name).replace(/^dsh-(?:host-|client-)?/, "");
493
+ }
494
+ /** Normalize the server's block reason to a list (it may arrive as a string). */
495
+ function blockedByList(item) {
496
+ const by = item.blockedBy;
497
+ if (Array.isArray(by)) return by.filter((entry) => typeof entry === "string");
498
+ return typeof by === "string" && by.length > 0 ? [by] : [];
499
+ }
500
+ /** The tag tone a state reads as, matching the shell's Tag palette. */
501
+ function stateTone(item) {
502
+ switch (item.state) {
503
+ case "update": return "info";
504
+ case "install": return "warning";
505
+ case "blocked": return "danger";
506
+ case "ignored": return "neutral";
507
+ case "current": return "success";
508
+ }
509
+ }
510
+ function stateLabel(item, copy) {
511
+ switch (item.state) {
512
+ case "current": return copy.state.current;
513
+ case "update": return copy.state.update;
514
+ case "install": return copy.state.install;
515
+ case "ignored": return copy.state.ignored;
516
+ case "blocked": return copy.state.blocked;
517
+ }
518
+ }
519
+ /** The one-line version transition under a card's title. */
520
+ function versionLine(item) {
521
+ switch (item.state) {
522
+ case "current": return item.currentVersion ?? item.availableVersion;
523
+ case "install": return item.availableVersion;
524
+ default: return item.currentVersion === null ? item.availableVersion : `${item.currentVersion} → ${item.availableVersion}`;
525
+ }
526
+ }
527
+ function blockedReason(item, copy) {
528
+ const by = blockedByList(item);
529
+ if (by.includes("host")) return copy.blockedReason.host;
530
+ if (by.includes("hq-edge-version")) return copy.blockedReason["hq-edge-version"];
531
+ if (by.includes("runtime-unknown")) return copy.blockedReason["runtime-unknown"];
532
+ return by.length > 0 ? by.join(", ") : void 0;
533
+ }
534
+ /** One package card: glyph, name with its state tag, version line, end actions. */
535
+ function packageCard(item, copy, installing, store) {
536
+ const card = el("li", "hqpu-card");
537
+ card.dataset.pluginName = item.name;
538
+ card.dataset.state = item.state;
539
+ card.title = `${item.name} · ${versionLine(item)}`;
540
+ const head = el("div", "hqpu-card-head");
541
+ const glyph = el("span", "hqpu-card-icon");
542
+ glyph.setAttribute("aria-hidden", "true");
543
+ glyph.innerHTML = PLUGIN_ICON(24);
544
+ const main = el("div", "hqpu-card-main");
545
+ const titleRow = el("div", "hqpu-title-row");
546
+ const title = el("span", "hqpu-card-title", shortName(item.name));
547
+ const tag = el("span", "hqpu-tag");
548
+ tag.dataset.tone = stateTone(item);
549
+ tag.textContent = stateLabel(item, copy);
550
+ titleRow.append(title, tag);
551
+ const desc = el("span", "hqpu-card-desc", versionLine(item));
552
+ main.append(titleRow, desc);
553
+ head.append(glyph, main);
554
+ const end = el("div", "hqpu-card-end");
555
+ if (item.state === "update" || item.state === "install") {
556
+ const button = el("button", "hqpu-btn hqpu-btn--sm hqpu-btn--primary");
557
+ button.type = "button";
558
+ button.disabled = installing;
559
+ button.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installOne}</span>`;
560
+ button.addEventListener("click", () => {
561
+ runInstall(store, copy);
562
+ });
563
+ end.append(button);
564
+ }
565
+ head.append(end);
566
+ card.append(head);
567
+ if (item.state === "blocked") {
568
+ const reason = blockedReason(item, copy);
569
+ if (reason !== void 0) card.append(el("p", "hqpu-card-reason", reason));
570
+ }
571
+ return card;
572
+ }
573
+ /** A section: group head with its count, then the cards. */
574
+ function group(heading, items, copy, installing, store) {
575
+ if (items.length === 0) return null;
576
+ const section = el("section", "hqpu-group");
577
+ section.dataset.group = heading;
578
+ const head = el("div", "hqpu-group-head");
579
+ head.append(el("h3", "hqpu-group-title", heading));
580
+ head.append(el("span", "hqpu-group-count", copy.groupCount(items.length)));
581
+ const list = el("ul", "hqpu-cards");
582
+ for (const item of items) list.append(packageCard(item, copy, installing, store));
583
+ section.append(head, list);
584
+ return section;
585
+ }
586
+ /** First-read placeholders, sharing the card and text-line layout. */
587
+ function skeleton(copy) {
588
+ const section = el("section", "hqpu-group");
589
+ section.setAttribute("role", "status");
590
+ section.dataset.loading = "";
591
+ const head = el("div", "hqpu-group-head");
592
+ head.setAttribute("aria-hidden", "true");
593
+ const heading = el("span", "hqpu-group-title hqpu-skeleton-text hqpu-skeleton-heading");
594
+ const headingBar = el("span", "hqpu-skeleton-fill hqpu-skeleton-bar");
595
+ heading.append(headingBar);
596
+ head.append(heading);
597
+ const list = el("ul", "hqpu-cards");
598
+ list.setAttribute("aria-hidden", "true");
599
+ for (let index = 0; index < 3; index += 1) {
600
+ const card = el("li", "hqpu-card");
601
+ const cardHead = el("div", "hqpu-card-head");
602
+ const glyph = el("span", "hqpu-card-icon hqpu-skeleton-fill hqpu-skeleton-icon");
603
+ const main = el("div", "hqpu-card-main");
604
+ const titleRow = el("div", "hqpu-title-row");
605
+ const title = el("span", "hqpu-card-title hqpu-skeleton-text hqpu-skeleton-title");
606
+ title.append(el("span", "hqpu-skeleton-fill hqpu-skeleton-bar"));
607
+ titleRow.append(title);
608
+ const desc = el("span", "hqpu-card-desc hqpu-skeleton-text hqpu-skeleton-desc");
609
+ desc.append(el("span", "hqpu-skeleton-fill hqpu-skeleton-bar"));
610
+ main.append(titleRow, desc);
611
+ cardHead.append(glyph, main);
612
+ const end = el("div", "hqpu-card-end");
613
+ end.setAttribute("aria-hidden", "true");
614
+ cardHead.append(end);
615
+ card.append(cardHead);
616
+ list.append(card);
617
+ }
618
+ section.append(head, list);
619
+ section.setAttribute("aria-label", copy.loading);
620
+ return section;
621
+ }
622
+ /** The independent HQ Edge runtime report, as ruled fact rows. */
623
+ function runtimeGroup(report, copy) {
624
+ const section = el("section", "hqpu-group");
625
+ section.dataset.group = "runtime";
626
+ const head = el("div", "hqpu-group-head");
627
+ head.append(el("h3", "hqpu-group-title", copy.runtime.title));
628
+ section.append(head);
629
+ const facts = el("dl", "hqpu-facts");
630
+ const runtime = report.runtime;
631
+ const row = (label, value, code = false) => {
632
+ const wrap = el("div");
633
+ wrap.append(el("dt", "", label));
634
+ const dd = el("dd");
635
+ if (code) {
636
+ const node = el("code", "", value);
637
+ dd.append(node);
638
+ } else dd.textContent = value;
639
+ wrap.append(dd);
640
+ facts.append(wrap);
641
+ };
642
+ if (runtime.configured === false) row(copy.runtime.source, copy.runtime.notConfigured);
643
+ else {
644
+ row(copy.runtime.current, runtime.currentVersion ?? copy.runtime.unknown);
645
+ if (runtime.updateAvailable && runtime.latest != null) row(copy.runtime.latest, runtime.latest);
646
+ if (report.manifestVersion !== null) row(copy.runtime.set, report.manifestVersion, true);
647
+ if (typeof runtime.checkedAt === "string" && runtime.checkedAt.length > 0) row(copy.runtime.checkedAt, new Date(runtime.checkedAt).toLocaleString());
648
+ }
649
+ section.append(facts);
650
+ section.append(el("p", "hqpu-status", copy.runtime.note));
651
+ if (typeof runtime.lastError === "string" && runtime.lastError.length > 0) {
652
+ const line = el("p", "hqpu-status hqpu-status--dot");
653
+ line.setAttribute("role", "status");
654
+ const dot = el("span", "hqpu-dot");
655
+ dot.dataset.state = "error";
656
+ line.append(dot, el("span", "", `${copy.lastError}: ${runtime.lastError}`));
657
+ section.append(line);
658
+ }
659
+ return section;
660
+ }
661
+ function renderList(section, report, copy, installing, store) {
662
+ section.textContent = "";
663
+ if (report === null) {
664
+ section.append(skeleton(copy));
665
+ return;
666
+ }
667
+ if (report.items.length === 0) {
668
+ section.append(el("div", "hqpu-empty", copy.empty));
669
+ return;
670
+ }
671
+ const byState = (state) => report.items.filter((item) => item.state === state);
672
+ const order = [
673
+ [copy.group.actionable, [...byState("update"), ...byState("install")]],
674
+ [copy.group.blocked, byState("blocked")],
675
+ [copy.group.ignored, byState("ignored")],
676
+ [copy.group.current, byState("current")]
677
+ ];
678
+ for (const [heading, items] of order) {
679
+ const node = group(heading, items, copy, installing, store);
680
+ if (node !== null) section.append(node);
681
+ }
682
+ }
683
+ function renderRuntime(section, report, copy) {
684
+ section.textContent = "";
685
+ if (report === null) return;
686
+ section.append(runtimeGroup(report, copy));
687
+ }
688
+ /** Run the install transaction, then keep polling until the state settles. */
689
+ async function runInstall(store, copy) {
690
+ const report = store.report();
691
+ if (report === null) return;
692
+ if (report.manifestVersion === null) {
693
+ showToast(copy.checkFailed, "error");
694
+ return;
695
+ }
696
+ try {
697
+ const result = await installSet(report.manifestVersion);
698
+ if (result.error !== void 0) {
699
+ showToast(`${copy.installError}: ${result.error}`, "error");
700
+ return;
701
+ }
702
+ showToast(copy.installSuccess, "success");
703
+ pollUntilSettled(store);
704
+ } catch (err) {
705
+ showToast(`${copy.installError}: ${err instanceof Error ? err.message : String(err)}`, "error");
706
+ }
707
+ }
708
+ /** The single live toast; a new message replaces the one on screen. */
709
+ let toastHost = null;
710
+ let toastTimer = null;
711
+ function showToast(message, tone) {
712
+ if (toastHost === null) return;
713
+ if (toastTimer !== null) window.clearTimeout(toastTimer);
714
+ toastHost.textContent = "";
715
+ const toast = el("div", "hqpu-toast");
716
+ toast.setAttribute("role", "status");
717
+ const icon = el("span", "hqpu-toast-icon");
718
+ icon.dataset.tone = tone;
719
+ icon.innerHTML = tone === "success" ? CHECK_CIRCLE_ICON(16) : WARNING_ICON(16);
720
+ toast.append(icon, el("span", "hqpu-toast-text", message));
721
+ toastHost.append(toast);
722
+ toastTimer = window.setTimeout(() => {
723
+ toastHost?.replaceChildren();
724
+ toastTimer = null;
725
+ }, TOAST_HOLD_MS);
726
+ }
727
+ function renderActions(bar, report, copy, store, installing) {
728
+ bar.textContent = "";
729
+ if (report === null) return;
730
+ if (installing) {
731
+ const line = el("span", "hqpu-status hqpu-status--dot");
732
+ const spinner = el("span", "hqpu-spinner");
733
+ line.append(spinner, el("span", "", copy.installing));
734
+ bar.append(line);
735
+ return;
736
+ }
737
+ const actionable = report.items.filter((item) => item.state === "update" || item.state === "install").length;
738
+ const hasSet = report.manifestVersion !== null;
739
+ if (actionable === 0) bar.append(el("span", "hqpu-status", copy.nothingToInstall));
740
+ else {
741
+ const installBtn = el("button", "hqpu-btn hqpu-btn--primary");
742
+ installBtn.type = "button";
743
+ installBtn.disabled = !hasSet || report.ignoredSet;
744
+ installBtn.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installAll(actionable)}</span>`;
745
+ installBtn.addEventListener("click", () => {
746
+ runInstall(store, copy);
747
+ });
748
+ bar.append(installBtn);
749
+ }
750
+ if (!hasSet) return;
751
+ if (report.ignoredSet) {
752
+ const unignore = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", copy.unignore);
753
+ unignore.type = "button";
754
+ unignore.addEventListener("click", () => {
755
+ unignoreOffer(report.manifestVersion).then(() => store.refresh()).catch(() => void 0);
756
+ });
757
+ bar.append(unignore);
758
+ } else if (actionable > 0) {
759
+ const ignore = el("button", "hqpu-btn hqpu-btn--ghost hqpu-btn--sm", copy.ignore);
760
+ ignore.type = "button";
761
+ ignore.addEventListener("click", () => {
762
+ ignoreOffer(report.manifestVersion).then(() => store.refresh()).catch(() => void 0);
763
+ });
764
+ bar.append(ignore);
765
+ }
766
+ }
767
+ /** After an install, poll the backend until the update report settles
768
+ * (installing flag clears or DSH restarts under us — the next load re-reads
769
+ * the fresh state). */
770
+ function pollUntilSettled(store) {
771
+ let attempts = 0;
772
+ const tick = () => {
773
+ store.refresh().then((report) => {
774
+ if (store.installing()) return;
775
+ if (report !== null && report.ignoredSet === false && attempts >= 2) return;
776
+ }).catch(() => void 0).finally(() => {
777
+ attempts += 1;
778
+ if (attempts < 10) window.setTimeout(tick, 2e3);
779
+ });
780
+ };
781
+ tick();
782
+ }
783
+ /**
784
+ * Open (or focus) the update panel. Re-renders from the store; when the store
785
+ * is stale the panel first refreshes the report.
786
+ */
787
+ function openUpdateView(store, copy) {
788
+ const existing = document.querySelector("[data-hq-plugin-update-view]");
789
+ if (existing !== null) {
790
+ existing.style.display = "";
791
+ return;
792
+ }
793
+ const overlay = el("div", "hqpu-overlay");
794
+ overlay.setAttribute("data-hq-plugin-update-view", "");
795
+ overlay.addEventListener("click", (ev) => {
796
+ if (ev.target === overlay) onClose();
797
+ });
798
+ const scrim = el("div", "hqpu-scrim");
799
+ const panel = el("div", "hqpu-panel");
800
+ panel.setAttribute("role", "dialog");
801
+ panel.setAttribute("aria-modal", "true");
802
+ panel.setAttribute("aria-label", copy.viewTitle);
803
+ panel.tabIndex = -1;
804
+ const head = el("div", "hqpu-head");
805
+ const headMain = el("div", "hqpu-head-main");
806
+ headMain.append(el("h1", "hqpu-title", copy.viewTitle));
807
+ const intro = el("p", "hqpu-intro");
808
+ intro.append(el("span", "", copy.viewIntro));
809
+ const info = el("button", "hqpu-info");
810
+ info.type = "button";
811
+ info.setAttribute("aria-label", copy.viewInfo);
812
+ info.title = copy.viewInfo;
813
+ info.innerHTML = INFO_ICON(11);
814
+ intro.append(info);
815
+ headMain.append(intro);
816
+ const toolbar = el("div", "hqpu-toolbar");
817
+ const refreshBtn = iconButton("hqpu-icon-btn", REFRESH_ICON(16), copy.refresh, () => {
818
+ store.refresh();
819
+ });
820
+ toolbar.append(refreshBtn);
821
+ const closeBtn = iconButton("hqpu-icon-btn", CLOSE_ICON(16), copy.close, () => {
822
+ onClose();
823
+ });
824
+ toolbar.append(closeBtn);
825
+ head.append(headMain, toolbar);
826
+ panel.append(head);
827
+ const scroll = el("div", "hqpu-scroll");
828
+ const column = el("div", "hqpu-column");
829
+ const listSection = el("div", "hqpu-stack");
830
+ const runtimeSection = el("div", "hqpu-stack");
831
+ const toastSection = el("div", "hqpu-toast-host");
832
+ column.append(listSection, runtimeSection, toastSection);
833
+ scroll.append(column);
834
+ panel.append(scroll);
835
+ const footer = el("div", "hqpu-footer");
836
+ const actionBar = el("div", "hqpu-actions");
837
+ footer.append(actionBar);
838
+ panel.append(footer);
839
+ overlay.append(scrim, panel);
840
+ document.body.appendChild(overlay);
841
+ toastHost = toastSection;
842
+ panel.focus();
843
+ const disposers = [];
844
+ const render = () => {
845
+ const report = store.report();
846
+ const installing = store.installing();
847
+ const stale = report === null;
848
+ refreshBtn.disabled = stale;
849
+ if (stale) {
850
+ listSection.textContent = "";
851
+ listSection.append(skeleton(copy));
852
+ } else renderList(listSection, report, copy, installing, store);
853
+ renderRuntime(runtimeSection, report, copy);
854
+ renderActions(actionBar, report, copy, store, installing);
855
+ };
856
+ const unsubscribe = store.subscribe(render);
857
+ disposers.push(unsubscribe);
858
+ store.refresh().then(() => {});
859
+ const onKeyDown = (ev) => {
860
+ if (ev.key === "Escape") {
861
+ ev.stopPropagation();
862
+ onClose();
863
+ }
864
+ };
865
+ document.addEventListener("keydown", onKeyDown);
866
+ disposers.push(() => document.removeEventListener("keydown", onKeyDown));
867
+ function onClose() {
868
+ for (const dispose of disposers) try {
869
+ dispose();
870
+ } catch {}
871
+ if (toastTimer !== null) window.clearTimeout(toastTimer);
872
+ toastTimer = null;
873
+ if (toastHost === toastSection) toastHost = null;
874
+ overlay.remove();
875
+ }
876
+ render();
877
+ }
878
+ //#endregion
879
+ //#region src/client/index.ts
880
+ /**
881
+ * `@huaqiu/dsh-tool-plugin-update` — browser half.
882
+ *
883
+ * Mounts one sidebar toolbutton in the Huaqiu family block ("插件更新") with a
884
+ * numeric badge equal to the actionable plugin-update count (spec §6.3/§7.1:
885
+ * only compatible `update` + `install` items after host/HQ-Edge filtering and
886
+ * after excluding the ignored offer; runtime updates are NOT counted). Click
887
+ * opens the review/confirm/ignore modal.
888
+ *
889
+ * The authoritative state is the hq-edge report fetched through the plugin's
890
+ * same-origin forwarding routes. A tiny in-memory store owns the latest report
891
+ * + installing flag; the sidebar badge and the modal subscribe to it. No
892
+ * timers beyond the install-poll and an idle 60 s refresh.
893
+ *
894
+ * @module
895
+ */
896
+ /** Stable data attributes identifying the injected entry row. */
897
+ const UPDATE_ENTRY_SELECTOR = "[data-hq-plugin-update-entry]";
898
+ const UPDATE_ENTRY_ATTRIBUTE = "data-hq-plugin-update-entry";
899
+ /** L2 semantic plugin id: rows carry data-dsh-plugin + data-dsh-part. */
900
+ const PLUGIN_ID = "huaqiu-plugin-update";
901
+ /** Row class names (defined by {@link ENTRY_CSS_TEXT}). */
902
+ const ENTRY_CSS = {
903
+ entry: "hqpu-sidebar-entry",
904
+ entryIcon: "hqpu-sidebar-entry__icon",
905
+ entryLabel: "hqpu-sidebar-entry__label",
906
+ entryBadge: "hqpu-sidebar-entry__badge"
907
+ };
908
+ /** Localized row copy: zh is the fallback, matching the family convention. */
909
+ function entryCopy(lang) {
910
+ return lang === "en" ? {
911
+ label: COPY.en.entryLabel,
912
+ tooltip: COPY.en.entryTooltip
913
+ } : {
914
+ label: COPY.zh.entryLabel,
915
+ tooltip: COPY.zh.entryTooltip
916
+ };
917
+ }
918
+ /** The sidebar glyph — the same pinwheel the shell's own Plugins entry draws
919
+ * (`IconPluginPinwheelOutlineRegular`), so both rows read as one feature. */
920
+ const UPDATE_ICON = PLUGIN_ICON(16);
921
+ /** Sidebar entry styles — matched to the family rows (same inset/padding/gap
922
+ * as the shell's own Plugins entry, `SidebarRoot.module.css` `.panelRow`).
923
+ * Every colour rides a `--dsw-alias-*` token so the row follows the shell's
924
+ * light/dark theme; the badge foreground uses the toast label, the shell's
925
+ * "always light ink on a saturated surface" token. */
926
+ const ENTRY_CSS_TEXT = `
927
+ .hqpu-sidebar-entry {
928
+ box-sizing: border-box;
929
+ display: flex;
930
+ align-items: center;
931
+ gap: 8px;
932
+ width: 100%;
933
+ margin: 0 2px;
934
+ min-height: 36px;
935
+ padding: 7px 8px;
936
+ background: transparent;
937
+ border: none;
938
+ border-radius: var(--dsw-radius-md);
939
+ color: var(--dsw-alias-label-primary);
940
+ cursor: pointer;
941
+ font: inherit;
942
+ line-height: 22px;
943
+ text-align: left;
944
+ white-space: nowrap;
945
+ }
946
+ .hqpu-sidebar-entry:hover {
947
+ background: var(--dsw-alias-interactive-bg-hover);
948
+ }
949
+ .hqpu-sidebar-entry[data-active] {
950
+ background: var(--dsw-alias-interactive-bg-hover);
951
+ color: var(--dsw-alias-label-primary);
952
+ }
953
+ .hqpu-sidebar-entry:focus-visible {
954
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
955
+ outline-offset: -2px;
956
+ }
957
+ .hqpu-sidebar-entry__icon {
958
+ display: inline-flex;
959
+ align-items: center;
960
+ justify-content: center;
961
+ flex: none;
962
+ }
963
+ .hqpu-sidebar-entry__icon svg {
964
+ display: block;
965
+ width: 16px;
966
+ height: 16px;
967
+ }
968
+ .hqpu-sidebar-entry__label {
969
+ min-width: 0;
970
+ overflow: hidden;
971
+ text-overflow: ellipsis;
972
+ flex: 1;
973
+ }
974
+ .hqpu-sidebar-entry__badge {
975
+ flex: none;
976
+ min-width: 16px;
977
+ height: 16px;
978
+ padding: 0 5px;
979
+ border-radius: 8px;
980
+ background: var(--dsw-alias-state-error-primary);
981
+ color: var(--dsw-alias-toast-label);
982
+ font-size: 11px;
983
+ line-height: 16px;
984
+ font-weight: 500;
985
+ text-align: center;
986
+ font-variant-numeric: tabular-nums;
987
+ }
988
+ /* Collapsed rail: icon-only, centered — same 36px square the shell uses. */
989
+ [data-sidebar-collapsed] .hqpu-sidebar-entry {
990
+ justify-content: center;
991
+ width: 36px;
992
+ height: 36px;
993
+ margin: 0;
994
+ padding: 0;
995
+ }
996
+ [data-sidebar-collapsed] .hqpu-sidebar-entry__icon svg {
997
+ width: 18px;
998
+ height: 18px;
999
+ }
1000
+ [data-sidebar-collapsed] .hqpu-sidebar-entry__label,
1001
+ [data-sidebar-collapsed] .hqpu-sidebar-entry__badge {
1002
+ display: none;
1003
+ }
1004
+ `;
1005
+ /**
1006
+ * View styles (hqpu-*). Plain overlay + panel, drawn with the SAME design
1007
+ * tokens and geometry the shell's own plugin manager uses
1008
+ * (`ui-plugin-manager/src/client/PluginManagerPage.module.css` +
1009
+ * `ui-primitives/src/{Modal,Toast,Tag,StateDot}.module.css`), so this page
1010
+ * reads as part of the product rather than a bolt-on.
1011
+ *
1012
+ * Two rules keep it honest across themes:
1013
+ * 1. every colour is a `--dsw-alias-*` token — no literal hex/rgb, so light
1014
+ * and dark both come out right (the old sheet invented
1015
+ * `--dsw-alias-bg-surface` / `--dsw-alias-line-border` / `--dsw-alias-bg-badge`,
1016
+ * none of which exist, and so always fell back to a white panel);
1017
+ * 2. `color-mix()` tints come from the state token itself, so a palette
1018
+ * change moves fill and ink together.
1019
+ *
1020
+ * The chrome (panel, scrim, header, footer) follows `ui-primitives/Modal`,
1021
+ * because this view IS a dialog; the body follows `PluginManagerPage`, because
1022
+ * that is the content it heads. The one token the bundled shell does not
1023
+ * publish is backfilled by {@link ensureDesignTokens} — see the note there.
1024
+ */
1025
+ const VIEW_CSS_TEXT = `
1026
+ /* ── Overlay + panel (ui-primitives Modal.module.css) ─────────────────── */
1027
+ .hqpu-overlay {
1028
+ position: fixed;
1029
+ inset: 0;
1030
+ z-index: 1000;
1031
+ display: flex;
1032
+ align-items: center;
1033
+ justify-content: center;
1034
+ /* The frame's own top inset keeps a tall card clear of the window chrome. */
1035
+ padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;
1036
+ }
1037
+ .hqpu-scrim {
1038
+ position: absolute;
1039
+ /* Only paint excludes the native caption; the root still blocks clicks. */
1040
+ inset: var(--dsh-frame-chrome-top, 0px) 0 0;
1041
+ background: var(--dsw-alias-bg-mask-1);
1042
+ animation: hqpu-fade var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease);
1043
+ }
1044
+ .hqpu-panel {
1045
+ position: relative;
1046
+ z-index: 1;
1047
+ box-sizing: border-box;
1048
+ display: flex;
1049
+ flex-direction: column;
1050
+ gap: 20px;
1051
+ width: min(920px, 100%);
1052
+ max-height: 100%;
1053
+ overflow: hidden;
1054
+ border: 0;
1055
+ border-radius: var(--dsw-radius-panel);
1056
+ background: var(--dsw-alias-bg-layer-2);
1057
+ box-shadow: var(--dsw-elevation-prominent);
1058
+ color: var(--dsw-alias-label-primary);
1059
+ animation: hqpu-panel-in var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease);
1060
+ }
1061
+ .hqpu-panel:focus {
1062
+ outline: none;
1063
+ }
1064
+ @keyframes hqpu-fade {
1065
+ from { opacity: 0; }
1066
+ to { opacity: 1; }
1067
+ }
1068
+ @keyframes hqpu-panel-in {
1069
+ from { opacity: 0; }
1070
+ to { opacity: 1; }
1071
+ }
1072
+
1073
+ /* ── Head row (ui-primitives Modal .header) ─────────────────────────────── */
1074
+ .hqpu-head {
1075
+ display: flex;
1076
+ flex: none;
1077
+ flex-wrap: wrap;
1078
+ align-items: flex-start;
1079
+ justify-content: space-between;
1080
+ gap: 16px;
1081
+ box-sizing: border-box;
1082
+ /* The official Title row: 22/14/12/24 — the toolbar sits 10px further right
1083
+ * than the body content on purpose, matching the shell's own close button. */
1084
+ padding: 22px 14px 12px 24px;
1085
+ }
1086
+ .hqpu-head-main {
1087
+ min-width: 0;
1088
+ max-width: 100%;
1089
+ }
1090
+ .hqpu-title {
1091
+ margin: 0;
1092
+ font-size: 20px;
1093
+ font-weight: 500;
1094
+ line-height: 28px;
1095
+ color: var(--dsw-alias-label-primary);
1096
+ }
1097
+ .hqpu-intro {
1098
+ display: flex;
1099
+ align-items: center;
1100
+ gap: 4px;
1101
+ margin: 4px 0 0;
1102
+ font-size: 13px;
1103
+ line-height: 20px;
1104
+ color: var(--dsw-alias-label-secondary);
1105
+ }
1106
+ .hqpu-info {
1107
+ display: inline-flex;
1108
+ align-items: center;
1109
+ justify-content: center;
1110
+ flex: none;
1111
+ width: 20px;
1112
+ height: 20px;
1113
+ padding: 0;
1114
+ border: 0;
1115
+ border-radius: var(--dsw-radius-sm);
1116
+ background: transparent;
1117
+ color: var(--dsw-alias-label-caption);
1118
+ cursor: pointer;
1119
+ }
1120
+ .hqpu-info:hover {
1121
+ background: var(--dsw-alias-interactive-bg-hover);
1122
+ color: var(--dsw-alias-label-secondary);
1123
+ }
1124
+ .hqpu-info:focus-visible {
1125
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
1126
+ outline-offset: 1px;
1127
+ }
1128
+ .hqpu-toolbar {
1129
+ display: flex;
1130
+ flex: none;
1131
+ align-items: center;
1132
+ gap: 16px;
1133
+ }
1134
+
1135
+ /* ── Scrolling body: the page's centred column ────────────────────────── */
1136
+ .hqpu-scroll {
1137
+ flex: 1;
1138
+ min-height: 0;
1139
+ overflow-y: auto;
1140
+ box-sizing: border-box;
1141
+ /* The panel's 20px gap already separates this from the head and footer. */
1142
+ padding: 0 clamp(16px, 3vw, 32px) 4px;
1143
+ /* Anything that scrolls inside the panel draws the l2 thumb. */
1144
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
1145
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
1146
+ }
1147
+ .hqpu-column {
1148
+ display: flex;
1149
+ flex-direction: column;
1150
+ gap: 24px;
1151
+ width: 100%;
1152
+ max-width: 880px;
1153
+ margin: 0 auto;
1154
+ }
1155
+ /* A stack of groups (or the single toast host) inside the column. */
1156
+ .hqpu-stack {
1157
+ display: flex;
1158
+ flex-direction: column;
1159
+ gap: 24px;
1160
+ }
1161
+ .hqpu-toast-host:empty {
1162
+ display: none;
1163
+ }
1164
+
1165
+ /* ── Groups (PluginManagerPage .group) ────────────────────────────────── */
1166
+ .hqpu-group {
1167
+ display: flex;
1168
+ flex-direction: column;
1169
+ gap: 8px;
1170
+ }
1171
+ .hqpu-group-head {
1172
+ display: flex;
1173
+ align-items: center;
1174
+ gap: 8px;
1175
+ }
1176
+ .hqpu-group-title {
1177
+ flex: 1;
1178
+ min-width: 0;
1179
+ margin: 0;
1180
+ font-size: 14px;
1181
+ font-weight: 500;
1182
+ line-height: 22px;
1183
+ color: var(--dsw-alias-label-primary);
1184
+ }
1185
+ .hqpu-group-count {
1186
+ flex: none;
1187
+ font-size: 12px;
1188
+ line-height: 18px;
1189
+ color: var(--dsw-alias-label-tertiary);
1190
+ font-variant-numeric: tabular-nums;
1191
+ }
1192
+
1193
+ /* ── Cards (PluginManagerPage .card) ──────────────────────────────────── */
1194
+ .hqpu-cards {
1195
+ margin: 0;
1196
+ padding: 0;
1197
+ list-style: none;
1198
+ display: flex;
1199
+ flex-direction: column;
1200
+ gap: 2px;
1201
+ }
1202
+ .hqpu-card {
1203
+ min-width: 0;
1204
+ /* The hover surface reaches 8px past the content on both sides. */
1205
+ margin: 0 -8px;
1206
+ border-radius: var(--dsw-radius-xl);
1207
+ }
1208
+ .hqpu-card:hover {
1209
+ background: var(--dsw-alias-interactive-bg-hover);
1210
+ }
1211
+ .hqpu-card-head {
1212
+ display: flex;
1213
+ align-items: center;
1214
+ gap: 14px;
1215
+ padding: 8px;
1216
+ }
1217
+ .hqpu-card-icon {
1218
+ display: inline-flex;
1219
+ flex: none;
1220
+ align-items: center;
1221
+ justify-content: center;
1222
+ width: 48px;
1223
+ height: 48px;
1224
+ border: 0.5px solid var(--dsw-alias-border-l3);
1225
+ border-radius: var(--dsw-radius-lg);
1226
+ color: var(--dsw-alias-label-secondary);
1227
+ }
1228
+ .hqpu-card-icon svg {
1229
+ display: block;
1230
+ width: 24px;
1231
+ height: 24px;
1232
+ }
1233
+ .hqpu-card-main {
1234
+ flex: 1;
1235
+ min-width: 0;
1236
+ display: flex;
1237
+ flex-direction: column;
1238
+ gap: 4px;
1239
+ }
1240
+ .hqpu-title-row {
1241
+ display: flex;
1242
+ align-items: center;
1243
+ flex-wrap: wrap;
1244
+ gap: 8px;
1245
+ min-width: 0;
1246
+ }
1247
+ .hqpu-card-title {
1248
+ min-width: 0;
1249
+ font-size: 14px;
1250
+ font-weight: 500;
1251
+ line-height: 20px;
1252
+ color: var(--dsw-alias-label-primary);
1253
+ overflow: hidden;
1254
+ text-overflow: ellipsis;
1255
+ white-space: nowrap;
1256
+ }
1257
+ .hqpu-card-desc {
1258
+ font-size: 13px;
1259
+ line-height: 18px;
1260
+ color: var(--dsw-alias-label-tertiary);
1261
+ display: -webkit-box;
1262
+ -webkit-box-orient: vertical;
1263
+ -webkit-line-clamp: 1;
1264
+ overflow: hidden;
1265
+ overflow-wrap: anywhere;
1266
+ }
1267
+ .hqpu-card-end {
1268
+ position: relative;
1269
+ z-index: 1;
1270
+ display: inline-flex;
1271
+ flex: none;
1272
+ align-items: center;
1273
+ gap: 8px;
1274
+ }
1275
+ .hqpu-card-reason {
1276
+ margin: 0;
1277
+ padding: 0 8px 8px 70px;
1278
+ font-size: 12px;
1279
+ line-height: 18px;
1280
+ color: var(--dsw-alias-state-error-primary);
1281
+ overflow-wrap: anywhere;
1282
+ }
1283
+
1284
+ /* ── Tags (ui-primitives Tag.module.css) ──────────────────────────────── */
1285
+ .hqpu-tag {
1286
+ display: inline-flex;
1287
+ align-items: center;
1288
+ flex: none;
1289
+ border-radius: 999px;
1290
+ corner-shape: round;
1291
+ padding: 1px 8px;
1292
+ font-size: 11px;
1293
+ font-weight: 500;
1294
+ line-height: 17px;
1295
+ white-space: nowrap;
1296
+ }
1297
+ .hqpu-tag[data-tone='success'] {
1298
+ background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);
1299
+ color: var(--dsw-alias-state-success-primary);
1300
+ }
1301
+ .hqpu-tag[data-tone='info'] {
1302
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent);
1303
+ color: var(--dsw-alias-state-business-primary);
1304
+ }
1305
+ .hqpu-tag[data-tone='warning'] {
1306
+ background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);
1307
+ color: var(--dsw-alias-state-warn-primary);
1308
+ }
1309
+ .hqpu-tag[data-tone='danger'] {
1310
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);
1311
+ color: var(--dsw-alias-state-error-primary);
1312
+ }
1313
+ .hqpu-tag[data-tone='neutral'] {
1314
+ background: var(--dsw-alias-bg-module-platform);
1315
+ color: var(--dsw-alias-label-secondary);
1316
+ }
1317
+
1318
+ /* ── Fact rows (PluginManagerPage .facts / .rows) ─────────────────────── */
1319
+ .hqpu-facts {
1320
+ margin: 0;
1321
+ display: flex;
1322
+ flex-direction: column;
1323
+ }
1324
+ .hqpu-facts > div {
1325
+ display: flex;
1326
+ align-items: center;
1327
+ justify-content: space-between;
1328
+ gap: 16px;
1329
+ min-height: 44px;
1330
+ padding: 8px 2px;
1331
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
1332
+ }
1333
+ .hqpu-facts > div:last-child {
1334
+ border-bottom: 0;
1335
+ }
1336
+ .hqpu-facts dt {
1337
+ flex: none;
1338
+ font-size: 13px;
1339
+ line-height: 20px;
1340
+ color: var(--dsw-alias-label-secondary);
1341
+ }
1342
+ .hqpu-facts dd {
1343
+ min-width: 0;
1344
+ margin: 0;
1345
+ font-size: 13px;
1346
+ line-height: 20px;
1347
+ text-align: end;
1348
+ overflow-wrap: anywhere;
1349
+ color: var(--dsw-alias-label-primary);
1350
+ }
1351
+ .hqpu-facts code {
1352
+ padding: 2px 6px;
1353
+ border-radius: var(--dsw-radius-xs);
1354
+ background: var(--dsw-alias-bg-module-platform);
1355
+ font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1356
+ font-size: 12px;
1357
+ }
1358
+
1359
+ /* ── Banner (PluginManagerPage .installSafety) ────────────────────────── */
1360
+ .hqpu-banner {
1361
+ display: flex;
1362
+ align-items: flex-start;
1363
+ gap: 8px;
1364
+ padding: 10px 12px;
1365
+ border-radius: var(--dsw-radius-md);
1366
+ background: var(--dsw-alias-state-warn-tertiary);
1367
+ font-size: 12.5px;
1368
+ line-height: 18px;
1369
+ color: var(--dsw-alias-label-secondary);
1370
+ }
1371
+ .hqpu-banner > svg {
1372
+ flex: none;
1373
+ margin-top: 2px;
1374
+ color: var(--dsw-alias-state-warn-primary);
1375
+ }
1376
+
1377
+ /* ── Status, empty, failure (PluginManagerPage .status / .empty / .failure) */
1378
+ .hqpu-status {
1379
+ margin: 0;
1380
+ font-size: 13px;
1381
+ line-height: 20px;
1382
+ color: var(--dsw-alias-label-tertiary);
1383
+ }
1384
+ .hqpu-status--dot {
1385
+ display: inline-flex;
1386
+ align-items: center;
1387
+ gap: 6px;
1388
+ }
1389
+ .hqpu-failure {
1390
+ display: flex;
1391
+ align-items: center;
1392
+ gap: 10px;
1393
+ color: var(--dsw-alias-state-error-primary);
1394
+ }
1395
+ .hqpu-empty {
1396
+ padding: 32px 0;
1397
+ text-align: center;
1398
+ font-size: 13px;
1399
+ line-height: 20px;
1400
+ color: var(--dsw-alias-label-tertiary);
1401
+ }
1402
+
1403
+ /* ── State dot + spinner (ui-primitives StateDot.module.css) ──────────── */
1404
+ .hqpu-dot {
1405
+ position: relative;
1406
+ display: inline-block;
1407
+ flex: none;
1408
+ width: 10px;
1409
+ height: 10px;
1410
+ }
1411
+ .hqpu-dot::after {
1412
+ content: '';
1413
+ position: absolute;
1414
+ inset: 20%;
1415
+ border-radius: 50%;
1416
+ corner-shape: round;
1417
+ background: currentColor;
1418
+ }
1419
+ .hqpu-dot[data-state='done'] { color: var(--dsw-alias-state-success-primary); }
1420
+ .hqpu-dot[data-state='warning'] { color: var(--dsw-alias-state-warn-primary); }
1421
+ .hqpu-dot[data-state='error'] { color: var(--dsw-alias-state-error-primary); }
1422
+ .hqpu-dot[data-state='idle'] { color: var(--dsw-alias-state-idle-primary); }
1423
+ .hqpu-spinner {
1424
+ flex: none;
1425
+ width: 14px;
1426
+ height: 14px;
1427
+ border-radius: 50%;
1428
+ border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
1429
+ border-top-color: currentColor;
1430
+ animation: hqpu-spin 1.5s linear infinite;
1431
+ }
1432
+ @keyframes hqpu-spin {
1433
+ to { transform: rotate(360deg); }
1434
+ }
1435
+
1436
+ /* ── Skeleton (PluginManagerPage .skeleton*) ──────────────────────────── */
1437
+ .hqpu-skeleton-fill {
1438
+ background: var(--dsw-alias-bg-skeleton);
1439
+ animation: hqpu-skeleton 2s cubic-bezier(0.36, 0, 0.64, 1) infinite;
1440
+ }
1441
+ .hqpu-skeleton-text {
1442
+ display: flex;
1443
+ align-items: center;
1444
+ height: 1lh;
1445
+ }
1446
+ .hqpu-skeleton-bar {
1447
+ width: 100%;
1448
+ height: 12px;
1449
+ border-radius: var(--dsw-radius-xs);
1450
+ }
1451
+ .hqpu-skeleton-heading { width: 96px; }
1452
+ .hqpu-skeleton-title { width: min(144px, 60%); }
1453
+ .hqpu-skeleton-desc { width: min(280px, 85%); }
1454
+ .hqpu-skeleton-icon { border-color: transparent; }
1455
+ @keyframes hqpu-skeleton {
1456
+ 0% { opacity: 1; }
1457
+ 40% { opacity: 0.6; }
1458
+ 80%, 100% { opacity: 1; }
1459
+ }
1460
+
1461
+ /* ── Toast (ui-primitives Toast.module.css, held inside the panel) ────── */
1462
+ .hqpu-toast {
1463
+ display: flex;
1464
+ align-items: center;
1465
+ gap: 10px;
1466
+ padding: 12px 16px;
1467
+ border-radius: var(--dsw-radius-lg);
1468
+ background: var(--dsw-alias-toast-bg);
1469
+ color: var(--dsw-alias-toast-label);
1470
+ font-size: 14px;
1471
+ line-height: 22px;
1472
+ box-shadow: var(--dsw-shadow-lv3);
1473
+ animation: hqpu-toast-in 160ms ease-out;
1474
+ }
1475
+ .hqpu-toast-icon {
1476
+ display: grid;
1477
+ place-items: center;
1478
+ flex: none;
1479
+ color: var(--dsw-alias-state-warn-label);
1480
+ }
1481
+ .hqpu-toast-icon[data-tone='success'] { color: var(--dsw-alias-state-success-primary); }
1482
+ .hqpu-toast-text { min-width: 0; }
1483
+ @keyframes hqpu-toast-in {
1484
+ from { opacity: 0; transform: translateY(-6px); }
1485
+ to { opacity: 1; transform: none; }
1486
+ }
1487
+
1488
+ /* ── Buttons (PluginManagerPage .addButton / .iconButton) ─────────────── */
1489
+ .hqpu-icon-btn {
1490
+ display: inline-flex;
1491
+ align-items: center;
1492
+ justify-content: center;
1493
+ flex: none;
1494
+ width: 28px;
1495
+ height: 28px;
1496
+ padding: 0;
1497
+ border: 0;
1498
+ border-radius: var(--dsw-radius-sm);
1499
+ background: transparent;
1500
+ color: var(--dsw-alias-label-caption);
1501
+ cursor: pointer;
1502
+ }
1503
+ .hqpu-icon-btn svg { display: block; }
1504
+ .hqpu-icon-btn:hover:not(:disabled) {
1505
+ background: var(--dsw-alias-interactive-bg-hover);
1506
+ color: var(--dsw-alias-label-secondary);
1507
+ }
1508
+ .hqpu-icon-btn:disabled {
1509
+ opacity: 0.5;
1510
+ cursor: default;
1511
+ }
1512
+ .hqpu-icon-btn:focus-visible {
1513
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
1514
+ outline-offset: 1px;
1515
+ }
1516
+ .hqpu-btn {
1517
+ box-sizing: border-box;
1518
+ display: inline-flex;
1519
+ align-items: center;
1520
+ justify-content: center;
1521
+ gap: 6px;
1522
+ height: 32px;
1523
+ padding: 0 12px;
1524
+ border: 0;
1525
+ border-radius: var(--dsw-radius-md);
1526
+ background: var(--dsw-alias-bg-layer-3);
1527
+ color: var(--dsw-alias-label-primary);
1528
+ font: inherit;
1529
+ font-size: 13px;
1530
+ line-height: 20px;
1531
+ cursor: pointer;
1532
+ }
1533
+ .hqpu-btn svg { display: block; }
1534
+ .hqpu-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-solid); }
1535
+ .hqpu-btn:disabled { opacity: 0.5; cursor: default; }
1536
+ .hqpu-btn:focus-visible {
1537
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
1538
+ outline-offset: 1px;
1539
+ }
1540
+ .hqpu-btn--primary {
1541
+ background: var(--dsw-alias-button-primary-fill);
1542
+ color: var(--dsw-alias-label-primary-foreground);
1543
+ }
1544
+ .hqpu-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
1545
+ .hqpu-btn--outline {
1546
+ background: transparent;
1547
+ border: 0.5px solid var(--dsw-alias-border-l4);
1548
+ color: var(--dsw-alias-label-secondary);
1549
+ }
1550
+ .hqpu-btn--outline:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1551
+ .hqpu-btn--ghost { background: transparent; color: var(--dsw-alias-label-secondary); }
1552
+ .hqpu-btn--ghost:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1553
+ .hqpu-btn--danger { color: var(--dsw-alias-state-error-primary); }
1554
+ .hqpu-btn--sm { height: 28px; padding: 0 10px; font-size: 12.5px; }
1555
+ .hqpu-actions {
1556
+ display: flex;
1557
+ align-items: center;
1558
+ flex-wrap: wrap;
1559
+ gap: 8px;
1560
+ }
1561
+
1562
+ /* ── Footer action row (ui-primitives Modal .footer) ───────────────────── */
1563
+ .hqpu-footer {
1564
+ display: flex;
1565
+ flex: none;
1566
+ align-items: center;
1567
+ justify-content: flex-end;
1568
+ gap: 8px;
1569
+ padding: 0 24px 20px;
1570
+ }
1571
+
1572
+ .hqpu-visually-hidden {
1573
+ position: absolute;
1574
+ width: 1px;
1575
+ height: 1px;
1576
+ margin: -1px;
1577
+ padding: 0;
1578
+ overflow: hidden;
1579
+ clip-path: inset(50%);
1580
+ white-space: nowrap;
1581
+ border: 0;
1582
+ }
1583
+
1584
+ /* ── Reduced motion: drop every decorative animation ──────────────────── */
1585
+ @media (prefers-reduced-motion: reduce) {
1586
+ .hqpu-scrim,
1587
+ .hqpu-panel,
1588
+ .hqpu-skeleton-fill,
1589
+ .hqpu-spinner,
1590
+ .hqpu-toast {
1591
+ animation: none;
1592
+ }
1593
+ }
1594
+ `;
1595
+ const STYLE_TAG_ID = "data-plugin-css=\"huaqiu-plugin-update\"";
1596
+ /**
1597
+ * Design tokens the bundled DSH shell may not publish.
1598
+ *
1599
+ * hq-edge ships an older `ui-theme` build. A live probe of the running shell
1600
+ * (both themes) shows 13 of the tokens this stylesheet reads are absent:
1601
+ * `--dsw-radius-{xs,sm,md,lg,xl,panel}`, `--dsw-focus-ring-width`,
1602
+ * `--dsw-focus-ring-color`, `--dsw-alias-toast-label`,
1603
+ * `--dsw-alias-state-idle-primary`, `--dsw-font-mono`,
1604
+ * `--dsh-frame-overlay-top` and `--dsh-frame-chrome-top`.
1605
+ *
1606
+ * An unresolved `var()` is not an error — it silently degrades the property to
1607
+ * its initial value. That is how this panel once rendered with
1608
+ * `border-radius: 0px` and a 3px `auto` focus ring while every colour was
1609
+ * already correct.
1610
+ *
1611
+ * The last three (`--dsw-font-mono`, `--dsh-frame-*`) are deliberately NOT
1612
+ * backfilled: the shell's own primitives rely on the inline `var(…, fallback)`
1613
+ * form for those (`Modal.module.css`, `PluginManagerPage.module.css`), so this
1614
+ * sheet does too. Everything else is backfilled below with the canonical value
1615
+ * from `ui-theme/src/styles/{base,focus,design-platform}.css`.
1616
+ *
1617
+ * Values are feature-detected at runtime and scoped to this plugin's own roots
1618
+ * rather than `:root`, so a newer shell always wins and filling a gap can never
1619
+ * restyle the host.
1620
+ */
1621
+ /** The two roots this plugin styles: the view overlay (built in
1622
+ * update-view.ts) and the sidebar row ({@link ENTRY_CSS}.entry). */
1623
+ const STYLE_ROOTS = ".hqpu-overlay, .hqpu-sidebar-entry";
1624
+ /** The shell's dark-theme carrier — audited live as `body[data-ds-dark-theme]`. */
1625
+ const DARK_THEME_SELECTOR = "body[data-ds-dark-theme]";
1626
+ /** Same roots, one rule per theme, for tokens whose official value flips. */
1627
+ const DARK_STYLE_ROOTS = STYLE_ROOTS.split(",").map((root) => `${DARK_THEME_SELECTOR} ${root.trim()}`).join(",");
1628
+ const DESIGN_TOKEN_FALLBACKS = [
1629
+ {
1630
+ token: "--dsw-radius-xs",
1631
+ value: "4px"
1632
+ },
1633
+ {
1634
+ token: "--dsw-radius-sm",
1635
+ value: "8px"
1636
+ },
1637
+ {
1638
+ token: "--dsw-radius-md",
1639
+ value: "12px"
1640
+ },
1641
+ {
1642
+ token: "--dsw-radius-lg",
1643
+ value: "16px"
1644
+ },
1645
+ {
1646
+ token: "--dsw-radius-xl",
1647
+ value: "20px"
1648
+ },
1649
+ {
1650
+ token: "--dsw-radius-panel",
1651
+ value: "28px"
1652
+ },
1653
+ {
1654
+ token: "--dsw-focus-ring-width",
1655
+ value: "2px"
1656
+ },
1657
+ {
1658
+ token: "--dsw-focus-ring-color",
1659
+ value: "var(--dsw-alias-state-business-primary, #C7000B)"
1660
+ },
1661
+ {
1662
+ token: "--dsw-alias-toast-label",
1663
+ value: "var(--dsw-static-neutral-bluish-00, #fff)"
1664
+ },
1665
+ {
1666
+ token: "--dsw-alias-state-idle-primary",
1667
+ value: {
1668
+ light: "var(--dsw-static-neutral-300, #d4d4d4)",
1669
+ dark: "var(--dsw-static-neutral-600, #545557)"
1670
+ }
1671
+ }
1672
+ ];
1673
+ const TOKEN_STYLE_TAG_ID = "data-plugin-css=\"huaqiu-plugin-update-tokens\"";
1674
+ /**
1675
+ * Publish canonical values for the design tokens the shell is missing.
1676
+ *
1677
+ * Idempotent and cheap: safe to call on every open, so a shell that publishes
1678
+ * its tokens late still wins (the stale fallbacks are removed in that case).
1679
+ */
1680
+ function ensureDesignTokens() {
1681
+ if (typeof document === "undefined" || document.body === null) return;
1682
+ const probe = getComputedStyle(document.body);
1683
+ const missing = DESIGN_TOKEN_FALLBACKS.filter((entry) => probe.getPropertyValue(entry.token).trim() === "");
1684
+ const existing = document.querySelector(`style[${TOKEN_STYLE_TAG_ID}]`);
1685
+ if (missing.length === 0) {
1686
+ existing?.remove();
1687
+ return;
1688
+ }
1689
+ const flat = missing.filter((entry) => typeof entry.value === "string");
1690
+ const themed = missing.filter((entry) => typeof entry.value !== "string");
1691
+ const rules = [];
1692
+ const lightDecls = [...flat.map((entry) => ` ${entry.token}: ${entry.value};`), ...themed.map((entry) => ` ${entry.token}: ${entry.value.light};`)];
1693
+ rules.push(`${STYLE_ROOTS} {\n${lightDecls.join("\n")}\n}`);
1694
+ if (themed.length > 0) {
1695
+ const darkDecls = themed.map((entry) => ` ${entry.token}: ${entry.value.dark};`);
1696
+ rules.push(`${DARK_STYLE_ROOTS} {\n${darkDecls.join("\n")}\n}`);
1697
+ }
1698
+ const tag = existing ?? document.createElement("style");
1699
+ tag.dataset.pluginCss = "huaqiu-plugin-update-tokens";
1700
+ tag.textContent = rules.join("\n");
1701
+ if (tag.parentNode === null) document.head.appendChild(tag);
1702
+ }
1703
+ /** Inject styles once (idempotent). */
1704
+ function injectUpdateStyles() {
1705
+ if (typeof document === "undefined") return;
1706
+ if (document.querySelector(`style[${STYLE_TAG_ID}]`) !== null) return;
1707
+ const tag = document.createElement("style");
1708
+ tag.dataset.pluginCss = "huaqiu-plugin-update";
1709
+ tag.textContent = `${ENTRY_CSS_TEXT}\n${VIEW_CSS_TEXT}`;
1710
+ document.head.appendChild(tag);
1711
+ }
1712
+ /** In-memory update-report store shared by the badge and the modal. */
1713
+ var UpdateStore = class {
1714
+ current = null;
1715
+ installingFlag = false;
1716
+ listeners = /* @__PURE__ */ new Set();
1717
+ refreshTimer = null;
1718
+ report() {
1719
+ return this.current;
1720
+ }
1721
+ installing() {
1722
+ return this.installingFlag;
1723
+ }
1724
+ subscribe(listener) {
1725
+ this.listeners.add(listener);
1726
+ return () => {
1727
+ this.listeners.delete(listener);
1728
+ };
1729
+ }
1730
+ /** Badge count per spec §6.3: actionable `update` + `install`, excluding the
1731
+ * ignored offer (the server already folds ignore into item states; keep the
1732
+ * client-side filter as a defensive second pass). */
1733
+ badgeCount() {
1734
+ const report = this.current;
1735
+ if (report === null) return 0;
1736
+ return report.items.filter((item) => item.state === "update" || item.state === "install").length;
1737
+ }
1738
+ emit() {
1739
+ for (const listener of [...this.listeners]) try {
1740
+ listener();
1741
+ } catch {}
1742
+ }
1743
+ async refresh() {
1744
+ try {
1745
+ const report = await fetchReport();
1746
+ this.current = report;
1747
+ this.emit();
1748
+ return report;
1749
+ } catch {
1750
+ return this.current;
1751
+ }
1752
+ }
1753
+ async check() {
1754
+ try {
1755
+ const report = await checkUpdates();
1756
+ this.current = report;
1757
+ this.emit();
1758
+ return report;
1759
+ } catch {
1760
+ return this.current;
1761
+ }
1762
+ }
1763
+ /** Mark the set as installing (set by the modal before the restart). */
1764
+ setInstalling(flag) {
1765
+ this.installingFlag = flag;
1766
+ this.emit();
1767
+ }
1768
+ /** Idle refresh while mounted (60 s) — no overlapping poll loop. */
1769
+ startIdleRefresh() {
1770
+ if (this.refreshTimer !== null) return;
1771
+ this.refreshTimer = window.setInterval(() => {
1772
+ this.refresh();
1773
+ }, 6e4);
1774
+ }
1775
+ stop() {
1776
+ if (this.refreshTimer !== null) {
1777
+ window.clearInterval(this.refreshTimer);
1778
+ this.refreshTimer = null;
1779
+ }
1780
+ this.listeners.clear();
1781
+ }
1782
+ };
1783
+ /**
1784
+ * Client cordis inject: no service dependencies — the plugin reads the
1785
+ * hq-edge report purely through same-origin fetch (forwarded by its own node
1786
+ * half). `inject` stays empty so the loader never waits on another service.
1787
+ */
1788
+ const inject = [];
1789
+ /**
1790
+ * Client plugin body — mount the sidebar toolbutton + review modal.
1791
+ * @param ctx - real cordis client context.
1792
+ * @returns disposer removing the row and all subscriptions.
1793
+ */
1794
+ function apply(ctx) {
1795
+ if (typeof document === "undefined") return () => {};
1796
+ injectUpdateStyles();
1797
+ ensureDesignTokens();
1798
+ const store = new UpdateStore();
1799
+ const copy = {
1800
+ zh: COPY.zh,
1801
+ en: COPY.en
1802
+ };
1803
+ const copyFor = () => locale === "en" ? copy.en : copy.zh;
1804
+ let locale = detectHostLocale();
1805
+ const localeSubscribers = /* @__PURE__ */ new Set();
1806
+ const refreshSubscribe = (listener) => {
1807
+ localeSubscribers.add(listener);
1808
+ return () => {
1809
+ localeSubscribers.delete(listener);
1810
+ };
1811
+ };
1812
+ const unsubscribeLocale = observeHostLocale((next) => {
1813
+ locale = next;
1814
+ for (const fn of [...localeSubscribers]) try {
1815
+ fn();
1816
+ } catch {}
1817
+ });
1818
+ const labelState = {
1819
+ label: entryCopy(locale).label,
1820
+ tooltip: entryCopy(locale).tooltip
1821
+ };
1822
+ const openView = () => {
1823
+ ensureDesignTokens();
1824
+ store.check().then((report) => {
1825
+ if (report !== null) openUpdateView(store, copyFor());
1826
+ });
1827
+ };
1828
+ const disposeEntry = mountSidebarEntry({
1829
+ rowAttribute: UPDATE_ENTRY_ATTRIBUTE,
1830
+ rowSelector: UPDATE_ENTRY_SELECTOR,
1831
+ plugin: PLUGIN_ID,
1832
+ icon: UPDATE_ICON,
1833
+ css: ENTRY_CSS,
1834
+ label: () => labelState.label,
1835
+ tooltip: () => labelState.tooltip,
1836
+ refresh: { subscribe: refreshSubscribe },
1837
+ badge: {
1838
+ subscribe: (listener) => store.subscribe(listener),
1839
+ count: () => store.badgeCount()
1840
+ },
1841
+ onToggle: openView,
1842
+ position: "after",
1843
+ familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY
1844
+ });
1845
+ store.startIdleRefresh();
1846
+ store.refresh();
1847
+ const dispose = () => {
1848
+ try {
1849
+ disposeEntry();
1850
+ } catch {}
1851
+ unsubscribeLocale();
1852
+ store.stop();
1853
+ };
1854
+ if (typeof ctx.effect === "function") ctx.effect(() => dispose, "plugin-update");
1855
+ return dispose;
1856
+ }
1857
+ //#endregion
1858
+ exports.UPDATE_ENTRY_ATTRIBUTE = UPDATE_ENTRY_ATTRIBUTE;
1859
+ exports.UPDATE_ENTRY_SELECTOR = UPDATE_ENTRY_SELECTOR;
1860
+ exports.UPDATE_ICON = UPDATE_ICON;
1861
+ exports.apply = apply;
1862
+ exports.ensureDesignTokens = ensureDesignTokens;
1863
+ exports.inject = inject;
1864
+ exports.injectUpdateStyles = injectUpdateStyles;
1865
+ return module.exports;
1866
+ }
1867
+ });
1868
+
1869
+ //# sourceMappingURL=client.js.map