dsh-theme-palette 1.9.10 → 1.11.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
@@ -4,6 +4,34 @@
4
4
 
5
5
  ## 版本一览
6
6
 
7
+ ### 1.11.0
8
+ 2026-10-27 全量审计(评分 89/100,报告见 [`AUDIT-2026-10-27.md`](AUDIT-2026-10-27.md))的 12 条问题全部修订。两条 P1 都是门面级小修;审计里有一处**自我修正**也如实落地:取色拖动的性能问题经实测降级为防御性优化。
9
+
10
+ - **npm 包内 README 截图修复(P1-1)**:`files` 此前不含 `assets/`,npmjs.com 上 README 的 4 张截图全部 404。现已入包(包体 140KB → 436KB)。选「打进包」而非「绝对 GitHub URL」:自包含、不依赖仓库地址、npm 与镜像双兼容。
11
+ - **界面语言回退改 en(P1-2)**:宿主语言不在 zh/ja/en 里(fr/de/es/pt/…)时,设置页从整页中文改为回落英文。`pickLocale` 进 dev 钩子导出,`check-audit` 第 21 组断言 fr → en。
12
+ - **深色注释(label-caption)过 Lc 闸(P2-3)**:原先深色侧只按浅色下限求解,实测 Lc 47.9–51.7,低于本插件给文字定的 Lc 60 线(三级同面 62–66、链接有 Lc 闸,唯独注释没有)。现标准档 Lc 60 / HC 档 70——HC 定 70 而非照抄链接的 75:HC 三级 Lc 落在 72–77,套 75 会反超三级破坏层级(实测暖砂 HC 原值 71.7 已达标故不变,靛蓝 HC 66.6 → 72.0)。抬完标准档注释 WCAG 5.73–6.19,仍低于三级的 6.4(层级断言钉住)。`check-audit` 第 4 组扩为 WCAG + Lc + 层级三重断言;DESIGN.md 口径表补「注释层」与「装饰层」两行(后者即 P3-12:label-dimmed/document-preview 按 3.0 装饰下限、有意不进承诺模型)。
13
+ - **取色拖动按帧合并(P2-4,防御性优化)**:取色器 `input` 在拖动中每帧多次触发,此前每次都全量重放覆盖层(宿主侧重写约 200 个 token 触发全页样式重算)。现在同帧多次取色合并到下一帧只重放一次、以最后一次为准;重置/派生/导入等离散动作保持同步。审计修正:插件侧构建实测仅约 0.05ms/次(60 次共 3ms),瓶颈在宿主侧重排——所以这是防御性优化而非实测缺陷修复。`check-selfheal` 新增第 6 场景(假宿主计数:帧内 +0、合并后 +1、末次生效),其沙箱的 localStorage 桩顺带从「恒返 null」改为 Map 实现(原桩让自定义色永远读回默认值,与真实宿主不符)。
14
+ - **CI(P2-5)**:新增 `.github/workflows/ci.yml`,push/PR 在 Node 22/24 跑 `npm run check` + `npm run report`;此前全部质量门只在本地/发版时跑。脚本只依赖 node 内建,无需 install;宿主契约在无 asar 的 CI 上自动 SKIP。
15
+ - **dev 钩子补导出(P2-6)**:`apcaContrast` / `raiseToLc` / `solveForeground` / `pickLocale` / `hostLocale` / `floorsDark` —— 外部工具复算 Lc 不必再自带一份 APCA 实现。
16
+ - **组合数口径统一为 1080(P3-10)**:五语 README 与 DESIGN.md 原写「2160 组(5 × 216 × 两档)」,公式把高对比算了两次(216 已含 hc 档),实际扫描 1080 组(1.9.10 撤右栏档前才是 2160)。
17
+ - **notice 文案版本同步断言(P3-11)**:`check-ui` 新增——四个语种的 `notice` 恰好 4 条且都含当前 `PLUGIN_VERSION`,升级漏改不会绿。
18
+ - **代码卫生(P3-7/8/9)**:短码编解码从废弃的 `escape`/`unescape` 换 `TextEncoder`/`TextDecoder`(产出逐字节相同,新旧短码互解码兼容,第 2 组旧码断言守着);`TEXT_TARGETS` 常量表从 `buildTokensRaw` 体内上提到模块级(紧随 `SURFACE_TARGETS`);`langPref` 声明先于引用它的 `getConfigCode` 闭包。
19
+ - **发版联动**:版本号 bump 至 1.11.0、四语 notice 换新、截图用 `npm run shots` 重拍(无头 Chrome/Edge + CDP 截 preview HTML,不需要 DSH 宿主——审计时「重拍需要宿主 GUI」的判断有误,发版时更正),`screenshots.meta.json` 的 `pluginVersion` 随之对齐。
20
+ - 复核:`npm run check` 六件套全绿(含新增断言);两档报表 exit 0,1080 组配置「未达标 0 / 低于 AA 4.5 = 0 / 硬失败 0」;`npm pack --dry-run` 确认 assets 入包(20 文件 / 435.6KB)。
21
+
22
+ ### 1.10.0
23
+ 按审计报告的改进清单做了六件事:色彩数学单测、高对比跟随系统、运行时覆盖报告、色觉缺陷守门、单色派生,另外把 display-P3 的可行性量清楚后决定**本期不做**。其中「色觉缺陷守门」第一次跑就抓到一处真问题(浅色链接在模拟下低于 AA),下限因此微调。
24
+
25
+ - **色彩数学单测**(新增 `tools/check-colormath.mjs`,约 40 条断言,并入 `npm run check` 与发版回归清单):锚点(OKLab 里的白 / 黑 / 中灰)、OKLCH↔hex 往返、色域裁切「保明度保色相」、WCAG 样例值(`#777` on `#fff` = 4.48、黑白 21)、`flatten()` 的 α 合成交换性、`harmonize()` 的三条不变量(保 L、色度按比例、色相朝目标按 `pull` 走)。以前这些函数只靠「界面上看着对」,现在挪一个常数就会红。
26
+ 写单测时踩了一次:`harmonize` 的色相断言最初写成「移动量 = pull」,实际是「按剩下的角度比例移动」(`dh1 ≈ pull × dh0`)—— 是断言错了,不是代码错。
27
+ - **高对比档新增「跟随系统」并设为默认**:系统开「提高对比度」(`prefers-contrast: more`)就自动用高对比档,关掉回标准档;监听 `change` 事件即时重算。手动选过「关 / 开」的用户读回自己的选择,升级不会被改成别的档位。
28
+ - **色觉缺陷守门**(`tools/check-audit.mjs` 第 20 组):四套配色 × 明暗 × 正文/次级/三级/链接 × 三个承载面 × deutan / protan / tritan(Machado 2009),断言最低对比度 ≥ 4.5。**这条断言一开始没过**:三级与链接的浅色下限原本压在 4.62(刚过 AA),模拟把比值推动 5–9% 后链接跌到 **4.22–4.34**。修法是把浅色下限抬到三级 **4.85**、链接 **4.95**,抬完三种模拟下最低 **4.61**。代价:浅色链接与三级文字比 1.9.x 略深一档。
29
+ - **自定义配色新增「由主色派生」**:只给一个主色,按它的色相与色度推出纸面与墨色(明度按当前明暗模式取引擎已验证的区间),派生结果直接过全部下限(实测正文 14.6–15.0 / 次级 6.8 / 三级 5.3 / 链接 5.4)。主色无彩时派生中性纸面。**它是一次性的填写动作,不是一种模式**:写进去的就是普通的三色,之后照常手改、导出分享码也只带这三个值;但正因为它会填这两格,面板里加了一行可见说明 ——「会覆盖你手动设置的底色与文字,派生之后仍可逐项手改」(四语种,`tools/check-custom.mjs` 断言这句必须是可见文本,不能只挂在 tooltip 上)。
30
+ - **自定义配色新增色觉提示**:把三个色按 deutan / protan / tritan 模拟后,若强调色与文字或底色的 ΔE 塌到 **12** 以下(典型是红 / 绿搭配),面板里给一行说明 —— 只提示,不阻止保存,也不改用户的颜色。
31
+ - **诊断导出新增运行时覆盖报告** `coverage`:枚举宿主样式表里所有 `--dsw-*`,分成「本插件接管 / var 引用 / 字面彩色 / 字面灰」四类,回答「宿主还有多少 token 没跟上配色」。只读、拿不到样式表就返回 `ok: false`。
32
+ - **display-P3:量完决定不做**。同一 L/h 下 P3 比 sRGB 能多给的色度:纸面层(00/60)只多 ΔC 0.0008–0.0053(ΔE 0.08–0.53,JND 以下,看不出来);能看出差别的是强调色(indigo ΔE 5.29、moss 2.96)。但强调色同时是链接 / info 填充 / 勾选支点的来源,按 P3 抬色度就要为 sRGB 回退再标定一套。数据与取舍写在 `docs/DESIGN.md`,将来若做,只值得给强调色加一层 `color(display-p3 …)`。
33
+ - 复核:`npm run check` 全绿(含新增单测与第 20 组);`palette-dev.mjs` 两档 exit 0,1080 组配置「未达标 0 / 低于 AA 4.5 = 0 / 硬失败 0」。
34
+
7
35
  ### 1.9.10
8
36
  撤除「右侧栏底色」这一档,并把它的技术债一起清干净(用户拍板)。**这不是修 bug,是承认做不到**:宿主右栏没有可染的落点,留着这个开关只会一直看起来像坏的。
9
37
 
package/README-es.md CHANGED
@@ -31,7 +31,7 @@ El plugin solo apila tokens de color mediante `ctx.theme.overrideTokens`; no reg
31
31
  - más de 27.000 referencias de token: texto, secundario, terciario, notas, enlaces, barras laterales, tarjetas, burbuja del usuario, botón primario, barras de desplazamiento, interruptores, títulos/negritas de Markdown, bloques de código;
32
32
  - ajustable: intensidad del texto (3), profundidad de la barra lateral (3), énfasis de Markdown, barras de desplazamiento, color de interruptores;
33
33
  - **autorreparable**: si el host descarta la capa de override, la paleta se vuelve a aplicar;
34
- - **promesas de contraste**: el cuerpo tiene la promesa de 12:1 en claro y 10:1 en oscuro (15:1 en alto contraste), el secundario ≥ 6.2 en claro y 8.0 en oscuro, y el terciario y los enlaces ≥ 4.62 en claro y 6.4 en oscuro (los enlaces además pasan una puerta APCA Lc ≥ 60 en oscuro / 75 en HC), medidos sobre las superficies donde el texto realmente cae (el lado oscuro necesita más: un cociente WCAG sobreestima la legibilidad sobre fondo oscuro);
34
+ - **promesas de contraste**: el cuerpo tiene la promesa de 12:1 en claro y 10:1 en oscuro (15:1 en alto contraste), el secundario ≥ 6.2 en claro y 8.0 en oscuro, y el terciario ≥ 4.85 y los enlaces ≥ 4.95 en claro, y 6.4 en oscuro (los mínimos claros llevan un margen para déficits de visión cromática; los enlaces además pasan una puerta APCA Lc ≥ 60 en oscuro / 75 en HC), medidos sobre las superficies donde el texto realmente cae (el lado oscuro necesita más: un cociente WCAG sobreestima la legibilidad sobre fondo oscuro);
35
35
  - las superficies prometidas son 00/60 en claro y 950/875 en oscuro; las oscuras 800 (superficie de módulo) y 850 (sub-superficie) **no** respaldan el texto del cuerpo: el informe las cuenta aparte como «fuera de política» (ni cuentan como aprobado ni como fallo);
36
36
  - interfaz en 简体中文 / 繁體中文 / English / 日本語 (sigue el idioma del host y se puede fijar).
37
37
 
@@ -69,10 +69,10 @@ OKLCH(L, C, tono) ──► ajuste al gamut (búsqueda binaria en C) ──► h
69
69
 
70
70
  - los alias semánticos mantienen cadenas `var(--dsw-static-neutral-bluish-NN)`, así que todo lo que sigue a un token sigue a la paleta;
71
71
  - superficies claras usan los pasos 00–300, las oscuras 800–950;
