@linxin666/dsh-client-ui-skin-center 0.1.12 → 0.1.13

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.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # node scripts/verify-docs.mjs --write <dir>
5
+ README.md: d76c6efe9b4b484a23911b70ca34f5e790d6a0f0
6
+ README.zh.md: db039c777f5ab54e27f54f278e24c9af27a6407e
package/README.md CHANGED
@@ -1,145 +1,106 @@
1
- # Skin Center(GUI 内嵌皮肤中心)
2
-
3
- `@linxin666/dsh-client-ui-skin-center`(cordis 插件 id `ui-skin-center`)把皮肤列表/试穿/应用
4
- 内嵌进真实 dsh Web GUI 的插件配置页,作为「Web UI 插件」组里的一张卡片
5
- (设置 → 插件配置 → Web UI 插件 → 皮肤中心),与 task-board / pet / live-stats 等
6
- 全家桶插件同一套槽位(`web-ui.plugin.item`),不占设置页一级导航。
7
-
8
- - 列表:展示「官方默认」+ 仓库里全部皮肤(qq98 / ths / xp / blue-fantasy / dragon-heir /
9
- minecraft)的名称、tagline、强调色;当前激活的目标带 Active 标记。
10
- - 试穿:点击「Try on」后按需加载该皮肤的 client bundle——host 路由
11
- `/api/skin-center/bundle/<id>` 以同源 script 提供 `lib/client.js`(内核加载插件的同一机制),
12
- factory 注册到页面自己的 `window.__ModuleLoader__`,`window.__DSH_MODULES__.import` 物化
13
- (不是模拟器、不用 eval),chrome 立即生效;
14
- 亮/暗切换走官方 theme 服务;「Exit try-on」完全还原——当前皮肤的样式、DOM、favicon、
15
- 标题、body 内联样式全部恢复。「官方默认」也可试穿:点一下皮肤立即收回、回到官方外观预览。
16
- - 互斥:试穿期间会按配方暂时收回当前激活皮肤的视觉写面(body 属性、背景内联样式、
17
- chrome 子节点、xp 的 footer taskbar),退出后原样恢复;同一时刻页面上只有一套皮肤。
18
- - 应用:host 半区(`src/index.ts` + `src/routes.ts`)暴露 `/api/skin-center/apply` 与
19
- `/api/skin-center/bundle/<id>`(按需提供皮肤 bundle),
20
- 点击「Apply / 恢复默认」即在服务端执行 `dsh-skin use <name>`(或 `use official`),
21
- 写入 `~/.dsh/cordis.patch.yml` 后由 DSH 配置 watcher 秒级热载入,页面自动刷新生效——
22
- **无需重启 dsh web,无需复制命令**。应用失败时错误提示里附带终端兜底命令。
23
- host 依赖 `dsh-skin` CLI 在 PATH(`~/.local/bin/dsh-skin`,仓库 `scripts/dsh-skin`)。
24
- Windows 兼容性:`dsh-skin` 的 harness home 取 `$DSH_HOME`(缺省 `~/.dsh`),仓库根目录从
25
- 脚本自身位置推导(可用 `DSH_SKIN_REPO` 覆盖),不依赖 `$HOME` 与固定路径;Windows 用户
26
- 自建 `dsh-skin.cmd` 包装(内容 `node scripts/dsh-skin %*`)放入 PATH 目录即可。host 端在
27
- Windows 上经 shell 调用该命令——`execFile` 无法直接启动裸名 `.cmd`。
28
-
29
- ## 安装(官方 plugin bundle 方式)
30
-
31
- 推荐先装皮肤全家桶聚合包 `@linxin666/dsh-skins` 一次到位(含全部皮肤与皮肤中心);
32
- 只装本包时用下列 link 命令。
1
+ # Skin Center (in-GUI embedded skin center)
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ `@linxin666/dsh-client-ui-skin-center` (cordis plugin id `ui-skin-center`) embeds the skin list / try-on / apply into the plugin configuration page of the real dsh Web GUI, as a card in the "Web UI plugins" group (settings → plugin config → Web UI plugins → 皮肤中心 / Skin Center), sharing the same slot (`web-ui.plugin.item`) as the family plugins such as task-board / pet / live-stats, without taking a top-level settings nav item.
6
+
7
+ - List: shows "官方默认" (official default) plus every skin in the repo (qq98 / ths / xp / blue-fantasy / dragon-heir / minecraft) with its name, tagline, and accent color; the currently active target carries the Active marker.
8
+ - Try-on: clicking "Try on" loads that skin's client bundle on demand — the host route `/api/skin-center/bundle/<id>` serves `lib/client.js` as a same-origin script (the same mechanism the core uses to load plugins), the factory registers with the page's own `window.__ModuleLoader__`, and `window.__DSH_MODULES__.import` materializes it (a real loader, not a simulator and no eval); the chrome takes effect immediately; light/dark switching rides the official theme service; "Exit try-on" restores everything — the current skin's styles, DOM, favicon, title, and body inline styles are all restored. "官方默认" (official default) can also be tried on: one click immediately withdraws the skin and returns to the official look preview.
9
+ - Mutual exclusion: during try-on, the active skin's visual writes (body attribute, background inline styles, chrome child nodes, xp's footer taskbar) are temporarily withdrawn per the prescription, and restored verbatim on exit; only one skin is on the page at a time.
10
+ - Apply: the host half (`src/index.ts` + `src/routes.ts`) exposes `/api/skin-center/apply` and `/api/skin-center/bundle/<id>` (serves skin bundles on demand); clicking "Apply / 恢复默认" (Apply / Restore default) runs the embedded in-process `dsh-skin use` port (`src/skin-switch.ts`) server-side, writes `<harness-home>/cordis.patch.yml`, and the DSH config watcher hot-loads it within seconds and the page auto-refreshes — **no dsh web restart, no copying a command, no `dsh-skin` binary on PATH**. On failure the error message includes a terminal fallback command. Harness home follows the dsh launcher: an injected HOME maps to `<home>/.dsh`, else a trimmed non-empty `$DSH_HOME` is used directly, else `~/.dsh`. The target profile resolves as: explicit option, then `$DSH_SKIN_PROFILE`, then `$DSH_PROFILE`, then `process.cwd()` when it is a directory directly under `<harness-home>/profiles/<name>`, then `web`. Windows compatibility: the same resolution rules apply with no `$HOME` or fixed paths, and profile links fall back to directory junctions when symlink privileges are missing.
11
+
12
+ ## Install (official plugin bundle)
13
+
14
+ Install the family skin aggregate package `@linxin666/dsh-skins` first (all skins plus the skin center in one); for this package alone use the `link:` commands below.
33
15
 
34
16
  ```sh
35
- # 装全部皮肤(推荐)
17
+ # All skins (recommended)
36
18
  dsh plugin --profile web add @linxin666/dsh-skins
37
- # 或单独装皮肤中心
19
+ # Or just the skin center
38
20
  dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center
39
- # 从仓库安装(开发调试):dsh plugin --profile web add link:$(pwd)/packages/skins/skin-center
21
+ # From the repo (dev): dsh plugin --profile web add link:$(pwd)/packages/skins/skin-center
40
22
  ```
