dsh-theme-palette 1.8.1 → 1.8.3

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/client.js +133 -9
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,46 @@
4
4
 
5
5
  ## 版本一览
6
6
 
7
+ ### 1.8.3
8
+ 补齐 token 覆盖缺口(按宿主 118 个 alias/specific 做传递闭包统计:76% 跟随;余下 28 个逐项判断该不该跟):
9
+
10
+ - **深色菜单**:`--dsw-specific-menu` 宿主带字面量兜底(深 `#303136f0`),只改 `menu-surface-fill` 不够,
11
+ 深色菜单仍是冷灰 → 现在按配色给(浅 50/深 850,alpha 0.94)
12
+ - **品牌新色**:`--dsw-alias-brand-primary-new-…` 浅色写死 `#4176e6` 宿主蓝 → 两模式都归到强调色
13
+ - **文件 diff 温度调和**:6 个 `file-diff-*` 保留绿/红语义,色相往配色拉 16–22°、压低饱和;
14
+ 断言「新增仍是绿色系、删除仍是红色系」+ 正文对 diff 底 ≥4.5
15
+ - **对比度矩阵加菜单面**:半透明面按画布合成后给出参考值,标 `※`(不计硬门),
16
+ UI 与 `palette-dev` 共用同一份 `CONTRAST_SURFACES`(含 token 面)
17
+ - **诊断导出**:矩阵区新增「复制诊断」,输出 settings + 对比度矩阵 JSON,便于提 issue
18
+ - **forced-colors**:核实我们改的都是 UA 强制色属性,系统高对比模式会自动接管,
19
+ 因此**不加** `forced-color-adjust: none` 对抗(代码内已注明理由)
20
+ - 契约测试补 `--dsw-specific-menu` / `--dsw-alias-brand-primary-new-…` / `--dsw-alias-file-diff-added-bg`
21
+ - 明确不跟:`bg-mask-*`(遮罩)、`border-inverted*`(反色边框)、onboarding(一次性引导)——刻意保留宿主原值
22
+
23
+ ### 1.8.2
24
+ **暖砂字体体系修正**(用户反馈“和其他两套不一样,尤其字体颜色”):
25
+
26
+ - **根因 1:深色正文字色度没有按色域归一**。`paperC` 是绝对值,×2.4 后暖砂请求 130%、靛蓝 166% 色域上限,
27
+ 实际色度被裁成 0.0244 / 0.0115 / 0.0303——暖砂深正文几乎是靛蓝的两倍黄。现在三套统一 `paperC=0.0053`
28
+ (标准档实际 ≈0.0127),浅/深正文色度比降到 1.21 / 1.10
29
+ - **根因 2:暖砂是唯一「深色面 60°、正文 84°」的双色相预置**。深色面橙棕、正文黄绿,色相分裂 24°;
30
+ 现在 `paper` 角色跟随 `darkHue`,并把暖砂 `hue` 84→72、`darkHue` 60→66,全套色相收敛到 12° 内
31
+ - 新增断言:深正文色相偏离 `darkHue` ≤12°、三套浅色正文色度比 ≤1.25、深色 ≤1.5
32
+ - 新增 `tools/preview-typography.mjs`:三套 × 明暗并排渲染同一段文字,发版前目检
33
+
34
+ 语法高亮跟随配色(1.8.1 里“未做”的那条,前提已实测):
35
+
36
+ - **实测依据**:宿主渲染器用 Shiki 的 `css-variables` 模式(`class="shiki css-variables"`,
37
+ `style="background-color:var(--shiki-background)"`),token 颜色走 CSS 变量,
38
+ 所以覆盖 `--shiki-token-*` 生效,不需要 `!important`
39
+ - **色相策略**:从强调色相取 3 个色族(强调 / +150° 冷族 / -60° 暖族),
40
+ 同族靠明度分常/表达;每个 token 对代码块底求解到 **≥4.6:1**,四族最小 ΔE **≥10**
41
+ - 覆盖 9 类 token(keyword / function / constant / parameter / string / string-expression /
42
+ comment / punctuation / link)+ `--shiki-background` / `--shiki-foreground`
43
+ - **md=off 时不输出**任何 shiki token,交回宿主原色
44
+ - 新增 `tools/preview-code.mjs`:用生产 token 渲染一段真实代码,发版前肉眼确认
45
+ - 断言:9 类 token 对比度 ≥4.6、四族 ΔE ≥10、shiki 背景与代码块一致、md=off 无输出
46
+
7
47
  ### 1.8.1