72
- - los mínimos de texto se exigen en tiempo de construcción: cuerpo ≥ 12:1 en claro y ≥ 10:1 en oscuro (alto contraste: 15:1), secundario ≥ 6.2 claro / 8.0 oscuro (HC 9 / 9.6), terciario/enlace ≥ 4.62 claro / 6.4 oscuro (HC 7 / 7.6), enlace con APCA Lc ≥ 60 oscuro (HC 75), texto en barra lateral ≥ 7, texto en burbuja ≥ 7;
72
+ - los mínimos de texto se exigen en tiempo de construcción: cuerpo ≥ 12:1 en claro y ≥ 10:1 en oscuro (alto contraste: 15:1), secundario ≥ 6.2 claro / 8.0 oscuro (HC 9 / 9.6), terciario/enlace ≥ 4.85 / 4.95 claro / 6.4 oscuro (HC 7 / 7.6), enlace con APCA Lc ≥ 60 oscuro (HC 75), texto en barra lateral ≥ 7, texto en burbuja ≥ 7;
73
73
  - las superficies prometidas son 00/60 en claro y 950/875 en oscuro; las oscuras 800 (superficie de módulo) y 850 (sub-superficie) **no** respaldan el texto del cuerpo: el informe las cuenta aparte como «fuera de política» (ni cuentan como aprobado ni como fallo), y toda superficie sigue teniendo que superar el suelo AA de 4.5;
74
74
  - el informe separa tres cosas: el objetivo declarado, el techo físico (lo que el blanco o el negro alcanzan en esa superficie) y la exención de política; 0.93 es solo margen del solucionador, para decidir si un objetivo queda por encima del techo;
75
- - `node palette-dev.mjs` comprueba 2160 combinaciones (5 paletas × 216 ajustes × los dos niveles de contraste).
75
+ - `node palette-dev.mjs` comprueba 1080 combinaciones (5 paletas × 216 ajustes, ambos niveles de contraste incluidos).
76
76
 
77
77
  Las notas de diseño, el criterio por componente y las tablas de contraste están en [docs/DESIGN.md](docs/DESIGN.md). Historial: [CHANGELOG.md](CHANGELOG.md).
78
78
 
package/README-hi.md CHANGED
@@ -31,7 +31,7 @@ DeepSeek Harness (वेब और डेस्कटॉप) के लिए
31
31
  - 27,000+ टोकन संदर्भ शामिल: मुख्य टेक्स्ट, द्वितीयक, तृतीयक, कैप्शन, लिंक, साइडबार, कार्ड, उपयोगकर्ता बबल, प्राइमरी बटन, स्क्रॉलबार, स्विच, Markdown शीर्षक/बोल्ड, कोड ब्लॉक;
32
32
  - समायोज्य: टेक्स्ट की तीव्रता (3), साइडबार की गहराई (3), Markdown ज़ोर, स्क्रॉलबार, स्विच का रंग;
33
33
  - **स्व-उपचार**: यदि होस्ट ओवरराइड परत हटा दे, पैलेट फिर से लगा दी जाती है;
34
- - **कंट्रास्ट के वादे**: मुख्य टेक्स्ट के लिए लाइट में 12:1 और डार्क में 10:1 (उच्च-कंट्रास्ट में 15:1), द्वितीयक ≥ 6.2 (लाइट) / 8.0 (डार्क), तृतीयक और लिंक ≥ 4.62 (लाइट) / 6.4 (डार्क), लिंक अतिरिक्त APCA Lc ≥ 60 (डार्क) / 75 (HC) — उन्हीं सतहों पर मापे गए जहाँ टेक्स्ट वास्तव में बैठता है (डार्क में ज़्यादा चाहिए: गहरे बैकग्राउंड पर WCAG अनुपात पढ़ने की सुविधा को बढ़ा-चढ़ा कर दिखाता है);
34
+ - **कंट्रास्ट के वादे**: मुख्य टेक्स्ट के लिए लाइट में 12:1 और डार्क में 10:1 (उच्च-कंट्रास्ट में 15:1), द्वितीयक ≥ 6.2 (लाइट) / 8.0 (डार्क), तृतीयक ≥ 4.85 और लिंक ≥ 4.95 (लाइट) / 6.4 (डार्क) — लाइट की सीमाओं में रंग-दृष्टि दोष के लिए गुंजाइश रखी गई है; लिंक अतिरिक्त APCA Lc ≥ 60 (डार्क) / 75 (HC) — उन्हीं सतहों पर मापे गए जहाँ टेक्स्ट वास्तव में बैठता है (डार्क में ज़्यादा चाहिए: गहरे बैकग्राउंड पर WCAG अनुपात पढ़ने की सुविधा को बढ़ा-चढ़ा कर दिखाता है);
35
35
  - वादे की गई सतहें लाइट में 00/60 और डार्क में 950/875 हैं; डार्क 800 (मॉड्यूल सतह) और 850 (उप-सतह) मुख्य टेक्स्ट को नहीं सहनाते — रिपोर्ट उन्हें अलग «नीति से बाहर» के रूप में गिनती है (न तो पास, न ही फेल);
36
36
  - इंटरफ़ेस 简体中文 / 繁體中文 / English / 日本語 में (होस्ट की भाषा का पालन करता है, हाथ से भी चुना जा सकता है)।
37
37
 