41
23
 
42
- `$(pwd)` 指克隆全家桶仓库后的目录。
24
+ `$(pwd)` is your clone of the dsh-web-ui monorepo.
43
25
 
44
- skin-center 是符合 DSH 官方插件标准的自包含 bundle(`dsh.bundle.patch` 指向
45
- `cordis.patch.yml`、`prepare` 用专用 tsdown 配置自包含构建,无项目引用、无类型检查),
46
- 也可经 git 安装:`dsh plugin --profile web add github:<org>/dsh-web-ui#<sha>`
47
- (`prepare` 会原地构建 `lib/`)。
26
+ skin-center is a self-contained bundle meeting the official DSH plugin standard (`dsh.bundle.patch` points to `cordis.patch.yml`; `prepare` uses a dedicated tsdown config for a self-contained build with no project references or type checking); it can also be installed via git: `dsh plugin --profile web add github:<org>/dsh-web-ui#<sha>` (the `prepare` script builds `lib/` in place).
48
27
 
49
- 本地 link 安装前需先在全家桶仓库内构建产物(`lib/` 被 git 忽略、不随仓库提交):
50
- `pnpm install && pnpm -r build` 后再 link 安装。
28
+ A local `link:` install needs built artifacts first — `lib/` is git-ignored and not committed, so run `pnpm install && pnpm -r build` in the monorepo before linking.
51
29
 
52
- pnpm ≥10 安装 git 依赖前需先授权 `allowBuilds`(`prepare` 会原地构建),本地 link 安装则无此要求。
30
+ pnpm ≥10 requires authorizing `allowBuilds` before installing a git dependency (the `prepare` script builds in place); a local `link:` install has no such requirement.
53
31
 
54
- 需要皮肤插件们(qq98 / ths / xp / blue-fantasy)在宿主里也可解析时,skin-center 才能
55
- 完整列出 / 试穿全部皮肤;skin-center 本身无互斥要求。
32
+ The skin plugins (qq98 / ths / xp / blue-fantasy) must resolve in the host for skin-center to list / try on the full set; skin-center itself has no mutual-exclusion requirement.
56
33
 
57
- ## 目录结构
34
+ ## Directory structure
58
35
 
59
36
  ```
60
37
  skins/skin-center/
61
- package.json / tsdown.config.ts / tsconfig.json # checkout 内构建所需的元数据
62
- src/index.ts # host 侧:注册 /api/skin-center/* 路由
63
- src/routes.ts # host 路由(代理 dsh-skin CLI)
64
- src/invariant.ts # invariant 伴随插件(无断言)
65
- src/client/index.ts # apply:注册 Web UI 插件组卡片 + body 作用域
66
- src/client/SkinCenter.tsx # 卡片组件(官方默认 + 列表/试穿/亮暗/一键应用)
67
- src/client/try-on.ts # 试穿引擎(真实 loader + 互斥还原,含官方试穿)
68
- src/client/locales.ts # en/zh 文案
69
- src/client/skin-center.module.css # 面板样式(--dsw-* token,随皮肤自适应)
70
- src/client/generated/skins.ts # 生成:皮肤注册表(仅元数据,勿手改)
38
+ package.json / tsdown.config.ts / tsconfig.json # metadata for building within the checkout
39
+ src/index.ts # host side: registers /api/skin-center/* routes
40
+ src/routes.ts # host routes (proxy to the dsh-skin CLI)
41
+ src/invariant.ts # invariant companion plugin (no assertions)
42
+ src/client/index.ts # apply: registers the Web UI plugin-group card + body scope
43
+ src/client/SkinCenter.tsx # card component (official default + list/try-on/light-dark/one-click apply)
44
+ src/client/try-on.ts # try-on engine (real loader + mutual-exclusion restore, incl. official try-on)
45
+ src/client/locales.ts # en/zh copy
46
+ src/client/skin-center.module.css # panel styles (--dsw-* tokens, adapting to the skin)
47
+ src/client/generated/skins.ts # generated: skin registry (metadata only, do not hand-edit)
71
48
  ```
72
49
 
73
- ## 机制要点
74
-
75
- - 皮肤枚举:`generated/skins.ts` 由 `scripts/skin-center-bundles` 生成(读
76
- `skins/<name>/skin.json`,校验 `lib/client.js` 存在)。**只含元数据,不内嵌 bundle 文本**:
77
- 冷启动不解析 ~700KB 的 base64 美术资源,且生成文件跨机器可复现(无构建机绝对路径)。
78
- - 试穿加载:host 路由 `/api/skin-center/bundle/<id>` 按需提供 `lib/client.js`
79
- (同源 script,`<script>` 标签加载——与内核 `defaultLoadBundle` 同一机制),bundle 体调用
80
- `window.__ModuleLoader__.load` 只注册 factory;`window.__DSH_MODULES__.import(package)`
81
- 物化模块(CSS `<style data-plugin>` 自动注入);`surface.apply(miniCtx)` 挂载,
82
- miniCtx 只实现 `effect(cb)`(皮肤唯一依赖)。不依赖 eval,因此不要求 CSP 放行
83
- `unsafe-eval`——只要求同源 script 可加载(页面自身加载插件 bundle 亦然)。
84
- - 失败语义:bundle 路由 404(皮肤未安装 / `lib/client.js` 未构建)或网络失败时,
85
- script 的 error 事件触发,试穿报通用错误并完整还原激活皮肤;加载与还原之间不会留下半套
86
- 皮肤(tryOn 的 catch 分支负责恢复)。
87
- - 退出还原:先跑皮肤的 disposer(属性/chrome/favicon/标题/背景全撤回),再
88
- `invalidate(package)` + 删 style 标签,最后把激活皮肤的视觉快照原样恢复。
89
- 官方默认试穿 = 同一套收回配方但不挂载任何皮肤,退出同样原样恢复。
90
- - 激活皮肤检测:`window.__DSH_BOOT__.entries` 只含启用条目,与注册表 package 比对;
91
- 无匹配即官方默认。
92
- - 一键应用:host `/api/skin-center/apply` 代理 `dsh-skin use <name>` / `use official`
93
- (CLI 是 managed 区段与 symlink 的唯一权威)。DSH 长驻表面自带配置 watcher
94
- (`watchUserPatches` + config-only HMR),patch 写入后数秒热载入、无需重启;
95
- 浏览器刷新页面取新 boot 图即生效(client 插件图行增删不在 `dsh-client-hmr` 语义内)。
96
-
97
- ## 构建(仓库内 tsdown,无需 DSH checkout)
98
-
99
- 皮肤中心与皮肤一样,用仓库内共享 tsdown 预设构建(`shared/tsdown.client.ts`
100
- 处理 CSS Modules 注入与平台外部化;类型来自官方 NPM SDK devDependencies):
50
+ ## Mechanism notes
51
+
52
+ - Skin enumeration: `generated/skins.ts` is produced by `scripts/skin-center-bundles` (reads `skins/<name>/skin.json`, validates that `lib/client.js` exists). **It contains metadata only, no embedded bundle text**: cold start does not parse the ~700KB base64 art assets, and the generated file is reproducible across machines (no build-machine absolute paths).
53
+ - Try-on loading: the host route `/api/skin-center/bundle/<id>` serves `lib/client.js` on demand (same-origin script, loaded via a `<script>` tag — the same mechanism as the core's `defaultLoadBundle`); the bundle body calls `window.__ModuleLoader__.load` to only register a factory; `window.__DSH_MODULES__.import(package)` materializes the module (CSS `<style data-plugin>` auto-injected); `surface.apply(miniCtx)` mounts with a miniCtx that only implements `effect(cb)` (the skin's only dependency). It does not rely on eval, so no CSP `unsafe-eval` pass is required — only same-origin script loading (as the page itself does when loading its plugin bundles).
54
+ - Failure semantics: when the bundle route 404s (skin not installed / `lib/client.js` not built) or the network fails, the script's error event fires, try-on reports a generic error and fully restores the active skin; no half skin is left between loading and restoring (the tryOn catch branch handles recovery).
55
+ - Exit restore: first run the skin's disposer (withdraws attribute/chrome/favicon/title/background entirely), then `invalidate(package)` + remove the style tag, then restore the active skin's visual snapshot verbatim. Official-default try-on = the same withdrawal prescription but without mounting any skin; exit restores identically.
56
+ - Active-skin detection: `window.__DSH_BOOT__.entries` only contains enabled entries, compared against the registry packages; no match means official default.
57
+ - One-click apply: host `/api/skin-center/apply` runs the embedded port of `dsh-skin use <name>` / `use official` (the port is the sole authority for the managed section and the symlink). Paths are `<harness-home>/cordis.patch.yml` and `<harness-home>/profiles/<profile>/node_modules` with the home/profile resolution described above. When the active skin is itself installed as a bundle — a real profile package dir whose own `cordis.patch.yml` inserts the same loader id, or a registry `bundleWired` skin — the home layer writes only the mutual-exclusion `disabled: true` rows and leaves the insert to the bundle patch; the bundled-carrier symlink layout keeps the home insert row. The DSH daemon surface ships its own config watcher (`watchUserPatches` + config-only HMR); after the patch is written it hot-loads in seconds without a restart; a browser refresh picks up the new boot graph (client plugin-graph row changes are not within `dsh-client-hmr` semantics).
58
+
59
+ ## Build (in-repo tsdown, no DSH checkout)
60
+
61
+ Like the skins, the skin center builds with the in-repo shared tsdown preset (`shared/tsdown.client.ts` handles CSS-Module injection and platform externalization; types come from the official NPM SDK devDependencies):
101
62
 
