@huaqiu/dsh-tool-plugin-update 0.6.7 → 0.6.8

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
@@ -718,6 +718,53 @@ window.__ModuleLoader__.load({
718
718
  changesTitle: "更新内容",
719
719
  advanced: "高级:更新源",
720
720
  publishedAt: (when) => `发布于 ${when}`
721
+ },
722
+ ui: {
723
+ title: "更新中心",
724
+ gearTooltip: "高级设置",
725
+ settingsTitle: "更新源配置 (高级)",
726
+ originDefault: "默认源",
727
+ originCustom: "自定义源",
728
+ pluginUrlLabel: "插件 Manifest URL",
729
+ runtimeUrlLabel: "运行时 Manifest URL",
730
+ saveAndRefresh: "保存并刷新",
731
+ checkedAgo: (ago) => `上次检查: ${ago}`,
732
+ checkedUnknown: "尚未检查更新",
733
+ agoJust: "刚刚",
734
+ agoMinutes: (n) => `${n} 分钟前`,
735
+ agoHours: (n) => `${n} 小时前`,
736
+ agoDays: (n) => `${n} 天前`,
737
+ jobPluginTitle: (v) => `正在升级全套插件 (v${v})`,
738
+ jobRuntimeTitle: (v) => `正在升级 HQ Edge 运行时 (v${v})`,
739
+ jobSteps: [
740
+ "下载",
741
+ "校验",
742
+ "替换",
743
+ "重启"
744
+ ],
745
+ rollbackTitle: "上次 HQ Edge 运行时更新已自动回滚",
746
+ failedTitle: "上次 HQ Edge 运行时更新失败",
747
+ rollbackBody: (oldVer, code) => `系统已安全恢复至旧版本 (v${oldVer})。代码: ${code}`,
748
+ failedBody: (oldVer, code) => `未能完成运行时替换 (旧版本 v${oldVer})。代码: ${code}`,
749
+ dismiss: "忽略",
750
+ successInline: "上次运行时更新成功",
751
+ runtimeEngine: "HQ Edge 核心运行时",
752
+ currentChip: (v) => `v${v} (当前)`,
753
+ newVersion: (v) => `新版本 v${v} 可用`,
754
+ breakingTag: "关键修复",
755
+ sizeLine: (size, summary) => `体积: ${size}${summary ? ` · ${summary}` : ""}`,
756
+ upgradeCore: "升级核心",
757
+ runtimeRestartNote: "升级运行时需自动重启 KiCad 进程服务",
758
+ pendingTitle: "待更新插件",
759
+ allChangelog: "展开全部更新日志",
760
+ changelogBtn: "日志",
761
+ changelogTitle: "更新内容:",
762
+ blockedBadge: "无法安装",
763
+ footerCount: (n) => `准备安装 ${n} 项兼容插件`,
764
+ footerSet: (v) => `版本集: v${v}`,
765
+ installAllSet: (v) => `一键更新全套插件 (v${v})`,
766
+ ignoreBtn: "忽略",
767
+ footnote: "更新将自动应用并重启 DSH 视图,不影响 EDA 图纸数据"
721
768
  }
722
769
  };
723
770
  /** Compact a byte count for display: "约 12.3 MB" / "约 480 MB". */
@@ -864,6 +911,53 @@ window.__ModuleLoader__.load({
864
911
  changesTitle: "What changed",
865
912
  advanced: "Advanced: update sources",
866
913
  publishedAt: (when) => `Published ${when}`
914
+ },
915
+ ui: {
916
+ title: "Update Center",
917
+ gearTooltip: "Advanced settings",
918
+ settingsTitle: "Update source (advanced)",
919
+ originDefault: "Default source",
920
+ originCustom: "Custom source",
921
+ pluginUrlLabel: "Plugin manifest URL",
922
+ runtimeUrlLabel: "Runtime manifest URL",
923
+ saveAndRefresh: "Save & refresh",
924
+ checkedAgo: (ago) => `Last checked: ${ago}`,
925
+ checkedUnknown: "No check yet",
926
+ agoJust: "just now",
927
+ agoMinutes: (n) => `${n} min ago`,
928
+ agoHours: (n) => `${n} h ago`,
929
+ agoDays: (n) => `${n} d ago`,
930
+ jobPluginTitle: (v) => `Updating plugin set (v${v})`,
931
+ jobRuntimeTitle: (v) => `Upgrading HQ Edge runtime (v${v})`,
932
+ jobSteps: [
933
+ "Download",
934
+ "Verify",
935
+ "Swap",
936
+ "Restart"
937
+ ],
938
+ rollbackTitle: "Last HQ Edge runtime update rolled back automatically",
939
+ failedTitle: "Last HQ Edge runtime update failed",
940
+ rollbackBody: (oldVer, code) => `Safely restored to the previous version (v${oldVer}). Code: ${code}`,
941
+ failedBody: (oldVer, code) => `Runtime swap did not complete (old v${oldVer}). Code: ${code}`,
942
+ dismiss: "Dismiss",
943
+ successInline: "Last runtime update succeeded",
944
+ runtimeEngine: "HQ Core Runtime",
945
+ currentChip: (v) => `v${v} (current)`,
946
+ newVersion: (v) => `New v${v} available`,
947
+ breakingTag: "Critical fix",
948
+ sizeLine: (size, summary) => `Size: ${size}${summary ? ` · ${summary}` : ""}`,
949
+ upgradeCore: "Upgrade core",
950
+ runtimeRestartNote: "Upgrading the runtime restarts the KiCad process service",
951
+ pendingTitle: "Plugins to update",
952
+ allChangelog: "Expand all changelogs",
953
+ changelogBtn: "Log",
954
+ changelogTitle: "What changed:",
955
+ blockedBadge: "Cannot install",
956
+ footerCount: (n) => `Ready to install ${n} compatible plugin${n === 1 ? "" : "s"}`,
957
+ footerSet: (v) => `Set: v${v}`,
958
+ installAllSet: (v) => `Update all plugins (v${v})`,
959
+ ignoreBtn: "Ignore",
960
+ footnote: "Updates apply automatically and restart the DSH view — your EDA schematic data is untouched"
867
961
  }
868
962
  }
869
963
  };
@@ -918,18 +1012,6 @@ window.__ModuleLoader__.load({
918
1012
  ]);
919
1013
  /** The settled-state glyph (`IconCheckCircleFillRegular`). */
920
1014
  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");
921
- /** The install action (`IconDownloadOutlineRegular`). */
922
- const DOWNLOAD_ICON = (size = 16) => glyph(size, REGULAR, [
923
- "M8 1.95317V10.0469",
924
- "M4.25 6.29688L8 10.0469L11.75 6.29688",
925
- "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"
926
- ]);
927
- /** The head's info affordance (`IconInfoOutlineRegular`, fill-only artwork). */
928
- const INFO_ICON = (size = 11) => fillGlyph(size, [
929
- "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",
930
- "M7.6127 3.18921V4.55986H6.38735V3.18921H7.6127Z",
931
- "M7.6127 5.68921V10.8109H6.38735V5.68921H7.6127Z"
932
- ], "0 0 14 14");
933
1015
  //#endregion
934
1016
  //#region src/client/update-view.ts
