dsh-plugin-update 0.6.0 → 0.7.0

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,10 @@
2
2
 
3
3
  格式为 Keep-a-Changelog 子集:`Added/Fixed/Changed` 必写,`Deprecated/Removed/Security` 有则透传,`Unreleased` 面板忽略,禁止 git-log 直倒。面板缺日志时中性提示,不挡安装。
4
4
 
5
+ ## [0.7.0] - 2026-10-06
6
+
7
+ ### Added
8
+ - 主题变量覆盖 `themeTokens`(第三方换肤一等口径):单面板、批量面板、入口件、批量入口件四个挂载口同收同语义(35 个语义键对 `--dsh-update-*` 变量,颜色收 hex 或英文名,其余走注入检查,非法挂载即抛);内联写根、变量继承,入口件打开的 dialog 同步透传,批量展开的详情行经继承生效;控制器同步新增 `setThemeTokens`(传 `undefined` 即清掉覆盖);被覆盖的 token 不再跟随深色媒体查询。README 补换肤章节与键表。
5
9
  ## [0.6.0] - 2026-10-06
6
10
 
7
11
  ### 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
  }
@@ -159,6 +163,8 @@ export declare function batchRowKnowledgeText(row: BatchRowView, entry: BatchKno
159
163
  export interface BatchPanelRenderInput {
160
164
  rows?: readonly BatchRowView[];
161
165
  theme?: UpdatePanelTheme;
166
+ /** 主题变量覆盖:见 panel 的 UpdateThemeTokens;写到批量根上,详情行经继承生效。 */
167
+ themeTokens?: UpdateThemeTokens;
162
168
  mode?: BatchPanelMode;
163
169
  /** 当前展开的那一家(键);null 即全部收起。 */
164
170
  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,
@@ -198,6 +199,7 @@ function renderBatchPanelHTML(input) {
198
199
  busy: installing,
199
200
  inFlight: input.inFlight === true,
200
201
  theme,
202
+ themeTokens: input.themeTokens ?? void 0,
201
203
  titles,
202
204
  skipped: null,
203
205
  notice: null,
@@ -285,8 +287,10 @@ function renderBatchPanelHTML(input) {
285
287
  const kernel = parts.join(String.fromCharCode(10));
286
288
  const archive = normalizePanelTheme(theme) === "archive";
287
289
  const attr = archive ? ' data-theme="archive"' : "";
290
+ const tokensStyle = themeTokensStyleFor(input.themeTokens ?? void 0);
291
+ const tokensAttr = tokensStyle ? ' style="' + tokensStyle + '"' : "";
288
292
  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>";
293
+ 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
294
  const body = mode === "dialog" ? '<div class="dsh-upd-overlay" data-mode="dialog">' + root + "</div>" : root;
291
295
  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
296
  return "<style>" + css + "</style>\n" + body;
@@ -411,6 +415,7 @@ function detailHTML(row, ctx) {
411
415
  pluginId: pluginIdOf(row),
412
416
  copyNotice: null,
413
417
  theme: ctx.theme,
418
+ themeTokens: ctx.themeTokens,
414
419
  profileName: typeof row.profileName === "string" && row.profileName ? row.profileName : null,
415
420
  actions: "none"
416
421
  }, ctx.lang);
@@ -762,6 +767,8 @@ function mountUpdateBatchPanel(container, options) {
762
767
  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
768
  }
764
769
  let theme = normalizePanelTheme(options.theme ?? "default");
770
+ themeTokensStyleFor(options.themeTokens ?? void 0);
771
+ let themeTokens = options.themeTokens;
765
772
  const call = options.call;
766
773
  const titles = options.titles && typeof options.titles === "object" ? options.titles : {};
767
774
  const onRestartRequested = options.onRestartRequested;
@@ -952,6 +959,7 @@ function mountUpdateBatchPanel(container, options) {
952
959
  const nextHTML = renderBatchPanelHTML({
953
960
  rows,
954
961
  theme,
962
+ themeTokens,
955
963
  mode,
956
964
  expandedKey,
957
965
  lastError,
@@ -1273,6 +1281,11 @@ function mountUpdateBatchPanel(container, options) {
1273
1281
  theme = normalizePanelTheme(next);
1274
1282
  render();
1275
1283
  }
1284
+ function setThemeTokens(next) {
1285
+ themeTokensStyleFor(next ?? void 0);
1286
+ themeTokens = next ?? void 0;
1287
+ render();
1288
+ }
1276
1289
  function setMode(next) {
1277
1290
  if (next !== "embedded" && next !== "dialog") {
1278
1291
  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 +1409,7 @@ function mountUpdateBatchPanel(container, options) {
1396
1409
  void refresh().then(() => {
1397
1410
  void autoOnce();
1398
1411
  });
1399
- return { refresh, act, setTheme, setMode, unmount };
1412
+ return { refresh, act, setTheme, setThemeTokens, setMode, unmount };
1400
1413
  }
1401
1414
  export {
1402
1415
  BATCH_PANEL_POLL,
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,50 @@ 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
+ fontSans?: string;
41
+ fontSerif?: string;
42
+ fontMono?: string;
43
+ shadow?: string;
44
+ radiusPanel?: string;
45
+ radiusButton?: string;
46
+ radiusBadge?: string;
47
+ entryFontSize?: string;
48
+ entryPadding?: string;
49
+ entryBorderRadius?: string;
50
+ /** 入口件整体缩放(对应 CSS `zoom`),须为大于 0 的有限数。 */
51
+ entryScale?: number;
52
+ }
53
+ /** CSS 值最小安全检查(与入口件 sizing 同口径):放 style 属性前先拦掉注入。 */
54
+ export declare function isSafeThemeCssValue(value: string): boolean;
55
+ /**
56
+ * `themeTokens` → 根 style 属性体(纯函数:同一输入永远算出同一串;空/缺省回空串,即不写 style)。
57
+ * 非法即抛(未知键、空串、注入字符、颜色形状不对、非正有限 entryScale 都不收)。
58
+ */
59
+ export declare function themeTokensStyleFor(tokens: UpdateThemeTokens | null | undefined): string;
17
60
  /** 与宿主通话的传输函数:面板只认这个签名,不认任何宿主对象的具体形状。 */
18
61
  export type UpdatePanelCall = (phoneName: string, args: Record<string, unknown>) => Promise<Record<string, unknown>>;
19
62
  /** 复制文本的出口:不传即尝试浏览器剪贴板,都没有则只留“已生成、请手动复制”提示。 */
@@ -29,6 +72,8 @@ export interface UpdatePanelOptions {
29
72
  mode?: UpdatePanelMode;
30
73
  /** 面板主题:默认 `default`(最小可用样式,一字不动);传 `archive` 切档案卷。 */
31
74
  theme?: UpdatePanelTheme;
75
+ /** 主题变量覆盖:见 UpdateThemeTokens;两个主题通用,不传即零回归。 */
76
+ themeTokens?: UpdateThemeTokens;
32
77
  /** 是否看他人排队明细:默认只看自己的(他人仅露正忙占位,位置照给)。 */
33
78
  showOthers?: boolean;
34
79
  /** 是否显示深挖日志指引:默认显示;面向纯终端用户的嵌入可关(失败证据行不受影响)。 */
@@ -90,6 +135,8 @@ export interface UpdatePanelController {
90
135
  setMode(mode: UpdatePanelMode): Promise<void>;
91
136
  /** 切换主题(同一内核重绘,只换肤;默认主题输出与旧版一字不差)。 */
92
137
  setTheme(theme: UpdatePanelTheme): Promise<void>;
138
+ /** 换一套主题变量(同一内核重绘,只换变量;传 undefined 即清掉覆盖,回主题默认)。 */
139
+ setThemeTokens(tokens: UpdateThemeTokens | undefined): Promise<void>;
93
140
  /** 切换排队可见性(重查一次,位置口径不变)。 */
94
141
  setShowOthers(show: boolean): Promise<void>;
95
142
  /** 收起/展开更新日志(纯视图态,不调电话;轮询与换肤不丢)。 */
@@ -256,6 +303,8 @@ export interface PanelRenderInput extends PanelViewInput {
256
303
  lang?: AppLang | string | null;
257
304
  /** 可选主题:不传即默认(输出与旧版一字不差);`archive` 切档案卷。 */
258
305
  theme?: UpdatePanelTheme;
306
+ /** 主题变量覆盖:见 UpdateThemeTokens;写到面板根上,不传即无 style 属性。 */
307
+ themeTokens?: UpdateThemeTokens;
259
308
  /** 使用范围名(profile):面板「使用范围」一栏的唯一来源,缺省显示“未知”,不猜。 */
260
309
  profileName?: string | null;
261
310
  /** 宿主种类:进诊断文本;缺省显示“未知”。 */
package/dist/panel.js CHANGED
@@ -15,6 +15,106 @@ 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
+ fontSans: "--dsh-update-font-sans",
42
+ fontSerif: "--dsh-update-font-serif",
43
+ fontMono: "--dsh-update-font-mono",
44
+ shadow: "--dsh-update-shadow",
45
+ radiusPanel: "--dsh-update-radius-panel",
46
+ radiusButton: "--dsh-update-radius-button",
47
+ radiusBadge: "--dsh-update-radius-badge",
48
+ entryFontSize: "--dsh-update-entry-font-size",
49
+ entryPadding: "--dsh-update-entry-padding",
50
+ entryBorderRadius: "--dsh-update-entry-border-radius",
51
+ entryScale: "--dsh-update-entry-scale"
52
+ };
53
+ const THEME_TOKEN_COLOR_KEYS = /* @__PURE__ */ new Set([
54
+ "text",
55
+ "textMuted",
56
+ "bg",
57
+ "bgSoft",
58
+ "border",
59
+ "borderStrong",
60
+ "buttonBg",
61
+ "primary",
62
+ "primaryDeep",
63
+ "focus",
64
+ "okBg",
65
+ "okBorder",
66
+ "okText",
67
+ "warnBg",
68
+ "warnBorder",
69
+ "warnText",
70
+ "badBg",
71
+ "badBorder",
72
+ "badText",
73
+ "busyBg",
74
+ "busyBorder",
75
+ "busyText"
76
+ ]);
77
+ function themeTokensError(raw) {
78
+ 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)})`);
79
+ }
80
+ function isSafeThemeCssValue(value) {
81
+ const v = value.trim();
82
+ if (!v || v.length > 200) return false;
83
+ if (/[;"'<>\`{}!&]/.test(v)) return false;
84
+ if (/url\s*\(/i.test(v)) return false;
85
+ if (/expression\s*\(/i.test(v)) return false;
86
+ if (/javascript\s*:/i.test(v)) return false;
87
+ return true;
88
+ }
89
+ function isThemeColorValue(value) {
90
+ const v = value.trim();
91
+ if (!v || v.length > 100) return false;
92
+ return /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{8}|[a-zA-Z]+)$/.test(v);
93
+ }
94
+ function themeTokensStyleFor(tokens) {
95
+ if (tokens === void 0 || tokens === null) return "";
96
+ if (typeof tokens !== "object" || Array.isArray(tokens)) throw themeTokensError(tokens);
97
+ for (const key of Object.keys(tokens)) {
98
+ if (!Object.prototype.hasOwnProperty.call(THEME_TOKEN_VARS, key)) throw themeTokensError(tokens);
99
+ }
100
+ const parts = [];
101
+ for (const key of Object.keys(THEME_TOKEN_VARS)) {
102
+ const value = tokens[key];
103
+ if (value === void 0) continue;
104
+ if (key === "entryScale") {
105
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw themeTokensError(tokens);
106
+ parts.push(THEME_TOKEN_VARS[key] + ":" + String(value));
107
+ continue;
108
+ }
109
+ if (typeof value !== "string") throw themeTokensError(tokens);
110
+ const text = value.trim();
111
+ if (THEME_TOKEN_COLOR_KEYS.has(key)) {
112
+ if (!isThemeColorValue(text)) throw themeTokensError(tokens);
113
+ } else if (!isSafeThemeCssValue(text)) throw themeTokensError(tokens);
114
+ parts.push(THEME_TOKEN_VARS[key] + ":" + text);
115
+ }
116
+ return parts.join(";");
117
+ }
18
118
  const BLOCKED_KEYS = {
19
119
  "unknown-profile": { title: "panel.blocked.unknown-profile.title", action: "panel.blocked.unknown-profile.action" },
20
120
  "source-install": { title: "panel.blocked.source-install.title", action: "panel.blocked.source-install.action" },
@@ -1005,10 +1105,12 @@ function renderUpdatePanelHTML(input, lang) {
1005
1105
  const kernel = renderUpdatePanelKernel(input, view, l);
1006
1106
  const archive = normalizePanelTheme(input.theme) === "archive";
1007
1107
  const attr = archive ? ' data-theme="archive"' : "";
1108
+ const tokensStyle = themeTokensStyleFor(input.themeTokens ?? void 0);
1109
+ const tokensAttr = tokensStyle ? ` style="${tokensStyle}"` : "";
1008
1110
  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}>
1111
+ 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
1112
  ${kernel}
1011
- </div></div>` : `<div class="dsh-upd" data-mode="embedded" data-plugin="${escapeHtml(input.pluginId)}"${sealAttr}${attr}>
1113
+ </div></div>` : `<div class="dsh-upd" data-mode="embedded" data-plugin="${escapeHtml(input.pluginId)}"${sealAttr}${attr}${tokensAttr}>
1012
1114
  ${kernel}
1013
1115
  </div>`;
1014
1116
  const css = archive ? `${UPDATE_PANEL_CSS}
@@ -1110,6 +1212,8 @@ function mountUpdatePanel(container, options) {
1110
1212
  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
1213
  }
1112
1214
  let theme = normalizePanelTheme(options.theme ?? "default");
1215
+ themeTokensStyleFor(options.themeTokens ?? void 0);
1216
+ let themeTokens = options.themeTokens;
1113
1217
  const localeOpt = options.locale ?? void 0;
1114
1218
  if (localeOpt !== void 0 && localeOpt !== null) {
1115
1219
  const isStr = typeof localeOpt === "string";
@@ -1426,6 +1530,7 @@ function mountUpdatePanel(container, options) {
1426
1530
  pluginId,
1427
1531
  copyNotice,
1428
1532
  theme,
1533
+ themeTokens,
1429
1534
  // 使用范围与宿主种类:调用方显式传的优先,否则用宿主回的真值。
1430
1535
  profileName: profileNameOption ?? envProfileName,
1431
1536
  hostKind: hostKind ?? envHostKind
@@ -1807,6 +1912,11 @@ function mountUpdatePanel(container, options) {
1807
1912
  theme = normalizePanelTheme(next);
1808
1913
  render();
1809
1914
  }
1915
+ async function setThemeTokens(next) {
1916
+ themeTokensStyleFor(next ?? void 0);
1917
+ themeTokens = next ?? void 0;
1918
+ render();
1919
+ }
1810
1920
  async function setShowOthers(show) {
1811
1921
  showOthers = show === true;
1812
1922
  await refresh();
@@ -1899,7 +2009,7 @@ function mountUpdatePanel(container, options) {
1899
2009
  } catch {
1900
2010
  }
1901
2011
  })();
1902
- return { refresh, act, setMode, setTheme, setShowOthers, setChangelogCollapsed, setChangelogMarkdown, unmount };
2012
+ return { refresh, act, setMode, setTheme, setThemeTokens, setShowOthers, setChangelogCollapsed, setChangelogMarkdown, unmount };
1903
2013
  }
1904
2014
  import { buildPhoneNames as buildPhoneNames2 } from "./config.js";
1905
2015
  import { buildChangelogPhoneName as buildChangelogPhoneName2 } from "./config.js";
@@ -1968,6 +2078,7 @@ export {
1968
2078
  isHeadOfQueue,
1969
2079
  isKnownFailureCode,
1970
2080
  isQueueBusy,
2081
+ isSafeThemeCssValue,
1971
2082
  isUnreleasedVersion,
1972
2083
  mountUpdatePanel,
1973
2084
  normalizePanelTheme,
@@ -1985,5 +2096,6 @@ export {
1985
2096
  renderUpdatePanelHTML,
1986
2097
  renderUpdatePanelKernel,
1987
2098
  selectChangelogEntries,
2099
+ themeTokensStyleFor,
1988
2100
  visibleQueueFor2 as visibleQueueFor
1989
2101
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-update",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
5
5
  "license": "MIT",
6
6
  "author": "王辰浩",