dsh-theme-gallery 0.5.0 → 0.6.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/README.md +34 -5
- package/lib/client.js +499 -44
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| 自己做皮肤(或让 AI 做) | 在 `lib/themes/` 加 JSON 后重跑 `npm run embed-themes`,或装 `dsh-theme-skin-author` 技能让 AI 生成 |
|
|
12
12
|
| 贡献一个皮肤给所有人 | 在 `lib/themes/` 放一个 JSON,提 PR |
|
|
13
13
|
|
|
14
|
-
已随 **0.
|
|
14
|
+
已随 **0.6.0** 发布 **7 套**皮肤:四套复刻自**蜂链商城**电商新零售系统管理后台
|
|
15
15
|
([`renjie2026/fenglianshop-open`](https://github.com/renjie2026/fenglianshop-open) 的
|
|
16
16
|
`admin-modular/src/utils/themes.js`),两套是本插件**原创**的宠物主题(同一套造色方法,色相取自中国传统色库),
|
|
17
17
|
另有一套是**「纯色/拼色」配色选择器**卡片(15 个可点色值,见下)。
|
|
@@ -26,14 +26,17 @@
|
|
|
26
26
|
| `hu-zi-a-huang` | **虎子阿黄**(原创) | `#896C39` 秋色(按钮/边框族) | `#F0C239` 缃色(中国传统色) |
|
|
27
27
|
| `shi-liu-jin` | **纯色/拼色**(配色选择器) | `#F20C00` 石榴红(默认那套「石榴金」) | `#9D2933` 胭脂(按钮族) |
|
|
28
28
|
|
|
29
|
-
面板里另有**两张内置外观卡**(`浅色` / `深色`)、**一张宠物挂件卡**(`宠物挂件 ·
|
|
30
|
-
|
|
29
|
+
面板里另有**两张内置外观卡**(`浅色` / `深色`)、**一张宠物挂件卡**(`宠物挂件 · 七只小伙伴`)、
|
|
30
|
+
**一张心情问候卡**(`心情问候`)和**一张菜单折叠卡**(`菜单折叠`):它们**都不是皮肤**,
|
|
31
|
+
所以**不计入上面这 7 套**。
|
|
31
32
|
内置卡把配色切回官方的浅色 / 深色(与「设置 → 通用 → 外观」同一套),我们在官方浅色/深色上加了一点**小惊喜**
|
|
32
33
|
—— 见 [效果预览](#效果预览) 最后一行。官方外观里的「跟随系统」**不出卡**:它就是浅色/深色二选一,
|
|
33
34
|
再占一张卡只是重复。宠物挂件卡则完全**不碰主题**:点头像换宠物、点卡身开关挂件,
|
|
34
35
|
与任何皮肤都能同时开启(见 [宠物挂件](#宠物挂件七只小伙伴))。
|
|
35
36
|
心情问候卡同样**不碰主题**:它控制标题栏上一条滚动的问候语,默认开启、35 分钟换一条,
|
|
36
37
|
可调速度 / 方向 / 位置区域(见 [心情问候语](#心情问候语))。
|
|
38
|
+
菜单折叠卡也**不碰主题**:开启后侧栏不再显示入口,设置页里留一个**轻量一键入口**
|
|
39
|
+
(打开面板 / 恢复侧栏),默认关闭(见 [菜单折叠](#菜单折叠))。
|
|
37
40
|
|
|
38
41
|
工作区里另有三套复刻皮肤仍在打磨,**尚未发布**:江畔冬云 `jiang-pan-dong-yun`、
|
|
39
42
|
徐山军月 `xu-shan-jun-yue`、光彩凤晨 `guang-cai-feng-chen`。
|
|
@@ -627,6 +630,8 @@ document.body.setAttribute('data-dsh-theme-reading', reading.colorScheme)
|
|
|
627
630
|
闲聊是单个可取消的定时器,拆台即清 —— 不会无界堆积(跑飞的事故见 [开发](#开发) 一节的背景);
|
|
628
631
|
- 名字等文本一律以 `textContent` 写入,**不进任何 HTML 字符串**;
|
|
629
632
|
- 急停开关与侧栏素材同源:`cordis.patch.yml` 里 `config: { ambient: false }` 会把挂件一并停掉;
|
|
633
|
+
- **任何模态弹窗打开时挂件自动隐藏**(设置弹窗、确认框等),关闭即恢复 —— 挂件层级
|
|
634
|
+
高于弹窗,不藏会浮在弹窗内容上;检测走语义属性(`role="dialog"` 等),不碰框架类名;
|
|
630
635
|
- 挂件状态(开关 / 当前宠物 / 名字表 / 位置偏移)**只存 `localStorage`**,绝不写 `ui-theme.preference`;
|
|
631
636
|
一期「小狗斑斑」的旧数据(名字 / 摆位)升级时**自动迁移**,不用重新设置。
|
|
632
637
|
|
|
@@ -661,6 +666,30 @@ document.body.setAttribute('data-dsh-theme-reading', reading.colorScheme)
|
|
|
661
666
|
状态只存 `localStorage`,绝不写 `ui-theme.preference`;问候条是否真的建出来,
|
|
662
667
|
面板诊断区有一行自检读数(警告无条件显示,例行读数走 debug 开关)。
|
|
663
668
|
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## 菜单折叠
|
|
672
|
+
|
|
673
|
+
面板里还有一张 **`菜单折叠`** 卡(序号 72,压轴)。它**不是主题,也不是皮肤**,
|
|
674
|
+
管的是**面板入口放在哪**:
|
|
675
|
+
|
|
676
|
+
- **默认关闭** —— 侧栏照常显示「主题皮肤」入口(就是装插件以来一直有的那个);
|
|
677
|
+
- **开启后**,左侧栏不再显示「主题皮肤」入口,腾出侧栏空间;同时
|
|
678
|
+
**左下角头像 → 设置 → 通用设置(agent预设 下方)**的分节里有一键入口:
|
|
679
|
+
**「打开主题皮肤面板」**(关掉设置弹窗、直接跳到侧栏的全屏面板)与
|
|
680
|
+
**「恢复侧栏入口」**(把侧栏的入口开回来)—— 卡面上就写着这条路径,
|
|
681
|
+
忘了从哪找回时看卡面即可;
|
|
682
|
+
- 设置分节是**轻量入口**(当前皮肤 / 侧栏状态两行读数 + 上面两颗按钮),
|
|
683
|
+
不把整面板塞进弹窗:换肤是全屏效果,弹窗挡着看不全 —— 操作都在主区全屏面板完成
|
|
684
|
+
(跳转用的是外壳的官方 `layout.selectPanel` 面,关闭弹窗用的是契约自带的 `close`);
|
|
685
|
+
- **再点一次卡(或在设置分节里点「恢复侧栏入口」)就恢复侧栏显示 ——**
|
|
686
|
+
开关是双向的,即时生效、不用重启;卡面上还有一枚**胶囊滑块开关**(与心情卡同款,
|
|
687
|
+
「侧栏入口」一行)—— 点胶囊、点卡身,效果一样,状态一眼可见;
|
|
688
|
+
- 收放用的是外壳槽位系统自己的注册/撤销机制(`sidebar.panellist` 的 disposer),
|
|
689
|
+
**不用 CSS 硬藏**(框架类名带构建哈希,藏不住也不该藏);
|
|
690
|
+
- 折叠状态**只存 `localStorage`**(`theme-gallery:menu-collapse`),
|
|
691
|
+
**绝不写 `ui-theme.preference`** —— 折叠只改入口显隐,**零主题写入**(一次 `setTheme` 都不发);
|
|
692
|
+
- 设置分节用官方 `settings.section` 槽位注册,**常驻**、不受折叠开关影响。
|
|
664
693
|
|
|
665
694
|
---
|
|
666
695
|
|
|
@@ -820,9 +849,9 @@ pnpm run bench # 调参台
|
|
|
820
849
|
|
|
821
850
|
1. **`tsc`(exit 0)**:`types/*.ts` 用官方发布的真实类型包校验插槽注册契约、`defineStore` 座位形状、主题注册与 token 契约。
|
|
822
851
|
2. **契约与桩**:`check-schema`(真实 schemastery 解析 + **10 个反向用例**,含缺 `label`、缺 `description`、`reading.alpha` 越界)、`smoke-host`(**真机加载** `lib/index.js` 跑 `apply`,并守住 `inject` 清单与插槽门控)、`check-store-contract`(`handle.create()` 每次返回新实例)、`check-client-module`(bundle 形状、挂载失败必须可见)。
|
|
823
|
-
3. **启动安全与端到端引导**:`check-boot-safety`(`inject` 清单、无 `modifies` 环)、`check-boot-timing`、`check-boot-path
|
|
852
|
+
3. **启动安全与端到端引导**:`check-boot-safety`(`inject` 清单、无 `modifies` 环)、`check-boot-timing`、`check-boot-path`(桩里跑完整引导:上色确认、默认皮肤、点内置卡不被弹回、切内置主题后素材保留、菜单折叠的侧栏收放与设置分节常驻 —— 每条都带**反证**)。
|
|
824
853
|
4. **静态审计(含自检与变异反证)**:`check-declaration-order`、`check-tdz-order` + `check-tdz-logic`、`check-scope-reach` + `check-scope-reach-logic`、`check-undefined-calls`、`check-bounded-work`(有界工作量:指纹守卫、单一路径渲染、循环有截止)、`check-self-emit-guard` + `check-self-emit-guard-logic`(数出**全部**主题服务写入点)。
|
|
825
|
-
5. **主题与皮肤**:`check-theme-contribution`(贡献逻辑与幂等,并断言**注册的 token 必须是字符串**——成对对象会变成 `[object Object]`)、`check-card-order`(卡片顺序、内置卡文案、默认皮肤、色块排版 `card.rows`,含 **
|
|
854
|
+
5. **主题与皮肤**:`check-theme-contribution`(贡献逻辑与幂等,并断言**注册的 token 必须是字符串**——成对对象会变成 `[object Object]`)、`check-card-order`(卡片顺序、内置卡文案、默认皮肤、色块排版 `card.rows`,含 **19 处变异反证**)、`check-pet-registry`(宠物家族注册表:七只齐全、美术四件套、动画锚点与 PET_CSS 对得上、台词与步态参数在界内、斑斑一期资产指纹、预览页七只都在,含 **4 处变异反证**)、`check-ambient-render`(场景标记与关键帧)、`check-mood`(心情问候语:语料红线黑名单、时段/节日/洗牌轮换、5 等份区域几何、默认开与胶囊开关、shell.overlay 座位与有界性,含 **11 处变异反证**)。
|
|
826
855
|
6. **介绍文字不落后于实际**:`check-copy-consistency-logic` —— 核对 README 的皮肤清单 / 数量 / 版本 / 入口说法、面板与预览页的计数口径、README 里写的校验组数,与 `lib/themes`、`package.json`、`lib/client.js` 一致(**10 处变异反证 + 1 个"正式发布一套皮肤必须全过"的对照组**)。发布自检 `npm run publish:check` 用同一份实现,所以"改了皮肤忘了改文案"会在发布前被拦住。
|
|
827
856
|
|
|
828
857
|
`scripts/embed-themes.mjs` 校验每套皮肤(形状 / id 唯一 / 双配色 / 12 必需 token / `reading`)后内联进 `lib/client.js`,幂等;两个预览页随后重建。
|
package/lib/client.js
CHANGED
|
@@ -82,6 +82,9 @@ window.__ModuleLoader__.load({
|
|
|
82
82
|
// module seed (`dsh-client-store`) or the shell (`react/jsx-runtime`).
|
|
83
83
|
const { defineStore } = require('@deepseek-ai/dsh-client-store')
|
|
84
84
|
const { jsx, jsxs } = require('react/jsx-runtime')
|
|
85
|
+
// 设置分节组件用:把 pinned store 订阅成 React 视图(useSyncExternalStore,
|
|
86
|
+
// React 18+;dsh-pet 的分节同样直接消费 react)。本插件其余部分仍零 hooks。
|
|
87
|
+
const { useSyncExternalStore } = require('react')
|
|
85
88
|
|
|
86
89
|
/** Panel id shared by the sidebar icon and the main-column page. */
|
|
87
90
|
const PANEL_ID = 'theme-gallery'
|
|
@@ -148,6 +151,7 @@ window.__ModuleLoader__.load({
|
|
|
148
151
|
'hu-zi-a-huang': 75,
|
|
149
152
|
'pet-family': 74,
|
|
150
153
|
'mood-greeting': 73,
|
|
154
|
+
'menu-collapse': 72,
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
/**
|
|
@@ -640,7 +644,7 @@ window.__ModuleLoader__.load({
|
|
|
640
644
|
* package.json, and the release workflow fails when re-running the embed step
|
|
641
645
|
* changes a tracked file.
|
|
642
646
|
*/
|
|
643
|
-
const BUNDLED_VERSION = '0.
|
|
647
|
+
const BUNDLED_VERSION = '0.6.0'
|
|
644
648
|
|
|
645
649
|
/**
|
|
646
650
|
* Themes this package contributes, inlined from lib/themes/*.json.
|
|
@@ -3649,11 +3653,20 @@ window.__ModuleLoader__.load({
|
|
|
3649
3653
|
const zh = {
|
|
3650
3654
|
title: '主题皮肤',
|
|
3651
3655
|
hint: '点一下即切换,选中会记入设置',
|
|
3652
|
-
count: '{count}
|
|
3656
|
+
count: '{count} 款皮肤(另有内置浅色/深色、宠物挂件、心情问候与菜单折叠五张卡)',
|
|
3653
3657
|
current: '当前',
|
|
3654
3658
|
applied: '已应用',
|
|
3655
3659
|
petOn: '已开启',
|
|
3656
3660
|
petOff: '已关闭',
|
|
3661
|
+
menuCollapsed: '已折叠',
|
|
3662
|
+
menuShown: '侧栏显示中',
|
|
3663
|
+
menuPillLabel: '侧栏入口',
|
|
3664
|
+
menuHint: '开启后,左侧栏不再显示「主题皮肤」入口;设置 → 通用设置(agent预设 下方)里有轻量入口,可一键打开面板或恢复侧栏显示。默认关闭。',
|
|
3665
|
+
sectionHint: '换皮肤、调挂件与问候语都在主区全屏面板完成,效果看得见;这里是一键入口。',
|
|
3666
|
+
openPanel: '打开主题皮肤面板',
|
|
3667
|
+
restoreSidebar: '恢复侧栏入口',
|
|
3668
|
+
sectionSkinLabel: '当前皮肤:',
|
|
3669
|
+
sectionSidebarLabel: '侧栏入口:',
|
|
3657
3670
|
empty: '正在读取官方主题注册表…',
|
|
3658
3671
|
// The diagnostics below the cards are a FEATURE — a live reading of the active skin —
|
|
3659
3672
|
// and a wall of monospace text reads as a fault unless the panel says otherwise.
|
|
@@ -3663,11 +3676,20 @@ window.__ModuleLoader__.load({
|
|
|
3663
3676
|
const en = {
|
|
3664
3677
|
title: 'Theme skins',
|
|
3665
3678
|
hint: 'Click to switch; the choice is remembered',
|
|
3666
|
-
count: '{count} skins (+ built-in light/dark,
|
|
3679
|
+
count: '{count} skins (+ built-in light/dark, pet-widget, mood-greeting and menu-collapse cards)',
|
|
3667
3680
|
current: 'Current',
|
|
3668
3681
|
applied: 'Applied',
|
|
3669
3682
|
petOn: 'On',
|
|
3670
3683
|
petOff: 'Off',
|
|
3684
|
+
menuCollapsed: 'Collapsed',
|
|
3685
|
+
menuShown: 'In sidebar',
|
|
3686
|
+
menuPillLabel: 'Sidebar entry',
|
|
3687
|
+
menuHint: 'When on, the sidebar no longer shows this entry; Settings → General (below the Agent preset) has a lightweight entry to open the panel or restore the sidebar. Off by default.',
|
|
3688
|
+
sectionHint: 'Skins, the pet and the greeting live in the full main-area panel where the effect is visible; this is a one-click entry.',
|
|
3689
|
+
openPanel: 'Open the theme panel',
|
|
3690
|
+
restoreSidebar: 'Show the sidebar entry',
|
|
3691
|
+
sectionSkinLabel: 'Current skin: ',
|
|
3692
|
+
sectionSidebarLabel: 'Sidebar entry: ',
|
|
3671
3693
|
empty: 'Reading the theme registry…',
|
|
3672
3694
|
diagNote: 'The lines below are intentional diagnostics of the live skin state, '
|
|
3673
3695
|
+ 'not an error report; only lines starting with ⚠ mean the scenery is not working',
|
|
@@ -3719,6 +3741,33 @@ window.__ModuleLoader__.load({
|
|
|
3719
3741
|
'.tg-badge{font-size:11px;padding:1px 7px;border-radius:999px;',
|
|
3720
3742
|
'background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground)}',
|
|
3721
3743
|
'.tg-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}',
|
|
3744
|
+
/* 菜单折叠卡的路径提示(卡面正文):与 .tg-hint(页头)无关,这是卡内元素。 */
|
|
3745
|
+
'.tg-path{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.55}',
|
|
3746
|
+
/* 菜单折叠卡的胶囊行容器:胶囊行(复用 .tg-mood-row 的排布)+ 路径提示上下排。 */
|
|
3747
|
+
'.tg-menu-body{display:flex;flex-direction:column;gap:8px;align-items:stretch}',
|
|
3748
|
+
/* ── the lightweight settings-section entry (form A, user-approved) ───────
|
|
3749
|
+
设置弹窗里的轻量分节:报状态 + 一键跳转主区面板 + 折叠时给恢复按钮。
|
|
3750
|
+
容器是弹窗内容列(宽度不由我们定),所以全部用自适应宽度 + 垂直节奏,
|
|
3751
|
+
不放网格 —— 完整面板在弹窗里既会挤也没意义(方案 A 的结论)。
|
|
3752
|
+
排版(用户返工 2026-09-27):读数块(label 左值右)+ 主次分明的按钮组
|
|
3753
|
+
(主按钮 = 品牌色整宽跳转,次按钮 = 描边恢复)。 */
|
|
3754
|
+
'.tg-section{display:flex;flex-direction:column;gap:14px;padding:6px 2px 2px}',
|
|
3755
|
+
'.tg-section-title{font-size:15px;font-weight:600}',
|
|
3756
|
+
'.tg-section-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.65}',
|
|
3757
|
+
'.tg-section-facts{display:flex;flex-direction:column;gap:8px;padding:10px 12px;'
|
|
3758
|
+
+ 'border:1px solid var(--dsw-alias-border-l3);border-radius:10px}',
|
|
3759
|
+
'.tg-section-row{display:flex;align-items:center;justify-content:space-between;gap:12px;'
|
|
3760
|
+
+ 'font-size:12.5px;color:var(--dsw-alias-label-secondary)}',
|
|
3761
|
+
'.tg-section-value{color:var(--dsw-alias-label-primary);font-weight:600;text-align:right}',
|
|
3762
|
+
'.tg-section-actions{display:flex;flex-direction:column;gap:8px}',
|
|
3763
|
+
'.tg-section-open{width:100%;padding:9px 14px;border:none;border-radius:10px;'
|
|
3764
|
+
+ 'background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground);'
|
|
3765
|
+
+ 'font-size:13px;font-weight:600;cursor:pointer}',
|
|
3766
|
+
'.tg-section-open:hover{filter:brightness(1.08)}',
|
|
3767
|
+
'.tg-section-restore{width:100%;padding:9px 14px;border:1px solid var(--dsw-alias-border-l3);'
|
|
3768
|
+
+ 'border-radius:10px;background:transparent;color:var(--dsw-alias-label-primary);'
|
|
3769
|
+
+ 'font-size:12.5px;cursor:pointer}',
|
|
3770
|
+
'.tg-section-restore:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
3722
3771
|
'.tg-strip{display:flex;height:6px;border-radius:999px;overflow:hidden;border:.5px solid var(--dsw-alias-border-l3)}',
|
|
3723
3772
|
'.tg-strip span{flex:1}',
|
|
3724
3773
|
/* ── the PALETTE PICKER card (15 clickable colour buttons) ────────────────
|
|
@@ -3839,6 +3888,7 @@ window.__ModuleLoader__.load({
|
|
|
3839
3888
|
scheme: '', selected: 'system', status: '', revision: -1,
|
|
3840
3889
|
petEnabled: false, petKind: 'ban-ban',
|
|
3841
3890
|
moodOn: true, moodSpeed: 'medium', moodDirection: 'ltr', moodZone: 'center',
|
|
3891
|
+
menuCollapsed: false,
|
|
3842
3892
|
}),
|
|
3843
3893
|
actions: {
|
|
3844
3894
|
sync: (draft, themes, selected, revision) => {
|
|
@@ -3936,6 +3986,15 @@ window.__ModuleLoader__.load({
|
|
|
3936
3986
|
draft.moodDirection = String(state.direction)
|
|
3937
3987
|
draft.moodZone = String(state.zone)
|
|
3938
3988
|
},
|
|
3989
|
+
/**
|
|
3990
|
+
* 记录菜单折叠的开关状态,给折叠卡的徽标与 aria-pressed 用。
|
|
3991
|
+
*
|
|
3992
|
+
* 与 markPet/markMood 同一条理由:折叠状态不是主题,单独字段,由 `publish()`
|
|
3993
|
+
* 从 localStorage 读出后写进来。**测试桩的 action 表必须跟着长**(规则 8)。
|
|
3994
|
+
* @param draft - store 草稿。
|
|
3995
|
+
* @param on - 侧栏入口是否已折叠。
|
|
3996
|
+
*/
|
|
3997
|
+
markMenuCollapse: (draft, on) => { draft.menuCollapsed = on === true },
|
|
3939
3998
|
},
|
|
3940
3999
|
})
|
|
3941
4000
|
}
|
|
@@ -4201,10 +4260,11 @@ window.__ModuleLoader__.load({
|
|
|
4201
4260
|
const {
|
|
4202
4261
|
id, label, description, swatches, rows, selected, applied,
|
|
4203
4262
|
selectedScheme, onSelect, onPickScheme, onTogglePet, petEnabled, petKind, onPickPet,
|
|
4204
|
-
mood, onToggleMood, onSetMood, t,
|
|
4263
|
+
mood, onToggleMood, onSetMood, menuCollapsed, onToggleMenuCollapse, t,
|
|
4205
4264
|
} = props
|
|
4206
4265
|
const isWidget = id === PET_WIDGET.id
|
|
4207
4266
|
const isMood = id === MOOD_WIDGET.id
|
|
4267
|
+
const isMenuCollapse = id === MENU_COLLAPSE_WIDGET.id
|
|
4208
4268
|
// ── WHY THE FLAG, AND NOT `blocks !== null` ──────────────────────────────
|
|
4209
4269
|
//
|
|
4210
4270
|
// Whether this card is a picker has to be read off the DATA, not off the rendered
|
|
@@ -4221,17 +4281,20 @@ window.__ModuleLoader__.load({
|
|
|
4221
4281
|
// the whole panel — the silent-failure shape this project keeps paying for.
|
|
4222
4282
|
const shape = cardRowShape({ card: { rows } }, PALETTE_SCHEMES)
|
|
4223
4283
|
const hasPicker = shape !== undefined
|
|
4224
|
-
//
|
|
4284
|
+
// 挂件/问候/折叠卡的徽标永远在场(它就是"开关状态"的读数);其余卡保持原样:选中才亮。
|
|
4225
4285
|
const badgeText = (isWidget || isMood)
|
|
4226
4286
|
? ((isWidget ? petEnabled === true : mood?.on === true) ? t('petOn') : t('petOff'))
|
|
4227
|
-
:
|
|
4287
|
+
: isMenuCollapse
|
|
4288
|
+
? (menuCollapsed === true ? t('menuCollapsed') : t('menuShown'))
|
|
4289
|
+
: applied
|
|
4228
4290
|
const head = jsxs('div', {
|
|
4229
4291
|
className: 'tg-card-top',
|
|
4230
4292
|
children: [
|
|
4231
4293
|
jsx('span', { className: 'tg-name', children: label || id }),
|
|
4232
|
-
(selected || isWidget || isMood)
|
|
4294
|
+
(selected || isWidget || isMood || isMenuCollapse)
|
|
4233
4295
|
? jsx('span', {
|
|
4234
4296
|
className: (isWidget && petEnabled !== true) || (isMood && mood?.on !== true)
|
|
4297
|
+
|| (isMenuCollapse && menuCollapsed !== true)
|
|
4235
4298
|
? 'tg-badge tg-badge-off'
|
|
4236
4299
|
: 'tg-badge',
|
|
4237
4300
|
children: badgeText,
|
|
@@ -4261,16 +4324,50 @@ window.__ModuleLoader__.load({
|
|
|
4261
4324
|
onSetMood,
|
|
4262
4325
|
dict: t,
|
|
4263
4326
|
})
|
|
4264
|
-
:
|
|
4265
|
-
?
|
|
4266
|
-
className: 'tg-
|
|
4267
|
-
children:
|
|
4327
|
+
: isMenuCollapse
|
|
4328
|
+
? jsxs('div', {
|
|
4329
|
+
className: 'tg-menu-body',
|
|
4330
|
+
children: [
|
|
4331
|
+
// 胶囊滑块(用户 2026-09-27 加):开关状态一眼可见,label 说明它管什么。
|
|
4332
|
+
// 样式/结构与心情卡的胶囊同款(类名有意复用 —— 视觉绝对一致,零新 CSS);
|
|
4333
|
+
// 点击拦冒泡 —— 卡身也响应点击,不拦会翻转两次。
|
|
4334
|
+
jsxs('div', {
|
|
4335
|
+
className: 'tg-mood-row',
|
|
4336
|
+
children: [
|
|
4337
|
+
jsx('span', { className: 'tg-mood-row-label', children: t('menuPillLabel') }),
|
|
4338
|
+
jsxs('button', {
|
|
4339
|
+
type: 'button',
|
|
4340
|
+
className: 'tg-mood-switch',
|
|
4341
|
+
role: 'switch',
|
|
4342
|
+
'aria-checked': menuCollapsed === true,
|
|
4343
|
+
title: '开=收起侧栏的「主题皮肤」入口;关=恢复显示(与点卡身等效)',
|
|
4344
|
+
onClick: (event) => {
|
|
4345
|
+
if (event !== undefined && typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
4346
|
+
onToggleMenuCollapse()
|
|
4347
|
+
},
|
|
4348
|
+
children: [
|
|
4349
|
+
jsx('span', { className: 'tg-mood-pill', 'aria-hidden': 'true' }),
|
|
4350
|
+
jsx('span', {
|
|
4351
|
+
className: 'tg-mood-state',
|
|
4352
|
+
children: menuCollapsed === true ? t('menuCollapsed') : t('menuShown'),
|
|
4353
|
+
}),
|
|
4354
|
+
],
|
|
4355
|
+
}),
|
|
4356
|
+
],
|
|
4357
|
+
}),
|
|
4358
|
+
jsx('span', { className: 'tg-path', children: t('menuHint') }),
|
|
4359
|
+
],
|
|
4268
4360
|
})
|
|
4269
|
-
:
|
|
4270
|
-
|
|
4361
|
+
: swatches.length > 0
|
|
4362
|
+
? jsx('span', {
|
|
4363
|
+
className: 'tg-strip',
|
|
4364
|
+
children: swatches.map((colour, index) => jsx('span', { style: { background: colour } }, index)),
|
|
4365
|
+
})
|
|
4366
|
+
: null
|
|
4367
|
+
// The picker fills the card and the widget/mood/menu cards draw their own controls,
|
|
4271
4368
|
// so none of them prints body copy; `description` stays the tooltip, which is
|
|
4272
4369
|
// what the schema says the field is for.
|
|
4273
|
-
const desc = !hasPicker && !isWidget && !isMood && description
|
|
4370
|
+
const desc = !hasPicker && !isWidget && !isMood && !isMenuCollapse && description
|
|
4274
4371
|
? jsx('span', { className: 'tg-desc', children: description })
|
|
4275
4372
|
: null
|
|
4276
4373
|
if (hasPicker) {
|
|
@@ -4311,6 +4408,19 @@ window.__ModuleLoader__.load({
|
|
|
4311
4408
|
children: [head, body, desc],
|
|
4312
4409
|
})
|
|
4313
4410
|
}
|
|
4411
|
+
if (isMenuCollapse) {
|
|
4412
|
+
// 菜单折叠卡:卡身是 **div**(与挂件卡/问候卡同构)——卡内现在装着胶囊按钮
|
|
4413
|
+
// (button 套 button 非法,浏览器会静默重排)。点卡身 = 开关折叠(保留),
|
|
4414
|
+
// 开关语义由**胶囊的 role="switch" + aria-checked** 与徽标表达;**绝不**调用
|
|
4415
|
+
// setTheme —— 折叠只改侧栏入口显隐、零主题写入。卡面正文是路径提示 +
|
|
4416
|
+
// 胶囊行(用户要求把找回路径写在卡上、胶囊让状态一眼可见)。
|
|
4417
|
+
return jsxs('div', {
|
|
4418
|
+
className: 'tg-card tg-menu-card',
|
|
4419
|
+
onClick: () => { onToggleMenuCollapse() },
|
|
4420
|
+
title: description || label,
|
|
4421
|
+
children: [head, body, desc],
|
|
4422
|
+
})
|
|
4423
|
+
}
|
|
4314
4424
|
return jsxs('button', {
|
|
4315
4425
|
type: 'button',
|
|
4316
4426
|
className: 'tg-card',
|
|
@@ -8155,6 +8265,22 @@ window.__ModuleLoader__.load({
|
|
|
8155
8265
|
}
|
|
8156
8266
|
}
|
|
8157
8267
|
|
|
8268
|
+
/**
|
|
8269
|
+
* 菜单折叠卡的卡片数据(moodCardTheme 同构):只在面板里存在的非主题卡,
|
|
8270
|
+
* 走同一条排序与渲染管线(CARD_ORDER 72、tooltip),不注册进主题服务,
|
|
8271
|
+
* 官方外观列表不会多出这一项。
|
|
8272
|
+
* @returns 只带卡片字段的对象(没有 tokens,默认色带为空)。
|
|
8273
|
+
*/
|
|
8274
|
+
function menuCollapseCardTheme() {
|
|
8275
|
+
return {
|
|
8276
|
+
id: MENU_COLLAPSE_WIDGET.id,
|
|
8277
|
+
label: MENU_COLLAPSE_WIDGET.label,
|
|
8278
|
+
description: MENU_COLLAPSE_WIDGET.description,
|
|
8279
|
+
colorScheme: 'light',
|
|
8280
|
+
tokens: {},
|
|
8281
|
+
}
|
|
8282
|
+
}
|
|
8283
|
+
|
|
8158
8284
|
/**
|
|
8159
8285
|
* 输入框(composer 座位)的矩形。
|
|
8160
8286
|
*
|
|
@@ -8223,6 +8349,9 @@ window.__ModuleLoader__.load({
|
|
|
8223
8349
|
/** 挂件层自己的样式表(与 PAGE_CSS 分开:它属于运行时舞台,不属于面板页面)。 */
|
|
8224
8350
|
const PET_CSS = [
|
|
8225
8351
|
`#${PET_STAGE_ID}{position:fixed;left:0;top:0;right:0;bottom:0;pointer-events:none;z-index:1200}`,
|
|
8352
|
+
// 弹窗打开时整层隐藏(body 标记类由 syncPetDialogHidden 维护)——
|
|
8353
|
+
// 挂件 z-index 高于模态弹窗,不藏会浮在弹窗内容上方(实机读数 2026-09-27)。
|
|
8354
|
+
'body.dsh-dialog-open #dsh-theme-pet{display:none!important}',
|
|
8226
8355
|
'.dsh-pet-actor,.dsh-pet-prop{position:absolute;left:0;top:0;pointer-events:auto;cursor:grab;will-change:transform}',
|
|
8227
8356
|
'.dsh-pet-actor:active,.dsh-pet-prop:active{cursor:grabbing}',
|
|
8228
8357
|
// flip 层与 figure 层必须分开:**镜像(内联 transform)绝不能和跑步动画(keyframes)
|
|
@@ -8471,12 +8600,76 @@ window.__ModuleLoader__.load({
|
|
|
8471
8600
|
}
|
|
8472
8601
|
}
|
|
8473
8602
|
|
|
8603
|
+
/* ── 弹窗打开时隐藏挂件(用户 2026-09-27 定)────────────────────────────
|
|
8604
|
+
*
|
|
8605
|
+
* 挂件层挂 body、z-index 1200,高于设置等模态弹窗 —— 弹窗一开,小雪兔就浮在
|
|
8606
|
+
* 弹窗内容上方(实测读数:设置 → 通用设置任何分节都如此)。检测走**语义属性**
|
|
8607
|
+
* (role="dialog" / aria-modal / 原生 <dialog>),绝不碰框架哈希类名(§13);
|
|
8608
|
+
* 任何弹窗都触发 —— 不止设置弹窗,以后别的确认框也不会被挂件挡。
|
|
8609
|
+
* observer 是事件驱动(childList/attributes),随舞台起停 —— 有界;
|
|
8610
|
+
* body 上的标记类只在挂件存活期间存在,拆除时一并清掉。
|
|
8611
|
+
*/
|
|
8612
|
+
|
|
8613
|
+
/** 弹窗隐藏的 observer 句柄;undefined = 未安装。 */
|
|
8614
|
+
let petDialogObserver
|
|
8615
|
+
|
|
8616
|
+
/** 现在有没有模态弹窗开着(语义选择器,一次查询)。 */
|
|
8617
|
+
function petDialogOpen() {
|
|
8618
|
+
try {
|
|
8619
|
+
return typeof document !== 'undefined' && document !== null
|
|
8620
|
+
&& document.querySelector('dialog[open], [role="dialog"], [aria-modal="true"]') !== null
|
|
8621
|
+
} catch {
|
|
8622
|
+
return false
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
|
|
8626
|
+
/**
|
|
8627
|
+
* 把"弹窗开着"落成 body 标记类(PET_CSS 里一条规则据此藏挂件层)。
|
|
8628
|
+
* @returns 弹窗是否开着(诊断读数用)。
|
|
8629
|
+
*/
|
|
8630
|
+
function syncPetDialogHidden() {
|
|
8631
|
+
const open = petDialogOpen()
|
|
8632
|
+
try {
|
|
8633
|
+
document.body?.classList?.toggle('dsh-dialog-open', open)
|
|
8634
|
+
} catch {
|
|
8635
|
+
// classList 不可用(极端桩)时挂件照常显示 —— 隐藏是体验优化,不是功能闸。
|
|
8636
|
+
}
|
|
8637
|
+
return open
|
|
8638
|
+
}
|
|
8639
|
+
|
|
8640
|
+
/** 随舞台安装:装上即先同步一次,之后由 DOM 变化驱动。 */
|
|
8641
|
+
function installPetDialogObserver() {
|
|
8642
|
+
if (petDialogObserver !== undefined) return
|
|
8643
|
+
if (typeof MutationObserver !== 'function') return
|
|
8644
|
+
syncPetDialogHidden()
|
|
8645
|
+
petDialogObserver = new MutationObserver(() => { syncPetDialogHidden() })
|
|
8646
|
+
petDialogObserver.observe(document.body, {
|
|
8647
|
+
childList: true, subtree: true,
|
|
8648
|
+
attributes: true, attributeFilter: ['open', 'role', 'aria-modal', 'class'],
|
|
8649
|
+
})
|
|
8650
|
+
}
|
|
8651
|
+
|
|
8652
|
+
/** 随舞台拆除:断开 observer、清掉标记类 —— 挂件不在了就不该留痕迹。 */
|
|
8653
|
+
function removePetDialogObserver() {
|
|
8654
|
+
if (petDialogObserver === undefined) return
|
|
8655
|
+
try { petDialogObserver.disconnect() } catch {
|
|
8656
|
+
// 断开失败只留一个空 observer,下一次安装会覆盖它。
|
|
8657
|
+
}
|
|
8658
|
+
petDialogObserver = undefined
|
|
8659
|
+
try {
|
|
8660
|
+
document.body?.classList?.remove?.('dsh-dialog-open')
|
|
8661
|
+
} catch {
|
|
8662
|
+
// 同上:清理失败不影响功能。
|
|
8663
|
+
}
|
|
8664
|
+
}
|
|
8665
|
+
|
|
8474
8666
|
/**
|
|
8475
8667
|
* 拆掉舞台与其样式表,清空运行态。
|
|
8476
8668
|
*
|
|
8477
8669
|
* 定时器、闲聊句柄、document 监听一并清掉:任何一句话都不许对已拆除的节点说。
|
|
8478
8670
|
*/
|
|
8479
8671
|
function removePetStage() {
|
|
8672
|
+
removePetDialogObserver()
|
|
8480
8673
|
for (const handle of petTimers) window.clearTimeout(handle)
|
|
8481
8674
|
petTimers = []
|
|
8482
8675
|
if (typeof window.clearTimeout === 'function') window.clearTimeout(petChatterTimer)
|
|
@@ -8889,6 +9082,8 @@ window.__ModuleLoader__.load({
|
|
|
8889
9082
|
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
8890
9083
|
document.addEventListener('input', petOnTyping, true)
|
|
8891
9084
|
}
|
|
9085
|
+
// 弹窗隐藏挂件的 observer 同步起停(装上即先按当前状态同步一次)。
|
|
9086
|
+
installPetDialogObserver()
|
|
8892
9087
|
schedulePetChatter()
|
|
8893
9088
|
}
|
|
8894
9089
|
|
|
@@ -9851,6 +10046,196 @@ window.__ModuleLoader__.load({
|
|
|
9851
10046
|
noteMoodReport({ enabled: false, seat: 'absent', css: false, line: null, pool: '-', note: '开关为关' })
|
|
9852
10047
|
}
|
|
9853
10048
|
|
|
10049
|
+
/* ═══════════════════ 菜单折叠(侧栏入口收放 + 设置页常驻面板)═══════════════════
|
|
10050
|
+
*
|
|
10051
|
+
* 方案二(用户定):设置页入口**常驻**,侧栏入口由本开关收放。三件事:
|
|
10052
|
+
* ① 面板第 15 张卡「菜单折叠」(默认关)—— 卡面带路径提示,点击只改 localStorage;
|
|
10053
|
+
* ② `settings.section` 槽位注册一个**常驻**的一级设置分节(用户指定:agent预设下方),
|
|
10054
|
+
* 内容 = 完整面板(ThemeGalleryPage + 同一个 pinned store),侧栏收起后面板仍可达
|
|
10055
|
+
* —— 这是"不会把自己锁在外面"的关键;
|
|
10056
|
+
* ③ 侧栏入口的注册/撤销随开关走(槽位 disposer,即时生效,不重启、不碰 CSS)。
|
|
10057
|
+
* 不写主题服务:折叠状态只存 localStorage(铁律)。
|
|
10058
|
+
*/
|
|
10059
|
+
|
|
10060
|
+
/** 折叠卡的 id、标题与 tooltip 文案。 */
|
|
10061
|
+
const MENU_COLLAPSE_WIDGET = {
|
|
10062
|
+
id: 'menu-collapse',
|
|
10063
|
+
label: '菜单折叠',
|
|
10064
|
+
description: '开启后,左侧栏不再显示「主题皮肤」入口;设置 → 通用设置(agent预设 下方)'
|
|
10065
|
+
+ '里有轻量入口,可一键打开面板或恢复侧栏显示。关闭后恢复侧栏显示。默认关闭。',
|
|
10066
|
+
}
|
|
10067
|
+
|
|
10068
|
+
/** 折叠状态记在这里。与皮肤 id 同一条铁律:绝不写 `ui-theme.preference`。 */
|
|
10069
|
+
const MENU_COLLAPSE_KEY = 'theme-gallery:menu-collapse'
|
|
10070
|
+
|
|
10071
|
+
/**
|
|
10072
|
+
* 设置分节在设置页导航里的位置(order)。
|
|
10073
|
+
*
|
|
10074
|
+
* 探针读数(app.asar + profile 插件,2026-09-27):settings.section 里 cool-theme=5、
|
|
10075
|
+
* skin-center=120、pet=130;General 行 appearance=10、font-size=11。「agent预设」的
|
|
10076
|
+
* 注册点不在客户端 bundle 里(无法静态标定它的 order),先取 **135**(排在已知插件
|
|
10077
|
+
* 分节之后);实机若不在 agent预设 下方,改这一个数字即可。
|
|
10078
|
+
*/
|
|
10079
|
+
const SETTINGS_SECTION_ORDER = 135
|
|
10080
|
+
|
|
10081
|
+
/**
|
|
10082
|
+
* 读取并消毒折叠状态。缺省**关**(= 侧栏显示现状),坏数据回缺省。
|
|
10083
|
+
* @returns `{ collapsed }`。
|
|
10084
|
+
*/
|
|
10085
|
+
function readMenuCollapseState() {
|
|
10086
|
+
try {
|
|
10087
|
+
const raw = window.localStorage?.getItem(MENU_COLLAPSE_KEY)
|
|
10088
|
+
if (typeof raw !== 'string' || raw === '') return { collapsed: false }
|
|
10089
|
+
const parsed = JSON.parse(raw)
|
|
10090
|
+
if (parsed === null || typeof parsed !== 'object') return { collapsed: false }
|
|
10091
|
+
return { collapsed: parsed.collapsed === true }
|
|
10092
|
+
} catch {
|
|
10093
|
+
return { collapsed: false }
|
|
10094
|
+
}
|
|
10095
|
+
}
|
|
10096
|
+
|
|
10097
|
+
/** 合并写回折叠状态(写失败只记录 —— localStorage 被禁用时本次会话仍可玩)。 */
|
|
10098
|
+
function saveMenuCollapseState(patch) {
|
|
10099
|
+
try {
|
|
10100
|
+
window.localStorage?.setItem(MENU_COLLAPSE_KEY, JSON.stringify({ ...readMenuCollapseState(), ...patch }))
|
|
10101
|
+
} catch (error) {
|
|
10102
|
+
console.error('[theme-gallery] could not save the menu-collapse state:', error)
|
|
10103
|
+
}
|
|
10104
|
+
}
|
|
10105
|
+
|
|
10106
|
+
/** 折叠开关现在是否开着(= 侧栏入口已收起)。 */
|
|
10107
|
+
function menuCollapsed() {
|
|
10108
|
+
return readMenuCollapseState().collapsed
|
|
10109
|
+
}
|
|
10110
|
+
|
|
10111
|
+
/** store 未就绪时分节读的兜底快照(hooks 必须恒在场,见 MenuSettingsSection 文档)。 */
|
|
10112
|
+
const SECTION_FALLBACK_SNAPSHOT = { labels: {}, selected: '', menuCollapsed: false }
|
|
10113
|
+
|
|
10114
|
+
/**
|
|
10115
|
+
* 设置页的常驻分节(方案二,形态 A —— 用户 2026-09-27 定):**轻量入口**。
|
|
10116
|
+
*
|
|
10117
|
+
* 实机读数:完整面板塞进弹窗内容列会白屏(根因 = `store.getState` 与真实
|
|
10118
|
+
* StoreInstance 契约漂移,真实只有 `getSnapshot` —— 已修),而且就算修好,
|
|
10119
|
+
* 弹窗也挡住大半屏幕,换肤的全屏效果在里面看不见。官方契约把 settings.section
|
|
10120
|
+
* 定位为"一个列表项一个设置页"(owner props 只有 close),dsh-pet 的分节同样
|
|
10121
|
+
* 是轻量表单 —— 所以分节只做三件事:报状态(当前皮肤 / 侧栏入口)、给
|
|
10122
|
+
* 「打开主题皮肤面板」跳转(`ctx.layout.selectPanel` —— 契约明说它是给插件
|
|
10123
|
+
* 做面板切换的横切面 + `close` 关弹窗)、折叠开着时给「恢复侧栏入口」。
|
|
10124
|
+
* 真正的换肤 / 挂件 / 问候操作都在主区全屏面板里完成。
|
|
10125
|
+
* @param props - 分节 props(t/close + inject 面成员)。
|
|
10126
|
+
* @returns 分节元素树。
|
|
10127
|
+
*/
|
|
10128
|
+
function MenuSettingsSection(props) {
|
|
10129
|
+
const { t, close, store, toggleMenuCollapse } = props
|
|
10130
|
+
// hooks 规则:订阅恒在场 —— store 缺失(inject 面没解析)时订阅空源、读兜底
|
|
10131
|
+
// 快照,分节退化为引导文案,绝不白屏(白屏教训就在眼前)。
|
|
10132
|
+
// 两个包装都是刻意的:抽出方法会丢 this,直接给属性引用在真实引擎上不安全。
|
|
10133
|
+
const snapshot = useSyncExternalStore(
|
|
10134
|
+
(onStoreChange) => (store?.subscribe ? store.subscribe(onStoreChange) : () => {}),
|
|
10135
|
+
() => (store?.getSnapshot ? store.getSnapshot() : SECTION_FALLBACK_SNAPSHOT),
|
|
10136
|
+
)
|
|
10137
|
+
const skinLabel = snapshot?.labels?.[snapshot?.selected] ?? snapshot?.selected ?? ''
|
|
10138
|
+
const collapsed = snapshot?.menuCollapsed === true
|
|
10139
|
+
// 排版(用户 2026-09-27 返工):标题 → 说明 → 读数块(两行,label 左值右)→
|
|
10140
|
+
// 按钮组(主按钮跳转、次按钮恢复)—— 主次分明、垂直节奏,适配弹窗窄容器。
|
|
10141
|
+
return jsxs('div', {
|
|
10142
|
+
className: 'tg-section',
|
|
10143
|
+
children: [
|
|
10144
|
+
jsx('div', { className: 'tg-section-title', children: t('title') }),
|
|
10145
|
+
jsx('div', { className: 'tg-section-hint', children: t('sectionHint') }),
|
|
10146
|
+
jsxs('div', {
|
|
10147
|
+
className: 'tg-section-facts',
|
|
10148
|
+
children: [
|
|
10149
|
+
jsxs('div', {
|
|
10150
|
+
className: 'tg-section-row',
|
|
10151
|
+
children: [
|
|
10152
|
+
jsx('span', { children: t('sectionSkinLabel') }),
|
|
10153
|
+
jsx('span', { className: 'tg-section-value', children: skinLabel }),
|
|
10154
|
+
],
|
|
10155
|
+
}),
|
|
10156
|
+
jsxs('div', {
|
|
10157
|
+
className: 'tg-section-row',
|
|
10158
|
+
children: [
|
|
10159
|
+
jsx('span', { children: t('sectionSidebarLabel') }),
|
|
10160
|
+
jsx('span', {
|
|
10161
|
+
className: 'tg-section-value',
|
|
10162
|
+
children: collapsed ? t('menuCollapsed') : t('menuShown'),
|
|
10163
|
+
}),
|
|
10164
|
+
],
|
|
10165
|
+
}),
|
|
10166
|
+
],
|
|
10167
|
+
}),
|
|
10168
|
+
jsxs('div', {
|
|
10169
|
+
className: 'tg-section-actions',
|
|
10170
|
+
children: [
|
|
10171
|
+
jsx('button', {
|
|
10172
|
+
type: 'button',
|
|
10173
|
+
className: 'tg-section-open',
|
|
10174
|
+
onClick: () => {
|
|
10175
|
+
// 软跳转:布局面不在 / 面板未注册都只留痕,绝不把设置弹窗带崩。
|
|
10176
|
+
try {
|
|
10177
|
+
if (ctx !== undefined && ctx !== null && typeof ctx.layout?.selectPanel === 'function') {
|
|
10178
|
+
ctx.layout.selectPanel(PANEL_ID)
|
|
10179
|
+
} else {
|
|
10180
|
+
console.error('[theme-gallery] ctx.layout.selectPanel is unavailable; cannot jump to the panel')
|
|
10181
|
+
}
|
|
10182
|
+
} catch (error) {
|
|
10183
|
+
console.error('[theme-gallery] could not jump to the panel:', error)
|
|
10184
|
+
}
|
|
10185
|
+
// 官方明说 close 是给"从分节离开设置"用的 —— 跳转后关掉弹窗,全屏效果可见。
|
|
10186
|
+
if (typeof close === 'function') close()
|
|
10187
|
+
},
|
|
10188
|
+
children: t('openPanel'),
|
|
10189
|
+
}),
|
|
10190
|
+
collapsed ? jsx('button', {
|
|
10191
|
+
type: 'button',
|
|
10192
|
+
className: 'tg-section-restore',
|
|
10193
|
+
onClick: () => {
|
|
10194
|
+
// 复用折叠卡的同一动作:写回 localStorage + 重新注册侧栏入口 + publish
|
|
10195
|
+
// 报回 store —— 分节经订阅重渲染,这颗按钮自己消失。
|
|
10196
|
+
try { toggleMenuCollapse() } catch (error) {
|
|
10197
|
+
console.error('[theme-gallery] could not restore the sidebar entry:', error)
|
|
10198
|
+
}
|
|
10199
|
+
},
|
|
10200
|
+
children: t('restoreSidebar'),
|
|
10201
|
+
}) : null,
|
|
10202
|
+
],
|
|
10203
|
+
}),
|
|
10204
|
+
],
|
|
10205
|
+
})
|
|
10206
|
+
}
|
|
10207
|
+
|
|
10208
|
+
/** 侧栏入口的 inject 句柄;undefined = 当前没有注册(已折叠)。 */
|
|
10209
|
+
let sidebarEntryDispose
|
|
10210
|
+
|
|
10211
|
+
/**
|
|
10212
|
+
* 侧栏入口的注册/撤销随折叠开关走(幂等)。
|
|
10213
|
+
*
|
|
10214
|
+
* 撤销用的是槽位系统自己的 disposer —— 官方机制,即时生效;**绝不**用 CSS 硬藏
|
|
10215
|
+
* (框架类名带哈希,§13 的教训)。每次状态变化都留痕(noteAmbientEvent),
|
|
10216
|
+
* "收起了/恢复了"必须可查,而不是靠用户盯着侧栏猜。
|
|
10217
|
+
*/
|
|
10218
|
+
function syncSidebarEntry() {
|
|
10219
|
+
if (typeof ctx === 'undefined' || ctx === null) return
|
|
10220
|
+
if (menuCollapsed()) {
|
|
10221
|
+
if (sidebarEntryDispose !== undefined) {
|
|
10222
|
+
sidebarEntryDispose()
|
|
10223
|
+
sidebarEntryDispose = undefined
|
|
10224
|
+
noteAmbientEvent('侧栏入口已收起(菜单折叠开)')
|
|
10225
|
+
}
|
|
10226
|
+
return
|
|
10227
|
+
}
|
|
10228
|
+
if (sidebarEntryDispose !== undefined) return
|
|
10229
|
+
sidebarEntryDispose = ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
|
10230
|
+
name: 'sidebar.panellist',
|
|
10231
|
+
id: PANEL_ID,
|
|
10232
|
+
order: 30,
|
|
10233
|
+
locale: NS,
|
|
10234
|
+
label: () => zh.title,
|
|
10235
|
+
}, PanelGlyph))
|
|
10236
|
+
noteAmbientEvent('侧栏入口已显示(菜单折叠关)')
|
|
10237
|
+
}
|
|
10238
|
+
|
|
9854
10239
|
/**
|
|
9855
10240
|
* Render the main-column gallery page.
|
|
9856
10241
|
*
|
|
@@ -9860,7 +10245,7 @@ window.__ModuleLoader__.load({
|
|
|
9860
10245
|
* @param props - composed slot props.
|
|
9861
10246
|
* @returns the page element tree.
|
|
9862
10247
|
*/
|
|
9863
|
-
function ThemeGalleryPage({ t, setTheme, pickScheme, togglePet, pickPet, toggleMood, setMood, useStore, usePanelInfo }) {
|
|
10248
|
+
function ThemeGalleryPage({ t, setTheme, pickScheme, togglePet, pickPet, toggleMood, setMood, toggleMenuCollapse, useStore, usePanelInfo }) {
|
|
9864
10249
|
const info = usePanelInfo((s) => s.activePanelId)
|
|
9865
10250
|
const ids = useStore((s) => s.ids)
|
|
9866
10251
|
const labels = useStore((s) => s.labels)
|
|
@@ -9874,6 +10259,7 @@ window.__ModuleLoader__.load({
|
|
|
9874
10259
|
const moodSpeed = useStore((s) => s.moodSpeed)
|
|
9875
10260
|
const moodDirection = useStore((s) => s.moodDirection)
|
|
9876
10261
|
const moodZone = useStore((s) => s.moodZone)
|
|
10262
|
+
const menuCollapsed = useStore((s) => s.menuCollapsed)
|
|
9877
10263
|
const selected = useStore((s) => s.selected)
|
|
9878
10264
|
const status = useStore((s) => s.status)
|
|
9879
10265
|
// The layout keeps this page registered whatever the selection is, so it
|
|
@@ -9905,10 +10291,11 @@ window.__ModuleLoader__.load({
|
|
|
9905
10291
|
// official appearance — so counting them here would advertise a number that disagrees
|
|
9906
10292
|
// with the README and with npm's description ("6 skins"). The cards are named in the
|
|
9907
10293
|
// same line instead, so nothing is hidden by leaving them out of the number.
|
|
9908
|
-
//
|
|
9909
|
-
// "皮肤数"之外(copy-consistency 的正则钉住的是
|
|
10294
|
+
// 宠物挂件卡/心情问候卡/菜单折叠卡同理:它们不是主题、不换配色,只开关各自的
|
|
10295
|
+
// 功能,所以与内置卡一起排除在"皮肤数"之外(copy-consistency 的正则钉住的是
|
|
10296
|
+
// skinCount 那一行的形状)。
|
|
9910
10297
|
const builtInCards = ids.filter((id) => BUILT_IN_LABELS[id] !== undefined
|
|
9911
|
-
|| id === PET_WIDGET.id || id === MOOD_WIDGET.id).length
|
|
10298
|
+
|| id === PET_WIDGET.id || id === MOOD_WIDGET.id || id === MENU_COLLAPSE_WIDGET.id).length
|
|
9912
10299
|
const skinCount = ids.length - builtInCards
|
|
9913
10300
|
return jsxs('div', {
|
|
9914
10301
|
className: 'tg-page',
|
|
@@ -9958,6 +10345,10 @@ window.__ModuleLoader__.load({
|
|
|
9958
10345
|
mood: { on: moodOn === true, speed: moodSpeed, direction: moodDirection, zone: moodZone },
|
|
9959
10346
|
onToggleMood: () => { toggleMood() },
|
|
9960
10347
|
onSetMood: (patch) => { setMood(patch) },
|
|
10348
|
+
// 菜单折叠卡:点击 = 收起/恢复侧栏入口(见 ThemeCard 的折叠分支)。
|
|
10349
|
+
// 状态从 localStorage 报来,徽标与 aria-pressed 都读它。
|
|
10350
|
+
menuCollapsed: menuCollapsed === true,
|
|
10351
|
+
onToggleMenuCollapse: () => { toggleMenuCollapse() },
|
|
9961
10352
|
t,
|
|
9962
10353
|
}, id)),
|
|
9963
10354
|
}),
|
|
@@ -10085,17 +10476,26 @@ window.__ModuleLoader__.load({
|
|
|
10085
10476
|
* That is the pair that makes it safe, and this package now has both:
|
|
10086
10477
|
*
|
|
10087
10478
|
* package.json dsh.client.inject = ["@deepseek-ai/dsh-client-locale",
|
|
10479
|
+
* "@deepseek-ai/dsh-client-ui-layout",
|
|
10088
10480
|
* "@deepseek-ai/dsh-client-ui-theme"]
|
|
10089
|
-
* here
|
|
10481
|
+
* here the inject list = ['slots', 'locale', 'theme', 'layout']
|
|
10090
10482
|
*
|
|
10091
10483
|
* The two earlier failures were the `modifies` cycle and the missing module declaration.
|
|
10092
|
-
* Both are fixed, and both are asserted in `tests/check-boot-safety.mjs`.
|
|
10484
|
+
* Both are fixed, and both are asserted in `tests/check-boot-safety.mjs`. `layout` joined
|
|
10485
|
+
* in 2026-09-27 for the settings-section jump: without the declaration `ctx.layout` is
|
|
10486
|
+
* undefined and the "open panel" button silently only closed the dialog (实测读数).
|
|
10487
|
+
* Note: the assertion regex is anchored to line start — prose examples like this one
|
|
10488
|
+
* must NOT be written as `exports.inject = [...]`, or the static audit reads the comment.
|
|
10093
10489
|
*
|
|
10094
10490
|
* `locale` is declared for the same reason: the sidebar row resolves its label through it,
|
|
10095
10491
|
* and the providing module is declared in the manifest.
|
|
10096
10492
|
* @type {string[]}
|
|
10097
10493
|
*/
|
|
10098
|
-
|
|
10494
|
+
// `layout` 是设置分节「打开主题皮肤面板」的跳转面(ctx.layout.selectPanel —— 官方
|
|
10495
|
+
// Context 合并声明,契约原文说它就是给其他插件做面板切换的横切面)。实机读数
|
|
10496
|
+
// 2026-09-27:没声明它时 ctx.layout 是 undefined,跳转按钮只关了弹窗 —— 插件
|
|
10497
|
+
// 能读到哪些服务完全由这份清单决定,缺一个就是静默失败。
|
|
10498
|
+
exports.inject = ['slots', 'locale', 'theme', 'layout']
|
|
10099
10499
|
|
|
10100
10500
|
/**
|
|
10101
10501
|
* Marks this client module as a PLUGIN rather than a plain service module.
|
|
@@ -10842,9 +11242,9 @@ window.__ModuleLoader__.load({
|
|
|
10842
11242
|
// never change which themes exist, what they contain, or the order they were
|
|
10843
11243
|
// contributed — `cardsInDisplayOrder` is a view over the filtered list.
|
|
10844
11244
|
// 宠物挂件卡不是主题、不在注册表里:作为一张**只存在于面板**的卡片数据追加进来,
|
|
10845
|
-
// 与其他卡走同一条排序管线(见 `petWidgetCardTheme
|
|
11245
|
+
// 与其他卡走同一条排序管线(见 `petWidgetCardTheme`)。菜单折叠卡同构。
|
|
10846
11246
|
const themes = cardsInDisplayOrder([...snapshot.themes].filter((theme) => !OMITTED_IDS.has(theme.id))
|
|
10847
|
-
.concat([petWidgetCardTheme(), moodCardTheme()]))
|
|
11247
|
+
.concat([petWidgetCardTheme(), moodCardTheme(), menuCollapseCardTheme()]))
|
|
10848
11248
|
storeActions.sync(themes, appliedCardId(snapshot), revision)
|
|
10849
11249
|
// 卡片上点亮哪一格,以及要不要把那一层配色叠上去 —— 两件事都读同一份
|
|
10850
11250
|
// localStorage,所以"看到的选中态"与"屏幕上的颜色"不可能对不上。
|
|
@@ -10855,6 +11255,8 @@ window.__ModuleLoader__.load({
|
|
|
10855
11255
|
storeActions.markPetKind(readPetState().kind)
|
|
10856
11256
|
// 心情问候卡的胶囊开关与三组 chips 读同一个来源。
|
|
10857
11257
|
storeActions.markMood(readMoodState())
|
|
11258
|
+
// 菜单折叠卡的徽标与 aria-pressed 读同一个来源。
|
|
11259
|
+
storeActions.markMenuCollapse(menuCollapsed())
|
|
10858
11260
|
// 问候条是幂等同步:状态/几何/文本没变就一个字节都不写(moodBuildBar 指纹守卫),
|
|
10859
11261
|
// 所以 publish 顺路调用它没有性能负担,还负责把"座位晚渲染"捡起来。
|
|
10860
11262
|
syncMoodBar()
|
|
@@ -10984,6 +11386,23 @@ window.__ModuleLoader__.load({
|
|
|
10984
11386
|
}
|
|
10985
11387
|
}
|
|
10986
11388
|
|
|
11389
|
+
/**
|
|
11390
|
+
* 用户点了菜单折叠卡:翻转折叠状态、收放侧栏入口、刷新面板。
|
|
11391
|
+
*
|
|
11392
|
+
* 这条路**完全不碰主题服务** —— 折叠只改侧栏入口的显隐,面板本体(main 槽位)
|
|
11393
|
+
* 与设置分节(settings.section)恒在,所以从任何一侧都能把开关翻回来。
|
|
11394
|
+
* `publish()` 会把新状态经 `markMenuCollapse` 报给页面。
|
|
11395
|
+
*/
|
|
11396
|
+
function toggleMenuCollapse() {
|
|
11397
|
+
try {
|
|
11398
|
+
saveMenuCollapseState({ collapsed: !menuCollapsed() })
|
|
11399
|
+
syncSidebarEntry()
|
|
11400
|
+
publish(ctx.theme.getTheme())
|
|
11401
|
+
} catch (error) {
|
|
11402
|
+
console.error('[theme-gallery] could not toggle the menu collapse:', error)
|
|
11403
|
+
}
|
|
11404
|
+
}
|
|
11405
|
+
|
|
10987
11406
|
/**
|
|
10988
11407
|
* 用户点了挂件卡上的一颗头像:换成那只宠物并放它出场。
|
|
10989
11408
|
*
|
|
@@ -11364,6 +11783,35 @@ window.__ModuleLoader__.load({
|
|
|
11364
11783
|
// shipped panel plugins use the plain form, so this does too — the extra
|
|
11365
11784
|
// machinery was unverified, and a subscription that never installs is
|
|
11366
11785
|
// indistinguishable from a panel that never registers.
|
|
11786
|
+
/**
|
|
11787
|
+
* 面板的 inject 面:侧栏页与设置分节(MenuSettingsSection)共用的同一份动作。
|
|
11788
|
+
* **刻意零主题写入的新增** —— setTheme 是外壳槽位动作(审计允许清单精确匹配
|
|
11789
|
+
* 的那一条),其余全部只写 localStorage。
|
|
11790
|
+
* @returns 动作面(含 pinned store,供设置分节的 useSyncExternalStore 订阅)。
|
|
11791
|
+
*/
|
|
11792
|
+
function buildPanelFace() {
|
|
11793
|
+
return {
|
|
11794
|
+
setTheme: (id) => { ctx.theme.setTheme(id) },
|
|
11795
|
+
// 配色卡上的 15 个色值按钮走这条。它**不直接**切主题(见 `chooseScheme`),
|
|
11796
|
+
// 所以这里不新增主题写入点。
|
|
11797
|
+
pickScheme: (schemeId) => { chooseScheme(schemeId) },
|
|
11798
|
+
// 挂件卡走这条:只开关挂件、只写 localStorage,同样不新增主题写入点。
|
|
11799
|
+
togglePet: () => { togglePetWidget() },
|
|
11800
|
+
// 头像选择行走这条:换宠物(必要时顺路打开),同样零主题写入。
|
|
11801
|
+
pickPet: (kindId) => { selectPetWidget(kindId) },
|
|
11802
|
+
// 心情问候卡走这两条:胶囊开关/点卡身开关、三组设置 chips。
|
|
11803
|
+
// 一样零主题写入 —— 问候与皮肤正交。
|
|
11804
|
+
toggleMood: () => { toggleMoodWidget() },
|
|
11805
|
+
setMood: (patch) => { setMoodSettings(patch) },
|
|
11806
|
+
// 菜单折叠卡走这条:收放侧栏入口、只写 localStorage,同样不新增主题写入点。
|
|
11807
|
+
toggleMenuCollapse: () => { toggleMenuCollapse() },
|
|
11808
|
+
// 设置分节组件用:pinned store 实例(subscribe/getSnapshot —— StoreInstance
|
|
11809
|
+
// 契约上没有 getState,白屏事故的根因就是按 getState 读的),MenuSettingsSection
|
|
11810
|
+
// 经 useSyncExternalStore 把它绑定成响应式视图。
|
|
11811
|
+
store: storeInstance,
|
|
11812
|
+
}
|
|
11813
|
+
}
|
|
11814
|
+
|
|
11367
11815
|
ctx.slots.inject('main', () => {
|
|
11368
11816
|
const disposePage = ctx.slots.register({
|
|
11369
11817
|
name: 'main',
|
|
@@ -11378,20 +11826,7 @@ window.__ModuleLoader__.load({
|
|
|
11378
11826
|
// — so it is set BEFORE the publish that reads it.
|
|
11379
11827
|
injectRan = true
|
|
11380
11828
|
publish(ctx.theme.getTheme())
|
|
11381
|
-
return
|
|
11382
|
-
setTheme: (id) => { ctx.theme.setTheme(id) },
|
|
11383
|
-
// 配色卡上的 15 个色值按钮走这条。它**不直接**切主题(见 `chooseScheme`),
|
|
11384
|
-
// 所以这里不新增主题写入点。
|
|
11385
|
-
pickScheme: (schemeId) => { chooseScheme(schemeId) },
|
|
11386
|
-
// 挂件卡走这条:只开关挂件、只写 localStorage,同样不新增主题写入点。
|
|
11387
|
-
togglePet: () => { togglePetWidget() },
|
|
11388
|
-
// 头像选择行走这条:换宠物(必要时顺路打开),同样零主题写入。
|
|
11389
|
-
pickPet: (kindId) => { selectPetWidget(kindId) },
|
|
11390
|
-
// 心情问候卡走这两条:胶囊开关/点卡身开关、三组设置 chips。
|
|
11391
|
-
// 一样零主题写入 —— 问候与皮肤正交。
|
|
11392
|
-
toggleMood: () => { toggleMoodWidget() },
|
|
11393
|
-
setMood: (patch) => { setMoodSettings(patch) },
|
|
11394
|
-
}
|
|
11829
|
+
return buildPanelFace()
|
|
11395
11830
|
},
|
|
11396
11831
|
}, ThemeGalleryPage)
|
|
11397
11832
|
|
|
@@ -11434,13 +11869,33 @@ window.__ModuleLoader__.load({
|
|
|
11434
11869
|
// Sidebar entrance: the list id IS the main-panel key. `locale` is declared
|
|
11435
11870
|
// the way the shipped panel entries declare it, so the row label resolves
|
|
11436
11871
|
// through the dictionary rather than a raw string.
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11872
|
+
// 菜单折叠(方案二):注册/撤销随折叠开关走 —— syncSidebarEntry 读 localStorage
|
|
11873
|
+
// 决定是否注册;设置页的常驻分节不受它影响(面板永远可达,不会把自己锁在外面)。
|
|
11874
|
+
syncSidebarEntry()
|
|
11875
|
+
|
|
11876
|
+
/**
|
|
11877
|
+
* 一级设置分节(`settings.section`):完整面板常驻于 设置 → 分节列表。
|
|
11878
|
+
* 槽位契约(官方 d.ts):id = 分节键、order = 导航位置、label = 注册方本地化文案、
|
|
11879
|
+
* 内容组件 + inject 面由注册方自带。order 的标定读数与"agent预设下方"的对位
|
|
11880
|
+
* 见 SETTINGS_SECTION_ORDER 的文档。
|
|
11881
|
+
*/
|
|
11882
|
+
ctx.slots.inject('settings.section', () => {
|
|
11883
|
+
try {
|
|
11884
|
+
const unregister = ctx.slots.register({
|
|
11885
|
+
name: 'settings.section',
|
|
11886
|
+
id: PANEL_ID,
|
|
11887
|
+
order: SETTINGS_SECTION_ORDER,
|
|
11888
|
+
label: () => zh.title,
|
|
11889
|
+
locale: NS,
|
|
11890
|
+
inject: () => buildPanelFace(),
|
|
11891
|
+
}, MenuSettingsSection)
|
|
11892
|
+
return () => { unregister() }
|
|
11893
|
+
} catch (error) {
|
|
11894
|
+
// 分节注册失败绝不拖垮挂载(面板仍在侧栏/主区可达);失败本身要留痕。
|
|
11895
|
+
console.error('[theme-gallery] could not register the settings section:', error)
|
|
11896
|
+
return () => {}
|
|
11897
|
+
}
|
|
11898
|
+
})
|
|
11444
11899
|
|
|
11445
11900
|
/* ---------------- mood greeting seat ----------------
|
|
11446
11901
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-theme-gallery",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A theme gallery for DeepSeek Harness (DSH): JSON-defined skins with full-screen coverage and sidebar scenery, a 15-swatch palette-picker card, a pet-widget card (7 registry-driven pets playing by the chat input),
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "A theme gallery for DeepSeek Harness (DSH): JSON-defined skins with full-screen coverage and sidebar scenery, a 15-swatch palette-picker card, a pet-widget card (7 registry-driven pets playing by the chat input), a mood-greeting card (a scrolling greeting on the title bar that rotates every 35 minutes), and a menu-collapse card that moves the panel entry into Settings, switched from the panel under the sidebar's Plugins icon — adding skins needs no code change",
|
|
5
5
|
"author": "renjie2026 (https://github.com/renjie2026)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"platform": "web",
|
|
40
40
|
"inject": [
|
|
41
41
|
"@deepseek-ai/dsh-client-locale",
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
42
43
|
"@deepseek-ai/dsh-client-ui-theme"
|
|
43
44
|
]
|
|
44
45
|
}
|
|
@@ -98,6 +99,7 @@
|
|
|
98
99
|
"peerDependencies": {
|
|
99
100
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
100
101
|
"@deepseek-ai/dsh-client-store": "*",
|
|
102
|
+
"@deepseek-ai/dsh-client-ui-layout": "*",
|
|
101
103
|
"@deepseek-ai/dsh-client-ui-slots": "*",
|
|
102
104
|
"@deepseek-ai/dsh-client-ui-theme": "*"
|
|
103
105
|
},
|