@huaqiu/dsh-tool-plugin-update 0.5.21 → 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 CHANGED
@@ -261,6 +261,13 @@ window.__ModuleLoader__.load({
261
261
  body: JSON.stringify({ setVersion })
262
262
  });
263
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
+ }
264
271
  /** POST unignore for the set version (reverses an ignore). */
265
272
  function unignoreOffer(setVersion) {
266
273
  return request("/unignore", {
@@ -273,15 +280,27 @@ window.__ModuleLoader__.load({
273
280
  entryLabel: "插件更新",
274
281
  entryTooltip: "查看可用的插件更新",
275
282
  viewTitle: "插件更新",
283
+ viewIntro: "检查并安装可用的插件更新",
284
+ viewInfo: "在这里查看插件集的更新。标注为「被阻止」的插件因主机或 HQ Edge 版本不兼容而无法安装;已是最新的插件无需操作。",
276
285
  refresh: "刷新",
277
286
  close: "关闭",
287
+ loading: "正在读取插件更新…",
278
288
  empty: "没有可用的插件更新",
279
- checkFailed: "检查更新失败",
280
- manifestVersion: "插件集",
281
- ignoredOffer: "已忽略当前插件集更新",
282
- unignore: "恢复提醒",
289
+ checkFailed: "检查更新失败,请重试",
283
290
  installAll: (count) => `安装 ${count} 项更新`,
284
- installing: "安装中…(DSH 将自动重启)",
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
+ },
285
304
  state: {
286
305
  current: "当前",
287
306
  update: "可更新",
@@ -296,14 +315,16 @@ window.__ModuleLoader__.load({
296
315
  },
297
316
  runtime: {
298
317
  title: "HQ Edge 运行时",
299
- updateAvailable: (version) => `发现新版本 ${version}`,
300
- current: (version) => `当前版本 ${version}`,
318
+ current: "当前版本",
319
+ latest: "最新版本",
320
+ set: "发布插件集",
321
+ checkedAt: "上次检查",
322
+ source: "更新源",
301
323
  unknown: "版本未知",
302
324
  notConfigured: "未配置更新源",
303
325
  note: "运行时更新由 EDA 安装器处理"
304
326
  },
305
327
  lastError: "上次检查失败",
306
- fetchedAt: "检查时间",
307
328
  installError: "安装失败",
308
329
  installSuccess: "更新已安装,正在重启 DSH…"
309
330
  },
@@ -311,15 +332,27 @@ window.__ModuleLoader__.load({
311
332
  entryLabel: "Plugin Updates",
312
333
  entryTooltip: "Review available plugin updates",
313
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.",
314
337
  refresh: "Refresh",
315
338
  close: "Close",
339
+ loading: "Reading plugin updates…",
316
340
  empty: "No plugin updates available",
317
- checkFailed: "Failed to check for updates",
318
- manifestVersion: "Plugin set",
319
- ignoredOffer: "The current plugin-set update is ignored",
320
- unignore: "Show again",
341
+ checkFailed: "Failed to check for updates — please retry",
321
342
  installAll: (count) => `Install ${count} update${count === 1 ? "" : "s"}`,
343
+ installOne: "Install",
322
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
+ },
323
356
  state: {
324
357
  current: "Current",
325
358
  update: "Update",
@@ -334,44 +367,146 @@ window.__ModuleLoader__.load({
334
367
  },
335
368
  runtime: {
336
369
  title: "HQ Edge Runtime",
337
- updateAvailable: (version) => `New version ${version} available`,
338
- current: (version) => `Current version ${version}`,
370
+ current: "Current version",
371
+ latest: "Latest version",
372
+ set: "Release set",
373
+ checkedAt: "Last checked",
374
+ source: "Update source",
339
375
  unknown: "Version unknown",
340
376
  notConfigured: "No update source configured",
341
377
  note: "Runtime updates are handled by the EDA installer"
342
378
  },
343
379
  lastError: "Last check failed",
344
- fetchedAt: "Checked at",
345
380
  installError: "Install failed",
346
381
  installSuccess: "Update installed — restarting DSH…"
347
382
  }
348
383
  };
349
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
350
448
  //#region src/client/update-view.ts
351
449
  /**
352
- * Plugin-update review modal (browser half).
450
+ * Plugin-update review view (browser half).
353
451
  *
354
- * Plain-DOM modal (no React tree) so it can never disturb the DSH shell's
452
+ * Plain-DOM panel (no React tree) so it can never disturb the DSH shell's
355
453
  * reconciliation — same rule as the sidebar entry rows. Rendered content:
356
454
  *
357
- * - header: title + refresh
358
- * - plugin list (name, current → available, state badge, blocked reason)
359
- * - ignored-offer banner + reversible "show again"
360
- * - install action (disabled while installing / nothing actionable / offer
361
- * ignored); after install the UI keeps polling the backend state instead
362
- * of trusting the HTTP response alone, because activating a set restarts
363
- * DSH (spec §7.1: never report success on the 200/202 alone).
364
- * - independent HQ Edge runtime section (read-only report; the actual
365
- * upgrade is owned by the EDA installer).
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.
366
465
  *
367
466
  * @module
368
467
  */
369
- function el(tag, className, text) {
468
+ /** How long the outcome toast stays up, in milliseconds. */
469
+ const TOAST_HOLD_MS = 4e3;
470
+ function el(tag, className = "", text) {
370
471
  const node = document.createElement(tag);
371
- node.className = className;
472
+ if (className.length > 0) node.className = className;
372
473
  if (text !== void 0) node.textContent = text;
373
474
  return node;
374
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
+ }
375
510
  function stateLabel(item, copy) {
376
511
  switch (item.state) {
377
512
  case "current": return copy.state.current;
@@ -381,152 +516,258 @@ window.__ModuleLoader__.load({
381
516
  case "blocked": return copy.state.blocked;
382
517
  }
383
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
+ }
384
527
  function blockedReason(item, copy) {
385
- const by = item.blockedBy ?? [];
528
+ const by = blockedByList(item);
386
529
  if (by.includes("host")) return copy.blockedReason.host;
387
530
  if (by.includes("hq-edge-version")) return copy.blockedReason["hq-edge-version"];
388
531
  if (by.includes("runtime-unknown")) return copy.blockedReason["runtime-unknown"];
389
532
  return by.length > 0 ? by.join(", ") : void 0;
390
533
  }
391
- /**
392
- * Open (or focus) the update modal. Re-renders from the store; when the store
393
- * is stale the modal first refreshes the report.
394
- */
395
- function openUpdateView(store, copy) {
396
- const existing = document.querySelector("[data-hq-plugin-update-view]");
397
- if (existing !== null) {
398
- existing.style.display = "";
399
- return;
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);
400
564
  }
401
- const overlay = el("div", "hqpu-overlay");
402
- overlay.setAttribute("data-hq-plugin-update-view", "");
403
- const panel = el("div", "hqpu-panel");
404
- const header = el("div", "hqpu-header");
405
- header.append(el("div", "hqpu-title", copy.viewTitle));
406
- const refreshBtn = el("button", "hqpu-btn", copy.refresh);
407
- header.append(refreshBtn);
408
- const closeBtn = el("button", "hqpu-btn hqpu-btn-ghost", copy.close);
409
- header.append(closeBtn);
410
- const body = el("div", "hqpu-body");
411
- const listSection = el("div", "hqpu-section");
412
- const runtimeSection = el("div", "hqpu-section hqpu-runtime");
413
- const actionBar = el("div", "hqpu-actions");
414
- panel.append(header, body);
415
- body.append(listSection, runtimeSection, actionBar);
416
- overlay.append(panel);
417
- document.body.appendChild(overlay);
418
- const disposers = [];
419
- const render = () => {
420
- const report = store.report();
421
- const installing = store.installing();
422
- renderList(listSection, report, copy, installing, store);
423
- renderRuntime(runtimeSection, report, copy);
424
- renderActions(actionBar, report, copy, store, installing);
425
- };
426
- const unsubscribe = store.subscribe(render);
427
- disposers.push(unsubscribe);
428
- store.refresh().then(() => {});
429
- const onClose = () => {
430
- for (const dispose of disposers) try {
431
- dispose();
432
- } catch {}
433
- overlay.remove();
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);
434
641
  };
435
- closeBtn.addEventListener("click", onClose);
436
- overlay.addEventListener("click", (ev) => {
437
- if (ev.target === overlay) onClose();
438
- });
439
- refreshBtn.addEventListener("click", () => {
440
- store.refresh();
441
- });
442
- disposers.push(() => {
443
- closeBtn.removeEventListener("click", onClose);
444
- overlay.removeEventListener("click", onClose);
445
- refreshBtn.removeEventListener("click", () => {});
446
- });
447
- render();
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;
448
660
  }
449
661
  function renderList(section, report, copy, installing, store) {
450
662
  section.textContent = "";
451
663
  if (report === null) {
452
- section.append(el("div", "hqpu-empty", copy.checkFailed));
664
+ section.append(skeleton(copy));
453
665
  return;
454
666
  }
455
667
  if (report.items.length === 0) {
456
668
  section.append(el("div", "hqpu-empty", copy.empty));
457
669
  return;
458
670
  }
459
- const manifestRow = el("div", "hqpu-manifest");
460
- manifestRow.append(el("span", "hqpu-manifest-label", `${copy.manifestVersion}`), el("span", "hqpu-manifest-value", report.manifestVersion));
461
- section.append(manifestRow);
462
- if (report.ignoredSet) {
463
- const banner = el("div", "hqpu-ignored-banner", copy.ignoredOffer);
464
- const unignore = el("button", "hqpu-btn hqpu-btn-small", copy.unignore);
465
- unignore.addEventListener("click", () => {
466
- unignoreOffer(report.manifestVersion).then(() => store.refresh()).catch(() => void 0);
467
- });
468
- banner.append(unignore);
469
- section.append(banner);
470
- }
471
- const list = el("ul", "hqpu-list");
472
- for (const item of report.items) {
473
- const row = el("li", `hqpu-item hqpu-item-${item.state}`);
474
- const name = el("span", "hqpu-item-name", item.name);
475
- const version = el("span", "hqpu-item-version", item.state === "current" ? item.currentVersion : `${item.currentVersion} → ${item.availableVersion}`);
476
- const state = el("span", `hqpu-item-state hqpu-state-${item.state}`, stateLabel(item, copy));
477
- row.append(name, version, state);
478
- const reason = blockedReason(item, copy);
479
- if (reason !== void 0) row.append(el("span", "hqpu-item-reason", reason));
480
- list.append(row);
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);
481
681
  }
482
- section.append(list);
483
682
  }
484
683
  function renderRuntime(section, report, copy) {
485
684
  section.textContent = "";
486
- const title = el("div", "hqpu-runtime-title", copy.runtime.title);
487
- section.append(title);
488
- const runtime = report?.runtime;
489
- if (runtime === void 0 || runtime.configured === false) {
490
- section.append(el("div", "hqpu-runtime-line", copy.runtime.notConfigured));
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");
491
694
  return;
492
695
  }
493
- if (runtime.updateAvailable && runtime.latest !== void 0) section.append(el("div", "hqpu-runtime-line", copy.runtime.updateAvailable(runtime.latest)));
494
- else if (runtime.currentVersion !== void 0) section.append(el("div", "hqpu-runtime-line", copy.runtime.current(runtime.currentVersion)));
495
- else section.append(el("div", "hqpu-runtime-line", copy.runtime.unknown));
496
- section.append(el("div", "hqpu-runtime-note", copy.runtime.note));
497
- if (runtime.lastError !== void 0 && runtime.lastError.length > 0) section.append(el("div", "hqpu-runtime-error", `${copy.lastError}: ${runtime.lastError}`));
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);
498
726
  }
499
727
  function renderActions(bar, report, copy, store, installing) {
500
728
  bar.textContent = "";
501
729
  if (report === null) return;
502
730
  if (installing) {
503
- bar.append(el("span", "hqpu-installing", copy.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);
504
735
  return;
505
736
  }
506
737
  const actionable = report.items.filter((item) => item.state === "update" || item.state === "install").length;
507
- const installBtn = el("button", "hqpu-btn hqpu-btn-primary", actionable > 0 ? copy.installAll(actionable) : copy.state.current);
508
- installBtn.disabled = actionable === 0 || report.ignoredSet;
509
- installBtn.addEventListener("click", () => {
510
- installSet(report.manifestVersion).then((result) => {
511
- if (result.error !== void 0) {
512
- bar.textContent = "";
513
- bar.append(el("div", "hqpu-error", `${copy.installError}: ${result.error}`));
514
- return;
515
- }
516
- bar.textContent = "";
517
- bar.append(el("span", "hqpu-installing", copy.installSuccess));
518
- pollUntilSettled(store, report.manifestVersion);
519
- }).catch((err) => {
520
- bar.textContent = "";
521
- bar.append(el("div", "hqpu-error", `${copy.installError}: ${err instanceof Error ? err.message : String(err)}`));
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);
522
747
  });
523
- });
524
- bar.append(installBtn);
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
+ }
525
766
  }
526
767
  /** After an install, poll the backend until the update report settles
527
768
  * (installing flag clears or DSH restarts under us — the next load re-reads
528
769
  * the fresh state). */
529
- function pollUntilSettled(store, setVersion) {
770
+ function pollUntilSettled(store) {
530
771
  let attempts = 0;
531
772
  const tick = () => {
532
773
  store.refresh().then((report) => {
@@ -539,6 +780,101 @@ window.__ModuleLoader__.load({
539
780
  };
540
781
  tick();
541
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
+ }
542
878
  //#endregion
543
879
  //#region src/client/index.ts
544
880
  /**
@@ -579,15 +915,14 @@ window.__ModuleLoader__.load({
579
915
  tooltip: COPY.zh.entryTooltip
580
916
  };
581
917
  }
582
- /** Inline update glyph — `currentColor`, 16×16 like the shell's nav glyphs. */
583
- const UPDATE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
584
- <g fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
585
- <path d="M2.5 8a5.5 5.5 0 1 1 1.61 3.89" />
586
- <path d="M2.5 8V4.75M2.5 8h3.25" />
587
- </g>
588
- </svg>`;
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);
589
921
  /** Sidebar entry styles — matched to the family rows (same inset/padding/gap
590
- * as the shell's own Plugins entry). The badge is a 16px rounded pill. */
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. */
591
926
  const ENTRY_CSS_TEXT = `
592
927
  .hqpu-sidebar-entry {
593
928
  box-sizing: border-box;
@@ -600,7 +935,7 @@ window.__ModuleLoader__.load({
600
935
  padding: 7px 8px;
601
936
  background: transparent;
602
937
  border: none;
603
- border-radius: 12px;
938
+ border-radius: var(--dsw-radius-md);
604
939
  color: var(--dsw-alias-label-primary);
605
940
  cursor: pointer;
606
941
  font: inherit;
@@ -615,6 +950,10 @@ window.__ModuleLoader__.load({
615
950
  background: var(--dsw-alias-interactive-bg-hover);
616
951
  color: var(--dsw-alias-label-primary);
617
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
+ }
618
957
  .hqpu-sidebar-entry__icon {
619
958
  display: inline-flex;
620
959
  align-items: center;
@@ -638,11 +977,13 @@ window.__ModuleLoader__.load({
638
977
  height: 16px;
639
978
  padding: 0 5px;
640
979
  border-radius: 8px;
641
- background: var(--dsw-alias-bg-badge, #e5484d);
642
- color: #fff;
980
+ background: var(--dsw-alias-state-error-primary);
981
+ color: var(--dsw-alias-toast-label);
643
982
  font-size: 11px;
644
983
  line-height: 16px;
984
+ font-weight: 500;
645
985
  text-align: center;
986
+ font-variant-numeric: tabular-nums;
646
987
  }
647
988
  /* Collapsed rail: icon-only, centered — same 36px square the shell uses. */
648
989
  [data-sidebar-collapsed] .hqpu-sidebar-entry {
@@ -661,9 +1002,28 @@ window.__ModuleLoader__.load({
661
1002
  display: none;
662
1003
  }
663
1004
  `;
664
- /** Modal styles (hqpu-*). Plain overlay + panel, aligned to the shell's
665
- * dark/light aliases. */
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
+ */
666
1025
  const VIEW_CSS_TEXT = `
1026
+ /* ── Overlay + panel (ui-primitives Modal.module.css) ─────────────────── */
667
1027
  .hqpu-overlay {
668
1028
  position: fixed;
669
1029
  inset: 0;
@@ -671,175 +1031,675 @@ window.__ModuleLoader__.load({
671
1031
  display: flex;
672
1032
  align-items: center;
673
1033
  justify-content: center;
674
- background: rgba(0, 0, 0, 0.45);
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);
675
1043
  }
676
1044
  .hqpu-panel {
1045
+ position: relative;
1046
+ z-index: 1;
677
1047
  box-sizing: border-box;
678
- width: min(640px, calc(100vw - 48px));
679
- max-height: min(80vh, 720px);
680
1048
  display: flex;
681
1049
  flex-direction: column;
682
- border-radius: 16px;
683
- background: var(--dsw-alias-bg-surface, #fff);
684
- color: var(--dsw-alias-label-primary, #1f2329);
685
- box-shadow: 0 24px 64px rgba(0, 0, 0, 0.24);
1050
+ gap: 20px;
1051
+ width: min(920px, 100%);
1052
+ max-height: 100%;
686
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; }
687
1071
  }
688
- .hqpu-header {
1072
+
1073
+ /* ── Head row (ui-primitives Modal .header) ─────────────────────────────── */
1074
+ .hqpu-head {
689
1075
  display: flex;
690
- align-items: center;
691
- gap: 8px;
692
- padding: 14px 16px;
693
- border-bottom: 1px solid var(--dsw-alias-line-border, rgba(0,0,0,.08));
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%;
694
1089
  }
695
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 {
696
1137
  flex: 1;
697
- font-size: 15px;
698
- font-weight: 600;
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);
699
1146
  }
700
- .hqpu-body {
701
- overflow: auto;
702
- padding: 16px;
1147
+ .hqpu-column {
703
1148
  display: flex;
704
1149
  flex-direction: column;
705
- gap: 16px;
1150
+ gap: 24px;
1151
+ width: 100%;
1152
+ max-width: 880px;
1153
+ margin: 0 auto;
706
1154
  }
707
- .hqpu-section {
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 {
708
1167
  display: flex;
709
1168
  flex-direction: column;
710
1169
  gap: 8px;
711
1170
  }
712
- .hqpu-manifest {
1171
+ .hqpu-group-head {
713
1172
  display: flex;
714
1173
  align-items: center;
715
1174
  gap: 8px;
716
- font-size: 12px;
717
- color: var(--dsw-alias-label-secondary, #646a73);
718
1175
  }
719
- .hqpu-manifest-value {
720
- font-weight: 600;
721
- color: var(--dsw-alias-label-primary, #1f2329);
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);
722
1184
  }
723
- .hqpu-list {
724
- list-style: none;
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 {
725
1195
  margin: 0;
726
1196
  padding: 0;
1197
+ list-style: none;
727
1198
  display: flex;
728
1199
  flex-direction: column;
729
- gap: 6px;
1200
+ gap: 2px;
730
1201
  }
731
- .hqpu-item {
732
- display: grid;
733
- grid-template-columns: 1fr auto auto;
734
- gap: 8px;
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;
735
1213
  align-items: center;
736
- padding: 8px 10px;
737
- border-radius: 10px;
738
- background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.04));
739
- font-size: 13px;
1214
+ gap: 14px;
1215
+ padding: 8px;
740
1216
  }
741
- .hqpu-item-name {
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;
742
1250
  font-weight: 500;
1251
+ line-height: 20px;
1252
+ color: var(--dsw-alias-label-primary);
743
1253
  overflow: hidden;
744
1254
  text-overflow: ellipsis;
1255
+ white-space: nowrap;
745
1256
  }
746
- .hqpu-item-version {
747
- color: var(--dsw-alias-label-secondary, #646a73);
748
- font-variant-numeric: tabular-nums;
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;
749
1266
  }
750
- .hqpu-item-state {
751
- padding: 1px 8px;
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;
752
1289
  border-radius: 999px;
1290
+ corner-shape: round;
1291
+ padding: 1px 8px;
753
1292
  font-size: 11px;
754
- line-height: 18px;
1293
+ font-weight: 500;
1294
+ line-height: 17px;
1295
+ white-space: nowrap;
755
1296
  }
756
- .hqpu-state-current { background: rgba(0,0,0,.06); color: var(--dsw-alias-label-secondary, #646a73); }
757
- .hqpu-state-update, .hqpu-state-install { background: rgba(229,72,77,.12); color: #d92d33; }
758
- .hqpu-state-ignored { background: rgba(0,0,0,.06); color: var(--dsw-alias-label-secondary, #646a73); }
759
- .hqpu-state-blocked { background: rgba(229,72,77,.12); color: #d92d33; }
760
- .hqpu-item-reason {
761
- grid-column: 1 / -1;
762
- font-size: 12px;
763
- color: var(--dsw-alias-label-tertiary, #8f959e);
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;
764
1323
  }
765
- .hqpu-ignored-banner {
1324
+ .hqpu-facts > div {
766
1325
  display: flex;
767
1326
  align-items: center;
768
- gap: 8px;
769
- padding: 8px 10px;
770
- border-radius: 10px;
771
- background: rgba(120,120,120,.1);
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);
772
1356
  font-size: 12px;
773
- color: var(--dsw-alias-label-secondary, #646a73);
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);
774
1394
  }
775
1395
  .hqpu-empty {
776
- padding: 24px 0;
1396
+ padding: 32px 0;
777
1397
  text-align: center;
778
- color: var(--dsw-alias-label-tertiary, #8f959e);
779
1398
  font-size: 13px;
1399
+ line-height: 20px;
1400
+ color: var(--dsw-alias-label-tertiary);
780
1401
  }
781
- .hqpu-runtime {
782
- border-top: 1px solid var(--dsw-alias-line-border, rgba(0,0,0,.08));
783
- padding-top: 12px;
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;
784
1410
  }
785
- .hqpu-runtime-title {
786
- font-size: 13px;
787
- font-weight: 600;
1411
+ .hqpu-dot::after {
1412
+ content: '';
1413
+ position: absolute;
1414
+ inset: 20%;
1415
+ border-radius: 50%;
1416
+ corner-shape: round;
1417
+ background: currentColor;
788
1418
  }
789
- .hqpu-runtime-line {
790
- font-size: 12px;
791
- color: var(--dsw-alias-label-secondary, #646a73);
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;
792
1431
  }
793
- .hqpu-runtime-note {
794
- font-size: 11px;
795
- color: var(--dsw-alias-label-tertiary, #8f959e);
1432
+ @keyframes hqpu-spin {
1433
+ to { transform: rotate(360deg); }
796
1434
  }
797
- .hqpu-runtime-error {
798
- font-size: 12px;
799
- color: #d92d33;
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;
800
1440
  }
801
- .hqpu-actions {
1441
+ .hqpu-skeleton-text {
802
1442
  display: flex;
803
1443
  align-items: center;
804
- gap: 8px;
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;
805
1515
  }
806
1516
  .hqpu-btn {
807
1517
  box-sizing: border-box;
808
- padding: 6px 14px;
809
- border-radius: 8px;
810
- border: 1px solid transparent;
811
- background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06));
812
- color: var(--dsw-alias-label-primary, #1f2329);
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);
813
1528
  font: inherit;
814
1529
  font-size: 13px;
1530
+ line-height: 20px;
815
1531
  cursor: pointer;
816
1532
  }
817
- .hqpu-btn-primary {
818
- background: #e5484d;
819
- color: #fff;
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;
820
1539
  }
821
- .hqpu-btn-primary:disabled {
822
- opacity: 0.5;
823
- cursor: not-allowed;
1540
+ .hqpu-btn--primary {
1541
+ background: var(--dsw-alias-button-primary-fill);
1542
+ color: var(--dsw-alias-label-primary-foreground);
824
1543
  }
825
- .hqpu-btn-ghost {
1544
+ .hqpu-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
1545
+ .hqpu-btn--outline {
826
1546
  background: transparent;
827
- border-color: var(--dsw-alias-line-border, rgba(0,0,0,.12));
1547
+ border: 0.5px solid var(--dsw-alias-border-l4);
1548
+ color: var(--dsw-alias-label-secondary);
828
1549
  }
829
- .hqpu-btn-small {
830
- padding: 2px 10px;
831
- font-size: 12px;
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;
832
1560
  }
833
- .hqpu-installing {
834
- font-size: 13px;
835
- color: var(--dsw-alias-label-secondary, #646a73);
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;
836
1570
  }
837
- .hqpu-error {
838
- font-size: 13px;
839
- color: #d92d33;
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
+ }
840
1593
  }
841
1594
  `;
842
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
+ }
843
1703
  /** Inject styles once (idempotent). */
844
1704
  function injectUpdateStyles() {
845
1705
  if (typeof document === "undefined") return;
@@ -934,6 +1794,7 @@ window.__ModuleLoader__.load({
934
1794
  function apply(ctx) {
935
1795
  if (typeof document === "undefined") return () => {};
936
1796
  injectUpdateStyles();
1797
+ ensureDesignTokens();
937
1798
  const store = new UpdateStore();
938
1799
  const copy = {
939
1800
  zh: COPY.zh,
@@ -959,6 +1820,7 @@ window.__ModuleLoader__.load({
959
1820
  tooltip: entryCopy(locale).tooltip
960
1821
  };
961
1822
  const openView = () => {
1823
+ ensureDesignTokens();
962
1824
  store.check().then((report) => {
963
1825
  if (report !== null) openUpdateView(store, copyFor());
964
1826
  });
@@ -989,7 +1851,7 @@ window.__ModuleLoader__.load({
989
1851
  unsubscribeLocale();
990
1852
  store.stop();
991
1853
  };
992
- if (typeof ctx.effect === "function") ctx.effect(dispose, "plugin-update");
1854
+ if (typeof ctx.effect === "function") ctx.effect(() => dispose, "plugin-update");
993
1855
  return dispose;
994
1856
  }
995
1857
  //#endregion
@@ -997,6 +1859,7 @@ window.__ModuleLoader__.load({
997
1859
  exports.UPDATE_ENTRY_SELECTOR = UPDATE_ENTRY_SELECTOR;
998
1860
  exports.UPDATE_ICON = UPDATE_ICON;
999
1861
  exports.apply = apply;
1862
+ exports.ensureDesignTokens = ensureDesignTokens;
1000
1863
  exports.inject = inject;
1001
1864
  exports.injectUpdateStyles = injectUpdateStyles;
1002
1865
  return module.exports;