102
63
  ```sh
103
- # 1. 重新生成注册表(皮肤元数据变化后重跑;bundle 文本按需走 host 路由,无需重生成)
64
+ # 1. Regenerate the registry (re-run after skin metadata changes; bundle text rides the host route on demand, no regen needed)
104
65
  node scripts/skin-center-bundles
105
- # 皮肤 bundle 自身变化只需重建对应皮肤(tsdown),GUI 下次试穿即取到新文本
66
+ # A skin bundle change itself only needs that skin rebuilt (tsdown); the GUI picks up the new text on the next try-on
106
67
 
107
- # 2. 在仓库内构建
108
- cd ~/code/dsh-web-ui && export NPM_TOKEN='<token>' # 若仍使用私有 scope 认证
68
+ # 2. Build in the repo
69
+ cd ~/code/dsh-web-ui && export NPM_TOKEN='<token>' # if private-scope auth is still required
109
70
  pnpm --filter @linxin666/dsh-client-ui-skin-center run bundle
110
71
  ```
111
72
 
112
- ## 安装(个人环境接线,不在 checkout 提交)
73
+ ## Install (personal environment wiring, not committed to the checkout)
113
74
 
114
75
  ```sh
115
- # 1. profile symlink(与 qq98/blue-fantasy 同款)
76
+ # 1. profile symlink (same as qq98/blue-fantasy)
116
77
  ln -sfn ~/code/dsh-web-ui/packages/skins/skin-center \
117
78
  ~/.dsh/profiles/node_modules/@linxin666/dsh-client-ui-skin-center
118
79
 
119
- # 2. ~/.dsh/cordis.patch.yml 增加(放在 dsh-skin managed 段之外,勿动该段):
80
+ # 2. add to ~/.dsh/cordis.patch.yml (outside the dsh-skin managed section, do not touch that section):
120
81
  # - insert:
121
82
  # - id: ui-skin-center
122
83
  # name: '@linxin666/dsh-client-ui-skin-center'
123
84
 
124
- # 3. 配置 watcher 秒级热载入;刷新页面即在 插件配置 → Web UI 插件 组里看到皮肤中心卡片
85
+ # 3. the config watcher hot-loads in seconds; refresh the page to see the skin-center card in 插件配置 → Web UI 插件
125
86
  ```
126
87
 
127
- ## 试穿互斥的还原配方(try-on.ts)
88
+ ## Try-on mutual-exclusion restore prescription (try-on.ts)
128
89
 
129
- | 皮肤 | body 属性 | 额外处理 |
90
+ | Skin | body attribute | extra handling |
130
91
  | --- | --- | --- |
131
- | 全部 | 收回 `bodyAttr`(CSS 失活) | 快照/清空 body 背景内联样式(blue-fantasy 鲸鱼背景);摘除 body 直接子节点中非 `#root` 的 chrome(实测仅皮肤 chrome);中性化观察器防幽灵写回 |
132
- | xp | 同上 | 额外注入 neutralizer CSS 隐藏 sidebar footer 的 taskbar/开始按钮(其规则未按属性作用域) |
92
+ | All | withdraw `bodyAttr` (CSS deactivated) | snapshot/clear the body background inline style (blue-fantasy whale background); remove chrome among the body's direct child nodes that are not `#root` (measured: only skin chrome); neutralize observers to prevent ghost write-backs |
93
+ | xp | same | additionally inject neutralizer CSS to hide the sidebar footer's taskbar/start button (its rules are not scoped by attribute) |
133
94
 
134
- 退出试穿 = 试穿皮肤 disposer(真实代码路径)→ 模块 invalidate + 样式清理 → 激活皮肤快照原样恢复。
95
+ Exit try-on = try-on skin disposer (real code path) → module invalidate + style cleanup → restore the active skin snapshot verbatim.
135
96
 
136
- ## 验收对照(README 顶层契约)
97
+ ## Acceptance checklist (top-level README contract)
137
98
 