@@ -69,10 +69,10 @@ OKLCH(L, C, ह्यू) ──► गैमट क्लैंप (C पर
69
69
 
70
70
  - सेमांटिक एलियस `var(--dsw-static-neutral-bluish-NN)` श्रृंखलाएँ बनाए रखते हैं, इसलिए जो भी टोकन का पालन करता है वह पैलेट का भी करता है;
71
71
  - लाइट सतहें स्टेप 00–300, डार्क सतहें 800–950;
72
- - टेक्स्ट की न्यूनतम सीमा बनाते समय लागू: मुख्य लाइट में ≥ 12:1 और डार्क में ≥ 10:1 (उच्च-कंट्रास्ट: 15:1), द्वितीयक ≥ 6.2 लाइट / 8.0 डार्क (HC 9 / 9.6), तृतीयक/लिंक ≥ 4.62 लाइट / 6.4 डार्क (HC 7 / 7.6), लिंक APCA Lc ≥ 60 डार्क (HC 75), साइडबार में ≥ 7, बबल में ≥ 7;
72
+ - टेक्स्ट की न्यूनतम सीमा बनाते समय लागू: मुख्य लाइट में ≥ 12:1 और डार्क में ≥ 10:1 (उच्च-कंट्रास्ट: 15:1), द्वितीयक ≥ 6.2 लाइट / 8.0 डार्क (HC 9 / 9.6), तृतीयक/लिंक ≥ 4.85 / 4.95 लाइट / 6.4 डार्क (HC 7 / 7.6), लिंक APCA Lc ≥ 60 डार्क (HC 75), साइडबार में ≥ 7, बबल में ≥ 7;
73
73
  - वादे की गई सतहें लाइट में 00/60 और डार्क में 950/875 हैं; डार्क 800 (मॉड्यूल सतह) और 850 (उप-सतह) मुख्य टेक्स्ट को नहीं सहनाते — रिपोर्ट उन्हें अलग «नीति से बाहर» के रूप में गिनती है (न तो पास, न ही फेल), और हर सतह को AA की न्यूनतम सीमा 4.5 से गुज़रना ही होता है;
74
74
  - रिपोर्ट तीन बातें अलग रखती है: घोषित लक्ष्य, भौतिक सीमा (उस सतह पर सफ़ेद या काला कितना हासिल कर सकता है) और नीति छूट; 0.93 केवल सॉल्वर का मार्जिन है, इससे तय होता है कि लक्ष्य सीमा से ऊपर है या नहीं;
75
- - `node palette-dev.mjs` 2160 संयोजन जाँचता है (5 पैलेट × 216 सेटिंग × दोनों कंट्रास्ट स्तर)।
75
+ - `node palette-dev.mjs` 1080 संयोजन जाँचता है (5 पैलेट × 216 सेटिंग, दोनों कंट्रास्ट स्तर सहित)।
76
76
 
77
77
  डिज़ाइन नोट्स, घटक-वार तर्क और कंट्रास्ट तालिकाएँ [docs/DESIGN.md](docs/DESIGN.md) में। इतिहास: [CHANGELOG.md](CHANGELOG.md)।
78
78
 
package/README-pt.md CHANGED
@@ -31,7 +31,7 @@ O plugin apenas empilha tokens de cor via `ctx.theme.overrideTokens`; não regis
31
31
  - mais de 27.000 referências de token: texto, secundário, terciário, legendas, links, barras laterais, cartões, balão do usuário, botão primário, barras de rolagem, interruptores, títulos/negrito de Markdown, blocos de código;
32
32
  - ajustável: intensidade do texto (3), profundidade da barra lateral (3), ênfase de Markdown, barras de rolagem, cor dos interruptores;
33
33
  - **autorrecuperação**: se o host descartar a camada de override, a paleta é reaplicada;
34
- - **promessas de contraste**: o corpo tem a promessa de 12:1 no claro e 10:1 no escuro (15:1 em alto contraste), o secundário ≥ 6.2 no claro e 8.0 no escuro, e o terciário e os links ≥ 4.62 no claro e 6.4 no escuro (os links ainda passam por um portão APCA Lc ≥ 60 no escuro / 75 em HC), medidos nas superfícies onde o texto realmente cai (o lado escuro precisa de mais: uma razão WCAG superestima a legibilidade sobre fundo escuro);
34
+ - **promessas de contraste**: o corpo tem a promessa de 12:1 no claro e 10:1 no escuro (15:1 em alto contraste), o secundário ≥ 6.2 no claro e 8.0 no escuro, e o terciário ≥ 4.85 e os links ≥ 4.95 no claro, e 6.4 no escuro (os pisos do lado claro têm margem para deficiências de visão cromática; os links ainda passam por um portão APCA Lc ≥ 60 no escuro / 75 em HC), medidos nas superfícies onde o texto realmente cai (o lado escuro precisa de mais: uma razão WCAG superestima a legibilidade sobre fundo escuro);
35
35
  - as superfícies prometidas são 00/60 no claro e 950/875 no escuro; as escuras 800 (superfície de módulo) e 850 (sub-superfície) **não** sustentam o texto do corpo: o relatório as conta à parte como «fora de política» (nem contam como aprovado nem como falha);
36
36
  - interface em 简体中文 / 繁體中文 / English / 日本語 (segue o idioma do host e pode ser fixada).
37
37
 
@@ -69,10 +69,10 @@ OKLCH(L, C, matiz) ──► limite de gamut (busca binária em C) ──► hex
69
69
 
70
70
  - os aliases semânticos mantêm cadeias `var(--dsw-static-neutral-bluish-NN)`, então tudo que segue um token segue a paleta;
71
71
  - superfícies claras usam os passos 00–300, as escuras 800–950;
72
- - os pisos de texto são exigidos na construção: corpo ≥ 12:1 no claro e ≥ 10:1 no escuro (alto contraste: 15:1), secundário ≥ 6.2 claro / 8.0 escuro (HC 9 / 9.6), terciário/link ≥ 4.62 claro / 6.4 escuro (HC 7 / 7.6), link com APCA Lc ≥ 60 escuro (HC 75), texto na barra lateral ≥ 7, texto no balão ≥ 7;
72
+ - os pisos de texto são exigidos na construção: corpo ≥ 12:1 no claro e ≥ 10:1 no escuro (alto contraste: 15:1), secundário ≥ 6.2 claro / 8.0 escuro (HC 9 / 9.6), terciário/link ≥ 4.85 / 4.95 claro / 6.4 escuro (HC 7 / 7.6), link com APCA Lc ≥ 60 escuro (HC 75), texto na barra lateral ≥ 7, texto no balão ≥ 7;
73
73
  - as superfícies prometidas são 00/60 no claro e 950/875 no escuro; as escuras 800 (superfície de módulo) e 850 (sub-superfície) **não** sustentam o texto do corpo: o relatório as conta à parte como «fora de política» (nem contam como aprovado nem como falha), e toda superfície ainda precisa passar pelo piso AA de 4.5;
74
74
  - o relatório separa três coisas: a meta declarada, o teto físico (o que o branco ou o preto alcançam nessa superfície) e a isenção de política; 0.93 é apenas margem do solucionador, usada para decidir se uma meta fica acima do teto;
75
- - `node palette-dev.mjs` verifica 2160 combinações (5 paletas × 216 ajustes × os dois níveis de contraste).
75
+ - `node palette-dev.mjs` verifica 1080 combinações (5 paletas × 216 ajustes, ambos os níveis de contraste incluídos).
76
76
 
77
77
  Notas de design, critério por componente e tabelas de contraste em [docs/DESIGN.md](docs/DESIGN.md). Histórico: [CHANGELOG.md](CHANGELOG.md).
78
78
 
package/README-zh.md CHANGED
@@ -32,7 +32,7 @@ package.json 的 `engines` 字段写的是 `^22.19.0 || >=24.0.0`——与上表
32
32
  - 可调:文字着色三档、侧栏三档、Markdown 强调、滚动条、开关配色;
33
33
  - **自定义配色**(给 底色/文字/强调 三色,文字色相也参与派生;过亮/过暗的强调色会归一化)、**配置导入导出**(`dshs1.` 短码含三色)、**高对比**档、4 种界面语言;
34
34
  - **自愈**:宿主丢弃 override 层时自动重放;
35
- - **对比度承诺**:正文浅色 12:1、深色 10:1(高对比 15:1),次级 ≥ 6.2(深色 8.0),三级/链接 ≥ 4.62(深色 6.4),链接另过 APCA Lc ≥ 60(高对比 75)—— 都在文字真实落到的承载面上实测(深色要高一些:WCAG 比值在暗底上会高估可读性);
35
+ - **对比度承诺**:正文浅色 12:1、深色 10:1(高对比 15:1),次级 ≥ 6.2(深色 8.0),三级 ≥ 4.85、链接 ≥ 4.95(深色 6.4;浅色留了色觉缺陷余量),链接另过 APCA Lc ≥ 60(高对比 75)—— 都在文字真实落到的承载面上实测(深色要高一些:WCAG 比值在暗底上会高估可读性);
36
36
  - 承诺面是浅色 00/60 与深色 950/875;深色 800(模块面)与 850(次级深面)**不为正文兜底**,报表单列为「政策外」(既不算达标也不算失败);
37
37
  - 界面支持 简体中文 / 繁體中文 / English / 日本語(跟随宿主,也可手选)。
38
38
 
@@ -70,10 +70,10 @@ OKLCH(L, C, hue) ──► 色域钳位(对 C 二分) ──► sRGB hex
70
70
 
71
71
  - 语义别名保留 `var(--dsw-static-neutral-bluish-NN)` 引用链,跟 token 的元素就跟着配色走;
72
72
  - 浅色面用色阶 00–300,深色面用 800–950;
73
- - 文字下限在构建期强制:正文浅色 ≥ 12:1、深色 ≥ 10:1(高对比 15:1),次级 ≥ 6.2 浅 / 8.0 深(HC 9 / 9.6),三级/链接 ≥ 4.62 浅 / 6.4 深(HC 7 / 7.6),链接另过 APCA Lc ≥ 60 深 / 75 HC,侧栏内 ≥ 7,气泡内 ≥ 7;
73
+ - 文字下限在构建期强制:正文浅色 ≥ 12:1、深色 ≥ 10:1(高对比 15:1),次级 ≥ 6.2 浅 / 8.0 深(HC 9 / 9.6),三级/链接 ≥ 4.85 / 4.95 浅 / 6.4 深(HC 7 / 7.6),链接另过 APCA Lc ≥ 60 深 / 75 HC,侧栏内 ≥ 7,气泡内 ≥ 7;
74
74
  - 承诺面是浅色 00/60 与深色 950/875;深色 800(模块面)与 850(次级深面)**不为正文兜底**,报表单列为「政策外」(既不算达标也不算失败),且每一档承载面仍须过 AA 底线 4.5;
75
75
  - 报表把三件事分开:声明目标、物理上限(该承载面上黑或白前景能达到的对比度)、政策豁免;0.93 只是求解余量,仅用于判断目标是否高于上限;
76
- - `node palette-dev.mjs` 扫全部 2160 组配置(5 套配色 × 216 组设置 × 标准/高对比两档)并打印承载面报表,`--hc` 把主报表切到高对比档;硬失败时退出码 1。
76
+ - `node palette-dev.mjs` 扫全部 1080 组配置(5 套配色 × 216 组设置,已含标准/高对比两档)并打印承载面报表,`--hc` 把主报表切到高对比档;硬失败时退出码 1。
77
77
 
78
78
  逐组件设计取舍与对比度表见 [docs/DESIGN.md](docs/DESIGN.md),版本记录见 [CHANGELOG.md](CHANGELOG.md)。
79
79
 
package/README.md CHANGED
@@ -32,7 +32,7 @@ The plugin only stacks colour tokens through `ctx.theme.overrideTokens`; it does
32
32
  - adjustable: text tint (3), sidebar depth (3), Markdown emphasis, scrollbars, switch colour;
33
33
  - **custom palette** (pick surface / text / accent — the engine keeps its contrast guarantees), **config export/import** via a `dshs1.` code, **high-contrast** mode, and 4 UI languages;
34
34
  - **self-healing**: if the host drops the override layer, the palette is re-applied;
35
- - **contrast promises**: body text is promised 12:1 in light and 10:1 in dark (15:1 in high contrast), secondary ≥ 6.2 in light / 8.0 in dark, tertiary and links ≥ 4.62 in light / 6.4 in dark (links also clear an APCA Lc ≥ 60 gate in dark / 75 in HC) — measured on the surfaces the text actually lands on (the dark side needs more, because a WCAG ratio overstates readability on a dark background);
35
+ - **contrast promises**: body text is promised 12:1 in light and 10:1 in dark (15:1 in high contrast), secondary ≥ 6.2 in light / 8.0 in dark, tertiary ≥ 4.85 and links ≥ 4.95 in light / 6.4 in dark (the light floors carry a colour-vision headroom; links also clear an APCA Lc ≥ 60 gate in dark / 75 in HC) — measured on the surfaces the text actually lands on (the dark side needs more, because a WCAG ratio overstates readability on a dark background);
36
36
  - the promised surfaces are light 00/60 and dark 950/875; dark 800 (module surface) and 850 (sub-surface) do **not** back body text — the report counts them apart as "out of policy" (neither pass nor fail);
37
37
  - UI in 简体中文 / 繁體中文 / English / 日本語 (follows the host language, can be pinned).
38
38
 
@@ -70,10 +70,10 @@ OKLCH(L, C, hue) ──► gamut clamp (binary search on C) ──► sRGB hex
70
70
 
71
71
  - semantic aliases keep `var(--dsw-static-neutral-bluish-NN)` chains, so anything following a token follows the palette;
72
72
  - light surfaces use steps 00–300, dark surfaces 800–950;
73
- - text floors are enforced at build time: body ≥ 12:1 in light and ≥ 10:1 in dark (high contrast: 15:1), secondary ≥ 6.2 in light / 8.0 in dark (HC 9 / 9.6), tertiary/link ≥ 4.62 in light / 6.4 in dark (HC 7 / 7.6), link APCA Lc ≥ 60 in dark (HC 75), sidebar text ≥ 7, bubble text ≥ 7;
73
+ - text floors are enforced at build time: body ≥ 12:1 in light and ≥ 10:1 in dark (high contrast: 15:1), secondary ≥ 6.2 in light / 8.0 in dark (HC 9 / 9.6), tertiary/link ≥ 4.85 / 4.95 in light / 6.4 in dark (HC 7 / 7.6), link APCA Lc ≥ 60 in dark (HC 75), sidebar text ≥ 7, bubble text ≥ 7;
74
74
  - the promised surfaces are light 00/60 and dark 950/875; dark 800 (module surface) and 850 (sub-surface) do **not** back body text — the report counts them apart as "out of policy" (neither pass nor fail), and every surface still has to clear the AA floor of 4.5;
75
75
  - the report separates three things: the declared target, the physical ceiling (what black or white can reach on that surface) and the policy exemption; 0.93 is only a solver margin, used to decide whether a target sits above the ceiling;
76
- - 2160 configuration combos (5 palettes × 216 settings × both contrast tiers) are checked by `node palette-dev.mjs`.
76
+ - 1080 configuration combos (5 palettes × 216 settings, both contrast tiers included) are checked by `node palette-dev.mjs`.
77
77
 
78
78
  Design notes, per-component rationale and the contrast tables live in [docs/DESIGN.md](docs/DESIGN.md). Release history: [CHANGELOG.md](CHANGELOG.md).
79
79
 
@@ -0,0 +1,81 @@
1
+ {
2
+ "generatedAt": "2026-10-27T03:52:58.491Z",
3
+ "pluginVersion": "1.11.0",
4
+ "chrome": "Chrome/156.0.8078.48",
5
+ "width": 620,
6
+ "scale": 1,
7
+ "images": [
8
+ {
9
+ "file": "assets/screenshot-settings-light.png",
10
+ "html": "tools/preview-light.html",
11
+ "preview": [
12
+ "--palette",
13
+ "warm",
14
+ "--adv",
15
+ "0"
16
+ ],
17
+ "htmlSha256": "0cebb53312c0c39018705b81ece89e2d62ab14e7073214535d2330b4a019bb60",
18
+ "cssWidth": 620,
19
+ "cssHeight": 888,
20
+ "width": 620,
21
+ "height": 888,
22
+ "bytes": 47503,
23
+ "sha256": "6378abcf6c4fc42f0a9f2d3f32ac2a9a1679534debe126a2d9e0dd8c9999495f"
24
+ },
25
+ {
26
+ "file": "assets/screenshot-settings-dark.png",
27
+ "html": "tools/preview-dark.html",
28
+ "preview": [
29
+ "--palette",
30
+ "warm",
31
+ "--adv",
32
+ "0"
33
+ ],
34
+ "htmlSha256": "c1dcd522dc7af24d3b916b4e92001f47287839d9e507689d07df959da9ca8103",
35
+ "cssWidth": 620,
36
+ "cssHeight": 888,
37
+ "width": 620,
38
+ "height": 888,
39
+ "bytes": 48041,
40
+ "sha256": "688476198bab7c192eb9ead70a96afccb761b721f5cc5df3309879caddb21b07"
41
+ },
42
+ {
43
+ "file": "assets/screenshot-settings-custom.png",
44
+ "html": "tools/preview-light-customadv.html",
45
+ "preview": [
46
+ "--palette",
47
+ "custom",
48
+ "--adv",
49
+ "1",
50
+ "--out",
51
+ "customadv"
52
+ ],
53
+ "htmlSha256": "e29b936a12a75f1b2e8047c46872cb3f192a9e3a32bddccb0226d74bc8ed18a2",
54
+ "cssWidth": 620,
55
+ "cssHeight": 1753,
56
+ "width": 620,
57
+ "height": 1753,
58
+ "bytes": 116461,
59
+ "sha256": "89a0b0d346e2b94bc6bbd6a43b1fba5a198e44628159a3770fafd00de3d0c054"
60
+ },
61
+ {
62
+ "file": "assets/screenshot-settings-hc.png",
63
+ "html": "tools/preview-light-hcadv.html",
64
+ "preview": [
65
+ "--hc",
66
+ "1",
67
+ "--adv",
68
+ "1",
69
+ "--out",
70
+ "hcadv"
71
+ ],
72
+ "htmlSha256": "e737c6c6bcb4ca5edad80cdd04c49eee9c30bcbca396b779320731a6860639ec",
73
+ "cssWidth": 620,
74
+ "cssHeight": 1463,
75
+ "width": 620,
76
+ "height": 1463,
77
+ "bytes": 94432,
78
+ "sha256": "c4312f097c283a44ece12257710455a676ff2c0a054c59d1efc47b9c991d81a7"
79
+ }
80
+ ]
81
+ }
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.9.10'
30
+ const PLUGIN_VERSION = '1.11.0'
31
31
  const STORE_KEY = 'dsh-theme-palette.v1'
32
32
  const OVERRIDE_SOURCE = 'dsh-theme-palette'
33
33
 
@@ -146,6 +146,19 @@ window.__ModuleLoader__.load({
146
146
  moss: { '00': '#f9fbf8', '60': '#f5f7f2', '150': '#e0e7dd', '950': '#131b16', '875': '#1a241d', '850': '#1d2a22', '800': '#2f3f34' },
147
147
  }
148
148
 
149
+ /* ---- 参考实现的 ink / link 精确对齐(1.9.0)----
150
+ * 参考的 ink 比求解结果更浅、更中性;这里直接取参考值,
151
+ * 但仍过一遍对比度下限:达不到的目标文字沿远离背景方向微调(只动明度,不动色相)。
152
+ * 三级文字在控件面上若参考本身不达 AA,会略微变深 —— 这是唯一允许偏离参考的地方。
153
+ * (审计 2026-10-27 P3-8:从 buildTokensRaw 体内上提 —— 它是不变的常量表,
154
+ * 放在函数里每次未命中缓存都要重新分配,且与 SURFACE_TARGETS 的组织不一致。)
155
+ */
156
+ const TEXT_TARGETS = {
157
+ warm: { light: { 1000: '#2b2622', 700: '#6b625a', 600: '#8c8177', link: '#8a5233' }, dark: { 50: '#f5efe8', 300: '#c4b8ac', 400: '#9a8d80', link: '#e6c4a0' } },
158
+ indigo: { light: { 1000: '#1a2330', 700: '#516074', 600: '#74839a', link: '#345286' }, dark: { 50: '#eef3fa', 300: '#b7c5d8', 400: '#8a9bb2', link: '#a6bfe3' } },
159
+ moss: { light: { 1000: '#1f2a22', 700: '#55645a', 600: '#7a8a7e', link: '#406236' }, dark: { 50: '#eef5ef', 300: '#b8c8bc', 400: '#8ba091', link: '#aed0a2' } },
160
+ }
161
+
149
162
  /* 对比度下限:主文本 16 / 次文本 6 / 三级文本 4.6(AA)/ 链接 4.6 */
150
163
  /* 对比度下限:按**真实承载面**定(卡层/模块层比画布更难达标),不是按画布定。
151
164
  正文 15.5 / 次级 6.2 / 三级 4.62(AA)/ 链接 4.62(AA) */
@@ -238,10 +251,36 @@ window.__ModuleLoader__.load({
238
251
  }
239
252
  }
240
253
 
254
+ /**
255
+ * 只给一个主色时派生整套自定义配色(1.10.0,审计建议 4)。
256
+ *
257
+ * 用户往往只有一个「想要的品牌色」,让他自己配底色/文字色既难又容易把对比度配坏。
258
+ * 这里按主色的色相与色度推出一对「纸面 + 墨色」:
259
+ * · 色相 = 主色色相;色度取主色的 15%–22% 并夹住上限 —— 纸面只该有一点点色,不该是整片主色;
260
+ * · 明度由明暗模式决定(亮底纸面 0.968 / 墨 0.235;暗底 0.165 / 0.935),这是引擎已验证的区间;
261
+ * · 主色原样保留,交给 customScheme 去钳位与求解(它会给链接/强调色兜对比度)。
262
+ * 主色无彩(C < 0.004)时退化为中性纸面,避免「黑底 + 白字 + 黄强调派生出一套淡粉」那类老问题。
263
+ */
264
+ function deriveCustomFromAccent(accentHex, pref) {
265
+ if (typeof accentHex !== 'string' || !HEX_RE.test(accentHex)) return null
266
+ const [L, C, h] = toOklch(accentHex)
267
+ const neutral = C < 0.004
268
+ const dark = pref === 'dark'
269
+ const tint = (scale, lo, hi) => (neutral ? 0 : Math.min(hi, Math.max(lo, C * scale)))
270
+ const bg = fromOklch(dark ? 0.165 : 0.968, tint(0.22, 0.006, 0.014), h)
271
+ const text = fromOklch(dark ? 0.935 : 0.235, tint(0.18, 0.004, 0.012), h)
272
+ return { bg, text, accent: String(accentHex).toLowerCase(), accentL: L }
273
+ }
274
+
241
275
  const MIN_MAIN = 12
242
276
  const MIN_SECONDARY = 6.2
243
- const MIN_TERTIARY = 4.62
244
- const MIN_LINK = 4.62
277
+ /* 三级/链接的浅色下限从 4.62 抬到 4.85 / 4.95(1.10.0):4.62 是「刚好过 AA」,
278
+ * 一点余量都不留 —— 而色觉缺陷模拟(Machado 2009,deutan/protan)会让同一对
279
+ * 前景/背景的比值上下浮动 5–9%,实测链接在 4.62 时就跌到 4.22–4.34(低于 AA)。
280
+ * 抬到 4.95 后三种色觉模拟下的最低值仍有 4.5+(tools/check-audit.mjs 第 19 组守着)。
281
+ * 代价:浅色链接与三级文字比 1.9.x 略深一档,肉眼几乎无差别。 */
282
+ const MIN_TERTIARY = 4.85
283
+ const MIN_LINK = 4.95
245
284
  /* 深色链接不再用 WCAG 比值兜底(1.9.9 补):深色面上同一比值换到的 Lc 差得远,
246
285
  4.62 在自定义配色的 875 面上只有 Lc 52.7,低于本插件给文字定的 Lc 60 下限。
247
286
  tuneRamps 里改为沿亮度把 Lc 顶到 60(高对比 75),这里只保留浅色侧的下限。 */
@@ -500,12 +539,16 @@ window.__ModuleLoader__.load({
500
539
  { id: 'off', label: '同正文' },
501
540
  ]
502
541
  const DEFAULT_MD = 'on'
503
- /** 高对比开关(反哺 A-4):不改配色,把文字下限抬到 AAA。 */
542
+ /** 高对比开关(反哺 A-4):不改配色,把文字下限抬到 AAA。
543
+ * auto(1.10.0,审计 3.11):跟随系统 prefers-contrast: more ——
544
+ * 系统开了「提高对比度」就自动用高对比档,没开就用标准档。
545
+ * 已存过 off/on 的老用户不受影响(读回自己的选择)。 */
504
546
  const HC_MODES = [
547
+ { id: 'auto', label: '跟随系统' },
505
548
  { id: 'off', label: '关' },
506
549
  { id: 'on', label: '开' },
507
550
  ]
508
- const DEFAULT_HC = 'off'
551
+ const DEFAULT_HC = 'auto'
509
552
  const MD_KEY = 'dsh-theme-palette.md.v1'
510
553
 
511
554
  /**
@@ -720,6 +763,40 @@ window.__ModuleLoader__.load({
720
763
  return rgbToHex(fg.map((v, i) => v * a + bg[i] * (1 - a)))
721
764
  }
722
765
 
766
+ /* ---------- 色觉缺陷模拟(Machado 2009,严重度 1.0)----------
767
+ * 只用于「告诉用户这个配色对色觉缺陷者是否还分得开」,不参与任何求解:
768
+ * 求解按明度走就已经对色觉友好,这里是把结论显示出来。
769
+ * 矩阵作用在线性 sRGB 上,和公开实现一致。
770
+ */
771
+ const CVD_MATRICES = {
772
+ deutan: [[0.367322, 0.860646, -0.227968], [0.280085, 0.672501, 0.047413], [-0.011820, 0.042940, 0.968881]],
773
+ protan: [[0.152286, 1.052583, -0.204868], [0.114503, 0.786281, 0.099216], [-0.003882, -0.048116, 1.051998]],
774
+ tritan: [[1.255528, -0.076749, -0.178779], [-0.078411, 0.930809, 0.147602], [0.004733, 0.691367, 0.303900]],
775
+ }
776
+ function simulateCvd(hex, kind) {
777
+ const m = CVD_MATRICES[kind]
778
+ if (!m || typeof hex !== 'string') return hex
779
+ const lin = hexToRgb(hex).map((v) => {
780
+ const s = v / 255
781
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
782
+ })
783
+ const out = m.map((row) => row[0] * lin[0] + row[1] * lin[1] + row[2] * lin[2])
784
+ return rgbToHex(out.map((v) => {
785
+ const c = v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055
786
+ return clamp(c * 255, 0, 255)
787
+ }))
788
+ }
789
+ /** 两个颜色在给定色觉模拟下的 OKLab 距离(×100,与设计软件的 ΔE 同量级)。 */
790
+ function deltaE(a, b, kind) {
791
+ const x = toOklch(simulateCvd(a, kind))
792
+ const y = toOklch(simulateCvd(b, kind))
793
+ const toLab = ([L, C, h]) => [L, C * Math.cos((h * Math.PI) / 180), C * Math.sin((h * Math.PI) / 180)]
794
+ const p = toLab(x)
795
+ const q = toLab(y)
796
+ return Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]) * 100
797
+ }
798
+
799
+
723
800
  /**
724
801
  * 语义色温度调和:保留色相为主(绿仍是绿、红仍是红),只把色相往配色的暖/冷方向
725
802
  * 拉一小段、并压低饱和度。用于 file diff 这种大面积色块——不能整块换成主题色,
@@ -1193,12 +1270,23 @@ window.__ModuleLoader__.load({
1193
1270
  // 注释/时间戳:既要跟随主题(色度由 midC 给),也要在画布/卡层上可读。
1194
1271
  // 直接拿 400 号色在浅色下只有约 2.4:1,所以这里对 00/60 求解到 AA。
1195
1272
  light: solveForeground(r.bluish, '400', [r.bluish['00'], r.bluish['60']], floors(hc).tertiary, 0.62),
1196
- dark: solveForeground(
1197
- r.bluish,
1198
- '500',
1273
+ /* 深色(审计 2026-10-27 P2-3):原先只按浅色下限(4.85)对 800/850 求解,
1274
+ * 实测 Lc 只有 48–52 —— 本插件给文字定的可读线是 Lc 60,三级文字同面有 62–66。
1275
+ * 深色侧的「WCAG 过线 ≠ 看得清」论证(见 MIN_SECONDARY_DARK)同样适用于注释。
1276
+ * 这里在 WCAG 求解之后再过一道 Lc 闸:
1277
+ * 标准档 60(与链接同线);HC 档 70 而不是照抄链接的 75 ——
1278
+ * HC 三级文字 Lc 落在 72–77,注释套 75 会反超三级(靛蓝 75 > 72),
1279
+ * 破坏「注释弱于三级」的层级。抬完后标准档注释 WCAG ≈ 5.6–6.0,仍低于三级的 6.4。 */
1280
+ dark: raiseToLc(
1281
+ solveForeground(
1282
+ r.bluish,
1283
+ '500',
1284
+ [r.bluish['800'], r.bluish['850']],
1285
+ Math.min(floors(hc).tertiary, contrast('#ffffff', r.bluish['800']) * 0.995),
1286
+ 0.30,
1287
+ ),
1199
1288
  [r.bluish['800'], r.bluish['850']],
1200
- Math.min(floors(hc).tertiary, contrast('#ffffff', r.bluish['800']) * 0.995),
1201
- 0.30,
1289
+ hc ? 70 : 60,
1202
1290
  ),
1203
1291
  },
