dsh-theme-gallery 0.2.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
@@ -84,13 +99,479 @@ window.__ModuleLoader__.load({
84
99
  const BUILT_IN_LABELS = { light: '浅色', dark: '深色', system: '跟随系统' }
85
100
 
86
101
  /**
87
- * Built-in ids the gallery leaves to the official Appearance row.
102
+ * One-line copy for the built-in cards.
103
+ *
104
+ * The official themes declare no `description` of their own, so without this their
105
+ * cards would be a bare label. These two cards switch the PALETTE back to the system
106
+ * appearance — and they deliberately keep the previous skin's sidebar scenery on
107
+ * screen, which is the behaviour the second sentence advertises (see `syncAmbient`).
108
+ */
109
+ const BUILT_IN_DESCRIPTIONS = {
110
+ light: '系统默认浅色外观:与「设置 → 通用 → 外观」里的浅色是同一套配色。'
111
+ + '先选中主题皮肤,再切换浅色/深色 会有惊喜哦',
112
+ dark: '系统默认深色外观:与「设置 → 通用 → 外观」里的深色是同一套配色。'
113
+ + '先选中主题皮肤,再切换浅色/深色 会有惊喜哦',
114
+ system: '跟随系统外观设置(按桌面端的浅色/深色自动决定,因此不作为卡片展示)',
115
+ }
116
+
117
+ /**
118
+ * Built-in ids the gallery does not show as a card.
119
+ *
120
+ * `system` is a preference value rather than a look of its own — it resolves to
121
+ * `light` or `dark` depending on the desktop — so a card for it would duplicate one
122
+ * of the two cards above and change meaning with the OS setting. `light` and `dark`
123
+ * are BOTH shown now: together they are the way back out of every skin.
124
+ */
125
+ const OMITTED_IDS = new Set(['system'])
126
+
127
+ /**
128
+ * Display order of the gallery's cards — larger first.
129
+ *
130
+ * These are the user's own ranking numbers, kept as one table so the order is data
131
+ * rather than an implicit consequence of some other list. The REGISTRY order (and
132
+ * with it the contribution order in `BUNDLED_THEMES`) is deliberately untouched:
133
+ * this table is applied to the copy the page renders, and to nothing else.
134
+ *
135
+ * A theme with no entry sorts after every ranked one and keeps its registry order
136
+ * among the unranked (the sort is stable), so a theme contributed by another plugin
137
+ * still appears on the page instead of silently vanishing.
138
+ */
139
+ const CARD_ORDER = {
140
+ light: 99,
141
+ dark: 98,
142
+ 'shi-liu-jin': 97,
143
+ 'ying-mu-cai-yun': 96,
144
+ 'shan-qing-ting-cai': 95,
145
+ 'pei-an-jie-xin': 91,
146
+ 'meng-hai-you-yu': 80,
147
+ 'hu-po-mao-mi': 76,
148
+ 'hu-zi-a-huang': 75,
149
+ }
150
+
151
+ /**
152
+ * The skin this plugin puts in effect when the user has never chosen anything.
153
+ *
154
+ * Requested behaviour: installing/enabling the plugin lands on 山青婷彩 instead of the
155
+ * built-in white theme. Applied by the ordinary restore path, so it obeys the same
156
+ * global write budget and paint confirmation as a remembered skin — and it stops the
157
+ * moment the user makes any choice of their own, because that choice is recorded.
158
+ */
159
+ const DEFAULT_SKIN = 'shan-qing-ting-cai'
160
+
161
+ /**
162
+ * The two keys this plugin owns in `localStorage`.
163
+ *
164
+ * Never `ui-theme.preference`: that field is read while the boot graph is still
165
+ * assembling, and a skin id in it makes `buildSnapshot()` throw `theme registry lost`
166
+ * and refuse to start. `localStorage` is private to this plugin, so a disabled or
167
+ * uninstalled plugin simply stops being consulted — that is the fallback.
168
+ *
169
+ * They live at module scope because the card-click handler runs in the page component,
170
+ * far from `mountGallery`, and it must be able to write them BEFORE the click reaches
171
+ * the theme service (which publishes synchronously).
172
+ */
173
+ const SKIN_KEY = 'theme-gallery:last-skin'
174
+ const BUILT_IN_KEY = 'theme-gallery:built-in-choice'
175
+
176
+ /**
177
+ * A theme's card rank; unranked themes sort last.
178
+ * @param id - the theme id.
179
+ * @returns its rank, or -1 when the table has no entry for it.
180
+ */
181
+ function cardRank(id) {
182
+ const rank = CARD_ORDER[id]
183
+ return typeof rank === 'number' ? rank : -1
184
+ }
185
+
186
+ /**
187
+ * Order a card list for display: highest rank first.
188
+ *
189
+ * Applied to the copy the page renders, never to the array the service is given, so a
190
+ * theme's registration and its card position are independent concerns (and a reorder
191
+ * can never change which themes exist or what they contain).
192
+ * @param themes - theme definitions, in registry order.
193
+ * @returns a new array in display order.
194
+ */
195
+ function cardsInDisplayOrder(themes) {
196
+ return [...themes].sort((a, b) => cardRank(b.id) - cardRank(a.id))
197
+ }
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
+
477
+ /**
478
+ * Record the user's card choice BEFORE it reaches the theme service.
479
+ *
480
+ * ── WHY THE ORDER IS LOAD-BEARING ───────────────────────────────────────────
481
+ *
482
+ * `ctx.theme.setTheme()` publishes synchronously, and the publisher re-applies the
483
+ * remembered skin whenever the active theme is not one of ours. So a built-in card
484
+ * clicked after any skin had been chosen used to be undone a microtask later: the click
485
+ * landed, the restore read `SKIN_KEY`, and the skin came straight back. On screen the
486
+ * 深色 card simply did nothing.
487
+ *
488
+ * Removing `SKIN_KEY` here, before the write, is what makes a built-in choice stick.
489
+ * `BUILT_IN_KEY` is written so the default skin does NOT fire on the next restart and
490
+ * undo the choice — see `wantedSkin`.
491
+ *
492
+ * Any theme this package does not provide counts as "not a skin": another plugin's
493
+ * theme must win as well, for exactly the same reason.
494
+ * @param id - the card's theme id.
495
+ */
496
+ function rememberCardChoice(id) {
497
+ try {
498
+ if (typeof window === 'undefined') return
499
+ const storage = window.localStorage
500
+ if (storage === undefined || storage === null) return
501
+ if (bundledTheme(id) === undefined) {
502
+ storage.removeItem(SKIN_KEY)
503
+ storage.setItem(BUILT_IN_KEY, String(id))
504
+ return
505
+ }
506
+ // A skin: it is recorded by the restore once it is genuinely active, so the stale
507
+ // built-in marker is cleared here rather than there.
508
+ storage.removeItem(BUILT_IN_KEY)
509
+ } catch (error) {
510
+ console.error('[theme-gallery] could not record the card choice:', error)
511
+ }
512
+ }
513
+
514
+ /**
515
+ * The built-in appearance the user picked in this panel, if any.
516
+ * @returns the id, or null when the user never picked one here.
517
+ */
518
+ function builtInChoice() {
519
+ try {
520
+ if (typeof window === 'undefined') return null
521
+ const value = window.localStorage?.getItem(BUILT_IN_KEY)
522
+ return typeof value === 'string' && value !== '' ? value : null
523
+ } catch {
524
+ return null
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Record the palette scheme the user clicked on the picker card.
88
530
  *
89
- * `light` is exactly what "follow the system" resolves to on a light desktop,
90
- * so offering it beside that row is duplicate UI. `dark` stays because it is a
91
- * distinct choice a user may want without a full skin.
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-…`).
92
535
  */
93
- const OMITTED_IDS = new Set(['light'])
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
+ }
94
575
 
95
576
  /** Body attribute publishing the reading state; the injected rule reads it. */
96
577
  const READING_ATTRIBUTE = 'data-dsh-theme-reading'
@@ -118,7 +599,7 @@ window.__ModuleLoader__.load({
118
599
  * package.json, and the release workflow fails when re-running the embed step
119
600
  * changes a tracked file.
120
601
  */
121
- const BUNDLED_VERSION = '0.2.0'
602
+ const BUNDLED_VERSION = '0.4.0'
122
603
 
123
604
  /**
124
605
  * Themes this package contributes, inlined from lib/themes/*.json.
@@ -131,10 +612,586 @@ window.__ModuleLoader__.load({
131
612
  * Regenerate after editing those files: `node scripts/embed-themes.mjs`.
132
613
  */
133
614
  const BUNDLED_THEMES = [
615
+ {
616
+ "id": "hu-po-mao-mi",
617
+ "label": "琥珀猫咪",
618
+ "description": "琥珀暖光,双猫伴读 —— 原创宠物主题:两只虎斑短毛猫守着洒满阳光的窗台",
619
+ "colorScheme": "light",
620
+ "tokens": {
621
+ "--dsw-alias-bg-base": {
622
+ "light": "linear-gradient(to bottom,#FEFBF5 0%,#FBF2E3 55%,#F8EDD8 100%)",
623
+ "dark": "linear-gradient(to bottom,#FEFBF5 0%,#FBF2E3 55%,#F8EDD8 100%)"
624
+ },
625
+ "--dsw-alias-bg-layer-1": {
626
+ "light": "#FEFCF7",
627
+ "dark": "#FEFCF7"
628
+ },
629
+ "--dsw-alias-bg-layer-2": {
630
+ "light": "#FFFFFF",
631
+ "dark": "#FFFFFF"
632
+ },
633
+ "--dsw-alias-bg-layer-3": {
634
+ "light": "#FFFFFF",
635
+ "dark": "#FFFFFF"
636
+ },
637
+ "--dsw-alias-bg-overlay": {
638
+ "light": "#F3E5CD",
639
+ "dark": "#F3E5CD"
640
+ },
641
+ "--dsw-alias-bg-skeleton": {
642
+ "light": "#9C5F2414",
643
+ "dark": "#9C5F2414"
644
+ },
645
+ "--dsw-alias-bg-module-platform": {
646
+ "light": "#F3E5CD",
647
+ "dark": "#F3E5CD"
648
+ },
649
+ "--dsw-alias-bg-multi-select": {
650
+ "light": "#F3E5CD",
651
+ "dark": "#F3E5CD"
652
+ },
653
+ "--dsw-alias-border-l1": {
654
+ "light": "#9C5F240F",
655
+ "dark": "#9C5F240F"
656
+ },
657
+ "--dsw-alias-border-l2": {
658
+ "light": "#9C5F2421",
659
+ "dark": "#9C5F2421"
660
+ },
661
+ "--dsw-alias-border-l3": {
662
+ "light": "#9C5F242E",
663
+ "dark": "#9C5F242E"
664
+ },
665
+ "--dsw-alias-border-l4": {
666
+ "light": "#9C5F243D",
667
+ "dark": "#9C5F243D"
668
+ },
669
+ "--dsw-alias-brand-primary": {
670
+ "light": "#9C5F24",
671
+ "dark": "#9C5F24"
672
+ },
673
+ "--dsw-alias-brand-primary-invert": {
674
+ "light": "#FFFFFF",
675
+ "dark": "#FFFFFF"
676
+ },
677
+ "--dsw-alias-brand-text": {
678
+ "light": "#9C5F24",
679
+ "dark": "#9C5F24"
680
+ },
681
+ "--dsw-alias-label-primary": {
682
+ "light": "#453019",
683
+ "dark": "#453019"
684
+ },
685
+ "--dsw-alias-label-primary-bluish": {
686
+ "light": "#453019",
687
+ "dark": "#453019"
688
+ },
689
+ "--dsw-alias-label-secondary": {
690
+ "light": "#6A4E2F",
691
+ "dark": "#6A4E2F"
692
+ },
693
+ "--dsw-alias-label-tertiary": {
694
+ "light": "#8C6F4C",
695
+ "dark": "#8C6F4C"
696
+ },
697
+ "--dsw-alias-label-caption": {
698
+ "light": "#8C6F4C",
699
+ "dark": "#8C6F4C"
700
+ },
701
+ "--dsw-alias-label-dimmed": {
702
+ "light": "#B49C7F",
703
+ "dark": "#B49C7F"
704
+ },
705
+ "--dsw-alias-label-primary-dimmed": {
706
+ "light": "#6A4E2F",
707
+ "dark": "#6A4E2F"
708
+ },
709
+ "--dsw-alias-label-primary-foreground": {
710
+ "light": "#FFFFFF",
711
+ "dark": "#FFFFFF"
712
+ },
713
+ "--dsw-alias-label-primary-inverted": {
714
+ "light": "#FFFFFF",
715
+ "dark": "#FFFFFF"
716
+ },
717
+ "--dsw-alias-link": {
718
+ "light": "#CA6924",
719
+ "dark": "#CA6924"
720
+ },
721
+ "--dsw-alias-interactive-bg-hover": {
722
+ "light": "#9C5F2414",
723
+ "dark": "#9C5F2414"
724
+ },
725
+ "--dsw-alias-interactive-bg-active": {
726
+ "light": "#CA69242E",
727
+ "dark": "#CA69242E"
728
+ },
729
+ "--dsw-alias-interactive-bg-hover-solid": {
730
+ "light": "#F3E5CD",
731
+ "dark": "#F3E5CD"
732
+ },
733
+ "--dsw-alias-interactive-bg-hover-accent": {
734
+ "light": "#CA69243D",
735
+ "dark": "#CA69243D"
736
+ },
737
+ "--dsw-alias-button-primary-fill": {
738
+ "light": "#9C5F24",
739
+ "dark": "#9C5F24"
740
+ },
741
+ "--dsw-alias-button-primary-hover": {
742
+ "light": "#B0702F",
743
+ "dark": "#B0702F"
744
+ },
745
+ "--dsw-alias-button-primary-dimmed": {
746
+ "light": "#9C5F2447",
747
+ "dark": "#9C5F2447"
748
+ },
749
+ "--dsw-alias-button-ghost-active-fill": {
750
+ "light": "#CA692429",
751
+ "dark": "#CA692429"
752
+ },
753
+ "--dsw-alias-button-ghost-active-border": {
754
+ "light": "#CA6924",
755
+ "dark": "#CA6924"
756
+ },
757
+ "--dsw-alias-button-ghost-active-hover": {
758
+ "light": "#CA692447",
759
+ "dark": "#CA692447"
760
+ },
761
+ "--dsw-alias-button-info-fill": {
762
+ "light": "#B0702F",
763
+ "dark": "#B0702F"
764
+ },
765
+ "--dsw-alias-button-info-hover": {
766
+ "light": "#9C5F24",
767
+ "dark": "#9C5F24"
768
+ },
769
+ "--dsw-alias-button-elevated-fill": {
770
+ "light": "#FFFFFF",
771
+ "dark": "#FFFFFF"
772
+ },
773
+ "--dsw-alias-button-floating-fill": {
774
+ "light": "#FFFFFF",
775
+ "dark": "#FFFFFF"
776
+ },
777
+ "--dsw-alias-button-floating-hover": {
778
+ "light": "#FDF8F0",
779
+ "dark": "#FDF8F0"
780
+ },
781
+ "--dsw-alias-button-contrast-fill": {
782
+ "light": "#453019",
783
+ "dark": "#453019"
784
+ },
785
+ "--dsw-alias-markdown-code-block": {
786
+ "light": "#F7EEDF",
787
+ "dark": "#F7EEDF"
788
+ },
789
+ "--dsw-alias-markdown-code-block-banner": {
790
+ "light": "#F0E3CB",
791
+ "dark": "#F0E3CB"
792
+ },
793
+ "--dsw-alias-markdown-inline-code": {
794
+ "light": "#F3E9D6",
795
+ "dark": "#F3E9D6"
796
+ },
797
+ "--dsw-alias-markdown-citation": {
798
+ "light": "#F3E9D6",
799
+ "dark": "#F3E9D6"
800
+ },
801
+ "--dsw-alias-markdown-tag": {
802
+ "light": "#F3E9D6",
803
+ "dark": "#F3E9D6"
804
+ },
805
+ "--dsw-alias-markdown-placeholder": {
806
+ "light": "#F0E3CB",
807
+ "dark": "#F0E3CB"
808
+ },
809
+ "--dsw-alias-markdown-code-segment-selected": {
810
+ "light": "#CA692429",
811
+ "dark": "#CA692429"
812
+ },
813
+ "--dsw-alias-markdown-code-segment-unselected": {
814
+ "light": "#F7EEDF",
815
+ "dark": "#F7EEDF"
816
+ },
817
+ "--dsw-alias-scrollbar-bg-l1": {
818
+ "light": "#9C5F2429",
819
+ "dark": "#9C5F2429"
820
+ },
821
+ "--dsw-alias-scrollbar-bg-l2": {
822
+ "light": "#9C5F2438",
823
+ "dark": "#9C5F2438"
824
+ },
825
+ "--dsw-alias-scrollbar-hover-l1": {
826
+ "light": "#9C5F2452",
827
+ "dark": "#9C5F2452"
828
+ },
829
+ "--dsw-alias-scrollbar-hover-l2": {
830
+ "light": "#9C5F2466",
831
+ "dark": "#9C5F2466"
832
+ },
833
+ "--dsw-alias-tooltip-bg": {
834
+ "light": "#453019",
835
+ "dark": "#453019"
836
+ },
837
+ "--dsw-alias-toast-bg": {
838
+ "light": "#FFFFFF",
839
+ "dark": "#FFFFFF"
840
+ },
841
+ "--dsw-alias-state-error-primary": {
842
+ "light": "#C65B6A",
843
+ "dark": "#C65B6A"
844
+ },
845
+ "--dsw-alias-state-error-secondary": {
846
+ "light": "#DB5A6B",
847
+ "dark": "#DB5A6B"
848
+ },
849
+ "--dsw-alias-state-success-primary": {
850
+ "light": "#21A675",
851
+ "dark": "#21A675"
852
+ },
853
+ "--dsw-alias-state-success-secondary": {
854
+ "light": "#9C5F24",
855
+ "dark": "#9C5F24"
856
+ },
857
+ "--dsw-alias-state-success-tertiary": {
858
+ "light": "#CFBA9C",
859
+ "dark": "#CFBA9C"
860
+ },
861
+ "--dsw-alias-state-warn-primary": {
862
+ "light": "#B97F3A",
863
+ "dark": "#B97F3A"
864
+ },
865
+ "--dsw-alias-state-warn-secondary": {
866
+ "light": "#D9A45E",
867
+ "dark": "#D9A45E"
868
+ },
869
+ "--dsw-alias-state-warn-tertiary": {
870
+ "light": "#EFDCC0",
871
+ "dark": "#EFDCC0"
872
+ },
873
+ "--dsw-alias-state-warn-label": {
874
+ "light": "#8A5A22",
875
+ "dark": "#8A5A22"
876
+ },
877
+ "--dsw-alias-state-business-primary": {
878
+ "light": "#CA6924",
879
+ "dark": "#CA6924"
880
+ },
881
+ "--dsw-alias-state-business-tertiary": {
882
+ "light": "#E29C45",
883
+ "dark": "#E29C45"
884
+ },
885
+ "--dsw-specific-sidebar-fill": {
886
+ "light": "linear-gradient(to bottom,#FBF0DE 0%,#F5E3C6 20%,#EDD5AE 36%,#E3C593 50%,#D8B57D 62%,#CBA468 74%,#C69E5F 86%,#BB904F 100%)",
887
+ "dark": "linear-gradient(to bottom,#FBF0DE 0%,#F5E3C6 20%,#EDD5AE 36%,#E3C593 50%,#D8B57D 62%,#CBA468 74%,#C69E5F 86%,#BB904F 100%)"
888
+ }
889
+ },
890
+ "reading": {
891
+ "colorScheme": "light",
892
+ "bg": "#FFFFFF",
893
+ "alpha": 0.62,
894
+ "blur": 3,
895
+ "maxWidth": 640
896
+ },
897
+ "accent": "#CA6924",
898
+ "ambient": {
899
+ "kind": "humao",
900
+ "dust": 6
901
+ }
902
+ },
903
+ {
904
+ "id": "hu-zi-a-huang",
905
+ "label": "虎子阿黄",
906
+ "description": "麦香田园,黄犬相迎 —— 原创宠物主题:中黄田园犬「虎子阿黄」摇着尾巴守在田埂上",
907
+ "colorScheme": "light",
908
+ "tokens": {
909
+ "--dsw-alias-bg-base": {
910
+ "light": "linear-gradient(to bottom,#FEFDF6 0%,#FBF5E0 55%,#F7EFD0 100%)",
911
+ "dark": "linear-gradient(to bottom,#FEFDF6 0%,#FBF5E0 55%,#F7EFD0 100%)"
912
+ },
913
+ "--dsw-alias-bg-layer-1": {
914
+ "light": "#FEFCF4",
915
+ "dark": "#FEFCF4"
916
+ },
917
+ "--dsw-alias-bg-layer-2": {
918
+ "light": "#FFFFFF",
919
+ "dark": "#FFFFFF"
920
+ },
921
+ "--dsw-alias-bg-layer-3": {
922
+ "light": "#FFFFFF",
923
+ "dark": "#FFFFFF"
924
+ },
925
+ "--dsw-alias-bg-overlay": {
926
+ "light": "#F4EAD0",
927
+ "dark": "#F4EAD0"
928
+ },
929
+ "--dsw-alias-bg-skeleton": {
930
+ "light": "#896C3914",
931
+ "dark": "#896C3914"
932
+ },
933
+ "--dsw-alias-bg-module-platform": {
934
+ "light": "#F4EAD0",
935
+ "dark": "#F4EAD0"
936
+ },
937
+ "--dsw-alias-bg-multi-select": {
938
+ "light": "#F4EAD0",
939
+ "dark": "#F4EAD0"
940
+ },
941
+ "--dsw-alias-border-l1": {
942
+ "light": "#896C390F",
943
+ "dark": "#896C390F"
944
+ },
945
+ "--dsw-alias-border-l2": {
946
+ "light": "#896C3921",
947
+ "dark": "#896C3921"
948
+ },
949
+ "--dsw-alias-border-l3": {
950
+ "light": "#896C392E",
951
+ "dark": "#896C392E"
952
+ },
953
+ "--dsw-alias-border-l4": {
954
+ "light": "#896C393D",
955
+ "dark": "#896C393D"
956
+ },
957
+ "--dsw-alias-brand-primary": {
958
+ "light": "#896C39",
959
+ "dark": "#896C39"
960
+ },
961
+ "--dsw-alias-brand-primary-invert": {
962
+ "light": "#FFFFFF",
963
+ "dark": "#FFFFFF"
964
+ },
965
+ "--dsw-alias-brand-text": {
966
+ "light": "#896C39",
967
+ "dark": "#896C39"
968
+ },
969
+ "--dsw-alias-label-primary": {
970
+ "light": "#3B3018",
971
+ "dark": "#3B3018"
972
+ },
973
+ "--dsw-alias-label-primary-bluish": {
974
+ "light": "#3B3018",
975
+ "dark": "#3B3018"
976
+ },
977
+ "--dsw-alias-label-secondary": {
978
+ "light": "#5D4E2B",
979
+ "dark": "#5D4E2B"
980
+ },
981
+ "--dsw-alias-label-tertiary": {
982
+ "light": "#857451",
983
+ "dark": "#857451"
984
+ },
985
+ "--dsw-alias-label-caption": {
986
+ "light": "#857451",
987
+ "dark": "#857451"
988
+ },
989
+ "--dsw-alias-label-dimmed": {
990
+ "light": "#B2A47E",
991
+ "dark": "#B2A47E"
992
+ },
993
+ "--dsw-alias-label-primary-dimmed": {
994
+ "light": "#5D4E2B",
995
+ "dark": "#5D4E2B"
996
+ },
997
+ "--dsw-alias-label-primary-foreground": {
998
+ "light": "#FFFFFF",
999
+ "dark": "#FFFFFF"
1000
+ },
1001
+ "--dsw-alias-label-primary-inverted": {
1002
+ "light": "#FFFFFF",
1003
+ "dark": "#FFFFFF"
1004
+ },
1005
+ "--dsw-alias-link": {
1006
+ "light": "#AE7000",
1007
+ "dark": "#AE7000"
1008
+ },
1009
+ "--dsw-alias-interactive-bg-hover": {
1010
+ "light": "#896C3914",
1011
+ "dark": "#896C3914"
1012
+ },
1013
+ "--dsw-alias-interactive-bg-active": {
1014
+ "light": "#F0C2392E",
1015
+ "dark": "#F0C2392E"
1016
+ },
1017
+ "--dsw-alias-interactive-bg-hover-solid": {
1018
+ "light": "#F4EAD0",
1019
+ "dark": "#F4EAD0"
1020
+ },
1021
+ "--dsw-alias-interactive-bg-hover-accent": {
1022
+ "light": "#F0C2393D",
1023
+ "dark": "#F0C2393D"
1024
+ },
1025
+ "--dsw-alias-button-primary-fill": {
1026
+ "light": "#896C39",
1027
+ "dark": "#896C39"
1028
+ },
1029
+ "--dsw-alias-button-primary-hover": {
1030
+ "light": "#A88462",
1031
+ "dark": "#A88462"
1032
+ },
1033
+ "--dsw-alias-button-primary-dimmed": {
1034
+ "light": "#896C3947",
1035
+ "dark": "#896C3947"
1036
+ },
1037
+ "--dsw-alias-button-ghost-active-fill": {
1038
+ "light": "#F0C23929",
1039
+ "dark": "#F0C23929"
1040
+ },
1041
+ "--dsw-alias-button-ghost-active-border": {
1042
+ "light": "#F0C239",
1043
+ "dark": "#F0C239"
1044
+ },
1045
+ "--dsw-alias-button-ghost-active-hover": {
1046
+ "light": "#F0C23947",
1047
+ "dark": "#F0C23947"
1048
+ },
1049
+ "--dsw-alias-button-info-fill": {
1050
+ "light": "#A88462",
1051
+ "dark": "#A88462"
1052
+ },
1053
+ "--dsw-alias-button-info-hover": {
1054
+ "light": "#896C39",
1055
+ "dark": "#896C39"
1056
+ },
1057
+ "--dsw-alias-button-elevated-fill": {
1058
+ "light": "#FFFFFF",
1059
+ "dark": "#FFFFFF"
1060
+ },
1061
+ "--dsw-alias-button-floating-fill": {
1062
+ "light": "#FFFFFF",
1063
+ "dark": "#FFFFFF"
1064
+ },
1065
+ "--dsw-alias-button-floating-hover": {
1066
+ "light": "#FCF8EC",
1067
+ "dark": "#FCF8EC"
1068
+ },
1069
+ "--dsw-alias-button-contrast-fill": {
1070
+ "light": "#3B3018",
1071
+ "dark": "#3B3018"
1072
+ },
1073
+ "--dsw-alias-markdown-code-block": {
1074
+ "light": "#F8F1DC",
1075
+ "dark": "#F8F1DC"
1076
+ },
1077
+ "--dsw-alias-markdown-code-block-banner": {
1078
+ "light": "#F1E7C6",
1079
+ "dark": "#F1E7C6"
1080
+ },
1081
+ "--dsw-alias-markdown-inline-code": {
1082
+ "light": "#F4EAD0",
1083
+ "dark": "#F4EAD0"
1084
+ },
1085
+ "--dsw-alias-markdown-citation": {
1086
+ "light": "#F4EAD0",
1087
+ "dark": "#F4EAD0"
1088
+ },
1089
+ "--dsw-alias-markdown-tag": {
1090
+ "light": "#F4EAD0",
1091
+ "dark": "#F4EAD0"
1092
+ },
1093
+ "--dsw-alias-markdown-placeholder": {
1094
+ "light": "#F1E7C6",
1095
+ "dark": "#F1E7C6"
1096
+ },
1097
+ "--dsw-alias-markdown-code-segment-selected": {
1098
+ "light": "#F0C23929",
1099
+ "dark": "#F0C23929"
1100
+ },
1101
+ "--dsw-alias-markdown-code-segment-unselected": {
1102
+ "light": "#F8F1DC",
1103
+ "dark": "#F8F1DC"
1104
+ },
1105
+ "--dsw-alias-scrollbar-bg-l1": {
1106
+ "light": "#896C3929",
1107
+ "dark": "#896C3929"
1108
+ },
1109
+ "--dsw-alias-scrollbar-bg-l2": {
1110
+ "light": "#896C3938",
1111
+ "dark": "#896C3938"
1112
+ },
1113
+ "--dsw-alias-scrollbar-hover-l1": {
1114
+ "light": "#896C3952",
1115
+ "dark": "#896C3952"
1116
+ },
1117
+ "--dsw-alias-scrollbar-hover-l2": {
1118
+ "light": "#896C3966",
1119
+ "dark": "#896C3966"
1120
+ },
1121
+ "--dsw-alias-tooltip-bg": {
1122
+ "light": "#3B3018",
1123
+ "dark": "#3B3018"
1124
+ },
1125
+ "--dsw-alias-toast-bg": {
1126
+ "light": "#FFFFFF",
1127
+ "dark": "#FFFFFF"
1128
+ },
1129
+ "--dsw-alias-state-error-primary": {
1130
+ "light": "#C65B6A",
1131
+ "dark": "#C65B6A"
1132
+ },
1133
+ "--dsw-alias-state-error-secondary": {
1134
+ "light": "#DB5A6B",
1135
+ "dark": "#DB5A6B"
1136
+ },
1137
+ "--dsw-alias-state-success-primary": {
1138
+ "light": "#21A675",
1139
+ "dark": "#21A675"
1140
+ },
1141
+ "--dsw-alias-state-success-secondary": {
1142
+ "light": "#896C39",
1143
+ "dark": "#896C39"
1144
+ },
1145
+ "--dsw-alias-state-success-tertiary": {
1146
+ "light": "#D1C69C",
1147
+ "dark": "#D1C69C"
1148
+ },
1149
+ "--dsw-alias-state-warn-primary": {
1150
+ "light": "#B97F3A",
1151
+ "dark": "#B97F3A"
1152
+ },
1153
+ "--dsw-alias-state-warn-secondary": {
1154
+ "light": "#D9A45E",
1155
+ "dark": "#D9A45E"
1156
+ },
1157
+ "--dsw-alias-state-warn-tertiary": {
1158
+ "light": "#EFDCC0",
1159
+ "dark": "#EFDCC0"
1160
+ },
1161
+ "--dsw-alias-state-warn-label": {
1162
+ "light": "#8A5A22",
1163
+ "dark": "#8A5A22"
1164
+ },
1165
+ "--dsw-alias-state-business-primary": {
1166
+ "light": "#F0C239",
1167
+ "dark": "#F0C239"
1168
+ },
1169
+ "--dsw-alias-state-business-tertiary": {
1170
+ "light": "#F5D46B",
1171
+ "dark": "#F5D46B"
1172
+ },
1173
+ "--dsw-specific-sidebar-fill": {
1174
+ "light": "linear-gradient(to bottom,#FAF3DC 0%,#F4EAC2 20%,#ECDFAB 36%,#E2D191 50%,#D6C078 62%,#C9AF63 74%,#C4A85F 86%,#B89B51 100%)",
1175
+ "dark": "linear-gradient(to bottom,#FAF3DC 0%,#F4EAC2 20%,#ECDFAB 36%,#E2D191 50%,#D6C078 62%,#C9AF63 74%,#C4A85F 86%,#B89B51 100%)"
1176
+ }
1177
+ },
1178
+ "reading": {
1179
+ "colorScheme": "light",
1180
+ "bg": "#FFFFFF",
1181
+ "alpha": 0.62,
1182
+ "blur": 3,
1183
+ "maxWidth": 640
1184
+ },
1185
+ "accent": "#F0C239",
1186
+ "ambient": {
1187
+ "kind": "ahuang",
1188
+ "dust": 5
1189
+ }
1190
+ },
134
1191
  {
135
1192
  "id": "meng-hai-you-yu",
136
1193
  "label": "梦海游鱼",
137
- "description": "梦幻海洋,游鱼作伴 —— 复刻自电商新零售系统管理后台的默认主题",
1194
+ "description": "梦幻海洋,游鱼作伴 —— 复刻自蜂链商城电商新零售系统管理后台的默认主题",
138
1195
  "colorScheme": "light",
139
1196
  "tokens": {
140
1197
  "--dsw-alias-bg-base": {
@@ -424,7 +1481,7 @@ window.__ModuleLoader__.load({
424
1481
  {
425
1482
  "id": "pei-an-jie-xin",
426
1483
  "label": "佩安杰心",
427
- "description": "檀香禅影,自在安顿 —— 复刻自电商新零售系统管理后台同名主题",
1484
+ "description": "檀香禅影,自在安顿 —— 复刻自蜂链商城电商新零售系统管理后台同名主题",
428
1485
  "colorScheme": "light",
429
1486
  "tokens": {
430
1487
  "--dsw-alias-bg-base": {
@@ -712,7 +1769,7 @@ window.__ModuleLoader__.load({
712
1769
  {
713
1770
  "id": "shan-qing-ting-cai",
714
1771
  "label": "山青婷彩",
715
- "description": "青山叠翠,蜓舞生姿 —— 复刻自电商新零售系统管理后台同名主题",
1772
+ "description": "青山叠翠,蜓舞生姿 —— 复刻自蜂链商城电商新零售系统管理后台同名主题",
716
1773
  "colorScheme": "light",
717
1774
  "tokens": {
718
1775
  "--dsw-alias-bg-base": {
@@ -976,31 +2033,349 @@ window.__ModuleLoader__.load({
976
2033
  "dark": "#E88BB0"
977
2034
  },
978
2035
  "--dsw-alias-state-business-tertiary": {
979
- "light": "#F8A8C2",
980
- "dark": "#F8A8C2"
2036
+ "light": "#F8A8C2",
2037
+ "dark": "#F8A8C2"
2038
+ },
2039
+ "--dsw-specific-sidebar-fill": {
2040
+ "light": "linear-gradient(to bottom,#EAF7F0 0%,#D8EFE4 20%,#C9E8DA 36%,#BFE2D2 50%,#B4DCCA 62%,#A8D2BE 74%,#9CC9B2 86%,#90C0A8 100%)",
2041
+ "dark": "linear-gradient(to bottom,#EAF7F0 0%,#D8EFE4 20%,#C9E8DA 36%,#BFE2D2 50%,#B4DCCA 62%,#A8D2BE 74%,#9CC9B2 86%,#90C0A8 100%)"
2042
+ }
2043
+ },
2044
+ "reading": {
2045
+ "colorScheme": "light",
2046
+ "bg": "#FFFFFF",
2047
+ "alpha": 0.62,
2048
+ "blur": 3,
2049
+ "maxWidth": 640
2050
+ },
2051
+ "accent": "#E88BB0",
2052
+ "ambient": {
2053
+ "kind": "shan",
2054
+ "petals": 7
2055
+ }
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"
981
2326
  },
982
2327
  "--dsw-specific-sidebar-fill": {
983
- "light": "linear-gradient(to bottom,#EAF7F0 0%,#D8EFE4 20%,#C9E8DA 36%,#BFE2D2 50%,#B4DCCA 62%,#A8D2BE 74%,#9CC9B2 86%,#90C0A8 100%)",
984
- "dark": "linear-gradient(to bottom,#EAF7F0 0%,#D8EFE4 20%,#C9E8DA 36%,#BFE2D2 50%,#B4DCCA 62%,#A8D2BE 74%,#9CC9B2 86%,#90C0A8 100%)"
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%)"
985
2330
  }
986
2331
  },
987
2332
  "reading": {
988
2333
  "colorScheme": "light",
989
- "bg": "#FFFFFF",
2334
+ "bg": "#FFFBF0",
990
2335
  "alpha": 0.62,
991
2336
  "blur": 3,
992
2337
  "maxWidth": 640
993
2338
  },
994
- "accent": "#E88BB0",
995
- "ambient": {
996
- "kind": "shan",
997
- "petals": 7
998
- }
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"
999
2374
  },
1000
2375
  {
1001
2376
  "id": "ying-mu-cai-yun",
1002
2377
  "label": "营慕彩云",
1003
- "description": "营幕之下,彩云为伴 —— 复刻自电商新零售系统管理后台同名主题",
2378
+ "description": "营幕之下,彩云为伴 —— 复刻自蜂链商城电商新零售系统管理后台同名主题",
1004
2379
  "colorScheme": "light",
1005
2380
  "tokens": {
1006
2381
  "--dsw-alias-bg-base": {
@@ -1287,6 +2662,181 @@ window.__ModuleLoader__.load({
1287
2662
  }
1288
2663
  ]
1289
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
+
1290
2840
  /**
1291
2841
  * Marks the ambient nodes this run created.
1292
2842
  *
@@ -1610,6 +3160,32 @@ window.__ModuleLoader__.load({
1610
3160
  '@keyframes dsh-amb-gc-wing{0%{transform:rotate(0deg)}40%{transform:rotate(-3.5deg)}60%{transform:rotate(2.5deg)}100%{transform:rotate(0deg)}}',
1611
3161
  '@keyframes dsh-amb-gc-dew{0%{opacity:.3;transform:scale(1)}100%{opacity:1;transform:scale(1.5)}}',
1612
3162
 
3163
+ /* ── 琥珀猫咪 / 虎子阿黄(原创宠物场景)──────────────────────────────
3164
+ Like the five ported scenes above: element geometry, colour and
3165
+ per-element timing are INLINE in the scene builders, so only the
3166
+ @keyframes live here — each prefixed (hm / hz) so they cannot collide,
3167
+ and each defined exactly once (a duplicate would silently win). Every
3168
+ animated vertical term stays at or below the resting baseline, because
3169
+ the scene box clips at its own top edge. */
3170
+ '@keyframes dsh-amb-hm-glow{from{opacity:.55}to{opacity:.85}}',
3171
+ '@keyframes dsh-amb-hm-dust{0%{transform:translate(0,0);opacity:0}12%{opacity:.9}'
3172
+ + '60%{transform:translate(-.5em,-3.2em);opacity:.45}100%{transform:translate(.4em,-4.6em);opacity:0}}',
3173
+ '@keyframes dsh-amb-hm-tail{from{transform:rotate(-3deg)}to{transform:rotate(5deg)}}',
3174
+ '@keyframes dsh-amb-hm-breathe{0%,100%{transform:scaleY(1)}50%{transform:scaleY(1.03)}}',
3175
+ '@keyframes dsh-amb-hm-ear{0%,86%,100%{transform:rotate(0)}90%{transform:rotate(-9deg)}94%{transform:rotate(4deg)}}',
3176
+ '@keyframes dsh-amb-hm-zzz{0%{transform:translate(0,0);opacity:0}18%{opacity:.85}'
3177
+ + '100%{transform:translate(1.2em,-2.6em);opacity:0}}',
3178
+ '@keyframes dsh-amb-hz-glow{from{opacity:.45}to{opacity:.75}}',
3179
+ /* The fluff crosses the whole band and dies at the far edge; its vertical
3180
+ wobble stays within ±1.2em so a low-seeded puff never reaches the clip. */
3181
+ '@keyframes dsh-amb-hz-fluff{0%{transform:translate(0,0);opacity:0}7%{opacity:.95}'
3182
+ + '30%{transform:translate(8em,-1.2em)}55%{transform:translate(14em,.7em)}'
3183
+ + '80%{transform:translate(20em,-.9em);opacity:.75}96%{opacity:.4}'
3184
+ + '100%{transform:translate(26em,0);opacity:0}}',
3185
+ '@keyframes dsh-amb-hz-wag{from{transform:rotate(-9deg)}to{transform:rotate(11deg)}}',
3186
+ '@keyframes dsh-amb-hz-grass{from{transform:rotate(-2.2deg)}to{transform:rotate(2.6deg)}}',
3187
+ '@keyframes dsh-amb-hz-tilt{0%,40%,100%{transform:rotate(0)}55%{transform:rotate(2.4deg)}75%{transform:rotate(-1.4deg)}}',
3188
+
1613
3189
  /* Respect a user who has asked the system for less motion: the scenery
1614
3190
  stays, the movement does not. */
1615
3191
  '@media (prefers-reduced-motion:reduce){#dsh-theme-ambient *{animation:none!important}}',
@@ -1619,7 +3195,7 @@ window.__ModuleLoader__.load({
1619
3195
  const zh = {
1620
3196
  title: '主题皮肤',
1621
3197
  hint: '点一下即切换,选中会记入设置',
1622
- count: '{count} 个可选主题',
3198
+ count: '{count} 款皮肤(另有内置浅色/深色两张卡)',
1623
3199
  current: '当前',
1624
3200
  applied: '已应用',
1625
3201
  empty: '正在读取官方主题注册表…',
@@ -1631,7 +3207,7 @@ window.__ModuleLoader__.load({
1631
3207
  const en = {
1632
3208
  title: 'Theme skins',
1633
3209
  hint: 'Click to switch; the choice is remembered',
1634
- count: '{count} themes available',
3210
+ count: '{count} skins (+ built-in light/dark cards)',
1635
3211
  current: 'Current',
1636
3212
  applied: 'Applied',
1637
3213
  empty: 'Reading the theme registry…',
@@ -1687,6 +3263,51 @@ window.__ModuleLoader__.load({
1687
3263
  '.tg-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}',
1688
3264
  '.tg-strip{display:flex;height:6px;border-radius:999px;overflow:hidden;border:.5px solid var(--dsw-alias-border-l3)}',
1689
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)}',
1690
3311
  ].join('')
1691
3312
 
1692
3313
  /**
@@ -1699,22 +3320,27 @@ window.__ModuleLoader__.load({
1699
3320
  */
1700
3321
  function createGalleryStore() {
1701
3322
  return defineStore({
1702
- 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
+ }),
1703
3327
  actions: {
1704
3328
  sync: (draft, themes, selected, revision) => {
1705
3329
  if (revision <= draft.revision) return
1706
3330
  const labels = {}
1707
3331
  const descriptions = {}
1708
3332
  const swatches = {}
3333
+ const cardRows = {}
1709
3334
  for (const theme of themes) {
1710
3335
  // Built-in themes carry no label — the official Appearance row
1711
3336
  // localizes them from its own dictionaries — so name them here
1712
3337
  // rather than showing a bare `light` / `dark` id.
1713
3338
  labels[theme.id] = theme.label || BUILT_IN_LABELS[theme.id] || theme.id
1714
- // A built-in theme derives its colours from the base palette rather
1715
- // than declaring tokens, so its card gets a plain line.
1716
- descriptions[theme.id] = theme.description
1717
- || (theme.id in BUILT_IN_LABELS ? '' : '')
3339
+ // A built-in theme derives its colours from the base palette rather than
3340
+ // declaring tokens, so its card gets a plain line — and it carries no
3341
+ // description of its own, so the copy for the built-in cards is the
3342
+ // gallery's own (`BUILT_IN_DESCRIPTIONS`).
3343
+ descriptions[theme.id] = theme.description || BUILT_IN_DESCRIPTIONS[theme.id] || ''
1718
3344
  // A card's colour strip comes from the theme's own tokens, so a
1719
3345
  // theme authored anywhere shows its identity without extra metadata.
1720
3346
  const tokens = theme.tokens || {}
@@ -1728,11 +3354,17 @@ window.__ModuleLoader__.load({
1728
3354
  pick('--dsw-alias-label-secondary'),
1729
3355
  pick('--dsw-alias-state-business-primary'),
1730
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
1731
3362
  }
1732
3363
  draft.ids = themes.map((theme) => theme.id)
1733
3364
  draft.labels = labels
1734
3365
  draft.descriptions = descriptions
1735
3366
  draft.swatches = swatches
3367
+ draft.cardRows = cardRows
1736
3368
  draft.selected = selected
1737
3369
  draft.revision = revision
1738
3370
  },
@@ -1745,39 +3377,170 @@ window.__ModuleLoader__.load({
1745
3377
  * @param status - one line describing the state.
1746
3378
  */
1747
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)
1748
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)),
1749
3465
  })
1750
3466
  }
1751
3467
 
1752
3468
  /**
1753
- * Render one selectable theme card.
1754
- * @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.
1755
3480
  * @returns the card element.
1756
3481
  */
1757
3482
  function ThemeCard(props) {
1758
- 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
+ }
1759
3537
  return jsxs('button', {
1760
3538
  type: 'button',
1761
3539
  className: 'tg-card',
1762
3540
  'aria-pressed': selected,
1763
3541
  onClick: () => { onSelect(id) },
1764
3542
  title: description || label,
1765
- children: [
1766
- jsxs('div', {
1767
- className: 'tg-card-top',
1768
- children: [
1769
- jsx('span', { className: 'tg-name', children: label || id }),
1770
- selected ? jsx('span', { className: 'tg-badge', children: applied }) : null,
1771
- ],
1772
- }),
1773
- swatches.length > 0
1774
- ? jsx('span', {
1775
- className: 'tg-strip',
1776
- children: swatches.map((colour, index) => jsx('span', { style: { background: colour } }, index)),
1777
- })
1778
- : null,
1779
- description ? jsx('span', { className: 'tg-desc', children: description }) : null,
1780
- ],
3543
+ children: [head, body, desc],
1781
3544
  })
1782
3545
  }
1783
3546
 
@@ -3279,6 +5042,328 @@ window.__ModuleLoader__.load({
3279
5042
  + '</div>'
3280
5043
  }
3281
5044
 
5045
+ /**
5046
+ * One sun-warmed haze for the two ORIGINAL pet scenes (琥珀猫咪 / 虎子阿黄).
5047
+ *
5048
+ * Both scenes open with the same soft light pool at the band's top, built the way
5049
+ * dream's corner glow was taught to behave: it starts AT the edge, is dimmer than
5050
+ * the source skies, and is masked to zero at its top so the scene box's
5051
+ * `overflow:hidden` clip never cuts a hard line against the un-lit sidebar above.
5052
+ * @param tint - the rgba() fill of the haze, in the theme's own family.
5053
+ * @param keyframe - the pulse animation, `dsh-amb-hm-glow` or `dsh-amb-hz-glow`.
5054
+ * @returns the haze markup.
5055
+ */
5056
+ function petHazeMarkup(tint, keyframe) {
5057
+ return open('div', {
5058
+ class: 'pet-haze',
5059
+ style: `position:absolute;top:0;left:-20%;width:130%;height:52%;z-index:2;`
5060
+ + `background:radial-gradient(ellipse at 30% 0%,${tint} 0%,rgba(255,236,200,0) 64%);`
5061
+ + 'filter:blur(10px);'
5062
+ + '-webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 42%);'
5063
+ + 'mask-image:linear-gradient(to bottom,transparent 0,#000 42%);'
5064
+ + `animation:${keyframe} 9s ease-in-out infinite alternate`,
5065
+ }) + '</div>'
5066
+ }
5067
+
5068
+ /**
5069
+ * The 琥珀猫咪 scene: a warm haze, a sunlit sill, TWO tabby shorthairs —
5070
+ * one sitting with a slow tail sway and the odd ear twitch, one curled asleep,
5071
+ * breathing, with tiny z's drifting off it — and amber dust motes floating
5072
+ * through the light.
5073
+ *
5074
+ * This is the first ORIGINAL scene in the pack (not ported from a source
5075
+ * system), drawn in the pack's own idiom: inline geometry in % / em, flat
5076
+ * fills from one warm amber family, the count knob seeding the dust, and
5077
+ * every animated transform bounded below the band's top edge — the sleeping
5078
+ * cat breathes from its base and the z's fade out well before they could
5079
+ * reach the clip.
5080
+ * @param dust - how many dust motes to seed.
5081
+ * @returns the scene markup.
5082
+ */
5083
+ function humaoAmbientScene(dust) {
5084
+ const count = Math.max(0, Math.min(20, dust ?? 6))
5085
+ let dustNodes = ''
5086
+ for (let n = 1; n <= count; n += 1) {
5087
+ const size = 2.5 + ((n * 5) % 4)
5088
+ dustNodes += open('div', {
5089
+ class: 'hm-dust',
5090
+ style: 'position:absolute;border-radius:50%;'
5091
+ + `left:${((n * 19) % 52) + 22}%;top:${((n * 13) % 24) + 36}%;`
5092
+ + `width:${size}px;height:${size}px;`
5093
+ + 'background:radial-gradient(circle,rgba(255,236,200,.95) 0%,rgba(255,214,140,0) 100%);'
5094
+ + 'box-shadow:0 0 6px rgba(255,214,140,.75);'
5095
+ + `animation:dsh-amb-hm-dust ${9 + ((n * 7) % 6)}s linear infinite;`
5096
+ + `animation-delay:${-(n * 2.3)}s`,
5097
+ }) + '</div>'
5098
+ }
5099
+
5100
+ // 猫 A —— 坐姿虎斑,面朝左(第一版形态:地面绕尾、橄榄形上身、后侧三道
5101
+ // 弧纹)。连接方式按用户指定:**不垫任何脖子形状**,头部整组(头圆 + 双耳
5102
+ // + 五官 + 胡须 + 额头纹)作为整体下移 28、右移 2,头圆下缘直接压进身体
5103
+ // 上缘约 7.5 个单位(肩点 (52,52) 距头心 14.1 < r15,也埋进头圆),同色
5104
+ // 填充使接缝不可见;下巴与胸口之间留下的小凹角正是猫下巴该有的位置。
5105
+ // 奶油色胸部椭圆按用户要求落在 (39,76)。
5106
+ const catSit = open('svg', {
5107
+ viewBox: '0 0 100 118',
5108
+ style: 'display:block;width:100%;height:auto;overflow:visible',
5109
+ })
5110
+ + open('g', {
5111
+ class: 'hm-tail',
5112
+ style: 'transform-box:fill-box;transform-origin:0% 100%;'
5113
+ + 'animation:dsh-amb-hm-tail 5.5s ease-in-out infinite alternate',
5114
+ })
5115
+ + open('path', {
5116
+ d: 'M62 112 C82 115 90 103 85 90 C82 82 74 80 70 84',
5117
+ fill: 'none', stroke: '#C98B4B', 'stroke-width': '7', 'stroke-linecap': 'round',
5118
+ }) + '</path>'
5119
+ + open('path', {
5120
+ d: 'M86 92 C83 84 76 81 71 84',
5121
+ fill: 'none', stroke: '#8A5A2B', 'stroke-width': '7', 'stroke-linecap': 'round',
5122
+ }) + '</path>'
5123
+ + '</g>'
5124
+ + open('path', {
5125
+ d: 'M30 116 C26 106 26 92 30 78 C34 64 42 56 52 52 C62 48 70 56 72 70 C74 86 72 104 66 116 Z',
5126
+ fill: '#D9A05B',
5127
+ }) + '</path>'
5128
+ + open('ellipse', { cx: '39', cy: '76', rx: '10', ry: '19', fill: '#F0D6A6' }) + '</ellipse>'
5129
+ + open('rect', { x: '32', y: '88', width: '6', height: '28', rx: '3', fill: '#E5BC7E', stroke: '#C98B4B', 'stroke-width': '1' }) + '</rect>'
5130
+ + open('rect', { x: '42', y: '88', width: '6', height: '28', rx: '3', fill: '#E5BC7E', stroke: '#C98B4B', 'stroke-width': '1' }) + '</rect>'
5131
+ + open('path', { d: 'M56 54 C64 58 68 64 70 72', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '4', 'stroke-linecap': 'round', opacity: '0.85' }) + '</path>'
5132
+ + open('path', { d: 'M60 74 C66 78 69 84 70 90', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '4', 'stroke-linecap': 'round', opacity: '0.85' }) + '</path>'
5133
+ + open('path', { d: 'M58 96 C63 99 66 104 67 110', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '4', 'stroke-linecap': 'round', opacity: '0.85' }) + '</path>'
5134
+ + open('path', { d: 'M34 40 L34 46 M38 39 L38 45 M30 41 L30 47', stroke: '#8A5A2B', 'stroke-width': '2', 'stroke-linecap': 'round' }) + '</path>'
5135
+ + open('path', { d: 'M26 46 L23 31 L35 39 Z', fill: '#D9A05B' }) + '</path>'
5136
+ + open('path', { d: 'M27.5 43 L26 35 L33 40 Z', fill: '#C97B6B', opacity: '0.75' }) + '</path>'
5137
+ + open('g', {
5138
+ class: 'hm-ear',
5139
+ style: 'transform-box:fill-box;transform-origin:0% 100%;'
5140
+ + 'animation:dsh-amb-hm-ear 7s ease-in-out infinite',
5141
+ })
5142
+ + open('path', { d: 'M44 42 L52 30 L50 44 Z', fill: '#D9A05B' }) + '</path>'
5143
+ + open('path', { d: 'M46 40.5 L50.5 33.5 L48.5 41.5 Z', fill: '#C97B6B', opacity: '0.75' }) + '</path>'
5144
+ + '</g>'
5145
+ + open('circle', { cx: '38', cy: '54', r: '15', fill: '#D9A05B' }) + '</circle>'
5146
+ + open('circle', { cx: '32', cy: '52', r: '2.2', fill: '#3B2A16' }) + '</circle>'
5147
+ + open('circle', { cx: '44', cy: '52', r: '2.2', fill: '#3B2A16' }) + '</circle>'
5148
+ + open('circle', { cx: '32.7', cy: '51.2', r: '0.7', fill: '#FFFFFF' }) + '</circle>'
5149
+ + open('circle', { cx: '44.7', cy: '51.2', r: '0.7', fill: '#FFFFFF' }) + '</circle>'
5150
+ + open('path', { d: 'M36 58 L40 58 L38 61 Z', fill: '#C97B6B' }) + '</path>'
5151
+ + open('path', { d: 'M38 61 C38 63 36 64 34 64 M38 61 C38 63 40 64 42 64', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '1', 'stroke-linecap': 'round' }) + '</path>'
5152
+ + open('path', { d: 'M26 57 L14 55 M26 60 L14 62 M52 57 L62 55 M52 60 L62 62', stroke: '#B98A55', 'stroke-width': '1', 'stroke-linecap': 'round', opacity: '0.8' }) + '</path>'
5153
+ + '</svg>'
5154
+
5155
+ // 猫 B —— 蜷成一团酣睡:圆头明显叠在身子右前侧(同色相接),折耳 +
5156
+ // 闭眼 + 鼻尖胡须 + 身前露出一只奶油色前爪,尾巴沿身前绕一圈、
5157
+ // 深色尾尖收在左侧;整体随呼吸微微起伏,头顶冒出两枚会消散的小 z。
5158
+ const catCurled = open('svg', {
5159
+ viewBox: '0 0 112 66',
5160
+ style: 'display:block;width:100%;height:auto;overflow:visible',
5161
+ })
5162
+ + open('g', {
5163
+ class: 'hm-breathe',
5164
+ style: 'transform-box:fill-box;transform-origin:50% 100%;'
5165
+ + 'animation:dsh-amb-hm-breathe 3.8s ease-in-out infinite',
5166
+ })
5167
+ + open('path', {
5168
+ d: 'M6 62 C6 44 24 28 52 27 C80 26 100 40 99 53 C98 60 92 62 84 62 Z',
5169
+ fill: '#D9A05B',
5170
+ }) + '</path>'
5171
+ + open('path', { d: 'M26 30 C28 38 28 48 26 57', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '4', 'stroke-linecap': 'round', opacity: '0.8' }) + '</path>'
5172
+ + open('path', { d: 'M38 28 C40 37 40 49 38 59', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '4', 'stroke-linecap': 'round', opacity: '0.8' }) + '</path>'
5173
+ + open('path', { d: 'M50 27 C52 36 52 47 51 56', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '4', 'stroke-linecap': 'round', opacity: '0.8' }) + '</path>'
5174
+ + open('path', { d: 'M6 58 C26 66 62 67 92 59', fill: 'none', stroke: '#C98B4B', 'stroke-width': '6', 'stroke-linecap': 'round' }) + '</path>'
5175
+ + open('path', { d: 'M6 58 C12 61 18 63 24 64', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '6', 'stroke-linecap': 'round' }) + '</path>'
5176
+ + open('circle', { cx: '80', cy: '44', r: '15', fill: '#D9A05B' }) + '</circle>'
5177
+ + open('path', { d: 'M70 33 L65 23 L75.5 28.5 Z', fill: '#D9A05B' }) + '</path>'
5178
+ + open('path', { d: 'M88 31 L94 22 L95.5 32 Z', fill: '#D9A05B' }) + '</path>'
5179
+ + open('path', { d: 'M70.5 30.5 L68 25 L73.5 28 Z', fill: '#C97B6B', opacity: '0.75' }) + '</path>'
5180
+ + open('path', { d: 'M88.5 29.5 L92.5 24 L93.5 30.5 Z', fill: '#C97B6B', opacity: '0.75' }) + '</path>'
5181
+ + open('path', { d: 'M72 45 Q75 48 78 45', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '1.4', 'stroke-linecap': 'round' }) + '</path>'
5182
+ + open('path', { d: 'M65.5 46.5 L69 46.5 L67.25 49.5 Z', fill: '#C97B6B' }) + '</path>'
5183
+ + open('path', { d: 'M62 48.5 L54 47.5 M62 51 L55 52.5', stroke: '#B98A55', 'stroke-width': '1', 'stroke-linecap': 'round', opacity: '0.8' }) + '</path>'
5184
+ + open('ellipse', { cx: '60', cy: '60', rx: '7', ry: '3.5', fill: '#F0D6A6', stroke: '#C98B4B', 'stroke-width': '1' }) + '</ellipse>'
5185
+ + '</g>'
5186
+ + '</svg>'
5187
+ + open('span', {
5188
+ class: 'hm-z hm-z-1',
5189
+ style: 'position:absolute;right:12%;top:-12%;font-size:.85em;font-style:italic;'
5190
+ + "font-family:Georgia,'Times New Roman',serif;color:#A97F4C;"
5191
+ + 'animation:dsh-amb-hm-zzz 5s ease-in-out infinite',
5192
+ }) + 'z</span>'
5193
+ + open('span', {
5194
+ class: 'hm-z hm-z-2',
5195
+ style: 'position:absolute;right:5%;top:-22%;font-size:.65em;font-style:italic;'
5196
+ + "font-family:Georgia,'Times New Roman',serif;color:#B98F55;"
5197
+ + 'animation:dsh-amb-hm-zzz 5s ease-in-out infinite;animation-delay:-2.5s',
5198
+ }) + 'z</span>'
5199
+
5200
+ return open('div', { class: 'hm', style: 'position:absolute;inset:0;display:block' })
5201
+ + petHazeMarkup('rgba(255,214,140,.5)', 'dsh-amb-hm-glow')
5202
+ + open('div', {
5203
+ class: 'hm-sill',
5204
+ style: 'position:absolute;left:0;right:0;bottom:0;height:12%;z-index:4;'
5205
+ + 'background:linear-gradient(to bottom,rgba(154,95,36,0) 0%,rgba(154,95,36,.16) 40%,rgba(138,84,30,.3) 100%)',
5206
+ })
5207
+ + open('div', {
5208
+ class: 'hm-sill-line',
5209
+ style: 'position:absolute;top:0;left:0;right:0;height:1.2px;opacity:.55;'
5210
+ + 'background:linear-gradient(90deg,transparent,rgba(255,244,220,.9),transparent)',
5211
+ }) + '</div>'
5212
+ + '</div>'
5213
+ + open('div', {
5214
+ class: 'hm-cat-sit',
5215
+ style: 'position:absolute;left:7%;bottom:9%;width:7.5em;z-index:6',
5216
+ }) + catSit + '</div>'
5217
+ + open('div', {
5218
+ class: 'hm-cat-curled',
5219
+ style: 'position:absolute;right:6%;bottom:7%;width:8.4em;z-index:6',
5220
+ }) + catCurled + '</div>'
5221
+ + dustNodes
5222
+ + '</div>'
5223
+ }
5224
+
5225
+ /**
5226
+ * The 虎子阿黄 scene: a golden haze, a field-path bank, the yellow Chinese
5227
+ * pastoral dog himself — upright ears, a sickle tail wagging at speed, an
5228
+ * occasional curious head tilt, a straw collar with a bell — with dry
5229
+ * grass tufts, a ball in the theme's own 缃色, and dandelion fluff drifting
5230
+ * across on the wind.
5231
+ *
5232
+ * The second ORIGINAL scene in the pack, same idiom as the cat: inline
5233
+ * % / em geometry, one warm wheat-gold family, and every animated transform
5234
+ * bounded below the band's top edge — the fluff fades out at the far edge
5235
+ * instead of clipping.
5236
+ * @param dust - how many dandelion-fluff puffs to seed.
5237
+ * @returns the scene markup.
5238
+ */
5239
+ function ahuangAmbientScene(dust) {
5240
+ const count = Math.max(0, Math.min(20, dust ?? 5))
5241
+ let fluffNodes = ''
5242
+ for (let n = 1; n <= count; n += 1) {
5243
+ const size = 4 + ((n * 3) % 3)
5244
+ fluffNodes += open('div', {
5245
+ class: 'hz-fluff',
5246
+ style: 'position:absolute;border-radius:50%;'
5247
+ + `left:-6%;top:${((n * 11) % 30) + 18}%;`
5248
+ + `width:${size}px;height:${size}px;`
5249
+ + 'background:radial-gradient(circle,#FFFDF4 0%,rgba(255,246,214,.85) 55%,rgba(255,246,214,0) 100%);'
5250
+ + 'box-shadow:0 0 5px rgba(255,244,200,.8);'
5251
+ + `animation:dsh-amb-hz-fluff ${15 + ((n * 7) % 8)}s linear infinite;`
5252
+ + `animation-delay:${-(n * 2.6)}s`,
5253
+ }) + '</div>'
5254
+ }
5255
+
5256
+ // 虎子阿黄 —— 坐姿面朝左。头与躯干同样按「必须重叠」构造:头部圆下缘
5257
+ // 压进胸腔椭圆上缘 3 个单位,项圈画在重叠带上把脖子束出来;立耳、
5258
+ // 镰刀尾(尾根埋进后臀圆里)快速摇摆、偶尔歪头,吐着舌头等主人。
5259
+ const dog = open('svg', {
5260
+ viewBox: '0 0 112 122',
5261
+ style: 'display:block;width:100%;height:auto;overflow:visible',
5262
+ })
5263
+ + open('g', {
5264
+ class: 'hz-tail',
5265
+ style: 'transform-box:fill-box;transform-origin:0% 100%;'
5266
+ + 'animation:dsh-amb-hz-wag .95s ease-in-out infinite alternate',
5267
+ })
5268
+ + open('path', {
5269
+ d: 'M76 70 C92 66 100 50 92 36 C88 30 82 29 78 32',
5270
+ fill: 'none', stroke: '#D8A44E', 'stroke-width': '8', 'stroke-linecap': 'round',
5271
+ }) + '</path>'
5272
+ + open('path', {
5273
+ d: 'M93 37 C90 32 85 30 79 32',
5274
+ fill: 'none', stroke: '#B97F3A', 'stroke-width': '8', 'stroke-linecap': 'round',
5275
+ }) + '</path>'
5276
+ + '</g>'
5277
+ + open('circle', { cx: '72', cy: '94', r: '26', fill: '#E8B45A' }) + '</circle>'
5278
+ + open('ellipse', { cx: '44', cy: '93', rx: '18', ry: '27', fill: '#E8B45A' }) + '</ellipse>'
5279
+ + open('ellipse', { cx: '56', cy: '117', rx: '13', ry: '5', fill: '#E8B45A' }) + '</ellipse>'
5280
+ + open('ellipse', { cx: '42', cy: '97', rx: '10', ry: '19', fill: '#F6E3B8' }) + '</ellipse>'
5281
+ + open('rect', { x: '32', y: '92', width: '7.5', height: '26', rx: '3.7', fill: '#E8B45A', stroke: '#C98F35', 'stroke-width': '1' }) + '</rect>'
5282
+ + open('rect', { x: '44', y: '92', width: '7.5', height: '26', rx: '3.7', fill: '#E8B45A', stroke: '#C98F35', 'stroke-width': '1' }) + '</rect>'
5283
+ + open('ellipse', { cx: '35.7', cy: '117', rx: '6', ry: '3.2', fill: '#F6E3B8' }) + '</ellipse>'
5284
+ + open('ellipse', { cx: '47.7', cy: '117', rx: '6', ry: '3.2', fill: '#F6E3B8' }) + '</ellipse>'
5285
+ + open('path', { d: 'M31 68 C38 74 50 74 56 66', fill: 'none', stroke: '#896C39', 'stroke-width': '5', 'stroke-linecap': 'round' }) + '</path>'
5286
+ + open('circle', { cx: '33', cy: '72.5', r: '2.6', fill: '#F0C239', stroke: '#B98A20', 'stroke-width': '0.8' }) + '</circle>'
5287
+ + open('path', { d: 'M30.6 72.5 L35.4 72.5', stroke: '#B98A20', 'stroke-width': '0.8' }) + '</path>'
5288
+ + open('g', {
5289
+ class: 'hz-head',
5290
+ style: 'transform-box:fill-box;transform-origin:60% 88%;'
5291
+ + 'animation:dsh-amb-hz-tilt 7s ease-in-out infinite',
5292
+ })
5293
+ + open('path', { d: 'M28 40 C24 30 23 23 26 18 C30 21 34 28 35.5 36 Z', fill: '#D8A44E' }) + '</path>'
5294
+ + open('path', { d: 'M48 38 C50 28 52 21 56 18 C58 23 56 30 52.5 38.5 Z', fill: '#D8A44E' }) + '</path>'
5295
+ + open('path', { d: 'M29 36 C27 29 26.5 25 28 21.5 C30.5 24.5 32.5 29 33.5 34 Z', fill: '#C97B6B', opacity: '0.7' }) + '</path>'
5296
+ + open('path', { d: 'M49.5 34 C51 27.5 52.5 23.5 55 21 C56 25 54.5 30 52 35.5 Z', fill: '#C97B6B', opacity: '0.7' }) + '</path>'
5297
+ + open('circle', { cx: '42', cy: '51', r: '18', fill: '#E8B45A' }) + '</circle>'
5298
+ + open('ellipse', { cx: '27', cy: '57', rx: '11', ry: '8.5', fill: '#F6E3B8' }) + '</ellipse>'
5299
+ + open('ellipse', { cx: '19', cy: '53.5', rx: '3', ry: '2.4', fill: '#4A331C' }) + '</ellipse>'
5300
+ + open('circle', { cx: '44', cy: '47', r: '2.4', fill: '#3B2A16' }) + '</circle>'
5301
+ + open('circle', { cx: '44.8', cy: '46.2', r: '0.8', fill: '#FFFFFF' }) + '</circle>'
5302
+ + open('path', { d: 'M22.5 61 C24.5 64.5 29.5 65.5 32.5 62.5', fill: 'none', stroke: '#8A5F2B', 'stroke-width': '1.3', 'stroke-linecap': 'round' }) + '</path>'
5303
+ + open('path', { d: 'M24 62.5 C28 63.5 29 67.5 27 70 C25 72 22 71 22 68.5 C22 66 22.5 64 24 62.5 Z', fill: '#DE7E68' }) + '</path>'
5304
+ + open('path', { d: 'M24.5 64 C25.5 65.5 25.8 67.5 25.2 69.5', fill: 'none', stroke: '#C05E4E', 'stroke-width': '0.9', 'stroke-linecap': 'round' }) + '</path>'
5305
+ + '</g>'
5306
+ + '</svg>'
5307
+
5308
+ const grassTuft = (left, bottom, width, zIndex) => open('div', {
5309
+ class: 'hz-grass',
5310
+ style: `position:absolute;${left};bottom:${bottom};width:${width};z-index:${zIndex}`,
5311
+ })
5312
+ + open('svg', {
5313
+ viewBox: '0 0 60 30',
5314
+ style: 'display:block;width:100%;height:auto;overflow:visible',
5315
+ })
5316
+ + open('g', {
5317
+ class: 'hz-blades',
5318
+ style: 'transform-box:fill-box;transform-origin:50% 100%;'
5319
+ + 'animation:dsh-amb-hz-grass 5.2s ease-in-out infinite alternate',
5320
+ })
5321
+ + open('path', { d: 'M6 30 C4 22 6 14 10 8', fill: 'none', stroke: '#C2A24E', 'stroke-width': '2.6', 'stroke-linecap': 'round' }) + '</path>'
5322
+ + open('path', { d: 'M14 30 C13 20 16 12 20 6', fill: 'none', stroke: '#B08F3E', 'stroke-width': '2.4', 'stroke-linecap': 'round' }) + '</path>'
5323
+ + open('path', { d: 'M24 30 C24 20 27 12 32 7', fill: 'none', stroke: '#C2A24E', 'stroke-width': '2.2', 'stroke-linecap': 'round' }) + '</path>'
5324
+ + open('path', { d: 'M36 30 C38 22 42 15 48 10', fill: 'none', stroke: '#B08F3E', 'stroke-width': '2.4', 'stroke-linecap': 'round' }) + '</path>'
5325
+ + open('path', { d: 'M46 30 C48 24 52 18 56 14', fill: 'none', stroke: '#C2A24E', 'stroke-width': '2.2', 'stroke-linecap': 'round' }) + '</path>'
5326
+ + '</g>'
5327
+ + '</svg>'
5328
+ + '</div>'
5329
+
5330
+ return open('div', { class: 'hz', style: 'position:absolute;inset:0;display:block' })
5331
+ + petHazeMarkup('rgba(240,194,57,.42)', 'dsh-amb-hz-glow')
5332
+ + open('div', {
5333
+ class: 'hz-bank',
5334
+ style: 'position:absolute;left:0;right:0;bottom:0;height:14%;z-index:4;'
5335
+ + 'background:linear-gradient(to bottom,rgba(137,108,57,0) 0%,rgba(137,108,57,.16) 40%,rgba(120,92,44,.3) 100%)',
5336
+ })
5337
+ + open('div', {
5338
+ class: 'hz-bank-line',
5339
+ style: 'position:absolute;top:0;left:0;right:0;height:1.2px;opacity:.5;'
5340
+ + 'background:linear-gradient(90deg,transparent,rgba(255,250,228,.9),transparent)',
5341
+ }) + '</div>'
5342
+ + '</div>'
5343
+ + grassTuft('left:2%', '0', '4.5em', '5')
5344
+ + grassTuft('left:17%', '1%', '3.6em', '7')
5345
+ + grassTuft('right:3%', '0', '4.2em', '5')
5346
+ + open('div', {
5347
+ class: 'hz-ball',
5348
+ style: 'position:absolute;left:14%;bottom:8%;width:1.7em;z-index:6',
5349
+ })
5350
+ + open('svg', {
5351
+ viewBox: '0 0 20 20',
5352
+ style: 'display:block;width:100%;height:auto',
5353
+ })
5354
+ + open('circle', { cx: '10', cy: '10', r: '9', fill: '#F0C239', stroke: '#C9A02F', 'stroke-width': '1' }) + '</circle>'
5355
+ + open('path', { d: 'M4 4 C8 8 8 12 4 16 M16 4 C12 8 12 12 16 16', fill: 'none', stroke: '#B98A20', 'stroke-width': '1.2', 'stroke-linecap': 'round' }) + '</path>'
5356
+ + open('ellipse', { cx: '7', cy: '6.5', rx: '2.6', ry: '1.6', fill: '#FFF3C4', opacity: '0.85' }) + '</ellipse>'
5357
+ + '</svg>'
5358
+ + '</div>'
5359
+ + open('div', {
5360
+ class: 'hz-dog',
5361
+ style: 'position:absolute;left:32%;bottom:7%;width:8em;z-index:6',
5362
+ }) + dog + '</div>'
5363
+ + fluffNodes
5364
+ + '</div>'
5365
+ }
5366
+
3282
5367
  /**
3283
5368
  * Apply a function on every tick until the thing it observes stops changing.
3284
5369
  *
@@ -3369,7 +5454,11 @@ window.__ModuleLoader__.load({
3369
5454
  ? jiexinAmbientScene(options?.dust)
3370
5455
  : kind === 'fengchen'
3371
5456
  ? fengchenAmbientScene(options?.feathers, options?.dew)
3372
- : ''
5457
+ : kind === 'humao'
5458
+ ? humaoAmbientScene(options?.dust)
5459
+ : kind === 'ahuang'
5460
+ ? ahuangAmbientScene(options?.dust)
5461
+ : ''
3373
5462
  // The builder is fed only attributes and hex values, but this is the one place
3374
5463
  // markup from outside this file could ever arrive, so it refuses anyway.
3375
5464
  if (/<script|\son[a-z]+\s*=/i.test(markup)) {
@@ -3731,11 +5820,12 @@ window.__ModuleLoader__.load({
3731
5820
  }
3732
5821
 
3733
5822
  /**
3734
- * Remove every ambient node, ownership aside.
5823
+ * Remove every legacy ambient SEAT, ownership aside.
3735
5824
  *
3736
- * Used when no scenery should exist at all: a theme without `ambient`, or a seat
3737
- * whose placement can no longer be computed. Leaving a layer behind would paint one
3738
- * theme's scenery over the next one's.
5825
+ * Despite the name it does NOT touch the live `.dsh-amb-control` layer: that layer is
5826
+ * owned by `drawScene`, and it is only rewritten when a theme actually has scenery. So
5827
+ * this call is safe on the "theme has no scenery" path, where leaving the layer in
5828
+ * place is the INTENDED behaviour — see the note in `syncAmbient`.
3739
5829
  */
3740
5830
  function removeAllAmbientSeats() {
3741
5831
  if (typeof document === 'undefined') return
@@ -3858,10 +5948,21 @@ window.__ModuleLoader__.load({
3858
5948
  /**
3859
5949
  * Render the active theme's scenery into the sidebar column.
3860
5950
  *
3861
- * Idempotent: the seat is created once and its contents are replaced whenever
3862
- * the active theme (or its ambient config) changes, so re-running is cheap and
3863
- * cannot stack duplicate scenes. Themes with no `ambient` get an empty seat —
3864
- * the scenery belongs to the skin, so switching skins must remove it.
5951
+ * Idempotent: the scene box is reused and its content is replaced only when the markup
5952
+ * changes, so re-running never stacks duplicate scenes (two copies of a scene = two of
5953
+ * every animated element, which is what produced the duplicated dragonflies).
5954
+ *
5955
+ * Two different "no scenery" cases, and they are NOT the same:
5956
+ *
5957
+ * · switching to ANOTHER skin — the markup differs, so the box is repainted with the
5958
+ * new skin's artwork;
5959
+ * · switching to a theme that has no `ambient` at all — the BUILT-IN 浅色 / 深色 cards.
5960
+ * Only the legacy seats are swept; the live `.dsh-amb-control` layer is deliberately
5961
+ * LEFT ALONE, so the previous skin's artwork (balloons, fish, mountains…) stays on
5962
+ * screen over the system palette. That leftover is a feature the user asked to keep
5963
+ * — the built-in cards advertise it ("会有惊喜哦") and
5964
+ * `tests/check-boot-path.mjs` asserts it, so a later "cleanup" cannot remove it
5965
+ * without turning the test red.
3865
5966
  * @param kind - the ambient kind, or undefined for none.
3866
5967
  * @param options - the theme's ambient options.
3867
5968
  */
@@ -3934,6 +6035,18 @@ window.__ModuleLoader__.load({
3934
6035
  const markup = sceneMarkup(kind, options)
3935
6036
 
3936
6037
  if (markup === '') {
6038
+ // ── THE LIVE LAYER IS NOT TOUCHED HERE, ON PURPOSE ───────────────────────
6039
+ //
6040
+ // This branch runs when the active theme has no scenery — i.e. after clicking one of
6041
+ // the built-in 浅色 / 深色 cards. `removeAllAmbientSeats()` only sweeps
6042
+ // `#dsh-theme-ambient` SEATS (a container earlier builds created); the layer that
6043
+ // actually paints is `.dsh-amb-control`, and it is only ever rewritten by
6044
+ // `drawScene()`. So the previous skin's artwork stays where it is.
6045
+ //
6046
+ // That is the user's "惊喜": pick a skin, then switch to 浅色 / 深色, and the
6047
+ // sidebar keeps the balloons / fish / mountains while the palette goes back to the
6048
+ // system appearance. It is asserted in `tests/check-boot-path.mjs`
6049
+ // (「切到内置主题后装饰层保留」) — do not "fix" it by clearing the layer here.
3937
6050
  removeAllAmbientSeats()
3938
6051
  if (record) noteAmbientAttempt({ kind, column: true, band: '无装饰' })
3939
6052
  ambientReport = { found: true, kind: '(none)', note: '该主题无装饰' }
@@ -4403,6 +6516,59 @@ window.__ModuleLoader__.load({
4403
6516
  })
4404
6517
  }
4405
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
+
4406
6572
  /**
4407
6573
  * Report whether the accent token layer actually reached the theme snapshot.
4408
6574
  *
@@ -4486,12 +6652,14 @@ window.__ModuleLoader__.load({
4486
6652
  * @param props - composed slot props.
4487
6653
  * @returns the page element tree.
4488
6654
  */
4489
- function ThemeGalleryPage({ t, setTheme, useStore, usePanelInfo }) {
6655
+ function ThemeGalleryPage({ t, setTheme, pickScheme, useStore, usePanelInfo }) {
4490
6656
  const info = usePanelInfo((s) => s.activePanelId)
4491
6657
  const ids = useStore((s) => s.ids)
4492
6658
  const labels = useStore((s) => s.labels)
4493
6659
  const descriptions = useStore((s) => s.descriptions)
4494
6660
  const swatches = useStore((s) => s.swatches)
6661
+ const cardRows = useStore((s) => s.cardRows)
6662
+ const scheme = useStore((s) => s.scheme)
4495
6663
  const selected = useStore((s) => s.selected)
4496
6664
  const status = useStore((s) => s.status)
4497
6665
  // The layout keeps this page registered whatever the selection is, so it
@@ -4511,6 +6679,14 @@ window.__ModuleLoader__.load({
4511
6679
  // Detail lines are opt-in; a line that reports a problem is not detail.
4512
6680
  const scenery = sceneryLine(selected)
4513
6681
  const showScenery = scenery !== null && (debugEnabled() || sceneryLineIsWarning(scenery))
6682
+ // ── THE HEADER COUNT IS SKINS, NOT CARDS ─────────────────────────────────
6683
+ //
6684
+ // The built-in 浅色 / 深色 cards are not skins — they only switch the palette back to the
6685
+ // official appearance — so counting them here would advertise a number that disagrees
6686
+ // with the README and with npm's description ("6 skins"). The cards are named in the
6687
+ // same line instead, so nothing is hidden by leaving them out of the number.
6688
+ const builtInCards = ids.filter((id) => BUILT_IN_LABELS[id] !== undefined).length
6689
+ const skinCount = ids.length - builtInCards
4514
6690
  return jsxs('div', {
4515
6691
  className: 'tg-page',
4516
6692
  children: [
@@ -4519,7 +6695,7 @@ window.__ModuleLoader__.load({
4519
6695
  children: [
4520
6696
  jsx('span', { className: 'tg-title', children: t('title') }),
4521
6697
  jsx('span', { className: 'tg-hint', children: t('hint') }),
4522
- jsx('span', { className: 'tg-hint', children: t('count', { count: ids.length }) }),
6698
+ jsx('span', { className: 'tg-hint', children: t('count', { count: skinCount }) }),
4523
6699
  // What this copy is, so a reader can compare it with the version npm
4524
6700
  // publishes without digging through the profile.
4525
6701
  jsx('span', { className: 'tg-hint', children: `v${BUNDLED_VERSION}` }),
@@ -4532,9 +6708,22 @@ window.__ModuleLoader__.load({
4532
6708
  label: labels[id],
4533
6709
  description: descriptions[id],
4534
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,
4535
6716
  selected: id === selected,
4536
6717
  applied: t('applied'),
4537
- onSelect: setTheme,
6718
+ // The click is recorded BEFORE it is handed to the shell, because the theme
6719
+ // service publishes synchronously and the publisher re-applies the remembered
6720
+ // skin: a built-in card clicked after a skin used to be undone immediately.
6721
+ // `rememberCardChoice` is what lets 浅色 / 深色 win — see its documentation.
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) },
4538
6727
  t,
4539
6728
  }, id)),
4540
6729
  }),
@@ -4918,8 +7107,11 @@ window.__ModuleLoader__.load({
4918
7107
  * handler, so the safety has to live in the CONFIGURATION CONTRACT rather than in code.
4919
7108
  * The one way to break it is to hand-write a skin id into `preference` — which the
4920
7109
  * official service would then reject at boot. Never do that.
7110
+ *
7111
+ * The keys themselves live at MODULE scope ({@link SKIN_KEY} / {@link BUILT_IN_KEY}):
7112
+ * the card-click handler runs in the page component, far from this body, and it must
7113
+ * be able to write them before the click reaches the theme service.
4921
7114
  */
4922
- const SKIN_KEY = 'theme-gallery:last-skin'
4923
7115
 
4924
7116
  /**
4925
7117
  * The token used to prove a skin's palette reached the document.
@@ -4952,6 +7144,45 @@ window.__ModuleLoader__.load({
4952
7144
  }
4953
7145
  }
4954
7146
 
7147
+ /**
7148
+ * Record that a skin is the active theme — the restart-time memory.
7149
+ *
7150
+ * Written only once the service actually reports the skin, so the key never names a
7151
+ * skin that was merely requested and then lost. Clearing the built-in marker in the
7152
+ * same breath keeps the two keys from disagreeing about what the user last chose.
7153
+ * @param id - the active skin id.
7154
+ */
7155
+ function rememberActiveSkin(id) {
7156
+ try {
7157
+ if (window.localStorage.getItem(SKIN_KEY) !== id) window.localStorage.setItem(SKIN_KEY, id)
7158
+ if (window.localStorage.getItem(BUILT_IN_KEY) !== null) window.localStorage.removeItem(BUILT_IN_KEY)
7159
+ } catch (error) {
7160
+ console.error('[theme-gallery] could not remember the active skin:', error)
7161
+ }
7162
+ }
7163
+
7164
+ /**
7165
+ * The skin this plugin should be showing, or null when the user is entitled to a
7166
+ * built-in appearance.
7167
+ *
7168
+ * Three-way precedence, and all three arms are load-bearing:
7169
+ *
7170
+ * 1. a skin the user chose — restore it, across restarts (the original feature);
7171
+ * 2. a built-in the user chose IN THIS PANEL — null, so the restore does not fight it;
7172
+ * 3. nothing chosen yet — the gallery's own default ({@link DEFAULT_SKIN}), which is
7173
+ * what makes a fresh install land on 山青婷彩 instead of the built-in white.
7174
+ *
7175
+ * Arm 2 is why the built-in cards work at all: without it, arm 1 keeps winning and the
7176
+ * click is undone a moment later.
7177
+ * @returns the skin id, or null.
7178
+ */
7179
+ function wantedSkin() {
7180
+ const remembered = rememberedSkin()
7181
+ if (remembered !== null) return remembered
7182
+ if (builtInChoice() !== null) return null
7183
+ return DEFAULT_SKIN
7184
+ }
7185
+
4955
7186
  /** The skin whose palette layer is currently stacked, and how to remove it. */
4956
7187
  let stackedSkin
4957
7188
  let stackedSkinDispose
@@ -5221,17 +7452,20 @@ window.__ModuleLoader__.load({
5221
7452
  // the presenter subscribed leaves the service reporting the skin while the page still
5222
7453
  // shows the built-in palette.
5223
7454
  if (typeof activeId === 'string' && bundledTheme(activeId) !== undefined) {
5224
- if (window.localStorage.getItem(SKIN_KEY) !== activeId) {
5225
- window.localStorage.setItem(SKIN_KEY, activeId)
5226
- }
7455
+ rememberActiveSkin(activeId)
5227
7456
  ensureSkinPainted(activeId, activeId)
5228
7457
  return
5229
7458
  }
5230
7459
 
5231
- // The app is on a built-in theme even though a skin is remembered: the boot race lost
5232
- // the preference. Re-checked on every publish rather than once per session, because a
5233
- // single attempt can be swallowed by a presenter that is not listening yet.
5234
- const wanted = rememberedSkin()
7460
+ // The app is on a built-in theme even though one of our skins is wanted: either the
7461
+ // boot race lost the preference, or nothing has been chosen yet and the gallery's
7462
+ // default has still to be applied. Re-checked on every publish rather than once per
7463
+ // session, because a single attempt can be swallowed by a presenter that is not
7464
+ // listening yet.
7465
+ //
7466
+ // `wantedSkin()` returns null when the user deliberately picked a built-in card, and
7467
+ // that null is the whole fix for "点深色又被弹回皮肤" — see its documentation.
7468
+ const wanted = wantedSkin()
5235
7469
  if (wanted === null) return
5236
7470
  ensureSkinPainted(wanted, activeId)
5237
7471
  } catch (error) {
@@ -5300,10 +7534,19 @@ window.__ModuleLoader__.load({
5300
7534
  */
5301
7535
  function publish(snapshot) {
5302
7536
  revision += 1
5303
- const themes = [...snapshot.themes].filter((theme) => !OMITTED_IDS.has(theme.id))
5304
- storeActions.sync(themes, snapshot.preference, revision)
7537
+ // DISPLAY ORDER IS APPLIED HERE, TO THE PAGE'S COPY ONLY.
7538
+ //
7539
+ // The registry keeps the order things were registered in, so re-ranking a card can
7540
+ // never change which themes exist, what they contain, or the order they were
7541
+ // contributed — `cardsInDisplayOrder` is a view over the filtered list.
7542
+ const themes = cardsInDisplayOrder([...snapshot.themes].filter((theme) => !OMITTED_IDS.has(theme.id)))
7543
+ storeActions.sync(themes, appliedCardId(snapshot), revision)
7544
+ // 卡片上点亮哪一格,以及要不要把那一层配色叠上去 —— 两件事都读同一份
7545
+ // localStorage,所以"看到的选中态"与"屏幕上的颜色"不可能对不上。
7546
+ storeActions.markScheme(wantedScheme())
5305
7547
  syncSkin(snapshot)
5306
7548
  syncRememberedSkin(snapshot)
7549
+ syncScheme(snapshot)
5307
7550
  // Expose what the theme SERVICE believes, so the page can compare it with
5308
7551
  // what is actually in effect on the document. The presenter consumes this
5309
7552
  // same snapshot (`snapshot.active.tokens`) and writes it to `body`, so a
@@ -5326,6 +7569,74 @@ window.__ModuleLoader__.load({
5326
7569
  }
5327
7570
  }
5328
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
+
5329
7640
  /**
5330
7641
  * Resolve a bundled theme into the shape `register` actually consumes.
5331
7642
  *
@@ -5556,7 +7867,9 @@ window.__ModuleLoader__.load({
5556
7867
  try {
5557
7868
  const snapshot = ctx.theme.getTheme()
5558
7869
  const active = snapshot?.active?.id
5559
- const wanted = rememberedSkin()
7870
+ // The default skin counts here too: on a fresh install this window is what keeps
7871
+ // asking until 山青婷彩 is genuinely on the document, not merely requested.
7872
+ const wanted = wantedSkin()
5560
7873
  if (wanted === null) {
5561
7874
  done = true
5562
7875
  } else if (skinIsPainted(wanted)) {
@@ -5662,7 +7975,12 @@ window.__ModuleLoader__.load({
5662
7975
  // — so it is set BEFORE the publish that reads it.
5663
7976
  injectRan = true
5664
7977
  publish(ctx.theme.getTheme())
5665
- 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
+ }
5666
7984
  },
5667
7985
  }, ThemeGalleryPage)
5668
7986