138
- - [x] 插件配置 → Web UI 插件 组里出现皮肤中心卡片,无 console 报错
139
- - [x] 列表含官方默认 + 全部皮肤,当前激活有标记
140
- - [x] 试穿真实生效(chrome/背景/标题/favicon),亮/暗正确;官方默认可试穿
141
- - [x] 退出完全还原;互斥(不出现两套标题栏)
142
- - [x] 一键应用:host API 执行 `dsh-skin use`,watcher 热载入,页面自动刷新生效(无重启);失败附命令兜底
143
- - [x] 回归:dsh-skin CLI(含 `use official`)、网页 Gallery、官方 GUI 不受影响
144
- - [x] 按需加载:冷启动不解析 ~700KB 内嵌 base64(`generated/skins.ts` 仅 5KB 元数据),试穿按需取 bundle;无 eval(CSP 无需 `unsafe-eval`)
145
- - [x] e2e 截图见 `docs/e2e/skin-center/`
99
+ - [x] The skin-center card appears in 插件配置 → Web UI 插件 without console errors
100
+ - [x] The list contains the official default plus all skins; the currently active one is marked
101
+ - [x] Try-on really takes effect (chrome/background/title/favicon); light/dark correct; the official default can be tried on
102
+ - [x] Exit fully restores; mutual exclusion (no two title bars)
103
+ - [x] One-click apply: the host API runs `dsh-skin use`, the watcher hot-loads, the page auto-refreshes (no restart); failure carries a command fallback
104
+ - [x] Regression: the dsh-skin CLI (incl. `use official`), the web gallery, and the official GUI are unaffected
105
+ - [x] On-demand loading: cold start does not parse the ~700KB embedded base64 (`generated/skins.ts` is only ~5KB of metadata); try-on fetches the bundle on demand; no eval (CSP needs no `unsafe-eval`)
106
+ - [x] e2e screenshots live in `docs/e2e/skin-center/`
package/README.zh.md ADDED
@@ -0,0 +1,106 @@
1
+ # Skin Center(GUI 内嵌皮肤中心)
2
+
3
+ [English](README.md) | 中文
4
+
5
+ `@linxin666/dsh-client-ui-skin-center`(cordis 插件 id `ui-skin-center`)把皮肤列表/试穿/应用内嵌进真实 dsh Web GUI 的插件配置页,作为「Web UI 插件」组里的一张卡片(设置 → 插件配置 → Web UI 插件 → 皮肤中心),与 task-board / pet / live-stats 等全家桶插件同一套槽位(`web-ui.plugin.item`),不占设置页一级导航。
6
+
7
+ - 列表:展示「官方默认」+ 仓库里全部皮肤(qq98 / ths / xp / blue-fantasy / dragon-heir / minecraft)的名称、tagline、强调色;当前激活的目标带 Active 标记。
8
+ - 试穿:点击「Try on」后按需加载该皮肤的 client bundle——host 路由 `/api/skin-center/bundle/<id>` 以同源 script 提供 `lib/client.js`(内核加载插件的同一机制),factory 注册到页面自己的 `window.__ModuleLoader__`,`window.__DSH_MODULES__.import` 物化(不是模拟器、不用 eval),chrome 立即生效;亮/暗切换走官方 theme 服务;「Exit try-on」完全还原——当前皮肤的样式、DOM、favicon、标题、body 内联样式全部恢复。「官方默认」也可试穿:点一下皮肤立即收回、回到官方外观预览。
9
+ - 互斥:试穿期间会按配方暂时收回当前激活皮肤的视觉写面(body 属性、背景内联样式、chrome 子节点、xp 的 footer taskbar),退出后原样恢复;同一时刻页面上只有一套皮肤。
10
+ - 应用:host 半区(`src/index.ts` + `src/routes.ts`)暴露 `/api/skin-center/apply` 与 `/api/skin-center/bundle/<id>`(按需提供皮肤 bundle),点击「Apply / 恢复默认」即在服务端执行内嵌的 `dsh-skin use` 进程内移植版(`src/skin-switch.ts`),写入 `<harness-home>/cordis.patch.yml` 后由 DSH 配置 watcher 秒级热载入,页面自动刷新生效——**无需重启 dsh web,无需复制命令,也不要求 PATH 上有 `dsh-skin` 二进制**。应用失败时错误提示里附带终端兜底命令。harness home 与 dsh 启动器一致:注入的 HOME 映射为 `<home>/.dsh`,否则优先使用去除首尾空白后非空的 `$DSH_HOME`(直接使用,不再追加后缀),最后回退到 `~/.dsh`。目标 profile 依次取:显式选项、`$DSH_SKIN_PROFILE`、`$DSH_PROFILE`、`process.cwd()` 直接位于 `<harness-home>/profiles/<name>` 下时的 `<name>`,最后 `web`。Windows 兼容性:同一套解析规则不依赖 `$HOME` 与固定路径,符号链接权限不足时 profile 链接回退为目录 junction。
11
+
12
+ ## 安装(官方 plugin bundle 方式)
13
+
14
+ 推荐先装皮肤全家桶聚合包 `@linxin666/dsh-skins` 一次到位(含全部皮肤与皮肤中心);只装本包时用下列 link 命令。
15
+
16
+ ```sh
17
+ # 装全部皮肤(推荐)
18
+ dsh plugin --profile web add @linxin666/dsh-skins
19
+ # 或单独装皮肤中心
20
+ dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center
21
+ # 从仓库安装(开发调试):dsh plugin --profile web add link:$(pwd)/packages/skins/skin-center
22
+ ```
23
+
24
+ `$(pwd)` 指克隆全家桶仓库后的目录。
25
+
26
+ skin-center 是符合 DSH 官方插件标准的自包含 bundle(`dsh.bundle.patch` 指向 `cordis.patch.yml`、`prepare` 用专用 tsdown 配置自包含构建,无项目引用、无类型检查),也可经 git 安装:`dsh plugin --profile web add github:<org>/dsh-web-ui#<sha>`(`prepare` 会原地构建 `lib/`)。
27
+
28
+ 本地 link 安装前需先在全家桶仓库内构建产物(`lib/` 被 git 忽略、不随仓库提交):`pnpm install && pnpm -r build` 后再 link 安装。
29
+
30
+ pnpm ≥10 安装 git 依赖前需先授权 `allowBuilds`(`prepare` 会原地构建),本地 link 安装则无此要求。
31
+
32
+ 需要皮肤插件们(qq98 / ths / xp / blue-fantasy)在宿主里也可解析时,skin-center 才能完整列出 / 试穿全部皮肤;skin-center 本身无互斥要求。
33
+
34
+ ## 目录结构
35
+
36
+ ```
37
+ skins/skin-center/
38
+ package.json / tsdown.config.ts / tsconfig.json # checkout 内构建所需的元数据
39
+ src/index.ts # host 侧:注册 /api/skin-center/* 路由
40
+ src/routes.ts # host 路由(代理 dsh-skin CLI)
41
+ src/invariant.ts # invariant 伴随插件(无断言)
42
+ src/client/index.ts # apply:注册 Web UI 插件组卡片 + body 作用域
43
+ src/client/SkinCenter.tsx # 卡片组件(官方默认 + 列表/试穿/亮暗/一键应用)
44
+ src/client/try-on.ts # 试穿引擎(真实 loader + 互斥还原,含官方试穿)
45
+ src/client/locales.ts # en/zh 文案
46
+ src/client/skin-center.module.css # 面板样式(--dsw-* token,随皮肤自适应)
47
+ src/client/generated/skins.ts # 生成:皮肤注册表(仅元数据,勿手改)
48
+ ```
49
+
50
+ ## 机制要点
51
+
52
+ - 皮肤枚举:`generated/skins.ts` 由 `scripts/skin-center-bundles` 生成(读 `skins/<name>/skin.json`,校验 `lib/client.js` 存在)。**只含元数据,不内嵌 bundle 文本**:冷启动不解析 ~700KB 的 base64 美术资源,且生成文件跨机器可复现(无构建机绝对路径)。
53
+ - 试穿加载:host 路由 `/api/skin-center/bundle/<id>` 按需提供 `lib/client.js`(同源 script,`<script>` 标签加载——与内核 `defaultLoadBundle` 同一机制),bundle 体调用 `window.__ModuleLoader__.load` 只注册 factory;`window.__DSH_MODULES__.import(package)` 物化模块(CSS `<style data-plugin>` 自动注入);`surface.apply(miniCtx)` 挂载,miniCtx 只实现 `effect(cb)`(皮肤唯一依赖)。不依赖 eval,因此不要求 CSP 放行 `unsafe-eval`——只要求同源 script 可加载(页面自身加载插件 bundle 亦然)。
54
+ - 失败语义:bundle 路由 404(皮肤未安装 / `lib/client.js` 未构建)或网络失败时,script 的 error 事件触发,试穿报通用错误并完整还原激活皮肤;加载与还原之间不会留下半套皮肤(tryOn 的 catch 分支负责恢复)。
55
+ - 退出还原:先跑皮肤的 disposer(属性/chrome/favicon/标题/背景全撤回),再 `invalidate(package)` + 删 style 标签,最后把激活皮肤的视觉快照原样恢复。官方默认试穿 = 同一套收回配方但不挂载任何皮肤,退出同样原样恢复。
56
+ - 激活皮肤检测:`window.__DSH_BOOT__.entries` 只含启用条目,与注册表 package 比对;无匹配即官方默认。
57
+ - 一键应用:host `/api/skin-center/apply` 执行内嵌的 `dsh-skin use <name>` / `use official` 移植版(该移植版是 managed 区段与 symlink 的唯一权威)。路径为 `<harness-home>/cordis.patch.yml` 与 `<harness-home>/profiles/<profile>/node_modules`,home/profile 按上文规则解析。当激活皮肤自身已作为 bundle 安装——profile 包目录是真实目录且其自身 `cordis.patch.yml` 插入了同一 loader id,或注册表标记 `bundleWired`——home 层只写互斥的 `disabled: true` 行,insert 留给 bundle patch;聚合包载体 symlink 布局仍保留 home 层 insert 行。DSH 长驻表面自带配置 watcher(`watchUserPatches` + config-only HMR),patch 写入后数秒热载入、无需重启;浏览器刷新页面取新 boot 图即生效(client 插件图行增删不在 `dsh-client-hmr` 语义内)。
58
+
59
+ ## 构建(仓库内 tsdown,无需 DSH checkout)
60
+
61
+ 皮肤中心与皮肤一样,用仓库内共享 tsdown 预设构建(`shared/tsdown.client.ts` 处理 CSS Modules 注入与平台外部化;类型来自官方 NPM SDK devDependencies):
62
+
63
+ ```sh
64
+ # 1. 重新生成注册表(皮肤元数据变化后重跑;bundle 文本按需走 host 路由,无需重生成)
65
+ node scripts/skin-center-bundles
66
+ # 皮肤 bundle 自身变化只需重建对应皮肤(tsdown),GUI 下次试穿即取到新文本
67
+
68
+ # 2. 在仓库内构建
69
+ cd ~/code/dsh-web-ui && export NPM_TOKEN='<token>' # 若仍使用私有 scope 认证
70
+ pnpm --filter @linxin666/dsh-client-ui-skin-center run bundle
71
+ ```
72
+
73
+ ## 安装(个人环境接线,不在 checkout 提交)
74
+
75
+ ```sh
76
+ # 1. profile symlink(与 qq98/blue-fantasy 同款)
77
+ ln -sfn ~/code/dsh-web-ui/packages/skins/skin-center \
78
+ ~/.dsh/profiles/node_modules/@linxin666/dsh-client-ui-skin-center
79
+
80
+ # 2. ~/.dsh/cordis.patch.yml 增加(放在 dsh-skin managed 段之外,勿动该段):
81
+ # - insert:
82
+ # - id: ui-skin-center
83
+ # name: '@linxin666/dsh-client-ui-skin-center'
84
+
85
+ # 3. 配置 watcher 秒级热载入;刷新页面即在 插件配置 → Web UI 插件 组里看到皮肤中心卡片
86
+ ```
87
+
88
+ ## 试穿互斥的还原配方(try-on.ts)
89
+
90
+ | 皮肤 | body 属性 | 额外处理 |
91
+ | --- | --- | --- |
92
+ | 全部 | 收回 `bodyAttr`(CSS 失活) | 快照/清空 body 背景内联样式(blue-fantasy 鲸鱼背景);摘除 body 直接子节点中非 `#root` 的 chrome(实测仅皮肤 chrome);中性化观察器防幽灵写回 |
93
+ | xp | 同上 | 额外注入 neutralizer CSS 隐藏 sidebar footer 的 taskbar/开始按钮(其规则未按属性作用域) |
94
+
95
+ 退出试穿 = 试穿皮肤 disposer(真实代码路径)→ 模块 invalidate + 样式清理 → 激活皮肤快照原样恢复。
96
+
97
+ ## 验收对照(README 顶层契约)
98
+
99
+ - [x] 插件配置 → Web UI 插件 组里出现皮肤中心卡片,无 console 报错
100
+ - [x] 列表含官方默认 + 全部皮肤,当前激活有标记
101
+ - [x] 试穿真实生效(chrome/背景/标题/favicon),亮/暗正确;官方默认可试穿
102
+ - [x] 退出完全还原;互斥(不出现两套标题栏)
103
+ - [x] 一键应用:host API 执行 `dsh-skin use`,watcher 热载入,页面自动刷新生效(无重启);失败附命令兜底
104
+ - [x] 回归:dsh-skin CLI(含 `use official`)、网页 Gallery、官方 GUI 不受影响
105
+ - [x] 按需加载:冷启动不解析 ~700KB 内嵌 base64(`generated/skins.ts` 仅 5KB 元数据),试穿按需取 bundle;无 eval(CSP 无需 `unsafe-eval`)
106
+ - [x] e2e 截图见 `docs/e2e/skin-center/`
package/lib/client.js CHANGED
@@ -464,8 +464,8 @@ window.__ModuleLoader__.load({
464
464
  }
465
465
  };
