dsh-theme-palette 1.5.3 → 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 +55 -0
- package/README-es.md +2 -0
- package/README-hi.md +2 -0
- package/README-pt.md +2 -0
- package/README-zh.md +7 -4
- package/README.md +3 -1
- package/client.js +554 -170
- package/docs/DESIGN.md +28 -0
- package/package.json +5 -1
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(深色下近白)→ 白底白字。
|
|
@@ -807,16 +898,10 @@ window.__ModuleLoader__.load({
|
|
|
807
898
|
return pairs
|
|
808
899
|
}
|
|
809
900
|
|
|
810
|
-
|
|
811
|
-
function swatches(scheme, textMode) {
|
|
812
|
-
if (!scheme.tinted) return ['#ffffff', '#e5e5e5', '#0f1115']
|
|
813
|
-
const r = rampsFor(scheme, textMode || DEFAULT_TEXT)
|
|
814
|
-
return [r.bluish['00'], r.bluish['100'], r.deepseek['500']]
|
|
815
|
-
}
|
|
816
|
-
|
|
901
|
+
|
|
817
902
|
|
|
818
903
|
/** 卡片缩略图用的一小组颜色(画布/侧栏/文字/强调),按当前强度档取。 */
|
|
819
|
-
function cardColors(scheme, textMode) {
|
|
904
|
+
function cardColors(scheme, textMode, sidebarMode) {
|
|
820
905
|
const r = rampsFor(scheme, textMode || DEFAULT_TEXT)
|
|
821
906
|
if (!r) {
|
|
822
907
|
return {
|
|
@@ -824,7 +909,7 @@ window.__ModuleLoader__.load({
|
|
|
824
909
|
dark: { canvas: '#151517', side: '#1b1b1c', text: '#f9fafb', accent: '#7aaaff' },
|
|
825
910
|
}
|
|
826
911
|
}
|
|
827
|
-
const side = sidebarTokens(r, DEFAULT_SIDEBAR, scheme.tinted)
|
|
912
|
+
const side = sidebarTokens(r, sidebarMode || DEFAULT_SIDEBAR, scheme.tinted)
|
|
828
913
|
return {
|
|
829
914
|
light: {
|
|
830
915
|
canvas: r.bluish['00'],
|
|
@@ -868,10 +953,15 @@ window.__ModuleLoader__.load({
|
|
|
868
953
|
.dsh-tp-head h2{margin:0;font-size:18px;font-weight:600;letter-spacing:.2px}
|
|
869
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}
|
|
870
955
|
.dsh-tp-sub{margin:-6px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
871
|
-
.dsh-tp-
|
|
872
|
-
|
|
873
|
-
.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}
|
|
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}
|
|
874
963
|
.dsh-tp-card:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
964
|
+
.dsh-tp-card:focus-visible{outline-offset:2px}
|
|
875
965
|
.dsh-tp-card.is-on{background:var(--dshtp-card-picked,var(--dsw-alias-bg-module-platform));border-color:var(--t-accent-l)}
|
|
876
966
|
body[data-ds-dark-theme] .dsh-tp-card.is-on{border-color:var(--t-accent-d)}
|
|
877
967
|
.dsh-tp-thumb{display:flex;height:56px;border-radius:9px;overflow:hidden;border:.5px solid rgba(128,128,128,.28)}
|
|
@@ -885,20 +975,21 @@ body[data-ds-dark-theme] .dsh-tp-thumb-main b{background:var(--t-text-d)}
|
|
|
885
975
|
.dsh-tp-thumb-main b:nth-of-type(3){width:44%;background:var(--t-accent-l);opacity:.9}
|
|
886
976
|
body[data-ds-dark-theme] .dsh-tp-thumb-main b:nth-of-type(3){background:var(--t-accent-d)}
|
|
887
977
|
.dsh-tp-card-meta{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}
|
|
888
|
-
.dsh-tp-card-name{font-size:
|
|
889
|
-
.dsh-tp-card-desc{font-size:
|
|
978
|
+
.dsh-tp-card-name{font-size:14px;font-weight:500}
|
|
979
|
+
.dsh-tp-card-desc{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
890
980
|
.dsh-tp-check{position:absolute;top:7px;right:7px;width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;line-height:1;background:var(--t-accent-l);color:var(--t-canvas-l);opacity:0;transition:opacity .12s ease}
|
|
891
981
|
body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--t-canvas-d)}
|
|
892
982
|
.dsh-tp-card.is-on .dsh-tp-check{opacity:1}
|
|
893
|
-
.dsh-tp-row{display:flex;align-items:center;gap:12px;padding:11px 0;border-top:.5px solid var(--dsw-alias-border-l2)}
|
|
983
|
+
.dsh-tp-row{display:flex;align-items:center;gap:12px;padding:11px 0;border-top:.5px solid var(--dsw-alias-border-l2);flex-wrap:wrap}
|
|
894
984
|
.dsh-tp-row-label{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
|
|
895
985
|
.dsh-tp-row-label b{font-size:13.5px;font-weight:500}
|
|
896
986
|
.dsh-tp-row-label span{font-size:11.5px;color:var(--dsw-alias-label-tertiary);line-height:16px}
|
|
897
|
-
.dsh-tp-seg{display:inline-flex;gap:2px;padding:2px;border-radius:10px;background:var(--dsw-alias-interactive-bg-hover);flex:none}
|
|
987
|
+
.dsh-tp-seg{display:inline-flex;flex-wrap:wrap;gap:2px;padding:2px;border-radius:10px;background:var(--dsw-alias-interactive-bg-hover);flex:none;max-width:100%}
|
|
898
988
|
.dsh-tp-seg button{font:inherit;font-size:12.5px;line-height:20px;cursor:pointer;border:0;border-radius:8px;padding:5px 12px;background:transparent;color:var(--dsw-alias-label-secondary);white-space:nowrap;transition:background .12s,color .12s}
|
|
899
989
|
.dsh-tp-seg button:hover{color:var(--dsw-alias-label-primary)}
|
|
900
990
|
.dsh-tp-seg button.is-on{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,.10)}
|
|
901
|
-
|
|
991
|
+
.dsh-tp-seg button:focus-visible{outline-offset:2px}
|
|
992
|
+
.dsh-tp-seg button[disabled]{opacity:.45;cursor:default}
|
|
902
993
|
.dsh-tp-more{display:inline-flex;align-items:center;gap:6px;font:inherit;font-size:12.5px;color:var(--dsw-alias-label-tertiary);background:0 0;border:0;padding:6px 0 2px;cursor:pointer;align-self:flex-start;transition:color .12s}
|
|
903
994
|
.dsh-tp-more:hover{color:var(--dsw-alias-label-primary)}
|
|
904
995
|
.dsh-tp-adv{display:flex;flex-direction:column}
|
|
@@ -906,21 +997,29 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
906
997
|
.dsh-tp-input::placeholder{color:var(--dsw-alias-label-tertiary)}
|
|
907
998
|
.dsh-tp-mini{font:inherit;font-size:12.5px;line-height:20px;padding:5px 12px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer}
|
|
908
999
|
.dsh-tp-mini:hover{background:var(--dsw-alias-bg-layer-1)}
|
|
909
|
-
.dsh-tp-custom{display:flex;flex-direction:column;gap:10px;padding:12px;border-radius:14px;border:.5px solid var(--dsw-alias-border-
|
|
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)}
|
|
910
1001
|
.dsh-tp-custom-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
911
1002
|
.dsh-tp-custom-head b{font-size:13.5px;font-weight:500}
|
|
912
|
-
.dsh-tp-swatches{display:grid;grid-template-columns:repeat(
|
|
913
|
-
@media (max-width:520px){.dsh-tp-swatches{grid-template-columns:1fr}}
|
|
1003
|
+
.dsh-tp-swatches{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,86px),1fr));gap:10px}
|
|
914
1004
|
.dsh-tp-swatch{display:flex;flex-direction:column;gap:6px;cursor:pointer}
|
|
915
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}
|
|
916
1006
|
.dsh-tp-color::-webkit-color-swatch-wrapper{padding:3px}
|
|
917
1007
|
.dsh-tp-color::-webkit-color-swatch{border:none;border-radius:8px}
|
|
918
1008
|
.dsh-tp-swatch-name{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
919
1009
|
.dsh-tp-hex{font:11.5px/1 ui-monospace,Consolas,monospace;color:var(--dsw-alias-label-tertiary)}
|
|
920
|
-
.dsh-tp-
|
|
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)}
|
|
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)}
|
|
1014
|
+
.dsh-tp-custom-hint{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
921
1015
|
.dsh-tp-note{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}
|
|
922
1016
|
.dsh-tp-status{margin:0;font-size:11.5px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
923
1017
|
.dsh-tp-status.is-bad{color:var(--dsw-alias-state-error-primary)}
|
|
1018
|
+
.dsh-tp-field{display:flex;flex-direction:column;gap:8px;padding:11px 0;border-top:.5px solid var(--dsw-alias-border-l2)}
|
|
1019
|
+
.dsh-tp-field-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
1020
|
+
.dsh-tp-field-title{font-size:13.5px;font-weight:500}
|
|
1021
|
+
.dsh-tp-input{text-overflow:ellipsis}
|
|
1022
|
+
@media (prefers-reduced-motion:reduce){.dsh-tp-card,.dsh-tp-seg button,.dsh-tp-more,.dsh-tp-check,.dsh-tp-mini{transition:none}}
|
|
924
1023
|
`
|
|
925
1024
|
|
|
926
1025
|
function installRightbarStyle() {
|
|
@@ -962,24 +1061,57 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
962
1061
|
*/
|
|
963
1062
|
const TOKEN_PROBE = [
|
|
964
1063
|
'--dsw-static-neutral-bluish-00',
|
|
1064
|
+
'--dsw-static-neutral-bluish-600',
|
|
965
1065
|
'--dsw-static-neutral-bluish-1000',
|
|
1066
|
+
'--dsw-static-deepseek-500',
|
|
966
1067
|
'--dsw-alias-link',
|
|
967
1068
|
'--dsw-specific-sidebar-fill',
|
|
968
1069
|
'--dsw-alias-label-caption',
|
|
969
1070
|
]
|
|
970
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
|
+
*/
|
|
971
1099
|
function probeApplied(tokens) {
|
|
972
1100
|
try {
|
|
973
1101
|
if (typeof getComputedStyle !== 'function') return null
|
|
974
1102
|
const style = getComputedStyle(document.body)
|
|
975
1103
|
let checked = 0
|
|
976
|
-
|
|
1104
|
+
const mismatched = []
|
|
977
1105
|
for (const name of TOKEN_PROBE) {
|
|
978
|
-
|
|
1106
|
+
const pair = tokens[name]
|
|
1107
|
+
if (!pair) continue
|
|
979
1108
|
checked++
|
|
980
|
-
|
|
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)
|
|
981
1113
|
}
|
|
982
|
-
return checked === 0 ? null : { ok, checked }
|
|
1114
|
+
return checked === 0 ? null : { ok: mismatched.length === 0, checked, mismatched }
|
|
983
1115
|
} catch (err) {
|
|
984
1116
|
return null
|
|
985
1117
|
}
|
|
@@ -994,6 +1126,11 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
994
1126
|
return 'mono'
|
|
995
1127
|
}
|
|
996
1128
|
|
|
1129
|
+
/** 是否已经有用户配置:新装用户不需要看“行为变更”提示。 */
|
|
1130
|
+
function hasStoredConfig() {
|
|
1131
|
+
try { return localStorage.getItem(STORE_KEY) !== null } catch (err) { return false }
|
|
1132
|
+
}
|
|
1133
|
+
|
|
997
1134
|
function writeStored(id) {
|
|
998
1135
|
try { localStorage.setItem(STORE_KEY, id) } catch (err) { /* 记不住不影响本次生效 */ }
|
|
999
1136
|
}
|
|
@@ -1082,14 +1219,30 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1082
1219
|
try { localStorage.setItem(HC_KEY, id) } catch (err) { /* 同理 */ }
|
|
1083
1220
|
}
|
|
1084
1221
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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`(自定义三色),解码时兼容:旧码仍可导入,只是不带自定义色。
|
|
1088
1236
|
*/
|
|
1089
1237
|
const CONFIG_PREFIX = 'dshs1.'
|
|
1090
1238
|
|
|
1091
1239
|
function encodeConfig(state) {
|
|
1092
|
-
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
|
+
}
|
|
1093
1246
|
try {
|
|
1094
1247
|
const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(raw))))
|
|
1095
1248
|
return CONFIG_PREFIX + b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
@@ -1118,7 +1271,12 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1118
1271
|
hc: hasId(HC_MODES, raw.h) ? raw.h : null,
|
|
1119
1272
|
lang: raw.l === 'auto' || LOCALES.indexOf(raw.l) !== -1 ? raw.l : null,
|
|
1120
1273
|
}
|
|
1121
|
-
|
|
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
|
|
1122
1280
|
} catch (err) { return null }
|
|
1123
1281
|
}
|
|
1124
1282
|
|
|
@@ -1172,14 +1330,10 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1172
1330
|
nav: '主题色',
|
|
1173
1331
|
hint: '配色与明暗模式独立;切换后立即生效并保存在 localStorage。',
|
|
1174
1332
|
md: 'Markdown 强调',
|
|
1175
|
-
mdHint: '标题与加粗用「正文色混强调色」,比正文更有色;关掉则与正文同色。',
|
|
1176
1333
|
control: '开关 / 勾选',
|
|
1177
|
-
controlHint: '宿主的开关、复选框、单选都压在近黑/近白上;开启后改用强调色。',
|
|
1178
1334
|
scroll: '滚动条',
|
|
1179
|
-
scrollHint: '默认中性灰;「跟随」上配色色相,「强调」用强调色。明暗关系不变。',
|
|
1180
1335
|
scrollHintMono: '当前是黑白配色,没有色相可跟随,滚动条保持中性灰。',
|
|
1181
1336
|
rightbar: '右侧栏底色',
|
|
1182
|
-
rightbarHint: '右栏本来没有自己的背景,露的是会话区画布色。',
|
|
1183
1337
|
advanced: '更多选项',
|
|
1184
1338
|
textSub: '正文与强调的着色程度',
|
|
1185
1339
|
sidebarSub: '相对会话区的层次',
|
|
@@ -1188,42 +1342,43 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1188
1342
|
scrollSub: '滚动条与滑轨的色相',
|
|
1189
1343
|
controlSub: '开关与复选框是否跟强调色',
|
|
1190
1344
|
langSub: '自动跟随宿主界面语言',
|
|
1191
|
-
surface: '面板表面',
|
|
1192
|
-
surfaceHint: '决定侧栏与右栏相对会话区是「浮起」还是「凹陷」。',
|
|
1193
|
-
ok: '✓ 配色已生效',
|
|
1194
1345
|
bad: '⚠ 部分 token 未生效(宿主可能改名),请查看控制台',
|
|
1195
1346
|
healed: '配色层被宿主丢过一次,已自动恢复 ',
|
|
1196
1347
|
healedSuffix: ' 次',
|
|
1197
1348
|
text: '强度',
|
|
1198
|
-
textHint: '让正文/次级文字带配色色味;三档都满足 AA。',
|
|
1199
1349
|
sidebar: '左侧栏底色',
|
|
1200
|
-
sidebarHint: '让侧栏比会话区更浅(或更深),做出区块层次。',
|
|
1201
1350
|
lang: '语言',
|
|
1202
1351
|
hc: '高对比',
|
|
1203
1352
|
custom: '自定义配色',
|
|
1204
|
-
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: '将覆盖:',
|
|
1205
1361
|
config: '配置',
|
|
1206
|
-
configHint: '
|
|
1362
|
+
configHint: '导出这台机器的配色选择(含自定义三色),或粘贴短码导入;导入会覆盖全部 9 项。',
|
|
1207
1363
|
reset: '恢复默认',
|
|
1208
1364
|
copy: '复制',
|
|
1209
1365
|
applyImport: '导入',
|
|
1210
1366
|
copied: '已复制',
|
|
1367
|
+
copyFail: '复制失败,请手选后 Ctrl+C',
|
|
1211
1368
|
importOk: '已导入',
|
|
1369
|
+
importLegacy: '已导入(旧短码不含自定义三色)',
|
|
1212
1370
|
importBad: '短码无效,已忽略',
|
|
1213
|
-
|
|
1371
|
+
badHex: 'HEX 需为 #RRGGBB',
|
|
1372
|
+
hcSub: '尽量把文字下限抬到 AAA(正文 ≥15、次文 ≥9、三级 ≥7);深色模块面的上限受表面亮度限制。',
|
|
1214
1373
|
},
|
|
1215
1374
|
'zh-TW': {
|
|
1216
1375
|
nav: '主題色',
|
|
1217
1376
|
hint: '配色與明暗模式獨立;切換後立即生效並存在 localStorage。',
|
|
1218
1377
|
md: 'Markdown 強調',
|
|
1219
|
-
mdHint: '標題與粗體用「本文色混強調色」,比本文更有色;關掉則與本文同色。',
|
|
1220
1378
|
control: '開關 / 勾選',
|
|
1221
|
-
controlHint: '宿主開關、核取方塊、單選都用近黑/近白;開啟後改用強調色。',
|
|
1222
1379
|
scroll: '捲軸',
|
|
1223
|
-
scrollHint: '預設中性灰;「跟隨」上配色色相,「強調」用強調色。明暗關係不變。',
|
|
1224
1380
|
scrollHintMono: '目前是黑白配色,沒有色相可跟隨,捲軸保持中性灰。',
|
|
1225
1381
|
rightbar: '右側欄底色',
|
|
1226
|
-
rightbarHint: '右欄本來沒有自己的背景,露的是會話區畫布色。',
|
|
1227
1382
|
advanced: '更多選項',
|
|
1228
1383
|
textSub: '本文與強調的著色程度',
|
|
1229
1384
|
sidebarSub: '相對會話區的層次',
|
|
@@ -1232,42 +1387,43 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1232
1387
|
scrollSub: '捲軸與軌道的色相',
|
|
1233
1388
|
controlSub: '開關與核取方塊是否跟強調色',
|
|
1234
1389
|
langSub: '自動跟隨宿主介面語言',
|
|
1235
|
-
surface: '面板表面',
|
|
1236
|
-
surfaceHint: '決定側欄與右欄相對會話區是「浮起」還是「凹陷」。',
|
|
1237
|
-
ok: '✓ 配色已生效',
|
|
1238
1390
|
bad: '⚠ 部分 token 未生效(宿主可能改名),請看主控台',
|
|
1239
1391
|
healed: '配色層被宿主丟棄過一次,已自動恢復 ',
|
|
1240
1392
|
healedSuffix: ' 次',
|
|
1241
1393
|
text: '強度',
|
|
1242
|
-
textHint: '讓本文/次級文字帶配色色味;三檔都符合 AA。',
|
|
1243
1394
|
sidebar: '左側欄底色',
|
|
1244
|
-
sidebarHint: '讓側欄比會話區更淺(或更深),做出區塊層次。',
|
|
1245
1395
|
lang: '語言',
|
|
1246
1396
|
hc: '高對比',
|
|
1247
1397
|
custom: '自訂配色',
|
|
1248
|
-
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: '將覆蓋:',
|
|
1249
1406
|
config: '設定',
|
|
1250
|
-
configHint: '
|
|
1407
|
+
configHint: '匯出這台機器的配色選擇(含自訂三色),或貼上短碼匯入;匯入會覆蓋全部 9 項。',
|
|
1251
1408
|
reset: '恢復預設',
|
|
1252
1409
|
copy: '複製',
|
|
1253
1410
|
applyImport: '匯入',
|
|
1254
1411
|
copied: '已複製',
|
|
1412
|
+
copyFail: '複製失敗,請手選後 Ctrl+C',
|
|
1255
1413
|
importOk: '已匯入',
|
|
1414
|
+
importLegacy: '已匯入(舊短碼不含自訂三色)',
|
|
1256
1415
|
importBad: '短碼無效,已忽略',
|
|
1257
|
-
|
|
1416
|
+
badHex: 'HEX 需為 #RRGGBB',
|
|
1417
|
+
hcSub: '盡量把文字下限抬到 AAA(正文 ≥15、次文 ≥9、三級 ≥7);深色模組面的上限受表面亮度限制。',
|
|
1258
1418
|
},
|
|
1259
1419
|
en: {
|
|
1260
1420
|
nav: 'Color theme',
|
|
1261
1421
|
hint: 'Independent of light/dark mode. Applies immediately and is saved to localStorage.',
|
|
1262
1422
|
md: 'Markdown emphasis',
|
|
1263
|
-
mdHint: 'Headings and bold mix the accent into body colour; off keeps them identical to body text.',
|
|
1264
1423
|
control: 'Switches / checks',
|
|
1265
|
-
controlHint: 'Switches, checkboxes and radios use near-black/white in the host. Turn this on to use the accent colour.',
|
|
1266
1424
|
scroll: 'Scrollbars',
|
|
1267
|
-
scrollHint: 'Neutral grey by default. Tint follows the palette hue; Accent uses the accent colour. Lightness is unchanged.',
|
|
1268
1425
|
scrollHintMono: 'Monochrome has no hue to follow; scrollbars stay neutral.',
|
|
1269
1426
|
rightbar: 'Right panel surface',
|
|
1270
|
-
rightbarHint: 'The right panel has no fill of its own; it shows the canvas.',
|
|
1271
1427
|
advanced: 'More options',
|
|
1272
1428
|
textSub: 'How strongly text follows the palette',
|
|
1273
1429
|
sidebarSub: 'Layer relative to the conversation',
|
|
@@ -1276,42 +1432,43 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1276
1432
|
scrollSub: 'Hue of the scrollbar',
|
|
1277
1433
|
controlSub: 'Switches and checkboxes',
|
|
1278
1434
|
langSub: 'Follow the host interface language',
|
|
1279
|
-
surface: 'Panel surfaces',
|
|
1280
|
-
surfaceHint: 'Whether the sidebars sit above or below the conversation area.',
|
|
1281
|
-
ok: '✓ Palette applied',
|
|
1282
1435
|
bad: '⚠ Some tokens did not apply (host may have renamed them); check the console',
|
|
1283
1436
|
healed: 'Palette layer was dropped by the host; restored ',
|
|
1284
1437
|
healedSuffix: 'x',
|
|
1285
1438
|
text: 'Strength',
|
|
1286
|
-
textHint: 'Tints body and secondary text toward the palette; all three levels meet AA.',
|
|
1287
1439
|
sidebar: 'Sidebar surface',
|
|
1288
|
-
sidebarHint: 'Makes the sidebar lighter (or deeper) than the conversation area.',
|
|
1289
1440
|
lang: 'Language',
|
|
1290
1441
|
hc: 'High contrast',
|
|
1291
1442
|
custom: 'Custom palette',
|
|
1292
|
-
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:',
|
|
1293
1451
|
config: 'Config',
|
|
1294
|
-
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.',
|
|
1295
1453
|
reset: 'Reset',
|
|
1296
1454
|
copy: 'Copy',
|
|
1297
1455
|
applyImport: 'Import',
|
|
1298
1456
|
copied: 'Copied',
|
|
1457
|
+
copyFail: 'Copy failed — select the code and press Ctrl+C',
|
|
1299
1458
|
importOk: 'Imported',
|
|
1459
|
+
importLegacy: 'Imported (old code has no custom colours)',
|
|
1300
1460
|
importBad: 'Invalid code, ignored',
|
|
1301
|
-
|
|
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.',
|
|
1302
1463
|
},
|
|
1303
1464
|
ja: {
|
|
1304
1465
|
nav: 'カラーテーマ',
|
|
1305
1466
|
hint: 'ライト/ダークとは独立です。変更は即時反映され、localStorage に保存されます。',
|
|
1306
1467
|
md: 'Markdown の強調',
|
|
1307
|
-
mdHint: '見出しと太字にアクセント色を混ぜます。オフにすると本文と同じ色になります。',
|
|
1308
1468
|
control: 'スイッチ / チェック',
|
|
1309
|
-
controlHint: 'ホストのスイッチ・チェックボックスはほぼ白黒です。オンにするとアクセント色になります。',
|
|
1310
1469
|
scroll: 'スクロールバー',
|
|
1311
|
-
scrollHint: '既定はニュートラルグレー。「追従」でテーマの色相、「アクセント」でアクセント色になります。',
|
|
1312
1470
|
scrollHintMono: 'モノクロには色相がないため、スクロールバーはグレーのままです。',
|
|
1313
1471
|
rightbar: '右パネルの背景',
|
|
1314
|
-
rightbarHint: '右パネルは独自の背景を持たず、キャンバス色が見えています。',
|
|
1315
1472
|
advanced: 'その他のオプション',
|
|
1316
1473
|
textSub: '本文と強調の色づき具合',
|
|
1317
1474
|
sidebarSub: '会話エリアとの段差',
|
|
@@ -1320,29 +1477,34 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1320
1477
|
scrollSub: 'スクロールバーの色相',
|
|
1321
1478
|
controlSub: 'スイッチとチェックの色',
|
|
1322
1479
|
langSub: 'ホストの表示言語に合わせます',
|
|
1323
|
-
surface: 'パネル表面',
|
|
1324
|
-
surfaceHint: 'サイドバーが会話エリアより浮くか沈むかを決めます。',
|
|
1325
|
-
ok: '✓ 配色を適用しました',
|
|
1326
1480
|
bad: '⚠ 一部の token が適用されていません(ホスト側で改名された可能性)。コンソールを確認してください',
|
|
1327
1481
|
healed: '配色レイヤーがホストに破棄されたため、自動で復元しました(',
|
|
1328
1482
|
healedSuffix: ' 回)',
|
|
1329
1483
|
text: '強さ',
|
|
1330
|
-
textHint: '本文と副次テキストをテーマ色に寄せます。3 段階とも AA を満たします。',
|
|
1331
1484
|
sidebar: '左サイドバーの背景',
|
|
1332
|
-
sidebarHint: 'サイドバーを会話エリアより明るく(または暗く)して段差を作ります。',
|
|
1333
1485
|
lang: '言語',
|
|
1334
1486
|
hc: 'ハイコントラスト',
|
|
1335
1487
|
custom: 'カスタム配色',
|
|
1336
|
-
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: '上書きする内容:',
|
|
1337
1496
|
config: '設定の共有',
|
|
1338
|
-
configHint: '
|
|
1497
|
+
configHint: 'この端末の配色設定(カスタム 3 色を含む)を書き出す/短いコードを貼り付けて読み込みます(9 項目すべて上書き)。',
|
|
1339
1498
|
reset: 'リセット',
|
|
1340
1499
|
copy: 'コピー',
|
|
1341
1500
|
applyImport: '読み込み',
|
|
1342
1501
|
copied: 'コピーしました',
|
|
1502
|
+
copyFail: 'コピーできません。選択して Ctrl+C してください',
|
|
1343
1503
|
importOk: '読み込みました',
|
|
1504
|
+
importLegacy: '読み込みました(旧コードにカスタム 3 色は含まれません)',
|
|
1344
1505
|
importBad: '無効なコードです',
|
|
1345
|
-
|
|
1506
|
+
badHex: 'HEX は #RRGGBB 形式で入力してください',
|
|
1507
|
+
hcSub: '本文の下限を AAA に近づけます(本文 ≥15、副次 ≥9、三次 ≥7)。暗いモジュール面は表面の明るさで上限が決まります。',
|
|
1346
1508
|
},
|
|
1347
1509
|
}
|
|
1348
1510
|
|
|
@@ -1392,8 +1554,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1392
1554
|
desc: { mono: '色なし · 既定', warm: 'クリーム + ブラウン', indigo: 'クールホワイト + ブルー', moss: 'グレーグリーン', custom: '3 色から派生' },
|
|
1393
1555
|
text: { neutral: '控えめ', tinted: '標準', deep: '強め' },
|
|
1394
1556
|
sidebar: { light: '明るく', match: '同じ', deep: '暗く' },
|
|
1395
|
-
rightbar: { sidebar: '
|
|
1396
|
-
scroll: { neutral: '
|
|
1557
|
+
rightbar: { sidebar: 'サイドバー', canvas: 'キャンバス' },
|
|
1558
|
+
scroll: { neutral: '無彩', tinted: '追従', accent: '強調' },
|
|
1397
1559
|
control: { accent: 'アクセント', mono: 'モノクロ' },
|
|
1398
1560
|
md: { on: '色づけ', off: '本文と同じ' },
|
|
1399
1561
|
lang: { auto: '自動' },
|
|
@@ -1403,6 +1565,11 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1403
1565
|
}
|
|
1404
1566
|
|
|
1405
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
|
+
|
|
1406
1573
|
function PaletteSection(props) {
|
|
1407
1574
|
const current = React.useSyncExternalStore(props.subscribe, props.getCurrent)
|
|
1408
1575
|
const sidebarMode = React.useSyncExternalStore(props.subscribeSidebar, props.getSidebarMode)
|
|
@@ -1415,36 +1582,98 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1415
1582
|
const health = React.useSyncExternalStore(props.subscribeHealth, props.getHealth)
|
|
1416
1583
|
const [importText, setImportText] = React.useState('')
|
|
1417
1584
|
const [importState, setImportState] = React.useState(null)
|
|
1585
|
+
const [importPreview, setImportPreview] = React.useState(null)
|
|
1586
|
+
const [copyState, setCopyState] = React.useState(null)
|
|
1418
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)
|
|
1419
1595
|
const locale = React.useSyncExternalStore(props.subscribeLocale, props.getLocale)
|
|
1420
1596
|
const langPref = React.useSyncExternalStore(props.subscribeLocale, props.getLangPref)
|
|
1597
|
+
const langAttr = langPref !== 'auto'
|
|
1598
|
+
? langPref
|
|
1599
|
+
: ((typeof document !== 'undefined' && document.documentElement && document.documentElement.lang) || 'zh-CN')
|
|
1421
1600
|
const txt = TEXT[locale] || TEXT['zh-CN']
|
|
1422
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
|
+
}
|
|
1423
1607
|
|
|
1424
|
-
|
|
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
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// A6:读屏需要知道"这是哪个设置的选项组";单选用 radio 语义更准
|
|
1629
|
+
const seg = (modes, value, onPick, labels, labelId, disabled) => h(
|
|
1425
1630
|
'div',
|
|
1426
|
-
{
|
|
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
|
+
},
|
|
1427
1638
|
modes.map((mode) => h(
|
|
1428
1639
|
'button',
|
|
1429
1640
|
{
|
|
1430
1641
|
key: mode.id,
|
|
1431
1642
|
type: 'button',
|
|
1643
|
+
role: 'radio',
|
|
1432
1644
|
className: mode.id === value ? 'is-on' : undefined,
|
|
1433
|
-
'aria-
|
|
1434
|
-
|
|
1645
|
+
'aria-checked': mode.id === value,
|
|
1646
|
+
'aria-disabled': disabled ? 'true' : undefined,
|
|
1647
|
+
// roving tabindex:只有选中项在 Tab 序列里,其余靠方向键进入
|
|
1648
|
+
tabIndex: mode.id === value ? 0 : -1,
|
|
1649
|
+
disabled: disabled === true,
|
|
1650
|
+
onClick: () => { if (!disabled) onPick(mode.id) },
|
|
1435
1651
|
},
|
|
1436
1652
|
(labels && labels[mode.id]) || mode.label,
|
|
1437
1653
|
)),
|
|
1438
1654
|
)
|
|
1439
1655
|
|
|
1440
|
-
const
|
|
1656
|
+
const rowId = (key) => 'dsh-tp-row-' + key
|
|
1657
|
+
const row = (key, titleText, subText, control) => h(
|
|
1441
1658
|
'div',
|
|
1442
1659
|
{ className: 'dsh-tp-row' },
|
|
1443
|
-
h('div', { className: 'dsh-tp-row-label'
|
|
1660
|
+
h('div', { className: 'dsh-tp-row-label', id: rowId(key) + '-desc' },
|
|
1661
|
+
h('b', { id: rowId(key) }, titleText), h('span', null, subText)),
|
|
1444
1662
|
control,
|
|
1445
1663
|
)
|
|
1446
1664
|
|
|
1447
|
-
|
|
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
|
+
}
|
|
1448
1677
|
const customPanel = current === 'custom' && h(
|
|
1449
1678
|
'div',
|
|
1450
1679
|
{ className: 'dsh-tp-custom' },
|
|
@@ -1456,25 +1685,68 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1456
1685
|
onClick: () => props.resetCustom(),
|
|
1457
1686
|
}, txt.reset)),
|
|
1458
1687
|
h('div', { className: 'dsh-tp-swatches' },
|
|
1459
|
-
['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
|
+
},
|
|
1460
1700
|
h('input', {
|
|
1461
1701
|
type: 'color',
|
|
1702
|
+
id: colorId,
|
|
1462
1703
|
className: 'dsh-tp-color',
|
|
1463
1704
|
value: customColors[field],
|
|
1464
1705
|
// input 是"拖动过程中"就触发,change 是"确认后"触发;两个都接,
|
|
1465
1706
|
// 拖动时实时看到整页变化,松手/确认后不会再跳一次
|
|
1466
1707
|
onInput: (e) => props.setCustomColor(field, e.target.value),
|
|
1467
1708
|
onChange: (e) => props.setCustomColor(field, e.target.value),
|
|
1468
|
-
'aria-label':
|
|
1709
|
+
'aria-label': name,
|
|
1710
|
+
}),
|
|
1711
|
+
h('label', { htmlFor: hexId, className: 'dsh-tp-swatch-name' }, name),
|
|
1712
|
+
h('input', {
|
|
1713
|
+
id: hexId,
|
|
1714
|
+
className: 'dsh-tp-hexinput',
|
|
1715
|
+
value: hexDraft[field] !== undefined ? hexDraft[field] : customColors[field],
|
|
1716
|
+
spellCheck: false,
|
|
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
|
+
},
|
|
1469
1733
|
}),
|
|
1470
|
-
h('span', { className: 'dsh-tp-
|
|
1471
|
-
|
|
1734
|
+
invalid && h('span', { id: errId, className: 'dsh-tp-hexerror', role: 'alert' }, txt.badHex))
|
|
1735
|
+
})),
|
|
1472
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
|
+
),
|
|
1473
1745
|
)
|
|
1474
1746
|
|
|
1475
1747
|
const card = (scheme) => {
|
|
1476
1748
|
const picked = scheme.id === current
|
|
1477
|
-
const c = cardColors(scheme, textMode)
|
|
1749
|
+
const c = cardColors(scheme, textMode, sidebarMode)
|
|
1478
1750
|
return h(
|
|
1479
1751
|
'button',
|
|
1480
1752
|
{
|
|
@@ -1502,16 +1774,20 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1502
1774
|
|
|
1503
1775
|
return h(
|
|
1504
1776
|
'section',
|
|
1505
|
-
{ className: 'dsh-tp' },
|
|
1777
|
+
{ className: 'dsh-tp', lang: langAttr },
|
|
1506
1778
|
h('div', { className: 'dsh-tp-head' },
|
|
1507
1779
|
h('h2', null, txt.nav),
|
|
1508
1780
|
h('span', { className: 'dsh-tp-badge' }, 'v' + PLUGIN_VERSION)),
|
|
1509
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)),
|
|
1510
1785
|
h('div', { className: 'dsh-tp-grid' }, SCHEMES.map(card)),
|
|
1511
1786
|
customPanel,
|
|
1512
|
-
row(txt.text, txt.textSub, seg(TEXT_MODES, textMode, props.setTextMode, lb.text)),
|
|
1513
|
-
row(txt.
|
|
1514
|
-
row(txt.
|
|
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'))),
|
|
1789
|
+
row('sidebar', txt.sidebar, txt.sidebarSub, seg(SIDEBAR_MODES, sidebarMode, props.setSidebarMode, lb.sidebar, rowId('sidebar'))),
|
|
1790
|
+
row('rightbar', txt.rightbar, txt.rightbarSub, seg(RIGHTBAR_MODES, rightbarMode, props.setRightbarMode, lb.rightbar, rowId('rightbar'))),
|
|
1515
1791
|
health && !health.ok && h('p', { className: 'dsh-tp-status is-bad' }, txt.bad),
|
|
1516
1792
|
health && health.ok && health.reapplies > 0 && h(
|
|
1517
1793
|
'p',
|
|
@@ -1527,50 +1803,96 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1527
1803
|
advanced && h(
|
|
1528
1804
|
'div',
|
|
1529
1805
|
{ className: 'dsh-tp-adv' },
|
|
1530
|
-
row(txt.lang, txt.langSub, seg(LANG_OPTIONS, langPref, props.setLangPref, lb.lang)),
|
|
1806
|
+
row('lang', txt.lang, txt.langSub, seg(LANG_OPTIONS, langPref, props.setLangPref, lb.lang, rowId('lang'))),
|
|
1531
1807
|
h(
|
|
1532
1808
|
'div',
|
|
1533
|
-
{
|
|
1534
|
-
h('div', {
|
|
1535
|
-
h('div', {
|
|
1809
|
+
{ className: 'dsh-tp-field' },
|
|
1810
|
+
h('div', { className: 'dsh-tp-field-title', id: rowId('config') }, txt.config),
|
|
1811
|
+
h('div', { className: 'dsh-tp-field-row' },
|
|
1536
1812
|
h('input', {
|
|
1537
1813
|
className: 'dsh-tp-input',
|
|
1538
1814
|
readOnly: true,
|
|
1539
1815
|
value: props.getConfigCode(),
|
|
1816
|
+
title: props.getConfigCode(),
|
|
1540
1817
|
onFocus: (e) => { try { e.target.select() } catch (err) { /* 忽略 */ } },
|
|
1541
|
-
'aria-label': txt.config,
|
|
1818
|
+
'aria-label': txt.config + ' — ' + txt.copy,
|
|
1542
1819
|
}),
|
|
1543
1820
|
h('button', {
|
|
1544
1821
|
type: 'button',
|
|
1545
1822
|
className: 'dsh-tp-mini',
|
|
1546
|
-
onClick: () => {
|
|
1823
|
+
onClick: async () => {
|
|
1547
1824
|
const code = props.getConfigCode()
|
|
1548
1825
|
try {
|
|
1549
|
-
if (navigator.clipboard
|
|
1550
|
-
|
|
1551
|
-
|
|
1826
|
+
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') throw new Error('no clipboard api')
|
|
1827
|
+
await navigator.clipboard.writeText(code)
|
|
1828
|
+
setCopyState('copied')
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
// 剪贴板可能被拒(无安全上下文/权限);不能谎报成功
|
|
1831
|
+
setCopyState('fail')
|
|
1832
|
+
}
|
|
1552
1833
|
},
|
|
1553
|
-
}, txt.copy)
|
|
1554
|
-
|
|
1834
|
+
}, txt.copy),
|
|
1835
|
+
copyState && h('span', { className: 'dsh-tp-note', role: 'status', 'aria-live': 'polite' },
|
|
1836
|
+
copyState === 'copied' ? txt.copied : txt.copyFail)),
|
|
1837
|
+
h('div', { className: 'dsh-tp-field-row' },
|
|
1555
1838
|
h('input', {
|
|
1556
1839
|
className: 'dsh-tp-input',
|
|
1557
1840
|
placeholder: 'dshs1.…',
|
|
1558
1841
|
value: importText,
|
|
1559
|
-
|
|
1842
|
+
'aria-label': txt.applyImport,
|
|
1843
|
+
onChange: (e) => {
|
|
1844
|
+
setImportText(e.target.value)
|
|
1845
|
+
setImportState(null)
|
|
1846
|
+
setImportPreview(null)
|
|
1847
|
+
},
|
|
1560
1848
|
}),
|
|
1561
1849
|
h('button', {
|
|
1562
1850
|
type: 'button',
|
|
1563
1851
|
className: 'dsh-tp-mini',
|
|
1564
|
-
onClick: () =>
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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),
|
|
1891
|
+
h('p', { className: 'dsh-tp-custom-hint' }, txt.configHint),
|
|
1569
1892
|
),
|
|
1570
|
-
row(txt.md, txt.mdSub, seg(MD_MODES, mdMode, props.setMdMode, lb.md)),
|
|
1571
|
-
row(txt.
|
|
1572
|
-
row(
|
|
1573
|
-
row(txt.control, txt.controlSub, seg(CONTROL_MODES, controlMode, props.setControlMode, lb.control)),
|
|
1893
|
+
row('md', txt.md, txt.mdSub, seg(MD_MODES, mdMode, props.setMdMode, lb.md, rowId('md'))),
|
|
1894
|
+
row('scroll', txt.scroll, current === 'mono' ? txt.scrollHintMono : txt.scrollSub, seg(SCROLL_MODES, scrollMode, props.setScrollMode, lb.scroll, rowId('scroll'), current === 'mono')),
|
|
1895
|
+
row('control', txt.control, txt.controlSub, seg(CONTROL_MODES, controlMode, props.setControlMode, lb.control, rowId('control'), current === 'mono')),
|
|
1574
1896
|
),
|
|
1575
1897
|
)
|
|
1576
1898
|
}
|
|
@@ -1594,7 +1916,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1594
1916
|
|
|
1595
1917
|
// 开发期接口:palette-dev.mjs 通过它拿到同一套引擎(避免「报表好看、上线不一样」)
|
|
1596
1918
|
if (DEV_OPT_IN && typeof window !== 'undefined' && typeof window.__DSH_THEME_PALETTE_DEV__ === 'function') {
|
|
1597
|
-
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 })
|
|
1598
1920
|
}
|
|
1599
1921
|
|
|
1600
1922
|
return {
|
|
@@ -1607,6 +1929,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1607
1929
|
}
|
|
1608
1930
|
|
|
1609
1931
|
let current = readStored()
|
|
1932
|
+
// 只有升级前已有配置的用户才需要“行为变更”提示
|
|
1933
|
+
const hadConfig = hasStoredConfig()
|
|
1610
1934
|
let sidebarMode = readSidebarStored()
|
|
1611
1935
|
let textMode = readTextStored()
|
|
1612
1936
|
let rightbarMode = readRightbarStored()
|
|
@@ -1627,6 +1951,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1627
1951
|
const controlListeners = new Set()
|
|
1628
1952
|
const mdListeners = new Set()
|
|
1629
1953
|
const hcListeners = new Set()
|
|
1954
|
+
const customListeners = new Set()
|
|
1630
1955
|
|
|
1631
1956
|
const getCurrent = () => current
|
|
1632
1957
|
const getSidebarMode = () => sidebarMode
|
|
@@ -1676,6 +2001,27 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1676
2001
|
return () => { hcListeners.delete(fn) }
|
|
1677
2002
|
}
|
|
1678
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
|
+
}
|
|
1679
2025
|
|
|
1680
2026
|
let disposeLayer = () => {}
|
|
1681
2027
|
/** 按当前配色 + 侧栏档位重建覆盖层(换配色与换侧栏档位走同一条路)。 */
|
|
@@ -1765,7 +2111,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1765
2111
|
const run = () => {
|
|
1766
2112
|
const next = probeApplied(tokens)
|
|
1767
2113
|
if (next === null) return
|
|
1768
|
-
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
|
|
1769
2116
|
health = { ...next, reapplies: reapplyCount }
|
|
1770
2117
|
if (!next.ok) console.warn('[dsh-theme-palette] 有 token 没写进 body,宿主可能改名了')
|
|
1771
2118
|
emitHealth()
|
|
@@ -1822,10 +2169,10 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1822
2169
|
applyAll()
|
|
1823
2170
|
emitMd()
|
|
1824
2171
|
}
|
|
1825
|
-
/**
|
|
2172
|
+
/** 导入:整份校验通过才落地;返回结果供 UI 显示(旧码不含自定义三色时 hasCustom=false) */
|
|
1826
2173
|
const applyConfigCode = (code) => {
|
|
1827
2174
|
const cfg = decodeConfig(code)
|
|
1828
|
-
if (!cfg) return false
|
|
2175
|
+
if (!cfg) return { ok: false, hasCustom: false }
|
|
1829
2176
|
current = cfg.palette
|
|
1830
2177
|
sidebarMode = cfg.sidebar
|
|
1831
2178
|
textMode = cfg.text
|
|
@@ -1835,6 +2182,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1835
2182
|
mdMode = cfg.md
|
|
1836
2183
|
hcMode = cfg.hc
|
|
1837
2184
|
langPref = cfg.lang
|
|
2185
|
+
if (cfg.custom) writeCustom(cfg.custom)
|
|
1838
2186
|
writeStored(current)
|
|
1839
2187
|
writeSidebarStored(sidebarMode)
|
|
1840
2188
|
writeTextStored(textMode)
|
|
@@ -1846,21 +2194,51 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1846
2194
|
writeLangPref(langPref)
|
|
1847
2195
|
applyAll('import')
|
|
1848
2196
|
emit(); emitSidebar(); emitText(); emitRightbar(); emitScroll(); emitControl(); emitMd(); emitHc(); emitLocale()
|
|
1849
|
-
|
|
2197
|
+
if (cfg.custom) emitCustom()
|
|
2198
|
+
return { ok: true, hasCustom: !!cfg.custom }
|
|
1850
2199
|
}
|
|
1851
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
|
+
}
|
|
1852
2229
|
const resetCustom = () => {
|
|
1853
2230
|
writeCustom(Object.assign({}, CUSTOM_DEFAULT))
|
|
1854
2231
|
applyAll('custom:reset')
|
|
1855
|
-
|
|
2232
|
+
emitCustom()
|
|
1856
2233
|
}
|
|
1857
2234
|
const setCustomColor = (field, hex) => {
|
|
1858
|
-
if (CUSTOM_FIELDS.indexOf(field) === -1) return
|
|
1859
|
-
if (typeof hex !== 'string' || !HEX_RE.test(hex)) return
|
|
1860
|
-
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() })
|
|
1861
2238
|
writeCustom(next)
|
|
1862
2239
|
applyAll('custom:' + field)
|
|
1863
|
-
|
|
2240
|
+
emitCustom()
|
|
2241
|
+
return true
|
|
1864
2242
|
}
|
|
1865
2243
|
const setHcMode = (id) => {
|
|
1866
2244
|
if (!HC_MODES.some((m) => m.id === id)) return
|
|
@@ -1995,6 +2373,12 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
|
|
|
1995
2373
|
getConfigCode,
|
|
1996
2374
|
applyConfigCode,
|
|
1997
2375
|
getCustomColors,
|
|
2376
|
+
getCustomExplain,
|
|
2377
|
+
previewConfigCode,
|
|
2378
|
+
subscribeNotice,
|
|
2379
|
+
getNoticePending,
|
|
2380
|
+
dismissNotice,
|
|
2381
|
+
subscribeCustom,
|
|
1998
2382
|
setCustomColor,
|
|
1999
2383
|
resetCustom,
|
|
2000
2384
|
getLangPref,
|