1204
1292
  // 主人气泡(`--dsw-specific-bubble`)宿主指向 deepseek-50:那是「品牌色的极浅底」,
@@ -1620,16 +1708,8 @@ window.__ModuleLoader__.load({
1620
1708
  if (!params.tinted && sidebarMode === 'light') {
1621
1709
  pairs['--dsw-static-neutral-bluish-00'] = { light: '#fafafa', dark: BASE.bluish['00'] }
1622
1710
  }
1623
- /* ---- 参考实现的 ink / link 精确对齐(1.9.0)----
1624
- * 参考的 ink 比我们求解结果更浅、更中性;这里直接取参考值,
1625
- * 但仍过一遍对比度下限:达不到的目标文字沿远离背景方向微调(只动明度,不动色相)。
1626
- * 三级文字在控件面上若参考本身不达 AA,会略微变深 —— 这是唯一允许偏离参考的地方。
1627
- */
1628
- const TEXT_TARGETS = {
1629
- warm: { light: { 1000: '#2b2622', 700: '#6b625a', 600: '#8c8177', link: '#8a5233' }, dark: { 50: '#f5efe8', 300: '#c4b8ac', 400: '#9a8d80', link: '#e6c4a0' } },
1630
- indigo: { light: { 1000: '#1a2330', 700: '#516074', 600: '#74839a', link: '#345286' }, dark: { 50: '#eef3fa', 300: '#b7c5d8', 400: '#8a9bb2', link: '#a6bfe3' } },
1631
- moss: { light: { 1000: '#1f2a22', 700: '#55645a', 600: '#7a8a7e', link: '#406236' }, dark: { 50: '#eef5ef', 300: '#b8c8bc', 400: '#8ba091', link: '#aed0a2' } },
1632
- }
1711
+ // 参考墨色表在模块级(TEXT_TARGETS,紧随 SURFACE_TARGETS);自定义没有参考表,
1712
+ // 走下面的通用求解路径。
1633
1713
  const TT = TEXT_TARGETS[scheme.id]
1634
1714
  // 三套预置走参考目标;自定义没有参考表,但同样要拿到跟随主题的注释/文字
1635
1715
  if (scheme.tinted) {
@@ -1862,6 +1942,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1862
1942
  .dsh-tp-hexerror{font-size:11.5px;line-height:16px;color:var(--dsw-alias-state-error-primary)}
1863
1943
  .dsh-tp-hexnote{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}
1864
1944
  .dsh-tp-custom-hint{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
1945
+ /* 「由主色派生」的副作用说明:左侧一道细边,读起来是提示而不是正文(1.10.0) */
1946
+ .dsh-tp-custom-note{margin:0;padding:6px 0 6px 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);border-left:2px solid var(--dsw-alias-border-l3)}
1865
1947
  .dsh-tp-note{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}
1866
1948
  .dsh-tp-status{margin:0;font-size:11.5px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
1867
1949
  .dsh-tp-status.is-bad{color:var(--dsw-alias-state-error-primary)}
@@ -2084,7 +2166,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2084
2166
  function readHcStored() {
2085
2167
  try {
2086
2168
  const raw = localStorage.getItem(HC_KEY)
2087
- if (raw === 'on' || raw === 'off') return raw
2169
+ if (raw === 'on' || raw === 'off' || raw === 'auto') return raw
2088
2170
  } catch (err) { /* 退回默认 */ }
2089
2171
  return DEFAULT_HC
2090
2172
  }
@@ -2118,7 +2200,12 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2118
2200
  x: { bg: custom.bg, text: custom.text, accent: custom.accent },
2119
2201
  }
2120
2202
  try {
2121
- const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(raw))))
2203
+ // TextEncoder 产出与旧版 escape/encodeURIComponent 链逐字节相同的 UTF-8(审计 2026-10-27 P3-7):
2204
+ // 新旧版本的短码可以互相解码,check-audit 第 2 组的旧码断言守着这一点。
2205
+ const bytes = new TextEncoder().encode(JSON.stringify(raw))
2206
+ let binary = ''
2207
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])
2208
+ const b64 = btoa(binary)
2122
2209
  return CONFIG_PREFIX + b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