466
466
  //#endregion
467
- //#region \0dsh-css:/home/runner/work/dsh-web-ui/dsh-web-ui/packages/skins/skin-center/src/client/skin-center.module.css.mjs
468
- const css = "body[data-dsh-skin-center] .aIQHxa_pluginCard{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:8px;list-style:none;overflow:hidden}body[data-dsh-skin-center] .aIQHxa_cardHeader{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;padding:11px 14px;transition:background .12s;display:flex}body[data-dsh-skin-center] .aIQHxa_cardHeader:hover{background:var(--dsw-alias-bg-layer-1,#f1f5f9)}body[data-dsh-skin-center] .aIQHxa_cardHeader:active{background:var(--dsw-alias-bg-layer-3,#e6ecf4)}body[data-dsh-skin-center] .aIQHxa_cardHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .aIQHxa_headText{flex-direction:column;flex:1;gap:3px;min-width:0;display:flex}body[data-dsh-skin-center] .aIQHxa_pluginName{color:var(--dsw-alias-label-primary,#172a45);align-items:baseline;gap:8px;font-size:13.5px;font-weight:600;display:flex}body[data-dsh-skin-center] .aIQHxa_cardDescription{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.4}body[data-dsh-skin-center] .aIQHxa_chevron,body[data-dsh-skin-center] .aIQHxa_chevronOpen{color:var(--dsw-alias-label-secondary,#6b7280);flex:none;margin-left:10px;font-size:12px;transition:transform .12s}body[data-dsh-skin-center] .aIQHxa_chevronOpen{transform:rotate(180deg)}body[data-dsh-skin-center] .aIQHxa_cardBody{border-top:1px solid var(--dsw-alias-border-l1,#e2e8f0);flex-direction:column;gap:12px;padding:12px 14px 14px;display:flex}body[data-dsh-skin-center] .aIQHxa_head{flex-direction:column;gap:6px;display:flex}body[data-dsh-skin-center] .aIQHxa_titleBadge{color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;font-weight:500}body[data-dsh-skin-center] .aIQHxa_intro{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12.5px;line-height:1.55}body[data-dsh-skin-center] .aIQHxa_themeRow{align-items:center;gap:8px;margin-top:2px;display:flex}body[data-dsh-skin-center] .aIQHxa_themeLabel{color:var(--dsw-alias-label-secondary,#6b7280);margin-right:2px;font-size:12px}body[data-dsh-skin-center] .aIQHxa_themeButton{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:6px;padding:5px 10px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .aIQHxa_themeButton:hover{border-color:var(--dsw-alias-border-l4,#94a3b8)}body[data-dsh-skin-center] .aIQHxa_themeButton:active{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .aIQHxa_themeButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .aIQHxa_themeButtonActive{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .aIQHxa_list{flex-direction:column;gap:10px;display:flex}body[data-dsh-skin-center] .aIQHxa_card{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:10px;flex-direction:column;gap:8px;padding:12px 14px;display:flex}body[data-dsh-skin-center] .aIQHxa_cardHead{align-items:center;gap:10px;min-width:0;display:flex}body[data-dsh-skin-center] .aIQHxa_swatch{width:14px;height:14px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);border-radius:50%;flex:none}body[data-dsh-skin-center] .aIQHxa_cardName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:13.5px;font-weight:600;overflow:hidden}body[data-dsh-skin-center] .aIQHxa_cardTagline{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.45}body[data-dsh-skin-center] .aIQHxa_badge{letter-spacing:.02em;border-radius:999px;flex:none;min-width:0;margin-left:auto;padding:2px 8px;font-size:11px;font-weight:600}body[data-dsh-skin-center] .aIQHxa_badgeActive{color:var(--dsw-alias-state-success-primary,#0f6b3a);background:var(--dsw-alias-state-success-tertiary,#dcf3e5)}body[data-dsh-skin-center] .aIQHxa_badgeTrying{color:var(--dsw-alias-brand-primary,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e2edfc)}body[data-dsh-skin-center] .aIQHxa_actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}body[data-dsh-skin-center] .aIQHxa_button{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:7px;padding:6px 12px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .aIQHxa_button:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2b7cd9);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .aIQHxa_button:active:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .aIQHxa_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .aIQHxa_buttonPrimary{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-fill,#2b7cd9);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .aIQHxa_buttonPrimary:hover:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .aIQHxa_buttonPrimary:active:not(:disabled),body[data-dsh-skin-center] .aIQHxa_buttonPrimary:focus-visible:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8)}body[data-dsh-skin-center] .aIQHxa_buttonGhost{background:0 0;border-color:#0000}body[data-dsh-skin-center] .aIQHxa_button:disabled{opacity:.55;cursor:default}body[data-dsh-skin-center] .aIQHxa_error{color:var(--dsw-alias-state-error-primary,#b42318);font-size:12px}body[data-dsh-skin-center] .aIQHxa_backgroundRow{flex-direction:column;gap:6px;padding:8px 0;display:flex}body[data-dsh-skin-center] .aIQHxa_backgroundHead{align-items:center;gap:8px;display:flex}body[data-dsh-skin-center] .aIQHxa_backgroundLabel{color:var(--dsw-alias-label-primary,#172a45);font-size:12.5px;font-weight:600}body[data-dsh-skin-center] .aIQHxa_backgroundValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-brand-primary,#2b7cd9);flex:none;margin-left:auto;font-size:12px}body[data-dsh-skin-center] .aIQHxa_backgroundRange{background:var(--dsw-alias-bg-layer-3,#e2e8f0);-webkit-appearance:none;appearance:none;cursor:pointer;border-radius:999px;width:100%;height:4px;margin:0}body[data-dsh-skin-center] .aIQHxa_backgroundRange::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-brand-primary,#2b7cd9);width:14px;height:14px;box-shadow:0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);cursor:pointer;border-radius:50%}body[data-dsh-skin-center] .aIQHxa_backgroundRange::-moz-range-thumb{border:2px solid var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-brand-primary,#2b7cd9);width:12px;height:12px;box-shadow:0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);cursor:pointer;border-radius:50%}body[data-dsh-skin-center] .aIQHxa_backgroundRange:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .aIQHxa_backgroundHint{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.5}body[data-dsh-skin-center] .aIQHxa_backgroundHintMuted{color:var(--dsw-alias-label-tertiary,#9aa4b5);font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){body[data-dsh-skin-center] .aIQHxa_cardHeader,body[data-dsh-skin-center] .aIQHxa_themeButton,body[data-dsh-skin-center] .aIQHxa_button,body[data-dsh-skin-center] .aIQHxa_chevron,body[data-dsh-skin-center] .aIQHxa_chevronOpen{transition:none}}";
467
+ //#region \0dsh-css:packages/skins/skin-center/src/client/skin-center.module.css.mjs
468
+ const css = "body[data-dsh-skin-center] .eDzMgW_pluginCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}body[data-dsh-skin-center] .eDzMgW_pluginCard:hover{border-color:var(--dsw-alias-label-dimmed)}body[data-dsh-skin-center] .eDzMgW_pluginCardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}body[data-dsh-skin-center] .eDzMgW_cardHeader{appearance:none;width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}body[data-dsh-skin-center] .eDzMgW_cardHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}body[data-dsh-skin-center] .eDzMgW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}body[data-dsh-skin-center] .eDzMgW_pluginName{color:var(--dsw-alias-label-primary);align-items:baseline;gap:8px;font-size:15px;font-weight:600;line-height:1.4;display:flex}body[data-dsh-skin-center] .eDzMgW_cardDescription{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}body[data-dsh-skin-center] .eDzMgW_chevron,body[data-dsh-skin-center] .eDzMgW_chevronOpen{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}body[data-dsh-skin-center] .eDzMgW_chevronOpen{transform:rotate(180deg)}body[data-dsh-skin-center] .eDzMgW_cardBody{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:12px;margin:0 16px;padding:12px 0 8px;display:flex}body[data-dsh-skin-center] .eDzMgW_head{flex-direction:column;gap:6px;display:flex}body[data-dsh-skin-center] .eDzMgW_titleBadge{color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;font-weight:500}body[data-dsh-skin-center] .eDzMgW_intro{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12.5px;line-height:1.55}body[data-dsh-skin-center] .eDzMgW_themeRow{align-items:center;gap:8px;margin-top:2px;display:flex}body[data-dsh-skin-center] .eDzMgW_themeLabel{color:var(--dsw-alias-label-secondary,#6b7280);margin-right:2px;font-size:12px}body[data-dsh-skin-center] .eDzMgW_themeButton{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:6px;padding:5px 10px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .eDzMgW_themeButton:hover{border-color:var(--dsw-alias-border-l4,#94a3b8)}body[data-dsh-skin-center] .eDzMgW_themeButton:active{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .eDzMgW_themeButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .eDzMgW_themeButtonActive{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .eDzMgW_list{flex-direction:column;gap:10px;display:flex}body[data-dsh-skin-center] .eDzMgW_card{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:10px;flex-direction:column;gap:8px;padding:12px 14px;display:flex}body[data-dsh-skin-center] .eDzMgW_cardHead{align-items:center;gap:10px;min-width:0;display:flex}body[data-dsh-skin-center] .eDzMgW_swatch{width:14px;height:14px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);border-radius:50%;flex:none}body[data-dsh-skin-center] .eDzMgW_cardName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:13.5px;font-weight:600;overflow:hidden}body[data-dsh-skin-center] .eDzMgW_cardTagline{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.45}body[data-dsh-skin-center] .eDzMgW_badge{letter-spacing:.02em;border-radius:999px;flex:none;min-width:0;margin-left:auto;padding:2px 8px;font-size:11px;font-weight:600}body[data-dsh-skin-center] .eDzMgW_badgeActive{color:var(--dsw-alias-state-success-primary,#0f6b3a);background:var(--dsw-alias-state-success-tertiary,#dcf3e5)}body[data-dsh-skin-center] .eDzMgW_badgeTrying{color:var(--dsw-alias-brand-primary,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e2edfc)}body[data-dsh-skin-center] .eDzMgW_actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}body[data-dsh-skin-center] .eDzMgW_button{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:7px;padding:6px 12px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .eDzMgW_button:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2b7cd9);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .eDzMgW_button:active:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .eDzMgW_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .eDzMgW_buttonPrimary{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-fill,#2b7cd9);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .eDzMgW_buttonPrimary:hover:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .eDzMgW_buttonPrimary:active:not(:disabled),body[data-dsh-skin-center] .eDzMgW_buttonPrimary:focus-visible:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8)}body[data-dsh-skin-center] .eDzMgW_buttonGhost{background:0 0;border-color:#0000}body[data-dsh-skin-center] .eDzMgW_button:disabled{opacity:.55;cursor:default}body[data-dsh-skin-center] .eDzMgW_error{color:var(--dsw-alias-state-error-primary,#b42318);font-size:12px}body[data-dsh-skin-center] .eDzMgW_backgroundRow{flex-direction:column;gap:6px;padding:8px 0;display:flex}body[data-dsh-skin-center] .eDzMgW_backgroundHead{align-items:center;gap:8px;display:flex}body[data-dsh-skin-center] .eDzMgW_backgroundLabel{color:var(--dsw-alias-label-primary,#172a45);font-size:12.5px;font-weight:600}body[data-dsh-skin-center] .eDzMgW_backgroundValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-brand-primary,#2b7cd9);flex:none;margin-left:auto;font-size:12px}body[data-dsh-skin-center] .eDzMgW_backgroundRange{background:var(--dsw-alias-bg-layer-3,#e2e8f0);-webkit-appearance:none;appearance:none;cursor:pointer;border-radius:999px;width:100%;height:4px;margin:0}body[data-dsh-skin-center] .eDzMgW_backgroundRange::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-brand-primary,#2b7cd9);width:14px;height:14px;box-shadow:0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);cursor:pointer;border-radius:50%}body[data-dsh-skin-center] .eDzMgW_backgroundRange::-moz-range-thumb{border:2px solid var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-brand-primary,#2b7cd9);width:12px;height:12px;box-shadow:0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);cursor:pointer;border-radius:50%}body[data-dsh-skin-center] .eDzMgW_backgroundRange:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .eDzMgW_backgroundHint{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.5}body[data-dsh-skin-center] .eDzMgW_backgroundHintMuted{color:var(--dsw-alias-label-tertiary,#9aa4b5);font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){body[data-dsh-skin-center] .eDzMgW_pluginCard,body[data-dsh-skin-center] .eDzMgW_cardHeader,body[data-dsh-skin-center] .eDzMgW_themeButton,body[data-dsh-skin-center] .eDzMgW_button,body[data-dsh-skin-center] .eDzMgW_chevron,body[data-dsh-skin-center] .eDzMgW_chevronOpen{transition:none}}";
469
469
  const tagId = "@linxin666/dsh-client-ui-skin-center/skin-center.module.css";
