dsh-theme-gallery 0.3.0 → 0.4.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/lib/client.js CHANGED
@@ -33,6 +33,21 @@
33
33
  * one plugin is one sidebar entrance plus one main-column page — so it has room
34
34
  * for previews, descriptions and whatever this gallery grows into.
35
35
  *
36
+ * ## 出处与致谢(这是别人的劳动成果,必须写清楚)
37
+ *
38
+ * - **纯色/拼色卡片上的 15 个色值,全部取自「中国传统色库」项目**
39
+ * (chinese-colors):<https://github.com/zerosoul/chinese-colors>
40
+ * 作者 **tristan**(GitHub [@zerosoul](https://github.com/zerosoul)),
41
+ * 在线手册 <https://colors.ichuantong.cn>,许可 ISC。
42
+ * 该项目的 `src/assets/colors.json` 整理了 170 个中国传统色的名字与色值,
43
+ * 本插件只做**取色与派生**(每套方案只挑三四个名字色,其余 token 由
44
+ * `schemeTokens()` 按同一条造色规则算出来)。色值与命名是作者整理的成果,
45
+ * **感谢作者 tristan 的整理与开源**。取色数据见 `lib/palette-schemes.json`,
46
+ * 每套方案都带 `source` 字段记着它在色库里的「组 + id」。
47
+ * - 七套**复刻**皮肤的场景与配色复刻自**蜂链商城**电商新零售系统管理后台的主题,
48
+ * 项目地址:<https://github.com/renjie2026/fenglianshop-open>(该项目的开发者即本插件作者)。
49
+ * - 两套宠物主题(琥珀猫咪、虎子阿黄)为本插件原创。
50
+ *
36
51
  * ## Two conversational states
37
52
  *
38
53
  * A skin covers the screen; a screen covered by a gradient is what makes a long
@@ -124,6 +139,7 @@ window.__ModuleLoader__.load({
124
139
  const CARD_ORDER = {
125
140
  light: 99,
126
141
  dark: 98,
142
+ 'shi-liu-jin': 97,
127
143
  'ying-mu-cai-yun': 96,
128
144
  'shan-qing-ting-cai': 95,
129
145
  'pei-an-jie-xin': 91,
@@ -180,6 +196,284 @@ window.__ModuleLoader__.load({
180
196
  return [...themes].sort((a, b) => cardRank(b.id) - cardRank(a.id))
181
197
  }
182
198
 
199
+ /** 配色方案 id 的形态:`p-` 前缀,避免与主题 id 混淆(锚主题就叫 `shi-liu-jin`)。 */
200
+ const SCHEME_ID = /^p-[a-z0-9-]+$/
201
+
202
+ /**
203
+ * 15 套「纯色/拼色」配色方案的落点。
204
+ *
205
+ * 声明在这里、赋值在文件下方(紧接内联的 `BUNDLED_PALETTES`)—— 工厂体自上而下执行,
206
+ * 而 `const BUNDLED_PALETTES` 内联在主题数组之后,所以这里只能先给一个空表。
207
+ * 表为空时**不会**静默画错:`cardRowShape` 找不到方案就退回默认色带,
208
+ * 而建期(`embed-themes.mjs`)与测试都要求每个方案 id 真实存在。
209
+ */
210
+ let PALETTE_SCHEMES = []
211
+
212
+ /** 配色卡自己的主题 id("锚"):只有它是活动主题时,所选方案才有效。 */
213
+ const PALETTE_ANCHOR = 'shi-liu-jin'
214
+
215
+ /** 所选配色方案记在这里。与皮肤 id 一样,**绝不**写进 `ui-theme.preference`。 */
216
+ const SCHEME_KEY = 'theme-gallery:palette'
217
+
218
+ /**
219
+ * 按 id 取一套配色方案。
220
+ * @param id - 方案 id。
221
+ * @param schemes - 方案表(显式传入,便于测试与预览各自喂数据)。
222
+ * @returns 方案,或 undefined。
223
+ */
224
+ function schemeById(id, schemes) {
225
+ const list = Array.isArray(schemes) ? schemes : []
226
+ return list.find((scheme) => scheme !== null && typeof scheme === 'object' && scheme.id === id)
227
+ }
228
+
229
+ /**
230
+ * 两个 6 位 hex 之间线性插值。
231
+ * @param a - 起点 `#rrggbb`。
232
+ * @param b - 终点 `#rrggbb`。
233
+ * @param t - 0 = a,1 = b。
234
+ * @returns `#rrggbb`。
235
+ */
236
+ function blendHex(a, b, t) {
237
+ const parts = (hex) => [1, 3, 5].map((at) => parseInt(hex.slice(at, at + 2), 16))
238
+ const [r1, g1, b1] = parts(a)
239
+ const [r2, g2, b2] = parts(b)
240
+ const mix = (x, y) => Math.round(x + (y - x) * t).toString(16).padStart(2, '0')
241
+ return `#${mix(r1, r2)}${mix(g1, g2)}${mix(b1, b2)}`
242
+ }
243
+
244
+ /**
245
+ * 把颜色往白(t > 0)或往黑(t < 0)推。
246
+ *
247
+ * 15 套配色里除了主色与底色,其余 60 多个 token 全靠它派生 —— 这样 15 套不会各写各的,
248
+ * 也就不会彼此漂移。压深出来的色值不是色库里的名字色,这是**派生的深浅阶**,
249
+ * 与既有皮肤的做法一致(例如琥珀猫咪用「琥珀深焙」做按钮族、`琥珀` 做强调色)。
250
+ * @param hex - `#rrggbb`。
251
+ * @param t - 正数提亮、负数压深,绝对值是幅度。
252
+ * @returns `#rrggbb`。
253
+ */
254
+ function shade(hex, t) {
255
+ if (!/^#[0-9a-fA-F]{6}$/.test(String(hex))) return hex
256
+ return t >= 0 ? blendHex(hex, '#FFFFFF', t) : blendHex(hex, '#000000', -t)
257
+ }
258
+
259
+ /**
260
+ * 卡片上那 15 个色值按钮的数据。
261
+ *
262
+ * ── 这一版换了语义 ─────────────────────────────────────────────────────────
263
+ *
264
+ * 早先 `card.rows` 直接写十六进制色块(纯展示)。现在每一格是**一个可点的配色方案 id**:
265
+ * 颜色从方案表来,所以卡片显示什么、点下去换什么,是同一条数据,不可能对不上。
266
+ *
267
+ * 仍然按老规矩消毒:`embed-themes.mjs` 校验的是本包出货的皮肤,而注册表里还有别的插件
268
+ * 贡献的主题;坏数据在这里退回默认色带,而不是把 `undefined` 画进背景。
269
+ * @param theme - 注册表里的主题定义。
270
+ * @param schemes - 配色方案表。
271
+ * @returns 两到三排 `{ kind, schemes }`,或 undefined(回落默认色带)。
272
+ */
273
+ function cardRowShape(theme, schemes) {
274
+ const card = theme.card
275
+ const rows = card === undefined || card === null ? undefined : card.rows
276
+ if (!Array.isArray(rows) || rows.length < 2 || rows.length > 3) return undefined
277
+ const clean = []
278
+ for (let index = 0; index < rows.length; index += 1) {
279
+ const row = rows[index]
280
+ if (row === null || typeof row !== 'object') return undefined
281
+ // 最后一排是「拼色」,前面都是「纯色」:这是用户定的版式,也让"哪一排是拼色"
282
+ // 不再靠眼睛判断。写歪就整张卡退回默认色带(可见地与预期不同,而不是微妙地错)。
283
+ const isLast = index === rows.length - 1
284
+ const wanted = isLast ? 'clash' : 'solid'
285
+ if (row.kind !== wanted) return undefined
286
+ if (!Array.isArray(row.schemes) || row.schemes.length < 1 || row.schemes.length > 5) return undefined
287
+ for (const id of row.schemes) {
288
+ if (typeof id !== 'string' || !SCHEME_ID.test(id)) return undefined
289
+ const scheme = schemeById(id, schemes)
290
+ // 方案存在、且它自己的 kind 与本排一致 —— 否则会把拼色画成一块纯色。
291
+ if (scheme === undefined || scheme.kind !== wanted) return undefined
292
+ }
293
+ clean.push({ kind: wanted, schemes: [...row.schemes] })
294
+ }
295
+ return clean
296
+ }
297
+
298
+ /**
299
+ * WCAG 对比度(1..21)。与 `scripts/lib/card-rows.mjs` 里那份是同一条公式的两个落点:
300
+ * 那份在**建期**拦配色表,这份在**运行期**决定按钮文字取白还是取深。
301
+ * @param a - `#rrggbb`。
302
+ * @param b - `#rrggbb`。
303
+ * @returns 对比度;任一输入非法时返回 0。
304
+ */
305
+ function wcagContrast(a, b) {
306
+ const lum = (hex) => {
307
+ if (!/^#[0-9a-fA-F]{6}$/.test(String(hex))) return null
308
+ const linear = [1, 3, 5].map((at) => parseInt(hex.slice(at, at + 2), 16) / 255)
309
+ .map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
310
+ return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
311
+ }
312
+ const la = lum(a)
313
+ const lb = lum(b)
314
+ if (la === null || lb === null) return 0
315
+ // 先算再返回,**不要**写成 `return (…)`:`check-scope-reach.mjs` 的调用识别会把
316
+ // `return (` 读成"调用了名为 return 的函数",于是报一条假违规(规则 6 的另一面:
317
+ // 审计误报同样要消除,否则它就不再可信)。
318
+ const ratio = (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05)
319
+ return ratio
320
+ }
321
+
322
+ /**
323
+ * 这个方案的按钮/链接填充色。
324
+ *
325
+ * 缃色、橘黄、桃红这类**亮色**做填充时,白字压上去只有 1.7–3.7:1(读不清)。
326
+ * 所以填充色按对比度**逐个候选**试出来,而不是写死一个压深量:
327
+ *
328
+ * 1. 本色 + 本方案的深色文字(最好:按钮留在色库那个名字色上)—— 缃色/橘黄/桃红都是这一条;
329
+ * 2. 本色 + 白字;
330
+ * 3. 依次压深 0.35 / 0.5 / 0.65 再用白字(海棠红、松柏绿、竹青、苍青走这条)。
331
+ *
332
+ * 方案可以用 `fill` 显式指定(石榴金就是:用户定的按钮色是色库里的「胭脂」`#9D2933`,
333
+ * 而不是把石榴红压深出来的合成色)。
334
+ * @param scheme - 一套配色方案。
335
+ * @returns `#rrggbb`。
336
+ */
337
+ function schemeButtonFill(scheme) {
338
+ const choice = schemeButtonChoice(scheme)
339
+ return choice[0]
340
+ }
341
+
342
+ /**
343
+ * 按钮填充色上的文字色(白或方案自己的深色),取对比度更高的那个。
344
+ * @param scheme - 一套配色方案。
345
+ * @returns `#rrggbb`。
346
+ */
347
+ function schemeOnMain(scheme) {
348
+ return schemeButtonChoice(scheme)[1]
349
+ }
350
+
351
+ /**
352
+ * 选出「填充色 + 文字色」这一对(规则见 {@link schemeButtonFill})。
353
+ * @param scheme - 一套配色方案。
354
+ * @returns `[fill, onFill]`。
355
+ */
356
+ function schemeButtonChoice(scheme) {
357
+ const main = String(scheme.main)
358
+ const ink = String(scheme.ink)
359
+ if (typeof scheme.fill === 'string' && /^#[0-9a-fA-F]{6}$/.test(scheme.fill)) {
360
+ const onFill = wcagContrast(scheme.fill, '#FFFFFF') >= wcagContrast(scheme.fill, ink)
361
+ ? '#FFFFFF' : ink
362
+ return [scheme.fill, onFill]
363
+ }
364
+ const candidates = [
365
+ [main, ink],
366
+ [main, '#FFFFFF'],
367
+ [shade(main, -0.35), '#FFFFFF'],
368
+ [shade(main, -0.5), '#FFFFFF'],
369
+ [shade(main, -0.65), '#FFFFFF'],
370
+ ]
371
+ for (const candidate of candidates) {
372
+ if (wcagContrast(candidate[0], candidate[1]) >= 4.5) return candidate
373
+ }
374
+ return candidates.reduce((best, candidate) => (
375
+ wcagContrast(candidate[0], candidate[1]) > wcagContrast(best[0], best[1]) ? candidate : best
376
+ ))
377
+ }
378
+
379
+ /**
380
+ * 把一套配色方案展开成完整的 67 个 token(`overrideTokens` 层的成对格式)。
381
+ *
382
+ * 只有 `main` / `ground` / `ink` / `dots` 是色库里的名字色,其余全部由
383
+ * {@link shade} 派生:底色深浅阶、主色透明度边线、侧栏渐变。侧栏**刻意停在浅阶**
384
+ * (最深处只到主色的 0.42 白化),因为 `label-*` 是全界面共享的 —— 深底会让导航文字读不清,
385
+ * 这条教训写在 SKILL §14(深色系源侧栏一律"保色相提明度")。
386
+ *
387
+ * `state-{error,success,warn}` 三族**不随配色变**:它们是语义色,跟着调色板走的后果是
388
+ * 报错不再像报错。`state-business-primary` 除外 —— 它画"当前文件夹"的图标,属于品牌语汇:
389
+ * 纯色方案用主色,拼色方案用第三个点缀色(那正是"撞"出来的那个)。
390
+ * @param scheme - 一套配色方案。
391
+ * @returns `{ token: { light, dark } }`。
392
+ */
393
+ function schemeTokens(scheme) {
394
+ const main = String(scheme.main)
395
+ const ground = String(scheme.ground)
396
+ const ink = String(scheme.ink)
397
+ const fill = schemeButtonFill(scheme)
398
+ const onFill = schemeOnMain(scheme)
399
+ const business = scheme.kind === 'clash' && Array.isArray(scheme.dots) && scheme.dots.length > 2
400
+ ? String(scheme.dots[2])
401
+ : main
402
+ const pair = (value) => ({ light: value, dark: value })
403
+ const stops = [0, 20, 36, 50, 62, 74, 86, 100]
404
+ const sidebar = [0.94, 0.86, 0.78, 0.70, 0.62, 0.54, 0.48, 0.42]
405
+ .map((t, index) => `${shade(main, t)} ${stops[index]}%`).join(',')
406
+ return {
407
+ '--dsw-alias-bg-base': pair(`linear-gradient(to bottom,${shade(ground, 0.5)} 0%,${ground} 55%,${shade(ground, -0.06)} 100%)`),
408
+ '--dsw-alias-bg-layer-1': pair(shade(ground, 0.72)),
409
+ '--dsw-alias-bg-layer-2': pair('#FFFFFF'),
410
+ '--dsw-alias-bg-layer-3': pair('#FFFFFF'),
411
+ '--dsw-alias-bg-overlay': pair(shade(ground, -0.1)),
412
+ '--dsw-alias-bg-skeleton': pair(`${main}14`),
413
+ '--dsw-alias-bg-module-platform': pair(shade(ground, -0.1)),
414
+ '--dsw-alias-bg-multi-select': pair(shade(ground, -0.1)),
415
+ '--dsw-alias-border-l1': pair(`${main}0F`),
416
+ '--dsw-alias-border-l2': pair(`${main}21`),
417
+ '--dsw-alias-border-l3': pair(`${main}2E`),
418
+ '--dsw-alias-border-l4': pair(`${main}3D`),
419
+ '--dsw-alias-brand-primary': pair(fill),
420
+ '--dsw-alias-brand-primary-invert': pair('#FFFFFF'),
421
+ '--dsw-alias-brand-text': pair(fill),
422
+ '--dsw-alias-label-primary': pair(ink),
423
+ '--dsw-alias-label-primary-bluish': pair(ink),
424
+ '--dsw-alias-label-secondary': pair(shade(ink, 0.22)),
425
+ '--dsw-alias-label-tertiary': pair(shade(ink, 0.42)),
426
+ '--dsw-alias-label-caption': pair(shade(ink, 0.42)),
427
+ '--dsw-alias-label-dimmed': pair(shade(ink, 0.62)),
428
+ '--dsw-alias-label-primary-dimmed': pair(shade(ink, 0.22)),
429
+ '--dsw-alias-label-primary-foreground': pair(onFill),
430
+ '--dsw-alias-label-primary-inverted': pair('#FFFFFF'),
431
+ '--dsw-alias-link': pair(fill),
432
+ '--dsw-alias-interactive-bg-hover': pair(`${main}14`),
433
+ '--dsw-alias-interactive-bg-active': pair(`${main}2E`),
434
+ '--dsw-alias-interactive-bg-hover-solid': pair(shade(ground, -0.08)),
435
+ '--dsw-alias-interactive-bg-hover-accent': pair(`${main}3D`),
436
+ '--dsw-alias-button-primary-fill': pair(fill),
437
+ '--dsw-alias-button-primary-hover': pair(shade(fill, 0.12)),
438
+ '--dsw-alias-button-primary-dimmed': pair(`${fill}47`),
439
+ '--dsw-alias-button-ghost-active-fill': pair(`${main}29`),
440
+ '--dsw-alias-button-ghost-active-border': pair(main),
441
+ '--dsw-alias-button-ghost-active-hover': pair(`${main}47`),
442
+ '--dsw-alias-button-info-fill': pair(shade(ink, 0.22)),
443
+ '--dsw-alias-button-info-hover': pair(shade(ink, 0.1)),
444
+ '--dsw-alias-button-elevated-fill': pair('#FFFFFF'),
445
+ '--dsw-alias-button-floating-fill': pair('#FFFFFF'),
446
+ '--dsw-alias-button-floating-hover': pair(shade(ground, 0.8)),
447
+ '--dsw-alias-button-contrast-fill': pair(ink),
448
+ '--dsw-alias-markdown-code-block': pair(shade(ground, 0.6)),
449
+ '--dsw-alias-markdown-code-block-banner': pair(shade(ground, 0.45)),
450
+ '--dsw-alias-markdown-inline-code': pair(shade(ground, -0.08)),
451
+ '--dsw-alias-markdown-citation': pair(shade(ground, -0.08)),
452
+ '--dsw-alias-markdown-tag': pair(shade(ground, -0.08)),
453
+ '--dsw-alias-markdown-placeholder': pair(shade(ground, 0.4)),
454
+ '--dsw-alias-markdown-code-segment-selected': pair(`${main}29`),
455
+ '--dsw-alias-markdown-code-segment-unselected': pair(shade(ground, 0.6)),
456
+ '--dsw-alias-scrollbar-bg-l1': pair(`${main}29`),
457
+ '--dsw-alias-scrollbar-bg-l2': pair(`${main}38`),
458
+ '--dsw-alias-scrollbar-hover-l1': pair(`${main}52`),
459
+ '--dsw-alias-scrollbar-hover-l2': pair(`${main}66`),
460
+ '--dsw-alias-tooltip-bg': pair(ink),
461
+ '--dsw-alias-toast-bg': pair('#FFFFFF'),
462
+ '--dsw-alias-state-error-primary': pair('#C2352B'),
463
+ '--dsw-alias-state-error-secondary': pair('#DB5A6B'),
464
+ '--dsw-alias-state-success-primary': pair('#1DA981'),
465
+ '--dsw-alias-state-success-secondary': pair('#7FE3C0'),
466
+ '--dsw-alias-state-success-tertiary': pair('#D8F5E8'),
467
+ '--dsw-alias-state-warn-primary': pair('#C89B40'),
468
+ '--dsw-alias-state-warn-secondary': pair('#EACD76'),
469
+ '--dsw-alias-state-warn-tertiary': pair('#F7EBC8'),
470
+ '--dsw-alias-state-warn-label': pair('#7A5A0E'),
471
+ '--dsw-alias-state-business-primary': pair(business),
472
+ '--dsw-alias-state-business-tertiary': pair(shade(business, 0.55)),
473
+ '--dsw-specific-sidebar-fill': pair(`linear-gradient(to bottom,${sidebar})`),
474
+ }
475
+ }
476
+
183
477
  /**
184
478
  * Record the user's card choice BEFORE it reaches the theme service.
185
479
  *
@@ -231,6 +525,54 @@ window.__ModuleLoader__.load({
231
525
  }
232
526
  }
233
527
 
528
+ /**
529
+ * Record the palette scheme the user clicked on the picker card.
530
+ *
531
+ * Same storage rule as the skin id: **`localStorage` only**, never
532
+ * `ui-theme.preference` (a value the boot graph reads would have to be a built-in
533
+ * id, and an unknown one makes `buildSnapshot()` throw and refuse to start).
534
+ * @param id - the scheme id (`p-…`).
535
+ */
536
+ function rememberScheme(id) {
537
+ try {
538
+ if (typeof window === 'undefined') return
539
+ window.localStorage?.setItem(SCHEME_KEY, String(id))
540
+ } catch (error) {
541
+ console.error('[theme-gallery] could not record the palette scheme:', error)
542
+ }
543
+ }
544
+
545
+ /**
546
+ * The palette scheme the user picked, if any.
547
+ * @returns the id, or null when the user never picked one.
548
+ */
549
+ function rememberedScheme() {
550
+ try {
551
+ if (typeof window === 'undefined') return null
552
+ const value = window.localStorage?.getItem(SCHEME_KEY)
553
+ if (typeof value !== 'string' || value === '') return null
554
+ // 只认表里真实存在的 id:换过色表、手改过 localStorage 时,宁可当作没选,
555
+ // 也不要叠一层空方案(那会把整屏的颜色悄悄换掉)。
556
+ return schemeById(value, PALETTE_SCHEMES) === undefined ? null : value
557
+ } catch {
558
+ return null
559
+ }
560
+ }
561
+
562
+ /** 卡片上默认点亮的那一格:锚主题自己的那套配色(没选过任何方案时)。 */
563
+ const DEFAULT_SCHEME = 'p-shi-liu-jin'
564
+
565
+ /**
566
+ * 卡片上应当点亮哪一格。
567
+ *
568
+ * 没选过时给默认值**只是为了显示**:那时并不叠配色层,屏幕上是锚主题自己
569
+ * 手工写的那套 token(与这一格同一组关键色,观感几乎一致)。
570
+ * @returns 方案 id。
571
+ */
572
+ function wantedScheme() {
573
+ return rememberedScheme() ?? DEFAULT_SCHEME
574
+ }
575
+
234
576
  /** Body attribute publishing the reading state; the injected rule reads it. */
235
577
  const READING_ATTRIBUTE = 'data-dsh-theme-reading'
236
578
 
@@ -257,7 +599,7 @@ window.__ModuleLoader__.load({
257
599
  * package.json, and the release workflow fails when re-running the embed step
258
600
  * changes a tracked file.
259
601
  */
260
- const BUNDLED_VERSION = '0.3.0'
602
+ const BUNDLED_VERSION = '0.4.0'
261
603
 
262
604
  /**
263
605
  * Themes this package contributes, inlined from lib/themes/*.json.
@@ -849,7 +1191,7 @@ window.__ModuleLoader__.load({
849
1191
  {
850
1192
  "id": "meng-hai-you-yu",
851
1193
  "label": "梦海游鱼",
852
- "description": "梦幻海洋,游鱼作伴 —— 复刻自电商新零售系统管理后台的默认主题",
1194
+ "description": "梦幻海洋,游鱼作伴 —— 复刻自蜂链商城电商新零售系统管理后台的默认主题",
853
1195
  "colorScheme": "light",
854
1196
  "tokens": {
855
1197
  "--dsw-alias-bg-base": {
@@ -1139,7 +1481,7 @@ window.__ModuleLoader__.load({
1139
1481
  {
1140
1482
  "id": "pei-an-jie-xin",
1141
1483
  "label": "佩安杰心",
1142
- "description": "檀香禅影,自在安顿 —— 复刻自电商新零售系统管理后台同名主题",
1484
+ "description": "檀香禅影,自在安顿 —— 复刻自蜂链商城电商新零售系统管理后台同名主题",
1143
1485
  "colorScheme": "light",
1144
1486
  "tokens": {
1145
1487
  "--dsw-alias-bg-base": {
@@ -1427,7 +1769,7 @@ window.__ModuleLoader__.load({
1427
1769
  {
1428
1770
  "id": "shan-qing-ting-cai",
1429
1771
  "label": "山青婷彩",
1430
- "description": "青山叠翠,蜓舞生姿 —— 复刻自电商新零售系统管理后台同名主题",
1772
+ "description": "青山叠翠,蜓舞生姿 —— 复刻自蜂链商城电商新零售系统管理后台同名主题",
1431
1773
  "colorScheme": "light",
1432
1774
  "tokens": {
1433
1775
  "--dsw-alias-bg-base": {
@@ -1712,10 +2054,328 @@ window.__ModuleLoader__.load({
1712
2054
  "petals": 7
1713
2055
  }
1714
2056
  },
2057
+ {
2058
+ "id": "shi-liu-jin",
2059
+ "label": "纯色/拼色",
2060
+ "description": "石榴红撞赤金与翡翠 —— 纯色排 × 拼色排的配色卡:15 个色块铺满卡面,不含侧栏素材",
2061
+ "colorScheme": "light",
2062
+ "tokens": {
2063
+ "--dsw-alias-bg-base": {
2064
+ "light": "linear-gradient(to bottom,#FBF7EC 0%,#F2ECDE 55%,#EADFC4 100%)",
2065
+ "dark": "linear-gradient(to bottom,#FBF7EC 0%,#F2ECDE 55%,#EADFC4 100%)"
2066
+ },
2067
+ "--dsw-alias-bg-layer-1": {
2068
+ "light": "#F0F0F4",
2069
+ "dark": "#F0F0F4"
2070
+ },
2071
+ "--dsw-alias-bg-layer-2": {
2072
+ "light": "#FFFBF0",
2073
+ "dark": "#FFFBF0"
2074
+ },
2075
+ "--dsw-alias-bg-layer-3": {
2076
+ "light": "#FFFBF0",
2077
+ "dark": "#FFFBF0"
2078
+ },
2079
+ "--dsw-alias-bg-overlay": {
2080
+ "light": "#EEDEB0",
2081
+ "dark": "#EEDEB0"
2082
+ },
2083
+ "--dsw-alias-bg-skeleton": {
2084
+ "light": "#D1D9E0",
2085
+ "dark": "#D1D9E0"
2086
+ },
2087
+ "--dsw-alias-bg-module-platform": {
2088
+ "light": "#EEDEB0",
2089
+ "dark": "#EEDEB0"
2090
+ },
2091
+ "--dsw-alias-bg-multi-select": {
2092
+ "light": "#EEDEB0",
2093
+ "dark": "#EEDEB0"
2094
+ },
2095
+ "--dsw-alias-border-l1": {
2096
+ "light": "#9D29330F",
2097
+ "dark": "#9D29330F"
2098
+ },
2099
+ "--dsw-alias-border-l2": {
2100
+ "light": "#9D293321",
2101
+ "dark": "#9D293321"
2102
+ },
2103
+ "--dsw-alias-border-l3": {
2104
+ "light": "#9D29332E",
2105
+ "dark": "#9D29332E"
2106
+ },
2107
+ "--dsw-alias-border-l4": {
2108
+ "light": "#D1D9E080",
2109
+ "dark": "#D1D9E080"
2110
+ },
2111
+ "--dsw-alias-brand-primary": {
2112
+ "light": "#9D2933",
2113
+ "dark": "#9D2933"
2114
+ },
2115
+ "--dsw-alias-brand-primary-invert": {
2116
+ "light": "#FFFFFF",
2117
+ "dark": "#FFFFFF"
2118
+ },
2119
+ "--dsw-alias-brand-text": {
2120
+ "light": "#9D2933",
2121
+ "dark": "#9D2933"
2122
+ },
2123
+ "--dsw-alias-label-primary": {
2124
+ "light": "#312520",
2125
+ "dark": "#312520"
2126
+ },
2127
+ "--dsw-alias-label-primary-bluish": {
2128
+ "light": "#312520",
2129
+ "dark": "#312520"
2130
+ },
2131
+ "--dsw-alias-label-secondary": {
2132
+ "light": "#574266",
2133
+ "dark": "#574266"
2134
+ },
2135
+ "--dsw-alias-label-tertiary": {
2136
+ "light": "#75664D",
2137
+ "dark": "#75664D"
2138
+ },
2139
+ "--dsw-alias-label-caption": {
2140
+ "light": "#75664D",
2141
+ "dark": "#75664D"
2142
+ },
2143
+ "--dsw-alias-label-dimmed": {
2144
+ "light": "#9A8C76",
2145
+ "dark": "#9A8C76"
2146
+ },
2147
+ "--dsw-alias-label-primary-dimmed": {
2148
+ "light": "#574266",
2149
+ "dark": "#574266"
2150
+ },
2151
+ "--dsw-alias-label-primary-foreground": {
2152
+ "light": "#FFFFFF",
2153
+ "dark": "#FFFFFF"
2154
+ },
2155
+ "--dsw-alias-label-primary-inverted": {
2156
+ "light": "#FFFFFF",
2157
+ "dark": "#FFFFFF"
2158
+ },
2159
+ "--dsw-alias-link": {
2160
+ "light": "#9D2933",
2161
+ "dark": "#9D2933"
2162
+ },
2163
+ "--dsw-alias-interactive-bg-hover": {
2164
+ "light": "#9D293314",
2165
+ "dark": "#9D293314"
2166
+ },
2167
+ "--dsw-alias-interactive-bg-active": {
2168
+ "light": "#F20C002E",
2169
+ "dark": "#F20C002E"
2170
+ },
2171
+ "--dsw-alias-interactive-bg-hover-solid": {
2172
+ "light": "#EEDEB0",
2173
+ "dark": "#EEDEB0"
2174
+ },
2175
+ "--dsw-alias-interactive-bg-hover-accent": {
2176
+ "light": "#F20C003D",
2177
+ "dark": "#F20C003D"
2178
+ },
2179
+ "--dsw-alias-button-primary-fill": {
2180
+ "light": "#9D2933",
2181
+ "dark": "#9D2933"
2182
+ },
2183
+ "--dsw-alias-button-primary-hover": {
2184
+ "light": "#B33A45",
2185
+ "dark": "#B33A45"
2186
+ },
2187
+ "--dsw-alias-button-primary-dimmed": {
2188
+ "light": "#9D293347",
2189
+ "dark": "#9D293347"
2190
+ },
2191
+ "--dsw-alias-button-ghost-active-fill": {
2192
+ "light": "#F20C0029",
2193
+ "dark": "#F20C0029"
2194
+ },
2195
+ "--dsw-alias-button-ghost-active-border": {
2196
+ "light": "#F20C00",
2197
+ "dark": "#F20C00"
2198
+ },
2199
+ "--dsw-alias-button-ghost-active-hover": {
2200
+ "light": "#F20C0047",
2201
+ "dark": "#F20C0047"
2202
+ },
2203
+ "--dsw-alias-button-info-fill": {
2204
+ "light": "#574266",
2205
+ "dark": "#574266"
2206
+ },
2207
+ "--dsw-alias-button-info-hover": {
2208
+ "light": "#75664D",
2209
+ "dark": "#75664D"
2210
+ },
2211
+ "--dsw-alias-button-elevated-fill": {
2212
+ "light": "#FFFBF0",
2213
+ "dark": "#FFFBF0"
2214
+ },
2215
+ "--dsw-alias-button-floating-fill": {
2216
+ "light": "#FFFBF0",
2217
+ "dark": "#FFFBF0"
2218
+ },
2219
+ "--dsw-alias-button-floating-hover": {
2220
+ "light": "#F7F0E0",
2221
+ "dark": "#F7F0E0"
2222
+ },
2223
+ "--dsw-alias-button-contrast-fill": {
2224
+ "light": "#312520",
2225
+ "dark": "#312520"
2226
+ },
2227
+ "--dsw-alias-markdown-code-block": {
2228
+ "light": "#F0F0F4",
2229
+ "dark": "#F0F0F4"
2230
+ },
2231
+ "--dsw-alias-markdown-code-block-banner": {
2232
+ "light": "#E4E6EC",
2233
+ "dark": "#E4E6EC"
2234
+ },
2235
+ "--dsw-alias-markdown-inline-code": {
2236
+ "light": "#EEDEB0",
2237
+ "dark": "#EEDEB0"
2238
+ },
2239
+ "--dsw-alias-markdown-citation": {
2240
+ "light": "#EEDEB0",
2241
+ "dark": "#EEDEB0"
2242
+ },
2243
+ "--dsw-alias-markdown-tag": {
2244
+ "light": "#EEDEB0",
2245
+ "dark": "#EEDEB0"
2246
+ },
2247
+ "--dsw-alias-markdown-placeholder": {
2248
+ "light": "#D1D9E0",
2249
+ "dark": "#D1D9E0"
2250
+ },
2251
+ "--dsw-alias-markdown-code-segment-selected": {
2252
+ "light": "#F20C0029",
2253
+ "dark": "#F20C0029"
2254
+ },
2255
+ "--dsw-alias-markdown-code-segment-unselected": {
2256
+ "light": "#F0F0F4",
2257
+ "dark": "#F0F0F4"
2258
+ },
2259
+ "--dsw-alias-scrollbar-bg-l1": {
2260
+ "light": "#9D293329",
2261
+ "dark": "#9D293329"
2262
+ },
2263
+ "--dsw-alias-scrollbar-bg-l2": {
2264
+ "light": "#9D293338",
2265
+ "dark": "#9D293338"
2266
+ },
2267
+ "--dsw-alias-scrollbar-hover-l1": {
2268
+ "light": "#9D293352",
2269
+ "dark": "#9D293352"
2270
+ },
2271
+ "--dsw-alias-scrollbar-hover-l2": {
2272
+ "light": "#9D293366",
2273
+ "dark": "#9D293366"
2274
+ },
2275
+ "--dsw-alias-tooltip-bg": {
2276
+ "light": "#312520",
2277
+ "dark": "#312520"
2278
+ },
2279
+ "--dsw-alias-toast-bg": {
2280
+ "light": "#FFFBF0",
2281
+ "dark": "#FFFBF0"
2282
+ },
2283
+ "--dsw-alias-state-error-primary": {
2284
+ "light": "#C2352B",
2285
+ "dark": "#C2352B"
2286
+ },
2287
+ "--dsw-alias-state-error-secondary": {
2288
+ "light": "#DB5A6B",
2289
+ "dark": "#DB5A6B"
2290
+ },
2291
+ "--dsw-alias-state-success-primary": {
2292
+ "light": "#1DA981",
2293
+ "dark": "#1DA981"
2294
+ },
2295
+ "--dsw-alias-state-success-secondary": {
2296
+ "light": "#7FE3C0",
2297
+ "dark": "#7FE3C0"
2298
+ },
2299
+ "--dsw-alias-state-success-tertiary": {
2300
+ "light": "#D8F5E8",
2301
+ "dark": "#D8F5E8"
2302
+ },
2303
+ "--dsw-alias-state-warn-primary": {
2304
+ "light": "#C89B40",
2305
+ "dark": "#C89B40"
2306
+ },
2307
+ "--dsw-alias-state-warn-secondary": {
2308
+ "light": "#EACD76",
2309
+ "dark": "#EACD76"
2310
+ },
2311
+ "--dsw-alias-state-warn-tertiary": {
2312
+ "light": "#F7EBC8",
2313
+ "dark": "#F7EBC8"
2314
+ },
2315
+ "--dsw-alias-state-warn-label": {
2316
+ "light": "#7A5A0E",
2317
+ "dark": "#7A5A0E"
2318
+ },
2319
+ "--dsw-alias-state-business-primary": {
2320
+ "light": "#3DE1AD",
2321
+ "dark": "#3DE1AD"
2322
+ },
2323
+ "--dsw-alias-state-business-tertiary": {
2324
+ "light": "#A8F0D8",
2325
+ "dark": "#A8F0D8"
2326
+ },
2327
+ "--dsw-specific-sidebar-fill": {
2328
+ "light": "linear-gradient(to bottom,#D6ECF0 0%,#E4E6E4 8%,#F2ECDE 18%,#F5E7CE 30%,#EFD6AE 44%,#E5BE86 58%,#D6A45C 72%,#C48F3C 86%,#BA8330 100%)",
2329
+ "dark": "linear-gradient(to bottom,#D6ECF0 0%,#E4E6E4 8%,#F2ECDE 18%,#F5E7CE 30%,#EFD6AE 44%,#E5BE86 58%,#D6A45C 72%,#C48F3C 86%,#BA8330 100%)"
2330
+ }
2331
+ },
2332
+ "reading": {
2333
+ "colorScheme": "light",
2334
+ "bg": "#FFFBF0",
2335
+ "alpha": 0.62,
2336
+ "blur": 3,
2337
+ "maxWidth": 640
2338
+ },
2339
+ "card": {
2340
+ "rows": [
2341
+ {
2342
+ "kind": "solid",
2343
+ "schemes": [
2344
+ "p-xiang-se",
2345
+ "p-ju-huang",
2346
+ "p-tao-hong",
2347
+ "p-hai-tang-hong",
2348
+ "p-jiang-zi"
2349
+ ]
2350
+ },
2351
+ {
2352
+ "kind": "solid",
2353
+ "schemes": [
2354
+ "p-song-bai-lu",
2355
+ "p-zhu-qing",
2356
+ "p-cang-qing",
2357
+ "p-dai-zi",
2358
+ "p-xuan-qing"
2359
+ ]
2360
+ },
2361
+ {
2362
+ "kind": "clash",
2363
+ "schemes": [
2364
+ "p-shi-liu-jin",
2365
+ "p-bao-lan-jin",
2366
+ "p-qing-lian-jin",
2367
+ "p-song-hua-tao",
2368
+ "p-wu-jin"
2369
+ ]
2370
+ }
2371
+ ]
2372
+ },
2373
+ "accent": "#F20C00"
2374
+ },
1715
2375
  {
1716
2376
  "id": "ying-mu-cai-yun",
1717
2377
  "label": "营慕彩云",
1718
- "description": "营幕之下,彩云为伴 —— 复刻自电商新零售系统管理后台同名主题",
2378
+ "description": "营幕之下,彩云为伴 —— 复刻自蜂链商城电商新零售系统管理后台同名主题",
1719
2379
  "colorScheme": "light",
1720
2380
  "tokens": {
1721
2381
  "--dsw-alias-bg-base": {
@@ -2002,6 +2662,181 @@ window.__ModuleLoader__.load({
2002
2662
  }
2003
2663
  ]
2004
2664
 
2665
+ /**
2666
+ * 15 套「纯色/拼色」配色(`lib/palette-schemes.json`,由 embed-themes 内联)。
2667
+ *
2668
+ * 它们**不是主题**:不注册进主题服务、不占面板卡片、不进官方「外观」下拉。
2669
+ * 点卡片上的色值按钮时,插件把 {@link schemeTokens} 展出来的这一层叠上去,
2670
+ * 整屏就换成那套配色;切到别的皮肤时这一层必须撤除(见 `syncScheme`)。
2671
+ */
2672
+ const BUNDLED_PALETTES = [
2673
+ {
2674
+ "id": "p-xiang-se",
2675
+ "kind": "solid",
2676
+ "label": "缃色",
2677
+ "main": "#F0C239",
2678
+ "ground": "#F2ECDE",
2679
+ "ink": "#312520",
2680
+ "source": "黄 1-9 缃色(浅黄色)"
2681
+ },
2682
+ {
2683
+ "id": "p-ju-huang",
2684
+ "kind": "solid",
2685
+ "label": "橘黄",
2686
+ "main": "#FF8936",
2687
+ "ground": "#F2ECDE",
2688
+ "ink": "#312520",
2689
+ "source": "黄 1-5 橘黄(柑橘的黄色)"
2690
+ },
2691
+ {
2692
+ "id": "p-tao-hong",
2693
+ "kind": "solid",
2694
+ "label": "桃红",
2695
+ "main": "#F47983",
2696
+ "ground": "#EDD1D8",
2697
+ "ink": "#56004F",
2698
+ "source": "红 0-3 桃红(桃花的颜色)"
2699
+ },
2700
+ {
2701
+ "id": "p-hai-tang-hong",
2702
+ "kind": "solid",
2703
+ "label": "海棠红",
2704
+ "main": "#DB5A6B",
2705
+ "ground": "#EDD1D8",
2706
+ "ink": "#56004F",
2707
+ "source": "红 0-4 海棠红(淡紫红色、较桃红色深一些)"
2708
+ },
2709
+ {
2710
+ "id": "p-jiang-zi",
2711
+ "kind": "solid",
2712
+ "label": "绛紫",
2713
+ "main": "#8C4356",
2714
+ "ground": "#EDD1D8",
2715
+ "ink": "#56004F",
2716
+ "source": "红 0-9 绛紫(紫中略带红的颜色)"
2717
+ },
2718
+ {
2719
+ "id": "p-song-bai-lu",
2720
+ "kind": "solid",
2721
+ "label": "松柏绿",
2722
+ "main": "#057748",
2723
+ "ground": "#E0F0E9",
2724
+ "ink": "#424C50",
2725
+ "source": "绿 2-30 松花绿(松柏叶的墨绿)"
2726
+ },
2727
+ {
2728
+ "id": "p-zhu-qing",
2729
+ "kind": "solid",
2730
+ "label": "竹青",
2731
+ "main": "#789262",
2732
+ "ground": "#E0EEE8",
2733
+ "ink": "#424C50",
2734
+ "source": "绿 2-3 竹青(竹子的绿色)"
2735
+ },
2736
+ {
2737
+ "id": "p-cang-qing",
2738
+ "kind": "solid",
2739
+ "label": "苍青",
2740
+ "main": "#7397AB",
2741
+ "ground": "#D6ECF0",
2742
+ "ink": "#4A4266",
2743
+ "source": "苍 4-3 苍青"
2744
+ },
2745
+ {
2746
+ "id": "p-dai-zi",
2747
+ "kind": "solid",
2748
+ "label": "黛紫",
2749
+ "main": "#574266",
2750
+ "ground": "#E4C6D0",
2751
+ "ink": "#56004F",
2752
+ "source": "蓝 3-12 黛紫(深紫色)"
2753
+ },
2754
+ {
2755
+ "id": "p-xuan-qing",
2756
+ "kind": "solid",
2757
+ "label": "玄青",
2758
+ "main": "#3D3B4F",
2759
+ "ground": "#F0F0F4",
2760
+ "ink": "#312520",
2761
+ "source": "黑 7-1 玄青(深黑色)"
2762
+ },
2763
+ {
2764
+ "id": "p-shi-liu-jin",
2765
+ "kind": "clash",
2766
+ "label": "石榴金",
2767
+ "main": "#F20C00",
2768
+ "ground": "#F2ECDE",
2769
+ "ink": "#312520",
2770
+ "fill": "#9D2933",
2771
+ "dots": [
2772
+ "#FFFFFF",
2773
+ "#D6ECF0",
2774
+ "#EACD76",
2775
+ "#3DE1AD"
2776
+ ],
2777
+ "source": "红 0-5 石榴红 + 金银 8-0 赤金 + 绿 2-14 翡翠色;按钮族用红 0-11 胭脂(白字 7.50:1,而压在石榴红上只有 4.35:1)"
2778
+ },
2779
+ {
2780
+ "id": "p-bao-lan-jin",
2781
+ "kind": "clash",
2782
+ "label": "宝蓝·赤金",
2783
+ "main": "#4B5CC4",
2784
+ "ground": "#D6ECF0",
2785
+ "ink": "#4A4266",
2786
+ "dots": [
2787
+ "#F2BE45",
2788
+ "#EACD76",
2789
+ "#D9B611",
2790
+ "#FFFFFF"
2791
+ ],
2792
+ "source": "蓝 3-5 宝蓝(多和小面积纯黄色(金色)配合使用)+ 金银 8-0 赤金"
2793
+ },
2794
+ {
2795
+ "id": "p-qing-lian-jin",
2796
+ "kind": "clash",
2797
+ "label": "青莲·金玉",
2798
+ "main": "#801DAE",
2799
+ "ground": "#E4C6D0",
2800
+ "ink": "#56004F",
2801
+ "dots": [
2802
+ "#F2BE45",
2803
+ "#FFF143",
2804
+ "#3DE1AD",
2805
+ "#FFFFFF"
2806
+ ],
2807
+ "source": "蓝 3-19 青莲(偏蓝的紫色)+ 黄 1-0 鹅黄 + 绿 2-14 翡翠色"
2808
+ },
2809
+ {
2810
+ "id": "p-song-hua-tao",
2811
+ "kind": "clash",
2812
+ "label": "松花·桃粉",
2813
+ "main": "#BCE672",
2814
+ "ground": "#F7FBEC",
2815
+ "ink": "#424C50",
2816
+ "dots": [
2817
+ "#C93756",
2818
+ "#057748"
2819
+ ],
2820
+ "source": "绿 2-31 松花色(嫩黄绿)+ 红 0-1 樱桃色 + 绿 2-30 松花绿"
2821
+ },
2822
+ {
2823
+ "id": "p-wu-jin",
2824
+ "kind": "clash",
2825
+ "label": "藏青·鹅黄",
2826
+ "main": "#2E4E7E",
2827
+ "ground": "#E2E9F2",
2828
+ "ink": "#312520",
2829
+ "dots": [
2830
+ "#F2BE45",
2831
+ "#FFFFFF",
2832
+ "#FFB3A7",
2833
+ "#3DE1AD"
2834
+ ],
2835
+ "source": "蓝 8-2 藏青(深蓝)+ 金银 8-0 赤金 + 精白 + 红 0-0 粉红 + 绿 2-14 翡翠色"
2836
+ }
2837
+ ]
2838
+ PALETTE_SCHEMES = Array.isArray(BUNDLED_PALETTES) ? BUNDLED_PALETTES : []
2839
+
2005
2840
  /**
2006
2841
  * Marks the ambient nodes this run created.
2007
2842
  *
@@ -2428,6 +3263,51 @@ window.__ModuleLoader__.load({
2428
3263
  '.tg-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}',
2429
3264
  '.tg-strip{display:flex;height:6px;border-radius:999px;overflow:hidden;border:.5px solid var(--dsw-alias-border-l3)}',
2430
3265
  '.tg-strip span{flex:1}',
3266
+ /* ── the PALETTE PICKER card (15 clickable colour buttons) ────────────────
3267
+ ** 这些色格里的颜色全部来自「中国传统色库」项目(chinese-colors)**
3268
+ https://github.com/zerosoul/chinese-colors
3269
+ 作者 tristan(GitHub @zerosoul)|在线手册 https://colors.ichuantong.cn |许可 ISC
3270
+ 取色表见 lib/palette-schemes.json(每套都带 source = 该库的「组 + id」)。
3271
+ 色名与色值是作者整理的开源成果,本插件只做取色与派生 —— **感谢作者 tristan**。
3272
+
3273
+ Three rows of 16px keep the picker within a few px of a strip card that
3274
+ carries a two-line description, so it does not sit visibly short in the
3275
+ grid. Each slot carries its own hairline: the pale schemes are near-white,
3276
+ and against a white card in the official LIGHT appearance a borderless
3277
+ swatch would simply vanish. `--dsw-alias-border-l3` adapts to both. */
3278
+ '.tg-picker{display:flex;flex-direction:column;gap:3px}',
3279
+ '.tg-pickrow{display:flex;gap:3px;height:16px}',
3280
+ /* 卡片本体也可点(= 用亮着的那一格),所以要给出"可点"的手势提示 —— 它是一个 div,
3281
+ 浏览器不会自带 cursor:pointer。 */
3282
+ '.tg-picker-card{cursor:pointer}',
3283
+ /* 色格本体做成 flex 行:里面的色带按 `flex-grow` 分宽度(主色 2、次色各 1)。 */
3284
+ '.tg-swatch{flex:1;min-width:0;padding:0;background:none;cursor:pointer;position:relative;',
3285
+ 'display:flex;border:.5px solid var(--dsw-alias-border-l3);border-radius:3px;font:inherit}',
3286
+ '.tg-swatch[aria-pressed="true"]{border-color:var(--dsw-alias-brand-primary);',
3287
+ 'box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary)}',
3288
+ '.tg-swatch:focus-visible{outline:1.5px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
3289
+ /* 一条色带。宽度由内联的 `flex-grow` 决定(纯色格是"一条带占满"的特例),
3290
+ 左右两端的圆角靠 `:first-child` / `:last-child` 补 —— 色格自己没有 overflow:hidden
3291
+ (那会把悬停标签裁掉)。 */
3292
+ '.tg-band{display:block;height:100%;min-width:0}',
3293
+ '.tg-band:first-child{border-radius:2px 0 0 2px}',
3294
+ '.tg-band:last-child{border-radius:0 2px 2px 0}',
3295
+ '.tg-band:only-child{border-radius:2px}',
3296
+ /* ── THE HOVER NAME (user's request: no layout change, name on hover) ──────
3297
+ 15 slots at ~34px wide cannot carry a legible name, and adding a name line
3298
+ would change the card's height — so the name appears ONLY while the pointer
3299
+ is on that slot, as an absolutely positioned chip (out of flow: nothing in
3300
+ the card moves by a single pixel).
3301
+
3302
+ Note `.tg-swatch` deliberately has NO `overflow:hidden`: it would clip this
3303
+ chip. The rounded corners are kept by rounding the CHILDREN (`.tg-band`)
3304
+ instead, which is what the clipping was for. */
3305
+ '.tg-swatch:hover::after,.tg-swatch:focus-visible::after{content:attr(data-name);',
3306
+ 'position:absolute;left:50%;bottom:calc(100% + 5px);transform:translateX(-50%);',
3307
+ 'white-space:nowrap;z-index:5;pointer-events:none;padding:2px 6px;border-radius:4px;',
3308
+ 'font-size:11px;line-height:1.3;background:var(--dsw-alias-bg-base);',
3309
+ 'color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l3);',
3310
+ 'box-shadow:0 2px 8px rgba(0,0,0,.18)}',
2431
3311
  ].join('')
2432
3312
 
2433
3313
  /**
@@ -2440,13 +3320,17 @@ window.__ModuleLoader__.load({
2440
3320
  */
2441
3321
  function createGalleryStore() {
2442
3322
  return defineStore({
2443
- init: () => ({ ids: [], labels: {}, descriptions: {}, swatches: {}, selected: 'system', status: '', revision: -1 }),
3323
+ init: () => ({
3324
+ ids: [], labels: {}, descriptions: {}, swatches: {}, cardRows: {},
3325
+ scheme: '', selected: 'system', status: '', revision: -1,
3326
+ }),
2444
3327
  actions: {
2445
3328
  sync: (draft, themes, selected, revision) => {
2446
3329
  if (revision <= draft.revision) return
2447
3330
  const labels = {}
2448
3331
  const descriptions = {}
2449
3332
  const swatches = {}
3333
+ const cardRows = {}
2450
3334
  for (const theme of themes) {
2451
3335
  // Built-in themes carry no label — the official Appearance row
2452
3336
  // localizes them from its own dictionaries — so name them here
@@ -2470,11 +3354,17 @@ window.__ModuleLoader__.load({
2470
3354
  pick('--dsw-alias-label-secondary'),
2471
3355
  pick('--dsw-alias-state-business-primary'),
2472
3356
  ].filter((value) => typeof value === 'string' && value !== '')
3357
+ // A palette-shaped skin can replace the strip with the palette picker
3358
+ // (`card.rows`). Absent — which is every scene-carrying skin and both
3359
+ // built-in cards — the strip above stays exactly as it was.
3360
+ const rows = cardRowShape(theme, PALETTE_SCHEMES)
3361
+ if (rows !== undefined) cardRows[theme.id] = rows
2473
3362
  }
2474
3363
  draft.ids = themes.map((theme) => theme.id)
2475
3364
  draft.labels = labels
2476
3365
  draft.descriptions = descriptions
2477
3366
  draft.swatches = swatches
3367
+ draft.cardRows = cardRows
2478
3368
  draft.selected = selected
2479
3369
  draft.revision = revision
2480
3370
  },
@@ -2487,39 +3377,170 @@ window.__ModuleLoader__.load({
2487
3377
  * @param status - one line describing the state.
2488
3378
  */
2489
3379
  note: (draft, status) => { draft.status = status },
3380
+ /**
3381
+ * 记住配色卡上点亮的是哪一格。
3382
+ *
3383
+ * 与主题列表分开放:方案 id 不是主题,混进 `ids` 会让"皮肤数"的计数口径
3384
+ * 与 README、与 npm 描述一起失真。
3385
+ * @param draft - store draft.
3386
+ * @param id - the scheme id.
3387
+ */
3388
+ markScheme: (draft, id) => { draft.scheme = String(id) },
3389
+ },
3390
+ })
3391
+ }
3392
+
3393
+ /**
3394
+ * Render one palette slot: a button that applies a colour scheme.
3395
+ *
3396
+ * ── 拼色格的画法:按**宽度比例**分带(不再用"底 + 圆点")──────────────────────
3397
+ *
3398
+ * 用户要的:拼色不要点子,改成把色块**按宽度分份** —— 主色占 2 份、每个次色各占 1 份。
3399
+ * 于是 2/1 就是"一主一次"、2/1/1 就是"一主两次";而当前 5 套拼色都是"一主四次"
3400
+ * (`dots` 各有 4 个),画出来自然是 2/1/1/1/1 —— **比值由数据算出来,不是写死的**。
3401
+ * 纯色格是同一机制的退化情况(一条带占满整格),所以两类格子共用一套 DOM 与样式,
3402
+ * 不会出现"两种格子两套画法"的漂移。
3403
+ *
3404
+ * 画出来的颜色与实际点击后生效的颜色来自**同一张表**,所以按钮不可能宣传一个
3405
+ * 方案根本没画的颜色。
3406
+ * @param scheme - one entry from `lib/palette-schemes.json`.
3407
+ * @param selected - whether this scheme is the one in effect.
3408
+ * @param onPick - called with the scheme id.
3409
+ * @param index - React key.
3410
+ * @returns the swatch button.
3411
+ */
3412
+ function schemeSwatch(scheme, selected, onPick, index) {
3413
+ const accents = Array.isArray(scheme.dots) ? scheme.dots : []
3414
+ const bands = scheme.kind === 'clash'
3415
+ ? [[scheme.main, 2]].concat(accents.map((colour) => [colour, 1]))
3416
+ : [[scheme.main, 1]]
3417
+ const inner = bands.map(([colour, weight], band) => jsx('span', {
3418
+ className: 'tg-band',
3419
+ style: { background: colour, flexGrow: weight },
3420
+ }, band))
3421
+ return jsx('button', {
3422
+ type: 'button',
3423
+ className: 'tg-swatch',
3424
+ 'aria-pressed': selected,
3425
+ // 悬停/聚焦时显示的色名(CSS `content:attr(data-name)`,不改布局)。
3426
+ 'data-name': scheme.label,
3427
+ // The label is the only text a picker slot carries; the full provenance
3428
+ // (which colour-library entry it came from) lives in the tooltip.
3429
+ title: scheme.source === undefined ? scheme.label : `${scheme.label} · ${scheme.source}`,
3430
+ onClick: (event) => {
3431
+ // ── 必须拦住冒泡 ──────────────────────────────────────────────────────
3432
+ //
3433
+ // 卡片本体也处理点击(用它记住的那一格)。不拦的话,点子格会先切到那一格,
3434
+ // 紧接着冒泡到卡片的处理器、被覆盖回原来那格 —— 症状就是"点色块没反应"。
3435
+ // 事件参数在测试里可能缺省,所以要容错(jsx 桩调用处理器时不给事件)。
3436
+ if (event !== undefined && typeof event.stopPropagation === 'function') event.stopPropagation()
3437
+ onPick(scheme.id)
2490
3438
  },
3439
+ children: inner,
3440
+ }, index)
3441
+ }
3442
+
3443
+ /**
3444
+ * Render the palette-picker card's three rows of slots.
3445
+ * @param rows - rows sanitised by {@link cardRowShape}.
3446
+ * @param selectedScheme - the scheme id currently in effect, if any.
3447
+ * @param onPick - called with the scheme id.
3448
+ * @param schemes - the palette table.
3449
+ * @returns the picker element.
3450
+ */
3451
+ function cardPickerElement(rows, selectedScheme, onPick, schemes) {
3452
+ return jsx('span', {
3453
+ className: 'tg-picker',
3454
+ children: rows.map((row, index) => jsx('span', {
3455
+ className: 'tg-pickrow',
3456
+ children: row.schemes.map((id, slot) => {
3457
+ const scheme = schemeById(id, schemes)
3458
+ // `cardRowShape` already proved every id resolves; the guard is here so a
3459
+ // table that changed between sanitising and rendering can only drop a
3460
+ // slot, never throw inside the render and blank the panel.
3461
+ if (scheme === undefined) return null
3462
+ return schemeSwatch(scheme, id === selectedScheme, onPick, slot)
3463
+ }),
3464
+ }, index)),
2491
3465
  })
2492
3466
  }
2493
3467
 
2494
3468
  /**
2495
- * Render one selectable theme card.
2496
- * @param props - id, label, description, swatches, selected, onSelect.
3469
+ * Render one card.
3470
+ *
3471
+ * Three layouts share the card: the default token strip, the palette picker (a
3472
+ * theme that declares `card.rows`), and — for the picker — no description body,
3473
+ * because the swatches already fill it. The description still becomes the tooltip
3474
+ * (`title`), which is what the schema says the field is for.
3475
+ *
3476
+ * A picker card is a `div`, not a `button`: it CONTAINS 15 buttons, and nesting
3477
+ * buttons inside a button is invalid markup that browsers silently re-parent.
3478
+ * @param props - id, label, description, swatches, rows, selected, applied,
3479
+ * selectedScheme, onSelect, onPickScheme.
2497
3480
  * @returns the card element.
2498
3481
  */
2499
3482
  function ThemeCard(props) {
2500
- const { id, label, description, swatches, selected, applied, onSelect, t } = props
3483
+ const {
3484
+ id, label, description, swatches, rows, selected, applied,
3485
+ selectedScheme, onSelect, onPickScheme, t,
3486
+ } = props
3487
+ // ── WHY THE FLAG, AND NOT `blocks !== null` ──────────────────────────────
3488
+ //
3489
+ // Whether this card is a picker has to be read off the DATA, not off the rendered
3490
+ // element. `jsx()` returns a real element in React but `null` under this
3491
+ // repository's jsx stub, so an element-identity test silently takes the STRIP
3492
+ // branch in every test while being correct in the app — a stub that does not
3493
+ // reproduce the runtime's return value is the same trap rule 8 records for
3494
+ // missing side effects.
3495
+ //
3496
+ // The flag comes from re-running the sanitiser rather than from a bare
3497
+ // `rows !== undefined`, so a component never trusts its inputs: whatever ends up
3498
+ // in the store, the card either draws a valid picker or falls back to the strip.
3499
+ // Handing malformed rows to the picker would throw inside the render and blank
3500
+ // the whole panel — the silent-failure shape this project keeps paying for.
3501
+ const shape = cardRowShape({ card: { rows } }, PALETTE_SCHEMES)
3502
+ const hasPicker = shape !== undefined
3503
+ const head = jsxs('div', {
3504
+ className: 'tg-card-top',
3505
+ children: [
3506
+ jsx('span', { className: 'tg-name', children: label || id }),
3507
+ selected ? jsx('span', { className: 'tg-badge', children: applied }) : null,
3508
+ ],
3509
+ })
3510
+ const body = hasPicker
3511
+ ? cardPickerElement(shape, selectedScheme, onPickScheme, PALETTE_SCHEMES)
3512
+ : swatches.length > 0
3513
+ ? jsx('span', {
3514
+ className: 'tg-strip',
3515
+ children: swatches.map((colour, index) => jsx('span', { style: { background: colour } }, index)),
3516
+ })
3517
+ : null
3518
+ // The picker fills the card, so it prints no body copy; `description` stays the
3519
+ // tooltip, which is what the schema says the field is for.
3520
+ const desc = !hasPicker && description
3521
+ ? jsx('span', { className: 'tg-desc', children: description })
3522
+ : null
3523
+ if (hasPicker) {
3524
+ return jsxs('div', {
3525
+ className: 'tg-card tg-picker-card',
3526
+ title: description || label,
3527
+ // ── 点卡片本体 = 用"亮着的那一格"──────────────────────────────────────
3528
+ //
3529
+ // 用户要的:点这张卡要**生效**(换成它的配色),点具体某一格才换成那一格。
3530
+ // 亮着的那一格就是记住的那套;从没选过时是 `DEFAULT_SCHEME`(石榴金,
3531
+ // 也就是锚主题自己那套)。所以"卡片本体"与"最左那格"最终落到同一件事上,
3532
+ // 只是入口不同 —— 用户不必知道这个区别。
3533
+ onClick: () => { onPickScheme(selectedScheme || DEFAULT_SCHEME) },
3534
+ children: [head, body, desc],
3535
+ })
3536
+ }
2501
3537
  return jsxs('button', {
2502
3538
  type: 'button',
2503
3539
  className: 'tg-card',
2504
3540
  'aria-pressed': selected,
2505
3541
  onClick: () => { onSelect(id) },
2506
3542
  title: description || label,
2507
- children: [
2508
- jsxs('div', {
2509
- className: 'tg-card-top',
2510
- children: [
2511
- jsx('span', { className: 'tg-name', children: label || id }),
2512
- selected ? jsx('span', { className: 'tg-badge', children: applied }) : null,
2513
- ],
2514
- }),
2515
- swatches.length > 0
2516
- ? jsx('span', {
2517
- className: 'tg-strip',
2518
- children: swatches.map((colour, index) => jsx('span', { style: { background: colour } }, index)),
2519
- })
2520
- : null,
2521
- description ? jsx('span', { className: 'tg-desc', children: description }) : null,
2522
- ],
3543
+ children: [head, body, desc],
2523
3544
  })
2524
3545
  }
2525
3546
 
@@ -5495,6 +6516,59 @@ window.__ModuleLoader__.load({
5495
6516
  })
5496
6517
  }
5497
6518
 
6519
+ /** 当前叠着的配色方案 id,没有就是 undefined。 */
6520
+ let stackedScheme
6521
+
6522
+ /** 配色层的 disposer(见 `syncScheme`)。 */
6523
+ let schemeLayerDispose
6524
+
6525
+ /**
6526
+ * 把所选配色方案叠到整屏上。
6527
+ *
6528
+ * ── 两条与 `syncAccent` 同源的约束 ────────────────────────────────────────
6529
+ *
6530
+ * 1. **只有锚主题是活动主题时才叠。** `overrideTokens` 的层是压在"当前活动主题"
6531
+ * 之上的,所以用户切到山青婷彩之后这一层必须撤除,否则会把配色糊到别人身上。
6532
+ * 判据取 `snapshot.active.id`,与 `syncAccent` 完全一致(**不能**引用
6533
+ * `wantedSkin` —— 那个函数在 `mountGallery` 内部,工厂级函数够不到它,
6534
+ * 正是硬性规则 4 记着的那个作用域事故)。
6535
+ * 2. **既有跳过、又有自发光守卫。** `overrideTokens` 与它返回的 disposer **都会**
6536
+ * emit `theme/change`,而本函数是被 `publish()` 调的,`publish` 又由那个事件驱动 ——
6537
+ * 少了守卫就是 0.1.4 那次 `RangeError` 刷屏的同一形状(规则 1)。
6538
+ * @param snapshot - 官方主题快照。
6539
+ */
6540
+ function syncScheme(snapshot) {
6541
+ if (ctx === undefined) return
6542
+ const active = snapshot === undefined || snapshot === null ? {} : (snapshot.active ?? {})
6543
+ const remembered = rememberedScheme()
6544
+ // ── 石榴金那一格**不叠派生层**(用户实机反馈:"不是你第一遍做的颜色")──────────
6545
+ //
6546
+ // 第一版那套配色(两排 10 个纯色 + 拼色那一排,合起来这一整套)**不是三个颜色**,
6547
+ // 而是锚主题皮肤 `lib/themes/shi-liu-jin.json` 里手工写好的 67 个 token —— 侧栏渐变、
6548
+ // 两层底、正文、按钮族都在里面。用户说"这个配色保留,很漂亮",留的就是这一整套。
6549
+ //
6550
+ // 所以这一格必须**等于皮肤自己那套**:只要锚主题是活动主题,`syncSkin` 就已经把
6551
+ // 皮肤自己的调色叠上了,这里撤掉配色层即可精确复原第一版。
6552
+ // 反过来说:若照旧拿 `main/ground/ink` 去派生 67 个 token,得到的是一套"相似但不等"
6553
+ // 的颜色(`state-business-primary` 会变成金色圆点色、侧栏渐变会按主色重算),
6554
+ // 用户一眼就看出来了 —— 这正是本次被指出来的问题。
6555
+ const wanted = remembered !== null && remembered !== DEFAULT_SCHEME && active.id === PALETTE_ANCHOR
6556
+ ? remembered
6557
+ : null
6558
+ if (wanted === stackedScheme) return
6559
+ const scheme = wanted === null ? undefined : schemeById(wanted, PALETTE_SCHEMES)
6560
+ emitting(() => {
6561
+ if (schemeLayerDispose !== undefined) {
6562
+ schemeLayerDispose()
6563
+ schemeLayerDispose = undefined
6564
+ }
6565
+ stackedScheme = undefined
6566
+ if (scheme === undefined) return
6567
+ schemeLayerDispose = ctx.theme.overrideTokens('theme-gallery: 配色', schemeTokens(scheme))
6568
+ stackedScheme = wanted
6569
+ })
6570
+ }
6571
+
5498
6572
  /**
5499
6573
  * Report whether the accent token layer actually reached the theme snapshot.
5500
6574
  *
@@ -5578,12 +6652,14 @@ window.__ModuleLoader__.load({
5578
6652
  * @param props - composed slot props.
5579
6653
  * @returns the page element tree.
5580
6654
  */
5581
- function ThemeGalleryPage({ t, setTheme, useStore, usePanelInfo }) {
6655
+ function ThemeGalleryPage({ t, setTheme, pickScheme, useStore, usePanelInfo }) {
5582
6656
  const info = usePanelInfo((s) => s.activePanelId)
5583
6657
  const ids = useStore((s) => s.ids)
5584
6658
  const labels = useStore((s) => s.labels)
5585
6659
  const descriptions = useStore((s) => s.descriptions)
5586
6660
  const swatches = useStore((s) => s.swatches)
6661
+ const cardRows = useStore((s) => s.cardRows)
6662
+ const scheme = useStore((s) => s.scheme)
5587
6663
  const selected = useStore((s) => s.selected)
5588
6664
  const status = useStore((s) => s.status)
5589
6665
  // The layout keeps this page registered whatever the selection is, so it
@@ -5632,6 +6708,11 @@ window.__ModuleLoader__.load({
5632
6708
  label: labels[id],
5633
6709
  description: descriptions[id],
5634
6710
  swatches: swatches[id] || [],
6711
+ // Absent for every strip card; the palette-picker card is the only one
6712
+ // that has it.
6713
+ rows: (cardRows || {})[id],
6714
+ // Which of the picker's 15 slots is lit. Ignored by strip cards.
6715
+ selectedScheme: scheme,
5635
6716
  selected: id === selected,
5636
6717
  applied: t('applied'),
5637
6718
  // The click is recorded BEFORE it is handed to the shell, because the theme
@@ -5639,6 +6720,10 @@ window.__ModuleLoader__.load({
5639
6720
  // skin: a built-in card clicked after a skin used to be undone immediately.
5640
6721
  // `rememberCardChoice` is what lets 浅色 / 深色 win — see its documentation.
5641
6722
  onSelect: (id) => { rememberCardChoice(id); setTheme(id) },
6723
+ // A picker slot: record the scheme first, then hand the anchor theme to the
6724
+ // shell exactly like clicking the card would — so the anchor becomes active
6725
+ // and the scheme layer has something to sit on.
6726
+ onPickScheme: (schemeId) => { pickScheme(schemeId) },
5642
6727
  t,
5643
6728
  }, id)),
5644
6729
  }),
@@ -6455,9 +7540,13 @@ window.__ModuleLoader__.load({
6455
7540
  // never change which themes exist, what they contain, or the order they were
6456
7541
  // contributed — `cardsInDisplayOrder` is a view over the filtered list.
6457
7542
  const themes = cardsInDisplayOrder([...snapshot.themes].filter((theme) => !OMITTED_IDS.has(theme.id)))
6458
- storeActions.sync(themes, snapshot.preference, revision)
7543
+ storeActions.sync(themes, appliedCardId(snapshot), revision)
7544
+ // 卡片上点亮哪一格,以及要不要把那一层配色叠上去 —— 两件事都读同一份
7545
+ // localStorage,所以"看到的选中态"与"屏幕上的颜色"不可能对不上。
7546
+ storeActions.markScheme(wantedScheme())
6459
7547
  syncSkin(snapshot)
6460
7548
  syncRememberedSkin(snapshot)
7549
+ syncScheme(snapshot)
6461
7550
  // Expose what the theme SERVICE believes, so the page can compare it with
6462
7551
  // what is actually in effect on the document. The presenter consumes this
6463
7552
  // same snapshot (`snapshot.active.tokens`) and writes it to `body`, so a
@@ -6480,6 +7569,74 @@ window.__ModuleLoader__.load({
6480
7569
  }
6481
7570
  }
6482
7571
 
7572
+ /**
7573
+ * 用户点了配色卡上的一个色值按钮(或卡片本体 = 用它记住的那一套,没有就是默认那套)。
7574
+ *
7575
+ * ── 为什么必须显式切主题(第一版漏了这一步,用户实机报"点了没反应")──────────
7576
+ *
7577
+ * 第一版只做了 `rememberScheme` + `rememberCardChoice(锚主题)` + `publish()`,以为
7578
+ * `syncRememberedSkin` 会把锚主题切过去。**它不会**:
7579
+ *
7580
+ * • `rememberCardChoice` 只**清掉**"内置外观"标记,它从不写 `last-skin`;
7581
+ * • 于是 `wantedSkin()` 仍指向原来那套皮肤,而 `syncRememberedSkin` 见到活动主题
7582
+ * 是本包皮肤就走"记住它"那条分支 —— 活动主题被原样记回去并继续维持;
7583
+ * • `syncScheme` 的判据是 `active.id === PALETTE_ANCHOR`,不成立 → 配色层永不叠加。
7584
+ *
7585
+ * 面板上的普通皮肤卡之所以能切,是因为它把 `setTheme` 交给了外壳(槽位动作那条例外)。
7586
+ *
7587
+ * ── 为什么这里用 `emitting(...)` 而不是再加一条审计例外 ─────────────────────
7588
+ *
7589
+ * 这次 `theme/change` 确实是"用户换了主题"的信号,但它**不需要**靠事件传递:本函数
7590
+ * 紧接着自己调一次 `publish()`,那一次的快照里活动主题已经是锚主题。把它标成"我造成的",
7591
+ * 订阅者就跳过这次回声,正好避免一次点击触发两轮同步(规则 1)。
7592
+ * @param schemeId - 配色方案 id(`p-…`)。
7593
+ */
7594
+ function chooseScheme(schemeId) {
7595
+ // 不认识的 id 直接丢:写进 localStorage 会让下次启动带着一个永远点不亮的选中态。
7596
+ if (schemeById(schemeId, PALETTE_SCHEMES) === undefined) return
7597
+ try {
7598
+ rememberScheme(schemeId)
7599
+ rememberCardChoice(PALETTE_ANCHOR)
7600
+ // 锚主题不是活动主题时必须真的切过去,否则上面那条链一步都不会发生。判据取活动主题,
7601
+ // 所以"已经在锚主题上再点另一格"不会白写一次 setTheme(同一个 id 的 setTheme
7602
+ // 是**不会 emit 的**,见 SKILL §12)。
7603
+ const activeId = ctx.theme.getTheme()?.active?.id
7604
+ if (activeId !== PALETTE_ANCHOR) {
7605
+ emitting(() => ctx.theme.setTheme(PALETTE_ANCHOR))
7606
+ }
7607
+ publish(ctx.theme.getTheme())
7608
+ } catch (error) {
7609
+ console.error('[theme-gallery] could not switch the palette scheme:', error)
7610
+ }
7611
+ }
7612
+
7613
+ /**
7614
+ * 卡片上「已应用」徽标应该落在哪一张卡上。
7615
+ *
7616
+ * ── 为什么不直接用 `snapshot.preference`(用户实机报的 bug)────────────────
7617
+ *
7618
+ * 点一个色值按钮时,徽标在「浅色」与「纯色/拼色」之间来回跳。根因是
7619
+ * {@link ensureSkinPainted} 的**跳转重试**:当服务已经报告皮肤、而文档还没落色时,
7620
+ * 它会先切到内置主题再切回皮肤(`emitting(() => ctx.theme.setTheme(BUILT_IN_PROBE_THEME))`),
7621
+ * 每次都是一次真实的 preference 变化 → 一次 publish → 徽标跟着那个**中间态**跳一下。
7622
+ *
7623
+ * 徽标要回答的是"用户选的是哪一张卡",不是"服务此刻停在哪一帧"。中间态有两个特征可以
7624
+ * 一票排除:它等于 {@link BUILT_IN_PROBE_THEME},而且用户**没有**在面板里选过任何内置外观
7625
+ * (`builtInChoice()` 为空)。两条同时成立时,按"想要的那套皮肤"显示 —— 跳转只是为了
7626
+ * 让表现层重新落色,不是一次选择。
7627
+ *
7628
+ * 其余情况一律照服务当下的活动主题显示(用户点深色就走深色、外部改外观也跟着走),
7629
+ * 所以这不是"徽标只认 localStorage",而是"只把中间态排除掉"。
7630
+ * @param snapshot - 官方主题快照。
7631
+ * @returns 应该带徽标的卡片 id。
7632
+ */
7633
+ function appliedCardId(snapshot) {
7634
+ const active = snapshot === undefined || snapshot === null ? {} : (snapshot.active ?? {})
7635
+ const wanted = wantedSkin()
7636
+ if (wanted !== null && active.id === BUILT_IN_PROBE_THEME && builtInChoice() === null) return wanted
7637
+ return active.id ?? snapshot?.preference
7638
+ }
7639
+
6483
7640
  /**
6484
7641
  * Resolve a bundled theme into the shape `register` actually consumes.
6485
7642
  *
@@ -6818,7 +7975,12 @@ window.__ModuleLoader__.load({
6818
7975
  // — so it is set BEFORE the publish that reads it.
6819
7976
  injectRan = true
6820
7977
  publish(ctx.theme.getTheme())
6821
- return { setTheme: (id) => { ctx.theme.setTheme(id) } }
7978
+ return {
7979
+ setTheme: (id) => { ctx.theme.setTheme(id) },
7980
+ // 配色卡上的 15 个色值按钮走这条。它**不直接**切主题(见 `chooseScheme`),
7981
+ // 所以这里不新增主题写入点。
7982
+ pickScheme: (schemeId) => { chooseScheme(schemeId) },
7983
+ }
6822
7984
  },
6823
7985
  }, ThemeGalleryPage)
6824
7986