2123
2210
  } catch (err) { return '' }
2124
2211
  }
@@ -2132,7 +2219,10 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2132
2219
  try {
2133
2220
  let b64 = text.slice(CONFIG_PREFIX.length).replace(/-/g, '+').replace(/_/g, '/')
2134
2221
  while (b64.length % 4 !== 0) b64 += '='
2135
- const raw = JSON.parse(decodeURIComponent(escape(atob(b64))))
2222
+ const bin = atob(b64)
2223
+ const bytes = new Uint8Array(bin.length)
2224
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
2225
+ const raw = JSON.parse(new TextDecoder().decode(bytes))
2136
2226
  if (!raw || raw.v !== 1) return null
2137
2227
  const next = {
2138
2228
  palette: SCHEMES.some((sc) => sc.id === raw.p) ? raw.p : null,
@@ -2195,7 +2285,9 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2195
2285
 
2196
2286
  function pickLocale(pref) {
2197
2287
  if (pref !== 'auto') return pref
2198
- return hostLocale() || 'zh-CN'
2288
+ // 兜底必须是 en 而不是 zh-CN(审计 2026-10-27 P1-2):宿主语言不在
2289
+ // zh/ja/en 里(fr/de/es/pt/…)时给整页中文,对国际用户是最差的选择。
2290
+ return hostLocale() || 'en'
2199
2291
  }
2200
2292
 
2201
2293
  const TEXT = {
@@ -2222,10 +2314,14 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2222
2314
  hc: '高对比',
2223
2315
  custom: '自定义配色',
2224
2316
  customHint: '取三个色的色相与色度;过亮/过暗的强调色会归一化以保证可读,明度由引擎按模式计算。',
2225
- notice: '1.9.9 按独立审计逐条修订:深色文字下限单独提高(次级 8 / 三级 6.4)、文字着色三档对预置配色真正生效、色阶不再出现「编号越大反而越亮」、高对比档的边框与滚动条也真提高了;报表新增 APCA 列。',
2317
+ derive: '由主色派生',
2318
+ deriveHint: '用当前强调色的色相与色度推导底色与文字(明暗跟随当前模式)',
2319
+ deriveNote: '「由主色派生」以强调色为唯一输入,自动算出底色与文字 —— 会覆盖你手动设置的底色与文字;派生之后仍可逐项手改。',
2320
+ notice: '1.11.0:深色模式下注释与时间戳更清晰(按感知对比度 Lc 加了一道闸);宿主语言不是中/英/日时界面改为回落英文;取色拖动按帧合并更流畅。',
2226
2321
  dismiss: '知道了',
2227
2322
  explainClamp: '强调色已归一化:{from} → {to}(保证按钮与勾选可读)',
2228
2323
  explainLink: '链接已为可读性压暗:{ratio}:1(目标 {target}:1)',
2324
+ explainCvd: '色觉缺陷提示:{kind} 视觉下强调色与底色/文字几乎同色(ΔE {dE})。文字仍可读,但按钮与色块会分不开;换一个更亮或更暗的强调色即可。',
2229
2325
  preview: '预览',
2230
2326
  sample: '试试示例',
2231
2327
  copyDiag: '复制诊断',
@@ -2261,7 +2357,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2261
2357
  importBad: '短码无效,已忽略',
2262
2358
  badHex: 'HEX 需为 #RRGGBB',
2263
2359
  hexStale: '这个值刚被别处改过,已保留最新值',
2264
- hcSub: '尽量把文字下限抬到 AAA(正文 ≥15、次文 ≥9、三级 ≥7);深色 800/850 不为正文兜底(政策外),但仍守 AA 底线。',
2360
+ hcSub: '把文字下限抬到 AAA(正文 ≥15、次文 ≥9、三级 ≥7);「跟随系统」= 系统「提高对比度」开启时自动切高对比档;深色 800/850 不为正文兜底(政策外),但仍守 AA 底线。',
2265
2361
  },
2266
2362
  'zh-TW': {
2267
2363
  nav: '主題色',
@@ -2286,10 +2382,14 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2286
2382
  hc: '高對比',
2287
2383
  custom: '自訂配色',
2288
2384
  customHint: '取三個色的色相與色度;過亮/過暗的強調色會正規化以確保可讀性,明度由引擎依模式計算。',
2289
- notice: '1.9.9 依獨立稽核逐條修訂:深色文字下限另行提高(次要 8 / 三級 6.4)、文字著色三檔對預設配色真正生效、色階不再出現「編號越大反而越亮」、高對比檔的邊框與捲軸也真提高了;報表新增 APCA 欄。',
2385
+ derive: '由主色派生',
2386
+ deriveHint: '用目前強調色的色相與彩度推導底色與文字(明暗跟隨目前模式)',
2387
+ deriveNote: '「由主色派生」以強調色為唯一輸入,自動算出底色與文字 —— 會覆蓋你手動設置的底色與文字;派生之後仍可逐項手改。',
2388
+ notice: '1.11.0:深色模式下註釋與時間戳更清晰(按感知對比度 Lc 加了一道閘);宿主語言不是中/英/日時介面改為回落英文;取色拖動按幀合併更流暢。',
2290
2389
  dismiss: '知道了',
2291
2390
  explainClamp: '強調色已正規化:{from} → {to}(確保按鈕與勾選可讀)',
2292
2391
  explainLink: '連結已為可讀性壓暗:{ratio}:1(目標 {target}:1)',
2392
+ explainCvd: '色覺缺陷提示:{kind} 視覺下強調色與底色/文字幾乎同色(ΔE {dE})。文字仍可讀,但按鈕與色塊會分不開;換一個更亮或更暗的強調色即可。',
2293
2393
  preview: '預覽',
2294
2394
  sample: '試試示例',
2295
2395
  copyDiag: '複製診斷',
@@ -2325,7 +2425,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2325
2425
  importBad: '短碼無效,已忽略',
2326
2426
  badHex: 'HEX 需為 #RRGGBB',
2327
2427
  hexStale: '這個值剛被別處改過,已保留最新值',
2328
- hcSub: '盡量把文字下限抬到 AAA(正文 ≥15、次文 ≥9、三級 ≥7);深色 800/850 不為正文兜底(政策外),但仍守 AA 底線。',
2428
+ hcSub: '把文字下限抬到 AAA(正文 ≥15、次文 ≥9、三級 ≥7);「跟隨系統」= 系統「提高對比度」開啟時自動切高對比檔;深色 800/850 不為正文兜底(政策外),但仍守 AA 底線。',
2329
2429
  },
2330
2430
  en: {
2331
2431
  nav: 'Color theme',
@@ -2350,10 +2450,14 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2350
2450
  hc: 'High contrast',
2351
2451
  custom: 'Custom palette',
2352
2452
  customHint: 'Takes hue and chroma from three colours; extreme accents are normalised for readability; lightness comes from the engine per mode.',
2353
- notice: '1.9.9 fixes the issues from an independent audit: dark text gets its own higher floors (secondary 8 / tertiary 6.4), the text-tint tiers now really work on presets, ramps no longer go "higher step, lighter colour", and high-contrast borders and scrollbars are genuinely raised; the report gains an APCA column.',
2453
+ derive: 'Derive from accent',
2454
+ deriveHint: 'Derive background and text from the current accent hue and chroma (follows the current light/dark mode)',
2455
+ deriveNote: '"Derive from accent" takes the accent as its only input and computes background and text from it — this overwrites the background and text you set by hand; you can still edit each of them afterwards.',
2456
+ notice: '1.11.0: captions and timestamps are clearer in dark mode (a perceptual-contrast Lc gate was added); the UI now falls back to English when the host language is not Chinese, English or Japanese; colour-picker drags are smoother (frame-coalesced).',
2354
2457
  dismiss: 'Got it',
2355
2458
  explainClamp: 'Accent normalised: {from} → {to} (keeps buttons and checks readable)',
2356
2459
  explainLink: 'Link darkened for readability: {ratio}:1 (target {target}:1)',
2460
+ explainCvd: 'Colour-vision note: with {kind} vision the accent is nearly the same colour as the background/text (ΔE {dE}). Text stays readable, but buttons and swatches blur together — pick a lighter or darker accent.',
2357
2461
  preview: 'Preview',
2358
2462
  sample: 'Try a sample',
2359
2463
  copyDiag: 'Copy diagnostics',
@@ -2389,7 +2493,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2389
2493
  importBad: 'Invalid code, ignored',
2390
2494
  badHex: 'HEX must be #RRGGBB',
2391
2495
  hexStale: 'This value was changed elsewhere; the latest value was kept',
2392
- hcSub: 'Raises text floors toward AAA (body ≥15, secondary ≥9, tertiary ≥7); dark 800/850 no longer back the body text (out of policy) but still hold the AA floor.',
2496
+ hcSub: 'Raises text floors toward AAA (body ≥15, secondary ≥9, tertiary ≥7); "System" = automatically switches to the high-contrast tier when the OS "increase contrast" setting is on; dark 800/850 no longer back the body text (out of policy) but still hold the AA floor.',
2393
2497
  },
2394
2498
  ja: {
2395
2499
  nav: 'カラーテーマ',
@@ -2414,10 +2518,14 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2414
2518
  hc: 'ハイコントラスト',
2415
2519
  custom: 'カスタム配色',
2416
2520
  customHint: '3 色の色相と彩度を使用。極端なアクセント色は可読性のため正規化され、明度はモードごとにエンジンが決めます。',
2417
- notice: '1.9.9 は独立監査の指摘を順に修正しました:ダークの文字下限を個別に引き上げ(二次 8 / 三次 6.4)、テキストの色味 3 段階がプリセットでも実際に効くようになり、色階が「番号が大きいほど明るい」現象も解消、ハイコントラストの枠線とスクロールバーも実際に強まりました。レポートに APCA 列を追加。',
2521
+ derive: 'メイン色から生成',
2522
+ deriveHint: '現在のアクセント色の色相と彩度から背景色と文字色を生成します(明暗は現在のモードに従います)',
2523
+ deriveNote: '「メイン色から生成」はアクセント色だけを入力として背景色と文字色を自動計算します。手動で設定した背景色と文字色は上書きされます(生成後に個別に変更できます)。',
2524
+ notice: '1.11.0:ダークモードのキャプション・タイムスタンプがより明瞭になりました(知覚コントラスト Lc のゲートを追加)。ホスト言語が中/英/日以外のとき UI は英語にフォールバックします。カラーピッカーのドラッグがフレーム統合でより滑らかになりました。',
2418
2525
  dismiss: '了解',
2419
2526
  explainClamp: 'アクセント色を正規化しました:{from} → {to}(ボタンとチェックの可読性のため)',
2420
2527
  explainLink: 'リンクを可読性のため暗くしました:{ratio}:1(目標 {target}:1)',
2528
+ explainCvd: '色覚のヒント:{kind} ではアクセント色が背景・文字とほぼ同色になります(ΔE {dE})。文字は読めますが、ボタンと色面は見分けにくくなります。もっと明るい/暗いアクセント色に変えてください。',
2421
2529
  preview: 'プレビュー',
2422
2530
  sample: 'サンプルを試す',
2423
2531
  copyDiag: '診断をコピー',
@@ -2453,7 +2561,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2453
2561
  importBad: '無効なコードです',
2454
2562
  badHex: 'HEX は #RRGGBB 形式で入力してください',
2455
2563
  hexStale: 'この値は別の場所で変更されました。最新の値を保持しています',
2456
- hcSub: '本文の下限を AAA に近づけます(本文 ≥15、副次 ≥9、三次 ≥7)。暗い 800/850 は本文を受け皿にしません(対象外)ですが、AA 下限は守ります。',
2564
+ hcSub: '本文の下限を AAA に近づけます(本文 ≥15、副次 ≥9、三次 ≥7)。「システム」= OS の「コントラストを上げる」が有効なとき自動的にハイコントラストになります。暗い 800/850 は本文を受け皿にしません(対象外)ですが、AA 下限は守ります。',
2457
2565
  },
2458
2566
  }
2459
2567
 
@@ -2468,7 +2576,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2468
2576
  control: { accent: '主题色', mono: '黑白' },
2469
2577
  md: { on: '着色', off: '同正文' },
2470
2578
  lang: { auto: '自动' },
2471
- hc: { off: '关', on: '开' },
2579
+ hc: { auto: '跟随系统', off: '关', on: '开' },
2472
2580
  custom: { bg: '底色', text: '文字', accent: '强调' },
2473
2581
  },
