dsh-plugin-update 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  格式为 Keep-a-Changelog 子集:`Added/Fixed/Changed` 必写,`Deprecated/Removed/Security` 有则透传,`Unreleased` 面板忽略,禁止 git-log 直倒。面板缺日志时中性提示,不挡安装。
4
4
 
5
+ ## [0.7.1] - 2026-10-06
6
+
7
+ ### Fixed
8
+ - 批量行三列对齐 + pending 显示最新版 + 新版红(#71):行主区改网格(名 5em/版 18ch/态 1fr,版 nowrap + tabular-nums,态换行同列起,失败跨整行);版本三段式只红新版(老版本弱化/箭头中性/专用红 token `newText`,深浅主题与档案皮肤及 forced-colors 回退,加粗 + 箭头 + 状态词为非颜色信号);版本会话优先、知识仅展示,无轮次无知识回还没查过(不再误 wait-turn),轮次中仍执行态;会话/安装语义未动。
9
+ - 默认弹窗 01~05 横滑消除 + 疏朗化(#72,A 方案落地)。
10
+
11
+ ### Added
12
+ - 主题变量新增 `newText`(`--dsh-update-new-text`,有新版版本号专用红,可覆盖,非法即抛)。
13
+
14
+ ## [0.7.0] - 2026-10-06
15
+
16
+ ### Added
17
+ - 主题变量覆盖 `themeTokens`(第三方换肤一等口径):单面板、批量面板、入口件、批量入口件四个挂载口同收同语义(35 个语义键对 `--dsh-update-*` 变量,颜色收 hex 或英文名,其余走注入检查,非法挂载即抛);内联写根、变量继承,入口件打开的 dialog 同步透传,批量展开的详情行经继承生效;控制器同步新增 `setThemeTokens`(传 `undefined` 即清掉覆盖);被覆盖的 token 不再跟随深色媒体查询。README 补换肤章节与键表。
5
18
  ## [0.6.0] - 2026-10-06
6
19
 
7
20
  ### Changed
package/README.md CHANGED
@@ -142,8 +142,30 @@ mountUpdatePanel(slot, { pluginId: 'my-notes-plugin', prefix: 'notes', call: hos
142
142
 
143
143
  面板画成档案五章:**01 检查与安装、02 更新日志、03 更新队列、04 错误信息、05 手工命令**(五章恒在,缺内容给中性提示,不跳号)。档案头一行是「插件名 + 使用范围 + `profile` 牌」:使用范围由面板自动向宿主索取(电话入参 `includeEnv`,宿主只回**范围名与宿主种类**、不回任何路径),显示的就是更新要落到的那个 profile——`web` 与 `desktop` 各装一份,装错范围是严重故障,所以这一栏宁可显示“未知”也不猜。调用方知道得更准时可以用 `profileName` 显式覆盖。
144
144
 
145
- 可选档案卷主题(纸面浅色案卷风,不替换默认):挂载时加 `theme: 'archive'` 即换肤(右上大印章「待查/可装/安装中/待重启/受阻/已最新」+ 横幅小印章一字 + profile 牌 + 待重启衬线横幅配手绘 SVG 标 + 窄屏印章固定 + 省略号逐字折叠 + 浅深双主题跟随系统),内核 DOM 顺序不动、复制诊断常在;不传即最小可用默认深色样式。运行时用 `panel.setTheme('archive' | 'default')` 可切。旧值 `theme: 'archive'` 仍可用(同一套渲染)。入口件与批量面板是同一个 `theme` 参数,取值同一套。
146
-
145
+ 可选档案卷主题(纸面浅色案卷风,不替换默认):挂载时加 `theme: 'archive'` 即换肤(右上大印章「待查/可装/安装中/待重启/受阻/已最新」+ 横幅小印章一字 + profile 牌 + 待重启衬线横幅配手绘 SVG 标 + 窄屏印章固定 + 省略号逐字折叠 + 浅深双主题跟随系统),内核 DOM 顺序不动、复制诊断常在;不传即最小可用默认深色样式。运行时用 `panel.setTheme('archive' | 'default')` 可切。入口件与批量面板是同一个 `theme` 参数,取值同一套。
146
+
147
+ 主题变量覆盖(第三方换肤):四个 UI 口(单面板、批量面板、入口件、批量入口件)都收 `themeTokens`,键为语义名,一次传入即换肤,不传即零回归。入口件打开的 dialog 面板同步透传;运行时用 `setThemeTokens({...})` 即时换肤,传 `undefined` 即清掉覆盖:
148
+
149
+ ```js
150
+ mountUpdatePanel(el, { pluginId: 'p', prefix: 'notes', call,
151
+ theme: 'archive',
152
+ themeTokens: { primary: '#c8402a', bg: '#fffdf6', radiusButton: '3px' },
153
+ })
154
+ ```
155
+
156
+ 键与样式表里的 `--dsh-update-*` 变量一一对应(括号里是变量名),手写 CSS 变量等价生效:
157
+
158
+ | 组 | 键(变量) |
159
+ |---|---|
160
+ | 文字背景边线 | `text`、`textMuted`、`bg`、`bgSoft`、`border`、`borderStrong`、`buttonBg` |
161
+ | 主色焦点 | `primary`、`primaryDeep`、`focus` |
162
+ | 状态四组 | `okBg / okBorder / okText`、`warnBg / warnBorder / warnText`、`badBg / badBorder / badText`、`busyBg / busyBorder / busyText` |
163
+ | 字体阴影 | `fontSans`、`fontSerif`、`fontMono`、`shadow` |
164
+ | 圆角 | `radiusPanel`、`radiusButton`、`radiusBadge` |
165
+ | 入口件尺寸 | `entryFontSize`、`entryPadding`、`entryBorderRadius`、`entryScale`(数字,大于 0) |
166
+
167
+ 三条约束:颜色只收 hex(`#rgb`/`#rrggbb`/`#rrggbbaa`)或英文名单词(含 `transparent`),`rgb()`/`color-mix()` 请自行换算成 hex;其余值走注入检查(分号引号括号等不许过,见 `sizing` 同口径);未知键与非法值挂载即抛错,不静默吞掉。被覆盖的 token 不再跟随深色模式(内联值固定,调用方自己保证深浅可读)。
168
+
147
169
  类型定义随包分发(`dsh-plugin-update/panel` 的 `.d.ts`),不用自编译;面板离线可读,与包版本绑定。
148
170
 
149
171
  整组件 HTTP 版(无 `host.call` 环境即跑:浏览器面板经 HTTP POST 直达宿主网关,只传三样):
@@ -1,7 +1,7 @@
1
1
  import { type BilingualKey } from './bilingual.js';
2
2
  import { type AppLang, type LocaleOption } from './lang.js';
3
3
  import { type BatchRowView } from './panel-batch.js';
4
- import { type UpdatePanelTheme } from './panel.js';
4
+ import { type UpdatePanelTheme, type UpdateThemeTokens } from './panel.js';
5
5
  /** 批量入口件的形态(与单入口同一套取值)。 */
6
6
  export type BatchEntryVariant = 'button' | 'badge' | 'inline';
7
7
  /** 什么时候主动查一次(只读;与单入口同一口径)。 */
@@ -42,6 +42,8 @@ export interface UpdateBatchEntryOptions {
42
42
  variant?: BatchEntryVariant;
43
43
  /** 主题:与单入口/面板同一套(default/archive)。 */
44
44
  theme?: UpdatePanelTheme;
45
+ /** 主题变量覆盖:见 panel 的 UpdateThemeTokens;批量入口根 + 打开的批量面板同步生效,不传即零回归。 */
46
+ themeTokens?: UpdateThemeTokens;
45
47
  /** 缺省 'mount':进页面静默查一次(只调 .batchStatus,只读)。'never' 则只在用户点击时查。 */
46
48
  autoCheck?: BatchEntryAutoCheck;
47
49
  /** 打开面板自动查(#59 D3,透传给批量面板):缺省 true;面板按用户偏好 > 本选项 > 缺省,并与入口预查共用知识时间戳去重(一击只查一次)。 */
@@ -82,6 +84,8 @@ export interface UpdateBatchEntryController {
82
84
  /** 当前聚合读数(含七档计数,接入方排版与 onActivate 共用)。 */
83
85
  summary(): BatchEntrySummary;
84
86
  setTheme(theme: UpdatePanelTheme): void;
87
+ /** 换一套主题变量(重绘;打开的批量面板同步;传 undefined 即清掉覆盖)。 */
88
+ setThemeTokens(tokens: UpdateThemeTokens | undefined): void;
85
89
  unmount(): void;
86
90
  }
87
91
  /**
@@ -11,7 +11,8 @@ import {
11
11
  } from "./panel-batch.js";
12
12
  import {
13
13
  failureCodeOf,
14
- normalizePanelTheme
14
+ normalizePanelTheme,
15
+ themeTokensStyleFor
15
16
  } from "./panel.js";
16
17
  function isObject(value) {
17
18
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -149,6 +150,8 @@ function mountUpdateBatchEntry(container, options) {
149
150
  throw new Error("[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08\u6536\u5230 " + JSON.stringify(options.theme) + "\uFF09");
150
151
  }
151
152
  let theme = normalizePanelTheme(options.theme ?? "default");
153
+ themeTokensStyleFor(options.themeTokens ?? void 0);
154
+ let themeTokens = options.themeTokens;
152
155
  const pollMs = options.pollMs;
153
156
  if (pollMs !== void 0 && (typeof pollMs !== "number" || !Number.isFinite(pollMs) || pollMs < MIN_PANEL_POLL_MS)) {
154
157
  throw new Error("[dsh-plugin-update] \u9762\u677F\u8F6E\u8BE2\u95F4\u9694\u975E\u6CD5\uFF1A\u4E0D\u5F97\u5C0F\u4E8E 250 \u6BEB\u79D2\uFF08\u6536\u5230 " + JSON.stringify(options.pollMs) + "\uFF09");
@@ -220,9 +223,11 @@ function mountUpdateBatchEntry(container, options) {
220
223
  const labelText = labelOverride ?? (activating ? copyText("batch-entry.action.checking", lang) : copyText(batchEntryBilingualKeyFor(stateOf()), lang, batchEntryBilingualValuesFor(stateOf())));
221
224
  const busyAttr = activating ? ' disabled aria-busy="true"' : "";
222
225
  const themeAttr = theme === "archive" ? ' data-theme="archive"' : "";
226
+ const tokensStyle = themeTokensStyleFor(themeTokens ?? void 0);
227
+ const tokensAttr = tokensStyle ? ' style="' + tokensStyle + '"' : "";
223
228
  const control = variant === "badge" ? '<button type="button" class="dsh-upd-entry-dot" ' + ENTRY_ATTR + '="activate" title="' + escapeHtml(labelText) + '" aria-label="' + escapeHtml(labelText) + '"' + busyAttr + "></button>" : '<button type="button" class="dsh-upd-entry-btn" ' + ENTRY_ATTR + '="activate"' + busyAttr + ">" + labelHTML + "</button>";
224
229
  const noteHTML = note ? '<span class="dsh-upd-entry-note" data-dsh-upd-note="1">' + escapeHtml(note) + "</span>" : "";
225
- return "<style>" + UPDATE_ENTRY_CSS + "\n" + BILINGUAL_CSS + '</style>\n<span class="dsh-upd-entry" data-variant="' + variant + '" data-state="' + summary.kind + '"' + themeAttr + ">" + control + noteHTML + "</span>";
230
+ return "<style>" + UPDATE_ENTRY_CSS + "\n" + BILINGUAL_CSS + '</style>\n<span class="dsh-upd-entry" data-variant="' + variant + '" data-state="' + summary.kind + '"' + themeAttr + tokensAttr + ">" + control + noteHTML + "</span>";
226
231
  }
227
232
  function render() {
228
233
  if (!mounted) return;
@@ -267,6 +272,7 @@ function mountUpdateBatchEntry(container, options) {
267
272
  prefix: options.prefix,
268
273
  mode,
269
274
  theme,
275
+ themeTokens,
270
276
  pollMs,
271
277
  titles,
272
278
  onRestartRequested: options.onRestartRequested,
@@ -346,6 +352,12 @@ function mountUpdateBatchEntry(container, options) {
346
352
  if (panel) void panel.setTheme(theme);
347
353
  else render();
348
354
  }
355
+ function setThemeTokens(next) {
356
+ themeTokensStyleFor(next ?? void 0);
357
+ themeTokens = next ?? void 0;
358
+ if (panel) void panel.setThemeTokens(themeTokens);
359
+ else render();
360
+ }
349
361
  function onClick(ev) {
350
362
  if (!mounted) return;
351
363
  try {
@@ -408,7 +420,7 @@ function mountUpdateBatchEntry(container, options) {
408
420
  if (variant === "inline") mountPanel("embedded");
409
421
  else render();
410
422
  if (autoCheck === "mount") void refresh();
411
- return { refresh, open, close, label: currentLabel, summary: summaryOf, setTheme, unmount };
423
+ return { refresh, open, close, label: currentLabel, summary: summaryOf, setTheme, setThemeTokens, unmount };
412
424
  }
413
425
  export {
414
426
  batchEntryBilingualHTMLFor,
package/dist/entry.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type UpdatePanelContainer, type UpdatePanelTheme } from './panel.js';
1
+ import { type UpdatePanelContainer, type UpdatePanelTheme, type UpdateThemeTokens } from './panel.js';
2
2
  import type { UpdateSnapshot } from './ports.js';
3
3
  import { type BilingualKey } from './bilingual.js';
4
4
  import { type AppLang, type LocaleOption } from './lang.js';
@@ -24,7 +24,7 @@ export interface UpdateEntryState {
24
24
  /**
25
25
  * 入口件按钮尺寸覆盖(#69):只影响 button 本体(字号/内边距/圆角/整体缩放),不碰 dialog 面板。
26
26
  * 缺省(不传)保持现状、零回归;badge 圆点与 inline 内嵌不读这些变量,天然不受影响。
27
- * 实现口径与现有 `--dsh-upd-*` 颜色变量一致:本选项只是把同样的 CSS 变量以内联方式写到容器上,
27
+ * 实现口径与现有 `--dsh-update-*` 颜色变量一致:本选项只是把同样的 CSS 变量以内联方式写到容器上,
28
28
  * 手写 CSS 变量(`--dsh-update-entry-font-size` / `--dsh-update-entry-padding` /
29
29
  * `--dsh-update-entry-border-radius` / `--dsh-update-entry-scale`)同样生效,两者等价。
30
30
  * `scale` 即宿主临时方案里容器 `zoom: 1` 开关的正式形态:改一个数即缩放(默认 1)。
@@ -46,7 +46,7 @@ export interface UpdateEntryOptions {
46
46
  /** 调宿主电话:(phoneName, args) => Promise<reply>。 */
47
47
  call: (name: string, args: Record<string, unknown>) => Promise<unknown>;
48
48
  variant?: EntryVariant;
49
- /** 主题:`default` 最小可用深色,`archive` 档案卷纸面浅色(仍可用)。 */
49
+ /** 主题:与面板同一套(`default` 最小可用,`archive` 档案卷纸面浅色)。 */
50
50
  theme?: UpdatePanelTheme;
51
51
  /** 缺省 'mount':进页面静默查一次(只读)。'never' 则只在用户点击时查。 */
52
52
  autoCheck?: EntryAutoCheck;
@@ -56,6 +56,8 @@ export interface UpdateEntryOptions {
56
56
  label?: string;
57
57
  /** 按钮尺寸覆盖(#69):见 EntrySizing;不传即零回归,badge/inline 不受影响。 */
58
58
  sizing?: EntrySizing;
59
+ /** 主题变量覆盖:见 panel 的 UpdateThemeTokens;入口件根 + 打开的 dialog 面板同步生效,不传即零回归。 */
60
+ themeTokens?: UpdateThemeTokens;
59
61
  /** 语言覆盖(#60 v2):'zh' | 'en' | { getActive(): string; subscribe?(cb): () => void },不传即跟随 html[lang] > navigator > zh。 */
60
62
  locale?: LocaleOption;
61
63
  profileName?: string;
@@ -82,6 +84,8 @@ export interface UpdateEntryController {
82
84
  /** 当前按钮上的状态文案(接入方做自定义排版时读它)。 */
83
85
  label(): string;
84
86
  setTheme(theme: UpdatePanelTheme): void;
87
+ /** 换一套主题变量(重绘;打开的 dialog 面板同步;传 undefined 即清掉覆盖)。 */
88
+ setThemeTokens(tokens: UpdateThemeTokens | undefined): void;
85
89
  unmount(): void;
86
90
  }
87
91
  /**
package/dist/entry.js CHANGED
@@ -3,7 +3,8 @@ import { DEFAULT_PREFIX, MIN_PANEL_POLL_MS, buildPhoneNames } from "./config.js"
3
3
  import {
4
4
  failureCodeOf,
5
5
  mountUpdatePanel,
6
- normalizePanelTheme
6
+ normalizePanelTheme,
7
+ themeTokensStyleFor
7
8
  } from "./panel.js";
8
9
  import { BILINGUAL_CSS, copyHTML, copyText } from "./bilingual.js";
9
10
  import { resolveLang, subscribeLang } from "./lang.js";
@@ -188,7 +189,8 @@ function mountUpdateEntry(container, options) {
188
189
  }
189
190
  const labelOverride = typeof options.label === "string" && options.label ? options.label : null;
190
191
  const sizingStyle = entrySizingStyleFor(options.sizing ?? void 0);
191
- const sizingAttr = sizingStyle ? ` style="${sizingStyle}"` : "";
192
+ themeTokensStyleFor(options.themeTokens ?? void 0);
193
+ let themeTokens = options.themeTokens;
192
194
  const localeOpt = options.locale ?? void 0;
193
195
  if (localeOpt !== void 0 && localeOpt !== null) {
194
196
  const isStr = typeof localeOpt === "string";
@@ -250,11 +252,14 @@ function mountUpdateEntry(container, options) {
250
252
  const labelText = labelOverride ?? (activating ? copyText("entry.action.checking", lang) : copyText(entryBilingualKeyFor(stateOf()), lang, entryBilingualValuesFor(stateOf())));
251
253
  const busyAttr = activating ? ' disabled aria-busy="true"' : "";
252
254
  const themeAttr = theme === "archive" ? ' data-theme="archive"' : "";
255
+ const tokensStyle = themeTokensStyleFor(themeTokens ?? void 0);
256
+ const styleBody = [sizingStyle, tokensStyle].filter((part) => part).join(";");
257
+ const styleAttr = styleBody ? ` style="${styleBody}"` : "";
253
258
  const control = variant === "badge" ? `<button type="button" class="dsh-upd-entry-dot" ${ENTRY_ATTR}="activate" title="${escapeHtml(labelText)}" aria-label="${escapeHtml(labelText)}"${busyAttr}></button>` : `<button type="button" class="dsh-upd-entry-btn" ${ENTRY_ATTR}="activate"${busyAttr}>${labelHTML}</button>`;
254
259
  const noteHTML = noteVersion ? `<span class="dsh-upd-entry-note" data-dsh-upd-note="1">${copyHTML("entry.note.up-to-date", lang, { version: noteVersion })}</span>` : "";
255
260
  return `<style>${UPDATE_ENTRY_CSS}
256
261
  ${BILINGUAL_CSS}</style>
257
- <span class="dsh-upd-entry" data-variant="${variant}" data-state="${kind}"${themeAttr}${sizingAttr}>${control}${noteHTML}</span>`;
262
+ <span class="dsh-upd-entry" data-variant="${variant}" data-state="${kind}"${themeAttr}${styleAttr}>${control}${noteHTML}</span>`;
258
263
  }
259
264
  function render() {
260
265
  if (!mounted) return;
@@ -299,6 +304,7 @@ ${BILINGUAL_CSS}</style>
299
304
  prefix,
300
305
  mode,
301
306
  theme,
307
+ themeTokens,
302
308
  pollMs,
303
309
  profileName,
304
310
  autoChangelog: options.autoChangelog,
@@ -366,6 +372,12 @@ ${BILINGUAL_CSS}</style>
366
372
  if (panel) void panel.setTheme(theme);
367
373
  else render();
368
374
  }
375
+ function setThemeTokens(next) {
376
+ themeTokensStyleFor(next ?? void 0);
377
+ themeTokens = next ?? void 0;
378
+ if (panel) void panel.setThemeTokens(themeTokens);
379
+ else render();
380
+ }
369
381
  function onClick(ev) {
370
382
  if (!mounted) return;
371
383
  try {
@@ -424,7 +436,7 @@ ${BILINGUAL_CSS}</style>
424
436
  if (variant === "inline") mountPanel("embedded");
425
437
  else render();
426
438
  if (autoCheck === "mount") void refresh();
427
- return { refresh, open, close, label: currentLabel, setTheme, unmount };
439
+ return { refresh, open, close, label: currentLabel, setTheme, setThemeTokens, unmount };
428
440
  }
429
441
  export {
430
442
  UPDATE_ENTRY_CSS,
@@ -1,4 +1,4 @@
1
- import { type UpdatePanelTheme } from './panel.js';
1
+ import { type UpdatePanelTheme, type UpdateThemeTokens } from './panel.js';
2
2
  /** 一行要画的东西(宿主回包里的形状)。 */
3
3
  export interface BatchRowView {
4
4
  key: string;
@@ -32,6 +32,8 @@ export interface BatchPanelOptions {
32
32
  call: (name: string, args: Record<string, unknown>) => Promise<unknown>;
33
33
  /** 主题:与单插件面板同一套(缺省 default)。 */
34
34
  theme?: UpdatePanelTheme;
35
+ /** 主题变量覆盖:见 panel 的 UpdateThemeTokens;批量根 + 展开详情行全继承,不传即零回归。 */
36
+ themeTokens?: UpdateThemeTokens;
35
37
  /** 摆放形态:内嵌或弹窗(缺省 embedded)。 */
36
38
  mode?: BatchPanelMode;
37
39
  /** 轮询间隔(毫秒,缺省 1500,下限 250)。 */
@@ -82,6 +84,8 @@ export interface BatchPanelController {
82
84
  refresh(): Promise<void>;
83
85
  act(action: BatchPanelActionKind, key?: string): Promise<void>;
84
86
  setTheme(theme: UpdatePanelTheme): void;
87
+ /** 换一套主题变量(重绘;传 undefined 即清掉覆盖,回主题默认)。 */
88
+ setThemeTokens(tokens: UpdateThemeTokens | undefined): void;
85
89
  setMode(mode: BatchPanelMode): void;
86
90
  unmount(): void;
87
91
  }
@@ -156,9 +160,30 @@ export declare function batchRowStatus(row: BatchRowView, skippedVersion?: strin
156
160
  * 无知识(从没查过)回 null,调用方回退旧 pending 文案。
157
161
  */
158
162
  export declare function batchRowKnowledgeText(row: BatchRowView, entry: BatchKnowledgeEntry | null, lang?: unknown): string | null;
163
+ /**
164
+ * 一行的版本展示(#71 Q3:永远显示当前版;有新版才 `当前 → 最新`)。
165
+ * latest 只做展示,不参与安装决策:会话目标版优先(安装语义),无会话目标时才看知识最新版。
166
+ * - 会话有目标版:current(快照运行版优先)→ 会话目标(current 缺失时占位 `?`,与旧口径一致)。
167
+ * - 会话无目标:current(快照优先,缺失退知识 installedVersion)→ 知识 latest(仅当两者皆有且不等;相等即已是最新,只显当前)。
168
+ * - 两者皆无:current 为 null、latest 为 null、hasUpdate 为 false(调用方不画版本 span)。
169
+ */
170
+ export interface BatchRowVersionParts {
171
+ current: string | null;
172
+ latest: string | null;
173
+ hasUpdate: boolean;
174
+ }
175
+ export declare function batchRowVersionParts(row: BatchRowView, knowledge: BatchKnowledgeEntry | null): BatchRowVersionParts;
176
+ /**
177
+ * 无轮次 pending 行的状态词(#71 Q3 四态:有新版 / 已是最新 / 还没查过 / 查失败)。
178
+ * 有知识走知识分支;无知识(inventory 缺失或该家无条目)回「还没查过」,不再误显示 wait-turn。
179
+ * 有轮次未做完(inRound)仍走执行态,由调用方按旧口径显示 wait-turn + 未终态标记。
180
+ */
181
+ export declare function batchRowPendingStatus(row: BatchRowView, knowledge: BatchKnowledgeEntry | null, lang?: unknown): string | null;
159
182
  export interface BatchPanelRenderInput {
160
183
  rows?: readonly BatchRowView[];
161
184
  theme?: UpdatePanelTheme;
185
+ /** 主题变量覆盖:见 panel 的 UpdateThemeTokens;写到批量根上,详情行经继承生效。 */
186
+ themeTokens?: UpdateThemeTokens;
162
187
  mode?: BatchPanelMode;
163
188
  /** 当前展开的那一家(键);null 即全部收起。 */
164
189
  expandedKey?: string | null;
@@ -16,7 +16,8 @@ import {
16
16
  failureCopy,
17
17
  isKnownFailureCode,
18
18
  normalizePanelTheme,
19
- renderUpdatePanelHTML
19
+ renderUpdatePanelHTML,
20
+ themeTokensStyleFor
20
21
  } from "./panel.js";
21
22
  const BATCH_PANEL_POLL = {
22
23
  defaultMs: 1500,
@@ -174,6 +175,38 @@ function batchRowKnowledgeText(row, entry, lang) {
174
175
  if (entry.lastCheckedAt > 0) return batchText("batch.row.current", lang);
175
176
  return batchText("batch.row.never", lang);
176
177
  }
178
+ function batchRowVersionParts(row, knowledge) {
179
+ const sessionTarget = typeof row.targetVersion === "string" && row.targetVersion ? row.targetVersion : null;
180
+ if (sessionTarget) {
181
+ const current2 = currentVersionOf(row);
182
+ if (current2 === null) return { current: null, latest: sessionTarget, hasUpdate: true };
183
+ if (current2 === sessionTarget) return { current: current2, latest: null, hasUpdate: false };
184
+ return { current: current2, latest: sessionTarget, hasUpdate: true };
185
+ }
186
+ const snapshotCurrent = currentVersionOf(row);
187
+ const current = snapshotCurrent ?? (knowledge && typeof knowledge.installedVersion === "string" && knowledge.installedVersion ? knowledge.installedVersion : null);
188
+ const latest = knowledge && typeof knowledge.latestVersion === "string" && knowledge.latestVersion ? knowledge.latestVersion : null;
189
+ if (current && latest && latest !== current) return { current, latest, hasUpdate: true };
190
+ if (current) return { current, latest: null, hasUpdate: false };
191
+ if (latest) return { current: latest, latest: null, hasUpdate: false };
192
+ return { current: null, latest: null, hasUpdate: false };
193
+ }
194
+ function batchRowVersionHTML(row, knowledge) {
195
+ const parts = batchRowVersionParts(row, knowledge);
196
+ if (parts.hasUpdate && parts.latest) {
197
+ const cur = parts.current ?? "?";
198
+ return '<span class="dsh-upd-bver"><span class="dsh-upd-bver-cur">' + escapeHtml(cur) + '</span><span class="dsh-upd-bver-arrow" aria-hidden="true"> \u2192 </span><span class="dsh-upd-bver-new">' + escapeHtml(parts.latest) + "</span></span>";
199
+ }
200
+ if (parts.current) return '<span class="dsh-upd-bver">' + escapeHtml(parts.current) + "</span>";
201
+ return "";
202
+ }
203
+ function batchRowPendingStatus(row, knowledge, lang) {
204
+ if (asBatchPhase(row.phase) !== "pending") return null;
205
+ const known = batchRowKnowledgeText(row, knowledge, lang);
206
+ if (known !== null) return known;
207
+ if (!knowledge) return batchText("batch.row.never", lang);
208
+ return null;
209
+ }
177
210
  function renderBatchPanelHTML(input) {
178
211
  const rows = Array.isArray(input.rows) ? input.rows : [];
179
212
  const theme = normalizePanelTheme(input.theme);
@@ -198,6 +231,7 @@ function renderBatchPanelHTML(input) {
198
231
  busy: installing,
199
232
  inFlight: input.inFlight === true,
200
233
  theme,
234
+ themeTokens: input.themeTokens ?? void 0,
201
235
  titles,
202
236
  skipped: null,
203
237
  notice: null,
@@ -285,8 +319,10 @@ function renderBatchPanelHTML(input) {
285
319
  const kernel = parts.join(String.fromCharCode(10));
286
320
  const archive = normalizePanelTheme(theme) === "archive";
287
321
  const attr = archive ? ' data-theme="archive"' : "";
322
+ const tokensStyle = themeTokensStyleFor(input.themeTokens ?? void 0);
323
+ const tokensAttr = tokensStyle ? ' style="' + tokensStyle + '"' : "";
288
324
  const seal = batchSealOf(counts, lang);
289
- const root = '<div class="dsh-upd dsh-upd-batch" data-mode="' + mode + '" data-seal="' + escapeHtml(seal.text) + '" data-seal-tone="' + seal.tone + '"' + attr + ">\n" + kernel + "\n</div>";
325
+ const root = '<div class="dsh-upd dsh-upd-batch" data-mode="' + mode + '" data-seal="' + escapeHtml(seal.text) + '" data-seal-tone="' + seal.tone + '"' + attr + tokensAttr + ">\n" + kernel + "\n</div>";
290
326
  const body = mode === "dialog" ? '<div class="dsh-upd-overlay" data-mode="dialog">' + root + "</div>" : root;
291
327
  const css = archive ? [UPDATE_PANEL_CSS, UPDATE_BATCH_PANEL_CSS, UPDATE_PANEL_ARCHIVE_CSS, UPDATE_BATCH_PANEL_ARCHIVE_CSS].join(String.fromCharCode(10)) : [UPDATE_PANEL_CSS, UPDATE_BATCH_PANEL_CSS].join(String.fromCharCode(10));
292
328
  return "<style>" + css + "</style>\n" + body;
@@ -349,8 +385,8 @@ function rowSetHTML(row, ctx) {
349
385
  const skipped = ctx.skipped;
350
386
  const queued = isQueuedRow(row);
351
387
  const position = queued ? queuePositionOf(row) : null;
352
- const current = currentVersionOf(row);
353
- const version = row.targetVersion ? (current === null ? "?" : current) + " \u2192 " + row.targetVersion : current === null ? "" : current;
388
+ const versionHTML = batchRowVersionHTML(row, ctx.knowledge);
389
+ const rowStatusText = queued ? batchQueuedStatus(position, ctx.lang) : ctx.inRound ? batchRowKnowledgeText(row, ctx.knowledge, ctx.lang) ?? batchRowStatus(row, skipped, ctx.lang) : batchRowPendingStatus(row, ctx.knowledge, ctx.lang) ?? batchRowStatus(row, skipped, ctx.lang);
354
390
  const actions = [];
355
391
  const off = ctx.inFlight ? " disabled" : "";
356
392
  if (phase === "installing") {
@@ -391,7 +427,7 @@ function rowSetHTML(row, ctx) {
391
427
  const withCode = escaped.includes(codeEsc) ? escaped.replace(codeEsc, "<code>" + codeEsc + "</code>") : escaped;
392
428
  return '<span class="dsh-upd-bfail">' + withCode + "</span>";
393
429
  })() : "";
394
- const main = '<button type="button" class="dsh-upd-brow-main" data-act="toggle-details" data-key="' + keyAttr + '" aria-expanded="' + (ctx.expanded ? "true" : "false") + '"><span class="dsh-upd-updot" data-tone="' + dotToneOf(row, phase, skipped, queued) + '"></span><span class="dsh-upd-bname">' + escapeHtml(titleOf(row, ctx.titles)) + "</span>" + (version ? '<span class="dsh-upd-bver">' + escapeHtml(version) + "</span>" : "") + '<span class="dsh-upd-bstat">' + escapeHtml(queued ? batchQueuedStatus(position, ctx.lang) : batchRowKnowledgeText(row, ctx.knowledge, ctx.lang) ?? batchRowStatus(row, skipped, ctx.lang)) + (ctx.inRound && !queued ? "\uFF08" + escapeHtml(batchText("batch.row.unfinished-tag", ctx.lang)) + "\uFF09" : "") + "</span>" + failLine + "</button>";
430
+ const main = '<button type="button" class="dsh-upd-brow-main" data-act="toggle-details" data-key="' + keyAttr + '" aria-expanded="' + (ctx.expanded ? "true" : "false") + '"><span class="dsh-upd-updot" data-tone="' + dotToneOf(row, phase, skipped, queued) + '"></span><span class="dsh-upd-bname">' + escapeHtml(titleOf(row, ctx.titles)) + "</span>" + versionHTML + '<span class="dsh-upd-bstat">' + escapeHtml(rowStatusText) + (ctx.inRound && !queued ? "\uFF08" + escapeHtml(batchText("batch.row.unfinished-tag", ctx.lang)) + "\uFF09" : "") + "</span>" + failLine + "</button>";
395
431
  const detail = ctx.expanded ? '<div class="dsh-upd-bdetail" data-key="' + keyAttr + '">' + detailActionsHTML(row, ctx) + (ctx.notice !== null ? '<div class="dsh-upd-bdetail-notice" role="status">' + escapeHtml(ctx.notice) + "</div>" : "") + detailHTML(row, ctx) + "</div>" : "";
396
432
  return '<div class="dsh-upd-brow-set" data-key="' + keyAttr + '"><div class="dsh-upd-brow" data-phase="' + escapeHtml(phase) + '" data-key="' + keyAttr + '"' + (queued ? ' data-queued="1"' : "") + ">" + main + '<span class="dsh-upd-brow-actions">' + actions.join("") + "</span></div>" + detail + "</div>";
397
433
  }
@@ -411,6 +447,7 @@ function detailHTML(row, ctx) {
411
447
  pluginId: pluginIdOf(row),
412
448
  copyNotice: null,
413
449
  theme: ctx.theme,
450
+ themeTokens: ctx.themeTokens,
414
451
  profileName: typeof row.profileName === "string" && row.profileName ? row.profileName : null,
415
452
  actions: "none"
416
453
  }, ctx.lang);
@@ -481,7 +518,7 @@ const UPDATE_BATCH_PANEL_CSS = [
481
518
  ".dsh-upd-brow-set{border-top:1px solid var(--dsh-update-border,#e5e7eb)}",
482
519
  ".dsh-upd-brow-set:first-child{border-top:0}",
483
520
  ".dsh-upd-brow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 0}",
484
- ".dsh-upd .dsh-upd-brow-main{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex:1 1 260px;min-width:0;",
521
+ ".dsh-upd .dsh-upd-brow-main{display:grid;grid-template-columns:auto minmax(5em,max-content) minmax(18ch,max-content) minmax(0,1fr);align-items:baseline;column-gap:8px;row-gap:2px;flex:1 1 260px;min-width:0;",
485
522
  "text-align:left;cursor:pointer;color:inherit;font:inherit;background:transparent;border:0;padding:2px 0;margin:0}",
486
523
  ".dsh-upd .dsh-upd-brow-main:hover{color:var(--dsh-update-primary,#2563eb)}",
487
524
  ".dsh-upd-brow-actions{flex:none;margin-left:auto}",
@@ -492,10 +529,16 @@ const UPDATE_BATCH_PANEL_CSS = [
492
529
  '.dsh-upd-updot[data-tone="busy"]{background:var(--dsh-update-busy-border,#2563eb);opacity:1}',
493
530
  '.dsh-upd-updot[data-tone="warn"]{background:var(--dsh-update-warn-border,#d97706);opacity:1}',
494
531
  '.dsh-upd-updot[data-tone="bad"]{background:var(--dsh-update-bad-border,#dc2626);opacity:1}',
495
- ".dsh-upd-bname{font-weight:700;flex:none}",
496
- ".dsh-upd-bver{font-family:Consolas,Menlo,monospace;font-size:12px;opacity:.8;flex:none}",
497
- ".dsh-upd-bstat{font-size:12.5px;opacity:.9}",
498
- ".dsh-upd-bfail{flex:1 1 100%;display:block;font-size:12.5px;color:var(--dsh-update-bad-border,#dc2626)}",
532
+ ".dsh-upd-bname{font-weight:700;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
533
+ ".dsh-upd-bver{font-family:Consolas,Menlo,monospace;font-size:12px;white-space:nowrap;font-variant-numeric:tabular-nums;min-width:0}",
534
+ ".dsh-upd-bver-cur{opacity:.65}",
535
+ ".dsh-upd-bver-arrow{opacity:.6}",
536
+ ".dsh-upd-bver-new{color:var(--dsh-update-new-text,#dc2626);font-weight:700}",
537
+ ".dsh-upd-bstat{font-size:12.5px;opacity:.9;min-width:0;overflow-wrap:anywhere}",
538
+ ".dsh-upd-bfail{grid-column:1/-1;display:block;font-size:12.5px;color:var(--dsh-update-bad-border,#dc2626)}",
539
+ ".dsh-upd-batch{--dsh-update-new-text:#dc2626}",
540
+ "@media (prefers-color-scheme: dark){.dsh-upd-batch{--dsh-update-new-text:#f87171}}",
541
+ "@media (forced-colors: active){.dsh-upd-bver-new{color:CanvasText}}",
499
542
  ".dsh-upd-blist{font-size:12.5px;margin-top:2px}",
500
543
  ".dsh-upd-batch-ledger{margin:8px 0 0;font-size:13px;opacity:.85}",
501
544
  ".dsh-upd-batch-more{margin-top:6px}",
@@ -531,6 +574,10 @@ const UPDATE_BATCH_PANEL_ARCHIVE_CSS = [
531
574
  '.dsh-upd[data-theme="archive"] .dsh-upd-brow-main:hover{color:var(--dsh-update-primary)}',
532
575
  '.dsh-upd[data-theme="archive"] .dsh-upd-bname{font-family:var(--dsh-update-font-serif)}',
533
576
  '.dsh-upd[data-theme="archive"] .dsh-upd-bver{font-family:var(--dsh-update-font-mono);color:var(--dsh-update-text-muted)}',
577
+ '.dsh-upd[data-theme="archive"] .dsh-upd-bver-cur{opacity:.75}',
578
+ '.dsh-upd[data-theme="archive"] .dsh-upd-bver-new{color:var(--dsh-update-new-text,#b3261e)}',
579
+ '.dsh-upd[data-theme="archive"] .dsh-upd-batch{--dsh-update-new-text:#b3261e}',
580
+ '@media (prefers-color-scheme: dark){.dsh-upd[data-theme="archive"] .dsh-upd-batch{--dsh-update-new-text:#ef8a7d}}',
534
581
  '.dsh-upd[data-theme="archive"] .dsh-upd-batch-ledger{color:var(--dsh-update-text-muted)}',
535
582
  '.dsh-upd[data-theme="archive"] .dsh-upd-batch-sum{color:var(--dsh-update-text)}',
536
583
  '.dsh-upd[data-theme="archive"] .dsh-upd-brow-set{border-top-color:var(--dsh-update-border)}'
@@ -762,6 +809,8 @@ function mountUpdateBatchPanel(container, options) {
762
809
  throw new Error("[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08\u6536\u5230 " + JSON.stringify(options.theme) + "\uFF09");
763
810
  }
764
811
  let theme = normalizePanelTheme(options.theme ?? "default");
812
+ themeTokensStyleFor(options.themeTokens ?? void 0);
813
+ let themeTokens = options.themeTokens;
765
814
  const call = options.call;
766
815
  const titles = options.titles && typeof options.titles === "object" ? options.titles : {};
767
816
  const onRestartRequested = options.onRestartRequested;
@@ -952,6 +1001,7 @@ function mountUpdateBatchPanel(container, options) {
952
1001
  const nextHTML = renderBatchPanelHTML({
953
1002
  rows,
954
1003
  theme,
1004
+ themeTokens,
955
1005
  mode,
956
1006
  expandedKey,
957
1007
  lastError,
@@ -1273,6 +1323,11 @@ function mountUpdateBatchPanel(container, options) {
1273
1323
  theme = normalizePanelTheme(next);
1274
1324
  render();
1275
1325
  }
1326
+ function setThemeTokens(next) {
1327
+ themeTokensStyleFor(next ?? void 0);
1328
+ themeTokens = next ?? void 0;
1329
+ render();
1330
+ }
1276
1331
  function setMode(next) {
1277
1332
  if (next !== "embedded" && next !== "dialog") {
1278
1333
  throw new Error("[dsh-plugin-update] \u6446\u653E\u5F62\u6001\u975E\u6CD5\uFF1A\u53EA\u6536 embedded \u6216 dialog\uFF08\u6536\u5230 " + JSON.stringify(next) + "\uFF09");
@@ -1396,7 +1451,7 @@ function mountUpdateBatchPanel(container, options) {
1396
1451
  void refresh().then(() => {
1397
1452
  void autoOnce();
1398
1453
  });
1399
- return { refresh, act, setTheme, setMode, unmount };
1454
+ return { refresh, act, setTheme, setThemeTokens, setMode, unmount };
1400
1455
  }
1401
1456
  export {
1402
1457
  BATCH_PANEL_POLL,
@@ -1406,7 +1461,9 @@ export {
1406
1461
  batchLedgerText,
1407
1462
  batchQueuedStatus,
1408
1463
  batchRowKnowledgeText,
1464
+ batchRowPendingStatus,
1409
1465
  batchRowStatus,
1466
+ batchRowVersionParts,
1410
1467
  buildBatchPhoneNames,
1411
1468
  hasUnfinishedRows,
1412
1469
  mountUpdateBatchPanel,
package/dist/panel.d.ts CHANGED
@@ -6,7 +6,6 @@ export type UpdatePanelMode = 'embedded' | 'dialog';
6
6
  /** 面板主题:默认最小可用样式;`archive` 为档案卷纸面浅色(只换肤,不换 DOM 顺序)。 */
7
7
  /**
8
8
  * 面板主题:`default` 最小可用深色;`archive` 档案卷纸面浅色(原型敲定的案卷风格,只换肤)。
9
- * `archive` 是 archive 的旧别名(历史取值),仍可用,渲染逐字相同。
10
9
  */
11
10
  export type UpdatePanelTheme = 'default' | 'archive';
12
11
  /**
@@ -14,6 +13,52 @@ export type UpdatePanelTheme = 'default' | 'archive';
14
13
  * 其余一律回 default(纯渲染函数永不抛;挂载/setTheme 的非法值另行抛错)。
15
14
  */
16
15
  export declare function normalizePanelTheme(value: unknown): 'default' | 'archive';
16
+ /** 第三方主题变量覆盖:键为语义名;颜色收 hex(#rgb/#rrggbb/#rrggbbaa)或英文名单词;其余收安全 CSS 值;缺省即主题默认值,零回归。 */
17
+ export interface UpdateThemeTokens {
18
+ text?: string;
19
+ textMuted?: string;
20
+ bg?: string;
21
+ bgSoft?: string;
22
+ border?: string;
23
+ borderStrong?: string;
24
+ buttonBg?: string;
25
+ primary?: string;
26
+ primaryDeep?: string;
27
+ focus?: string;
28
+ okBg?: string;
29
+ okBorder?: string;
30
+ okText?: string;
31
+ warnBg?: string;
32
+ warnBorder?: string;
33
+ warnText?: string;
34
+ badBg?: string;
35
+ badBorder?: string;
36
+ badText?: string;
37
+ busyBg?: string;
38
+ busyBorder?: string;
39
+ busyText?: string;
40
+ /** 有新版版本号的醒目红(#71 Q1 专用;缺省跟随坏项红的对比度口径,深浅主题另覆)。 */
41
+ newText?: string;
42
+ fontSans?: string;
43
+ fontSerif?: string;
44
+ fontMono?: string;
45
+ shadow?: string;
46
+ radiusPanel?: string;
47
+ radiusButton?: string;
48
+ radiusBadge?: string;
49
+ entryFontSize?: string;
50
+ entryPadding?: string;
51
+ entryBorderRadius?: string;
52
+ /** 入口件整体缩放(对应 CSS `zoom`),须为大于 0 的有限数。 */
53
+ entryScale?: number;
54
+ }
55
+ /** CSS 值最小安全检查(与入口件 sizing 同口径):放 style 属性前先拦掉注入。 */
56
+ export declare function isSafeThemeCssValue(value: string): boolean;
57
+ /**
58
+ * `themeTokens` → 根 style 属性体(纯函数:同一输入永远算出同一串;空/缺省回空串,即不写 style)。
59
+ * 非法即抛(未知键、空串、注入字符、颜色形状不对、非正有限 entryScale 都不收)。
60
+ */
61
+ export declare function themeTokensStyleFor(tokens: UpdateThemeTokens | null | undefined): string;
17
62
  /** 与宿主通话的传输函数:面板只认这个签名,不认任何宿主对象的具体形状。 */
18
63
  export type UpdatePanelCall = (phoneName: string, args: Record<string, unknown>) => Promise<Record<string, unknown>>;
19
64
  /** 复制文本的出口:不传即尝试浏览器剪贴板,都没有则只留“已生成、请手动复制”提示。 */
@@ -29,6 +74,8 @@ export interface UpdatePanelOptions {
29
74
  mode?: UpdatePanelMode;
30
75
  /** 面板主题:默认 `default`(最小可用样式,一字不动);传 `archive` 切档案卷。 */
31
76
  theme?: UpdatePanelTheme;
77
+ /** 主题变量覆盖:见 UpdateThemeTokens;两个主题通用,不传即零回归。 */
78
+ themeTokens?: UpdateThemeTokens;
32
79
  /** 是否看他人排队明细:默认只看自己的(他人仅露正忙占位,位置照给)。 */
33
80
  showOthers?: boolean;
34
81
  /** 是否显示深挖日志指引:默认显示;面向纯终端用户的嵌入可关(失败证据行不受影响)。 */
@@ -90,6 +137,8 @@ export interface UpdatePanelController {
90
137
  setMode(mode: UpdatePanelMode): Promise<void>;
91
138
  /** 切换主题(同一内核重绘,只换肤;默认主题输出与旧版一字不差)。 */
92
139
  setTheme(theme: UpdatePanelTheme): Promise<void>;
140
+ /** 换一套主题变量(同一内核重绘,只换变量;传 undefined 即清掉覆盖,回主题默认)。 */
141
+ setThemeTokens(tokens: UpdateThemeTokens | undefined): Promise<void>;
93
142
  /** 切换排队可见性(重查一次,位置口径不变)。 */
94
143
  setShowOthers(show: boolean): Promise<void>;
95
144
  /** 收起/展开更新日志(纯视图态,不调电话;轮询与换肤不丢)。 */
@@ -256,6 +305,8 @@ export interface PanelRenderInput extends PanelViewInput {
256
305
  lang?: AppLang | string | null;
257
306
  /** 可选主题:不传即默认(输出与旧版一字不差);`archive` 切档案卷。 */
258
307
  theme?: UpdatePanelTheme;
308
+ /** 主题变量覆盖:见 UpdateThemeTokens;写到面板根上,不传即无 style 属性。 */
309
+ themeTokens?: UpdateThemeTokens;
259
310
  /** 使用范围名(profile):面板「使用范围」一栏的唯一来源,缺省显示“未知”,不猜。 */
260
311
  profileName?: string | null;
261
312
  /** 宿主种类:进诊断文本;缺省显示“未知”。 */
package/dist/panel.js CHANGED
@@ -15,6 +15,108 @@ import { normalizeLangTag, resolveLang, subscribeLang } from "./lang.js";
15
15
  function normalizePanelTheme(value) {
16
16
  return value === "archive" ? "archive" : "default";
17
17
  }
18
+ const THEME_TOKEN_VARS = {
19
+ text: "--dsh-update-text",
20
+ textMuted: "--dsh-update-text-muted",
21
+ bg: "--dsh-update-bg",
22
+ bgSoft: "--dsh-update-bg-soft",
23
+ border: "--dsh-update-border",
24
+ borderStrong: "--dsh-update-border-strong",
25
+ buttonBg: "--dsh-update-button-bg",
26
+ primary: "--dsh-update-primary",
27
+ primaryDeep: "--dsh-update-primary-deep",
28
+ focus: "--dsh-update-focus",
29
+ okBg: "--dsh-update-ok-bg",
30
+ okBorder: "--dsh-update-ok-border",
31
+ okText: "--dsh-update-ok-text",
32
+ warnBg: "--dsh-update-warn-bg",
33
+ warnBorder: "--dsh-update-warn-border",
34
+ warnText: "--dsh-update-warn-text",
35
+ badBg: "--dsh-update-bad-bg",
36
+ badBorder: "--dsh-update-bad-border",
37
+ badText: "--dsh-update-bad-text",
38
+ busyBg: "--dsh-update-busy-bg",
39
+ busyBorder: "--dsh-update-busy-border",
40
+ busyText: "--dsh-update-busy-text",
41
+ newText: "--dsh-update-new-text",
42
+ fontSans: "--dsh-update-font-sans",
43
+ fontSerif: "--dsh-update-font-serif",
44
+ fontMono: "--dsh-update-font-mono",
45
+ shadow: "--dsh-update-shadow",
46
+ radiusPanel: "--dsh-update-radius-panel",
47
+ radiusButton: "--dsh-update-radius-button",
48
+ radiusBadge: "--dsh-update-radius-badge",
49
+ entryFontSize: "--dsh-update-entry-font-size",
50
+ entryPadding: "--dsh-update-entry-padding",
51
+ entryBorderRadius: "--dsh-update-entry-border-radius",
52
+ entryScale: "--dsh-update-entry-scale"
53
+ };
54
+ const THEME_TOKEN_COLOR_KEYS = /* @__PURE__ */ new Set([
55
+ "text",
56
+ "textMuted",
57
+ "bg",
58
+ "bgSoft",
59
+ "border",
60
+ "borderStrong",
61
+ "buttonBg",
62
+ "primary",
63
+ "primaryDeep",
64
+ "focus",
65
+ "okBg",
66
+ "okBorder",
67
+ "okText",
68
+ "warnBg",
69
+ "warnBorder",
70
+ "warnText",
71
+ "badBg",
72
+ "badBorder",
73
+ "badText",
74
+ "busyBg",
75
+ "busyBorder",
76
+ "busyText",
77
+ "newText"
78
+ ]);
79
+ function themeTokensError(raw) {
80
+ return new Error(`[dsh-plugin-update] \u4E3B\u9898\u53C2\u6570 themeTokens \u975E\u6CD5\uFF1A\u53EA\u6536\u5DF2\u77E5 token \u952E\uFF08\u989C\u8272\u7528 hex \u6216\u82F1\u6587\u540D\uFF0C\u5B57\u4F53/\u5706\u89D2/\u9634\u5F71/\u5C3A\u5BF8\u4E3A\u5B89\u5168 CSS \u503C\uFF0CentryScale \u4E3A\u5927\u4E8E 0 \u7684\u6709\u9650\u6570\uFF09\uFF08\u6536\u5230 ${JSON.stringify(raw ?? null)})`);
81
+ }
82
+ function isSafeThemeCssValue(value) {
83
+ const v = value.trim();
84
+ if (!v || v.length > 200) return false;
85
+ if (/[;"'<>\`{}!&]/.test(v)) return false;
86
+ if (/url\s*\(/i.test(v)) return false;
87
+ if (/expression\s*\(/i.test(v)) return false;
88
+ if (/javascript\s*:/i.test(v)) return false;
89
+ return true;
90
+ }
91
+ function isThemeColorValue(value) {
92
+ const v = value.trim();
93
+ if (!v || v.length > 100) return false;
94
+ return /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{8}|[a-zA-Z]+)$/.test(v);
95
+ }
96
+ function themeTokensStyleFor(tokens) {
97
+ if (tokens === void 0 || tokens === null) return "";
98
+ if (typeof tokens !== "object" || Array.isArray(tokens)) throw themeTokensError(tokens);
99
+ for (const key of Object.keys(tokens)) {
100
+ if (!Object.prototype.hasOwnProperty.call(THEME_TOKEN_VARS, key)) throw themeTokensError(tokens);
101
+ }
102
+ const parts = [];
103
+ for (const key of Object.keys(THEME_TOKEN_VARS)) {
104
+ const value = tokens[key];
105
+ if (value === void 0) continue;
106
+ if (key === "entryScale") {
107
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw themeTokensError(tokens);
108
+ parts.push(THEME_TOKEN_VARS[key] + ":" + String(value));
109
+ continue;
110
+ }
111
+ if (typeof value !== "string") throw themeTokensError(tokens);
112
+ const text = value.trim();
113
+ if (THEME_TOKEN_COLOR_KEYS.has(key)) {
114
+ if (!isThemeColorValue(text)) throw themeTokensError(tokens);
115
+ } else if (!isSafeThemeCssValue(text)) throw themeTokensError(tokens);
116
+ parts.push(THEME_TOKEN_VARS[key] + ":" + text);
117
+ }
118
+ return parts.join(";");
119
+ }
18
120
  const BLOCKED_KEYS = {
19
121
  "unknown-profile": { title: "panel.blocked.unknown-profile.title", action: "panel.blocked.unknown-profile.action" },
20
122
  "source-install": { title: "panel.blocked.source-install.title", action: "panel.blocked.source-install.action" },
@@ -475,7 +577,7 @@ function panelViewModelCore(input, lang) {
475
577
  };
476
578
  }
477
579
  const UPDATE_PANEL_CSS = [
478
- '.dsh-upd{font:14px/1.6 system-ui,"Microsoft YaHei",sans-serif;color:var(--dsh-update-text,#1f2937);',
580
+ '.dsh-upd{font:14px/1.75 system-ui,"Microsoft YaHei",sans-serif;color:var(--dsh-update-text,#1f2937);',
479
581
  "background:var(--dsh-update-bg,#ffffff);border:1px solid var(--dsh-update-border,#e5e7eb);border-radius:8px;padding:12px 14px;max-width:560px;",
480
582
  // 横幅配色走变量(浅色默认 + 深色覆盖,见下方 dark 媒体块):硬编码浅色会让深色下
481
583
  // 「浅底 + 浅字」读不出来(现场回归:默认主题深色模式更新横幅白底浅字)。
@@ -483,7 +585,7 @@ const UPDATE_PANEL_CSS = [
483
585
  "--dsh-update-bad-bg:#fef2f2;--dsh-update-bad-border:#dc2626;--dsh-update-busy-bg:#eff6ff;--dsh-update-busy-border:#2563eb}",
484
586
  ".dsh-upd *{box-sizing:border-box}",
485
587
  ".dsh-upd button{font:inherit;border:1px solid var(--dsh-update-border-strong,#d1d5db);border-radius:var(--dsh-update-radius-button,6px);background:var(--dsh-update-button-bg,#f9fafb);",
486
- "color:inherit;padding:4px 12px;cursor:pointer;margin:2px 6px 2px 0}",
588
+ "color:inherit;padding:7px 14px;cursor:pointer;margin:2px 6px 2px 0}",
487
589
  ".dsh-upd button:disabled{opacity:.45;cursor:not-allowed}",
488
590
  ".dsh-upd button:focus-visible{outline:2px solid var(--dsh-update-focus,#2563eb);outline-offset:1px}",
489
591
  '.dsh-upd button[data-primary="1"]{background:var(--dsh-update-primary,#2563eb);border-color:var(--dsh-update-primary,#2563eb);color:#fff}',
@@ -513,7 +615,8 @@ const UPDATE_PANEL_CSS = [
513
615
  ".dsh-upd-overlay .dsh-upd{background:var(--dsh-update-bg,#ffffff);max-height:85vh;display:flex;flex-direction:column;overflow:hidden}",
514
616
  // —— 弹窗分栏滚动:头(抬头/档案头/横幅/版本条)与尾固定,只有 01–05 章节区滚动 ——
515
617
  ".dsh-upd-body{min-height:0}",
516
- ".dsh-upd-overlay .dsh-upd-body{flex:1 1 auto;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dsh-update-border,#e5e7eb) transparent}",
618
+ ".dsh-upd-body *{min-width:0}",
619
+ ".dsh-upd-overlay .dsh-upd-body{flex:1 1 auto;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dsh-update-border,#e5e7eb) transparent}",
517
620
  ".dsh-upd-overlay .dsh-upd-body::-webkit-scrollbar{width:8px}",
518
621
  ".dsh-upd-overlay .dsh-upd-body::-webkit-scrollbar-thumb{background:var(--dsh-update-border,#e5e7eb);border-radius:4px}",
519
622
  ".dsh-upd-overlay .dsh-upd-body::-webkit-scrollbar-track{background:transparent}",
@@ -528,11 +631,11 @@ const UPDATE_PANEL_CSS = [
528
631
  ".dsh-upd-meta{font-size:12.5px;opacity:.75}",
529
632
  ".dsh-upd-proftag{font-family:Consolas,Menlo,monospace;font-size:11px;border:1px solid var(--dsh-update-border,#d1d5db);border-radius:3px;padding:0 5px;margin-left:6px;letter-spacing:.06em}",
530
633
  ".dsh-upd-strip{display:flex;flex-wrap:wrap;margin:8px 0 0;border:1px solid var(--dsh-update-border,#e5e7eb);border-radius:4px;overflow:hidden;font-size:12.5px}",
531
- ".dsh-upd-strip>div{flex:1 1 110px;padding:6px 10px;border-left:1px solid var(--dsh-update-border,#e5e7eb)}",
634
+ ".dsh-upd-strip>div{flex:1 1 110px;min-width:0;padding:6px 10px;border-left:1px solid var(--dsh-update-border,#e5e7eb)}",
532
635
  ".dsh-upd-strip>div:first-child{border-left:0}",
533
636
  ".dsh-upd-strip-k{display:block;font-size:11px;letter-spacing:.14em;opacity:.7}",
534
- ".dsh-upd-strip-v{font-family:Consolas,Menlo,monospace;font-size:12.5px}",
535
- ".dsh-upd-chapter{margin-top:14px;padding-top:10px;border-top:1px solid var(--dsh-update-border,#e5e7eb)}",
637
+ ".dsh-upd-strip-v{font-family:Consolas,Menlo,monospace;font-size:12.5px;word-break:break-all}",
638
+ ".dsh-upd-chapter{margin-top:18px;padding-top:14px;border-top:1px solid var(--dsh-update-border,#e5e7eb)}",
536
639
  // 右下角独立 footer 区(#47 定案 A):与第一章 actions 脱钩,右对齐,一次找到。
537
640
  ".dsh-upd-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:14px;padding-top:10px;border-top:1px solid var(--dsh-update-border,#e5e7eb)}",
538
641
  ".dsh-upd-foot-note{margin-right:auto;font-size:12px;opacity:.7}",
@@ -545,7 +648,7 @@ const UPDATE_PANEL_CSS = [
545
648
  ".dsh-upd-chap-note{flex:none;font-size:12px;opacity:.75}",
546
649
  ".dsh-upd-chap-note button{margin:0}",
547
650
  // 更新队列两行键值(用户定案的设计):结构两主题共用,皮肤各自收敛。
548
- ".dsh-upd-qrow{display:flex;align-items:baseline;gap:10px;padding:6px 0}",
651
+ ".dsh-upd-qrow{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px;padding:6px 0;min-width:0}",
549
652
  ".dsh-upd-qrow+.dsh-upd-qrow{border-top:1px solid var(--dsh-update-border,#e5e7eb)}",
550
653
  ".dsh-upd-qdot{width:8px;height:8px;border-radius:50%;flex:none;align-self:center;background:currentColor;opacity:.5}",
551
654
  '.dsh-upd-qdot[data-tone="busy"]{background:var(--dsh-update-warn-border,#d97706);opacity:1}',
@@ -1005,10 +1108,12 @@ function renderUpdatePanelHTML(input, lang) {
1005
1108
  const kernel = renderUpdatePanelKernel(input, view, l);
1006
1109
  const archive = normalizePanelTheme(input.theme) === "archive";
1007
1110
  const attr = archive ? ' data-theme="archive"' : "";
1111
+ const tokensStyle = themeTokensStyleFor(input.themeTokens ?? void 0);
1112
+ const tokensAttr = tokensStyle ? ` style="${tokensStyle}"` : "";
1008
1113
  const sealAttr = ` data-seal="${escapeHtml(view.seal.text)}" data-seal-tone="${view.seal.tone}"`;
1009
- const body = input.mode === "dialog" ? `<div class="dsh-upd-overlay" data-mode="dialog"><div class="dsh-upd" data-mode="dialog" data-plugin="${escapeHtml(input.pluginId)}"${sealAttr}${attr}>
1114
+ const body = input.mode === "dialog" ? `<div class="dsh-upd-overlay" data-mode="dialog"><div class="dsh-upd" data-mode="dialog" data-plugin="${escapeHtml(input.pluginId)}"${sealAttr}${attr}${tokensAttr}>
1010
1115
  ${kernel}
1011
- </div></div>` : `<div class="dsh-upd" data-mode="embedded" data-plugin="${escapeHtml(input.pluginId)}"${sealAttr}${attr}>
1116
+ </div></div>` : `<div class="dsh-upd" data-mode="embedded" data-plugin="${escapeHtml(input.pluginId)}"${sealAttr}${attr}${tokensAttr}>
1012
1117
  ${kernel}
1013
1118
  </div>`;
1014
1119
  const css = archive ? `${UPDATE_PANEL_CSS}
@@ -1110,6 +1215,8 @@ function mountUpdatePanel(container, options) {
1110
1215
  throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08\u6536\u5230 ${JSON.stringify(options.theme)}\uFF09`);
1111
1216
  }
1112
1217
  let theme = normalizePanelTheme(options.theme ?? "default");
1218
+ themeTokensStyleFor(options.themeTokens ?? void 0);
1219
+ let themeTokens = options.themeTokens;
1113
1220
  const localeOpt = options.locale ?? void 0;
1114
1221
  if (localeOpt !== void 0 && localeOpt !== null) {
1115
1222
  const isStr = typeof localeOpt === "string";
@@ -1426,6 +1533,7 @@ function mountUpdatePanel(container, options) {
1426
1533
  pluginId,
1427
1534
  copyNotice,
1428
1535
  theme,
1536
+ themeTokens,
1429
1537
  // 使用范围与宿主种类:调用方显式传的优先,否则用宿主回的真值。
1430
1538
  profileName: profileNameOption ?? envProfileName,
1431
1539
  hostKind: hostKind ?? envHostKind
@@ -1807,6 +1915,11 @@ function mountUpdatePanel(container, options) {
1807
1915
  theme = normalizePanelTheme(next);
1808
1916
  render();
1809
1917
  }
1918
+ async function setThemeTokens(next) {
1919
+ themeTokensStyleFor(next ?? void 0);
1920
+ themeTokens = next ?? void 0;
1921
+ render();
1922
+ }
1810
1923
  async function setShowOthers(show) {
1811
1924
  showOthers = show === true;
1812
1925
  await refresh();
@@ -1899,7 +2012,7 @@ function mountUpdatePanel(container, options) {
1899
2012
  } catch {
1900
2013
  }
1901
2014
  })();
1902
- return { refresh, act, setMode, setTheme, setShowOthers, setChangelogCollapsed, setChangelogMarkdown, unmount };
2015
+ return { refresh, act, setMode, setTheme, setThemeTokens, setShowOthers, setChangelogCollapsed, setChangelogMarkdown, unmount };
1903
2016
  }
1904
2017
  import { buildPhoneNames as buildPhoneNames2 } from "./config.js";
1905
2018
  import { buildChangelogPhoneName as buildChangelogPhoneName2 } from "./config.js";
@@ -1968,6 +2081,7 @@ export {
1968
2081
  isHeadOfQueue,
1969
2082
  isKnownFailureCode,
1970
2083
  isQueueBusy,
2084
+ isSafeThemeCssValue,
1971
2085
  isUnreleasedVersion,
1972
2086
  mountUpdatePanel,
1973
2087
  normalizePanelTheme,
@@ -1985,5 +2099,6 @@ export {
1985
2099
  renderUpdatePanelHTML,
1986
2100
  renderUpdatePanelKernel,
1987
2101
  selectChangelogEntries,
2102
+ themeTokensStyleFor,
1988
2103
  visibleQueueFor2 as visibleQueueFor
1989
2104
  };
package/package.json CHANGED
@@ -1,76 +1,76 @@
1
- {
2
- "name": "dsh-plugin-update",
3
- "version": "0.6.0",
4
- "description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
5
- "license": "MIT",
6
- "author": "王辰浩",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/FeatherHunter/dsh-plugin-update.git"
10
- },
11
- "engines": {
12
- "node": ">=22"
13
- },
14
- "type": "module",
15
- "types": "./dist/host.d.ts",
16
- "exports": {
17
- ".": {
18
- "types": "./dist/host.d.ts",
19
- "default": "./dist/host.js"
20
- },
21
- "./panel": {
22
- "types": "./dist/panel.d.ts",
23
- "default": "./dist/panel.js"
24
- },
25
- "./client": {
26
- "types": "./dist/client.d.ts",
27
- "default": "./dist/client.js"
28
- },
29
- "./batch": {
30
- "types": "./dist/host-batch.d.ts",
31
- "default": "./dist/host-batch.js"
32
- },
33
- "./panel-batch": {
34
- "types": "./dist/panel-batch.d.ts",
35
- "default": "./dist/panel-batch.js"
36
- },
37
- "./entry": {
38
- "types": "./dist/entry.d.ts",
39
- "default": "./dist/entry.js"
40
- },
41
- "./entry-batch": {
42
- "types": "./dist/entry-batch.d.ts",
43
- "default": "./dist/entry-batch.js"
44
- },
45
- "./http": {
46
- "types": "./dist/http.d.ts",
47
- "default": "./dist/http.js"
48
- },
49
- "./package.json": "./package.json"
50
- },
51
- "files": [
52
- "dist",
53
- "derive-client-values.mjs",
54
- "event-list.template.json",
55
- "README.md",
56
- "CHANGELOG.md",
57
- "LICENSE"
58
- ],
59
- "publishConfig": {
60
- "access": "public",
61
- "registry": "https://registry.npmjs.org/"
62
- },
63
- "scripts": {
64
- "typecheck": "tsc -p tsconfig.json",
65
- "build": "node build.mjs",
66
- "prepack": "npm run build",
67
- "pretest": "npm run build",
68
- "test": "node --test tests/*.test.mjs",
69
- "gate:bilingual": "node scripts/bilingual-gate.mjs",
70
- "gate:bilingual:release": "node scripts/bilingual-gate.mjs --release"
71
- },
72
- "devDependencies": {
73
- "esbuild": "^0.28.2",
74
- "typescript": "5.6.3"
75
- }
76
- }
1
+ {
2
+ "name": "dsh-plugin-update",
3
+ "version": "0.7.1",
4
+ "description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
5
+ "license": "MIT",
6
+ "author": "王辰浩",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/FeatherHunter/dsh-plugin-update.git"
10
+ },
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "type": "module",
15
+ "types": "./dist/host.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/host.d.ts",
19
+ "default": "./dist/host.js"
20
+ },
21
+ "./panel": {
22
+ "types": "./dist/panel.d.ts",
23
+ "default": "./dist/panel.js"
24
+ },
25
+ "./client": {
26
+ "types": "./dist/client.d.ts",
27
+ "default": "./dist/client.js"
28
+ },
29
+ "./batch": {
30
+ "types": "./dist/host-batch.d.ts",
31
+ "default": "./dist/host-batch.js"
32
+ },
33
+ "./panel-batch": {
34
+ "types": "./dist/panel-batch.d.ts",
35
+ "default": "./dist/panel-batch.js"
36
+ },
37
+ "./entry": {
38
+ "types": "./dist/entry.d.ts",
39
+ "default": "./dist/entry.js"
40
+ },
41
+ "./entry-batch": {
42
+ "types": "./dist/entry-batch.d.ts",
43
+ "default": "./dist/entry-batch.js"
44
+ },
45
+ "./http": {
46
+ "types": "./dist/http.d.ts",
47
+ "default": "./dist/http.js"
48
+ },
49
+ "./package.json": "./package.json"
50
+ },
51
+ "files": [
52
+ "dist",
53
+ "derive-client-values.mjs",
54
+ "event-list.template.json",
55
+ "README.md",
56
+ "CHANGELOG.md",
57
+ "LICENSE"
58
+ ],
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "registry": "https://registry.npmjs.org/"
62
+ },
63
+ "scripts": {
64
+ "typecheck": "tsc -p tsconfig.json",
65
+ "build": "node build.mjs",
66
+ "prepack": "npm run build",
67
+ "pretest": "npm run build",
68
+ "test": "node --test tests/*.test.mjs",
69
+ "gate:bilingual": "node scripts/bilingual-gate.mjs",
70
+ "gate:bilingual:release": "node scripts/bilingual-gate.mjs --release"
71
+ },
72
+ "devDependencies": {
73
+ "esbuild": "^0.28.2",
74
+ "typescript": "5.6.3"
75
+ }
76
+ }