470
470
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
471
471
  const tag = document.createElement("style");
@@ -475,42 +475,43 @@ window.__ModuleLoader__.load({
475
475
  document.head.appendChild(tag);
476
476
  }
477
477
  var skin_center_module_css_default = {
478
- "actions": "aIQHxa_actions",
479
- "backgroundHead": "aIQHxa_backgroundHead",
480
- "backgroundHint": "aIQHxa_backgroundHint",
481
- "backgroundHintMuted": "aIQHxa_backgroundHintMuted",
482
- "backgroundLabel": "aIQHxa_backgroundLabel",
483
- "backgroundRange": "aIQHxa_backgroundRange",
484
- "backgroundRow": "aIQHxa_backgroundRow",
485
- "backgroundValue": "aIQHxa_backgroundValue",
486
- "badge": "aIQHxa_badge",
487
- "badgeActive": "aIQHxa_badgeActive",
488
- "badgeTrying": "aIQHxa_badgeTrying",
489
- "button": "aIQHxa_button",
490
- "buttonGhost": "aIQHxa_buttonGhost",
491
- "buttonPrimary": "aIQHxa_buttonPrimary",
492
- "card": "aIQHxa_card",
493
- "cardBody": "aIQHxa_cardBody",
494
- "cardDescription": "aIQHxa_cardDescription",
495
- "cardHead": "aIQHxa_cardHead",
496
- "cardHeader": "aIQHxa_cardHeader",
497
- "cardName": "aIQHxa_cardName",
498
- "cardTagline": "aIQHxa_cardTagline",
499
- "chevron": "aIQHxa_chevron",
500
- "chevronOpen": "aIQHxa_chevronOpen",
501
- "error": "aIQHxa_error",
502
- "head": "aIQHxa_head",
503
- "headText": "aIQHxa_headText",
504
- "intro": "aIQHxa_intro",
505
- "list": "aIQHxa_list",
506
- "pluginCard": "aIQHxa_pluginCard",
507
- "pluginName": "aIQHxa_pluginName",
508
- "swatch": "aIQHxa_swatch",
509
- "themeButton": "aIQHxa_themeButton",
510
- "themeButtonActive": "aIQHxa_themeButtonActive",
511
- "themeLabel": "aIQHxa_themeLabel",
512
- "themeRow": "aIQHxa_themeRow",
513
- "titleBadge": "aIQHxa_titleBadge"
478
+ "actions": "eDzMgW_actions",
479
+ "backgroundHead": "eDzMgW_backgroundHead",
480
+ "backgroundHint": "eDzMgW_backgroundHint",
481
+ "backgroundHintMuted": "eDzMgW_backgroundHintMuted",
482
+ "backgroundLabel": "eDzMgW_backgroundLabel",
483
+ "backgroundRange": "eDzMgW_backgroundRange",
484
+ "backgroundRow": "eDzMgW_backgroundRow",
485
+ "backgroundValue": "eDzMgW_backgroundValue",
486
+ "badge": "eDzMgW_badge",
487
+ "badgeActive": "eDzMgW_badgeActive",
488
+ "badgeTrying": "eDzMgW_badgeTrying",
489
+ "button": "eDzMgW_button",
490
+ "buttonGhost": "eDzMgW_buttonGhost",
491
+ "buttonPrimary": "eDzMgW_buttonPrimary",
492
+ "card": "eDzMgW_card",
493
+ "cardBody": "eDzMgW_cardBody",
494
+ "cardDescription": "eDzMgW_cardDescription",
495
+ "cardHead": "eDzMgW_cardHead",
496
+ "cardHeader": "eDzMgW_cardHeader",
497
+ "cardName": "eDzMgW_cardName",
498
+ "cardTagline": "eDzMgW_cardTagline",
499
+ "chevron": "eDzMgW_chevron",
500
+ "chevronOpen": "eDzMgW_chevronOpen",
501
+ "error": "eDzMgW_error",
502
+ "head": "eDzMgW_head",
503
+ "headText": "eDzMgW_headText",
504
+ "intro": "eDzMgW_intro",
505
+ "list": "eDzMgW_list",
506
+ "pluginCard": "eDzMgW_pluginCard",
507
+ "pluginCardOpen": "eDzMgW_pluginCardOpen",
508
+ "pluginName": "eDzMgW_pluginName",
509
+ "swatch": "eDzMgW_swatch",
510
+ "themeButton": "eDzMgW_themeButton",
511
+ "themeButtonActive": "eDzMgW_themeButtonActive",
512
+ "themeLabel": "eDzMgW_themeLabel",
513
+ "themeRow": "eDzMgW_themeRow",
514
+ "titleBadge": "eDzMgW_titleBadge"
514
515
  };
515
516
  //#endregion
516
517
  //#region src/client/SkinCenter.tsx
@@ -546,6 +547,13 @@ window.__ModuleLoader__.load({
546
547
  const [tryingOfficial, setTryingOfficial] = (0, react.useState)(false);
547
548
  const [applying, setApplying] = (0, react.useState)(null);
548
549
  const [error, setError] = (0, react.useState)(null);
550
+ const mounted = (0, react.useRef)(false);
551
+ (0, react.useEffect)(() => {
552
+ mounted.current = true;
553
+ return () => {
554
+ mounted.current = false;
555
+ };
556
+ }, []);
549
557
  const tryOn = (entry) => {
550
558
  setError(null);
551
559
  controller.tryOn(entry).then(() => {
@@ -584,6 +592,10 @@ window.__ModuleLoader__.load({
584
592
  const expected = target === OFFICIAL ? "none" : target;
585
593
  let tries = 0;
586
594
  const tick = () => {
595
+ if (!mounted.current) {
596
+ resolve(false);
597
+ return;
598
+ }
587
599
  tries += 1;
588
600
  fetch("/api/skin-center/state").then(async (response) => {
589
601
  const payload = await response.json().catch(() => null);
@@ -591,10 +603,10 @@ window.__ModuleLoader__.load({
591
603
  resolve(true);
592
604
  return;
593
605
  }
594
- if (tries >= 20) resolve(false);
606
+ if (tries >= 20 || !mounted.current) resolve(false);
595
607
  else window.setTimeout(tick, 250);
596
608
  }).catch(() => {
597
- if (tries >= 20) resolve(false);
609
+ if (tries >= 20 || !mounted.current) resolve(false);
598
610
  else window.setTimeout(tick, 250);
599
611
  });
600
612
  };
@@ -618,6 +630,7 @@ window.__ModuleLoader__.load({
618
630
  if (!response.ok || payload?.ok !== true) throw new Error(payload?.error ?? `HTTP ${response.status}`);
619
631
  setApplying(null);
620
632
  confirmActive(target).then((confirmed) => {
633
+ if (!mounted.current) return;
621
634
  if (confirmed) window.location.reload();
622
635
  else {
623
636
  const command = target === OFFICIAL ? "dsh-skin use official" : `dsh-skin use ${target}`;
@@ -661,7 +674,7 @@ window.__ModuleLoader__.load({
661
674
  })]
662
675
  });
663
676
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
664
- className: skin_center_module_css_default.pluginCard,
677
+ className: open ? `${skin_center_module_css_default.pluginCard} ${skin_center_module_css_default.pluginCardOpen}` : skin_center_module_css_default.pluginCard,
665
678
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
666
679
  type: "button",
667
680
  className: skin_center_module_css_default.cardHeader,
@@ -683,9 +696,17 @@ window.__ModuleLoader__.load({
683
696
  title: t("cardDescription"),
684
697
  children: t("cardDescription")
685
698
  })]
686
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
687
- className: open ? skin_center_module_css_default.chevronOpen : skin_center_module_css_default.chevron,
688
- children: "▾"
699
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
700
+ width: "14",
701
+ height: "14",
702
+ viewBox: "0 0 14 14",
703
+ fill: "none",
704
+ xmlns: "http://www.w3.org/2000/svg",
705
+ className: open ? `${skin_center_module_css_default.chevron} ${skin_center_module_css_default.chevronOpen}` : skin_center_module_css_default.chevron,
706
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
707
+ d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
708
+ fill: "currentColor"
709
+ })
689
710
  })]
690
711
  }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
691
712
  className: skin_center_module_css_default.cardBody,