2474
2582
  'zh-TW': {
@@ -2480,7 +2588,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2480
2588
  control: { accent: '主題色', mono: '黑白' },
2481
2589
  md: { on: '著色', off: '同本文' },
2482
2590
  lang: { auto: '自動' },
2483
- hc: { off: '關', on: '開' },
2591
+ hc: { auto: '跟隨系統', off: '關', on: '開' },
2484
2592
  custom: { bg: '底色', text: '文字', accent: '強調' },
2485
2593
  },
2486
2594
  en: {
@@ -2492,7 +2600,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2492
2600
  control: { accent: 'Accent', mono: 'Mono' },
2493
2601
  md: { on: 'Tinted', off: 'Body colour' },
2494
2602
  lang: { auto: 'Auto' },
2495
- hc: { off: 'Off', on: 'On' },
2603
+ hc: { auto: 'System', off: 'Off', on: 'On' },
2496
2604
  custom: { bg: 'Surface', text: 'Text', accent: 'Accent' },
2497
2605
  },
2498
2606
  ja: {
@@ -2504,7 +2612,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2504
2612
  control: { accent: 'アクセント', mono: 'モノクロ' },
2505
2613
  md: { on: '色づけ', off: '本文と同じ' },
2506
2614
  lang: { auto: '自動' },
2507
- hc: { off: 'オフ', on: 'オン' },
2615
+ hc: { auto: 'システム', off: 'オフ', on: 'オン' },
2508
2616
  custom: { bg: '背景', text: '本文', accent: 'アクセント' },
2509
2617
  },
2510
2618
  }
@@ -2641,6 +2749,12 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2641
2749
  { className: 'dsh-tp-custom' },
2642
2750
  h('div', { className: 'dsh-tp-custom-head' },
2643
2751
  h('b', null, txt.custom),
2752
+ h('button', {
2753
+ type: 'button',
2754
+ className: 'dsh-tp-mini',
2755
+ title: txt.deriveHint,
2756
+ onClick: () => props.deriveFromAccent(),
2757
+ }, txt.derive),
2644
2758
  h('button', {
2645
2759
  type: 'button',
2646
2760
  className: 'dsh-tp-mini',
@@ -2705,14 +2819,17 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
2705
2819
  invalid && h('span', { id: errId, className: 'dsh-tp-hexerror', role: 'alert' }, txt.badHex),
2706
2820
  stale && h('span', { id: staleId, className: 'dsh-tp-hexnote', role: 'status' }, txt.hexStale))
2707
2821
  })),
2822
+ h('p', { className: 'dsh-tp-custom-note' }, txt.deriveNote),
2708
2823
  h('p', { className: 'dsh-tp-custom-hint' }, txt.customHint),
2709
- explain && (explain.accentClamped || explain.linkShifted) && h(
2824
+ explain && (explain.accentClamped || explain.linkShifted || explain.cvdRisk) && h(
2710
2825
  'div',
2711
2826
  { className: 'dsh-tp-explain', role: 'status' },
2712
2827
  explain.accentClamped && h('span', null,
2713
2828
  fmt(txt.explainClamp, { from: explain.accentFrom, to: explain.accentTo })),
2714
2829
  explain.linkShifted && h('span', null,
2715
2830
  fmt(txt.explainLink, { ratio: explain.linkRatio.toFixed(2), target: explain.linkTarget })),
2831
+ explain.cvdRisk && h('span', null,
2832
+ fmt(txt.explainCvd, { kind: explain.cvdKind, dE: explain.cvdDeltaE })),
2716
2833
  ),
2717
2834
  )
2718
2835
 
@@ -3031,7 +3148,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3031
3148
 
3032
3149
  // 开发期接口:palette-dev.mjs 通过它拿到同一套引擎(避免「报表好看、上线不一样」)
3033
3150
  if (DEV_OPT_IN && typeof window !== 'undefined' && typeof window.__DSH_THEME_PALETTE_DEV__ === 'function') {
3034
- window.__DSH_THEME_PALETTE_DEV__({ buildTokens, SCHEMES, SIDEBAR_MODES, TEXT_MODES, SCROLL_MODES, CONTROL_MODES, MD_MODES, HC_MODES, contrast, toOklch, fromOklch, flatten, rampsFor, floors, encodeConfig, decodeConfig, customScheme, readCustom, writeCustom, CONTRAST_SURFACES, CONTRAST_ROLES, buildContrastReport, devContrastReport, CONTRAST_TARGETS, CONTRAST_EXEMPT, CONTRAST_FLOOR, CONTRAST_SOLVER_MARGIN, LAYER_MARK, settingsCss: SETTINGS_CSS, markdownCss: MARKDOWN_CSS, cardColors })
3151
+ window.__DSH_THEME_PALETTE_DEV__({ buildTokens, SCHEMES, SIDEBAR_MODES, TEXT_MODES, SCROLL_MODES, CONTROL_MODES, MD_MODES, HC_MODES, contrast, toOklch, fromOklch, flatten, luminance, withAlpha, mixHex, harmonize, deltaE, simulateCvd, rampsFor, floors, floorsDark, apcaContrast, raiseToLc, solveForeground, pickLocale, hostLocale, encodeConfig, decodeConfig, customScheme, deriveCustomFromAccent, readCustom, writeCustom, CONTRAST_SURFACES, CONTRAST_ROLES, buildContrastReport, devContrastReport, CONTRAST_TARGETS, CONTRAST_EXEMPT, CONTRAST_FLOOR, CONTRAST_SOLVER_MARGIN, LAYER_MARK, settingsCss: SETTINGS_CSS, markdownCss: MARKDOWN_CSS, cardColors })
3035
3152
  }
3036
3153
 
