dsh-theme-palette 1.6.0 → 1.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 +26 -0
- package/README-zh.md +4 -3
- package/client.js +482 -93
- package/docs/DESIGN.md +14 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@
|
|
|
4
4
|
|
|
5
5
|
## 版本一览
|
|
6
6
|
|
|
7
|
+
### 1.7.0
|
|
8
|
+
依据 2026-10-23 审计反哺(对 DSH 0.2.1-rc.2 实际 token 与承载面核实):
|
|
9
|
+
|
|
10
|
+
- **A01 极端强调色**:`accentL` 钳位到 0.30–0.64;链接求解器改为双向搜索并对 00/60/150 同时达标;
|
|
11
|
+
新增 `--dsw-alias-button-info-fill` 覆盖(与主按钮一样按白字 4.5 压暗),不再出现白底白链/白勾
|
|
12
|
+
- **A02 自定义色订阅**:`readCustom` 改为按存储内容缓存的冻结快照,新增 `subscribeCustom`;
|
|
13
|
+
取色/重置/导入会更换引用并触发重渲染,空闲时快照稳定(`useSyncExternalStore` 语义)
|
|
14
|
+
- **A03 分享完整**:`dshs1.` 短码新增 `x`(三色),旧码仍可导入并提示未含自定义色;
|
|
15
|
+
`applyConfigCode` 返回 `{ok,hasCustom}`,非法码不部分写入
|
|
16
|
+
- **A04 承载面求解**:深色正文(50)、次级(300)、caption(500)补上求解;目标按承载面可达上限
|
|
17
|
+
钳位(0.93 余量),`palette-dev` 打印「实际/目标/可达」并标注「表面上限」,不再虚报 12/15;
|
|
18
|
+
caption 注释里的 6.5:1 更正为实测值
|
|
19
|
+
- **A05 文字色相**:自定义文字色相进入 700/750/1000/50 档(此前只取色度)
|
|
20
|
+
- **A06 HEX 校验**:改为 draft + 提交,非法值显示 `aria-invalid` 与行内错误,Enter 提交、Esc 回滚
|
|
21
|
+
- **A07 键盘模型**:分段控件 roving tabindex + 方向键/Home/End
|
|
22
|
+
- **A08 标签结构**:自定义色改为 `role=group` + 色盘/HEX 两个独立 label
|
|
23
|
+
- **A09 健康检测**:探针比对期望值(支持 `#rgb/#rrggbb/rgb()/rgba()`),报 `mismatched`;mono 覆盖更多 token
|
|
24
|
+
- **A10 测试门**:`palette-dev.mjs` 改用生产 `floors()`,支持 `--hc`,硬失败退出码 1
|
|
25
|
+
- **A11 边界**:grid/swatches 改 `minmax(min(100%,N),1fr)`;说明 `_markdown_`/`.md-code-block`
|
|
26
|
+
在当前宿主构建 CSS 中仍存在(属契约风险而非失效);ramp 缓存上限 48
|
|
27
|
+
- **建议落地**:自定义面板新增**解释性校正**(强调色归一化 / 链接压暗,带实际对比度);
|
|
28
|
+
导入改为**先预览后确认**(预览不写存储);新增按版本一次的**升级提示**(`notice.v1`,不碰配色键;
|
|
29
|
+
仅升级前已有配置的用户可见);高对比从「更多选项」移到阅读区;新增 `tools/check-host-contract.mjs`
|
|
30
|
+
契约测试与 `tools/check-ui.mjs` 结构回归,并把它们接入 `npm run check` 与发版预检
|
|
31
|
+
- 新增 `tools/check-audit.mjs`:覆盖极端色、分享往返、文字色相、caption、深色正文可达上限、HC 卡层
|
|
32
|
+
|
|
7
33
|
### 未发版 · 发版预检补强
|
|
8
34
|
- `tools/release.mjs` 新增 **engines.dsh 兼容性预检**:按宿主 app-boot 的同一口径
|
|
9
35
|
(`semver.satisfies(v, range, { includePrerelease: true })`)计算,守住两条底线——
|
package/README-zh.md
CHANGED
|
@@ -24,8 +24,9 @@ DeepSeek Harness(Web / 桌面)的主题配色插件:**4 套配色 × 明
|
|
|
24
24
|
- 色阶**整体重算**而不是替换:OKLCH 重建 → 角色化色度 → **sRGB 色域钳位** → 在真实承载面上求解对比度;
|
|
25
25
|
- 覆盖 27,000+ 处 token 引用:正文、次级、三级、注释、链接、侧栏、卡片、主人气泡、主按钮、滚动条、开关、Markdown 标题/加粗、代码块;
|
|
26
26
|
- 可调:文字着色三档、侧栏三档、右栏两档、Markdown 强调、滚动条、开关配色;
|
|
27
|
-
- **自定义配色**(给 底色/文字/强调
|
|
27
|
+
- **自定义配色**(给 底色/文字/强调 三色,文字色相也参与派生;过亮/过暗的强调色会归一化)、**配置导入导出**(`dshs1.` 短码含三色)、**高对比**档、4 种界面语言;
|
|
28
28
|
- **自愈**:宿主丢弃 override 层时自动重放;
|
|
29
|
+
- 高对比的 15:1 以浅色卡层/深色 950 为承诺面;深色模块面 800 受表面亮度限制,报表会标“表面上限”。
|
|
29
30
|
- 界面支持 简体中文 / 繁體中文 / English / 日本語(跟随宿主,也可手选)。
|
|
30
31
|
|
|
31
32
|
## Install
|
|
@@ -68,8 +69,8 @@ OKLCH(L, C, hue) ──► 色域钳位(对 C 二分) ──► sRGB hex
|
|
|
68
69
|
|
|
69
70
|
- 语义别名保留 `var(--dsw-static-neutral-bluish-NN)` 引用链,跟 token 的元素就跟着配色走;
|
|
70
71
|
- 浅色面用色阶 00–300,深色面用 800–950;
|
|
71
|
-
- 文字下限在构建期强制:正文 ≥ 12、次级 ≥ 6.
|
|
72
|
-
- `node palette-dev.mjs`
|
|
72
|
+
- 文字下限在构建期强制:正文 ≥ 12、次级 ≥ 6.2、三级/链接 ≥ 4.62、侧栏内 ≥ 7、气泡内 ≥ 7;深色模块面按可达上限钳位并标注;
|
|
73
|
+
- `node palette-dev.mjs` 跑标准档承载面报表,`--hc` 跑高对比档;硬失败时退出码 1。
|
|
73
74
|
|
|
74
75
|
逐组件设计取舍与对比度表见 [docs/DESIGN.md](docs/DESIGN.md),版本记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
75
76
|
|
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.
|
|
30
|
+
const PLUGIN_VERSION = '1.7.0'
|
|
31
31
|
const STORE_KEY = 'dsh-theme-palette.v1'
|
|
32
32
|
const OVERRIDE_SOURCE = 'dsh-theme-palette'
|
|
33
33
|
|
|
@@ -141,20 +141,50 @@ window.__ModuleLoader__.load({
|
|
|
141
141
|
const CUSTOM_DEFAULT = { bg: '#f5efe6', text: '#1d1a17', accent: '#a9683e' }
|
|
142
142
|
const HEX_RE = /^#[0-9a-f]{6}$/i
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 自定义色的**不可变快照**。
|
|
146
|
+
*
|
|
147
|
+
* useSyncExternalStore 要求 getSnapshot 在数据未变时返回同一引用,否则每次渲染都会
|
|
148
|
+
* 得到新对象并陷入重渲染;但也不能让快照永远不变 —— 取色/重置/导入都必须换新对象。
|
|
149
|
+
* 所以这里缓存一份冻结对象,写入时替换缓存。
|
|
150
|
+
*/
|
|
151
|
+
let customCache = null
|
|
152
|
+
// undefined = 内存里的写入没落盘(隐私模式),缓存仍然有效;字符串 = 与存储内容对应
|
|
153
|
+
let customCacheRaw
|
|
154
|
+
|
|
144
155
|
function readCustom() {
|
|
156
|
+
let raw = null
|
|
157
|
+
try { raw = localStorage.getItem(CUSTOM_KEY) } catch (err) { /* 读取失败按空处理 */ }
|
|
158
|
+
if (customCache !== null && (customCacheRaw === undefined || raw === customCacheRaw)) return customCache
|
|
145
159
|
const out = Object.assign({}, CUSTOM_DEFAULT)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (raw) {
|
|
160
|
+
if (raw) {
|
|
161
|
+
try {
|
|
149
162
|
const parsed = JSON.parse(raw)
|
|
150
|
-
for (const k of CUSTOM_FIELDS) if (parsed && typeof parsed[k] === 'string' && HEX_RE.test(parsed[k])) out[k] = parsed[k]
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
163
|
+
for (const k of CUSTOM_FIELDS) if (parsed && typeof parsed[k] === 'string' && HEX_RE.test(parsed[k])) out[k] = parsed[k].toLowerCase()
|
|
164
|
+
} catch (err) { /* 退回默认 */ }
|
|
165
|
+
}
|
|
166
|
+
customCache = Object.freeze(out)
|
|
167
|
+
customCacheRaw = raw
|
|
168
|
+
return customCache
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
function writeCustom(next) {
|
|
157
|
-
|
|
172
|
+
const clean = {}
|
|
173
|
+
for (const k of CUSTOM_FIELDS) {
|
|
174
|
+
const v = next && typeof next[k] === 'string' && HEX_RE.test(next[k]) ? next[k].toLowerCase() : CUSTOM_DEFAULT[k]
|
|
175
|
+
clean[k] = v
|
|
176
|
+
}
|
|
177
|
+
const serialized = JSON.stringify(clean)
|
|
178
|
+
// 值没变就保持同一引用:useSyncExternalStore 的快照在数据未变时必须稳定
|
|
179
|
+
const unchanged = customCache !== null && CUSTOM_FIELDS.every((k) => customCache[k] === clean[k])
|
|
180
|
+
try {
|
|
181
|
+
localStorage.setItem(CUSTOM_KEY, serialized)
|
|
182
|
+
customCacheRaw = serialized
|
|
183
|
+
} catch (err) {
|
|
184
|
+
customCacheRaw = undefined
|
|
185
|
+
}
|
|
186
|
+
if (!unchanged) customCache = Object.freeze(clean)
|
|
187
|
+
return customCache
|
|
158
188
|
}
|
|
159
189
|
|
|
160
190
|
function customScheme(colors) {
|
|
@@ -166,10 +196,14 @@ window.__ModuleLoader__.load({
|
|
|
166
196
|
id: 'custom', label: '自定义', desc: '三色派生', tinted: true, custom: true,
|
|
167
197
|
hue: bg[2], darkHue: bg[2],
|
|
168
198
|
lightC: clampN(bg[1] * 0.9, 0.008, 0.045), midC: 0.010,
|
|
169
|
-
|
|
199
|
+
// 文字色相必须真的来自「文字」这一项:只用色度会让「同色度、不同色相」完全同效,
|
|
200
|
+
// 与 UI 上「三色的色相与色度」的承诺不符。
|
|
201
|
+
textC: clampN(tx[1], 0.002, 0.030), textHue: tx[2],
|
|
170
202
|
darkC: clampN(bg[1] * 0.75, 0.006, 0.035), darkLift: 0.030,
|
|
171
|
-
paperC: clampN(tx[1], 0.004, 0.030),
|
|
172
|
-
|
|
203
|
+
paperC: clampN(tx[1], 0.004, 0.030), paperHue: tx[2],
|
|
204
|
+
// accentL 必须钳位:500 档同时是 --dsw-alias-link、button-info-fill 与勾选支点的来源。
|
|
205
|
+
// 纯白/荧光黄会让填充与白字同色;只在极端输入时归一化,预设色不受影响。
|
|
206
|
+
accentHue: ac[2], accentC: clampN(ac[1], 0.04, 0.18), accentL: clampN(ac[0], 0.30, 0.64), accentScale: 1.0,
|
|
173
207
|
}
|
|
174
208
|
}
|
|
175
209
|
|
|
@@ -190,6 +224,8 @@ window.__ModuleLoader__.load({
|
|
|
190
224
|
: { main: MIN_MAIN, secondary: MIN_SECONDARY, tertiary: MIN_TERTIARY, link: MIN_LINK, linkDark: MIN_LINK_DARK }
|
|
191
225
|
}
|
|
192
226
|
const HC_KEY = 'dsh-theme-palette.hc.v1'
|
|
227
|
+
// 只记录“已读哪个版本”的升级提示,不参与配色,也不迁移任何用户数据
|
|
228
|
+
const NOTICE_KEY = 'dsh-theme-palette.notice.v1'
|
|
193
229
|
|
|
194
230
|
/**
|
|
195
231
|
* 区块底色:左侧栏相对主画布的深浅档位。
|
|
@@ -436,7 +472,11 @@ window.__ModuleLoader__.load({
|
|
|
436
472
|
const lifted = role === 'dark' ? L + scheme.darkLift : L + liftText
|
|
437
473
|
// 深色层单独给色相:暖砂的 88° 是「黄」,在 L≈0.22 的暗部读成橄榄绿;
|
|
438
474
|
// 换成 60°(橙棕)才是咖啡色。浅色侧仍是 88°(米白)。
|
|
439
|
-
|
|
475
|
+
// 文字档(含深色正文 paper)跟随「文字」这一项自己的色相;预设没有 textHue,行为不变。
|
|
476
|
+
const tintRole = role === 'text' || role === 'paper'
|
|
477
|
+
const hue = tintRole && scheme.textHue !== undefined
|
|
478
|
+
? scheme.textHue
|
|
479
|
+
: (role === 'dark' && scheme.darkHue !== undefined ? scheme.darkHue : scheme.hue)
|
|
440
480
|
bluish[step] = fromOklch(clamp(compressL(lifted), 0, 0.995), roleChroma(role, weight, scheme, scales), hue)
|
|
441
481
|
}
|
|
442
482
|
const neutral = {}
|
|
@@ -472,47 +512,74 @@ window.__ModuleLoader__.load({
|
|
|
472
512
|
}
|
|
473
513
|
|
|
474
514
|
/**
|
|
475
|
-
* 对比度求解:前景达不到 target
|
|
476
|
-
*
|
|
515
|
+
* 对比度求解:前景达不到 target 就沿亮度微调,**两个方向都找**(取离原色最近的可行解)。
|
|
516
|
+
*
|
|
517
|
+
* 只朝「远离背景」一个方向走会漏掉可行解:当强调色比浅色背景还亮(纯白/荧光黄),
|
|
518
|
+
* 继续往白走只会更不可读,正确解在暗的一侧。bgs 可以是多个承载面,要求同时达标。
|
|
477
519
|
*/
|
|
478
|
-
function solveForeground(hexes, step,
|
|
479
|
-
const
|
|
520
|
+
function solveForeground(hexes, step, bgs, target, cap) {
|
|
521
|
+
const list = Array.isArray(bgs) ? bgs : [bgs]
|
|
522
|
+
const maxShift = typeof cap === 'number' ? cap : 0.13
|
|
480
523
|
const [L0, C, hue] = toOklch(hexes[step])
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
524
|
+
const passes = (candidate) => list.every((bg) => contrast(candidate, bg) >= target)
|
|
525
|
+
if (passes(hexes[step])) return hexes[step]
|
|
526
|
+
const steps = Math.ceil(maxShift / 0.004)
|
|
527
|
+
for (let i = 1; i <= steps; i++) {
|
|
528
|
+
// 同距离先试暗侧:浅色模式下正文/链接的可行解绝大多数在暗侧
|
|
529
|
+
for (const dir of [-1, 1]) {
|
|
530
|
+
const L = L0 + dir * i * 0.004
|
|
531
|
+
if (L < 0 || L > 1) continue
|
|
532
|
+
const candidate = fromOklch(L, C, hue)
|
|
533
|
+
if (passes(candidate)) return candidate
|
|
534
|
+
}
|
|
489
535
|
}
|
|
490
|
-
|
|
536
|
+
// 位移上限内无解:黑/白里取「最差承载面」更高的那个,保证可读性优先
|
|
537
|
+
const score = (candidate) => Math.min(...list.map((bg) => contrast(candidate, bg)))
|
|
538
|
+
return score('#000000') >= score('#ffffff') ? '#000000' : '#ffffff'
|
|
491
539
|
}
|
|
492
540
|
|
|
493
541
|
function tuneRamps(ramps, scheme, hc) {
|
|
494
542
|
const F = floors(hc)
|
|
495
543
|
// 靶点必须用**真实承载面**,不能用画布:三级文字与链接大量落在卡层(60)、
|
|
496
|
-
//
|
|
544
|
+
// 深色模块层(800)上,只对 00/950 求解会得到「画布达标、卡层不达标」。
|
|
545
|
+
//
|
|
546
|
+
// 深色侧还要面对**物理上限**:白色对 800 面最多只有 10.1–11.0:1,
|
|
547
|
+
// 12:1(HC 15:1)不可能达成。这里按「目标与可达上限取小」求解,并留 7% 色彩余量,
|
|
548
|
+
// 由 palette-dev 报告「承诺 / 可达 / 实际」,不再假装能到 15。
|
|
497
549
|
const lightCanvas = ramps.bluish['00']
|
|
498
550
|
const lightCard = ramps.bluish['60']
|
|
499
551
|
const darkCanvas = ramps.bluish['950']
|
|
500
552
|
const darkModule = ramps.bluish['800']
|
|
553
|
+
const darkCard = ramps.bluish['850']
|
|
554
|
+
const maxOn = (bg, lightFg) => contrast(lightFg ? '#ffffff' : '#000000', bg)
|
|
555
|
+
// 0.93 而不是 0.995:贴着上限求解会把深色正文推成纯白,文字色味(paperC)全丢;
|
|
556
|
+
// 留出余量才能既达到可见的最高档,又保住「近白但有色味」的设计意图。
|
|
557
|
+
const clampTarget = (bg, target, lightFg) => Math.min(target, Math.max(1, maxOn(bg, lightFg) * 0.93))
|
|
501
558
|
const jobs = [
|
|
502
|
-
['1000', lightCanvas, F.main],
|
|
503
|
-
['700', lightCard, F.secondary],
|
|
504
|
-
['600', lightCard, F.tertiary],
|
|
505
|
-
|
|
559
|
+
['1000', [lightCard, lightCanvas], F.main, false],
|
|
560
|
+
['700', [lightCard], F.secondary, false],
|
|
561
|
+
['600', [lightCard], F.tertiary, false],
|
|
562
|
+
// 深色次级:(300) 之前从未被求解;800 是最亮的深色面,对浅色文字最难
|
|
563
|
+
['300', [darkModule, darkCard, darkCanvas], F.secondary, true],
|
|
564
|
+
['400', [darkModule], F.tertiary, true],
|
|
506
565
|
]
|
|
507
|
-
for (const [step,
|
|
508
|
-
|
|
509
|
-
ramps.bluish[step] = solveForeground(ramps.bluish, step,
|
|
566
|
+
for (const [step, bgs, target, lightFg] of jobs) {
|
|
567
|
+
const effective = bgs.reduce((acc, bg) => Math.min(acc, clampTarget(bg, target, lightFg)), target)
|
|
568
|
+
ramps.bluish[step] = solveForeground(ramps.bluish, step, bgs, effective, hc ? 0.30 : 0.13)
|
|
569
|
+
}
|
|
570
|
+
// 深色正文:以最亮的深色面(800)为求解面。它对浅色文字最难;把它抬到可达上限后,
|
|
571
|
+
// 850/875/950 会自然更高。12/15 在 800 上物理不可达,所以目标按该面上限钳位,
|
|
572
|
+
// 由 palette-dev 报告「承诺 / 可达 / 实际」,不再假装能到 15。
|
|
573
|
+
{
|
|
574
|
+
const target = Math.min(F.main, maxOn(darkModule, true) * 0.93)
|
|
575
|
+
ramps.bluish['50'] = solveForeground(ramps.bluish, '50', darkModule, target, hc ? 0.30 : 0.13)
|
|
510
576
|
}
|
|
511
577
|
// 链接按「最亮的深色承载面(800)」兜底。
|
|
512
578
|
// 注意这里**只有 400**:浅色链接不走这条色阶的中段(500 是设计稿的标准色,
|
|
513
579
|
// 不能被求解器按承载面改写),浅色链接在 aliasOverrides 里从 500 派生。
|
|
514
580
|
if (contrast(ramps.deepseek['400'], darkModule) < F.linkDark) {
|
|
515
|
-
|
|
581
|
+
const effective = clampTarget(darkModule, F.linkDark, true)
|
|
582
|
+
ramps.deepseek['400'] = solveForeground(ramps.deepseek, '400', darkModule, effective, hc ? 0.30 : 0.13)
|
|
516
583
|
}
|
|
517
584
|
return ramps
|
|
518
585
|
}
|
|
@@ -522,21 +589,29 @@ window.__ModuleLoader__.load({
|
|
|
522
589
|
*
|
|
523
590
|
* 为什么不能直接用 500:设计稿给的标准值(暖砂 `#A9683E`)在浅色悬浮面 `150` 上只有
|
|
524
591
|
* 3.46:1、画布上 4.06 —— 那是「品牌色」不是「链接色」。链接要 4.62 起,所以同色相同色度、
|
|
525
|
-
*
|
|
592
|
+
* 明度往可行的一侧收(两个方向都找),收到达标为止。
|
|
526
593
|
* 这样宿主里 500 的其它用途(info 按钮填充、焦点环、deep-diving 文字、卡片预览色)
|
|
527
594
|
* 全部保持标准值,只有「当文字用」的链接换一条。
|
|
528
595
|
*/
|
|
529
596
|
function lightLink(r, hc) {
|
|
530
597
|
const target = floors(hc).link
|
|
531
598
|
if (r === BASE) return BASE.deepseek['500'] // 黑白配色没被覆盖,走宿主原值
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
599
|
+
// 链接出现在卡层(60)、画布(00) 与悬浮层(150) 上;150 最难,要求同时达标。
|
|
600
|
+
const surfaces = [r.bluish['150'], r.bluish['60'], r.bluish['00']]
|
|
601
|
+
if (surfaces.every((bg) => contrast(r.deepseek['500'], bg) >= target)) return r.deepseek['500']
|
|
602
|
+
// 链接允许比正文移动更大:语义色必须可读,宁可偏离品牌色
|
|
603
|
+
return solveForeground(r.deepseek, '500', surfaces, target, hc ? 0.60 : 0.45)
|
|
535
604
|
}
|
|
536
605
|
|
|
537
606
|
/* ================= token 组装 ================= */
|
|
538
607
|
/** 重建结果缓存:一个配色只算一次(渲染时每张卡片都会取预览色)。 */
|
|
539
608
|
const rampCache = new Map()
|
|
609
|
+
// 自定义取色会持续产生新键;给一个小上限,避免长会话里只增不减
|
|
610
|
+
function rememberRamp(key, value) {
|
|
611
|
+
rampCache.set(key, value)
|
|
612
|
+
while (rampCache.size > 48) rampCache.delete(rampCache.keys().next().value)
|
|
613
|
+
return value
|
|
614
|
+
}
|
|
540
615
|
function scalesFor(textMode) {
|
|
541
616
|
const m = TEXT_MODES.find((x) => x.id === textMode) || TEXT_MODES[1]
|
|
542
617
|
return { textScale: m.textScale, paperScale: m.paperScale, textLift: m.textLift || 0 }
|
|
@@ -563,7 +638,7 @@ window.__ModuleLoader__.load({
|
|
|
563
638
|
scheme,
|
|
564
639
|
hc,
|
|
565
640
|
)
|
|
566
|
-
|
|
641
|
+
rememberRamp(monoKey, mono)
|
|
567
642
|
}
|
|
568
643
|
return mono
|
|
569
644
|
}
|
|
@@ -571,7 +646,7 @@ window.__ModuleLoader__.load({
|
|
|
571
646
|
let cached = rampCache.get(key)
|
|
572
647
|
if (cached === undefined) {
|
|
573
648
|
cached = tuneRamps(buildRamps(params, scalesFor(textMode)), params, hc)
|
|
574
|
-
|
|
649
|
+
rememberRamp(key, cached)
|
|
575
650
|
}
|
|
576
651
|
return cached
|
|
577
652
|
}
|
|
@@ -626,8 +701,18 @@ window.__ModuleLoader__.load({
|
|
|
626
701
|
},
|
|
627
702
|
'--dsw-alias-bg-mask-drop': { light: withAlpha(r.bluish['00'], 0.70), dark: withAlpha(r.bluish['800'], 0.70) },
|
|
628
703
|
// 注释文字:宿主浅色用 400、深色用 600。深色的 600 被「浅色三级文字要 AA」的求解压暗过,
|
|
629
|
-
//
|
|
630
|
-
|
|
704
|
+
// 直接用在深底上只有 3.x:1;这里改用 500 并对**深色模块面(800)/卡层(850)** 求解到 AA。
|
|
705
|
+
// 实测旧注释「约 6.5:1」不成立(实际 4.4–4.7),所以既改值也改注释。
|
|
706
|
+
'--dsw-alias-label-caption': {
|
|
707
|
+
light: r.bluish['400'],
|
|
708
|
+
dark: solveForeground(
|
|
709
|
+
r.bluish,
|
|
710
|
+
'500',
|
|
711
|
+
[r.bluish['800'], r.bluish['850']],
|
|
712
|
+
Math.min(floors(hc).tertiary, contrast('#ffffff', r.bluish['800']) * 0.995),
|
|
713
|
+
0.30,
|
|
714
|
+
),
|
|
715
|
+
},
|
|
631
716
|
// 主人气泡(`--dsw-specific-bubble`)宿主指向 deepseek-50:那是「品牌色的极浅底」,
|
|
632
717
|
// 我的强调色色度高,浅端直接算出 #ffede0(R 顶满 255)→ 看着发粉发红。
|
|
633
718
|
// 改成「强调色 8–10% 混画布」,气泡是有色味的浅底而不是一块彩色。
|
|
@@ -745,6 +830,12 @@ window.__ModuleLoader__.load({
|
|
|
745
830
|
light: darkenUntil(ramps.deepseek['500'], ramps.bluish['00'], 4.5),
|
|
746
831
|
dark: ramps.deepseek['400'],
|
|
747
832
|
}
|
|
833
|
+
// info 按钮同样是「强调色填充 + 白字」,宿主直接指向 deepseek-500。
|
|
834
|
+
// 极端自定义强调色下会变成浅底白字;与主按钮一样按白字 4.5 压暗。
|
|
835
|
+
pairs['--dsw-alias-button-info-fill'] = {
|
|
836
|
+
light: darkenUntil(ramps.deepseek['500'], ramps.bluish['00'], 4.5),
|
|
837
|
+
dark: ramps.deepseek['400'],
|
|
838
|
+
}
|
|
748
839
|
}
|
|
749
840
|
// 选中卡片的淡底:必须按模式给,原写法(强调色混**浅色**画布)在深色模式下
|
|
750
841
|
// 会得到一张近白卡片,而卡片文字是 label-primary(深色下近白)→ 白底白字。
|
|
@@ -862,7 +953,12 @@ window.__ModuleLoader__.load({
|
|
|
862
953
|
.dsh-tp-head h2{margin:0;font-size:18px;font-weight:600;letter-spacing:.2px}
|
|
863
954
|
.dsh-tp-badge{margin-left:auto;font:11px/1.7 ui-monospace,Consolas,monospace;color:var(--dsw-alias-label-tertiary);border:.5px solid var(--dsw-alias-border-l4);border-radius:999px;padding:0 8px}
|
|
864
955
|
.dsh-tp-sub{margin:-6px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
865
|
-
.dsh-tp-
|
|
956
|
+
.dsh-tp-notice{display:flex;align-items:flex-start;gap:10px;justify-content:space-between;padding:10px 12px;border-radius:12px;border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1)}
|
|
957
|
+
.dsh-tp-notice p{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
958
|
+
.dsh-tp-explain{margin:0;display:flex;flex-direction:column;gap:4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
959
|
+
.dsh-tp-preview{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border-radius:10px;border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1)}
|
|
960
|
+
.dsh-tp-preview p{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
961
|
+
.dsh-tp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,196px),1fr));gap:10px}
|
|
866
962
|
.dsh-tp-card{position:relative;display:flex;flex-direction:column;gap:8px;text-align:left;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;padding:9px;border-radius:14px;border:.5px solid var(--dsw-alias-border-l4);background:transparent;transition:background .12s ease,border-color .12s ease}
|
|
867
963
|
.dsh-tp-card:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
868
964
|
.dsh-tp-card:focus-visible{outline-offset:2px}
|
|
@@ -904,7 +1000,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
904
1000
|
.dsh-tp-custom{display:flex;flex-direction:column;gap:10px;padding:12px;border-radius:14px;border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1)}
|
|
905
1001
|
.dsh-tp-custom-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
906
1002
|
.dsh-tp-custom-head b{font-size:13.5px;font-weight:500}
|
|
907
|
-
.dsh-tp-swatches{display:grid;grid-template-columns:repeat(auto-fit,minmax(86px,1fr));gap:10px}
|
|
1003
|
+
.dsh-tp-swatches{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,86px),1fr));gap:10px}
|
|
908
1004
|
.dsh-tp-swatch{display:flex;flex-direction:column;gap:6px;cursor:pointer}
|
|
909
1005
|
.dsh-tp-color{width:100%;height:44px;padding:0;border:.5px solid var(--dsw-alias-border-l4);border-radius:10px;background:transparent;cursor:pointer}
|
|
910
1006
|
.dsh-tp-color::-webkit-color-swatch-wrapper{padding:3px}
|
|
@@ -913,6 +1009,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
913
1009
|
.dsh-tp-hex{font:11.5px/1 ui-monospace,Consolas,monospace;color:var(--dsw-alias-label-tertiary)}
|
|
914
1010
|
.dsh-tp-hexinput{width:100%;box-sizing:border-box;font:11.5px/16px ui-monospace,Consolas,monospace;padding:3px 6px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}
|
|
915
1011
|
.dsh-tp-hexinput:focus-visible{outline-offset:1px}
|
|
1012
|
+
.dsh-tp-hexinput[aria-invalid="true"]{border-color:var(--dsw-alias-state-error-primary)}
|
|
1013
|
+
.dsh-tp-hexerror{font-size:11.5px;line-height:16px;color:var(--dsw-alias-state-error-primary)}
|
|
916
1014
|
.dsh-tp-custom-hint{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
917
1015
|
.dsh-tp-note{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}
|
|
918
1016
|
.dsh-tp-status{margin:0;font-size:11.5px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
@@ -963,24 +1061,57 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
963
1061
|
*/
|
|
964
1062
|
const TOKEN_PROBE = [
|
|
965
1063
|
'--dsw-static-neutral-bluish-00',
|
|
1064
|
+
'--dsw-static-neutral-bluish-600',
|
|
966
1065
|
'--dsw-static-neutral-bluish-1000',
|
|
1066
|
+
'--dsw-static-deepseek-500',
|
|
967
1067
|
'--dsw-alias-link',
|
|
968
1068
|
'--dsw-specific-sidebar-fill',
|
|
969
1069
|
'--dsw-alias-label-caption',
|
|
970
1070
|
]
|
|
971
1071
|
|
|
1072
|
+
/** 只用于比对的颜色解析:支持 #rgb/#rrggbb/#rrggbbaa 与 rgb()/rgba()。 */
|
|
1073
|
+
function parseColor(value) {
|
|
1074
|
+
const s = String(value).trim().toLowerCase()
|
|
1075
|
+
if (s.charAt(0) === '#') {
|
|
1076
|
+
if (s.length === 4) return [parseInt(s[1], 16) * 17, parseInt(s[2], 16) * 17, parseInt(s[3], 16) * 17, 1]
|
|
1077
|
+
if (s.length === 7) return [parseInt(s.slice(1, 3), 16), parseInt(s.slice(3, 5), 16), parseInt(s.slice(5, 7), 16), 1]
|
|
1078
|
+
if (s.length === 9) return [parseInt(s.slice(1, 3), 16), parseInt(s.slice(3, 5), 16), parseInt(s.slice(5, 7), 16), parseInt(s.slice(7, 9), 16) / 255]
|
|
1079
|
+
}
|
|
1080
|
+
const m = s.match(/^rgba?\(([^)]+)\)$/)
|
|
1081
|
+
if (m) {
|
|
1082
|
+
const p = m[1].split(/[,\s/]+/).filter(Boolean).map((x) => (x.charAt(x.length - 1) === '%' ? parseFloat(x) * 2.55 : parseFloat(x)))
|
|
1083
|
+
return [p[0], p[1], p[2], p[3] === undefined ? 1 : p[3]]
|
|
1084
|
+
}
|
|
1085
|
+
return null
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function sameColor(actual, expected) {
|
|
1089
|
+
const a = parseColor(actual)
|
|
1090
|
+
const b = parseColor(expected)
|
|
1091
|
+
if (!a || !b) return String(actual).trim().toLowerCase() === String(expected).trim().toLowerCase()
|
|
1092
|
+
return Math.abs(a[0] - b[0]) <= 1 && Math.abs(a[1] - b[1]) <= 1 && Math.abs(a[2] - b[2]) <= 1 && Math.abs(a[3] - b[3]) <= 0.02
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* 不只验证「非空」:宿主原始 token 本来就有值,覆盖层丢失时非空检查会报假健康。
|
|
1097
|
+
* 这里比对计算值与本层期望值(var()/inherit 这类间接值跳过),并把不一致的 token 报出来。
|
|
1098
|
+
*/
|
|
972
1099
|
function probeApplied(tokens) {
|
|
973
1100
|
try {
|
|
974
1101
|
if (typeof getComputedStyle !== 'function') return null
|
|
975
1102
|
const style = getComputedStyle(document.body)
|
|
976
1103
|
let checked = 0
|
|
977
|
-
|
|
1104
|
+
const mismatched = []
|
|
978
1105
|
for (const name of TOKEN_PROBE) {
|
|
979
|
-
|
|
1106
|
+
const pair = tokens[name]
|
|
1107
|
+
if (!pair) continue
|
|
980
1108
|
checked++
|
|
981
|
-
|
|
1109
|
+
const actual = style.getPropertyValue(name).trim()
|
|
1110
|
+
if (actual === '') { mismatched.push(name); continue }
|
|
1111
|
+
const expected = [pair.light, pair.dark].filter((v) => typeof v === 'string' && v.charAt(0) === '#')
|
|
1112
|
+
if (expected.length > 0 && !expected.some((v) => sameColor(actual, v))) mismatched.push(name)
|
|
982
1113
|
}
|
|
983
|
-
return checked === 0 ? null : { ok, checked }
|
|
1114
|
+
return checked === 0 ? null : { ok: mismatched.length === 0, checked, mismatched }
|
|
984
1115
|
} catch (err) {
|
|
985
1116
|
return null
|
|
986
1117
|
}
|
|
@@ -995,6 +1126,11 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
995
1126
|
return 'mono'
|
|
996
1127
|
}
|
|
997
1128
|
|
|
1129
|
+
/** 是否已经有用户配置:新装用户不需要看“行为变更”提示。 */
|
|
1130
|
+
function hasStoredConfig() {
|
|
1131
|
+
try { return localStorage.getItem(STORE_KEY) !== null } catch (err) { return false }
|
|
1132
|
+
}
|
|
1133
|
+
|
|
998
1134
|
function writeStored(id) {
|
|
999
1135
|
try { localStorage.setItem(STORE_KEY, id) } catch (err) { /* 记不住不影响本次生效 */ }
|
|
1000
1136
|
}
|
|
@@ -1083,14 +1219,30 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1083
1219
|
try { localStorage.setItem(HC_KEY, id) } catch (err) { /* 同理 */ }
|
|
1084
1220
|
}
|
|
1085
1221
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1222
|
+
function readNoticeSeen() {
|
|
1223
|
+
try {
|
|
1224
|
+
const raw = localStorage.getItem(NOTICE_KEY)
|
|
1225
|
+
return typeof raw === 'string' ? raw : ''
|
|
1226
|
+
} catch (err) { return '' }
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function writeNoticeSeen(version) {
|
|
1230
|
+
try { localStorage.setItem(NOTICE_KEY, version) } catch (err) { /* 记不住就每次都提示,不阻断使用 */ }
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/* ---------- 配置导入/导出(反哺 A-2 / A03)----------
|
|
1234
|
+
* 短码格式:`dshs1.` + base64url(JSON),字段全部用单字母。
|
|
1235
|
+
* v1 早期短码没有 `x`(自定义三色),解码时兼容:旧码仍可导入,只是不带自定义色。
|
|
1089
1236
|
*/
|
|
1090
1237
|
const CONFIG_PREFIX = 'dshs1.'
|
|
1091
1238
|
|
|
1092
1239
|
function encodeConfig(state) {
|
|
1093
|
-
const
|
|
1240
|
+
const custom = state.custom || readCustom()
|
|
1241
|
+
const raw = {
|
|
1242
|
+
v: 1, p: state.palette, s: state.sidebar, t: state.text, r: state.rightbar,
|
|
1243
|
+
c: state.scroll, k: state.control, m: state.md, h: state.hc, l: state.lang,
|
|
1244
|
+
x: { bg: custom.bg, text: custom.text, accent: custom.accent },
|
|
1245
|
+
}
|
|
1094
1246
|
try {
|
|
1095
1247
|
const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(raw))))
|
|
1096
1248
|
return CONFIG_PREFIX + b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
@@ -1119,7 +1271,12 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1119
1271
|
hc: hasId(HC_MODES, raw.h) ? raw.h : null,
|
|
1120
1272
|
lang: raw.l === 'auto' || LOCALES.indexOf(raw.l) !== -1 ? raw.l : null,
|
|
1121
1273
|
}
|
|
1122
|
-
|
|
1274
|
+
// 三色要么全合法、要么整体忽略(旧码没有这一项)
|
|
1275
|
+
const x = raw.x
|
|
1276
|
+
const custom = x && CUSTOM_FIELDS.every((k) => typeof x[k] === 'string' && HEX_RE.test(x[k]))
|
|
1277
|
+
? { bg: x.bg.toLowerCase(), text: x.text.toLowerCase(), accent: x.accent.toLowerCase() }
|
|
1278
|
+
: null
|
|
1279
|
+
return Object.values(next).every((v) => v !== null) ? Object.assign(next, { custom }) : null
|
|
1123
1280
|
} catch (err) { return null }
|
|
1124
1281
|
}
|
|
1125
1282
|
|
|
@@ -1193,17 +1350,26 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1193
1350
|
lang: '语言',
|
|
1194
1351
|
hc: '高对比',
|
|
1195
1352
|
custom: '自定义配色',
|
|
1196
|
-
customHint: '
|
|
1353
|
+
customHint: '取三个色的色相与色度;过亮/过暗的强调色会归一化以保证可读,明度由引擎按模式计算。',
|
|
1354
|
+
notice: '1.7.0 调整了派生规则:极端强调色会归一化、文字色相参与派生、深色模块面按可达上限报告对比度。',
|
|
1355
|
+
dismiss: '知道了',
|
|
1356
|
+
explainClamp: '强调色已归一化:{from} → {to}(保证按钮与勾选可读)',
|
|
1357
|
+
explainLink: '链接已为可读性压暗:{ratio}:1(目标 {target}:1)',
|
|
1358
|
+
preview: '预览',
|
|
1359
|
+
cancel: '取消',
|
|
1360
|
+
importSummary: '将覆盖:',
|
|
1197
1361
|
config: '配置',
|
|
1198
|
-
configHint: '
|
|
1362
|
+
configHint: '导出这台机器的配色选择(含自定义三色),或粘贴短码导入;导入会覆盖全部 9 项。',
|
|
1199
1363
|
reset: '恢复默认',
|
|
1200
1364
|
copy: '复制',
|
|
1201
1365
|
applyImport: '导入',
|
|
1202
1366
|
copied: '已复制',
|
|
1203
1367
|
copyFail: '复制失败,请手选后 Ctrl+C',
|
|
1204
1368
|
importOk: '已导入',
|
|
1369
|
+
importLegacy: '已导入(旧短码不含自定义三色)',
|
|
1205
1370
|
importBad: '短码无效,已忽略',
|
|
1206
|
-
|
|
1371
|
+
badHex: 'HEX 需为 #RRGGBB',
|
|
1372
|
+
hcSub: '尽量把文字下限抬到 AAA(正文 ≥15、次文 ≥9、三级 ≥7);深色模块面的上限受表面亮度限制。',
|
|
1207
1373
|
},
|
|
1208
1374
|
'zh-TW': {
|
|
1209
1375
|
nav: '主題色',
|
|
@@ -1229,17 +1395,26 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1229
1395
|
lang: '語言',
|
|
1230
1396
|
hc: '高對比',
|
|
1231
1397
|
custom: '自訂配色',
|
|
1232
|
-
customHint: '
|
|
1398
|
+
customHint: '取三個色的色相與色度;過亮/過暗的強調色會正規化以確保可讀性,明度由引擎依模式計算。',
|
|
1399
|
+
notice: '1.7.0 調整了衍生規則:極端強調色會正規化、文字色相參與衍生、深色模組面按可達上限回報對比度。',
|
|
1400
|
+
dismiss: '知道了',
|
|
1401
|
+
explainClamp: '強調色已正規化:{from} → {to}(確保按鈕與勾選可讀)',
|
|
1402
|
+
explainLink: '連結已為可讀性壓暗:{ratio}:1(目標 {target}:1)',
|
|
1403
|
+
preview: '預覽',
|
|
1404
|
+
cancel: '取消',
|
|
1405
|
+
importSummary: '將覆蓋:',
|
|
1233
1406
|
config: '設定',
|
|
1234
|
-
configHint: '
|
|
1407
|
+
configHint: '匯出這台機器的配色選擇(含自訂三色),或貼上短碼匯入;匯入會覆蓋全部 9 項。',
|
|
1235
1408
|
reset: '恢復預設',
|
|
1236
1409
|
copy: '複製',
|
|
1237
1410
|
applyImport: '匯入',
|
|
1238
1411
|
copied: '已複製',
|
|
1239
1412
|
copyFail: '複製失敗,請手選後 Ctrl+C',
|
|
1240
1413
|
importOk: '已匯入',
|
|
1414
|
+
importLegacy: '已匯入(舊短碼不含自訂三色)',
|
|
1241
1415
|
importBad: '短碼無效,已忽略',
|
|
1242
|
-
|
|
1416
|
+
badHex: 'HEX 需為 #RRGGBB',
|
|
1417
|
+
hcSub: '盡量把文字下限抬到 AAA(正文 ≥15、次文 ≥9、三級 ≥7);深色模組面的上限受表面亮度限制。',
|
|
1243
1418
|
},
|
|
1244
1419
|
en: {
|
|
1245
1420
|
nav: 'Color theme',
|
|
@@ -1265,17 +1440,26 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1265
1440
|
lang: 'Language',
|
|
1266
1441
|
hc: 'High contrast',
|
|
1267
1442
|
custom: 'Custom palette',
|
|
1268
|
-
customHint: 'Takes hue and chroma from three colours; lightness comes from the engine per mode
|
|
1443
|
+
customHint: 'Takes hue and chroma from three colours; extreme accents are normalised for readability; lightness comes from the engine per mode.',
|
|
1444
|
+
notice: '1.7.0 changes how colours are derived: extreme accents are normalised, text hue now affects derivation, and dark module surfaces report contrast against their achievable ceiling.',
|
|
1445
|
+
dismiss: 'Got it',
|
|
1446
|
+
explainClamp: 'Accent normalised: {from} → {to} (keeps buttons and checks readable)',
|
|
1447
|
+
explainLink: 'Link darkened for readability: {ratio}:1 (target {target}:1)',
|
|
1448
|
+
preview: 'Preview',
|
|
1449
|
+
cancel: 'Cancel',
|
|
1450
|
+
importSummary: 'Will overwrite:',
|
|
1269
1451
|
config: 'Config',
|
|
1270
|
-
configHint: 'Export this machine’s choices, or paste a short code to import. Import overwrites all
|
|
1452
|
+
configHint: 'Export this machine’s choices (including the three custom colours), or paste a short code to import. Import overwrites all 9 settings.',
|
|
1271
1453
|
reset: 'Reset',
|
|
1272
1454
|
copy: 'Copy',
|
|
1273
1455
|
applyImport: 'Import',
|
|
1274
1456
|
copied: 'Copied',
|
|
1275
1457
|
copyFail: 'Copy failed — select the code and press Ctrl+C',
|
|
1276
1458
|
importOk: 'Imported',
|
|
1459
|
+
importLegacy: 'Imported (old code has no custom colours)',
|
|
1277
1460
|
importBad: 'Invalid code, ignored',
|
|
1278
|
-
|
|
1461
|
+
badHex: 'HEX must be #RRGGBB',
|
|
1462
|
+
hcSub: 'Raises text floors toward AAA (body ≥15, secondary ≥9, tertiary ≥7); dark module surfaces are limited by surface luminance.',
|
|
1279
1463
|
},
|
|
1280
1464
|
ja: {
|
|
1281
1465
|
nav: 'カラーテーマ',
|
|
@@ -1301,17 +1485,26 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1301
1485
|
lang: '言語',
|
|
1302
1486
|
hc: 'ハイコントラスト',
|
|
1303
1487
|
custom: 'カスタム配色',
|
|
1304
|
-
customHint: '3
|
|
1488
|
+
customHint: '3 色の色相と彩度を使用。極端なアクセント色は可読性のため正規化され、明度はモードごとにエンジンが決めます。',
|
|
1489
|
+
notice: '1.7.0 で派生ルールを変更しました:極端なアクセント色は正規化され、文字の色相が派生に使われ、暗いモジュール面は到達可能な上限でコントラストを報告します。',
|
|
1490
|
+
dismiss: '了解',
|
|
1491
|
+
explainClamp: 'アクセント色を正規化しました:{from} → {to}(ボタンとチェックの可読性のため)',
|
|
1492
|
+
explainLink: 'リンクを可読性のため暗くしました:{ratio}:1(目標 {target}:1)',
|
|
1493
|
+
preview: 'プレビュー',
|
|
1494
|
+
cancel: 'キャンセル',
|
|
1495
|
+
importSummary: '上書きする内容:',
|
|
1305
1496
|
config: '設定の共有',
|
|
1306
|
-
configHint: '
|
|
1497
|
+
configHint: 'この端末の配色設定(カスタム 3 色を含む)を書き出す/短いコードを貼り付けて読み込みます(9 項目すべて上書き)。',
|
|
1307
1498
|
reset: 'リセット',
|
|
1308
1499
|
copy: 'コピー',
|
|
1309
1500
|
applyImport: '読み込み',
|
|
1310
1501
|
copied: 'コピーしました',
|
|
1311
1502
|
copyFail: 'コピーできません。選択して Ctrl+C してください',
|
|
1312
1503
|
importOk: '読み込みました',
|
|
1504
|
+
importLegacy: '読み込みました(旧コードにカスタム 3 色は含まれません)',
|
|
1313
1505
|
importBad: '無効なコードです',
|
|
1314
|
-
|
|
1506
|
+
badHex: 'HEX は #RRGGBB 形式で入力してください',
|
|
1507
|
+
hcSub: '本文の下限を AAA に近づけます(本文 ≥15、副次 ≥9、三次 ≥7)。暗いモジュール面は表面の明るさで上限が決まります。',
|
|
1315
1508
|
},
|
|
1316
1509
|
}
|
|
1317
1510
|
|
|
@@ -1372,6 +1565,11 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1372
1565
|
}
|
|
1373
1566
|
|
|
1374
1567
|
/* ================= 设置页(左侧导航独立一节) ================= */
|
|
1568
|
+
/** 文案插值:只替换 {name},缺值留空,不做 HTML 解释。 */
|
|
1569
|
+
function fmt(template, vars) {
|
|
1570
|
+
return String(template).replace(/\{(\w+)\}/g, (m, key) => (vars[key] === undefined ? '' : String(vars[key])))
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1375
1573
|
function PaletteSection(props) {
|
|
1376
1574
|
const current = React.useSyncExternalStore(props.subscribe, props.getCurrent)
|
|
1377
1575
|
const sidebarMode = React.useSyncExternalStore(props.subscribeSidebar, props.getSidebarMode)
|
|
@@ -1384,8 +1582,16 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1384
1582
|
const health = React.useSyncExternalStore(props.subscribeHealth, props.getHealth)
|
|
1385
1583
|
const [importText, setImportText] = React.useState('')
|
|
1386
1584
|
const [importState, setImportState] = React.useState(null)
|
|
1585
|
+
const [importPreview, setImportPreview] = React.useState(null)
|
|
1387
1586
|
const [copyState, setCopyState] = React.useState(null)
|
|
1388
1587
|
const [advanced, setAdvanced] = React.useState(false)
|
|
1588
|
+
// 升级提示按版本只出现一次
|
|
1589
|
+
const noticePending = React.useSyncExternalStore(props.subscribeNotice, props.getNoticePending)
|
|
1590
|
+
// HEX 输入需要「草稿」态:非法值不能写进主题,但用户还在输入时也不该被立刻回滚。
|
|
1591
|
+
const [hexDraft, setHexDraft] = React.useState({})
|
|
1592
|
+
const [hexError, setHexError] = React.useState({})
|
|
1593
|
+
// 自定义色有自己的不可变快照与订阅;只订阅 current 不会因三色变化而重渲染。
|
|
1594
|
+
const customColors = React.useSyncExternalStore(props.subscribeCustom, props.getCustomColors)
|
|
1389
1595
|
const locale = React.useSyncExternalStore(props.subscribeLocale, props.getLocale)
|
|
1390
1596
|
const langPref = React.useSyncExternalStore(props.subscribeLocale, props.getLangPref)
|
|
1391
1597
|
const langAttr = langPref !== 'auto'
|
|
@@ -1393,11 +1599,42 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1393
1599
|
: ((typeof document !== 'undefined' && document.documentElement && document.documentElement.lang) || 'zh-CN')
|
|
1394
1600
|
const txt = TEXT[locale] || TEXT['zh-CN']
|
|
1395
1601
|
const lb = LABELS[locale] || LABELS['zh-CN']
|
|
1602
|
+
// 只在自定义面板显示;解释是只读的,不改变任何设置。取色异常不应拖垮设置页。
|
|
1603
|
+
let explain = null
|
|
1604
|
+
if (current === 'custom') {
|
|
1605
|
+
try { explain = props.getCustomExplain() } catch (err) { explain = null }
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// A07:radiogroup 的键盘模型 —— 组只占一个 Tab 位,方向键循环切换,Home/End 到首尾。
|
|
1609
|
+
const onSegKeyDown = (event, modes, value, onPick, disabled) => {
|
|
1610
|
+
if (disabled) return
|
|
1611
|
+
if (['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', 'Home', 'End'].indexOf(event.key) === -1) return
|
|
1612
|
+
event.preventDefault()
|
|
1613
|
+
const currentIndex = Math.max(0, modes.findIndex((m) => m.id === value))
|
|
1614
|
+
let nextIndex = currentIndex
|
|
1615
|
+
if (event.key === 'Home') nextIndex = 0
|
|
1616
|
+
else if (event.key === 'End') nextIndex = modes.length - 1
|
|
1617
|
+
else {
|
|
1618
|
+
const delta = event.key === 'ArrowRight' || event.key === 'ArrowDown' ? 1 : -1
|
|
1619
|
+
nextIndex = (currentIndex + delta + modes.length) % modes.length
|
|
1620
|
+
}
|
|
1621
|
+
if (nextIndex === currentIndex) return
|
|
1622
|
+
onPick(modes[nextIndex].id)
|
|
1623
|
+
const buttons = event.currentTarget.children
|
|
1624
|
+
const target = buttons && buttons[nextIndex]
|
|
1625
|
+
if (target && typeof target.focus === 'function') target.focus()
|
|
1626
|
+
}
|
|
1396
1627
|
|
|
1397
1628
|
// A6:读屏需要知道"这是哪个设置的选项组";单选用 radio 语义更准
|
|
1398
1629
|
const seg = (modes, value, onPick, labels, labelId, disabled) => h(
|
|
1399
1630
|
'div',
|
|
1400
|
-
{
|
|
1631
|
+
{
|
|
1632
|
+
className: 'dsh-tp-seg',
|
|
1633
|
+
role: 'radiogroup',
|
|
1634
|
+
'aria-labelledby': labelId,
|
|
1635
|
+
'aria-disabled': disabled ? 'true' : undefined,
|
|
1636
|
+
onKeyDown: (event) => onSegKeyDown(event, modes, value, onPick, disabled),
|
|
1637
|
+
},
|
|
1401
1638
|
modes.map((mode) => h(
|
|
1402
1639
|
'button',
|
|
1403
1640
|
{
|
|
@@ -1407,6 +1644,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1407
1644
|
className: mode.id === value ? 'is-on' : undefined,
|
|
1408
1645
|
'aria-checked': mode.id === value,
|
|
1409
1646
|
'aria-disabled': disabled ? 'true' : undefined,
|
|
1647
|
+
// roving tabindex:只有选中项在 Tab 序列里,其余靠方向键进入
|
|
1648
|
+
tabIndex: mode.id === value ? 0 : -1,
|
|
1410
1649
|
disabled: disabled === true,
|
|
1411
1650
|
onClick: () => { if (!disabled) onPick(mode.id) },
|
|
1412
1651
|
},
|
|
@@ -1423,7 +1662,18 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1423
1662
|
control,
|
|
1424
1663
|
)
|
|
1425
1664
|
|
|
1426
|
-
|
|
1665
|
+
// A06:HEX 以「草稿 + 提交」处理;非法值不写主题,也不假装保存成功。
|
|
1666
|
+
const commitHex = (field) => {
|
|
1667
|
+
const draft = hexDraft[field]
|
|
1668
|
+
const raw = String(draft !== undefined ? draft : customColors[field]).trim()
|
|
1669
|
+
if (HEX_RE.test(raw)) {
|
|
1670
|
+
props.setCustomColor(field, raw)
|
|
1671
|
+
setHexDraft(Object.assign({}, hexDraft, { [field]: undefined }))
|
|
1672
|
+
setHexError(Object.assign({}, hexError, { [field]: false }))
|
|
1673
|
+
} else {
|
|
1674
|
+
setHexError(Object.assign({}, hexError, { [field]: true }))
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1427
1677
|
const customPanel = current === 'custom' && h(
|
|
1428
1678
|
'div',
|
|
1429
1679
|
{ className: 'dsh-tp-custom' },
|
|
@@ -1435,28 +1685,63 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1435
1685
|
onClick: () => props.resetCustom(),
|
|
1436
1686
|
}, txt.reset)),
|
|
1437
1687
|
h('div', { className: 'dsh-tp-swatches' },
|
|
1438
|
-
['bg', 'text', 'accent'].map((field) =>
|
|
1688
|
+
['bg', 'text', 'accent'].map((field) => {
|
|
1689
|
+
const name = (lb.custom && lb.custom[field]) || field
|
|
1690
|
+
const colorId = rowId('color-' + field)
|
|
1691
|
+
const hexId = rowId('hex-' + field)
|
|
1692
|
+
const errId = rowId('hexerr-' + field)
|
|
1693
|
+
const invalid = hexError[field] === true
|
|
1694
|
+
return h('div', {
|
|
1695
|
+
key: field,
|
|
1696
|
+
className: 'dsh-tp-swatch',
|
|
1697
|
+
role: 'group',
|
|
1698
|
+
'aria-label': name,
|
|
1699
|
+
},
|
|
1439
1700
|
h('input', {
|
|
1440
1701
|
type: 'color',
|
|
1702
|
+
id: colorId,
|
|
1441
1703
|
className: 'dsh-tp-color',
|
|
1442
1704
|
value: customColors[field],
|
|
1443
1705
|
// input 是"拖动过程中"就触发,change 是"确认后"触发;两个都接,
|
|
1444
1706
|
// 拖动时实时看到整页变化,松手/确认后不会再跳一次
|
|
1445
1707
|
onInput: (e) => props.setCustomColor(field, e.target.value),
|
|
1446
1708
|
onChange: (e) => props.setCustomColor(field, e.target.value),
|
|
1447
|
-
'aria-label':
|
|
1709
|
+
'aria-label': name,
|
|
1448
1710
|
}),
|
|
1449
|
-
h('
|
|
1711
|
+
h('label', { htmlFor: hexId, className: 'dsh-tp-swatch-name' }, name),
|
|
1450
1712
|
h('input', {
|
|
1451
|
-
|
|
1713
|
+
id: hexId,
|
|
1452
1714
|
className: 'dsh-tp-hexinput',
|
|
1453
|
-
|
|
1715
|
+
value: hexDraft[field] !== undefined ? hexDraft[field] : customColors[field],
|
|
1454
1716
|
spellCheck: false,
|
|
1455
|
-
'aria-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1717
|
+
'aria-invalid': invalid ? 'true' : undefined,
|
|
1718
|
+
'aria-describedby': invalid ? errId : undefined,
|
|
1719
|
+
'aria-label': name + ' HEX',
|
|
1720
|
+
onChange: (e) => {
|
|
1721
|
+
setHexDraft(Object.assign({}, hexDraft, { [field]: e.target.value }))
|
|
1722
|
+
if (invalid) setHexError(Object.assign({}, hexError, { [field]: false }))
|
|
1723
|
+
},
|
|
1724
|
+
onBlur: () => commitHex(field),
|
|
1725
|
+
onKeyDown: (e) => {
|
|
1726
|
+
if (e.key === 'Enter') { e.preventDefault(); commitHex(field) }
|
|
1727
|
+
if (e.key === 'Escape') {
|
|
1728
|
+
setHexDraft(Object.assign({}, hexDraft, { [field]: undefined }))
|
|
1729
|
+
setHexError(Object.assign({}, hexError, { [field]: false }))
|
|
1730
|
+
e.currentTarget.blur()
|
|
1731
|
+
}
|
|
1732
|
+
},
|
|
1733
|
+
}),
|
|
1734
|
+
invalid && h('span', { id: errId, className: 'dsh-tp-hexerror', role: 'alert' }, txt.badHex))
|
|
1735
|
+
})),
|
|
1459
1736
|
h('p', { className: 'dsh-tp-custom-hint' }, txt.customHint),
|
|
1737
|
+
explain && (explain.accentClamped || explain.linkShifted) && h(
|
|
1738
|
+
'div',
|
|
1739
|
+
{ className: 'dsh-tp-explain', role: 'status' },
|
|
1740
|
+
explain.accentClamped && h('span', null,
|
|
1741
|
+
fmt(txt.explainClamp, { from: explain.accentFrom, to: explain.accentTo })),
|
|
1742
|
+
explain.linkShifted && h('span', null,
|
|
1743
|
+
fmt(txt.explainLink, { ratio: explain.linkRatio.toFixed(2), target: explain.linkTarget })),
|
|
1744
|
+
),
|
|
1460
1745
|
)
|
|
1461
1746
|
|
|
1462
1747
|
const card = (scheme) => {
|
|
@@ -1494,9 +1779,13 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1494
1779
|
h('h2', null, txt.nav),
|
|
1495
1780
|
h('span', { className: 'dsh-tp-badge' }, 'v' + PLUGIN_VERSION)),
|
|
1496
1781
|
h('p', { className: 'dsh-tp-sub' }, txt.hint),
|
|
1782
|
+
noticePending && h('div', { className: 'dsh-tp-notice' },
|
|
1783
|
+
h('p', { role: 'status' }, txt.notice),
|
|
1784
|
+
h('button', { type: 'button', className: 'dsh-tp-mini', onClick: () => props.dismissNotice() }, txt.dismiss)),
|
|
1497
1785
|
h('div', { className: 'dsh-tp-grid' }, SCHEMES.map(card)),
|
|
1498
1786
|
customPanel,
|
|
1499
1787
|
row('text', txt.text, txt.textSub, seg(TEXT_MODES, textMode, props.setTextMode, lb.text, rowId('text'))),
|
|
1788
|
+
row('hc', txt.hc, txt.hcSub, seg(HC_MODES, hcMode, props.setHcMode, lb.hc, rowId('hc'))),
|
|
1500
1789
|
row('sidebar', txt.sidebar, txt.sidebarSub, seg(SIDEBAR_MODES, sidebarMode, props.setSidebarMode, lb.sidebar, rowId('sidebar'))),
|
|
1501
1790
|
row('rightbar', txt.rightbar, txt.rightbarSub, seg(RIGHTBAR_MODES, rightbarMode, props.setRightbarMode, lb.rightbar, rowId('rightbar'))),
|
|
1502
1791
|
health && !health.ok && h('p', { className: 'dsh-tp-status is-bad' }, txt.bad),
|
|
@@ -1551,19 +1840,57 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1551
1840
|
placeholder: 'dshs1.…',
|
|
1552
1841
|
value: importText,
|
|
1553
1842
|
'aria-label': txt.applyImport,
|
|
1554
|
-
onChange: (e) => {
|
|
1843
|
+
onChange: (e) => {
|
|
1844
|
+
setImportText(e.target.value)
|
|
1845
|
+
setImportState(null)
|
|
1846
|
+
setImportPreview(null)
|
|
1847
|
+
},
|
|
1555
1848
|
}),
|
|
1556
1849
|
h('button', {
|
|
1557
1850
|
type: 'button',
|
|
1558
1851
|
className: 'dsh-tp-mini',
|
|
1559
|
-
onClick: () =>
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1852
|
+
onClick: () => {
|
|
1853
|
+
const result = props.previewConfigCode(importText)
|
|
1854
|
+
if (result && result.ok) {
|
|
1855
|
+
setImportPreview(result.config)
|
|
1856
|
+
setImportState(null)
|
|
1857
|
+
} else {
|
|
1858
|
+
setImportPreview(null)
|
|
1859
|
+
setImportState('bad')
|
|
1860
|
+
}
|
|
1861
|
+
},
|
|
1862
|
+
}, txt.preview),
|
|
1863
|
+
importState === 'bad' && h('span', { className: 'dsh-tp-note', role: 'alert', 'aria-live': 'polite' }, txt.importBad)),
|
|
1864
|
+
importPreview && h('div', { className: 'dsh-tp-preview', role: 'group', 'aria-label': txt.importSummary },
|
|
1865
|
+
h('p', null, txt.importSummary + ' ' + [
|
|
1866
|
+
(lb.scheme[importPreview.palette] || importPreview.palette),
|
|
1867
|
+
txt.sidebar + ' ' + (lb.sidebar[importPreview.sidebar] || importPreview.sidebar),
|
|
1868
|
+
txt.text + ' ' + (lb.text[importPreview.text] || importPreview.text),
|
|
1869
|
+
txt.hc + ' ' + (lb.hc[importPreview.hc] || importPreview.hc),
|
|
1870
|
+
].join(' · ')),
|
|
1871
|
+
importPreview.custom
|
|
1872
|
+
? h('p', null, txt.custom + ' ' + importPreview.custom.bg + ' / ' + importPreview.custom.text + ' / ' + importPreview.custom.accent)
|
|
1873
|
+
: h('p', null, txt.importLegacy),
|
|
1874
|
+
h('div', { className: 'dsh-tp-field-row' },
|
|
1875
|
+
h('button', {
|
|
1876
|
+
type: 'button',
|
|
1877
|
+
className: 'dsh-tp-mini',
|
|
1878
|
+
onClick: () => {
|
|
1879
|
+
const result = props.applyConfigCode(importText)
|
|
1880
|
+
setImportPreview(null)
|
|
1881
|
+
setImportState(result && result.ok ? (result.hasCustom ? 'ok' : 'legacy') : 'bad')
|
|
1882
|
+
},
|
|
1883
|
+
}, txt.applyImport),
|
|
1884
|
+
h('button', {
|
|
1885
|
+
type: 'button',
|
|
1886
|
+
className: 'dsh-tp-mini',
|
|
1887
|
+
onClick: () => { setImportPreview(null); setImportState(null) },
|
|
1888
|
+
}, txt.cancel))),
|
|
1889
|
+
importState && importState !== 'bad' && h('span', { className: 'dsh-tp-note', role: 'status', 'aria-live': 'polite' },
|
|
1890
|
+
importState === 'ok' ? txt.importOk : txt.importLegacy),
|
|
1563
1891
|
h('p', { className: 'dsh-tp-custom-hint' }, txt.configHint),
|
|
1564
1892
|
),
|
|
1565
1893
|
row('md', txt.md, txt.mdSub, seg(MD_MODES, mdMode, props.setMdMode, lb.md, rowId('md'))),
|
|
1566
|
-
row('hc', txt.hc, txt.hcSub, seg(HC_MODES, hcMode, props.setHcMode, lb.hc, rowId('hc'))),
|
|
1567
1894
|
row('scroll', txt.scroll, current === 'mono' ? txt.scrollHintMono : txt.scrollSub, seg(SCROLL_MODES, scrollMode, props.setScrollMode, lb.scroll, rowId('scroll'), current === 'mono')),
|
|
1568
1895
|
row('control', txt.control, txt.controlSub, seg(CONTROL_MODES, controlMode, props.setControlMode, lb.control, rowId('control'), current === 'mono')),
|
|
1569
1896
|
),
|
|
@@ -1589,7 +1916,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1589
1916
|
|
|
1590
1917
|
// 开发期接口:palette-dev.mjs 通过它拿到同一套引擎(避免「报表好看、上线不一样」)
|
|
1591
1918
|
if (DEV_OPT_IN && typeof window !== 'undefined' && typeof window.__DSH_THEME_PALETTE_DEV__ === 'function') {
|
|
1592
|
-
window.__DSH_THEME_PALETTE_DEV__({ buildTokens, SCHEMES, SIDEBAR_MODES, TEXT_MODES, RIGHTBAR_MODES, SCROLL_MODES, CONTROL_MODES, MD_MODES, contrast, toOklch, rampsFor, settingsCss: SETTINGS_CSS, cardColors })
|
|
1919
|
+
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, settingsCss: SETTINGS_CSS, cardColors })
|
|
1593
1920
|
}
|
|
1594
1921
|
|
|
1595
1922
|
return {
|
|
@@ -1602,6 +1929,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1602
1929
|
}
|
|
1603
1930
|
|
|
1604
1931
|
let current = readStored()
|
|
1932
|
+
// 只有升级前已有配置的用户才需要“行为变更”提示
|
|
1933
|
+
const hadConfig = hasStoredConfig()
|
|
1605
1934
|
let sidebarMode = readSidebarStored()
|
|
1606
1935
|
let textMode = readTextStored()
|
|
1607
1936
|
let rightbarMode = readRightbarStored()
|
|
@@ -1622,6 +1951,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1622
1951
|
const controlListeners = new Set()
|
|
1623
1952
|
const mdListeners = new Set()
|
|
1624
1953
|
const hcListeners = new Set()
|
|
1954
|
+
const customListeners = new Set()
|
|
1625
1955
|
|
|
1626
1956
|
const getCurrent = () => current
|
|
1627
1957
|
const getSidebarMode = () => sidebarMode
|
|
@@ -1671,6 +2001,27 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1671
2001
|
return () => { hcListeners.delete(fn) }
|
|
1672
2002
|
}
|
|
1673
2003
|
const emitHc = () => { for (const fn of hcListeners) { try { fn() } catch (err) { /* 同理 */ } } }
|
|
2004
|
+
// 自定义色的独立订阅:三个色变了但 current 没变,不能靠配色订阅触发重渲染
|
|
2005
|
+
const subscribeCustom = (fn) => {
|
|
2006
|
+
customListeners.add(fn)
|
|
2007
|
+
return () => { customListeners.delete(fn) }
|
|
2008
|
+
}
|
|
2009
|
+
const emitCustom = () => { for (const fn of customListeners) { try { fn() } catch (err) { /* 同理 */ } } }
|
|
2010
|
+
|
|
2011
|
+
// 升级提示:每个版本只出现一次;只读写 notice 键,不碰配色设置
|
|
2012
|
+
let noticeSeenVersion = readNoticeSeen()
|
|
2013
|
+
const noticeListeners = new Set()
|
|
2014
|
+
const subscribeNotice = (fn) => {
|
|
2015
|
+
noticeListeners.add(fn)
|
|
2016
|
+
return () => { noticeListeners.delete(fn) }
|
|
2017
|
+
}
|
|
2018
|
+
const getNoticePending = () => hadConfig && noticeSeenVersion !== PLUGIN_VERSION
|
|
2019
|
+
const emitNotice = () => { for (const fn of noticeListeners) { try { fn() } catch (err) { /* 同理 */ } } }
|
|
2020
|
+
const dismissNotice = () => {
|
|
2021
|
+
noticeSeenVersion = PLUGIN_VERSION
|
|
2022
|
+
writeNoticeSeen(noticeSeenVersion)
|
|
2023
|
+
emitNotice()
|
|
2024
|
+
}
|
|
1674
2025
|
|
|
1675
2026
|
let disposeLayer = () => {}
|
|
1676
2027
|
/** 按当前配色 + 侧栏档位重建覆盖层(换配色与换侧栏档位走同一条路)。 */
|
|
@@ -1760,7 +2111,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1760
2111
|
const run = () => {
|
|
1761
2112
|
const next = probeApplied(tokens)
|
|
1762
2113
|
if (next === null) return
|
|
1763
|
-
if (health !== null && health.ok === next.ok && health.checked === next.checked
|
|
2114
|
+
if (health !== null && health.ok === next.ok && health.checked === next.checked
|
|
2115
|
+
&& String(health.mismatched) === String(next.mismatched)) return
|
|
1764
2116
|
health = { ...next, reapplies: reapplyCount }
|
|
1765
2117
|
if (!next.ok) console.warn('[dsh-theme-palette] 有 token 没写进 body,宿主可能改名了')
|
|
1766
2118
|
emitHealth()
|
|
@@ -1817,10 +2169,10 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1817
2169
|
applyAll()
|
|
1818
2170
|
emitMd()
|
|
1819
2171
|
}
|
|
1820
|
-
/**
|
|
2172
|
+
/** 导入:整份校验通过才落地;返回结果供 UI 显示(旧码不含自定义三色时 hasCustom=false) */
|
|
1821
2173
|
const applyConfigCode = (code) => {
|
|
1822
2174
|
const cfg = decodeConfig(code)
|
|
1823
|
-
if (!cfg) return false
|
|
2175
|
+
if (!cfg) return { ok: false, hasCustom: false }
|
|
1824
2176
|
current = cfg.palette
|
|
1825
2177
|
sidebarMode = cfg.sidebar
|
|
1826
2178
|
textMode = cfg.text
|
|
@@ -1830,6 +2182,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1830
2182
|
mdMode = cfg.md
|
|
1831
2183
|
hcMode = cfg.hc
|
|
1832
2184
|
langPref = cfg.lang
|
|
2185
|
+
if (cfg.custom) writeCustom(cfg.custom)
|
|
1833
2186
|
writeStored(current)
|
|
1834
2187
|
writeSidebarStored(sidebarMode)
|
|
1835
2188
|
writeTextStored(textMode)
|
|
@@ -1841,21 +2194,51 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1841
2194
|
writeLangPref(langPref)
|
|
1842
2195
|
applyAll('import')
|
|
1843
2196
|
emit(); emitSidebar(); emitText(); emitRightbar(); emitScroll(); emitControl(); emitMd(); emitHc(); emitLocale()
|
|
1844
|
-
|
|
2197
|
+
if (cfg.custom) emitCustom()
|
|
2198
|
+
return { ok: true, hasCustom: !!cfg.custom }
|
|
1845
2199
|
}
|
|
1846
2200
|
const getCustomColors = () => readCustom()
|
|
2201
|
+
/**
|
|
2202
|
+
* 解释性校正:告诉用户“你的取色被如何处理了”,而不是让颜色悄悄变掉。
|
|
2203
|
+
* 只读、可重复调用;ramp 缓存保证同一组设置不会重复计算。
|
|
2204
|
+
*/
|
|
2205
|
+
const getCustomExplain = () => {
|
|
2206
|
+
const colors = readCustom()
|
|
2207
|
+
const scheme = customScheme(colors)
|
|
2208
|
+
const hc = hcMode === 'on'
|
|
2209
|
+
const t = buildTokens(scheme, sidebarMode, textMode, rightbarMode, scrollMode, controlMode, mdMode, hc)
|
|
2210
|
+
const link = t['--dsw-alias-link'].light
|
|
2211
|
+
const base = t['--dsw-static-deepseek-500'].light
|
|
2212
|
+
const surfaces = ['150', '60', '00'].map((k) => t['--dsw-static-neutral-bluish-' + k].light)
|
|
2213
|
+
const ratio = Math.min(...surfaces.map((bg) => contrast(link, bg)))
|
|
2214
|
+
const acL = toOklch(colors.accent)[0]
|
|
2215
|
+
const accentTo = Math.min(0.64, Math.max(0.30, acL))
|
|
2216
|
+
return {
|
|
2217
|
+
accentClamped: Math.abs(acL - accentTo) > 0.005,
|
|
2218
|
+
accentFrom: Math.round(acL * 100) / 100,
|
|
2219
|
+
accentTo: Math.round(accentTo * 100) / 100,
|
|
2220
|
+
linkShifted: link.toLowerCase() !== base.toLowerCase(),
|
|
2221
|
+
linkRatio: Math.round(ratio * 100) / 100,
|
|
2222
|
+
linkTarget: floors(hc).link,
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
const previewConfigCode = (code) => {
|
|
2226
|
+
const cfg = decodeConfig(code)
|
|
2227
|
+
return cfg ? { ok: true, config: cfg } : { ok: false }
|
|
2228
|
+
}
|
|
1847
2229
|
const resetCustom = () => {
|
|
1848
2230
|
writeCustom(Object.assign({}, CUSTOM_DEFAULT))
|
|
1849
2231
|
applyAll('custom:reset')
|
|
1850
|
-
|
|
2232
|
+
emitCustom()
|
|
1851
2233
|
}
|
|
1852
2234
|
const setCustomColor = (field, hex) => {
|
|
1853
|
-
if (CUSTOM_FIELDS.indexOf(field) === -1) return
|
|
1854
|
-
if (typeof hex !== 'string' || !HEX_RE.test(hex)) return
|
|
1855
|
-
const next = Object.assign(readCustom(), { [field]: hex })
|
|
2235
|
+
if (CUSTOM_FIELDS.indexOf(field) === -1) return false
|
|
2236
|
+
if (typeof hex !== 'string' || !HEX_RE.test(hex)) return false
|
|
2237
|
+
const next = Object.assign({}, readCustom(), { [field]: hex.toLowerCase() })
|
|
1856
2238
|
writeCustom(next)
|
|
1857
2239
|
applyAll('custom:' + field)
|
|
1858
|
-
|
|
2240
|
+
emitCustom()
|
|
2241
|
+
return true
|
|
1859
2242
|
}
|
|
1860
2243
|
const setHcMode = (id) => {
|
|
1861
2244
|
if (!HC_MODES.some((m) => m.id === id)) return
|
|
@@ -1990,6 +2373,12 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1990
2373
|
getConfigCode,
|
|
1991
2374
|
applyConfigCode,
|
|
1992
2375
|
getCustomColors,
|
|
2376
|
+
getCustomExplain,
|
|
2377
|
+
previewConfigCode,
|
|
2378
|
+
subscribeNotice,
|
|
2379
|
+
getNoticePending,
|
|
2380
|
+
dismissNotice,
|
|
2381
|
+
subscribeCustom,
|
|
1993
2382
|
setCustomColor,
|
|
1994
2383
|
resetCustom,
|
|
1995
2384
|
getLangPref,
|
package/docs/DESIGN.md
CHANGED
|
@@ -56,6 +56,11 @@
|
|
|
56
56
|
高对比档(`HC_MODES`,默认关)把文字下限整体抬到正文 ≥15 / 次文 ≥9 / 三级 ≥7 / 链接 ≥7,
|
|
57
57
|
并把求解器位移上限从 0.13 放宽到 0.30(低档位下 4.6 → 7 需要 ΔL≈0.2,原上限够不着)。
|
|
58
58
|
|
|
59
|
+
> **2026-10-23 反哺修正**:深色模块面 `800` 对纯白也只有 10.1–11.0:1,12/15 在该面**物理不可达**。
|
|
60
|
+
> 求解器现在按承载面可达上限钳位(留 7% 色彩余量),`palette-dev.mjs` 同时打印
|
|
61
|
+
> 「实际 / 目标 / 可达」,不可达的配对标「表面上限」而不再算硬失败。深色正文以 `800` 为求解面,
|
|
62
|
+
> 850/875/950 自然更高;caption 也在 800/850 上求解到 AA,不再沿用未求解的 500。
|
|
63
|
+
|
|
59
64
|
## 文字着色
|
|
60
65
|
|
|
61
66
|
正文原来只给 0.004 的色度(近中性),视觉上就是「黑字 / 白字」。现在有第三行「文字着色」,
|
|
@@ -240,3 +245,12 @@ alpha **浅色与内置一致、深色压到 0.6 倍**(见下):
|
|
|
240
245
|
「同一套配色各自适配明暗」就得注册两个 id + 自己盯 `prefers-color-scheme` 反复 `setTheme()`,
|
|
241
246
|
还会撞三个官方约束(自定义 id 不持久化、`adopt()` 回灌覆盖、dispose 生效中的主题会重置 preference)。
|
|
242
247
|
所以用更弱的 `ctx.theme.overrideTokens(source, tokens)`:明暗归官方「外观」,插件只换色。
|
|
248
|
+
|
|
249
|
+
## 设置页的反馈与迁移(2026-10-23)
|
|
250
|
+
|
|
251
|
+
- **解释性校正**:自定义面板在强调色被归一化、或链接被压暗时显示实际数值(`{from} → {to}`、`linkRatio:1`),
|
|
252
|
+
让“颜色为什么变了”可解释,而不是静默改写。
|
|
253
|
+
- **导入预览**:短码先解码成摘要(配色 / 侧栏 / 文字 / 高对比 / 三色),确认后才写入;预览不触碰存储。
|
|
254
|
+
- **升级提示**:`notice.v1` 只记录“已读版本”,与配色存储完全分离;每个版本出现一次,可关闭,不阻断操作。
|
|
255
|
+
- **契约测试**:`tools/check-host-contract.mjs` 断言插件依赖的 token 与 `data-rightbar-col` / `_markdown_` /
|
|
256
|
+
`md-code-block` 仍存在于当前宿主构建;找不到 asar 时 SKIP。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-theme-palette",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "DSH 主题配色:4 套配色 × 明暗两模式,OKLCH 色域安全重建 + 真实承载面对比度求解,可调强度与面板表面。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"node": "^22.19.0 || >=24.0.0",
|
|
39
39
|
"dsh": ">=0.2.1-rc.1"
|
|
40
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",
|
|
43
|
+
"report": "node palette-dev.mjs && node palette-dev.mjs --hc"
|
|
44
|
+
},
|
|
41
45
|
"dsh": {
|
|
42
46
|
"bundle": {
|
|
43
47
|
"patch": "./cordis.patch.yml"
|