8
48
  Markdown 配色丰富化(非加色相,而是加层级 + 修代码块底):
9
49
 
package/client.js CHANGED
@@ -27,7 +27,7 @@ window.__ModuleLoader__.load({
27
27
  const React = require('react')
28
28
  const h = React.createElement
29
29
 
30
- const PLUGIN_VERSION = '1.8.1'
30
+ const PLUGIN_VERSION = '1.8.3'
31
31
  const STORE_KEY = 'dsh-theme-palette.v1'
32
32
  const OVERRIDE_SOURCE = 'dsh-theme-palette'
33
33
 
@@ -107,22 +107,22 @@ window.__ModuleLoader__.load({
107
107
  { id: 'mono', label: '黑白', desc: '无色调 · 默认', tinted: false },
108
108
  {
109
109
  id: 'warm', label: '暖砂', desc: '米白 + 棕调', tinted: true,
110
- hue: 84, darkHue: 60, lightC: 0.030, midC: 0.010, textC: 0.004,
111
- darkC: 0.014, darkLift: 0.026, paperC: 0.014, // 深底 #1e1b14 L 0.223 / 30% 色域
110
+ hue: 72, darkHue: 66, lightC: 0.030, midC: 0.010, textC: 0.004,
111
+ darkC: 0.014, darkLift: 0.026, paperC: 0.0053, // 深正文按色域归一:×2.4 后 ≈0.0127
112
112
  accentHue: 60, accentC: 0.101, accentL: 0.58, // 标准色 #A9683E
113
113
  accentScale: 1.0,
114
114
  },
115
115
  {
116
116
  id: 'indigo', label: '靛蓝', desc: '冷白 + 蓝调', tinted: true,
117
117
  hue: 266, lightC: 0.020, midC: 0.010, textC: 0.004,
118
- darkC: 0.028, darkLift: 0.032, paperC: 0.009, // 深底 #161c2a L 0.227 / 19% 色域
118
+ darkC: 0.028, darkLift: 0.032, paperC: 0.0053, // 同上:三套统一,避免被色域裁成两倍差
119
119
  accentHue: 259, accentC: 0.107, accentL: 0.5108, // 标准色 #3F66A3
120
120
  accentScale: 1.0,
121
121
  },
122
122
  {
123
123
  id: 'moss', label: '苔绿', desc: '浅灰绿 + 绿调', tinted: true,
124
124
  hue: 162, lightC: 0.016, midC: 0.010, textC: 0.004,
125
- darkC: 0.015, darkLift: 0.042, paperC: 0.0125, // 深底 #1a211b L 0.239 / 24% 色域
125
+ darkC: 0.015, darkLift: 0.042, paperC: 0.0053, // 同上
126
126
  accentHue: 139, accentC: 0.096, accentL: 0.5335, // 标准色 #4F7A42
127
127
  accentScale: 1.0,
128
128
  },
@@ -240,6 +240,10 @@ window.__ModuleLoader__.load({
240
240
  { id: '875', mode: 'dark', scope: true },
241
241
  { id: '850', mode: 'dark', scope: true },
242
242
  { id: '800', mode: 'dark', scope: true },
243
+ // 菜单是半透明面,实际颜色取决于 backdrop;这里按"合成在画布上"给出参考值,
244
+ // 标 scope:false(不计硬门),并在 UI 用 ※ 注明依赖 backdrop
245
+ { id: 'menuL', label: 'menu', mode: 'light', scope: false, token: '--dsw-specific-menu' },
246
+ { id: 'menuD', label: 'menu', mode: 'dark', scope: false, token: '--dsw-specific-menu' },
243
247
  ]
244
248
  const CONTRAST_ROLES = [
245
249
  { id: 'main', step: { light: '1000', dark: '50' } },
@@ -437,6 +441,26 @@ window.__ModuleLoader__.load({
437
441
  return hex + Math.round(clamp(alpha, 0, 1) * 255).toString(16).padStart(2, '0')
438
442
  }
439
443
 
444
+ /** 8 位 hex(带 alpha)按「覆盖在 bg 上」合成为不透明色;不带 alpha 时原样返回。 */
445
+ function flatten(hex, bgHex) {
446
+ if (typeof hex !== 'string' || hex.length !== 9) return hex
447
+ const fg = hexToRgb(hex.slice(0, 7))
448
+ const bg = hexToRgb(bgHex)
449
+ const a = parseInt(hex.slice(7, 9), 16) / 255
450
+ return rgbToHex(fg.map((v, i) => v * a + bg[i] * (1 - a)))
451
+ }
452
+
453
+ /**
454
+ * 语义色温度调和:保留色相为主(绿仍是绿、红仍是红),只把色相往配色的暖/冷方向
455
+ * 拉一小段、并压低饱和度。用于 file diff 这种大面积色块——不能整块换成主题色,
456
+ * 那会让「新增/删除」失去语义。
457
+ */
458
+ function harmonize(hex, targetHue, pull, chromaScale) {
459
+ const [L, C, h] = toOklch(hex)
460
+ const dh = ((targetHue - h + 540) % 360) - 180
461
+ return fromOklch(L, C * chromaScale, h + dh * pull)
462
+ }
463
+
440
464
  /**
441
465
  * 区块底色微调:在 OKLab 里给亮度加 delta,同时按需削减色度。
442
466
  *
@@ -495,9 +519,10 @@ window.__ModuleLoader__.load({
495
519
  // 换成 60°(橙棕)才是咖啡色。浅色侧仍是 88°(米白)。
496
520
  // 文字档(含深色正文 paper)跟随「文字」这一项自己的色相;预设没有 textHue,行为不变。
497
521
  const tintRole = role === 'text' || role === 'paper'
522
+ // 深色角色(含深色正文 paper)统一走 darkHue,避免「深色面橙棕、正文黄绿」的双色相
498
523
  const hue = tintRole && scheme.textHue !== undefined
499
524
  ? scheme.textHue
500
- : (role === 'dark' && scheme.darkHue !== undefined ? scheme.darkHue : scheme.hue)
525
+ : ((role === 'dark' || role === 'paper') && scheme.darkHue !== undefined ? scheme.darkHue : scheme.hue)
501
526
  bluish[step] = fromOklch(clamp(compressL(lifted), 0, 0.995), roleChroma(role, weight, scheme, scales), hue)
502
527
  }
503
528
  const neutral = {}
@@ -715,6 +740,17 @@ window.__ModuleLoader__.load({
715
740
  light: withAlpha(r.bluish['50'], 0.58),
716
741
  dark: withAlpha(r.bluish['750'], 0.45),
717
742
  },
743
+ // 宿主 `--dsw-specific-menu` 自带字面量兜底(浅 #f8f9faf0 / 深 #303136f0),
744
+ // 只改 menu-surface-fill 不够:深色菜单实际用的是它,会留着冷灰。这里一并对齐。
745
+ '--dsw-specific-menu': {
746
+ light: withAlpha(r.bluish['50'], 0.94),
747
+ dark: withAlpha(r.bluish['850'], 0.94),
748
+ },
749
+ // 浅色写死 #4176e6(宿主蓝),深色已引用 deepseek-450;两模式都归到强调色
750
+ '--dsw-alias-brand-primary-new-colorprimary-new-color': {
751
+ light: r.deepseek['500'],
752
+ dark: r.deepseek['400'],
753
+ },
718
754
  '--dsw-alias-menu-group-header-fill': {
719
755
  // 用 60 而不是 50:50 现在是「深色模式正文」的色度,拿去当浅色菜单头会偏艳
720
756
  light: withAlpha(r.bluish['60'], 0.94),
@@ -935,6 +971,63 @@ window.__ModuleLoader__.load({
935
971
  light: mixHex(ramps.deepseek['500'], ramps.bluish['50'], 0.05),
936
972
  dark: mixHex(ramps.deepseek['400'], ramps.bluish['900'], 0.07),
937
973
  }
974
+ /* ---- 文件 diff:宿主是固定冷绿/冷红的大面积色块,暖砂/苔绿下最显眼。
975
+ * 只做「温度调和」:保留绿=新增、红=删除的语义色相,往配色色相拉 16–22° 并压低饱和。 */
976
+ if (params.tinted) {
977
+ const diffHue = params.hue
978
+ const area = (hex) => harmonize(hex, diffHue, 0.16, 0.90)
979
+ const mark = (hex) => harmonize(hex, diffHue, 0.22, 0.95)
980
+ pairs['--dsw-alias-file-diff-added-bg'] = { light: area('#e6f4e7'), dark: area('#1f3124') }
981
+ pairs['--dsw-alias-file-diff-added-gutter'] = { light: area('#edf7ed'), dark: area('#132016') }
982
+ pairs['--dsw-alias-file-diff-added-marker'] = { light: mark('#01a241'), dark: mark('#41c977') }
983
+ pairs['--dsw-alias-file-diff-deleted-bg'] = { light: area('#fce6e2'), dark: area('#3c1f1b') }
984
+ pairs['--dsw-alias-file-diff-deleted-gutter'] = { light: area('#fdece9'), dark: area('#28130e') }
985
+ pairs['--dsw-alias-file-diff-deleted-marker'] = { light: mark('#ba2723'), dark: mark('#fa423e') }
986
+ }
987
+ /* ---- 语法高亮(1.8.2)----
988
+ * 宿主渲染器用 Shiki 的 `css-variables` 模式(className="shiki css-variables",
989
+ * 样式走 var(--shiki-*)),所以覆盖这些变量有效,不需要 !important。
990
+ *
991
+ * 色相策略:不引入与配色无关的固定色,而是从强调色相出发取 3 个色族
992
+ * (强调 / +150° 的冷族 / -55° 的暖族),再用明度与色度拉开可辨度;
993
+ * 每个 token 都必须对代码块底达到 4.6:1,否则沿远离背景的方向调整明度。
994
+ */
995
+ if (params.tinted && mdMode === 'on') {
996
+ const baseHue = params.accentHue === undefined ? scheme.hue : params.accentHue
997
+ const pickL = (bg) => (contrast('#ffffff', bg) >= contrast('#000000', bg) ? 0.80 : 0.45)
998
+ const dirOf = (bg) => (contrast('#ffffff', bg) >= contrast('#000000', bg) ? 1 : -1)
999
+ const ink = (bg, hue, chroma, startL, target) => {
1000
+ const dir = dirOf(bg)
1001
+ let L = startL
1002
+ for (let i = 0; i < 160; i++) {
1003
+ const c = fromOklch(clamp(L, 0.02, 0.99), chroma, hue)
1004
+ if (contrast(c, bg) >= target) return c
1005
+ L += dir * 0.004
1006
+ }
1007
+ return fromOklch(clamp(L, 0.02, 0.99), chroma, hue)
1008
+ }
1009
+ // 每个色族自带目标对比度:同族靠明度分"常/表达",异族靠色相差 55–150°
1010
+ const families = {
1011
+ keyword: [baseHue, 0.15, 4.6],
1012
+ function: [baseHue + 70, 0.12, 5.0],
1013
+ constant: [baseHue - 60, 0.13, 4.8],
1014
+ parameter: [baseHue + 30, 0.10, 5.6],
1015
+ string: [baseHue + 150, 0.10, 4.8],
1016
+ 'string-expression': [baseHue + 150, 0.085, 5.8],
1017
+ comment: [baseHue, 0.015, 4.6],
1018
+ punctuation: [baseHue, 0.02, 6.0],
1019
+ }
1020
+ for (const [name, [hue, chroma, target]] of Object.entries(families)) {
1021
+ const key = '--shiki-token-' + name
1022
+ pairs[key] = {
1023
+ light: ink(codeFill.light, hue, chroma, pickL(codeFill.light), target),
1024
+ dark: ink(codeFill.dark, hue, chroma, pickL(codeFill.dark), target),
1025
+ }
1026
+ }
1027
+ pairs['--shiki-token-link'] = { light: lightLink(ramps, false), dark: ramps.deepseek['400'] }
1028
+ pairs['--shiki-background'] = codeFill
1029
+ pairs['--shiki-foreground'] = { light: ramps.bluish['1000'], dark: ramps.bluish['50'] }
1030
+ }
938
1031
  const scroll = scrollbarTokens(ramps, params, scrollMode)
939
1032
  for (const [name, pair] of Object.entries(scroll)) pairs[name] = pair
940
1033
  // 开关/勾选/单选的共用支点:brand-primary 本来是近黑/近白,改成强调色。
@@ -983,6 +1076,8 @@ window.__ModuleLoader__.load({
983
1076
  * 命中 ui-layout 的 `data-rightbar-col`(稳定语义钩子,不是哈希类名)。
984
1077
  * 全屏右栏时退回画布色 —— 那时右栏就是整块内容区,染成侧栏色会像只剩一条侧栏。
985
1078
  */
1079
+ // 说明:本插件不额外处理 forced-colors —— 我们改的 color/background-color/border-color
1080
+ // 都在 UA 强制色列表内,系统高对比模式下会被自动接管,无需(也不应)用 forced-color-adjust 对抗。
986
1081
  const RIGHTBAR_CSS = [
987
1082
  '[data-rightbar-col]{background:var(--dshtp-rightbar-fill,var(--dsw-alias-bg-base))}',
988
1083
  '[data-rightbar-fullscreen] [data-rightbar-col]{background:var(--dsw-alias-bg-base)}',
@@ -1429,6 +1524,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1429
1524
  explainLink: '链接已为可读性压暗:{ratio}:1(目标 {target}:1)',
1430
1525
  preview: '预览',
1431
1526
  sample: '试试示例',
1527
+ copyDiag: '复制诊断',
1432
1528
  contrast: '对比度',
1433
1529
  contrastSub: '实测 / 目标(按承载面;※ 不计硬门·△ 表面受限)',
1434
1530
  cMain: '正文',
@@ -1482,6 +1578,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1482
1578
  explainLink: '連結已為可讀性壓暗:{ratio}:1(目標 {target}:1)',
1483
1579
  preview: '預覽',
1484
1580
  sample: '試試示例',
1581
+ copyDiag: '複製診斷',
1485
1582
  contrast: '對比度',
1486
1583
  contrastSub: '實測 / 目標(依承載面;※ 不計硬門·△ 表面受限)',
1487
1584
  cMain: '正文',
@@ -1535,6 +1632,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1535
1632
  explainLink: 'Link darkened for readability: {ratio}:1 (target {target}:1)',
1536
1633
  preview: 'Preview',
1537
1634
  sample: 'Try a sample',
1635
+ copyDiag: 'Copy diagnostics',
1538
1636
  contrast: 'Contrast',
1539
1637
  contrastSub: 'measured / target per surface (※ not gated · △ surface-limited)',
1540
1638
  cMain: 'Body',
@@ -1588,6 +1686,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1588
1686
  explainLink: 'リンクを可読性のため暗くしました:{ratio}:1(目標 {target}:1)',
1589
1687
  preview: 'プレビュー',
1590
1688
  sample: 'サンプルを試す',
1689
+ copyDiag: '診断をコピー',
1591
1690
  contrast: 'コントラスト',
1592
1691
  contrastSub: '実測 / 目標(面ごと;※ 判定外・△ 面の上限)',
1593
1692
  cMain: '本文',
@@ -1690,6 +1789,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1690
1789
  const [copyState, setCopyState] = React.useState(null)
1691
1790
  const [advanced, setAdvanced] = React.useState(false)
1692
1791
  const [matrixOpen, setMatrixOpen] = React.useState(false)
1792
+ const [diagState, setDiagState] = React.useState(null)
1693
1793
  // 升级提示按版本只出现一次
1694
1794
  const noticePending = React.useSyncExternalStore(props.subscribeNotice, props.getNoticePending)
1695
1795
  // HEX 输入需要「草稿」态:非法值不能写进主题,但用户还在输入时也不该被立刻回滚。
@@ -1862,6 +1962,20 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1862
1962
  onClick: () => setMatrixOpen(!matrixOpen),
1863
1963
  }, (matrixOpen ? '▾ ' : '▸ ') + txt.contrast),
1864
1964
  h('span', { className: 'dsh-tp-matrix-sub' }, txt.contrastSub),
1965
+ h('div', { className: 'dsh-tp-field-row' },
1966
+ h('button', {
1967
+ type: 'button',
1968
+ className: 'dsh-tp-mini',
1969
+ onClick: async () => {
1970
+ try {
1971
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') throw new Error('no clipboard')
1972
+ await navigator.clipboard.writeText(JSON.stringify(props.getDiagnostics(), null, 2))
1973
+ setDiagState('copied')
1974
+ } catch (err) { setDiagState('fail') }
1975
+ },
1976
+ }, txt.copyDiag),
1977
+ diagState && h('span', { className: 'dsh-tp-note', role: 'status', 'aria-live': 'polite' },
1978
+ diagState === 'copied' ? txt.copied : txt.copyFail)),
1865
1979
  matrixOpen && (() => {
1866
1980
  const report = props.getContrastReport()
1867
1981
  return h(
@@ -1869,7 +1983,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1869
1983
  { id: 'dsh-tp-matrix-body' },
1870
1984
  h('thead', null, h('tr', null,
1871
1985
  h('th', { scope: 'col' }, ''),
1872
- CONTRAST_SURFACES.map((sf) => h('th', { key: sf.id, scope: 'col' }, sf.id + (sf.scope === false ? '※' : ''))))),
1986
+ CONTRAST_SURFACES.map((sf) => h('th', { key: sf.id, scope: 'col' }, (sf.label || sf.id) + (sf.scope === false ? '※' : ''))))),
1873
1987
  h('tbody', null, report.rows.map((row) => h('tr', { key: row.role },
1874
1988
  h('th', { scope: 'row' }, roleLabel[row.role] || row.role),
1875
1989
  row.cells.map((cell) => {
@@ -2078,7 +2192,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
2078
2192
 
2079
2193
  // 开发期接口:palette-dev.mjs 通过它拿到同一套引擎(避免「报表好看、上线不一样」)
2080
2194
  if (DEV_OPT_IN && typeof window !== 'undefined' && typeof window.__DSH_THEME_PALETTE_DEV__ === 'function') {
2081
- window.__DSH_THEME_PALETTE_DEV__({ buildTokens, SCHEMES, SIDEBAR_MODES, TEXT_MODES, RIGHTBAR_MODES, SCROLL_MODES, CONTROL_MODES, MD_MODES, HC_MODES, contrast, toOklch, rampsFor, floors, encodeConfig, decodeConfig, customScheme, readCustom, writeCustom, CONTRAST_SURFACES, CONTRAST_ROLES, settingsCss: SETTINGS_CSS, cardColors })
2195
+ window.__DSH_THEME_PALETTE_DEV__({ buildTokens, SCHEMES, SIDEBAR_MODES, TEXT_MODES, RIGHTBAR_MODES, SCROLL_MODES, CONTROL_MODES, MD_MODES, HC_MODES, contrast, toOklch, fromOklch, flatten, rampsFor, floors, encodeConfig, decodeConfig, customScheme, readCustom, writeCustom, CONTRAST_SURFACES, CONTRAST_ROLES, settingsCss: SETTINGS_CSS, cardColors })
2082
2196
  }
2083
2197
 
2084
2198
  return {
@@ -2384,6 +2498,13 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
2384
2498
  linkTarget: floors(hc).link,
2385
2499
  }
2386
2500
  }
2501
+ /** 诊断导出:把当前配色与对比度矩阵打成可粘贴的 JSON(提 issue 用)。 */
2502
+ const getDiagnostics = () => ({
2503
+ plugin: 'dsh-theme-palette',
2504
+ version: PLUGIN_VERSION,
2505
+ settings: { palette: current, sidebar: sidebarMode, text: textMode, rightbar: rightbarMode, scroll: scrollMode, control: controlMode, md: mdMode, hc: hcMode, lang: langPref },
2506
+ report: getContrastReport(),
2507
+ })
2387
2508
  const previewConfigCode = (code) => {
2388
2509
  const cfg = decodeConfig(code)
2389
2510
  return cfg ? { ok: true, config: cfg } : { ok: false }
@@ -2407,7 +2528,9 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
2407
2528
  const fgHex = role.id === 'link'
2408
2529
  ? t['--dsw-alias-link'][sf.mode]
2409
2530
  : t['--dsw-static-neutral-bluish-' + role.step[sf.mode]][sf.mode]
2410
- const bg = t['--dsw-static-neutral-bluish-' + sf.id][sf.mode]
2531
+ const bg = sf.token
2532
+ ? flatten(t[sf.token][sf.mode], t['--dsw-static-neutral-bluish-' + (sf.mode === 'light' ? '00' : '950')][sf.mode])
2533
+ : t['--dsw-static-neutral-bluish-' + sf.id][sf.mode]
2411
2534
  const ratio = contrast(fgHex, bg)
2412
2535
  const claim = sf.mode === 'light' ? F[role.id] : (role.id === 'link' ? F.linkDark : F[role.id])
2413
2536
  const ceiling = contrast(sf.mode === 'light' ? '#000000' : '#ffffff', bg)
@@ -2582,6 +2705,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
2582
2705
  previewConfigCode,
2583
2706
  getSampleCode,
2584
2707
  getContrastReport,
2708
+ getDiagnostics,
2585
2709
  subscribeNotice,
2586
2710
  getNoticePending,
2587
2711
  dismissNotice,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-theme-palette",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "description": "DSH 主题配色:4 套配色 × 明暗两模式,OKLCH 色域安全重建 + 真实承载面对比度求解,可调强度与面板表面。",
5
5
  "type": "module",
6
6
  "main": "index.js",