3037
3154
  return {
@@ -3052,8 +3169,9 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3052
3169
  let controlMode = readControlStored()
3053
3170
  let mdMode = readMdStored()
3054
3171
  let hcMode = readHcStored()
3055
- const getConfigCode = () => encodeConfig({ palette: current, sidebar: sidebarMode, text: textMode, scroll: scrollMode, control: controlMode, md: mdMode, hc: hcMode, lang: langPref })
3172
+ // langPref 必须先于引用它的闭包声明(审计 2026-10-27 P3-9:原顺序运行时无害,但极易误读成 TDZ)
3056
3173
  let langPref = readLangPref()
3174
+ const getConfigCode = () => encodeConfig({ palette: current, sidebar: sidebarMode, text: textMode, scroll: scrollMode, control: controlMode, md: mdMode, hc: hcMode, lang: langPref })
3057
3175
  const localeListeners = new Set()
3058
3176
  const currentLocale = () => pickLocale(langPref)
3059
3177
  const emitLocale = () => { for (const fn of localeListeners) { try { fn() } catch (err) { /* 忽略 */ } } }
@@ -3141,9 +3259,17 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3141
3259
  // 但覆盖层写失败时屏幕上仍是旧配色。分开报,别让用户以为换成功了。
3142
3260
  let applyError = null
3143
3261
 
3262
+ // 系统对比度偏好(审计 3.11):auto 档监听 prefers-contrast: more。
3263
+ // 测试沙箱里没有 matchMedia —— 判空退回「系统没要求」,auto 等于标准档。
3264
+ const contrastMq = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
3265
+ ? window.matchMedia('(prefers-contrast: more)')
3266
+ : null
3267
+ const systemWantsHc = () => !!(contrastMq && contrastMq.matches)
3268
+ const effectiveHc = () => hcMode === 'on' || (hcMode === 'auto' && systemWantsHc())
3269
+
3144
3270
  const applyAll = (reason) => {
3145
3271
  const scheme = SCHEMES.find((s) => s.id === current) || SCHEMES[0]
3146
- const tokens = buildTokens(scheme, sidebarMode, textMode, scrollMode, controlMode, mdMode, hcMode === 'on')
3272
+ const tokens = buildTokens(scheme, sidebarMode, textMode, scrollMode, controlMode, mdMode, effectiveHc())
3147
3273
  let next = () => {}
3148
3274
  if (tokens !== undefined) {
3149
3275
  try {
@@ -3347,7 +3473,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3347
3473
  const getCustomExplain = () => {
3348
3474
  const colors = readCustom()
3349
3475
  const scheme = customScheme(colors)
3350
- const hc = hcMode === 'on'
3476
+ const hc = effectiveHc()
3351
3477
  const t = buildTokens(scheme, sidebarMode, textMode, scrollMode, controlMode, mdMode, hc)
3352
3478
  const link = t['--dsw-alias-link'].light
3353
3479
  const base = t['--dsw-static-deepseek-500'].light
@@ -3355,6 +3481,11 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3355
3481
  const ratio = Math.min(...surfaces.map((bg) => contrast(link, bg)))
3356
3482
  const acL = toOklch(colors.accent)[0]
3357
3483
  const accentTo = Math.min(0.64, Math.max(0.30, acL))
3484
+ // 色觉缺陷下的可分辨度:强调色 vs 底色 / 文字,三种模拟取最差的那种。
3485
+ // 只提示不拦:用户的配色归用户,插件只负责把风险说出来。
3486
+ const cvd = ['deutan', 'protan', 'tritan']
3487
+ .map((kind) => ({ kind, dE: Math.min(deltaE(colors.accent, colors.bg, kind), deltaE(colors.accent, colors.text, kind)) }))
3488
+ .sort((a, b) => a.dE - b.dE)[0]
3358
3489
  return {
3359
3490
  accentClamped: Math.abs(acL - accentTo) > 0.005,
3360
3491
  accentFrom: Math.round(acL * 100) / 100,
@@ -3362,6 +3493,9 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3362
3493
  linkShifted: link.toLowerCase() !== base.toLowerCase(),
3363
3494
  linkRatio: Math.round(ratio * 100) / 100,
3364
3495
  linkTarget: floors(hc).link,
3496
+ cvdRisk: cvd.dE < 12,
3497
+ cvdKind: cvd.kind,
3498
+ cvdDeltaE: Math.round(cvd.dE * 10) / 10,
3365
3499
  }
3366
3500
  }
3367
3501
  /** 诊断导出:把当前配色与对比度矩阵打成可粘贴的 JSON(提 issue 用)。 */
@@ -3370,7 +3504,57 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3370
3504
  version: PLUGIN_VERSION,
3371
3505
  settings: { palette: current, sidebar: sidebarMode, text: textMode, scroll: scrollMode, control: controlMode, md: mdMode, hc: hcMode, lang: langPref },
3372
3506
  report: getContrastReport(),
3507
+ coverage: getCoverageReport(),
3373
3508
  })
3509
+ /**
3510
+ * 运行时覆盖报告(审计建议 5):枚举宿主样式表里的 --dsw-* 自定义属性,按四类分:
3511
+ * ours —— 本插件覆盖层接管(lastTokens 里);
3512
+ * derived —— 值是 var(--dsw-…) 引用(宿主别名,跟着基础 token 走,无需接管);
3513
+ * literal-chromatic —— 字面色值且带色彩(宿主语义色等,有意不染);
3514
+ * literal-gray —— 字面灰值(跟配色的候选:接管后体验更一致)。
3515
+ * 只读、尽力而为:沙箱/跨域拿不到 styleSheets 时返回 { ok:false },不影响其它诊断。
3516
+ */
3517
+ const getCoverageReport = () => {
3518
+ const found = new Map()
3519
+ try {
3520
+ const sheets = typeof document !== 'undefined' && document.styleSheets ? Array.from(document.styleSheets) : []
3521
+ const walk = (rules) => {
3522
+ for (const rule of rules) {
3523
+ if (!rule) continue
3524
+ if (rule.cssRules) { try { walk(rule.cssRules) } catch (err) { /* 跨域子表 */ } continue }
3525
+ const style = rule.style
3526
+ if (!style || typeof style.length !== 'number') continue
3527
+ for (let i = 0; i < style.length; i++) {
3528
+ const prop = style[i]
3529
+ if (typeof prop === 'string' && prop.indexOf('--dsw-') === 0) {
3530
+ found.set(prop, style.getPropertyValue(prop).trim())
3531
+ }
3532
+ }
3533
+ }
3534
+ }
3535
+ for (const sheet of sheets) { try { walk(sheet.cssRules) } catch (err) { /* 跨域表跳过 */ } }
3536
+ } catch (err) {
3537
+ return { ok: false, reason: String((err && err.message) || err) }
3538
+ }
3539
+ const ours = new Set(lastTokens ? Object.keys(lastTokens) : [])
3540
+ const HEX_LIT = /^\s*#[0-9a-f]{3,8}\s*$/i
3541
+ const out = { ok: true, total: found.size, ours: [], derived: [], literalChromatic: [], literalGray: [] }
3542
+ for (const [name, value] of found) {
3543
+ if (ours.has(name)) { out.ours.push(name); continue }
3544
+ if (value.indexOf('var(') !== -1) { out.derived.push(name); continue }
3545
+ if (HEX_LIT.test(value)) {
3546
+ const hex = value.trim().slice(1)
3547
+ const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex
3548
+ const r = parseInt(full.slice(0, 2), 16), g = parseInt(full.slice(2, 4), 16), b = parseInt(full.slice(4, 6), 16)
3549
+ if (Math.abs(r - g) <= 2 && Math.abs(g - b) <= 2 && Math.abs(r - b) <= 2) out.literalGray.push(name)
3550
+ else out.literalChromatic.push(name)
3551
+ continue
3552
+ }
3553
+ out.derived.push(name) // rgb()/oklch() 等非 var 引用:归「其它声明」,暂不细分
3554
+ }
3555
+ out.counts = { total: out.total, ours: out.ours.length, derived: out.derived.length, literalChromatic: out.literalChromatic.length, literalGray: out.literalGray.length }
3556
+ return out
3557
+ }
3374
3558
  const previewConfigCode = (code) => {
3375
3559
  const cfg = decodeConfig(code)
3376
3560
  return cfg ? { ok: true, config: cfg } : { ok: false }
@@ -3383,7 +3567,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3383
3567
  */
3384
3568
  const getContrastReport = () => {
3385
3569
  const scheme = SCHEMES.find((s) => s.id === current) || SCHEMES[0]
3386
- const hc = hcMode === 'on'
3570
+ const hc = effectiveHc()
3387
3571
  const t = buildTokens(scheme, sidebarMode, textMode, scrollMode, controlMode, mdMode, hc)
3388
3572
  return buildContrastReport(t, hc, { mono: !scheme.tinted })
3389
3573
  }
@@ -3401,12 +3585,43 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3401
3585
  applyAll('custom:reset')
3402
3586
  emitCustom()
3403
3587
  }
3588
+ /* 取色拖动的帧合并(审计 2026-10-27 P2-4):取色器 input 事件在拖动中每帧来多次,
3589
+ * 每次都重放覆盖层会让宿主连续做全页样式重写(约 200 个 token)+ 样式回读。
3590
+ * 同帧多次取色合并到下一帧只应用一次;离散动作(重置/派生/导入)不走这里,保持同步。 */
3591
+ let customFrameQueued = false
3592
+ const scheduleCustomApply = (reason) => {
3593
+ if (customFrameQueued) return
3594
+ customFrameQueued = true
3595
+ const run = () => {
3596
+ customFrameQueued = false
3597
+ applyAll(reason)
3598
+ emitCustom()
3599
+ }
3600
+ try { requestAnimationFrame(run) } catch (err) {
3601
+ try { setTimeout(run, 0) } catch (err2) { run() }
3602
+ }
3603
+ }
3404
3604
  const setCustomColor = (field, hex) => {
3405
3605
  if (CUSTOM_FIELDS.indexOf(field) === -1) return false
3406
3606
  if (typeof hex !== 'string' || !HEX_RE.test(hex)) return false
3407
3607
  const next = Object.assign({}, readCustom(), { [field]: hex.toLowerCase() })
3408
3608
  writeCustom(next)
3409
- applyAll('custom:' + field)
3609
+ scheduleCustomApply('custom:' + field)
3610
+ return true
3611
+ }
3612
+ /** 以当前主色为唯一输入,重算底色与文字色(1.10.0,审计建议 4)。 */
3613
+ const deriveFromAccent = () => {
3614
+ const pref = (() => {
3615
+ try {
3616
+ const p = theme.getTheme().preference
3617
+ if (p === 'dark' || p === 'light') return p
3618
+ } catch (err) { /* 读不到偏好就按页面实际明暗判断 */ }
3619
+ try { return document.body.hasAttribute('data-ds-dark-theme') ? 'dark' : 'light' } catch (err) { return 'light' }
3620
+ })()
3621
+ const next = deriveCustomFromAccent(readCustom().accent, pref)
3622
+ if (!next) return false
3623
+ writeCustom({ bg: next.bg, text: next.text, accent: next.accent })
3624
+ applyAll('custom:derive')
3410
3625
  emitCustom()
3411
3626
  return true
3412
3627
  }
@@ -3477,6 +3692,14 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3477
3692
  window.addEventListener('pageshow', healOnShow)
3478
3693
  } catch (err) { /* 无 DOM 时跳过 */ }
3479
3694
  try { ctx.on('theme/change', healOnTheme) } catch (err) { /* 事件名变了就只靠定时器 */ }
3695
+ // auto 档:系统对比度偏好一变就重算(开 → 高对比档,关 → 回标准档)。
3696
+ // 老浏览器没有 addEventListener 就退化为「下次任意重算时跟进」。
3697
+ try {
3698
+ if (contrastMq && typeof contrastMq.addEventListener === 'function') {
3699
+ const onContrastChange = () => { applyAll('prefers-contrast'); emitHc() }
3700
+ contrastMq.addEventListener('change', onContrastChange)
3701
+ }
3702
+ } catch (err) { /* 无 matchMedia 环境跳过 */ }
3480
3703
  try {
3481
3704
  healTimer = setInterval(() => {
3482
3705
  // 页面不可见时不做样式回读:容器里长时间后台驻留时,4 秒一次 getComputedStyle 是纯浪费
@@ -3549,6 +3772,7 @@ body[data-ds-dark-theme] .dsh-tp{color-scheme:dark}
3549
3772
  dismissNotice,
3550
3773
  subscribeCustom,
3551
3774
  setCustomColor,
3775
+ deriveFromAccent,
3552
3776
  resetCustom,
3553
3777
  getLangPref,
3554
3778
  getLocale,
package/docs/DESIGN.md CHANGED
@@ -17,7 +17,7 @@
17
17
  正文对比度:浅色承诺面(`00`/`60`)13.50–15.57(目标 12:1),深色承诺面(`950`/`875`)10.13–12.05(目标 10:1)。
18
18
  深色 `800`(模块面)与 `850`(次级深面)**不为正文兜底** —— 报表把它们按「政策外」单列(既不算达标也不算失败):
19
19
  实测 6.20–9.70,物理上限 10.22–15.45,**AA 底线与 APCA 底线仍须过**(见下节)。
20
- 三级与链接的浅色承诺目标都是 4.62(HC 7),深色分别是 **6.4**(HC 7.6)与 **6.4**(HC 7.6)。
20
+ 三级与链接的浅色承诺目标是 **4.85 / 4.95**(HC 7;4.62 的旧值在色觉缺陷模拟下会掉到 AA 以下,见「文字对比度在 CVD 下也守 AA」),深色分别是 **6.4**(HC 7.6)与 **6.4**(HC 7.6)。
21
21
  链接另加一道 APCA 门(深色 Lc ≥60 / HC ≥75)—— 它常在 `800` 模块面上出现,而 4.62 在深色 `800` 上
22
22
  只相当于 Lc≈48,见「链接」一节。
23
23
 
@@ -118,7 +118,7 @@
118
118
  | **物理上限** | 该承载面上黑或白前景能达到的对比度 | 纯计算,**不含**求解余量 |
119
119
  | **政策豁免** | 我们不承诺该面承载正文(深色 `800` / `850`) | 产品策略 |
120
120
 
121
- - 承诺目标(浅色):正文 **12:1**、次级 **6.2**、三级 **4.62**、链接 **4.62**(高对比 15 / 9 / 7 / 7)。
121
+ - 承诺目标(浅色):正文 **12:1**、次级 **6.2**、三级 **4.85**、链接 **4.95**(高对比 15 / 9 / 7 / 7;后两项比 1.9.x 各抬了一档,见 CVD 一节)。
122
122
  - 承诺目标(深色):正文 **10:1**、次级 **8.0**、三级 **6.4**、链接 **6.4**(高对比 15 / 9.6 / 7.6 / 7.6)。
123
123
  深色侧单独一套更高的下限,理由见「为什么深色要更高的比值」。
124
124
  - 承诺面:浅色 **00/60**,深色 **950/875**。
@@ -127,6 +127,11 @@
127
127
  - 物理上限只由计算得出。求解器里的 **0.93** 只是**求解余量**,仅用于判断「目标是否高于上限」,不混进上限本身。
128
128
  - **底线是硬的**:政策外的面也不许掉到 AA 4.5 以下;深色侧还要同时满足 APCA 的 Lc 下限
129
129
  (正文 60 / 次级 60 / 三级 45,实测已在 61.9–68.5)。
130
+ - **注释层(`label-caption`,1.11.0)**:不在承诺模型的角色表里,但深色侧同样过 Lc 闸 ——
131
+ 标准档 **Lc 60**(与链接同线)、HC 档 **70**(HC 三级文字 Lc 落在 72–77,注释套链接的 75 会反超三级、
132
+ 破坏「注释弱于三级」的层级)。抬完标准档注释 WCAG 约 5.6–6.0,仍低于三级的 6.4;`tools/check-audit.mjs` 第 4 组守着。
133
+ - **装饰层(`label-dimmed` / `label-document-preview`)**:按「装饰性文字」解到 **3.0**,刻意低于所有可读承诺 ——
134
+ 宿主把它们用于占位/预览这类辅助场景;不在 `CONTRAST_ROLES` 承诺模型内,也不进报表(有意的政策外)。
130
135
  - 三个出口共用同一份数据:设置页 UI、报表 CLI(`palette-dev.mjs`)、审计断言(`tools/check-audit.mjs`)。
131
136
  单元格公开 `measured` / `declaredTarget` / `physicalCeiling` / `policyApplicable` / `limitedByPhysics` /
132
137
  `meetsFloor` / `apca` / `pass`;政策外的 `pass` 为 `null`(不算达标也不算失败)。
@@ -249,6 +254,63 @@ WCAG 2.x 的相对亮度比在暗底上**系统性高估**可读性。同一个
249
254
 
250
255
  三套预置在 achromat(全色盲)下的层间明度差仍保留(ΔE 3.7–5.9),层级不依赖色度。
251
256
 
257
+ ### 文字对比度在 CVD 下也守 AA(1.10.0)
258
+
259
+ 文字与承载面的对比度由**明度差**决定,配色只改色度与色相 —— 所以色觉缺陷理论上不该影响读数。
260
+ `tools/check-audit.mjs` 第 20 组把这个「理论上」变成实测:四套配色 × 明暗 × 正文/次级/三级/链接 ×
261
+ 三个承载面 × deutan / protan / tritan(Machado 2009 线性矩阵),断言最低值 ≥ 4.5。
262
+
263
+ 这条断言当初**没过**,而且原因不在 CVD 本身:三级与链接的浅色下限原本就压在 **4.62**(刚过 AA、
264
+ 一点余量不留),而模拟会把同一对前景/背景的比值上下推动 5–9% —— 链接实测跌到 **4.22–4.34**,
265
+ 比 AA 还低。修法是把浅色下限抬到三级 **4.85**、链接 **4.95**,把这段浮动留给模型误差;
266
+ 抬完三种模拟下的最低值是 **4.61**。代价只是浅色链接与三级文字比 1.9.x 略深一档。
267
+
268
+ ## 三项「少让人做决定」的机制(1.10.0)
269
+
270
+ ### 高对比:跟随系统(`auto`)
271
+
272
+ 高对比档原有「关 / 开」两态,现在加 `auto` 并设为默认:系统开了「提高对比度」
273
+ (`prefers-contrast: more`)就自动用高对比档,关掉就回标准档。`matchMedia` 的 `change` 事件一响
274
+ 就重算(`applyAll('prefers-contrast')`),不用刷新页面。
275
+ 手动选过「关 / 开」的用户读回的是自己的选择 —— 升级不会把他们的档位改掉;
276
+ 只有从没碰过这一项的用户落到 `auto`。
277
+
278
+ ### 自定义配色:由主色派生底色与文字
279
+
280
+ 用户往往只有一个「想要的品牌色」,让他自己配纸面与墨色既难也容易把对比度配坏。
281
+ 设置页的自定义面板多了一个「由主色派生」:拿当前强调色的**色相**、色度的 15%–22%(夹上限)
282
+ 推出纸面与墨色,明度按当前明暗模式取(亮 0.968 / 0.235,暗 0.165 / 0.935)——
283
+ 这几个值是引擎已验证过的区间,派生结果直接过全部下限(实测正文 14.6–15.0、次级 6.8–6.9、
284
+ 三级 5.3–5.4、链接 5.4–5.5)。主色无彩(C < 0.004)时派生中性纸面,
285
+ 避免「黑底 + 白字 + 黄强调派生出一套淡粉」那类老问题。
286
+
287
+ ### 运行时覆盖报告(`getCoverageReport`)
288
+
289
+ 诊断导出(设置页「复制诊断信息」)多一份 `coverage`:遍历宿主样式表里所有 `--dsw-*` 自定义属性,
290
+ 按四类分账 —— `ours`(本插件覆盖层接管)、`derived`(值是 `var(--dsw-…)` 引用,跟着基础 token 走)、
291
+ `literalChromatic`(字面带彩色值,宿主语义色等,有意不染)、`literalGray`(字面灰值,是「跟配色」
292
+ 的候选)。它回答的是「宿主还有多少 token 没跟上配色」,只读、尽力而为:
293
+ 拿不到 `styleSheets`(沙箱、跨域)就返回 `{ ok: false }`,不影响其它诊断。
294
+
295
+ ## 宽色域(display-P3):本期不做(1.10.0)
296
+
297
+ sRGB 之外还有 P3 的色度余量,问题是「同一个 L/h 下 P3 能多给多少色度」以及「这些色度用不用得上」。
298
+ 用 OKLCH → XYZ → sRGB / P3 的数值求逆量了各档位(脚本在审计目录 `probe-p3-headroom-v2.mjs`):
299
+
300
+ | 档位 | 现用色度 C | sRGB 上限 | P3 上限 | 可加 ΔC | ΔE |
301
+ |---|---|---|---|---|---|
302
+ | 画布 00(暖砂 / 靛蓝 / 苔绿) | 0.0045 / 0.0034 / 0.0061 | 0.0099 / 0.0080 / 0.0289 | 0.0123 / 0.0088 / 0.0317 | 0.0025 / 0.0008 / 0.0028 | 0.25 / 0.08 / 0.28 |
303
+ | 卡片 60 | 0.0107 / 0.0074 / 0.0114 | 0.0209 / 0.0132 / 0.0605 | 0.0262 / 0.0145 / 0.0658 | 0.0053 / 0.0013 / 0.0052 | 0.53 / 0.13 / 0.52 |
304
+ | 内容面 150 | 0.0236 / 0.0139 / 0.0249 | 0.0898 / 0.0385 / 0.1697 | 0.1115 / 0.0422 / 0.1880 | 0.0217 / 0.0037 / 0.0183 | 2.17 / 0.37 / 1.83 |
305
+ | 强调 500 | 0.1013 / 0.1071 / 0.0962 | 0.1369 / 0.2001 / 0.1684 | 0.1566 / 0.2530 / 0.1980 | 0.0196 / 0.0529 / 0.0296 | 1.96 / 5.29 / 2.96 |
306
+ | 正文 1000 | 0.0127 / 0.0120 / 0.0105 | 0.0532 / 0.1548 / 0.0529 | 0.0616 / 0.1558 / 0.0727 | 0.0084 / 0.0009 / 0.0198 | 0.84 / 0.09 / 1.98 |
307
+
308
+ 结论:**纸面层(00/60)的 P3 余量在 JND 以下**(ΔE 0.08–0.53),用不用看不出来;
309
+ 真正能看出差别的是强调色(indigo ΔE 5.29、moss 2.96)。但强调色同时是链接、info 填充与勾选支点的来源,
310
+ 一旦按 P3 抬色度,就得为 sRGB 回退再标定一套(色域裁切会改色相,链接的 AA 读数要重算)。
311
+ 这一版的取舍是**不做**,把余量记在这里:将来若要做,只值得给强调色加
312
+ `color(display-p3 …)` 一层并在 sRGB 里保持现值的双口径,纸面层不必动。
313
+
252
314
  ## 报表量的是屏幕上的字(1.9.8)
253
315
 
254
316
  报表要回答的是「屏幕上这条字看不看得清」,所以量的必须是**真正渲染出来的那个 token**,
@@ -266,8 +328,8 @@ WCAG 2.x 的相对亮度比在暗底上**系统性高估**可读性。同一个
266
328
  黑白配色不覆盖这些别名 → 全部回落色阶步进(本来就没有可测读数,报表整格记「未测量」)。
267
329
 
268
330
  **为什么必须这么量**:按步进值报,自定义配色下三级文字会被报成 **4.21**(低于 AA 4.5 的假警报),
269
- 而实际渲染的覆盖值已被求解器兜到 ≥ **4.62**。1.9.8 改完之后,全部 **2160** 组配置
270
- (黑白 + 三套预置 + 自定义共 5 套配色 × 216 组设置 × 标准/高对比两档)里,
331
+ 而实际渲染的覆盖值已被求解器兜到 ≥ **4.62**。1.9.8 改完之后,全部组合配置
332
+ (5 套配色 × 216 组设置 = **1080** 组,216 已含标准/高对比两档;1.9.10 撤右栏档前是 2160 组)里,
271
333
  低于 AA 4.5 底线的格子 = **0**,硬失败(pass=false)也是 **0**。
272
334
 
273
335
  **深色压暗面的修正**:只按 `950`/`875` 压暗时,暖砂标准档的三级文字在 `800`(模块面)上只有 **3.79**、
package/package.json CHANGED
@@ -1,114 +1,116 @@
1
- {
2
- "name": "dsh-theme-palette",
3
- "version": "1.9.10",
4
- "description": "DSH 主题配色:4 套配色 × 明暗两模式,OKLCH 色域安全重建 + 真实承载面对比度求解,可调强度与面板表面。",
5
- "type": "module",
6
- "main": "index.js",
7
- "exports": {
8
- ".": "./index.js",
9
- "./client": "./client.js",
10
- "./client.js": "./client.js",
11
- "./index.js": "./index.js",
12
- "./cordis.patch.yml": "./cordis.patch.yml",
13
- "./locale/*.json": "./locale/*.json",
14
- "./icon": "./icon.svg",
15
- "./package.json": "./package.json"
16
- },
17
- "files": [
18
- "index.js",
19
- "client.js",
20
- "cordis.patch.yml",
21
- "icon.svg",
22
- "README.md",
23
- "LICENSE",
24
- "CHANGELOG.md",
25
- "docs/DESIGN.md",
26
- "README-zh.md",
27
- "README-es.md",
28
- "README-pt.md",
29
- "README-hi.md",
30
- "locale"
31
- ],
32
- "peerDependencies": {
33
- "@deepseek-ai/cordis": "^4.0.0",
34
- "@deepseek-ai/dsh-client-ui-theme": "^0.2.1-rc.1",
35
- "@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.1"
36
- },
37
- "engines": {
38
- "node": "^22.19.0 || >=24.0.0",
39
- "dsh": ">=0.2.1-rc.1"
40
- },
41
- "scripts": {
42
- "check": "node tools/check-audit.mjs && node tools/check-custom.mjs && node tools/check-ui.mjs && node tools/check-host-contract.mjs && node tools/check-selfheal.mjs",
43
- "report": "node palette-dev.mjs && node palette-dev.mjs --hc",
44
- "packs": "node tools/export-theme-pack.mjs"
45
- },
46
- "dsh": {
47
- "bundle": {
48
- "patch": "./cordis.patch.yml"
49
- },
50
- "client": {
51
- "platform": "web",
52
- "immediately": true,
53
- "inject": [
54
- "@deepseek-ai/dsh-client-ui-theme",
55
- "@deepseek-ai/dsh-client-ui-settings"
56
- ]
57
- },
58
- "compatibility": {
59
- "dsh": ">=0.2.1-rc.1",
60
- "dshReleases": {
61
- "0.2.1-rc.2": "compatible"
62
- },
63
- "dshOperations": {
64
- "0.2.1-rc.2": {
65
- "install": "passed",
66
- "start": "passed",
67
- "uninstall": "unknown",
68
- "rollback": "unknown"
69
- }
70
- },
71
- "profiles": [
72
- "desktop",
73
- "web"
74
- ]
75
- }
76
- },
77
- "keywords": [
78
- "dsh",
79
- "dsh-plugin",
80
- "deepseek-harness",
81
- "theme",
82
- "palette",
83
- "oklch",
84
- "dark-mode"
85
- ],
86
- "license": "MIT",
87
- "repository": {
88
- "type": "git",
89
- "url": "git+https://github.com/zuoyunlai/dsh-theme-palette.git"
90
- },
91
- "homepage": "https://github.com/zuoyunlai/dsh-theme-palette#readme",
92
- "bugs": {
93
- "url": "https://github.com/zuoyunlai/dsh-theme-palette/issues"
94
- },
95
- "publishConfig": {
96
- "access": "public",
97
- "registry": "https://registry.npmjs.org"
98
- },
99
- "packageManager": "pnpm@11.7.0",
100
- "peerDependenciesMeta": {
101
- "@deepseek-ai/cordis": {
102
- "optional": true
103
- },
104
- "@deepseek-ai/dsh-client-ui-theme": {
105
- "optional": true
106
- },
107
- "@deepseek-ai/dsh-client-ui-settings": {
108
- "optional": true
109
- }
110
- },
111
- "devDependencies": {
112
- "semver": "^7.7.3"
113
- }
114
- }
1
+ {
2
+ "name": "dsh-theme-palette",
3
+ "version": "1.11.0",
4
+ "description": "DSH 主题配色:4 套配色 × 明暗两模式,OKLCH 色域安全重建 + 真实承载面对比度求解,可调强度与面板表面。",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./client.js",
10
+ "./client.js": "./client.js",
11
+ "./index.js": "./index.js",
12
+ "./cordis.patch.yml": "./cordis.patch.yml",
13
+ "./locale/*.json": "./locale/*.json",
14
+ "./icon": "./icon.svg",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "index.js",
19
+ "client.js",
20
+ "assets",
21
+ "cordis.patch.yml",
22
+ "icon.svg",
23
+ "README.md",
24
+ "LICENSE",
25
+ "CHANGELOG.md",
26
+ "docs/DESIGN.md",
27
+ "README-zh.md",
28
+ "README-es.md",
29
+ "README-pt.md",
30
+ "README-hi.md",
31
+ "locale"
32
+ ],
33
+ "peerDependencies": {
34
+ "@deepseek-ai/cordis": "^4.0.0",
35
+ "@deepseek-ai/dsh-client-ui-theme": "^0.2.1-rc.1",
36
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.1"
37
+ },
38
+ "engines": {
39
+ "node": "^22.19.0 || >=24.0.0",
40
+ "dsh": ">=0.2.1-rc.1"
41
+ },
42
+ "scripts": {
43
+ "check": "node tools/check-audit.mjs && node tools/check-custom.mjs && node tools/check-ui.mjs && node tools/check-host-contract.mjs && node tools/check-selfheal.mjs && node tools/check-colormath.mjs",
44
+ "report": "node palette-dev.mjs && node palette-dev.mjs --hc",
45
+ "packs": "node tools/export-theme-pack.mjs",
46
+ "shots": "node tools/shoot-settings.mjs"
47
+ },
48
+ "dsh": {
49
+ "bundle": {
50
+ "patch": "./cordis.patch.yml"
51
+ },
52
+ "client": {
53
+ "platform": "web",
54
+ "immediately": true,
55
+ "inject": [
56
+ "@deepseek-ai/dsh-client-ui-theme",
57
+ "@deepseek-ai/dsh-client-ui-settings"
58
+ ]
59
+ },
60
+ "compatibility": {
61
+ "dsh": ">=0.2.1-rc.1",
62
+ "dshReleases": {
63
+ "0.2.1-rc.2": "compatible"
64
+ },
65
+ "dshOperations": {
66
+ "0.2.1-rc.2": {
67
+ "install": "passed",
68
+ "start": "passed",
69
+ "uninstall": "unknown",
70
+ "rollback": "unknown"
71
+ }
72
+ },
73
+ "profiles": [
74
+ "desktop",
75
+ "web"
76
+ ]
77
+ }
78
+ },
79
+ "keywords": [
80
+ "dsh",
81
+ "dsh-plugin",
82
+ "deepseek-harness",
83
+ "theme",
84
+ "palette",
85
+ "oklch",
86
+ "dark-mode"
87
+ ],
88
+ "license": "MIT",
89
+ "repository": {
90
+ "type": "git",
91
+ "url": "git+https://github.com/zuoyunlai/dsh-theme-palette.git"
92
+ },
93
+ "homepage": "https://github.com/zuoyunlai/dsh-theme-palette#readme",
94
+ "bugs": {
95
+ "url": "https://github.com/zuoyunlai/dsh-theme-palette/issues"
96
+ },
97
+ "publishConfig": {
98
+ "access": "public",
99
+ "registry": "https://registry.npmjs.org"
100
+ },
101
+ "packageManager": "pnpm@11.7.0",
102
+ "peerDependenciesMeta": {
103
+ "@deepseek-ai/cordis": {
104
+ "optional": true
105
+ },
106
+ "@deepseek-ai/dsh-client-ui-theme": {
107
+ "optional": true
108
+ },
109
+ "@deepseek-ai/dsh-client-ui-settings": {
110
+ "optional": true
111
+ }
112
+ },
113
+ "devDependencies": {
114
+ "semver": "^7.7.3"
115
+ }
116
+ }