dsh-theme-gallery 0.2.0 → 0.3.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
@@ -84,13 +84,152 @@ window.__ModuleLoader__.load({
84
84
  const BUILT_IN_LABELS = { light: '浅色', dark: '深色', system: '跟随系统' }
85
85
 
86
86
  /**
87
- * Built-in ids the gallery leaves to the official Appearance row.
87
+ * One-line copy for the built-in cards.
88
88
  *
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.
89
+ * The official themes declare no `description` of their own, so without this their
90
+ * cards would be a bare label. These two cards switch the PALETTE back to the system
91
+ * appearance — and they deliberately keep the previous skin's sidebar scenery on
92
+ * screen, which is the behaviour the second sentence advertises (see `syncAmbient`).
92
93
  */
93
- const OMITTED_IDS = new Set(['light'])
94
+ const BUILT_IN_DESCRIPTIONS = {
95
+ light: '系统默认浅色外观:与「设置 → 通用 → 外观」里的浅色是同一套配色。'
96
+ + '先选中主题皮肤,再切换浅色/深色 会有惊喜哦',
97
+ dark: '系统默认深色外观:与「设置 → 通用 → 外观」里的深色是同一套配色。'
98
+ + '先选中主题皮肤,再切换浅色/深色 会有惊喜哦',
99
+ system: '跟随系统外观设置(按桌面端的浅色/深色自动决定,因此不作为卡片展示)',
100
+ }
101
+
102
+ /**
103
+ * Built-in ids the gallery does not show as a card.
104
+ *
105
+ * `system` is a preference value rather than a look of its own — it resolves to
106
+ * `light` or `dark` depending on the desktop — so a card for it would duplicate one
107
+ * of the two cards above and change meaning with the OS setting. `light` and `dark`
108
+ * are BOTH shown now: together they are the way back out of every skin.
109
+ */
110
+ const OMITTED_IDS = new Set(['system'])
111
+
112
+ /**
113
+ * Display order of the gallery's cards — larger first.
114
+ *
115
+ * These are the user's own ranking numbers, kept as one table so the order is data
116
+ * rather than an implicit consequence of some other list. The REGISTRY order (and
117
+ * with it the contribution order in `BUNDLED_THEMES`) is deliberately untouched:
118
+ * this table is applied to the copy the page renders, and to nothing else.
119
+ *
120
+ * A theme with no entry sorts after every ranked one and keeps its registry order
121
+ * among the unranked (the sort is stable), so a theme contributed by another plugin
122
+ * still appears on the page instead of silently vanishing.
123
+ */
124
+ const CARD_ORDER = {
125
+ light: 99,
126
+ dark: 98,
127
+ 'ying-mu-cai-yun': 96,
128
+ 'shan-qing-ting-cai': 95,
129
+ 'pei-an-jie-xin': 91,
130
+ 'meng-hai-you-yu': 80,
131
+ 'hu-po-mao-mi': 76,
132
+ 'hu-zi-a-huang': 75,
133
+ }
134
+
135
+ /**
136
+ * The skin this plugin puts in effect when the user has never chosen anything.
137
+ *
138
+ * Requested behaviour: installing/enabling the plugin lands on 山青婷彩 instead of the
139
+ * built-in white theme. Applied by the ordinary restore path, so it obeys the same
140
+ * global write budget and paint confirmation as a remembered skin — and it stops the
141
+ * moment the user makes any choice of their own, because that choice is recorded.
142
+ */
143
+ const DEFAULT_SKIN = 'shan-qing-ting-cai'
144
+
145
+ /**
146
+ * The two keys this plugin owns in `localStorage`.
147
+ *
148
+ * Never `ui-theme.preference`: that field is read while the boot graph is still
149
+ * assembling, and a skin id in it makes `buildSnapshot()` throw `theme registry lost`
150
+ * and refuse to start. `localStorage` is private to this plugin, so a disabled or
151
+ * uninstalled plugin simply stops being consulted — that is the fallback.
152
+ *
153
+ * They live at module scope because the card-click handler runs in the page component,
154
+ * far from `mountGallery`, and it must be able to write them BEFORE the click reaches
155
+ * the theme service (which publishes synchronously).
156
+ */
157
+ const SKIN_KEY = 'theme-gallery:last-skin'
158
+ const BUILT_IN_KEY = 'theme-gallery:built-in-choice'
159
+
160
+ /**
161
+ * A theme's card rank; unranked themes sort last.
162
+ * @param id - the theme id.
163
+ * @returns its rank, or -1 when the table has no entry for it.
164
+ */
165
+ function cardRank(id) {
166
+ const rank = CARD_ORDER[id]
167
+ return typeof rank === 'number' ? rank : -1
168
+ }
169
+
170
+ /**
171
+ * Order a card list for display: highest rank first.
172
+ *
173
+ * Applied to the copy the page renders, never to the array the service is given, so a
174
+ * theme's registration and its card position are independent concerns (and a reorder
175
+ * can never change which themes exist or what they contain).
176
+ * @param themes - theme definitions, in registry order.
177
+ * @returns a new array in display order.
178
+ */
179
+ function cardsInDisplayOrder(themes) {
180
+ return [...themes].sort((a, b) => cardRank(b.id) - cardRank(a.id))
181
+ }
182
+
183
+ /**
184
+ * Record the user's card choice BEFORE it reaches the theme service.
185
+ *
186
+ * ── WHY THE ORDER IS LOAD-BEARING ───────────────────────────────────────────
187
+ *
188
+ * `ctx.theme.setTheme()` publishes synchronously, and the publisher re-applies the
189
+ * remembered skin whenever the active theme is not one of ours. So a built-in card
190
+ * clicked after any skin had been chosen used to be undone a microtask later: the click
191
+ * landed, the restore read `SKIN_KEY`, and the skin came straight back. On screen the
192
+ * 深色 card simply did nothing.
193
+ *
194
+ * Removing `SKIN_KEY` here, before the write, is what makes a built-in choice stick.
195
+ * `BUILT_IN_KEY` is written so the default skin does NOT fire on the next restart and
196
+ * undo the choice — see `wantedSkin`.
197
+ *
198
+ * Any theme this package does not provide counts as "not a skin": another plugin's
199
+ * theme must win as well, for exactly the same reason.
200
+ * @param id - the card's theme id.
201
+ */
202
+ function rememberCardChoice(id) {
203
+ try {
204
+ if (typeof window === 'undefined') return
205
+ const storage = window.localStorage
206
+ if (storage === undefined || storage === null) return
207
+ if (bundledTheme(id) === undefined) {
208
+ storage.removeItem(SKIN_KEY)
209
+ storage.setItem(BUILT_IN_KEY, String(id))
210
+ return
211
+ }
212
+ // A skin: it is recorded by the restore once it is genuinely active, so the stale
213
+ // built-in marker is cleared here rather than there.
214
+ storage.removeItem(BUILT_IN_KEY)
215
+ } catch (error) {
216
+ console.error('[theme-gallery] could not record the card choice:', error)
217
+ }
218
+ }
219
+
220
+ /**
221
+ * The built-in appearance the user picked in this panel, if any.
222
+ * @returns the id, or null when the user never picked one here.
223
+ */
224
+ function builtInChoice() {
225
+ try {
226
+ if (typeof window === 'undefined') return null
227
+ const value = window.localStorage?.getItem(BUILT_IN_KEY)
228
+ return typeof value === 'string' && value !== '' ? value : null
229
+ } catch {
230
+ return null
231
+ }
232
+ }
94
233
 
95
234
  /** Body attribute publishing the reading state; the injected rule reads it. */
96
235
  const READING_ATTRIBUTE = 'data-dsh-theme-reading'
@@ -118,7 +257,7 @@ window.__ModuleLoader__.load({
118
257
  * package.json, and the release workflow fails when re-running the embed step
119
258
  * changes a tracked file.
120
259
  */
121
- const BUNDLED_VERSION = '0.2.0'
260
+ const BUNDLED_VERSION = '0.3.0'
122
261
 
123
262
  /**
124
263
  * Themes this package contributes, inlined from lib/themes/*.json.
@@ -131,6 +270,582 @@ window.__ModuleLoader__.load({
131
270
  * Regenerate after editing those files: `node scripts/embed-themes.mjs`.
132
271
  */
133
272
  const BUNDLED_THEMES = [
273
+ {
274
+ "id": "hu-po-mao-mi",
275
+ "label": "琥珀猫咪",
276
+ "description": "琥珀暖光,双猫伴读 —— 原创宠物主题:两只虎斑短毛猫守着洒满阳光的窗台",
277
+ "colorScheme": "light",
278
+ "tokens": {
279
+ "--dsw-alias-bg-base": {
280
+ "light": "linear-gradient(to bottom,#FEFBF5 0%,#FBF2E3 55%,#F8EDD8 100%)",
281
+ "dark": "linear-gradient(to bottom,#FEFBF5 0%,#FBF2E3 55%,#F8EDD8 100%)"
282
+ },
283
+ "--dsw-alias-bg-layer-1": {
284
+ "light": "#FEFCF7",
285
+ "dark": "#FEFCF7"
286
+ },
287
+ "--dsw-alias-bg-layer-2": {
288
+ "light": "#FFFFFF",
289
+ "dark": "#FFFFFF"
290
+ },
291
+ "--dsw-alias-bg-layer-3": {
292
+ "light": "#FFFFFF",
293
+ "dark": "#FFFFFF"
294
+ },
295
+ "--dsw-alias-bg-overlay": {
296
+ "light": "#F3E5CD",
297
+ "dark": "#F3E5CD"
298
+ },
299
+ "--dsw-alias-bg-skeleton": {
300
+ "light": "#9C5F2414",
301
+ "dark": "#9C5F2414"
302
+ },
303
+ "--dsw-alias-bg-module-platform": {
304
+ "light": "#F3E5CD",
305
+ "dark": "#F3E5CD"
306
+ },
307
+ "--dsw-alias-bg-multi-select": {
308
+ "light": "#F3E5CD",
309
+ "dark": "#F3E5CD"
310
+ },
311
+ "--dsw-alias-border-l1": {
312
+ "light": "#9C5F240F",
313
+ "dark": "#9C5F240F"
314
+ },
315
+ "--dsw-alias-border-l2": {
316
+ "light": "#9C5F2421",
317
+ "dark": "#9C5F2421"
318
+ },
319
+ "--dsw-alias-border-l3": {
320
+ "light": "#9C5F242E",
321
+ "dark": "#9C5F242E"
322
+ },
323
+ "--dsw-alias-border-l4": {
324
+ "light": "#9C5F243D",
325
+ "dark": "#9C5F243D"
326
+ },
327
+ "--dsw-alias-brand-primary": {
328
+ "light": "#9C5F24",
329
+ "dark": "#9C5F24"
330
+ },
331
+ "--dsw-alias-brand-primary-invert": {
332
+ "light": "#FFFFFF",
333
+ "dark": "#FFFFFF"
334
+ },
335
+ "--dsw-alias-brand-text": {
336
+ "light": "#9C5F24",
337
+ "dark": "#9C5F24"
338
+ },
339
+ "--dsw-alias-label-primary": {
340
+ "light": "#453019",
341
+ "dark": "#453019"
342
+ },
343
+ "--dsw-alias-label-primary-bluish": {
344
+ "light": "#453019",
345
+ "dark": "#453019"
346
+ },
347
+ "--dsw-alias-label-secondary": {
348
+ "light": "#6A4E2F",
349
+ "dark": "#6A4E2F"
350
+ },
351
+ "--dsw-alias-label-tertiary": {
352
+ "light": "#8C6F4C",
353
+ "dark": "#8C6F4C"
354
+ },
355
+ "--dsw-alias-label-caption": {
356
+ "light": "#8C6F4C",
357
+ "dark": "#8C6F4C"
358
+ },
359
+ "--dsw-alias-label-dimmed": {
360
+ "light": "#B49C7F",
361
+ "dark": "#B49C7F"
362
+ },
363
+ "--dsw-alias-label-primary-dimmed": {
364
+ "light": "#6A4E2F",
365
+ "dark": "#6A4E2F"
366
+ },
367
+ "--dsw-alias-label-primary-foreground": {
368
+ "light": "#FFFFFF",
369
+ "dark": "#FFFFFF"
370
+ },
371
+ "--dsw-alias-label-primary-inverted": {
372
+ "light": "#FFFFFF",
373
+ "dark": "#FFFFFF"
374
+ },
375
+ "--dsw-alias-link": {
376
+ "light": "#CA6924",
377
+ "dark": "#CA6924"
378
+ },
379
+ "--dsw-alias-interactive-bg-hover": {
380
+ "light": "#9C5F2414",
381
+ "dark": "#9C5F2414"
382
+ },
383
+ "--dsw-alias-interactive-bg-active": {
384
+ "light": "#CA69242E",
385
+ "dark": "#CA69242E"
386
+ },
387
+ "--dsw-alias-interactive-bg-hover-solid": {
388
+ "light": "#F3E5CD",
389
+ "dark": "#F3E5CD"
390
+ },
391
+ "--dsw-alias-interactive-bg-hover-accent": {
392
+ "light": "#CA69243D",
393
+ "dark": "#CA69243D"
394
+ },
395
+ "--dsw-alias-button-primary-fill": {
396
+ "light": "#9C5F24",
397
+ "dark": "#9C5F24"
398
+ },
399
+ "--dsw-alias-button-primary-hover": {
400
+ "light": "#B0702F",
401
+ "dark": "#B0702F"
402
+ },
403
+ "--dsw-alias-button-primary-dimmed": {
404
+ "light": "#9C5F2447",
405
+ "dark": "#9C5F2447"
406
+ },
407
+ "--dsw-alias-button-ghost-active-fill": {
408
+ "light": "#CA692429",
409
+ "dark": "#CA692429"
410
+ },
411
+ "--dsw-alias-button-ghost-active-border": {
412
+ "light": "#CA6924",
413
+ "dark": "#CA6924"
414
+ },
415
+ "--dsw-alias-button-ghost-active-hover": {
416
+ "light": "#CA692447",
417
+ "dark": "#CA692447"
418
+ },
419
+ "--dsw-alias-button-info-fill": {
420
+ "light": "#B0702F",
421
+ "dark": "#B0702F"
422
+ },
423
+ "--dsw-alias-button-info-hover": {
424
+ "light": "#9C5F24",
425
+ "dark": "#9C5F24"
426
+ },
427
+ "--dsw-alias-button-elevated-fill": {
428
+ "light": "#FFFFFF",
429
+ "dark": "#FFFFFF"
430
+ },
431
+ "--dsw-alias-button-floating-fill": {
432
+ "light": "#FFFFFF",
433
+ "dark": "#FFFFFF"
434
+ },
435
+ "--dsw-alias-button-floating-hover": {
436
+ "light": "#FDF8F0",
437
+ "dark": "#FDF8F0"
438
+ },
439
+ "--dsw-alias-button-contrast-fill": {
440
+ "light": "#453019",
441
+ "dark": "#453019"
442
+ },
443
+ "--dsw-alias-markdown-code-block": {
444
+ "light": "#F7EEDF",
445
+ "dark": "#F7EEDF"
446
+ },
447
+ "--dsw-alias-markdown-code-block-banner": {
448
+ "light": "#F0E3CB",
449
+ "dark": "#F0E3CB"
450
+ },
451
+ "--dsw-alias-markdown-inline-code": {
452
+ "light": "#F3E9D6",
453
+ "dark": "#F3E9D6"
454
+ },
455
+ "--dsw-alias-markdown-citation": {
456
+ "light": "#F3E9D6",
457
+ "dark": "#F3E9D6"
458
+ },
459
+ "--dsw-alias-markdown-tag": {
460
+ "light": "#F3E9D6",
461
+ "dark": "#F3E9D6"
462
+ },
463
+ "--dsw-alias-markdown-placeholder": {
464
+ "light": "#F0E3CB",
465
+ "dark": "#F0E3CB"
466
+ },
467
+ "--dsw-alias-markdown-code-segment-selected": {
468
+ "light": "#CA692429",
469
+ "dark": "#CA692429"
470
+ },
471
+ "--dsw-alias-markdown-code-segment-unselected": {
472
+ "light": "#F7EEDF",
473
+ "dark": "#F7EEDF"
474
+ },
475
+ "--dsw-alias-scrollbar-bg-l1": {
476
+ "light": "#9C5F2429",
477
+ "dark": "#9C5F2429"
478
+ },
479
+ "--dsw-alias-scrollbar-bg-l2": {
480
+ "light": "#9C5F2438",
481
+ "dark": "#9C5F2438"
482
+ },
483
+ "--dsw-alias-scrollbar-hover-l1": {
484
+ "light": "#9C5F2452",
485
+ "dark": "#9C5F2452"
486
+ },
487
+ "--dsw-alias-scrollbar-hover-l2": {
488
+ "light": "#9C5F2466",
489
+ "dark": "#9C5F2466"
490
+ },
491
+ "--dsw-alias-tooltip-bg": {
492
+ "light": "#453019",
493
+ "dark": "#453019"
494
+ },
495
+ "--dsw-alias-toast-bg": {
496
+ "light": "#FFFFFF",
497
+ "dark": "#FFFFFF"
498
+ },
499
+ "--dsw-alias-state-error-primary": {
500
+ "light": "#C65B6A",
501
+ "dark": "#C65B6A"
502
+ },
503
+ "--dsw-alias-state-error-secondary": {
504
+ "light": "#DB5A6B",
505
+ "dark": "#DB5A6B"
506
+ },
507
+ "--dsw-alias-state-success-primary": {
508
+ "light": "#21A675",
509
+ "dark": "#21A675"
510
+ },
511
+ "--dsw-alias-state-success-secondary": {
512
+ "light": "#9C5F24",
513
+ "dark": "#9C5F24"
514
+ },
515
+ "--dsw-alias-state-success-tertiary": {
516
+ "light": "#CFBA9C",
517
+ "dark": "#CFBA9C"
518
+ },
519
+ "--dsw-alias-state-warn-primary": {
520
+ "light": "#B97F3A",
521
+ "dark": "#B97F3A"
522
+ },
523
+ "--dsw-alias-state-warn-secondary": {
524
+ "light": "#D9A45E",
525
+ "dark": "#D9A45E"
526
+ },
527
+ "--dsw-alias-state-warn-tertiary": {
528
+ "light": "#EFDCC0",
529
+ "dark": "#EFDCC0"
530
+ },
531
+ "--dsw-alias-state-warn-label": {
532
+ "light": "#8A5A22",
533
+ "dark": "#8A5A22"
534
+ },
535
+ "--dsw-alias-state-business-primary": {
536
+ "light": "#CA6924",
537
+ "dark": "#CA6924"
538
+ },
539
+ "--dsw-alias-state-business-tertiary": {
540
+ "light": "#E29C45",
541
+ "dark": "#E29C45"
542
+ },
543
+ "--dsw-specific-sidebar-fill": {
544
+ "light": "linear-gradient(to bottom,#FBF0DE 0%,#F5E3C6 20%,#EDD5AE 36%,#E3C593 50%,#D8B57D 62%,#CBA468 74%,#C69E5F 86%,#BB904F 100%)",
545
+ "dark": "linear-gradient(to bottom,#FBF0DE 0%,#F5E3C6 20%,#EDD5AE 36%,#E3C593 50%,#D8B57D 62%,#CBA468 74%,#C69E5F 86%,#BB904F 100%)"
546
+ }
547
+ },
548
+ "reading": {
549
+ "colorScheme": "light",
550
+ "bg": "#FFFFFF",
551
+ "alpha": 0.62,
552
+ "blur": 3,
553
+ "maxWidth": 640
554
+ },
555
+ "accent": "#CA6924",
556
+ "ambient": {
557
+ "kind": "humao",
558
+ "dust": 6
559
+ }
560
+ },
561
+ {
562
+ "id": "hu-zi-a-huang",
563
+ "label": "虎子阿黄",
564
+ "description": "麦香田园,黄犬相迎 —— 原创宠物主题:中黄田园犬「虎子阿黄」摇着尾巴守在田埂上",
565
+ "colorScheme": "light",
566
+ "tokens": {
567
+ "--dsw-alias-bg-base": {
568
+ "light": "linear-gradient(to bottom,#FEFDF6 0%,#FBF5E0 55%,#F7EFD0 100%)",
569
+ "dark": "linear-gradient(to bottom,#FEFDF6 0%,#FBF5E0 55%,#F7EFD0 100%)"
570
+ },
571
+ "--dsw-alias-bg-layer-1": {
572
+ "light": "#FEFCF4",
573
+ "dark": "#FEFCF4"
574
+ },
575
+ "--dsw-alias-bg-layer-2": {
576
+ "light": "#FFFFFF",
577
+ "dark": "#FFFFFF"
578
+ },
579
+ "--dsw-alias-bg-layer-3": {
580
+ "light": "#FFFFFF",
581
+ "dark": "#FFFFFF"
582
+ },
583
+ "--dsw-alias-bg-overlay": {
584
+ "light": "#F4EAD0",
585
+ "dark": "#F4EAD0"
586
+ },
587
+ "--dsw-alias-bg-skeleton": {
588
+ "light": "#896C3914",
589
+ "dark": "#896C3914"
590
+ },
591
+ "--dsw-alias-bg-module-platform": {
592
+ "light": "#F4EAD0",
593
+ "dark": "#F4EAD0"
594
+ },
595
+ "--dsw-alias-bg-multi-select": {
596
+ "light": "#F4EAD0",
597
+ "dark": "#F4EAD0"
598
+ },
599
+ "--dsw-alias-border-l1": {
600
+ "light": "#896C390F",
601
+ "dark": "#896C390F"
602
+ },
603
+ "--dsw-alias-border-l2": {
604
+ "light": "#896C3921",
605
+ "dark": "#896C3921"
606
+ },
607
+ "--dsw-alias-border-l3": {
608
+ "light": "#896C392E",
609
+ "dark": "#896C392E"
610
+ },
611
+ "--dsw-alias-border-l4": {
612
+ "light": "#896C393D",
613
+ "dark": "#896C393D"
614
+ },
615
+ "--dsw-alias-brand-primary": {
616
+ "light": "#896C39",
617
+ "dark": "#896C39"
618
+ },
619
+ "--dsw-alias-brand-primary-invert": {
620
+ "light": "#FFFFFF",
621
+ "dark": "#FFFFFF"
622
+ },
623
+ "--dsw-alias-brand-text": {
624
+ "light": "#896C39",
625
+ "dark": "#896C39"
626
+ },
627
+ "--dsw-alias-label-primary": {
628
+ "light": "#3B3018",
629
+ "dark": "#3B3018"
630
+ },
631
+ "--dsw-alias-label-primary-bluish": {
632
+ "light": "#3B3018",
633
+ "dark": "#3B3018"
634
+ },
635
+ "--dsw-alias-label-secondary": {
636
+ "light": "#5D4E2B",
637
+ "dark": "#5D4E2B"
638
+ },
639
+ "--dsw-alias-label-tertiary": {
640
+ "light": "#857451",
641
+ "dark": "#857451"
642
+ },
643
+ "--dsw-alias-label-caption": {
644
+ "light": "#857451",
645
+ "dark": "#857451"
646
+ },
647
+ "--dsw-alias-label-dimmed": {
648
+ "light": "#B2A47E",
649
+ "dark": "#B2A47E"
650
+ },
651
+ "--dsw-alias-label-primary-dimmed": {
652
+ "light": "#5D4E2B",
653
+ "dark": "#5D4E2B"
654
+ },
655
+ "--dsw-alias-label-primary-foreground": {
656
+ "light": "#FFFFFF",
657
+ "dark": "#FFFFFF"
658
+ },
659
+ "--dsw-alias-label-primary-inverted": {
660
+ "light": "#FFFFFF",
661
+ "dark": "#FFFFFF"
662
+ },
663
+ "--dsw-alias-link": {
664
+ "light": "#AE7000",
665
+ "dark": "#AE7000"
666
+ },
667
+ "--dsw-alias-interactive-bg-hover": {
668
+ "light": "#896C3914",
669
+ "dark": "#896C3914"
670
+ },
671
+ "--dsw-alias-interactive-bg-active": {
672
+ "light": "#F0C2392E",
673
+ "dark": "#F0C2392E"
674
+ },
675
+ "--dsw-alias-interactive-bg-hover-solid": {
676
+ "light": "#F4EAD0",
677
+ "dark": "#F4EAD0"
678
+ },
679
+ "--dsw-alias-interactive-bg-hover-accent": {
680
+ "light": "#F0C2393D",
681
+ "dark": "#F0C2393D"
682
+ },
683
+ "--dsw-alias-button-primary-fill": {
684
+ "light": "#896C39",
685
+ "dark": "#896C39"
686
+ },
687
+ "--dsw-alias-button-primary-hover": {
688
+ "light": "#A88462",
689
+ "dark": "#A88462"
690
+ },
691
+ "--dsw-alias-button-primary-dimmed": {
692
+ "light": "#896C3947",
693
+ "dark": "#896C3947"
694
+ },
695
+ "--dsw-alias-button-ghost-active-fill": {
696
+ "light": "#F0C23929",
697
+ "dark": "#F0C23929"
698
+ },
699
+ "--dsw-alias-button-ghost-active-border": {
700
+ "light": "#F0C239",
701
+ "dark": "#F0C239"
702
+ },
703
+ "--dsw-alias-button-ghost-active-hover": {
704
+ "light": "#F0C23947",
705
+ "dark": "#F0C23947"
706
+ },
707
+ "--dsw-alias-button-info-fill": {
708
+ "light": "#A88462",
709
+ "dark": "#A88462"
710
+ },
711
+ "--dsw-alias-button-info-hover": {
712
+ "light": "#896C39",
713
+ "dark": "#896C39"
714
+ },
715
+ "--dsw-alias-button-elevated-fill": {
716
+ "light": "#FFFFFF",
717
+ "dark": "#FFFFFF"
718
+ },
719
+ "--dsw-alias-button-floating-fill": {
720
+ "light": "#FFFFFF",
721
+ "dark": "#FFFFFF"
722
+ },
723
+ "--dsw-alias-button-floating-hover": {
724
+ "light": "#FCF8EC",
725
+ "dark": "#FCF8EC"
726
+ },
727
+ "--dsw-alias-button-contrast-fill": {
728
+ "light": "#3B3018",
729
+ "dark": "#3B3018"
730
+ },
731
+ "--dsw-alias-markdown-code-block": {
732
+ "light": "#F8F1DC",
733
+ "dark": "#F8F1DC"
734
+ },
735
+ "--dsw-alias-markdown-code-block-banner": {
736
+ "light": "#F1E7C6",
737
+ "dark": "#F1E7C6"
738
+ },
739
+ "--dsw-alias-markdown-inline-code": {
740
+ "light": "#F4EAD0",
741
+ "dark": "#F4EAD0"
742
+ },
743
+ "--dsw-alias-markdown-citation": {
744
+ "light": "#F4EAD0",
745
+ "dark": "#F4EAD0"
746
+ },
747
+ "--dsw-alias-markdown-tag": {
748
+ "light": "#F4EAD0",
749
+ "dark": "#F4EAD0"
750
+ },
751
+ "--dsw-alias-markdown-placeholder": {
752
+ "light": "#F1E7C6",
753
+ "dark": "#F1E7C6"
754
+ },
755
+ "--dsw-alias-markdown-code-segment-selected": {
756
+ "light": "#F0C23929",
757
+ "dark": "#F0C23929"
758
+ },
759
+ "--dsw-alias-markdown-code-segment-unselected": {
760
+ "light": "#F8F1DC",
761
+ "dark": "#F8F1DC"
762
+ },
763
+ "--dsw-alias-scrollbar-bg-l1": {
764
+ "light": "#896C3929",
765
+ "dark": "#896C3929"
766
+ },
767
+ "--dsw-alias-scrollbar-bg-l2": {
768
+ "light": "#896C3938",
769
+ "dark": "#896C3938"
770
+ },
771
+ "--dsw-alias-scrollbar-hover-l1": {
772
+ "light": "#896C3952",
773
+ "dark": "#896C3952"
774
+ },
775
+ "--dsw-alias-scrollbar-hover-l2": {
776
+ "light": "#896C3966",
777
+ "dark": "#896C3966"
778
+ },
779
+ "--dsw-alias-tooltip-bg": {
780
+ "light": "#3B3018",
781
+ "dark": "#3B3018"
782
+ },
783
+ "--dsw-alias-toast-bg": {
784
+ "light": "#FFFFFF",
785
+ "dark": "#FFFFFF"
786
+ },
787
+ "--dsw-alias-state-error-primary": {
788
+ "light": "#C65B6A",
789
+ "dark": "#C65B6A"
790
+ },
791
+ "--dsw-alias-state-error-secondary": {
792
+ "light": "#DB5A6B",
793
+ "dark": "#DB5A6B"
794
+ },
795
+ "--dsw-alias-state-success-primary": {
796
+ "light": "#21A675",
797
+ "dark": "#21A675"
798
+ },
799
+ "--dsw-alias-state-success-secondary": {
800
+ "light": "#896C39",
801
+ "dark": "#896C39"
802
+ },
803
+ "--dsw-alias-state-success-tertiary": {
804
+ "light": "#D1C69C",
805
+ "dark": "#D1C69C"
806
+ },
807
+ "--dsw-alias-state-warn-primary": {
808
+ "light": "#B97F3A",
809
+ "dark": "#B97F3A"
810
+ },
811
+ "--dsw-alias-state-warn-secondary": {
812
+ "light": "#D9A45E",
813
+ "dark": "#D9A45E"
814
+ },
815
+ "--dsw-alias-state-warn-tertiary": {
816
+ "light": "#EFDCC0",
817
+ "dark": "#EFDCC0"
818
+ },
819
+ "--dsw-alias-state-warn-label": {
820
+ "light": "#8A5A22",
821
+ "dark": "#8A5A22"
822
+ },
823
+ "--dsw-alias-state-business-primary": {
824
+ "light": "#F0C239",
825
+ "dark": "#F0C239"
826
+ },
827
+ "--dsw-alias-state-business-tertiary": {
828
+ "light": "#F5D46B",
829
+ "dark": "#F5D46B"
830
+ },
831
+ "--dsw-specific-sidebar-fill": {
832
+ "light": "linear-gradient(to bottom,#FAF3DC 0%,#F4EAC2 20%,#ECDFAB 36%,#E2D191 50%,#D6C078 62%,#C9AF63 74%,#C4A85F 86%,#B89B51 100%)",
833
+ "dark": "linear-gradient(to bottom,#FAF3DC 0%,#F4EAC2 20%,#ECDFAB 36%,#E2D191 50%,#D6C078 62%,#C9AF63 74%,#C4A85F 86%,#B89B51 100%)"
834
+ }
835
+ },
836
+ "reading": {
837
+ "colorScheme": "light",
838
+ "bg": "#FFFFFF",
839
+ "alpha": 0.62,
840
+ "blur": 3,
841
+ "maxWidth": 640
842
+ },
843
+ "accent": "#F0C239",
844
+ "ambient": {
845
+ "kind": "ahuang",
846
+ "dust": 5
847
+ }
848
+ },
134
849
  {
135
850
  "id": "meng-hai-you-yu",
136
851
  "label": "梦海游鱼",
@@ -1610,6 +2325,32 @@ window.__ModuleLoader__.load({
1610
2325
  '@keyframes dsh-amb-gc-wing{0%{transform:rotate(0deg)}40%{transform:rotate(-3.5deg)}60%{transform:rotate(2.5deg)}100%{transform:rotate(0deg)}}',
1611
2326
  '@keyframes dsh-amb-gc-dew{0%{opacity:.3;transform:scale(1)}100%{opacity:1;transform:scale(1.5)}}',
1612
2327
 
2328
+ /* ── 琥珀猫咪 / 虎子阿黄(原创宠物场景)──────────────────────────────
2329
+ Like the five ported scenes above: element geometry, colour and
2330
+ per-element timing are INLINE in the scene builders, so only the
2331
+ @keyframes live here — each prefixed (hm / hz) so they cannot collide,
2332
+ and each defined exactly once (a duplicate would silently win). Every
2333
+ animated vertical term stays at or below the resting baseline, because
2334
+ the scene box clips at its own top edge. */
2335
+ '@keyframes dsh-amb-hm-glow{from{opacity:.55}to{opacity:.85}}',
2336
+ '@keyframes dsh-amb-hm-dust{0%{transform:translate(0,0);opacity:0}12%{opacity:.9}'
2337
+ + '60%{transform:translate(-.5em,-3.2em);opacity:.45}100%{transform:translate(.4em,-4.6em);opacity:0}}',
2338
+ '@keyframes dsh-amb-hm-tail{from{transform:rotate(-3deg)}to{transform:rotate(5deg)}}',
2339
+ '@keyframes dsh-amb-hm-breathe{0%,100%{transform:scaleY(1)}50%{transform:scaleY(1.03)}}',
2340
+ '@keyframes dsh-amb-hm-ear{0%,86%,100%{transform:rotate(0)}90%{transform:rotate(-9deg)}94%{transform:rotate(4deg)}}',
2341
+ '@keyframes dsh-amb-hm-zzz{0%{transform:translate(0,0);opacity:0}18%{opacity:.85}'
2342
+ + '100%{transform:translate(1.2em,-2.6em);opacity:0}}',
2343
+ '@keyframes dsh-amb-hz-glow{from{opacity:.45}to{opacity:.75}}',
2344
+ /* The fluff crosses the whole band and dies at the far edge; its vertical
2345
+ wobble stays within ±1.2em so a low-seeded puff never reaches the clip. */
2346
+ '@keyframes dsh-amb-hz-fluff{0%{transform:translate(0,0);opacity:0}7%{opacity:.95}'
2347
+ + '30%{transform:translate(8em,-1.2em)}55%{transform:translate(14em,.7em)}'
2348
+ + '80%{transform:translate(20em,-.9em);opacity:.75}96%{opacity:.4}'
2349
+ + '100%{transform:translate(26em,0);opacity:0}}',
2350
+ '@keyframes dsh-amb-hz-wag{from{transform:rotate(-9deg)}to{transform:rotate(11deg)}}',
2351
+ '@keyframes dsh-amb-hz-grass{from{transform:rotate(-2.2deg)}to{transform:rotate(2.6deg)}}',
2352
+ '@keyframes dsh-amb-hz-tilt{0%,40%,100%{transform:rotate(0)}55%{transform:rotate(2.4deg)}75%{transform:rotate(-1.4deg)}}',
2353
+
1613
2354
  /* Respect a user who has asked the system for less motion: the scenery
1614
2355
  stays, the movement does not. */
1615
2356
  '@media (prefers-reduced-motion:reduce){#dsh-theme-ambient *{animation:none!important}}',
@@ -1619,7 +2360,7 @@ window.__ModuleLoader__.load({
1619
2360
  const zh = {
1620
2361
  title: '主题皮肤',
1621
2362
  hint: '点一下即切换,选中会记入设置',
1622
- count: '{count} 个可选主题',
2363
+ count: '{count} 款皮肤(另有内置浅色/深色两张卡)',
1623
2364
  current: '当前',
1624
2365
  applied: '已应用',
1625
2366
  empty: '正在读取官方主题注册表…',
@@ -1631,7 +2372,7 @@ window.__ModuleLoader__.load({
1631
2372
  const en = {
1632
2373
  title: 'Theme skins',
1633
2374
  hint: 'Click to switch; the choice is remembered',
1634
- count: '{count} themes available',
2375
+ count: '{count} skins (+ built-in light/dark cards)',
1635
2376
  current: 'Current',
1636
2377
  applied: 'Applied',
1637
2378
  empty: 'Reading the theme registry…',
@@ -1711,10 +2452,11 @@ window.__ModuleLoader__.load({
1711
2452
  // localizes them from its own dictionaries — so name them here
1712
2453
  // rather than showing a bare `light` / `dark` id.
1713
2454
  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 ? '' : '')
2455
+ // A built-in theme derives its colours from the base palette rather than
2456
+ // declaring tokens, so its card gets a plain line — and it carries no
2457
+ // description of its own, so the copy for the built-in cards is the
2458
+ // gallery's own (`BUILT_IN_DESCRIPTIONS`).
2459
+ descriptions[theme.id] = theme.description || BUILT_IN_DESCRIPTIONS[theme.id] || ''
1718
2460
  // A card's colour strip comes from the theme's own tokens, so a
1719
2461
  // theme authored anywhere shows its identity without extra metadata.
1720
2462
  const tokens = theme.tokens || {}
@@ -3279,6 +4021,328 @@ window.__ModuleLoader__.load({
3279
4021
  + '</div>'
3280
4022
  }
3281
4023
 
4024
+ /**
4025
+ * One sun-warmed haze for the two ORIGINAL pet scenes (琥珀猫咪 / 虎子阿黄).
4026
+ *
4027
+ * Both scenes open with the same soft light pool at the band's top, built the way
4028
+ * dream's corner glow was taught to behave: it starts AT the edge, is dimmer than
4029
+ * the source skies, and is masked to zero at its top so the scene box's
4030
+ * `overflow:hidden` clip never cuts a hard line against the un-lit sidebar above.
4031
+ * @param tint - the rgba() fill of the haze, in the theme's own family.
4032
+ * @param keyframe - the pulse animation, `dsh-amb-hm-glow` or `dsh-amb-hz-glow`.
4033
+ * @returns the haze markup.
4034
+ */
4035
+ function petHazeMarkup(tint, keyframe) {
4036
+ return open('div', {
4037
+ class: 'pet-haze',
4038
+ style: `position:absolute;top:0;left:-20%;width:130%;height:52%;z-index:2;`
4039
+ + `background:radial-gradient(ellipse at 30% 0%,${tint} 0%,rgba(255,236,200,0) 64%);`
4040
+ + 'filter:blur(10px);'
4041
+ + '-webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 42%);'
4042
+ + 'mask-image:linear-gradient(to bottom,transparent 0,#000 42%);'
4043
+ + `animation:${keyframe} 9s ease-in-out infinite alternate`,
4044
+ }) + '</div>'
4045
+ }
4046
+
4047
+ /**
4048
+ * The 琥珀猫咪 scene: a warm haze, a sunlit sill, TWO tabby shorthairs —
4049
+ * one sitting with a slow tail sway and the odd ear twitch, one curled asleep,
4050
+ * breathing, with tiny z's drifting off it — and amber dust motes floating
4051
+ * through the light.
4052
+ *
4053
+ * This is the first ORIGINAL scene in the pack (not ported from a source
4054
+ * system), drawn in the pack's own idiom: inline geometry in % / em, flat
4055
+ * fills from one warm amber family, the count knob seeding the dust, and
4056
+ * every animated transform bounded below the band's top edge — the sleeping
4057
+ * cat breathes from its base and the z's fade out well before they could
4058
+ * reach the clip.
4059
+ * @param dust - how many dust motes to seed.
4060
+ * @returns the scene markup.
4061
+ */
4062
+ function humaoAmbientScene(dust) {
4063
+ const count = Math.max(0, Math.min(20, dust ?? 6))
4064
+ let dustNodes = ''
4065
+ for (let n = 1; n <= count; n += 1) {
4066
+ const size = 2.5 + ((n * 5) % 4)
4067
+ dustNodes += open('div', {
4068
+ class: 'hm-dust',
4069
+ style: 'position:absolute;border-radius:50%;'
4070
+ + `left:${((n * 19) % 52) + 22}%;top:${((n * 13) % 24) + 36}%;`
4071
+ + `width:${size}px;height:${size}px;`
4072
+ + 'background:radial-gradient(circle,rgba(255,236,200,.95) 0%,rgba(255,214,140,0) 100%);'
4073
+ + 'box-shadow:0 0 6px rgba(255,214,140,.75);'
4074
+ + `animation:dsh-amb-hm-dust ${9 + ((n * 7) % 6)}s linear infinite;`
4075
+ + `animation-delay:${-(n * 2.3)}s`,
4076
+ }) + '</div>'
4077
+ }
4078
+
4079
+ // 猫 A —— 坐姿虎斑,面朝左(第一版形态:地面绕尾、橄榄形上身、后侧三道
4080
+ // 弧纹)。连接方式按用户指定:**不垫任何脖子形状**,头部整组(头圆 + 双耳
4081
+ // + 五官 + 胡须 + 额头纹)作为整体下移 28、右移 2,头圆下缘直接压进身体
4082
+ // 上缘约 7.5 个单位(肩点 (52,52) 距头心 14.1 < r15,也埋进头圆),同色
4083
+ // 填充使接缝不可见;下巴与胸口之间留下的小凹角正是猫下巴该有的位置。
4084
+ // 奶油色胸部椭圆按用户要求落在 (39,76)。
4085
+ const catSit = open('svg', {
4086
+ viewBox: '0 0 100 118',
4087
+ style: 'display:block;width:100%;height:auto;overflow:visible',
4088
+ })
4089
+ + open('g', {
4090
+ class: 'hm-tail',
4091
+ style: 'transform-box:fill-box;transform-origin:0% 100%;'
4092
+ + 'animation:dsh-amb-hm-tail 5.5s ease-in-out infinite alternate',
4093
+ })
4094
+ + open('path', {
4095
+ d: 'M62 112 C82 115 90 103 85 90 C82 82 74 80 70 84',
4096
+ fill: 'none', stroke: '#C98B4B', 'stroke-width': '7', 'stroke-linecap': 'round',
4097
+ }) + '</path>'
4098
+ + open('path', {
4099
+ d: 'M86 92 C83 84 76 81 71 84',
4100
+ fill: 'none', stroke: '#8A5A2B', 'stroke-width': '7', 'stroke-linecap': 'round',
4101
+ }) + '</path>'
4102
+ + '</g>'
4103
+ + open('path', {
4104
+ 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',
4105
+ fill: '#D9A05B',
4106
+ }) + '</path>'
4107
+ + open('ellipse', { cx: '39', cy: '76', rx: '10', ry: '19', fill: '#F0D6A6' }) + '</ellipse>'
4108
+ + open('rect', { x: '32', y: '88', width: '6', height: '28', rx: '3', fill: '#E5BC7E', stroke: '#C98B4B', 'stroke-width': '1' }) + '</rect>'
4109
+ + open('rect', { x: '42', y: '88', width: '6', height: '28', rx: '3', fill: '#E5BC7E', stroke: '#C98B4B', 'stroke-width': '1' }) + '</rect>'
4110
+ + 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>'
4111
+ + 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>'
4112
+ + 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>'
4113
+ + open('path', { d: 'M34 40 L34 46 M38 39 L38 45 M30 41 L30 47', stroke: '#8A5A2B', 'stroke-width': '2', 'stroke-linecap': 'round' }) + '</path>'
4114
+ + open('path', { d: 'M26 46 L23 31 L35 39 Z', fill: '#D9A05B' }) + '</path>'
4115
+ + open('path', { d: 'M27.5 43 L26 35 L33 40 Z', fill: '#C97B6B', opacity: '0.75' }) + '</path>'
4116
+ + open('g', {
4117
+ class: 'hm-ear',
4118
+ style: 'transform-box:fill-box;transform-origin:0% 100%;'
4119
+ + 'animation:dsh-amb-hm-ear 7s ease-in-out infinite',
4120
+ })
4121
+ + open('path', { d: 'M44 42 L52 30 L50 44 Z', fill: '#D9A05B' }) + '</path>'
4122
+ + open('path', { d: 'M46 40.5 L50.5 33.5 L48.5 41.5 Z', fill: '#C97B6B', opacity: '0.75' }) + '</path>'
4123
+ + '</g>'
4124
+ + open('circle', { cx: '38', cy: '54', r: '15', fill: '#D9A05B' }) + '</circle>'
4125
+ + open('circle', { cx: '32', cy: '52', r: '2.2', fill: '#3B2A16' }) + '</circle>'
4126
+ + open('circle', { cx: '44', cy: '52', r: '2.2', fill: '#3B2A16' }) + '</circle>'
4127
+ + open('circle', { cx: '32.7', cy: '51.2', r: '0.7', fill: '#FFFFFF' }) + '</circle>'
4128
+ + open('circle', { cx: '44.7', cy: '51.2', r: '0.7', fill: '#FFFFFF' }) + '</circle>'
4129
+ + open('path', { d: 'M36 58 L40 58 L38 61 Z', fill: '#C97B6B' }) + '</path>'
4130
+ + 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>'
4131
+ + 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>'
4132
+ + '</svg>'
4133
+
4134
+ // 猫 B —— 蜷成一团酣睡:圆头明显叠在身子右前侧(同色相接),折耳 +
4135
+ // 闭眼 + 鼻尖胡须 + 身前露出一只奶油色前爪,尾巴沿身前绕一圈、
4136
+ // 深色尾尖收在左侧;整体随呼吸微微起伏,头顶冒出两枚会消散的小 z。
4137
+ const catCurled = open('svg', {
4138
+ viewBox: '0 0 112 66',
4139
+ style: 'display:block;width:100%;height:auto;overflow:visible',
4140
+ })
4141
+ + open('g', {
4142
+ class: 'hm-breathe',
4143
+ style: 'transform-box:fill-box;transform-origin:50% 100%;'
4144
+ + 'animation:dsh-amb-hm-breathe 3.8s ease-in-out infinite',
4145
+ })
4146
+ + open('path', {
4147
+ d: 'M6 62 C6 44 24 28 52 27 C80 26 100 40 99 53 C98 60 92 62 84 62 Z',
4148
+ fill: '#D9A05B',
4149
+ }) + '</path>'
4150
+ + 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>'
4151
+ + 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>'
4152
+ + 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>'
4153
+ + open('path', { d: 'M6 58 C26 66 62 67 92 59', fill: 'none', stroke: '#C98B4B', 'stroke-width': '6', 'stroke-linecap': 'round' }) + '</path>'
4154
+ + open('path', { d: 'M6 58 C12 61 18 63 24 64', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '6', 'stroke-linecap': 'round' }) + '</path>'
4155
+ + open('circle', { cx: '80', cy: '44', r: '15', fill: '#D9A05B' }) + '</circle>'
4156
+ + open('path', { d: 'M70 33 L65 23 L75.5 28.5 Z', fill: '#D9A05B' }) + '</path>'
4157
+ + open('path', { d: 'M88 31 L94 22 L95.5 32 Z', fill: '#D9A05B' }) + '</path>'
4158
+ + open('path', { d: 'M70.5 30.5 L68 25 L73.5 28 Z', fill: '#C97B6B', opacity: '0.75' }) + '</path>'
4159
+ + open('path', { d: 'M88.5 29.5 L92.5 24 L93.5 30.5 Z', fill: '#C97B6B', opacity: '0.75' }) + '</path>'
4160
+ + open('path', { d: 'M72 45 Q75 48 78 45', fill: 'none', stroke: '#8A5A2B', 'stroke-width': '1.4', 'stroke-linecap': 'round' }) + '</path>'
4161
+ + open('path', { d: 'M65.5 46.5 L69 46.5 L67.25 49.5 Z', fill: '#C97B6B' }) + '</path>'
4162
+ + 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>'
4163
+ + open('ellipse', { cx: '60', cy: '60', rx: '7', ry: '3.5', fill: '#F0D6A6', stroke: '#C98B4B', 'stroke-width': '1' }) + '</ellipse>'
4164
+ + '</g>'
4165
+ + '</svg>'
4166
+ + open('span', {
4167
+ class: 'hm-z hm-z-1',
4168
+ style: 'position:absolute;right:12%;top:-12%;font-size:.85em;font-style:italic;'
4169
+ + "font-family:Georgia,'Times New Roman',serif;color:#A97F4C;"
4170
+ + 'animation:dsh-amb-hm-zzz 5s ease-in-out infinite',
4171
+ }) + 'z</span>'
4172
+ + open('span', {
4173
+ class: 'hm-z hm-z-2',
4174
+ style: 'position:absolute;right:5%;top:-22%;font-size:.65em;font-style:italic;'
4175
+ + "font-family:Georgia,'Times New Roman',serif;color:#B98F55;"
4176
+ + 'animation:dsh-amb-hm-zzz 5s ease-in-out infinite;animation-delay:-2.5s',
4177
+ }) + 'z</span>'
4178
+
4179
+ return open('div', { class: 'hm', style: 'position:absolute;inset:0;display:block' })
4180
+ + petHazeMarkup('rgba(255,214,140,.5)', 'dsh-amb-hm-glow')
4181
+ + open('div', {
4182
+ class: 'hm-sill',
4183
+ style: 'position:absolute;left:0;right:0;bottom:0;height:12%;z-index:4;'
4184
+ + 'background:linear-gradient(to bottom,rgba(154,95,36,0) 0%,rgba(154,95,36,.16) 40%,rgba(138,84,30,.3) 100%)',
4185
+ })
4186
+ + open('div', {
4187
+ class: 'hm-sill-line',
4188
+ style: 'position:absolute;top:0;left:0;right:0;height:1.2px;opacity:.55;'
4189
+ + 'background:linear-gradient(90deg,transparent,rgba(255,244,220,.9),transparent)',
4190
+ }) + '</div>'
4191
+ + '</div>'
4192
+ + open('div', {
4193
+ class: 'hm-cat-sit',
4194
+ style: 'position:absolute;left:7%;bottom:9%;width:7.5em;z-index:6',
4195
+ }) + catSit + '</div>'
4196
+ + open('div', {
4197
+ class: 'hm-cat-curled',
4198
+ style: 'position:absolute;right:6%;bottom:7%;width:8.4em;z-index:6',
4199
+ }) + catCurled + '</div>'
4200
+ + dustNodes
4201
+ + '</div>'
4202
+ }
4203
+
4204
+ /**
4205
+ * The 虎子阿黄 scene: a golden haze, a field-path bank, the yellow Chinese
4206
+ * pastoral dog himself — upright ears, a sickle tail wagging at speed, an
4207
+ * occasional curious head tilt, a straw collar with a bell — with dry
4208
+ * grass tufts, a ball in the theme's own 缃色, and dandelion fluff drifting
4209
+ * across on the wind.
4210
+ *
4211
+ * The second ORIGINAL scene in the pack, same idiom as the cat: inline
4212
+ * % / em geometry, one warm wheat-gold family, and every animated transform
4213
+ * bounded below the band's top edge — the fluff fades out at the far edge
4214
+ * instead of clipping.
4215
+ * @param dust - how many dandelion-fluff puffs to seed.
4216
+ * @returns the scene markup.
4217
+ */
4218
+ function ahuangAmbientScene(dust) {
4219
+ const count = Math.max(0, Math.min(20, dust ?? 5))
4220
+ let fluffNodes = ''
4221
+ for (let n = 1; n <= count; n += 1) {
4222
+ const size = 4 + ((n * 3) % 3)
4223
+ fluffNodes += open('div', {
4224
+ class: 'hz-fluff',
4225
+ style: 'position:absolute;border-radius:50%;'
4226
+ + `left:-6%;top:${((n * 11) % 30) + 18}%;`
4227
+ + `width:${size}px;height:${size}px;`
4228
+ + 'background:radial-gradient(circle,#FFFDF4 0%,rgba(255,246,214,.85) 55%,rgba(255,246,214,0) 100%);'
4229
+ + 'box-shadow:0 0 5px rgba(255,244,200,.8);'
4230
+ + `animation:dsh-amb-hz-fluff ${15 + ((n * 7) % 8)}s linear infinite;`
4231
+ + `animation-delay:${-(n * 2.6)}s`,
4232
+ }) + '</div>'
4233
+ }
4234
+
4235
+ // 虎子阿黄 —— 坐姿面朝左。头与躯干同样按「必须重叠」构造:头部圆下缘
4236
+ // 压进胸腔椭圆上缘 3 个单位,项圈画在重叠带上把脖子束出来;立耳、
4237
+ // 镰刀尾(尾根埋进后臀圆里)快速摇摆、偶尔歪头,吐着舌头等主人。
4238
+ const dog = open('svg', {
4239
+ viewBox: '0 0 112 122',
4240
+ style: 'display:block;width:100%;height:auto;overflow:visible',
4241
+ })
4242
+ + open('g', {
4243
+ class: 'hz-tail',
4244
+ style: 'transform-box:fill-box;transform-origin:0% 100%;'
4245
+ + 'animation:dsh-amb-hz-wag .95s ease-in-out infinite alternate',
4246
+ })
4247
+ + open('path', {
4248
+ d: 'M76 70 C92 66 100 50 92 36 C88 30 82 29 78 32',
4249
+ fill: 'none', stroke: '#D8A44E', 'stroke-width': '8', 'stroke-linecap': 'round',
4250
+ }) + '</path>'
4251
+ + open('path', {
4252
+ d: 'M93 37 C90 32 85 30 79 32',
4253
+ fill: 'none', stroke: '#B97F3A', 'stroke-width': '8', 'stroke-linecap': 'round',
4254
+ }) + '</path>'
4255
+ + '</g>'
4256
+ + open('circle', { cx: '72', cy: '94', r: '26', fill: '#E8B45A' }) + '</circle>'
4257
+ + open('ellipse', { cx: '44', cy: '93', rx: '18', ry: '27', fill: '#E8B45A' }) + '</ellipse>'
4258
+ + open('ellipse', { cx: '56', cy: '117', rx: '13', ry: '5', fill: '#E8B45A' }) + '</ellipse>'
4259
+ + open('ellipse', { cx: '42', cy: '97', rx: '10', ry: '19', fill: '#F6E3B8' }) + '</ellipse>'
4260
+ + open('rect', { x: '32', y: '92', width: '7.5', height: '26', rx: '3.7', fill: '#E8B45A', stroke: '#C98F35', 'stroke-width': '1' }) + '</rect>'
4261
+ + open('rect', { x: '44', y: '92', width: '7.5', height: '26', rx: '3.7', fill: '#E8B45A', stroke: '#C98F35', 'stroke-width': '1' }) + '</rect>'
4262
+ + open('ellipse', { cx: '35.7', cy: '117', rx: '6', ry: '3.2', fill: '#F6E3B8' }) + '</ellipse>'
4263
+ + open('ellipse', { cx: '47.7', cy: '117', rx: '6', ry: '3.2', fill: '#F6E3B8' }) + '</ellipse>'
4264
+ + open('path', { d: 'M31 68 C38 74 50 74 56 66', fill: 'none', stroke: '#896C39', 'stroke-width': '5', 'stroke-linecap': 'round' }) + '</path>'
4265
+ + open('circle', { cx: '33', cy: '72.5', r: '2.6', fill: '#F0C239', stroke: '#B98A20', 'stroke-width': '0.8' }) + '</circle>'
4266
+ + open('path', { d: 'M30.6 72.5 L35.4 72.5', stroke: '#B98A20', 'stroke-width': '0.8' }) + '</path>'
4267
+ + open('g', {
4268
+ class: 'hz-head',
4269
+ style: 'transform-box:fill-box;transform-origin:60% 88%;'
4270
+ + 'animation:dsh-amb-hz-tilt 7s ease-in-out infinite',
4271
+ })
4272
+ + open('path', { d: 'M28 40 C24 30 23 23 26 18 C30 21 34 28 35.5 36 Z', fill: '#D8A44E' }) + '</path>'
4273
+ + open('path', { d: 'M48 38 C50 28 52 21 56 18 C58 23 56 30 52.5 38.5 Z', fill: '#D8A44E' }) + '</path>'
4274
+ + 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>'
4275
+ + 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>'
4276
+ + open('circle', { cx: '42', cy: '51', r: '18', fill: '#E8B45A' }) + '</circle>'
4277
+ + open('ellipse', { cx: '27', cy: '57', rx: '11', ry: '8.5', fill: '#F6E3B8' }) + '</ellipse>'
4278
+ + open('ellipse', { cx: '19', cy: '53.5', rx: '3', ry: '2.4', fill: '#4A331C' }) + '</ellipse>'
4279
+ + open('circle', { cx: '44', cy: '47', r: '2.4', fill: '#3B2A16' }) + '</circle>'
4280
+ + open('circle', { cx: '44.8', cy: '46.2', r: '0.8', fill: '#FFFFFF' }) + '</circle>'
4281
+ + 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>'
4282
+ + 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>'
4283
+ + 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>'
4284
+ + '</g>'
4285
+ + '</svg>'
4286
+
4287
+ const grassTuft = (left, bottom, width, zIndex) => open('div', {
4288
+ class: 'hz-grass',
4289
+ style: `position:absolute;${left};bottom:${bottom};width:${width};z-index:${zIndex}`,
4290
+ })
4291
+ + open('svg', {
4292
+ viewBox: '0 0 60 30',
4293
+ style: 'display:block;width:100%;height:auto;overflow:visible',
4294
+ })
4295
+ + open('g', {
4296
+ class: 'hz-blades',
4297
+ style: 'transform-box:fill-box;transform-origin:50% 100%;'
4298
+ + 'animation:dsh-amb-hz-grass 5.2s ease-in-out infinite alternate',
4299
+ })
4300
+ + open('path', { d: 'M6 30 C4 22 6 14 10 8', fill: 'none', stroke: '#C2A24E', 'stroke-width': '2.6', 'stroke-linecap': 'round' }) + '</path>'
4301
+ + open('path', { d: 'M14 30 C13 20 16 12 20 6', fill: 'none', stroke: '#B08F3E', 'stroke-width': '2.4', 'stroke-linecap': 'round' }) + '</path>'
4302
+ + open('path', { d: 'M24 30 C24 20 27 12 32 7', fill: 'none', stroke: '#C2A24E', 'stroke-width': '2.2', 'stroke-linecap': 'round' }) + '</path>'
4303
+ + open('path', { d: 'M36 30 C38 22 42 15 48 10', fill: 'none', stroke: '#B08F3E', 'stroke-width': '2.4', 'stroke-linecap': 'round' }) + '</path>'
4304
+ + open('path', { d: 'M46 30 C48 24 52 18 56 14', fill: 'none', stroke: '#C2A24E', 'stroke-width': '2.2', 'stroke-linecap': 'round' }) + '</path>'
4305
+ + '</g>'
4306
+ + '</svg>'
4307
+ + '</div>'
4308
+
4309
+ return open('div', { class: 'hz', style: 'position:absolute;inset:0;display:block' })
4310
+ + petHazeMarkup('rgba(240,194,57,.42)', 'dsh-amb-hz-glow')
4311
+ + open('div', {
4312
+ class: 'hz-bank',
4313
+ style: 'position:absolute;left:0;right:0;bottom:0;height:14%;z-index:4;'
4314
+ + 'background:linear-gradient(to bottom,rgba(137,108,57,0) 0%,rgba(137,108,57,.16) 40%,rgba(120,92,44,.3) 100%)',
4315
+ })
4316
+ + open('div', {
4317
+ class: 'hz-bank-line',
4318
+ style: 'position:absolute;top:0;left:0;right:0;height:1.2px;opacity:.5;'
4319
+ + 'background:linear-gradient(90deg,transparent,rgba(255,250,228,.9),transparent)',
4320
+ }) + '</div>'
4321
+ + '</div>'
4322
+ + grassTuft('left:2%', '0', '4.5em', '5')
4323
+ + grassTuft('left:17%', '1%', '3.6em', '7')
4324
+ + grassTuft('right:3%', '0', '4.2em', '5')
4325
+ + open('div', {
4326
+ class: 'hz-ball',
4327
+ style: 'position:absolute;left:14%;bottom:8%;width:1.7em;z-index:6',
4328
+ })
4329
+ + open('svg', {
4330
+ viewBox: '0 0 20 20',
4331
+ style: 'display:block;width:100%;height:auto',
4332
+ })
4333
+ + open('circle', { cx: '10', cy: '10', r: '9', fill: '#F0C239', stroke: '#C9A02F', 'stroke-width': '1' }) + '</circle>'
4334
+ + 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>'
4335
+ + open('ellipse', { cx: '7', cy: '6.5', rx: '2.6', ry: '1.6', fill: '#FFF3C4', opacity: '0.85' }) + '</ellipse>'
4336
+ + '</svg>'
4337
+ + '</div>'
4338
+ + open('div', {
4339
+ class: 'hz-dog',
4340
+ style: 'position:absolute;left:32%;bottom:7%;width:8em;z-index:6',
4341
+ }) + dog + '</div>'
4342
+ + fluffNodes
4343
+ + '</div>'
4344
+ }
4345
+
3282
4346
  /**
3283
4347
  * Apply a function on every tick until the thing it observes stops changing.
3284
4348
  *
@@ -3369,7 +4433,11 @@ window.__ModuleLoader__.load({
3369
4433
  ? jiexinAmbientScene(options?.dust)
3370
4434
  : kind === 'fengchen'
3371
4435
  ? fengchenAmbientScene(options?.feathers, options?.dew)
3372
- : ''
4436
+ : kind === 'humao'
4437
+ ? humaoAmbientScene(options?.dust)
4438
+ : kind === 'ahuang'
4439
+ ? ahuangAmbientScene(options?.dust)
4440
+ : ''
3373
4441
  // The builder is fed only attributes and hex values, but this is the one place
3374
4442
  // markup from outside this file could ever arrive, so it refuses anyway.
3375
4443
  if (/<script|\son[a-z]+\s*=/i.test(markup)) {
@@ -3731,11 +4799,12 @@ window.__ModuleLoader__.load({
3731
4799
  }
3732
4800
 
3733
4801
  /**
3734
- * Remove every ambient node, ownership aside.
4802
+ * Remove every legacy ambient SEAT, ownership aside.
3735
4803
  *
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.
4804
+ * Despite the name it does NOT touch the live `.dsh-amb-control` layer: that layer is
4805
+ * owned by `drawScene`, and it is only rewritten when a theme actually has scenery. So
4806
+ * this call is safe on the "theme has no scenery" path, where leaving the layer in
4807
+ * place is the INTENDED behaviour — see the note in `syncAmbient`.
3739
4808
  */
3740
4809
  function removeAllAmbientSeats() {
3741
4810
  if (typeof document === 'undefined') return
@@ -3858,10 +4927,21 @@ window.__ModuleLoader__.load({
3858
4927
  /**
3859
4928
  * Render the active theme's scenery into the sidebar column.
3860
4929
  *
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.
4930
+ * Idempotent: the scene box is reused and its content is replaced only when the markup
4931
+ * changes, so re-running never stacks duplicate scenes (two copies of a scene = two of
4932
+ * every animated element, which is what produced the duplicated dragonflies).
4933
+ *
4934
+ * Two different "no scenery" cases, and they are NOT the same:
4935
+ *
4936
+ * · switching to ANOTHER skin — the markup differs, so the box is repainted with the
4937
+ * new skin's artwork;
4938
+ * · switching to a theme that has no `ambient` at all — the BUILT-IN 浅色 / 深色 cards.
4939
+ * Only the legacy seats are swept; the live `.dsh-amb-control` layer is deliberately
4940
+ * LEFT ALONE, so the previous skin's artwork (balloons, fish, mountains…) stays on
4941
+ * screen over the system palette. That leftover is a feature the user asked to keep
4942
+ * — the built-in cards advertise it ("会有惊喜哦") and
4943
+ * `tests/check-boot-path.mjs` asserts it, so a later "cleanup" cannot remove it
4944
+ * without turning the test red.
3865
4945
  * @param kind - the ambient kind, or undefined for none.
3866
4946
  * @param options - the theme's ambient options.
3867
4947
  */
@@ -3934,6 +5014,18 @@ window.__ModuleLoader__.load({
3934
5014
  const markup = sceneMarkup(kind, options)
3935
5015
 
3936
5016
  if (markup === '') {
5017
+ // ── THE LIVE LAYER IS NOT TOUCHED HERE, ON PURPOSE ───────────────────────
5018
+ //
5019
+ // This branch runs when the active theme has no scenery — i.e. after clicking one of
5020
+ // the built-in 浅色 / 深色 cards. `removeAllAmbientSeats()` only sweeps
5021
+ // `#dsh-theme-ambient` SEATS (a container earlier builds created); the layer that
5022
+ // actually paints is `.dsh-amb-control`, and it is only ever rewritten by
5023
+ // `drawScene()`. So the previous skin's artwork stays where it is.
5024
+ //
5025
+ // That is the user's "惊喜": pick a skin, then switch to 浅色 / 深色, and the
5026
+ // sidebar keeps the balloons / fish / mountains while the palette goes back to the
5027
+ // system appearance. It is asserted in `tests/check-boot-path.mjs`
5028
+ // (「切到内置主题后装饰层保留」) — do not "fix" it by clearing the layer here.
3937
5029
  removeAllAmbientSeats()
3938
5030
  if (record) noteAmbientAttempt({ kind, column: true, band: '无装饰' })
3939
5031
  ambientReport = { found: true, kind: '(none)', note: '该主题无装饰' }
@@ -4511,6 +5603,14 @@ window.__ModuleLoader__.load({
4511
5603
  // Detail lines are opt-in; a line that reports a problem is not detail.
4512
5604
  const scenery = sceneryLine(selected)
4513
5605
  const showScenery = scenery !== null && (debugEnabled() || sceneryLineIsWarning(scenery))
5606
+ // ── THE HEADER COUNT IS SKINS, NOT CARDS ─────────────────────────────────
5607
+ //
5608
+ // The built-in 浅色 / 深色 cards are not skins — they only switch the palette back to the
5609
+ // official appearance — so counting them here would advertise a number that disagrees
5610
+ // with the README and with npm's description ("6 skins"). The cards are named in the
5611
+ // same line instead, so nothing is hidden by leaving them out of the number.
5612
+ const builtInCards = ids.filter((id) => BUILT_IN_LABELS[id] !== undefined).length
5613
+ const skinCount = ids.length - builtInCards
4514
5614
  return jsxs('div', {
4515
5615
  className: 'tg-page',
4516
5616
  children: [
@@ -4519,7 +5619,7 @@ window.__ModuleLoader__.load({
4519
5619
  children: [
4520
5620
  jsx('span', { className: 'tg-title', children: t('title') }),
4521
5621
  jsx('span', { className: 'tg-hint', children: t('hint') }),
4522
- jsx('span', { className: 'tg-hint', children: t('count', { count: ids.length }) }),
5622
+ jsx('span', { className: 'tg-hint', children: t('count', { count: skinCount }) }),
4523
5623
  // What this copy is, so a reader can compare it with the version npm
4524
5624
  // publishes without digging through the profile.
4525
5625
  jsx('span', { className: 'tg-hint', children: `v${BUNDLED_VERSION}` }),
@@ -4534,7 +5634,11 @@ window.__ModuleLoader__.load({
4534
5634
  swatches: swatches[id] || [],
4535
5635
  selected: id === selected,
4536
5636
  applied: t('applied'),
4537
- onSelect: setTheme,
5637
+ // The click is recorded BEFORE it is handed to the shell, because the theme
5638
+ // service publishes synchronously and the publisher re-applies the remembered
5639
+ // skin: a built-in card clicked after a skin used to be undone immediately.
5640
+ // `rememberCardChoice` is what lets 浅色 / 深色 win — see its documentation.
5641
+ onSelect: (id) => { rememberCardChoice(id); setTheme(id) },
4538
5642
  t,
4539
5643
  }, id)),
4540
5644
  }),
@@ -4918,8 +6022,11 @@ window.__ModuleLoader__.load({
4918
6022
  * handler, so the safety has to live in the CONFIGURATION CONTRACT rather than in code.
4919
6023
  * The one way to break it is to hand-write a skin id into `preference` — which the
4920
6024
  * official service would then reject at boot. Never do that.
6025
+ *
6026
+ * The keys themselves live at MODULE scope ({@link SKIN_KEY} / {@link BUILT_IN_KEY}):
6027
+ * the card-click handler runs in the page component, far from this body, and it must
6028
+ * be able to write them before the click reaches the theme service.
4921
6029
  */
4922
- const SKIN_KEY = 'theme-gallery:last-skin'
4923
6030
 
4924
6031
  /**
4925
6032
  * The token used to prove a skin's palette reached the document.
@@ -4952,6 +6059,45 @@ window.__ModuleLoader__.load({
4952
6059
  }
4953
6060
  }
4954
6061
 
6062
+ /**
6063
+ * Record that a skin is the active theme — the restart-time memory.
6064
+ *
6065
+ * Written only once the service actually reports the skin, so the key never names a
6066
+ * skin that was merely requested and then lost. Clearing the built-in marker in the
6067
+ * same breath keeps the two keys from disagreeing about what the user last chose.
6068
+ * @param id - the active skin id.
6069
+ */
6070
+ function rememberActiveSkin(id) {
6071
+ try {
6072
+ if (window.localStorage.getItem(SKIN_KEY) !== id) window.localStorage.setItem(SKIN_KEY, id)
6073
+ if (window.localStorage.getItem(BUILT_IN_KEY) !== null) window.localStorage.removeItem(BUILT_IN_KEY)
6074
+ } catch (error) {
6075
+ console.error('[theme-gallery] could not remember the active skin:', error)
6076
+ }
6077
+ }
6078
+
6079
+ /**
6080
+ * The skin this plugin should be showing, or null when the user is entitled to a
6081
+ * built-in appearance.
6082
+ *
6083
+ * Three-way precedence, and all three arms are load-bearing:
6084
+ *
6085
+ * 1. a skin the user chose — restore it, across restarts (the original feature);
6086
+ * 2. a built-in the user chose IN THIS PANEL — null, so the restore does not fight it;
6087
+ * 3. nothing chosen yet — the gallery's own default ({@link DEFAULT_SKIN}), which is
6088
+ * what makes a fresh install land on 山青婷彩 instead of the built-in white.
6089
+ *
6090
+ * Arm 2 is why the built-in cards work at all: without it, arm 1 keeps winning and the
6091
+ * click is undone a moment later.
6092
+ * @returns the skin id, or null.
6093
+ */
6094
+ function wantedSkin() {
6095
+ const remembered = rememberedSkin()
6096
+ if (remembered !== null) return remembered
6097
+ if (builtInChoice() !== null) return null
6098
+ return DEFAULT_SKIN
6099
+ }
6100
+
4955
6101
  /** The skin whose palette layer is currently stacked, and how to remove it. */
4956
6102
  let stackedSkin
4957
6103
  let stackedSkinDispose
@@ -5221,17 +6367,20 @@ window.__ModuleLoader__.load({
5221
6367
  // the presenter subscribed leaves the service reporting the skin while the page still
5222
6368
  // shows the built-in palette.
5223
6369
  if (typeof activeId === 'string' && bundledTheme(activeId) !== undefined) {
5224
- if (window.localStorage.getItem(SKIN_KEY) !== activeId) {
5225
- window.localStorage.setItem(SKIN_KEY, activeId)
5226
- }
6370
+ rememberActiveSkin(activeId)
5227
6371
  ensureSkinPainted(activeId, activeId)
5228
6372
  return
5229
6373
  }
5230
6374
 
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()
6375
+ // The app is on a built-in theme even though one of our skins is wanted: either the
6376
+ // boot race lost the preference, or nothing has been chosen yet and the gallery's
6377
+ // default has still to be applied. Re-checked on every publish rather than once per
6378
+ // session, because a single attempt can be swallowed by a presenter that is not
6379
+ // listening yet.
6380
+ //
6381
+ // `wantedSkin()` returns null when the user deliberately picked a built-in card, and
6382
+ // that null is the whole fix for "点深色又被弹回皮肤" — see its documentation.
6383
+ const wanted = wantedSkin()
5235
6384
  if (wanted === null) return
5236
6385
  ensureSkinPainted(wanted, activeId)
5237
6386
  } catch (error) {
@@ -5300,7 +6449,12 @@ window.__ModuleLoader__.load({
5300
6449
  */
5301
6450
  function publish(snapshot) {
5302
6451
  revision += 1
5303
- const themes = [...snapshot.themes].filter((theme) => !OMITTED_IDS.has(theme.id))
6452
+ // DISPLAY ORDER IS APPLIED HERE, TO THE PAGE'S COPY ONLY.
6453
+ //
6454
+ // The registry keeps the order things were registered in, so re-ranking a card can
6455
+ // never change which themes exist, what they contain, or the order they were
6456
+ // contributed — `cardsInDisplayOrder` is a view over the filtered list.
6457
+ const themes = cardsInDisplayOrder([...snapshot.themes].filter((theme) => !OMITTED_IDS.has(theme.id)))
5304
6458
  storeActions.sync(themes, snapshot.preference, revision)
5305
6459
  syncSkin(snapshot)
5306
6460
  syncRememberedSkin(snapshot)
@@ -5556,7 +6710,9 @@ window.__ModuleLoader__.load({
5556
6710
  try {
5557
6711
  const snapshot = ctx.theme.getTheme()
5558
6712
  const active = snapshot?.active?.id
5559
- const wanted = rememberedSkin()
6713
+ // The default skin counts here too: on a fresh install this window is what keeps
6714
+ // asking until 山青婷彩 is genuinely on the document, not merely requested.
6715
+ const wanted = wantedSkin()
5560
6716
  if (wanted === null) {
5561
6717
  done = true
5562
6718
  } else if (skinIsPainted(wanted)) {