935
1017
  /**
@@ -983,33 +1065,6 @@ window.__ModuleLoader__.load({
983
1065
  if (Array.isArray(by)) return by.filter((entry) => typeof entry === "string");
984
1066
  return typeof by === "string" && by.length > 0 ? [by] : [];
985
1067
  }
986
- /** The tag tone a state reads as, matching the shell's Tag palette. */
987
- function stateTone(item) {
988
- switch (item.state) {
989
- case "update": return "info";
990
- case "install": return "warning";
991
- case "blocked": return "danger";
992
- case "ignored": return "neutral";
993
- case "current": return "success";
994
- }
995
- }
996
- function stateLabel(item, copy) {
997
- switch (item.state) {
998
- case "current": return copy.state.current;
999
- case "update": return copy.state.update;
1000
- case "install": return copy.state.install;
1001
- case "ignored": return copy.state.ignored;
1002
- case "blocked": return copy.state.blocked;
1003
- }
1004
- }
1005
- /** The one-line version transition under a card's title. */
1006
- function versionLine(item) {
1007
- switch (item.state) {
1008
- case "current": return item.currentVersion ?? item.availableVersion;
1009
- case "install": return item.availableVersion;
1010
- default: return item.currentVersion === null ? item.availableVersion : `${item.currentVersion} → ${item.availableVersion}`;
1011
- }
1012
- }
1013
1068
  function blockedReason(item, copy) {
1014
1069
  const by = blockedByList(item);
1015
1070
  if (by.includes("host")) return copy.blockedReason.host;
@@ -1017,72 +1072,93 @@ window.__ModuleLoader__.load({
1017
1072
  if (by.includes("runtime-unknown")) return copy.blockedReason["runtime-unknown"];
1018
1073
  return by.length > 0 ? by.join(", ") : void 0;
1019
1074
  }
1020
- /** One package card: glyph, name with its state tag, version line, end actions. */
1021
- function packageCard(item, copy, installing, store) {
1022
- const card = el("li", "hqpu-card");
1075
+ /** Two-letter avatar initials for a card (CJK-safe). */
1076
+ function avatarInitials(item) {
1077
+ const src = typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName.trim() : shortName(item.name);
1078
+ if (/[⺀-鿿]/.test(src)) return src.slice(0, 2);
1079
+ const words = src.split(/[\s_\-./@]+/).filter(Boolean);
1080
+ if (words.length >= 2) return (words[0][0] + words[1][0]).toUpperCase();
1081
+ return src.replace(/^@/, "").slice(0, 2).toUpperCase();
1082
+ }
1083
+ /** Shared per-card changelog toggle drawer (the per-plugin changes[] list). */
1084
+ function changelogDrawer(item, copy, open) {
1085
+ const drawer = el("div", "hqpu-changelog-drawer");
1086
+ drawer.style.display = open ? "" : "none";
1087
+ drawer.append(el("div", "hqpu-changelog-drawer-title", copy.ui.changelogTitle));
1088
+ const ul = el("ul");
1089
+ for (const change of item.changes ?? []) if (typeof change === "string" && change.length > 0) ul.append(el("li", "", change));
1090
+ drawer.append(ul);
1091
+ return drawer;
1092
+ }
1093
+ /** One package card per the mockup: avatar initials, display name + package
1094
+ * subtitle, old→new version row (new in emerald), a 日志 toggle expanding the
1095
+ * inline changelog. Blocked/incompatible cards are dimmed with a rose
1096
+ * 无法安装 badge and a reason line, and carry NO install button. */
1097
+ function packageCard(item, copy, open, onToggle) {
1098
+ const blocked = item.state === "blocked";
1099
+ const card = el("li", blocked ? "hqpu-card hqpu-card--blocked" : "hqpu-card");
1023
1100
  card.dataset.pluginName = item.name;
1024
1101
  card.dataset.state = item.state;
1025
- card.title = `${item.name} · ${versionLine(item)}`;
1026
1102
  const head = el("div", "hqpu-card-head");
1027
- const glyph = el("span", "hqpu-card-icon");
1028
- glyph.setAttribute("aria-hidden", "true");
1029
- glyph.innerHTML = PLUGIN_ICON(24);
1103
+ const left = el("div");
1104
+ const avatar = el("span", "hqpu-avatar", avatarInitials(item));
1105
+ avatar.setAttribute("aria-hidden", "true");
1030
1106
  const main = el("div", "hqpu-card-main");
1031
- const titleRow = el("div", "hqpu-title-row");
1032
- const title = el("span", "hqpu-card-title", typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName.trim() : shortName(item.name));
1033
- const tag = el("span", "hqpu-tag");
1034
- tag.dataset.tone = stateTone(item);
1035
- tag.textContent = stateLabel(item, copy);
1036
- titleRow.append(title, tag);
1037
- const size = formatMb(item.sizeBytes ?? 0);
1038
- const desc = el("span", "hqpu-card-desc", size.length > 0 ? `${versionLine(item)} · ${size}` : versionLine(item));
1039
- main.append(titleRow, desc);
1040
- if (typeof item.description === "string" && item.description.trim().length > 0) main.append(el("span", "hqpu-card-desc", item.description.trim()));
1041
- head.append(glyph, main);
1042
- const end = el("div", "hqpu-card-end");
1043
- if (item.state === "update" || item.state === "install") {
1044
- const button = el("button", "hqpu-btn hqpu-btn--sm hqpu-btn--primary");
1045
- button.type = "button";
1046
- button.disabled = installing;
1047
- button.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installOne}</span>`;
1048
- button.title = copy.installOneTip;
1049
- button.addEventListener("click", () => {
1050
- runInstall(store, copy);
1051
- });
1052
- end.append(button);
1107
+ const titleText = typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName.trim() : shortName(item.name);
1108
+ main.append(el("span", "hqpu-card-title", titleText));
1109
+ main.append(el("span", "hqpu-card-sub", shortName(item.name)));
1110
+ const ver = el("div", "hqpu-version-row");
1111
+ if (item.currentVersion !== null && item.currentVersion !== void 0) {
1112
+ ver.append(el("span", "hqpu-ver-old", item.currentVersion));
1113
+ ver.append(el("span", "hqpu-ver-arrow", "→"));
1053
1114
  }
1054
- head.append(end);
1055
- card.append(head);
1056
- if (Array.isArray(item.changes) && item.changes.length > 0) {
1057
- const expander = el("div", "hqpu-changes");
1058
- const toggle = el("button", "hqpu-btn hqpu-btn--ghost hqpu-btn--sm", copy.release.changesTitle);
1115
+ const size = formatMb(item.sizeBytes ?? 0);
1116
+ ver.append(el("span", "hqpu-ver-new", size.length > 0 ? `${item.availableVersion} (${size})` : item.availableVersion));
1117
+ main.append(ver);
1118
+ left.append(avatar, main);
1119
+ const right = el("div", "hqpu-card-end");
1120
+ if (blocked) right.append(el("span", "hqpu-blocked-badge", copy.ui.blockedBadge));
1121
+ else if (Array.isArray(item.changes) && item.changes.length > 0) {
1122
+ const toggle = el("button", "hqpu-log-toggle");
1059
1123
  toggle.type = "button";
1060
- const list = el("ul", "hqpu-changes-list");
1061
- list.style.display = "none";
1062
- for (const change of item.changes) if (typeof change === "string" && change.length > 0) list.append(el("li", "", change));
1063
- toggle.addEventListener("click", () => {
1064
- list.style.display = list.style.display === "none" ? "" : "none";
1065
- });
1066
- expander.append(toggle, list);
1067
- card.append(expander);
1124
+ toggle.dataset.open = open ? "true" : "false";
1125
+ toggle.innerHTML = `${copy.ui.changelogBtn}<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6"/></svg>`;
1126
+ toggle.addEventListener("click", onToggle);
1127
+ right.append(toggle);
1068
1128
  }
1069
- if (item.state === "blocked") {
1129
+ head.append(left, right);
1130
+ card.append(head);
1131
+ if (!blocked && Array.isArray(item.changes) && item.changes.length > 0) card.append(changelogDrawer(item, copy, open));
1132
+ if (blocked) {
1070
1133
  const reason = blockedReason(item, copy);
1071
- if (reason !== void 0) card.append(el("p", "hqpu-card-reason", reason));
1072
- for (const entry of [...item.reasons ?? [], ...item.notes ?? []]) if (typeof entry === "string" && entry.length > 0) card.append(el("p", "hqpu-card-reason", entry));
1134
+ const entries = [...reason !== void 0 ? [reason] : [], ...[...item.reasons ?? [], ...item.notes ?? []].filter((e) => typeof e === "string" && e.length > 0)];
1135
+ for (const entry of entries) {
1136
+ const line = el("p", "hqpu-reason-line");
1137
+ line.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5V8.5M8 11.25h.01M14.5 8a6.5 6.5 0 11-13 0 6.5 6.5 0 0113 0Z"/></svg>`;
1138
+ line.append(document.createTextNode(entry));
1139
+ card.append(line);
1140
+ }
1073
1141
  }
1074
1142
  return card;
1075
1143
  }
1076
- /** A section: group head with its count, then the cards. */
1077
- function group(heading, items, copy, installing, store) {
1144
+ /** A section: head (title + count pill + expand-all toggle) then its cards. */
1145
+ function group(heading, items, copy, changelogOpen, allOpen, onToggle, onToggleAll) {
1078
1146
  if (items.length === 0) return null;
1079
1147
  const section = el("section", "hqpu-group");
1080
1148
  section.dataset.group = heading;
1081
- const head = el("div", "hqpu-group-head");
1082
- head.append(el("h3", "hqpu-group-title", heading));
1083
- head.append(el("span", "hqpu-group-count", copy.groupCount(items.length)));
1149
+ const head = el("div", "hqpu-section-head");
1150
+ const titleWrap = el("div", "hqpu-section-title");
1151
+ titleWrap.append(el("span", "", heading));
1152
+ titleWrap.append(el("span", "hqpu-count-pill", String(items.length)));
1153
+ head.append(titleWrap);
1154
+ if (items.some((i) => i.state !== "blocked" && Array.isArray(i.changes) && i.changes.length > 0)) {
1155
+ const all = el("button", "hqpu-all-changelog", copy.ui.allChangelog);
1156
+ all.type = "button";
1157
+ all.addEventListener("click", onToggleAll);
1158
+ head.append(all);
1159
+ }
1084
1160
  const list = el("ul", "hqpu-cards");
1085
- for (const item of items) list.append(packageCard(item, copy, installing, store));
1161
+ for (const item of items) list.append(packageCard(item, copy, allOpen || changelogOpen.has(item.name), () => onToggle(item.name)));
1086
1162
  section.append(head, list);
1087
1163
  return section;
1088
1164
  }
@@ -1130,13 +1206,17 @@ window.__ModuleLoader__.load({
1130
1206
  if (n >= 1024) return `${(n / 1024).toFixed(0)} KB`;
1131
1207
  return `${n} B`;
1132
1208
  }
1133
- /** Format an ETA in seconds to a short human string, or null when unknown. */
1134
- function formatEta(seconds) {
1135
- if (seconds === void 0 || !Number.isFinite(seconds) || seconds <= 0) return null;
1136
- if (seconds < 60) return `${Math.round(seconds)} s`;
1137
- const minutes = Math.round(seconds / 60);
1138
- if (minutes < 60) return `${minutes} min`;
1139
- return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
1209
+ /** "上次检查: N 分钟前" — relative time from an ISO timestamp. */
1210
+ function relativeAgo(iso, copy) {
1211
+ if (typeof iso !== "string" || iso.length === 0) return null;
1212
+ const then = new Date(iso).getTime();
1213
+ if (!Number.isFinite(then)) return null;
1214
+ const diffMin = Math.round((Date.now() - then) / 6e4);
1215
+ if (diffMin < 1) return copy.ui.agoJust;
1216
+ if (diffMin < 60) return copy.ui.agoMinutes(diffMin);
1217
+ const hours = Math.round(diffMin / 60);
1218
+ if (hours < 24) return copy.ui.agoHours(hours);
1219
+ return copy.ui.agoDays(Math.round(hours / 24));
1140
1220
  }
1141
1221
  function jobKindLabel(job, copy) {
1142
1222
  return job.kind === "runtime" ? copy.job.kindRuntime : copy.job.kindPluginSet;
@@ -1163,29 +1243,40 @@ window.__ModuleLoader__.load({
1163
1243
  wrap.append(ol);
1164
1244
  return wrap;
1165
1245
  }
1246
+ /** Map a job phase onto the 4-step indicator index (下载/校验/替换/重启). */
1247
+ function stepIndex(phase) {
1248
+ switch (phase) {
1249
+ case "downloading": return 0;
1250
+ case "verifying":
1251
+ case "resolving": return 1;
1252
+ case "staging":
1253
+ case "activating":
1254
+ case "awaiting-host": return 2;
1255
+ case "restarting":
1256
+ case "verifying-after-restart": return 3;
1257
+ case "queued": return -1;
1258
+ default: return -1;
1259
+ }
1260
+ }
1166
1261
  /**
1167
- * The ACTIVE-JOB view: type + target, previous→target, current phase, a
1168
- * determinate bar only when a real percentage exists, bytes/speed/ETA when the
1169
- * server publishes them, and what happens next. Closing the panel never
1170
- * cancels the job — say so.
1262
+ * The ACTIVE-JOB banner per the mockup: pulsing brand dot + job title with the
1263
+ * target version, mono percentage, a gradient bar only when `percent != null`,
1264
+ * phase + speed line ("3.1 MB/s (286MB / 450MB)"), and a 4-step indicator row
1265
+ * (done=emerald, active=pulsing brand, upcoming=muted).
1171
1266
  */
1172
1267
  function activeJobView(job, copy) {
1173
1268
  const section = el("section", "hqpu-group");
1174
1269
  section.dataset.group = "active-job";
1175
- const head = el("div", "hqpu-group-head");
1176
- const tag = el("span", "hqpu-tag");
1177
- tag.dataset.tone = "info";
1178
- tag.textContent = jobKindLabel(job, copy);
1179
- head.append(el("h3", "hqpu-group-title", `${jobKindLabel(job, copy)} → ${job.targetVersion}`), tag);
1180
- section.append(head);
1181
- const card = el("div", "hqpu-job");
1182
- const phaseLine = el("p", "hqpu-status hqpu-status--dot");
1183
- phaseLine.append(el("span", "hqpu-spinner"));
1184
- const transition = job.previousVersion !== null && job.previousVersion.length > 0 ? `${job.previousVersion} → ${job.targetVersion} · ${phaseLabel(job, copy)}` : `${job.targetVersion} · ${phaseLabel(job, copy)}`;
1185
- phaseLine.append(el("span", "", transition));
1186
- card.append(phaseLine);
1187
- if (typeof job.message === "string" && job.message.length > 0) card.append(el("p", "hqpu-status", job.message));
1270
+ const card = el("div", "hqpu-active");
1271
+ const top = el("div", "hqpu-active-top");
1272
+ const titleWrap = el("div", "hqpu-active-title");
1273
+ const pulse = el("span", "hqpu-pulse-dot");
1274
+ pulse.append(el("span", "hqpu-pulse-ring"));
1275
+ const title = job.kind === "runtime" ? copy.ui.jobRuntimeTitle(job.targetVersion) : copy.ui.jobPluginTitle(job.targetVersion);
1276
+ titleWrap.append(pulse, el("span", "", title));
1188
1277
  const pct = job.progress.percent;
1278
+ if (pct !== null) top.append(el("span", "hqpu-active-percent", `${Math.round(pct)}%`));
1279
+ card.append(top);
1189
1280
  if (pct !== null) {
1190
1281
  const bar = el("div", "hqpu-progress");
1191
1282
  const fill = el("div", "hqpu-progress-fill");
@@ -1193,20 +1284,27 @@ window.__ModuleLoader__.load({
1193
1284
  bar.append(fill);
1194
1285
  card.append(bar);
1195
1286
  }
1196
- const bits = [];
1197
- if (pct !== null) bits.push(`${Math.round(pct)}%`);
1287
+ const meta = el("div", "hqpu-active-meta");
1288
+ const phaseText = typeof job.message === "string" && job.message.length > 0 ? job.message : phaseLabel(job, copy);
1289
+ meta.append(el("span", "", phaseText));
1290
+ const speed = formatBytes(job.progress.bytesPerSecond);
1198
1291
  const done = formatBytes(job.progress.bytesDone);
1199
1292
  const total = formatBytes(job.progress.bytesTotal);
1200
- if (done !== null && total !== null) bits.push(copy.job.bytes(done, total));
1201
- const speed = formatBytes(job.progress.bytesPerSecond);
1202
- if (speed !== null) bits.push(copy.job.speed(speed));
1203
- const eta = formatEta(job.progress.etaSeconds);
1204
- if (eta !== null) bits.push(copy.job.eta(eta));
1205
- if (bits.length > 0) card.append(el("p", "hqpu-status hqpu-job-meta", bits.join(" · ")));
1206
- const steps = stepsList(job, copy);
1207
- if (steps !== null) card.append(steps);
1208
- card.append(el("p", "hqpu-status", job.kind === "runtime" ? copy.job.willSwapRuntime : copy.job.willRestartDsh));
1209
- card.append(el("p", "hqpu-status", copy.job.closeNote));
1293
+ const telemetry = speed !== null && done !== null && total !== null ? `${speed}/s (${done} / ${total})` : speed !== null ? `${speed}/s` : done !== null && total !== null ? copy.job.bytes(done, total) : "";
1294
+ if (telemetry.length > 0) meta.append(el("span", "", telemetry));
1295
+ card.append(meta);
1296
+ const transition = el("div", "hqpu-active-meta");
1297
+ transition.append(el("span", "", job.previousVersion !== null && job.previousVersion.length > 0 ? `${job.previousVersion} → ${job.targetVersion}` : job.targetVersion));
1298
+ transition.append(el("span", "", job.kind === "runtime" ? copy.job.willSwapRuntime : copy.job.willRestartDsh));
1299
+ card.append(transition);
1300
+ card.append(el("p", "hqpu-job-meta", copy.job.closeNote));
1301
+ const current = stepIndex(job.phase);
1302
+ const stepsRow = el("div", "hqpu-job-steps4");
1303
+ copy.ui.jobSteps.forEach((label, index) => {
1304
+ const cls = current === -1 ? "hqpu-job-step--todo" : index < current ? "hqpu-job-step--done" : index === current ? "hqpu-job-step--active" : "hqpu-job-step--todo";
1305
+ stepsRow.append(el("div", cls, label));
1306
+ });
1307
+ card.append(stepsRow);
1210
1308
  section.append(card);
1211
1309
  return section;
1212
1310
  }
@@ -1328,14 +1426,6 @@ window.__ModuleLoader__.load({
1328
1426
  }
1329
1427
  return section;
1330
1428
  }
1331
- /** Native disclosure that hides the developer-facing source editors by
1332
- * default (end users should never need to type a manifest URL). */
1333
- function advancedBlock(copy, nodes) {
1334
- const details = el("details", "hqpu-advanced");
1335
- details.append(el("summary", "", copy.release.advanced));
1336
- for (const node of nodes) if (node !== null) details.append(node);
1337
- return details;
1338
- }
1339
1429
  /**
1340
1430
  * Normalize the report's `runtime.source` to the full shape.
1341
1431
  *
@@ -1354,29 +1444,6 @@ window.__ModuleLoader__.load({
1354
1444
  override: candidate.override === true
1355
1445
  };
1356
1446
  }
1357
- /** The tag naming where the URL in effect came from. */
1358
- function originLabel(origin, copy) {
1359
- switch (origin) {
1360
- case "override": return copy.originOverride;
1361
- case "config": return copy.originConfig;
1362
- default: return copy.originUnset;
1363
- }
1364
- }
1365
- /** The 更新源 fact row: the URL in effect plus an origin tag. */
1366
- function sourceFactRow(facts, source, copy) {
1367
- const wrap = el("div");
1368
- wrap.append(el("dt", "", copy.runtime.source));
1369
- const dd = el("dd");
1370
- const line = el("div", "hqpu-source-line");
1371
- const code = el("code", "hqpu-source-url", source.url.length > 0 ? source.url : copy.runtime.notConfigured);
1372
- const tag = el("span", "hqpu-tag");
1373
- tag.dataset.tone = source.override ? "info" : source.origin === "config" ? "success" : "neutral";
1374
- tag.textContent = originLabel(source.origin, copy);
1375
- line.append(code, tag);
1376
- dd.append(line);
1377
- wrap.append(dd);
1378
- facts.append(wrap);
1379
- }
1380
1447
  /** Pick the labels for one kind out of the copy table. */
1381
1448
  function sourceLabels(copy, kind) {
1382
1449
  return kind === "runtime" ? {
@@ -1409,6 +1476,9 @@ window.__ModuleLoader__.load({
1409
1476
  const labels = sourceLabels(copy, kind);
1410
1477
  const block = el("div", `hqpu-source-editor hqpu-source-editor--${kind}`);
1411
1478
  if (!actions.sourceEditing) {
1479
+ const urlLine = el("p", "hqpu-source-current-url", source.url);
1480
+ urlLine.style.cssText = "margin:2px 0 6px;font-size:9px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#a1a1aa;word-break:break-all;";
1481
+ block.append(urlLine);
1412
1482
  block.append(el("p", "hqpu-status hqpu-source-hint", labels.hint));
1413
1483
  const edit = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", labels.edit);
1414
1484
  edit.type = "button";
@@ -1463,125 +1533,108 @@ window.__ModuleLoader__.load({
1463
1533
  return block;
1464
1534
  }
1465
1535
  /**
1466
- * The plugin release-manifest source, as its own group above the plugin
1467
- * groups: it says where the whole list comes from, so it belongs above the
1468
- * list rather than inside it. Absent on an older hq-edge (no `pluginSource`
1469
- * in the report) — then nothing is rendered at all.
1536
+ * The host's own report of the last runtime swap. Rolled-back / failed renders
1537
+ * as an AMBER alert card (title, old→new body, errorCode chip, 忽略 dismiss);
1538
+ * succeeded renders as a subtle green inline line — not a loud banner. The
1539
+ * plugin has NO log-open affordance, so "查看诊断日志" is omitted (never faked).
1470
1540
  */
1471
- function pluginSourceGroup(report, copy, actions) {
1472
- const source = normalizeSource(report.pluginSource);
1473
- if (source === null) return null;
1541
+ function runtimeResultAlert(report, copy, dismissed, onDismiss) {
1542
+ const lastResult = report.runtime.lastResult;
1543
+ if (lastResult === null || lastResult === void 0) return null;
1544
+ if (lastResult.state === "succeeded") {
1545
+ const line = el("p", "hqpu-alert-green");
1546
+ line.dataset.role = "runtime-success";
1547
+ line.textContent = copy.ui.successInline;
1548
+ return line;
1549
+ }
1550
+ if (dismissed) return null;
1474
1551
  const section = el("section", "hqpu-group");
1475
- section.dataset.group = "plugin-source";
1476
- const head = el("div", "hqpu-group-head");
1477
- head.append(el("h3", "hqpu-group-title", copy.pluginSource.title));
1478
- section.append(head);
1479
- const facts = el("dl", "hqpu-facts");
1480
- sourceFactRow(facts, source, copy);
1481
- section.append(facts);
1482
- section.append(sourceEditor("plugin", source, copy, actions));
1552
+ section.dataset.group = "runtime-alert";
1553
+ const card = el("div", "hqpu-alert hqpu-alert--amber");
1554
+ card.innerHTML = WARNING_ICON(14);
1555
+ const body = el("div");
1556
+ const title = el("div", "hqpu-alert-title");
1557
+ title.textContent = lastResult.state === "failed" ? copy.ui.failedTitle : copy.ui.rollbackTitle;
1558
+ body.append(title);
1559
+ const oldVer = lastResult.installedVersion ?? lastResult.targetVersion ?? "—";
1560
+ const code = typeof lastResult.errorCode === "string" && lastResult.errorCode.length > 0 ? lastResult.errorCode : "—";
1561
+ lastResult.state === "failed" ? copy.ui.failedBody(oldVer, code) : copy.ui.rollbackBody(oldVer, code);
1562
+ const bodyP = el("p", "hqpu-alert-body");
1563
+ const textOnly = lastResult.state === "failed" ? copy.ui.failedBody(oldVer, "") : copy.ui.rollbackBody(oldVer, "");
1564
+ bodyP.append(document.createTextNode(textOnly.replace(/\s*$/, "")));
1565
+ if (typeof lastResult.errorCode === "string" && lastResult.errorCode.length > 0) {
1566
+ bodyP.append(document.createTextNode(" "));
1567
+ const chip = el("code", "", lastResult.errorCode);
1568
+ bodyP.append(chip);
1569
+ }
1570
+ if (typeof lastResult.message === "string" && lastResult.message.length > 0) bodyP.append(el("br"), document.createTextNode(lastResult.message));
1571
+ body.append(bodyP);
1572
+ const actions = el("div", "hqpu-alert-actions");
1573
+ const dismiss = el("button", "hqpu-alert-dismiss", copy.ui.dismiss);
1574
+ dismiss.type = "button";
1575
+ dismiss.addEventListener("click", onDismiss);
1576
+ actions.append(dismiss);
1577
+ body.append(actions);
1578
+ card.append(body);
1579
+ section.append(card);
1483
1580
  return section;
1484
1581
  }
1485
- /** The independent HQ Edge runtime report, as ruled fact rows. */
1486
- function runtimeGroup(report, copy, actions) {
1487
- const section = el("section", "hqpu-group");
1488
- section.dataset.group = "runtime";
1489
- const head = el("div", "hqpu-group-head");
1490
- head.append(el("h3", "hqpu-group-title", copy.runtime.title));
1491
- section.append(head);
1492
- const facts = el("dl", "hqpu-facts");
1582
+ /** The HQ Edge runtime engine card per the mockup: current chip, "新版本 vY
1583
+ * 可用" with a 关键修复 tag when breaking/notes exist, size+summary line, a
1584
+ * 升级核心 button (existing arm→confirm flow), and the amber restart note. */
1585
+ function runtimeEngineCard(report, copy, actions) {
1493
1586
  const runtime = report.runtime;
1494
1587
  const latestVersion = runtimeLatestVersion(runtime);
1495
- const row = (label, value, code = false) => {
1496
- const wrap = el("div");
1497
- wrap.append(el("dt", "", label));
1498
- const dd = el("dd");
1499
- if (code) {
1500
- const node = el("code", "", value);
1501
- dd.append(node);
1502
- } else dd.textContent = value;
1503
- wrap.append(dd);
1504
- facts.append(wrap);
1505
- };
1506
- const source = normalizeSource(runtime.source);
1507
- if (runtime.configured === false) {
1508
- if (source !== null) sourceFactRow(facts, source, copy);
1509
- else row(copy.runtime.source, copy.runtime.notConfigured);
1510
- } else {
1511
- row(copy.runtime.current, runtime.currentVersion ?? copy.runtime.unknown);
1512
- if (runtime.updateAvailable && latestVersion !== null) {
1513
- row(copy.runtime.latest, latestVersion);
1514
- const latestEntry = runtime.latest;
1515
- const size = typeof latestEntry?.sizeBytes === "number" ? formatMb(latestEntry.sizeBytes) : "";
1516
- if (size.length > 0) row(copy.runtime.sizeLabel, size);
1517
- }
1518
- if (report.manifestVersion !== null) row(copy.runtime.set, report.manifestVersion, true);
1519
- if (typeof runtime.checkedAt === "string" && runtime.checkedAt.length > 0) row(copy.runtime.checkedAt, new Date(runtime.checkedAt).toLocaleString());
1520
- if (source !== null) sourceFactRow(facts, source, copy);
1521
- }
1522
- section.append(facts);
1523
- if (source !== null) section.append(advancedBlock(copy, [sourceEditor("runtime", source, copy, actions)]));
1588
+ const card = el("section", "hqpu-engine");
1589
+ card.dataset.group = "runtime";
1590
+ const head = el("div", "hqpu-engine-head");
1591
+ const titleWrap = el("div", "hqpu-engine-title");
1592
+ titleWrap.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.33333 2L4.66667 8H8L6.66667 14L11.33333 8H8L9.33333 2Z"/></svg>`;
1593
+ titleWrap.append(el("span", "", copy.ui.runtimeEngine));
1594
+ head.append(titleWrap);
1595
+ if (typeof runtime.currentVersion === "string" && runtime.currentVersion.length > 0) head.append(el("span", "hqpu-chip", copy.ui.currentChip(runtime.currentVersion)));
1596
+ card.append(head);
1524
1597
  if (runtime.updateAvailable && latestVersion !== null) {
1525
- const row = el("div", "hqpu-actions hqpu-runtime-actions");
1526
- if (actions.accepted) {
1527
- const line = el("span", "hqpu-status hqpu-status--dot");
1528
- line.setAttribute("role", "status");
1529
- line.append(el("span", "hqpu-spinner"), el("span", "", copy.runtime.updateAccepted));
1530
- row.append(line);
1531
- } else if (actions.busy) {
1532
- const line = el("span", "hqpu-status hqpu-status--dot");
1533
- line.setAttribute("role", "status");
1534
- line.append(el("span", "hqpu-spinner"), el("span", "", copy.runtime.updating));
1535
- row.append(line);
1536
- } else if (actions.armed) {
1537
- row.append(el("p", "hqpu-status hqpu-runtime-confirm", copy.runtime.updateConfirm(latestVersion)));
1538
- const yes = el("button", "hqpu-btn hqpu-btn--primary hqpu-btn--sm", copy.confirmInstall);
1598
+ const row = el("div", "hqpu-engine-update-row");
1599
+ const left = el("div");
1600
+ const newLine = el("div", "hqpu-engine-new-line");
1601
+ newLine.append(el("span", "", copy.ui.newVersion(latestVersion)));
1602
+ const latestEntry = runtime.latest;
1603
+ if (Array.isArray(latestEntry?.breaking) && (latestEntry?.breaking?.length ?? 0) > 0 || Array.isArray(latestEntry?.notes) && (latestEntry?.notes?.length ?? 0) > 0) newLine.append(el("span", "hqpu-tag--brand", copy.ui.breakingTag));
1604
+ left.append(newLine);
1605
+ const size = typeof latestEntry?.sizeBytes === "number" ? formatMb(latestEntry.sizeBytes) : "";
1606
+ const summary = typeof latestEntry?.summary === "string" ? latestEntry.summary : "";
1607
+ if (size.length > 0 || summary.length > 0) left.append(el("p", "hqpu-engine-size", copy.ui.sizeLine(size, summary)));
1608
+ row.append(left);
1609
+ if (actions.accepted) row.append(el("span", "hqpu-status", copy.runtime.updateAccepted));
1610
+ else if (actions.busy) row.append(el("span", "hqpu-status", copy.runtime.updating));
1611
+ else if (actions.armed) {
1612
+ const confirmWrap = el("div");
1613
+ confirmWrap.append(el("p", "hqpu-status", copy.runtime.updateConfirm(latestVersion)));
1614
+ const yes = el("button", "hqpu-btn hqpu-btn--engine", copy.confirmInstall);
1539
1615
  yes.type = "button";
1540
1616
  yes.dataset.runtimeConfirm = "";
1541
1617
  yes.addEventListener("click", actions.onConfirm);
1542
- const no = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", copy.cancel);
1618
+ const no = el("button", "hqpu-btn hqpu-btn--engine", copy.cancel);
1543
1619
  no.type = "button";
1544
1620
  no.addEventListener("click", actions.onCancel);
1545
- row.append(yes, no);
1621
+ confirmWrap.append(yes, no);
1622
+ row.append(confirmWrap);
1546
1623
  } else {
1547
- const start = el("button", "hqpu-btn hqpu-btn--primary hqpu-btn--sm");
1624
+ const start = el("button", "hqpu-btn hqpu-btn--engine", copy.ui.upgradeCore);
1548
1625
  start.type = "button";
1549
1626
  start.dataset.runtimeUpdate = "";
1550
- start.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.runtime.update}</span>`;
1551
1627
  start.addEventListener("click", actions.onArm);
1552
1628
  row.append(start);
1553
1629
  }
1554
- section.append(row);
1555
- section.append(el("p", "hqpu-status hqpu-job-meta", copy.runtime.durationNote));
1556
- } else section.append(el("p", "hqpu-status", copy.runtime.note));
1557
- const lastResult = runtime.lastResult;
1558
- if (lastResult !== null && lastResult !== void 0) {
1559
- const state = lastResult.state;
1560
- const line = el("p", "hqpu-status hqpu-status--dot");
1561
- line.setAttribute("role", "status");
1562
- const dot = el("span", "hqpu-dot");
1563
- dot.dataset.state = state === "succeeded" ? "done" : state === "failed" ? "error" : "warning";
1564
- const parts = [state === "succeeded" ? copy.runtime.lastResult.succeeded : state === "failed" ? copy.runtime.lastResult.failed : copy.runtime.lastResult.rolledBack];
1565
- if (lastResult.targetVersion) parts.push(`→ ${lastResult.targetVersion}`);
1566
- if (lastResult.installedVersion) parts.push(`(${lastResult.installedVersion})`);
1567
- if (lastResult.at) parts.push(new Date(lastResult.at).toLocaleString());
1568
- line.append(dot, el("span", "", parts.join(" ")));
1569
- section.append(line);
1570
- if (typeof lastResult.message === "string" && lastResult.message.length > 0) section.append(el("p", "hqpu-status", lastResult.message));
1571
- if (typeof lastResult.errorCode === "string" && lastResult.errorCode.length > 0) section.append(el("p", "hqpu-status hqpu-job-meta", lastResult.errorCode));
1572
- if (state !== "succeeded") section.append(el("p", "hqpu-status hqpu-job-meta", copy.runtime.lastResult.elevation));
1573
- }
1574
- if (typeof runtime.lastError === "string" && runtime.lastError.length > 0) {
1575
- const line = el("p", "hqpu-status hqpu-status--dot");
1576
- line.setAttribute("role", "status");
1577
- const dot = el("span", "hqpu-dot");
1578
- dot.dataset.state = "error";
1579
- line.append(dot, el("span", "", `${copy.lastError}: ${runtime.lastError}`));
1580
- section.append(line);
1581
- }
1582
- return section;
1630
+ card.append(row);
1631
+ const note = el("p", "hqpu-engine-note");
1632
+ note.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5L14 13H2L8 2.5Z"/></svg><span>${copy.ui.runtimeRestartNote}</span>`;
1633
+ card.append(note);
1634
+ } else if (typeof runtime.lastError === "string" && runtime.lastError.length > 0) card.append(el("p", "hqpu-status", `${copy.lastError}: ${runtime.lastError}`));
1635
+ return card;
1583
1636
  }
1584
- function renderList(section, report, copy, installing, store, onRetryCheck) {
1637
+ function renderList(section, report, copy, changelogOpen, allOpen, onToggleCard, onToggleAll, onRetryCheck) {
1585
1638
  section.textContent = "";
1586
1639
  if (report === null) {
1587
1640
  section.append(skeleton(copy));
@@ -1603,23 +1656,17 @@ window.__ModuleLoader__.load({
1603
1656
  [copy.group.current, byState("current")]
1604
1657
  ];
1605
1658
  for (const [heading, items] of order) {
1606
- const node = group(heading, items, copy, installing, store);
1659
+ const node = group(heading, items, copy, changelogOpen, allOpen, onToggleCard, onToggleAll);
1607
1660
  if (node !== null) section.append(node);
1608
1661
  }
1609
1662
  }
1610
- function renderRuntime(section, report, copy, actions) {
1611
- section.textContent = "";
1612
- if (report === null) return;
1613
- section.append(runtimeGroup(report, copy, actions));
1614
- }
1615
- /** The plugin-source group, above the plugin groups, hidden behind an advanced
1616
- * disclosure (manifest-URL editing is a developer affordance). No-op without
1617
- * the field. */
1618
- function renderPluginSource(section, report, copy, actions) {
1663
+ function renderRuntime(section, report, copy, actions, dismissedAlert, onDismissAlert) {
1619
1664
  section.textContent = "";
1620
1665
  if (report === null) return;
1621
- const node = pluginSourceGroup(report, copy, actions);
1622
- if (node !== null) section.append(advancedBlock(copy, [node]));
1666
+ const alert = runtimeResultAlert(report, copy, dismissedAlert, onDismissAlert);
1667
+ if (alert !== null) section.append(alert);
1668
+ const engine = runtimeEngineCard(report, copy, actions);
1669
+ if (engine !== null) section.append(engine);
1623
1670
  }
1624
1671
  /** Run the install transaction (a new durable job), then let the store poll it. */
1625
1672
  async function runInstall(store, copy) {
@@ -1663,45 +1710,65 @@ window.__ModuleLoader__.load({
1663
1710
  toastTimer = null;
1664
1711
  }, TOAST_HOLD_MS);
1665
1712
  }
1666
- function renderActions(bar, report, copy, store, installing) {
1667
- bar.textContent = "";
1668
- if (report === null) return;
1669
- if (installing) {
1670
- const line = el("span", "hqpu-status hqpu-status--dot");
1671
- const spinner = el("span", "hqpu-spinner");
1672
- line.append(spinner, el("span", "", copy.installing));
1673
- bar.append(line);
1713
+ /**
1714
+ * The collapsible "更新源配置 (高级)" panel (gear-toggled). It hosts the
1715
+ * EXISTING plugin-source and runtime-source editors (POST /plugin-source,
1716
+ * POST /runtime-source, save→check→refresh already wired). No new endpoints:
1717
+ * if a manifest source has no data, that field is omitted rather than faked.
1718
+ */
1719
+ function renderSettingsPanel(panel, report, copy, open, pluginActions, runtimeActions) {
1720
+ panel.textContent = "";
1721
+ panel.style.display = open ? "" : "none";
1722
+ if (!open || report === null) return;
1723
+ const row = el("div", "hqpu-settings-row");
1724
+ row.append(el("div", "hqpu-settings-title", copy.ui.settingsTitle));
1725
+ const pluginSource = normalizeSource(report.pluginSource);
1726
+ const runtimeSource = normalizeSource(report.runtime.source);
1727
+ const overridden = (pluginSource?.override ?? false) || (runtimeSource?.override ?? false);
1728
+ const badge = el("span", "hqpu-origin-badge", overridden ? copy.ui.originCustom : copy.ui.originDefault);
1729
+ badge.dataset.origin = overridden ? "override" : "config";
1730
+ row.append(badge);
1731
+ panel.append(row);
1732
+ if (pluginSource !== null) {
1733
+ panel.append(el("label", "", copy.ui.pluginUrlLabel));
1734
+ panel.append(sourceEditor("plugin", pluginSource, copy, pluginActions));
1735
+ }
1736
+ if (runtimeSource !== null) {
1737
+ panel.append(el("label", "", copy.ui.runtimeUrlLabel));
1738
+ panel.append(sourceEditor("runtime", runtimeSource, copy, runtimeActions));
1739
+ }
1740
+ }
1741
+ /**
1742
+ * The sticky footer: left meta ("准备安装 N 项兼容插件" / "版本集: vX"), a
1743
+ * full-width primary "一键更新全套插件 (vX)" (install = one durable job),
1744
+ * a secondary 忽略 wired to the existing ignore/unignore affordance, and the
1745
+ * footnote. The count EXCLUDES blocked / ignored / already-current entries.
1746
+ */
1747
+ function renderFooter(footer, installBtn, ignoreBtn, countEl, setEl, report, copy, installing, store) {
1748
+ if (report === null) {
1749
+ countEl.textContent = "";
1750
+ setEl.textContent = "";
1751
+ installBtn.disabled = true;
1752
+ ignoreBtn.style.display = "none";
1674
1753
  return;
1675
1754
  }
1676
1755
  const actionable = report.items.filter((item) => item.state === "update" || item.state === "install").length;
1677
1756
  const hasSet = report.manifestVersion !== null;
1678
- if (actionable === 0) bar.append(el("span", "hqpu-status", copy.nothingToInstall));
1679
- else {
1680
- const installBtn = el("button", "hqpu-btn hqpu-btn--primary");
1681
- installBtn.type = "button";
1682
- installBtn.disabled = !hasSet || report.ignoredSet;
1683
- installBtn.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installAll(actionable)}</span>`;
1684
- installBtn.addEventListener("click", () => {
1685
- runInstall(store, copy);
1686
- });
1687
- bar.append(installBtn);
1688
- }
1689
- if (!hasSet) return;
1690
- if (report.ignoredSet) {
1691
- const unignore = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", copy.unignore);
1692
- unignore.type = "button";
1693
- unignore.addEventListener("click", () => {
1694
- unignoreOffer(report.manifestVersion).then(() => store.refresh()).catch(() => void 0);
1695
- });
1696
- bar.append(unignore);
1697
- } else if (actionable > 0) {
1698
- const ignore = el("button", "hqpu-btn hqpu-btn--ghost hqpu-btn--sm", copy.ignore);
1699
- ignore.type = "button";
1700
- ignore.addEventListener("click", () => {
1701
- ignoreOffer(report.manifestVersion).then(() => store.refresh()).catch(() => void 0);
1702
- });
1703
- bar.append(ignore);
1704
- }
1757
+ const setVersion = report.manifestVersion ?? "";
1758
+ countEl.textContent = copy.ui.footerCount(actionable);
1759
+ setEl.textContent = hasSet ? copy.ui.footerSet(setVersion) : "";
1760
+ installBtn.textContent = hasSet ? copy.ui.installAllSet(setVersion) : copy.ui.installAllSet("—");
1761
+ installBtn.disabled = installing || actionable === 0 || !hasSet || report.ignoredSet;
1762
+ installBtn.dataset.busy = installing ? "true" : "false";
1763
+ installBtn.onclick = () => {
1764
+ runInstall(store, copy);
1765
+ };
1766
+ ignoreBtn.style.display = hasSet ? "" : "none";
1767
+ ignoreBtn.textContent = report.ignoredSet ? copy.unignore : copy.ui.ignoreBtn;
1768
+ ignoreBtn.onclick = () => {
1769
+ const version = report.manifestVersion;
1770
+ (report.ignoredSet ? unignoreOffer(version) : ignoreOffer(version)).then(() => store.refresh()).catch(() => void 0);
1771
+ };
1705
1772
  }
1706
1773
  /**
1707
1774
  * Open (or focus) the update panel. Re-renders from the store; when the store
@@ -1726,40 +1793,53 @@ window.__ModuleLoader__.load({
1726
1793
  panel.tabIndex = -1;
1727
1794
  const head = el("div", "hqpu-head");
1728
1795
  const headMain = el("div", "hqpu-head-main");
1729
- headMain.append(el("h1", "hqpu-title", copy.viewTitle));
1730
- const intro = el("p", "hqpu-intro");
1731
- intro.append(el("span", "", copy.viewIntro));
1732
- const info = el("button", "hqpu-info");
1733
- info.type = "button";
1734
- info.setAttribute("aria-label", copy.viewInfo);
1735
- info.title = copy.viewInfo;
1736
- info.innerHTML = INFO_ICON(11);
1737
- intro.append(info);
1738
- headMain.append(intro);
1796
+ const brandIcon = el("span", "hqpu-brand-icon");
1797
+ brandIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5.6 2.6a6 6 0 015.6 1.6M12.4 13.4a6 6 0 01-5.6-1.6M10.6 2V5.4H7.2M5.4 14V10.6H8.8"/></svg>`;
1798
+ const titleBlock = el("div", "hqpu-title-block");
1799
+ titleBlock.append(el("h1", "hqpu-title", copy.ui.title));
1800
+ const sub = el("p", "hqpu-sub");
1801
+ titleBlock.append(sub);
1802
+ headMain.append(brandIcon, titleBlock);
1739
1803
  const toolbar = el("div", "hqpu-toolbar");
1740
1804
  const refreshBtn = iconButton("hqpu-icon-btn", REFRESH_ICON(16), copy.refresh, () => {
1741
- store.refresh();
1805
+ store.check();
1742
1806
  });
1743
1807
  toolbar.append(refreshBtn);
1808
+ const gearBtn = iconButton("hqpu-icon-btn", `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2.2"/><path d="M13.5 9.2v-2.4l-1.8-.6a4.4 4.4 0 00-.5-1.2l.9-1.7-1.7-1.7-1.7.9a4.4 4.4 0 00-1.2-.5L7.2 2.5H4.8l-.6 1.8a4.4 4.4 0 00-1.2.5l-1.7-.9-1.7 1.7.9 1.7a4.4 4.4 0 00-.5 1.2L2.5 7.2v2.4l1.8.6a4.4 4.4 0 00.5 1.2l-.9 1.7 1.7 1.7 1.7-.9a4.4 4.4 0 001.2.5l.6 1.8h2.4l.6-1.8a4.4 4.4 0 001.2-.5l1.7.9 1.7-1.7-.9-1.7a4.4 4.4 0 00.5-1.2l1.8-.6z"/></svg>`, copy.ui.gearTooltip, () => {
1809
+ settingsOpen = !settingsOpen;
1810
+ render();
1811
+ });
1812
+ toolbar.append(gearBtn);
1744
1813
  const closeBtn = iconButton("hqpu-icon-btn", CLOSE_ICON(16), copy.close, () => {
1745
1814
  onClose();
1746
1815
  });
1747
1816
  toolbar.append(closeBtn);
1748
1817
  head.append(headMain, toolbar);
1749
1818
  panel.append(head);
1819
+ const settingsPanel = el("div", "hqpu-settings");
1820
+ panel.append(settingsPanel);
1750
1821
  const scroll = el("div", "hqpu-scroll");
1751
1822
  const column = el("div", "hqpu-column");
1752
1823
  const jobSection = el("div", "hqpu-stack");
1753
- const pluginSourceSection = el("div", "hqpu-stack");
1754
1824
  const listSection = el("div", "hqpu-stack");
1755
1825
  const runtimeSection = el("div", "hqpu-stack");
1756
1826
  const toastSection = el("div", "hqpu-toast-host");
1757
- column.append(jobSection, pluginSourceSection, listSection, runtimeSection, toastSection);
1827
+ column.append(jobSection, listSection, runtimeSection, toastSection);
1758
1828
  scroll.append(column);
1759
1829
  panel.append(scroll);
1760
1830
  const footer = el("div", "hqpu-footer");
1761
- const actionBar = el("div", "hqpu-actions");
1762
- footer.append(actionBar);
1831
+ const footMeta = el("div", "hqpu-foot-meta");
1832
+ const footCount = el("span", "");
1833
+ const footSet = el("span", "hqpu-foot-set");
1834
+ footMeta.append(footCount, footSet);
1835
+ const footActions = el("div", "hqpu-foot-actions");
1836
+ const installAllBtn = el("button", "hqpu-btn hqpu-btn--block");
1837
+ installAllBtn.type = "button";
1838
+ const ignoreBtn = el("button", "hqpu-btn--ignore", copy.ui.ignoreBtn);
1839
+ ignoreBtn.type = "button";
1840
+ footActions.append(installAllBtn, ignoreBtn);
1841
+ const footnote = el("p", "hqpu-footnote", copy.ui.footnote);
1842
+ footer.append(footMeta, footActions, footnote);
1763
1843
  panel.append(footer);
1764
1844
  overlay.append(scrim, panel);
1765
1845
  document.body.appendChild(overlay);
@@ -1769,6 +1849,10 @@ window.__ModuleLoader__.load({
1769
1849
  let runtimeArmed = false;
1770
1850
  let runtimeBusy = false;
1771
1851
  let runtimeAccepted = false;
1852
+ const changelogOpen = /* @__PURE__ */ new Set();
1853
+ let allOpen = false;
1854
+ let settingsOpen = false;
1855
+ let dismissedAlert = false;
1772
1856
  const runtimeSourceState = {
1773
1857
  editing: false,
1774
1858
  saving: false,
@@ -1935,16 +2019,29 @@ window.__ModuleLoader__.load({
1935
2019
  const installing = store.installing() || jobActive;
1936
2020
  const stale = report === null;
1937
2021
  refreshBtn.disabled = stale;
2022
+ refreshBtn.title = copy.refresh;
2023
+ const ago = relativeAgo(report?.checkedAt, copy);
2024
+ sub.textContent = ago !== null ? copy.ui.checkedAgo(ago) : copy.ui.checkedUnknown;
2025
+ renderSettingsPanel(settingsPanel, report, copy, settingsOpen, pluginActions, runtimeSourceActions);
1938
2026
  renderJobSection();
1939
2027
  if (stale) {
1940
2028
  listSection.textContent = "";
1941
2029
  listSection.append(skeleton(copy));
1942
- } else renderList(listSection, report, copy, installing, store, () => {
2030
+ } else renderList(listSection, report, copy, changelogOpen, allOpen, (name) => {
2031
+ if (changelogOpen.has(name)) changelogOpen.delete(name);
2032
+ else changelogOpen.add(name);
2033
+ render();
2034
+ }, () => {
2035
+ allOpen = !allOpen;
2036
+ render();
2037
+ }, () => {
1943
2038
  store.check();
1944
2039
  });
1945
- renderPluginSource(pluginSourceSection, report, copy, pluginActions);
1946
- renderRuntime(runtimeSection, report, copy, runtimeActions);
1947
- renderActions(actionBar, report, copy, store, installing);
2040
+ renderRuntime(runtimeSection, report, copy, runtimeActions, dismissedAlert, () => {
2041
+ dismissedAlert = true;
2042
+ render();
2043
+ });
2044
+ renderFooter(footer, installAllBtn, ignoreBtn, footCount, footSet, report, copy, installing, store);
1948
2045
  };
1949
2046
  const unsubscribe = store.subscribe(render);
1950
2047
  disposers.push(unsubscribe);
@@ -2834,10 +2931,649 @@ window.__ModuleLoader__.load({
2834
2931
  .hqpu-panel,
2835
2932
  .hqpu-skeleton-fill,
2836
2933
  .hqpu-spinner,
2837
- .hqpu-toast {
2934
+ .hqpu-toast,
2935
+ .hqpu-pulse-ring,
2936
+ .hqpu-job-step--active {
2838
2937
  animation: none;
2839
2938
  }
2840
2939
  }
2940
+
2941
+ /* ════════════════════════════════════════════════════════════════════════
2942
+ * Refactored dock (interactive_refactored_ui_mockup.html) — a narrow dark-zinc
2943
+ * update-center column. These rules cascade over the shell-token base: the
2944
+ * panel becomes ~340px wide, the zinc palette hard-coded per mockup, with a
2945
+ * sticky header / scroll body / sticky footer. No Tailwind, no dependencies.
2946
+ * ════════════════════════════════════════════════════════════════════════ */
2947
+ .hqpu-panel,
2948
+ .hqpu-panel * {
2949
+ --hq-bg: #18181b;
2950
+ --hq-card: #27272a;
2951
+ --hq-card-soft: #1c1c1f;
2952
+ --hq-border: #3f3f46;
2953
+ --hq-hover: #323236;
2954
+ --hq-text: #f4f4f5;
2955
+ --hq-muted: #a1a1aa;
2956
+ --hq-faint: #71717a;
2957
+ --hq-brand: #dc2626;
2958
+ --hq-brand-soft: #ef4444;
2959
+ --hq-emerald: #34d399;
2960
+ --hq-amber: #f59e0b;
2961
+ }
2962
+ .hqpu-panel {
2963
+ width: min(360px, calc(100vw - 32px));
2964
+ max-height: min(86vh, 720px);
2965
+ background: var(--hq-bg);
2966
+ color: var(--hq-text);
2967
+ border: 0.5px solid var(--hq-border);
2968
+ border-radius: 12px;
2969
+ gap: 0;
2970
+ overflow: hidden;
2971
+ }
2972
+ .hqpu-scrim {
2973
+ background: rgba(0, 0, 0, 0.55);
2974
+ }
2975
+
2976
+ /* Sticky header */
2977
+ .hqpu-head {
2978
+ flex: none;
2979
+ display: flex;
2980
+ align-items: center;
2981
+ justify-content: space-between;
2982
+ gap: 8px;
2983
+ padding: 10px 12px;
2984
+ border-bottom: 0.5px solid var(--hq-border);
2985
+ background: color-mix(in srgb, var(--hq-bg) 95%, transparent);
2986
+ position: sticky;
2987
+ top: 0;
2988
+ z-index: 2;
2989
+ }
2990
+ .hqpu-head-main {
2991
+ display: flex;
2992
+ align-items: center;
2993
+ gap: 8px;
2994
+ min-width: 0;
2995
+ margin: 0;
2996
+ }
2997
+ .hqpu-brand-icon {
2998
+ display: flex;
2999
+ align-items: center;
3000
+ justify-content: center;
3001
+ flex: none;
3002
+ width: 24px;
3003
+ height: 24px;
3004
+ border-radius: 6px;
3005
+ background: color-mix(in srgb, var(--hq-brand) 20%, transparent);
3006
+ color: var(--hq-brand-soft);
3007
+ }
3008
+ .hqpu-brand-icon svg { width: 14px; height: 14px; display: block; }
3009
+ .hqpu-title-block { min-width: 0; }
3010
+ .hqpu-title {
3011
+ margin: 0;
3012
+ font-size: 13px;
3013
+ font-weight: 600;
3014
+ line-height: 16px;
3015
+ color: var(--hq-text);
3016
+ }
3017
+ .hqpu-sub {
3018
+ margin: 2px 0 0;
3019
+ font-size: 10px;
3020
+ line-height: 14px;
3021
+ color: var(--hq-muted);
3022
+ white-space: nowrap;
3023
+ overflow: hidden;
3024
+ text-overflow: ellipsis;
3025
+ }
3026
+ .hqpu-toolbar { gap: 2px; }
3027
+ .hqpu-icon-btn {
3028
+ width: 26px;
3029
+ height: 26px;
3030
+ border-radius: 6px;
3031
+ color: var(--hq-muted);
3032
+ }
3033
+ .hqpu-icon-btn:hover:not(:disabled) {
3034
+ background: var(--hq-hover);
3035
+ color: var(--hq-text);
3036
+ }
3037
+ .hqpu-icon-btn svg { width: 14px; height: 14px; }
3038
+
3039
+ /* Collapsible settings panel */
3040
+ .hqpu-settings {
3041
+ flex: none;
3042
+ padding: 10px 12px;
3043
+ border-bottom: 0.5px solid var(--hq-border);
3044
+ background: #131316;
3045
+ }
3046
+ .hqpu-settings-row {
3047
+ display: flex;
3048
+ align-items: center;
3049
+ justify-content: space-between;
3050
+ gap: 8px;
3051
+ margin-bottom: 8px;
3052
+ }
3053
+ .hqpu-settings-title {
3054
+ font-size: 11px;
3055
+ font-weight: 500;
3056
+ color: #d4d4d8;
3057
+ }
3058
+ .hqpu-origin-badge {
3059
+ font-size: 9px;
3060
+ padding: 1px 6px;
3061
+ border-radius: 4px;
3062
+ border: 0.5px solid;
3063
+ }
3064
+ .hqpu-origin-badge[data-origin='config'] {
3065
+ background: rgba(6, 78, 59, 0.4);
3066
+ color: var(--hq-emerald);
3067
+ border-color: rgba(6, 95, 70, 0.6);
3068
+ }
3069
+ .hqpu-origin-badge[data-origin='override'] {
3070
+ background: rgba(120, 53, 15, 0.4);
3071
+ color: var(--hq-amber);
3072
+ border-color: rgba(180, 83, 9, 0.5);
3073
+ }
3074
+ .hqpu-settings label {
3075
+ display: block;
3076
+ font-size: 10px;
3077
+ color: var(--hq-muted);
3078
+ margin: 6px 0 3px;
3079
+ }
3080
+ .hqpu-source-input {
3081
+ width: 100%;
3082
+ height: 26px;
3083
+ font-size: 10px;
3084
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3085
+ background: var(--hq-bg);
3086
+ border: 0.5px solid var(--hq-border);
3087
+ border-radius: 5px;
3088
+ color: #d4d4d8;
3089
+ padding: 0 8px;
3090
+ box-sizing: border-box;
3091
+ }
3092
+ .hqpu-source-input:focus-visible {
3093
+ outline: none;
3094
+ border-color: var(--hq-brand-soft);
3095
+ }
3096
+ .hqpu-settings-actions {
3097
+ display: flex;
3098
+ justify-content: flex-end;
3099
+ gap: 6px;
3100
+ margin-top: 8px;
3101
+ }
3102
+
3103
+ /* Scroll body */
3104
+ .hqpu-scroll {
3105
+ flex: 1;
3106
+ padding: 12px;
3107
+ background: var(--hq-bg);
3108
+ --dsh-scrollbar-thumb: #3f3f46;
3109
+ --dsh-scrollbar-thumb-hover: #52525b;
3110
+ }
3111
+ .hqpu-column {
3112
+ max-width: none;
3113
+ margin: 0;
3114
+ gap: 10px;
3115
+ }
3116
+ .hqpu-stack { gap: 10px; }
3117
+
3118
+ /* ── Active-job banner ─────────────────────────────────────────────────── */
3119
+ .hqpu-active {
3120
+ position: relative;
3121
+ padding: 12px;
3122
+ border-radius: 8px;
3123
+ border: 0.5px solid color-mix(in srgb, var(--hq-brand-soft) 30%, transparent);
3124
+ background: #18181b;
3125
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
3126
+ }
3127
+ .hqpu-active-top {
3128
+ display: flex;
3129
+ align-items: center;
3130
+ justify-content: space-between;
3131
+ gap: 8px;
3132
+ }
3133
+ .hqpu-active-title {
3134
+ display: flex;
3135
+ align-items: center;
3136
+ gap: 6px;
3137
+ min-width: 0;
3138
+ font-size: 11px;
3139
+ font-weight: 600;
3140
+ color: #e4e4e7;
3141
+ }
3142
+ .hqpu-pulse-dot {
3143
+ position: relative;
3144
+ flex: none;
3145
+ width: 8px;
3146
+ height: 8px;
3147
+ }
3148
+ .hqpu-pulse-dot::before {
3149
+ content: '';
3150
+ position: absolute;
3151
+ inset: 0;
3152
+ border-radius: 50%;
3153
+ background: var(--hq-brand-soft);
3154
+ }
3155
+ .hqpu-pulse-ring {
3156
+ position: absolute;
3157
+ inset: 0;
3158
+ border-radius: 50%;
3159
+ background: var(--hq-brand-soft);
3160
+ opacity: 0.6;
3161
+ animation: hqpu-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
3162
+ }
3163
+ @keyframes hqpu-ping {
3164
+ 75%, 100% { transform: scale(2.2); opacity: 0; }
3165
+ }
3166
+ .hqpu-active-percent {
3167
+ flex: none;
3168
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3169
+ font-size: 11px;
3170
+ font-weight: 500;
3171
+ color: var(--hq-brand-soft);
3172
+ }
3173
+ .hqpu-progress {
3174
+ height: 6px;
3175
+ margin-top: 10px;
3176
+ border-radius: 999px;
3177
+ background: #27272a;
3178
+ overflow: hidden;
3179
+ }
3180
+ .hqpu-progress-fill {
3181
+ background: linear-gradient(to right, var(--hq-brand), var(--hq-brand-soft));
3182
+ transition: width 0.3s ease;
3183
+ }
3184
+ .hqpu-active-meta {
3185
+ display: flex;
3186
+ justify-content: space-between;
3187
+ gap: 8px;
3188
+ margin-top: 4px;
3189
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3190
+ font-size: 10px;
3191
+ color: var(--hq-muted);
3192
+ }
3193
+ /* 4-step indicator row */
3194
+ .hqpu-job-steps4 {
3195
+ display: grid;
3196
+ grid-template-columns: repeat(4, 1fr);
3197
+ gap: 4px;
3198
+ margin-top: 10px;
3199
+ padding-top: 8px;
3200
+ border-top: 0.5px solid #27272a;
3201
+ text-align: center;
3202
+ font-size: 9px;
3203
+ font-weight: 500;
3204
+ }
3205
+ .hqpu-job-step--done { color: var(--hq-emerald); }
3206
+ .hqpu-job-step--active {
3207
+ color: var(--hq-brand-soft);
3208
+ animation: hqpu-step-pulse 1.2s ease-in-out infinite;
3209
+ }
3210
+ .hqpu-job-step--todo { color: var(--hq-muted); }
3211
+ @keyframes hqpu-step-pulse {
3212
+ 0%, 100% { opacity: 1; }
3213
+ 50% { opacity: 0.45; }
3214
+ }
3215
+
3216
+ /* ── Result / rollback alert (amber) vs green success ──────────────────── */
3217
+ .hqpu-alert {
3218
+ display: flex;
3219
+ align-items: flex-start;
3220
+ gap: 8px;
3221
+ padding: 10px;
3222
+ border-radius: 8px;
3223
+ }
3224
+ .hqpu-alert--amber {
3225
+ background: rgba(69, 26, 3, 0.35);
3226
+ border: 0.5px solid rgba(180, 83, 9, 0.4);
3227
+ }
3228
+ .hqpu-alert--amber svg { color: var(--hq-amber); flex: none; margin-top: 1px; }
3229
+ .hqpu-alert-green {
3230
+ background: rgba(6, 78, 59, 0.3);
3231
+ border: 0.5px solid rgba(6, 95, 70, 0.4);
3232
+ color: #a7f3d0;
3233
+ font-size: 11px;
3234
+ }
3235
+ .hqpu-alert-title {
3236
+ font-size: 11px;
3237
+ font-weight: 500;
3238
+ color: #fcd34d;
3239
+ }
3240
+ .hqpu-alert-body {
3241
+ margin: 2px 0 0;
3242
+ font-size: 10px;
3243
+ line-height: 15px;
3244
+ color: rgba(252, 211, 77, 0.8);
3245
+ }
3246
+ .hqpu-alert-body code {
3247
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3248
+ background: rgba(120, 53, 15, 0.5);
3249
+ padding: 1px 4px;
3250
+ border-radius: 3px;
3251
+ }
3252
+ .hqpu-alert-actions {
3253
+ display: flex;
3254
+ gap: 8px;
3255
+ margin-top: 6px;
3256
+ }
3257
+ .hqpu-alert-dismiss {
3258
+ background: none;
3259
+ border: 0;
3260
+ padding: 0;
3261
+ font-size: 10px;
3262
+ color: #fbbf24;
3263
+ cursor: pointer;
3264
+ text-decoration: underline;
3265
+ }
3266
+
3267
+ /* ── Runtime engine card ────────────────────────────────────────────────── */
3268
+ .hqpu-engine {
3269
+ padding: 12px;
3270
+ border-radius: 8px;
3271
+ border: 0.5px solid var(--hq-border);
3272
+ background: var(--hq-card);
3273
+ }
3274
+ .hqpu-engine-head {
3275
+ display: flex;
3276
+ align-items: center;
3277
+ justify-content: space-between;
3278
+ gap: 8px;
3279
+ }
3280
+ .hqpu-engine-title {
3281
+ display: flex;
3282
+ align-items: center;
3283
+ gap: 6px;
3284
+ font-size: 12px;
3285
+ font-weight: 600;
3286
+ color: #e4e4e7;
3287
+ }
3288
+ .hqpu-engine-title svg { width: 14px; height: 14px; color: #a1a1aa; }
3289
+ .hqpu-chip {
3290
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3291
+ font-size: 9px;
3292
+ padding: 2px 6px;
3293
+ border-radius: 4px;
3294
+ background: #27272a;
3295
+ border: 0.5px solid #52525b;
3296
+ color: #d4d4d8;
3297
+ }
3298
+ .hqpu-engine-update-row {
3299
+ display: flex;
3300
+ align-items: center;
3301
+ justify-content: space-between;
3302
+ gap: 8px;
3303
+ margin-top: 10px;
3304
+ padding: 8px;
3305
+ border-radius: 6px;
3306
+ border: 0.5px solid rgba(63, 63, 70, 0.7);
3307
+ background: rgba(24, 24, 27, 0.6);
3308
+ }
3309
+ .hqpu-engine-new-line {
3310
+ display: flex;
3311
+ align-items: center;
3312
+ gap: 4px;
3313
+ font-size: 11px;
3314
+ font-weight: 500;
3315
+ color: #d4d4d8;
3316
+ }
3317
+ .hqpu-tag--brand {
3318
+ font-size: 9px;
3319
+ padding: 1px 4px;
3320
+ border-radius: 3px;
3321
+ color: var(--hq-brand-soft);
3322
+ background: rgba(69, 10, 10, 0.6);
3323
+ border: 0.5px solid rgba(153, 27, 27, 0.5);
3324
+ }
3325
+ .hqpu-engine-size {
3326
+ margin: 3px 0 0;
3327
+ font-size: 10px;
3328
+ color: var(--hq-muted);
3329
+ overflow: hidden;
3330
+ text-overflow: ellipsis;
3331
+ white-space: nowrap;
3332
+ }
3333
+ .hqpu-btn--engine {
3334
+ flex: none;
3335
+ height: auto;
3336
+ padding: 6px 10px;
3337
+ font-size: 10px;
3338
+ background: #27272a;
3339
+ border: 0.5px solid #52525b;
3340
+ color: #e4e4e7;
3341
+ border-radius: 5px;
3342
+ }
3343
+ .hqpu-btn--engine:hover:not(:disabled) { background: #3f3f46; }
3344
+ .hqpu-engine-note {
3345
+ display: flex;
3346
+ align-items: center;
3347
+ gap: 4px;
3348
+ margin: 8px 0 0;
3349
+ font-size: 9px;
3350
+ color: var(--hq-muted);
3351
+ }
3352
+ .hqpu-engine-note svg { width: 10px; height: 10px; color: var(--hq-amber); flex: none; }
3353
+
3354
+ /* ── Plugin cards ───────────────────────────────────────────────────────── */
3355
+ .hqpu-section-head {
3356
+ display: flex;
3357
+ align-items: center;
3358
+ justify-content: space-between;
3359
+ gap: 8px;
3360
+ padding: 0 2px;
3361
+ }
3362
+ .hqpu-section-title {
3363
+ display: flex;
3364
+ align-items: center;
3365
+ gap: 6px;
3366
+ font-size: 12px;
3367
+ font-weight: 600;
3368
+ color: #e4e4e7;
3369
+ }
3370
+ .hqpu-count-pill {
3371
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3372
+ font-size: 10px;
3373
+ font-weight: 700;
3374
+ padding: 1px 6px;
3375
+ border-radius: 999px;
3376
+ background: color-mix(in srgb, var(--hq-brand) 20%, transparent);
3377
+ color: var(--hq-brand-soft);
3378
+ }
3379
+ .hqpu-all-changelog {
3380
+ background: none;
3381
+ border: 0;
3382
+ padding: 0;
3383
+ font-size: 10px;
3384
+ color: var(--hq-muted);
3385
+ cursor: pointer;
3386
+ }
3387
+ .hqpu-all-changelog:hover { color: var(--hq-text); }
3388
+
3389
+ .hqpu-cards { gap: 8px; }
3390
+ .hqpu-card {
3391
+ margin: 0;
3392
+ padding: 10px;
3393
+ border-radius: 8px;
3394
+ border: 0.5px solid var(--hq-border);
3395
+ background: var(--hq-card);
3396
+ transition: border-color 0.15s ease;
3397
+ }
3398
+ .hqpu-card:hover { border-color: #52525b; }
3399
+ .hqpu-card-head {
3400
+ display: flex;
3401
+ align-items: flex-start;
3402
+ justify-content: space-between;
3403
+ gap: 8px;
3404
+ padding: 0;
3405
+ }
3406
+ .hqpu-card-head > * { display: flex; align-items: center; gap: 8px; min-width: 0; }
3407
+ .hqpu-avatar {
3408
+ display: flex;
3409
+ align-items: center;
3410
+ justify-content: center;
3411
+ flex: none;
3412
+ width: 28px;
3413
+ height: 28px;
3414
+ border-radius: 6px;
3415
+ border: 0.5px solid #52525b;
3416
+ background: #27272a;
3417
+ color: #d4d4d8;
3418
+ font-size: 10px;
3419
+ font-weight: 700;
3420
+ }
3421
+ .hqpu-card-title {
3422
+ font-size: 12px;
3423
+ font-weight: 600;
3424
+ color: #e4e4e7;
3425
+ }
3426
+ .hqpu-card-sub {
3427
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3428
+ font-size: 9px;
3429
+ color: var(--hq-faint);
3430
+ }
3431
+ .hqpu-version-row {
3432
+ display: flex;
3433
+ align-items: center;
3434
+ gap: 4px;
3435
+ margin-top: 2px;
3436
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3437
+ font-size: 10px;
3438
+ }
3439
+ .hqpu-ver-old { color: var(--hq-muted); }
3440
+ .hqpu-ver-arrow { color: #52525b; }
3441
+ .hqpu-ver-new { color: var(--hq-emerald); font-weight: 700; }
3442
+ .hqpu-log-toggle {
3443
+ display: inline-flex;
3444
+ align-items: center;
3445
+ gap: 2px;
3446
+ background: none;
3447
+ border: 0;
3448
+ padding: 4px;
3449
+ font-size: 10px;
3450
+ color: var(--hq-muted);
3451
+ cursor: pointer;
3452
+ }
3453
+ .hqpu-log-toggle:hover { color: var(--hq-text); }
3454
+ .hqpu-log-toggle svg { width: 12px; height: 12px; transition: transform 0.15s ease; }
3455
+ .hqpu-log-toggle[data-open='true'] svg { transform: rotate(180deg); }
3456
+ .hqpu-changelog-drawer {
3457
+ margin-top: 8px;
3458
+ padding: 8px;
3459
+ border-radius: 6px;
3460
+ border: 0.5px solid #27272a;
3461
+ background: rgba(24, 24, 27, 0.8);
3462
+ font-size: 10px;
3463
+ }
3464
+ .hqpu-changelog-drawer-title {
3465
+ font-size: 9px;
3466
+ font-weight: 500;
3467
+ text-transform: uppercase;
3468
+ letter-spacing: 0.05em;
3469
+ color: #a1a1aa;
3470
+ }
3471
+ .hqpu-changelog-drawer ul {
3472
+ margin: 4px 0 0;
3473
+ padding-left: 16px;
3474
+ color: #d4d4d8;
3475
+ line-height: 16px;
3476
+ }
3477
+
3478
+ /* Blocked / incompatible card: dimmed, rose badge, no install button */
3479
+ .hqpu-card--blocked {
3480
+ opacity: 0.8;
3481
+ background: rgba(39, 39, 42, 0.6);
3482
+ border-color: #27272a;
3483
+ }
3484
+ .hqpu-card--blocked .hqpu-avatar {
3485
+ background: rgba(39, 39, 42, 0.5);
3486
+ border-color: #27272a;
3487
+ color: #71717a;
3488
+ }
3489
+ .hqpu-card--blocked .hqpu-card-title { color: #a1a1aa; }
3490
+ .hqpu-blocked-badge {
3491
+ font-size: 9px;
3492
+ padding: 2px 6px;
3493
+ border-radius: 4px;
3494
+ color: #fb7185;
3495
+ background: rgba(76, 5, 25, 0.5);
3496
+ border: 0.5px solid rgba(136, 19, 55, 0.5);
3497
+ }
3498
+ .hqpu-reason-line {
3499
+ display: flex;
3500
+ align-items: flex-start;
3501
+ gap: 4px;
3502
+ margin: 8px 0 0;
3503
+ padding: 6px;
3504
+ border-radius: 5px;
3505
+ border: 0.5px solid rgba(136, 19, 55, 0.2);
3506
+ background: rgba(76, 5, 25, 0.2);
3507
+ font-size: 10px;
3508
+ color: rgba(251, 113, 133, 0.9);
3509
+ }
3510
+ .hqpu-reason-line svg { width: 12px; height: 12px; flex: none; margin-top: 1px; }
3511
+
3512
+ /* Sticky footer */
3513
+ .hqpu-footer {
3514
+ flex: none;
3515
+ display: flex;
3516
+ flex-direction: column;
3517
+ align-items: stretch;
3518
+ gap: 8px;
3519
+ padding: 10px 12px 12px;
3520
+ border-top: 0.5px solid var(--hq-border);
3521
+ background: color-mix(in srgb, var(--hq-bg) 95%, transparent);
3522
+ }
3523
+ .hqpu-foot-meta {
3524
+ display: flex;
3525
+ align-items: center;
3526
+ justify-content: space-between;
3527
+ gap: 8px;
3528
+ font-size: 10px;
3529
+ color: var(--hq-muted);
3530
+ }
3531
+ .hqpu-foot-set {
3532
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3533
+ }
3534
+ .hqpu-foot-actions {
3535
+ display: flex;
3536
+ gap: 8px;
3537
+ }
3538
+ .hqpu-btn--block {
3539
+ flex: 1;
3540
+ height: 32px;
3541
+ background: var(--hq-brand);
3542
+ color: #fff;
3543
+ font-size: 11px;
3544
+ font-weight: 500;
3545
+ border-radius: 6px;
3546
+ box-shadow: 0 4px 12px rgba(127, 29, 29, 0.4);
3547
+ }
3548
+ .hqpu-btn--block:hover:not(:disabled) { background: var(--hq-brand-soft); }
3549
+ .hqpu-btn--block:disabled { opacity: 0.5; box-shadow: none; }
3550
+ .hqpu-btn--ignore {
3551
+ height: 32px;
3552
+ padding: 0 10px;
3553
+ background: var(--hq-card);
3554
+ border: 0.5px solid var(--hq-border);
3555
+ color: var(--hq-muted);
3556
+ font-size: 10px;
3557
+ border-radius: 6px;
3558
+ }
3559
+ .hqpu-btn--ignore:hover:not(:disabled) { background: var(--hq-hover); color: var(--hq-text); }
3560
+ .hqpu-footnote {
3561
+ margin: 0;
3562
+ font-size: 9px;
3563
+ text-align: center;
3564
+ color: var(--hq-muted);
3565
+ }
3566
+
3567
+ /* Shared bits on the new palette */
3568
+ .hqpu-status { color: var(--hq-muted); font-size: 11px; }
3569
+ .hqpu-empty { color: var(--hq-muted); font-size: 11px; padding: 16px 0; }
3570
+ .hqpu-toast { background: var(--hq-card); border: 0.5px solid var(--hq-border); color: var(--hq-text); font-size: 12px; }
3571
+ .hqpu-skeleton-fill { background: #3f3f46; }
3572
+ .hqpu-job { background: var(--hq-card); border-color: var(--hq-border); }
3573
+ .hqpu-job-meta { color: var(--hq-muted); }
3574
+ .hqpu-release-notes { color: var(--hq-muted); font-size: 11px; }
3575
+ .hqpu-advanced > summary { color: var(--hq-muted); }
3576
+ .hqpu-recent-jobs li { color: var(--hq-muted); font-size: 10px; }
2841
3577
  `;
2842
3578
  const STYLE_TAG_ID = "data-plugin-css=\"huaqiu-plugin-update\"";
2843
3579
  /**