@linxin666/dsh-client-ui-skin-center 0.1.17 → 0.1.18

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.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: 780270d67eab83021ac6ab1260105b51279d240c
6
- README.zh.md: 937c72fad580b084d23b9c339c03e8af89c2f15b
5
+ README.md: 0f2055eff6d26f61407584e4e18f70e716a815c2
6
+ README.zh.md: 4747255cf51b02f43009eb8723d230f8e75c4990
package/README.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
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.
5
+ `@linxin666/dsh-client-ui-skin-center` (cordis plugin id `ui-skin-center`) puts the skin list / try-on / apply into the real dsh Web GUI as a first-level settings section (settings → 皮肤中心 / Skin Center), a sibling nav item of General / Models / Plugins / Agent presets and of the Web UI Plugins group and Pet sections. The card carries its own enable switch (off disables try-on, apply and the background controls).
6
6
 
7
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
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
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
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
11
 
12
+ - Background controls: a background-occlusion slider (0–100%) veils the backdrop behind the panels for skins that paint one (blue-fantasy / whale-song), plus two per-state Gaussian-blur sliders (0–20 px) — 空对话 (blur when the conversation is empty) and 有对话 (blur once it has content). The active blur is applied through a fixed `backdrop-filter` element behind the shell; 0 disables it entirely (no element, no GPU cost). These apply only to skins that paint a backdrop; the official default has none.
13
+
12
14
  ## Install (official plugin bundle)
13
15
 
14
16
  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.
@@ -39,7 +41,7 @@ skins/skin-center/
39
41
  src/index.ts # host side: registers /api/skin-center/* routes
40
42
  src/routes.ts # host routes (proxy to the dsh-skin CLI)
41
43
  src/invariant.ts # invariant companion plugin (no assertions)
42
- src/client/index.ts # apply: registers the Web UI plugin-group card + body scope
44
+ src/client/index.ts # apply: registers the first-level settings section + body scope
43
45
  src/client/SkinCenter.tsx # card component (official default + list/try-on/light-dark/one-click apply)
44
46
  src/client/try-on.ts # try-on engine (real loader + mutual-exclusion restore, incl. official try-on)
45
47
  src/client/locales.ts # en/zh copy
@@ -82,7 +84,7 @@ ln -sfn ~/code/dsh-web-ui/packages/skins/skin-center \
82
84
  # - id: ui-skin-center
83
85
  # name: '@linxin666/dsh-client-ui-skin-center'
84
86
 
85
- # 3. the config watcher hot-loads in seconds; refresh the page to see the skin-center card in 插件配置 → Web UI 插件
87
+ # 3. the config watcher hot-loads in seconds; refresh the page to see the skin-center section in 设置 → 皮肤中心
86
88
  ```
87
89
 
88
90
  ## Try-on mutual-exclusion restore prescription (try-on.ts)
@@ -96,7 +98,7 @@ Exit try-on = try-on skin disposer (real code path) → module invalidate + styl
96
98
 
97
99
  ## Acceptance checklist (top-level README contract)
98
100
 
99
- - [x] The skin-center card appears in 插件配置 → Web UI 插件 without console errors
101
+ - [x] The skin-center section appears in 设置 → 皮肤中心 without console errors
100
102
  - [x] The list contains the official default plus all skins; the currently active one is marked
101
103
  - [x] Try-on really takes effect (chrome/background/title/favicon); light/dark correct; the official default can be tried on
102
104
  - [x] Exit fully restores; mutual exclusion (no two title bars)
package/README.zh.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  [English](README.md) | 中文
4
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`),不占设置页一级导航。
5
+ `@linxin666/dsh-client-ui-skin-center`(cordis 插件 id `ui-skin-center`)把皮肤列表/试穿/应用放进真实 dsh Web GUI 的设置页,作为一级菜单项(设置 → 皮肤中心),与通用设置 / 模式 / 插件 / Agent 预设以及 Web UI 插件组、宠物同级。卡片自带启用开关(关闭后停用试穿、应用与背景控件)。
6
6
 
7
7
  - 列表:展示「官方默认」+ 仓库里全部皮肤(qq98 / ths / xp / blue-fantasy / dragon-heir / minecraft)的名称、tagline、强调色;当前激活的目标带 Active 标记。
8
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
9
  - 互斥:试穿期间会按配方暂时收回当前激活皮肤的视觉写面(body 属性、背景内联样式、chrome 子节点、xp 的 footer taskbar),退出后原样恢复;同一时刻页面上只有一套皮肤。
10
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
11
 
12
+ - 背景控制:背景遮挡滑块(0–100%)为带背景插画的皮肤(blue-fantasy / whale-song)背后的背景加遮罩;另有两条按对话状态区分的背景高斯模糊滑块(0–20 px)——「空对话」在对话为空时生效、「有对话」在出现内容后生效。生效的模糊通过 shell 背后的一个固定 `backdrop-filter` 元素施加;设为 0 即完全关闭(无元素、无 GPU 开销)。这些控件仅对带背景插画的皮肤可见有效;官方默认无背景图。
13
+
12
14
  ## 安装(官方 plugin bundle 方式)
13
15
 
14
16
  推荐先装皮肤全家桶聚合包 `@linxin666/dsh-skins` 一次到位(含全部皮肤与皮肤中心);只装本包时用下列 link 命令。
@@ -39,7 +41,7 @@ skins/skin-center/
39
41
  src/index.ts # host 侧:注册 /api/skin-center/* 路由
40
42
  src/routes.ts # host 路由(代理 dsh-skin CLI)
41
43
  src/invariant.ts # invariant 伴随插件(无断言)
42
- src/client/index.ts # apply:注册 Web UI 插件组卡片 + body 作用域
44
+ src/client/index.ts # apply:注册一级设置菜单项 + body 作用域
43
45
  src/client/SkinCenter.tsx # 卡片组件(官方默认 + 列表/试穿/亮暗/一键应用)
44
46
  src/client/try-on.ts # 试穿引擎(真实 loader + 互斥还原,含官方试穿)
45
47
  src/client/locales.ts # en/zh 文案
@@ -82,7 +84,7 @@ ln -sfn ~/code/dsh-web-ui/packages/skins/skin-center \
82
84
  # - id: ui-skin-center
83
85
  # name: '@linxin666/dsh-client-ui-skin-center'
84
86
 
85
- # 3. 配置 watcher 秒级热载入;刷新页面即在 插件配置 → Web UI 插件 组里看到皮肤中心卡片
87
+ # 3. 配置 watcher 秒级热载入;刷新页面即在 设置 → 皮肤中心 看到皮肤中心菜单项
86
88
  ```
87
89
 
88
90
  ## 试穿互斥的还原配方(try-on.ts)
@@ -96,7 +98,7 @@ ln -sfn ~/code/dsh-web-ui/packages/skins/skin-center \
96
98
 
97
99
  ## 验收对照(README 顶层契约)
98
100
 
99
- - [x] 插件配置 → Web UI 插件 组里出现皮肤中心卡片,无 console 报错
101
+ - [x] 设置 → 皮肤中心 出现皮肤中心菜单项,无 console 报错
100
102
  - [x] 列表含官方默认 + 全部皮肤,当前激活有标记
101
103
  - [x] 试穿真实生效(chrome/背景/标题/favicon),亮/暗正确;官方默认可试穿
102
104
  - [x] 退出完全还原;互斥(不出现两套标题栏)
package/lib/client.js CHANGED
@@ -583,7 +583,7 @@ window.__ModuleLoader__.load({
583
583
  };
584
584
  //#endregion
585
585
  //#region \0dsh-css:packages/skins/skin-center/src/client/skin-center.module.css.mjs
586
- 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}}";
586
+ const css = "body[data-dsh-skin-center] .eDzMgW_sectionList{margin:0;padding:0;list-style:none}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_cardHeaderStatic{align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}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_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_enableRow{flex-wrap:wrap;align-items:center;gap:8px;padding:8px 0;display:flex}body[data-dsh-skin-center] .eDzMgW_enableLabel{color:var(--dsw-alias-label-primary,#172a45);font-size:12.5px;font-weight:600}body[data-dsh-skin-center] .eDzMgW_enableHint{min-width:100%;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;margin:0;font-size:12px;line-height:1.5}body[data-dsh-skin-center] .eDzMgW_switch{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-3,#e2e8f0);cursor:pointer;border-radius:999px;flex:none;align-items:center;width:40px;height:22px;padding:2px;transition:background .12s,border-color .12s;display:inline-flex;position:relative}body[data-dsh-skin-center] .eDzMgW_switchOn{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-brand-primary,#2b7cd9)}body[data-dsh-skin-center] .eDzMgW_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .eDzMgW_switchThumb{background:var(--dsw-alias-label-primary-foreground,#fff);width:18px;height:18px;box-shadow:0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);border-radius:50%;transition:transform .12s;display:block;transform:translate(0)}body[data-dsh-skin-center] .eDzMgW_switchOn .eDzMgW_switchThumb{transform:translate(18px)}body[data-dsh-skin-center] .eDzMgW_offNote{color:var(--dsw-alias-label-secondary,#6b7280);margin:0;font-size:12.5px;line-height:1.5}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_themeButton,body[data-dsh-skin-center] .eDzMgW_button,body[data-dsh-skin-center] .eDzMgW_switch,body[data-dsh-skin-center] .eDzMgW_switchThumb{transition:none}}";
587
587
  const tagId = "@linxin666/dsh-client-ui-skin-center/skin-center.module.css";
588
588
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
589
589
  const tag = document.createElement("style");
@@ -611,20 +611,25 @@ window.__ModuleLoader__.load({
611
611
  "cardBody": "eDzMgW_cardBody",
612
612
  "cardDescription": "eDzMgW_cardDescription",
613
613
  "cardHead": "eDzMgW_cardHead",
614
- "cardHeader": "eDzMgW_cardHeader",
614
+ "cardHeaderStatic": "eDzMgW_cardHeaderStatic",
615
615
  "cardName": "eDzMgW_cardName",
616
616
  "cardTagline": "eDzMgW_cardTagline",
617
- "chevron": "eDzMgW_chevron",
618
- "chevronOpen": "eDzMgW_chevronOpen",
617
+ "enableHint": "eDzMgW_enableHint",
618
+ "enableLabel": "eDzMgW_enableLabel",
619
+ "enableRow": "eDzMgW_enableRow",
619
620
  "error": "eDzMgW_error",
620
621
  "head": "eDzMgW_head",
621
622
  "headText": "eDzMgW_headText",
622
623
  "intro": "eDzMgW_intro",
623
624
  "list": "eDzMgW_list",
625
+ "offNote": "eDzMgW_offNote",
624
626
  "pluginCard": "eDzMgW_pluginCard",
625
- "pluginCardOpen": "eDzMgW_pluginCardOpen",
626
627
  "pluginName": "eDzMgW_pluginName",
628
+ "sectionList": "eDzMgW_sectionList",
627
629
  "swatch": "eDzMgW_swatch",
630
+ "switch": "eDzMgW_switch",
631
+ "switchOn": "eDzMgW_switchOn",
632
+ "switchThumb": "eDzMgW_switchThumb",
628
633
  "themeButton": "eDzMgW_themeButton",
629
634
  "themeButtonActive": "eDzMgW_themeButtonActive",
630
635
  "themeLabel": "eDzMgW_themeLabel",
@@ -634,33 +639,35 @@ window.__ModuleLoader__.load({
634
639
  //#endregion
635
640
  //#region src/client/SkinCenter.tsx
636
641
  /**
637
- * The skin-center plugin card: one disclosure card inside the Web UI plugin
638
- * group (插件配置 → Web UI 插件), listing every installed skin plus the
639
- * official stock look. Live try-on executes the real bundle inside the GUI
640
- * (light/dark preview, full restore on exit); Apply is one click — the host
641
- * half runs `dsh-skin use` through /api/skin-center/apply, the config
642
- * watcher hot-reloads the patch, and the page reloads into the new skin.
643
- * Copy rides the standard `t` seat; the theme preview control drives the
644
- * official theme service (persisted, same as the Appearance row).
642
+ * The skin-center card: rendered as the content of a first-level settings
643
+ * section, listing every installed skin plus the official stock look. Live
644
+ * try-on executes the real bundle inside the GUI (light/dark preview, full
645
+ * restore on exit); Apply is one click — the host half runs `dsh-skin use`
646
+ * through /api/skin-center/apply, the config watcher hot-reloads the patch,
647
+ * and the page reloads into the new skin. Copy rides the standard `t` seat;
648
+ * the theme preview control drives the official theme service (persisted,
649
+ * same as the Appearance row).
645
650
  */
646
651
  /** The apply target of the official stock-look card. */
647
652
  const OFFICIAL = "official";
648
653
  /** Skin ids that read the background-scrim variable and paint a backdrop. */
649
654
  const BACKDROP_SKIN_IDS = /* @__PURE__ */ new Set(["blue-fantasy", "whale-song"]);
650
655
  /**
651
- * Render the skin-center card: a disclosure header naming the plugin, with
652
- * the skin list (official default + every installed skin; try-on / theme
653
- * preview / one-click apply) inside its body.
656
+ * Render the skin-center card: a static header naming the plugin, with the
657
+ * always-visible skin list (official default + every installed skin; try-on /
658
+ * theme preview / one-click apply) rendered below it.
654
659
  * @param props - card props.
655
660
  * @returns the plugin card.
656
661
  */
657
662
  function SkinCenter({ t, controller, theme, background }) {
658
663
  const snapshot = (0, react.useSyncExternalStore)(theme.subscribe, theme.getTheme);
664
+ const enabled = (0, react.useSyncExternalStore)(background.subscribe, background.enabled);
659
665
  const opacity = (0, react.useSyncExternalStore)(background.subscribe, background.opacity);
666
+ const blurEmpty = (0, react.useSyncExternalStore)(background.subscribe, background.blurEmpty);
667
+ const blurContent = (0, react.useSyncExternalStore)(background.subscribe, background.blurContent);
660
668
  const activePackage = activeSkinEntry()?.package;
661
669
  const activeId = activeSkinEntry()?.id;
662
670
  const backdropActive = activeId !== void 0 && BACKDROP_SKIN_IDS.has(activeId);
663
- const [open, setOpen] = (0, react.useState)(false);
664
671
  const [tryingId, setTryingId] = (0, react.useState)(null);
665
672
  const [tryingOfficial, setTryingOfficial] = (0, react.useState)(false);
666
673
  const [loadingId, setLoadingId] = (0, react.useState)(null);
@@ -848,16 +855,10 @@ window.__ModuleLoader__.load({
848
855
  })]
849
856
  });
850
857
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
851
- className: open ? `${skin_center_module_css_default.pluginCard} ${skin_center_module_css_default.pluginCardOpen}` : skin_center_module_css_default.pluginCard,
852
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
853
- type: "button",
854
- className: skin_center_module_css_default.cardHeader,
855
- "aria-expanded": open,
856
- "aria-label": `${t(open ? "collapse" : "expand")}: ${t("title")}`,
857
- onClick: () => {
858
- setOpen((current) => !current);
859
- },
860
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
858
+ className: skin_center_module_css_default.pluginCard,
859
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
860
+ className: skin_center_module_css_default.cardHeaderStatic,
861
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
861
862
  className: skin_center_module_css_default.headText,
862
863
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
863
864
  className: skin_center_module_css_default.pluginName,
@@ -870,21 +871,34 @@ window.__ModuleLoader__.load({
870
871
  title: t("cardDescription"),
871
872
  children: t("cardDescription")
872
873
  })]
873
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
874
- width: "14",
875
- height: "14",
876
- viewBox: "0 0 14 14",
877
- fill: "none",
878
- xmlns: "http://www.w3.org/2000/svg",
879
- className: open ? `${skin_center_module_css_default.chevron} ${skin_center_module_css_default.chevronOpen}` : skin_center_module_css_default.chevron,
880
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
881
- 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",
882
- fill: "currentColor"
883
- })
884
- })]
885
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
874
+ })
875
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
886
876
  className: skin_center_module_css_default.cardBody,
887
- children: [
877
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
878
+ className: skin_center_module_css_default.enableRow,
879
+ children: [
880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
881
+ className: skin_center_module_css_default.enableLabel,
882
+ title: t("enabled"),
883
+ children: t("enabled")
884
+ }),
885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
886
+ type: "button",
887
+ role: "switch",
888
+ "aria-checked": enabled,
889
+ "aria-label": t("enabled"),
890
+ className: enabled ? skin_center_module_css_default.switch + " " + skin_center_module_css_default.switchOn : skin_center_module_css_default.switch,
891
+ onClick: () => {
892
+ background.setEnabled(!enabled);
893
+ },
894
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: skin_center_module_css_default.switchThumb })
895
+ }),
896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
897
+ className: skin_center_module_css_default.enableHint,
898
+ children: t("enabledHint")
899
+ })
900
+ ]
901
+ }), enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
888
902
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
889
903
  className: skin_center_module_css_default.head,
890
904
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -951,6 +965,65 @@ window.__ModuleLoader__.load({
951
965
  })
952
966
  ]
953
967
  }),
968
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
969
+ className: skin_center_module_css_default.backgroundRow,
970
+ children: [
971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
972
+ className: skin_center_module_css_default.backgroundHead,
973
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
974
+ className: skin_center_module_css_default.backgroundLabel,
975
+ children: t("backgroundBlurEmpty")
976
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
977
+ className: skin_center_module_css_default.backgroundValue,
978
+ "aria-hidden": "true",
979
+ children: [blurEmpty, "px"]
980
+ })]
981
+ }),
982
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
983
+ id: "skin-center-background-blur-empty",
984
+ className: skin_center_module_css_default.backgroundRange,
985
+ type: "range",
986
+ min: "0",
987
+ max: "20",
988
+ step: "1",
989
+ value: blurEmpty,
990
+ "aria-valuetext": `${blurEmpty}px`,
991
+ "aria-label": t("backgroundBlurEmpty"),
992
+ onChange: (event) => {
993
+ background.setBlurEmpty(Number(event.target.value));
994
+ }
995
+ }),
996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
997
+ className: skin_center_module_css_default.backgroundHead,
998
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
999
+ className: skin_center_module_css_default.backgroundLabel,
1000
+ children: t("backgroundBlurContent")
1001
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1002
+ className: skin_center_module_css_default.backgroundValue,
1003
+ "aria-hidden": "true",
1004
+ children: [blurContent, "px"]
1005
+ })]
1006
+ }),
1007
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1008
+ id: "skin-center-background-blur-content",
1009
+ className: skin_center_module_css_default.backgroundRange,
1010
+ type: "range",
1011
+ min: "0",
1012
+ max: "20",
1013
+ step: "1",
1014
+ value: blurContent,
1015
+ "aria-valuetext": `${blurContent}px`,
1016
+ "aria-label": t("backgroundBlurContent"),
1017
+ onChange: (event) => {
1018
+ background.setBlurContent(Number(event.target.value));
1019
+ }
1020
+ }),
1021
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1022
+ className: backdropActive ? skin_center_module_css_default.backgroundHint : skin_center_module_css_default.backgroundHintMuted,
1023
+ children: backdropActive ? t("backgroundBlurHint") : t("backgroundBlurInert")
1024
+ })
1025
+ ]
1026
+ }),
954
1027
  error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
955
1028
  className: skin_center_module_css_default.error,
956
1029
  children: error
@@ -1041,8 +1114,25 @@ window.__ModuleLoader__.load({
1041
1114
  }, entry.id);
1042
1115
  })]
1043
1116
  })
1044
- ]
1045
- }) : null]
1117
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1118
+ className: skin_center_module_css_default.offNote,
1119
+ role: "status",
1120
+ children: t("offNote")
1121
+ })]
1122
+ })]
1123
+ });
1124
+ }
1125
+ /** Render the skin-center card as a first-level settings page. */
1126
+ function SkinCenterSection(props) {
1127
+ const { t, controller, theme, background } = props;
1128
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1129
+ className: skin_center_module_css_default.sectionList,
1130
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkinCenter, {
1131
+ t,
1132
+ controller,
1133
+ theme,
1134
+ background
1135
+ })
1046
1136
  });
1047
1137
  }
1048
1138
  //#endregion
@@ -1051,31 +1141,85 @@ window.__ModuleLoader__.load({
1051
1141
  const SKIN_BACKGROUND_NS = "skin-background";
1052
1142
  /** Field of the background value inside the namespace section. */
1053
1143
  const OPACITY_FIELD = "backgroundOpacity";
1144
+ /** Field of the empty-conversation backdrop blur inside the namespace section. */
1145
+ const BLUR_EMPTY_FIELD = "backgroundBlurEmpty";
1146
+ /** Field of the with-content backdrop blur inside the namespace section. */
1147
+ const BLUR_CONTENT_FIELD = "backgroundBlurContent";
1054
1148
  /** CSS custom property written to document.body and read by backdrop skins. */
1055
1149
  const SCRIM_VAR = "--dsw-skin-scrim";
1056
1150
  /**
1057
- * Own the skin-background scope: read the latest occlusion, apply it to the
1058
- * body CSS variable instantly, and persist changes through the settings scope.
1151
+ * Selector for a conversation message row inside the shell's center column.
1152
+ * The `data-pane="conversation"` attribute is stamped by the dsh-web-ui-all
1153
+ * compat shim on the center column; the _userRow / _compactionRow /
1154
+ * _contextRow / _turnErrorRow suffixes are the official shell's CSS-module
1155
+ * hashed message-row classes (hash prefix varies, suffix is stable). Stable
1156
+ * like the repo's compat shim, not hash-dependent.
1157
+ */
1158
+ const CONVERSATION_CONTENT_SELECTOR = [
1159
+ "[data-pane=\"conversation\"] [class*=\"_userRow\"]",
1160
+ "[data-pane=\"conversation\"] [class*=\"_compactionRow\"]",
1161
+ "[data-pane=\"conversation\"] [class*=\"_contextRow\"]",
1162
+ "[data-pane=\"conversation\"] [class*=\"_turnErrorRow\"]"
1163
+ ].join(", ");
1164
+ /**
1165
+ * Own the skin-background scope: read the latest occlusion + blur strengths,
1166
+ * apply them to the body instantly, and persist changes through the settings
1167
+ * scope.
1059
1168
  */
1060
1169
  var BackgroundController = class {
1061
- value = 0;
1170
+ enabledValue = true;
1171
+ opacityValue = 0;
1172
+ blurEmptyValue = 0;
1173
+ blurContentValue = 0;
1062
1174
  listeners = /* @__PURE__ */ new Set();
1063
1175
  scope;
1176
+ /** The fixed backdrop-filter element, present only while active blur > 0. */
1177
+ blurElement = null;
1178
+ /** The body MutationObserver, installed lazily once a blur is active. */
1179
+ observer = null;
1180
+ /** Pending requestAnimationFrame id for a coalesced recheck. */
1181
+ rafId = null;
1182
+ /** Guard: after dispose no scheduled work may reinstall anything. */
1183
+ disposed = false;
1064
1184
  /**
1065
1185
  * @param scope - the bound skin-background settings scope.
1066
1186
  */
1067
1187
  constructor(scope) {
1068
1188
  this.scope = scope;
1069
- this.value = this.read();
1070
- this.apply();
1189
+ this.enabledValue = this.readEnabled();
1190
+ this.opacityValue = this.readOpacity();
1191
+ this.blurEmptyValue = this.readBlur(BLUR_EMPTY_FIELD);
1192
+ this.blurContentValue = this.readBlur(BLUR_CONTENT_FIELD);
1193
+ this.applyOcclusion();
1194
+ this.syncBlur();
1071
1195
  scope.subscribe(() => {
1072
- this.value = this.read();
1073
- this.apply();
1196
+ this.enabledValue = this.readEnabled();
1197
+ this.opacityValue = this.readOpacity();
1198
+ this.blurEmptyValue = this.readBlur(BLUR_EMPTY_FIELD);
1199
+ this.blurContentValue = this.readBlur(BLUR_CONTENT_FIELD);
1200
+ this.applyOcclusion();
1201
+ this.syncBlur();
1074
1202
  this.publish();
1075
1203
  });
1076
1204
  }
1205
+ enabled() {
1206
+ return this.enabledValue;
1207
+ }
1208
+ setEnabled(value) {
1209
+ this.enabledValue = value;
1210
+ this.applyOcclusion();
1211
+ this.syncBlur();
1212
+ this.publish();
1213
+ this.scope.set("enabled", value);
1214
+ }
1077
1215
  opacity() {
1078
- return this.value;
1216
+ return this.opacityValue;
1217
+ }
1218
+ blurEmpty() {
1219
+ return this.blurEmptyValue;
1220
+ }
1221
+ blurContent() {
1222
+ return this.blurContentValue;
1079
1223
  }
1080
1224
  subscribe(listener) {
1081
1225
  this.listeners.add(listener);
@@ -1085,20 +1229,133 @@ window.__ModuleLoader__.load({
1085
1229
  }
1086
1230
  set(opacity) {
1087
1231
  const clamped = Math.max(0, Math.min(100, Math.round(opacity)));
1088
- this.value = clamped;
1089
- this.apply();
1232
+ this.opacityValue = clamped;
1233
+ this.applyOcclusion();
1090
1234
  this.publish();
1091
1235
  this.scope.set(OPACITY_FIELD, clamped);
1092
1236
  }
1093
- /** The effective section value, clamped 0-100, defaulting to 0. */
1094
- read() {
1237
+ setBlurEmpty(value) {
1238
+ const clamped = this.clampBlur(value);
1239
+ this.blurEmptyValue = clamped;
1240
+ this.ensureObserver();
1241
+ this.syncBlur();
1242
+ this.publish();
1243
+ this.scope.set(BLUR_EMPTY_FIELD, clamped);
1244
+ }
1245
+ setBlurContent(value) {
1246
+ const clamped = this.clampBlur(value);
1247
+ this.blurContentValue = clamped;
1248
+ this.ensureObserver();
1249
+ this.syncBlur();
1250
+ this.publish();
1251
+ this.scope.set(BLUR_CONTENT_FIELD, clamped);
1252
+ }
1253
+ dispose() {
1254
+ this.disposed = true;
1255
+ if (this.rafId !== null) {
1256
+ cancelAnimationFrame(this.rafId);
1257
+ this.rafId = null;
1258
+ }
1259
+ this.removeBlurElement();
1260
+ if (this.observer !== null) {
1261
+ this.observer.disconnect();
1262
+ this.observer = null;
1263
+ }
1264
+ }
1265
+ /** The effective master-switch section value, defaulting to true when absent. */
1266
+ readEnabled() {
1267
+ const raw = this.scope.getSnapshot().value?.enabled;
1268
+ return typeof raw !== "boolean" ? true : raw;
1269
+ }
1270
+ /** The effective occlusion section value, clamped 0-100, defaulting to 0. */
1271
+ readOpacity() {
1095
1272
  const raw = this.scope.getSnapshot().value?.backgroundOpacity;
1096
1273
  if (typeof raw !== "number" || !Number.isFinite(raw)) return 0;
1097
1274
  return Math.max(0, Math.min(100, raw));
1098
1275
  }
1276
+ /** The effective blur section value for one field, clamped 0-20, defaulting to 0. */
1277
+ readBlur(field) {
1278
+ const raw = this.scope.getSnapshot().value?.[field];
1279
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return 0;
1280
+ return this.clampBlur(raw);
1281
+ }
1282
+ clampBlur(value) {
1283
+ return Math.max(0, Math.min(20, Math.round(value)));
1284
+ }
1099
1285
  /** Write the current occlusion onto the body CSS variable (0..1 alpha). */
1100
- apply() {
1101
- document.body.style.setProperty(SCRIM_VAR, String(this.value / 100));
1286
+ applyOcclusion() {
1287
+ if (!this.enabledValue) {
1288
+ document.body.style.removeProperty(SCRIM_VAR);
1289
+ return;
1290
+ }
1291
+ document.body.style.setProperty(SCRIM_VAR, String(this.opacityValue / 100));
1292
+ }
1293
+ /**
1294
+ * Apply the active blur: empty or with-content strength depending on the
1295
+ * conversation state. A value > 0 ensures the fixed blur element exists
1296
+ * with the matching backdrop-filter; 0 removes it.
1297
+ */
1298
+ syncBlur() {
1299
+ if (this.disposed) return;
1300
+ if (!this.enabledValue) {
1301
+ this.removeBlurElement();
1302
+ return;
1303
+ }
1304
+ this.ensureObserver();
1305
+ const active = this.hasConversationContent() ? this.blurContentValue : this.blurEmptyValue;
1306
+ if (active > 0) this.ensureBlurElement(active);
1307
+ else this.removeBlurElement();
1308
+ }
1309
+ /** True when the conversation pane hosts at least one message row. */
1310
+ hasConversationContent() {
1311
+ return document.querySelector(CONVERSATION_CONTENT_SELECTOR) !== null;
1312
+ }
1313
+ /** Create (if needed) and size the fixed backdrop-filter element. */
1314
+ ensureBlurElement(active) {
1315
+ if (this.blurElement === null) {
1316
+ const element = document.createElement("div");
1317
+ element.style.position = "fixed";
1318
+ element.style.inset = "0";
1319
+ element.style.zIndex = "-1";
1320
+ element.style.pointerEvents = "none";
1321
+ element.setAttribute("aria-hidden", "true");
1322
+ this.blurElement = element;
1323
+ document.body.appendChild(element);
1324
+ }
1325
+ const blur = "blur(" + active + "px)";
1326
+ this.blurElement.style.backdropFilter = blur;
1327
+ this.blurElement.style.setProperty("-webkit-backdrop-filter", blur);
1328
+ }
1329
+ /** Remove the fixed blur element, if present. */
1330
+ removeBlurElement() {
1331
+ if (this.blurElement === null) return;
1332
+ this.blurElement.remove();
1333
+ this.blurElement = null;
1334
+ }
1335
+ /**
1336
+ * Install the MutationObserver on document.body only when either blur
1337
+ * field is active, so a fully-disabled blur never pays the observation
1338
+ * cost. Runs lazily on the first non-zero set.
1339
+ */
1340
+ ensureObserver() {
1341
+ if (this.disposed || this.observer !== null) return;
1342
+ if (this.blurEmptyValue <= 0 && this.blurContentValue <= 0) return;
1343
+ this.observer = new MutationObserver(() => this.scheduleRecheck());
1344
+ this.observer.observe(document.body, {
1345
+ childList: true,
1346
+ subtree: true,
1347
+ attributes: true,
1348
+ attributeFilter: ["class"]
1349
+ });
1350
+ }
1351
+ /** Coalesce burst mutations into one rAF-delayed recheck. */
1352
+ scheduleRecheck() {
1353
+ if (this.disposed || this.rafId !== null) return;
1354
+ this.rafId = requestAnimationFrame(() => {
1355
+ this.rafId = null;
1356
+ if (this.disposed) return;
1357
+ this.syncBlur();
1358
+ });
1102
1359
  }
1103
1360
  publish() {
1104
1361
  for (const listener of this.listeners) listener();
@@ -1109,8 +1366,9 @@ window.__ModuleLoader__.load({
1109
1366
  const en = {
1110
1367
  title: "Skin Center",
1111
1368
  cardDescription: "Try on any installed skin live in the GUI — exit restores instantly, applying persists in one click.",
1112
- expand: "Expand",
1113
- collapse: "Collapse",
1369
+ enabled: "Enable skin center",
1370
+ enabledHint: "When off, try-on, apply and background controls are disabled; turn it back on to resume.",
1371
+ offNote: "The skin center is turned off.",
1114
1372
  intro: "Try on any skin live — it takes effect instantly, exit restores the current look. Apply persists it across restarts.",
1115
1373
  official: "Official default",
1116
1374
  officialTagline: "The stock DSH look with no skin applied.",
@@ -1129,14 +1387,19 @@ window.__ModuleLoader__.load({
1129
1387
  themeDark: "Dark",
1130
1388
  tryOnError: "Try-on failed — see console",
1131
1389
  backgroundOpacity: "Background occlusion",
1390
+ backgroundBlurEmpty: "Blur when empty",
1391
+ backgroundBlurContent: "Blur with content",
1392
+ backgroundBlurHint: "Applies a separate Gaussian blur to the backdrop for the empty conversation and the conversation with content; 0 disables.",
1393
+ backgroundBlurInert: "Visible only with skins that paint a backdrop; the official default has none.",
1132
1394
  backgroundHint: "Instantly veils the backdrop behind the panels — higher values obscure the art to help you focus.",
1133
1395
  backgroundHintInert: "Only applies to skins that paint a backdrop (Blue Fantasy / Whale Song). Applies to the official default automatically once such a skin is active."
1134
1396
  };
1135
1397
  const zh = {
1136
1398
  title: "皮肤中心",
1137
1399
  cardDescription: "在 GUI 内即时试穿任意皮肤,退出即完全还原;应用一键完成并自动刷新。",
1138
- expand: "展开",
1139
- collapse: "收起",
1400
+ enabled: "启用皮肤中心",
1401
+ enabledHint: "关闭后停用试穿、应用与背景控件,重新打开即恢复。",
1402
+ offNote: "皮肤中心已关闭。",
1140
1403
  intro: "任意皮肤可即时试穿,退出即完全还原;「应用」一键持久化,页面自动刷新生效。",
1141
1404
  official: "官方默认",
1142
1405
  officialTagline: "还原 DSH 官方默认外观,不应用任何皮肤。",
@@ -1155,6 +1418,10 @@ window.__ModuleLoader__.load({
1155
1418
  themeDark: "暗色",
1156
1419
  tryOnError: "试穿失败,详见控制台",
1157
1420
  backgroundOpacity: "背景遮挡",
1421
+ backgroundBlurEmpty: "空对话背景模糊",
1422
+ backgroundBlurContent: "有对话背景模糊",
1423
+ backgroundBlurHint: "对话为空与有内容时分别应用不同的背景高斯模糊强度,0 为关闭。",
1424
+ backgroundBlurInert: "仅对带背景图插画的皮肤可见;官方默认无背景图。",
1158
1425
  backgroundHint: "即时为面板背后的背景加遮罩——数值越高越能弱化插画,帮你集中注意力。",
1159
1426
  backgroundHintInert: "仅对带背景图插画的皮肤(蓝色幻想 / 鲸吟)生效;官方默认无背景图,该滑块对这些皮肤自动生效。"
1160
1427
  };
@@ -1173,7 +1440,7 @@ window.__ModuleLoader__.load({
1173
1440
  ];
1174
1441
  /**
1175
1442
  * Register the skin-center dictionaries, the body scope attribute, and the
1176
- * Skins plugin card inside the Web UI plugin group.
1443
+ * Skin Center as a first-level settings section.
1177
1444
  * @param ctx - client root context.
1178
1445
  */
1179
1446
  function apply(ctx) {
@@ -1190,6 +1457,7 @@ window.__ModuleLoader__.load({
1190
1457
  const theme = ctx.get("theme");
1191
1458
  const controller = new TryOnController();
1192
1459
  const background = new BackgroundController((ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: SKIN_BACKGROUND_NS }));
1460
+ ctx.effect(() => () => background.dispose(), "ui-skin-center: background dispose");
1193
1461
  const injected = () => ({
1194
1462
  controller,
1195
1463
  theme: {
@@ -1198,18 +1466,26 @@ window.__ModuleLoader__.load({
1198
1466
  setTheme: (id) => theme.setTheme(id)
1199
1467
  },
1200
1468
  background: {
1469
+ enabled: () => background.enabled(),
1470
+ setEnabled: (value) => background.setEnabled(value),
1201
1471
  opacity: () => background.opacity(),
1472
+ blurEmpty: () => background.blurEmpty(),
1473
+ blurContent: () => background.blurContent(),
1202
1474
  subscribe: (listener) => background.subscribe(listener),
1203
- set: (opacity) => background.set(opacity)
1475
+ set: (opacity) => background.set(opacity),
1476
+ setBlurEmpty: (value) => background.setBlurEmpty(value),
1477
+ setBlurContent: (value) => background.setBlurContent(value),
1478
+ dispose: () => background.dispose()
1204
1479
  }
1205
1480
  });
1206
- ctx.slots.inject("web-ui.plugin.item", () => ctx.slots.register({
1207
- name: "web-ui.plugin.item",
1208
- id: "skins",
1209
- order: 110,
1210
- locale: NS,
1481
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1482
+ name: "settings.section",
1483
+ id: "skin-center",
1484
+ order: 120,
1485
+ label: () => ctx.locale.bind("skinCenter")("title"),
1486
+ locale: "skinCenter",
1211
1487
  inject: injected
1212
- }, SkinCenter));
1488
+ }, SkinCenterSection));
1213
1489
  }
1214
1490
  //#endregion
1215
1491
  exports.NS = NS;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["css"],"sources":["../../../packages/skins/skin-center/src/client/generated/skins.ts","../../../packages/skins/skin-center/src/client/manifest.ts","../../../packages/skins/skin-center/src/client/try-on.ts","../../../packages/skins/skin-center/src/client/SkinCenter.tsx","../../../packages/skins/skin-center/src/client/background.ts","../../../packages/skins/skin-center/src/client/locales.ts","../../../packages/skins/skin-center/src/client/index.ts"],"sourcesContent":["/**\n * AUTO-GENERATED by scripts/skin-center-bundles — do not edit by hand.\n * Skin registry for the GUI skin center: gallery metadata only. Client\n * bundle text is NOT embedded here — try-on fetches each skin's\n * prebuilt lib/client.js on demand from the host route\n * /api/skin-center/bundle/<id> (a same-origin script executed by the\n * page's own module loader; no eval, and the GUI never parses the\n * embedded art base64 at startup).\n * Regenerate with `node scripts/skin-center-bundles`.\n */\n\n/** One registry entry: gallery metadata for the skin center card. */\nexport interface SkinCenterEntry {\n /** Skin id (packages/skins/<id>/). */\n id: string\n /** Display name (zh). */\n name: string\n /** Display name (en). */\n nameEn: string\n /** One-line tagline. */\n tagline: string\n /** Accent color for the swatch. */\n accent: string\n /** Author credit. */\n author?: string\n /** Longer description. */\n description?: string\n /** Tag list. */\n tags?: string[]\n /** Body attribute that scopes the skin styles. */\n bodyAttr: string\n /** Plugin package name (the boot-graph entry id when active). */\n package: string\n /** Gallery sort order (packages/skins/<name>/skin.json). */\n order?: number\n}\n\n/** Every skin, ordered by packages/skins/<name>/skin.json `order`. */\nexport const SKIN_CENTER_ENTRIES: readonly SkinCenterEntry[] = [\n {\"id\":\"blue-fantasy\",\"name\":\"蓝色幻想\",\"nameEn\":\"Blue Fantasy\",\"author\":\"powerdog996(DreamSkin 社区)· dsh-web-ui 适配\",\"tagline\":\"鲸鱼插画背景 · periwinkle 靛蓝调色板 · 半透明面板\",\"description\":\"DreamSkin「DeepSeek-鲸鱼娘」Codex 桌面主题的 dsh 适配:鲸鱼插画背景垫在半透明面板之下,遮罩随亮/暗主题实时切换,periwinkle 靛蓝色调重映射到全部 dsh token。\",\"tags\":[\"dreamskin\",\"whale\",\"indigo\",\"art\",\"translucent\"],\"accent\":\"#4a5fa8\",\"bodyAttr\":\"data-dsh-blue-fantasy\",\"package\":\"@linxin666/dsh-client-ui-skin-blue-fantasy\",\"order\":1},\n\n {\"id\":\"whale-song\",\"name\":\"鲸吟\",\"nameEn\":\"Whale Song\",\"author\":\"dsh-web-ui\",\"tagline\":\"深海鲸语女神背景 · 冰蓝海洋调色板 · 金色细线点缀\",\"description\":\"《鲸吟》— 深海鲸语女神主题:无文字纯氛围背景画(蓝发女神与鲸群居左、冰蓝星座网格与金线点缀、右侧大量留白)垫在半透明面板之下,遮罩随亮/暗主题实时切换,冰蓝/浅青/深海军蓝/钴蓝冷色体系重映射到全部 dsh token,暗色变体为深海夜航调。\",\"tags\":[\"whale\",\"ocean\",\"ice-blue\",\"goddess\",\"art\",\"translucent\"],\"accent\":\"#4d8fd4\",\"bodyAttr\":\"data-dsh-whale-song\",\"package\":\"@linxin666/dsh-client-ui-skin-whale-song\",\"order\":2},\n\n {\"id\":\"harbor\",\"name\":\"夕港\",\"nameEn\":\"Harbor\",\"author\":\"moeblack\",\"tagline\":\"暮光蓝港 · 日落橙辉 · 半透明夜色面板\",\"description\":\"《夕港》黄昏港口主题:动漫少女黄昏港口背景(暮光蓝天空渐入日落橙)垫在半透明面板之下,遮罩随亮/暗主题实时切换,深暮蓝 #141a2e 底与日落橙 #ff9d5c 主色重映射到 dsh token,亮色是薄暮纱、暗色是深海夜航纱,同一幅画两种读法。\",\"tags\":[\"harbor\",\"dusk\",\"twilight\",\"sunset\",\"amber\",\"art\",\"translucent\"],\"accent\":\"#ff9d5c\",\"bodyAttr\":\"data-dsh-harbor\",\"package\":\"@linxin666/dsh-client-ui-skin-harbor\",\"order\":3},\n\n {\"id\":\"qq98\",\"name\":\"QQ2008 怀旧版\",\"nameEn\":\"QQ2008 Retro\",\"author\":\"dsh-web-ui\",\"tagline\":\"水晶蓝桌面 · 玻璃深蓝标题栏 · 戴围巾企鹅\",\"description\":\"dsh web ui 家族收录的第一个皮肤:QQ2008 水晶蓝年代。深蓝渐变桌面、玻璃质感标题栏、浅蓝状态栏和圆角高光控件,配一只戴围巾的企鹅。\",\"tags\":[\"retro\",\"qq\",\"2008\",\"crystal-blue\",\"nostalgia\"],\"accent\":\"#2b7cd9\",\"bodyAttr\":\"data-dsh-retro\",\"package\":\"@linxin666/dsh-client-ui-skin-qq98\",\"order\":4},\n\n {\"id\":\"ths\",\"name\":\"同花顺风格\",\"nameEn\":\"Tonghuashun Trading\",\"author\":\"dsh-web-ui\",\"tagline\":\"品牌红标题栏 · 实时行情状态栏 · 灰蓝数据终端\",\"description\":\"同花顺风格炒股主题:品牌红标题栏带上证指数行情签,状态栏红涨绿跌,自选股风格的侧边栏和交易终端面板,写代码也像盯盘。\",\"tags\":[\"stock\",\"trading\",\"terminal\",\"red\"],\"accent\":\"#e60012\",\"bodyAttr\":\"data-dsh-ths\",\"package\":\"@linxin666/dsh-client-ui-skin-ths\",\"order\":5},\n\n {\"id\":\"xp\",\"name\":\"Windows XP (Luna)\",\"nameEn\":\"Windows XP Luna\",\"author\":\"dsh-web-ui\",\"tagline\":\"Luna 蓝窗口条 · 绿色开始按钮 · Bliss 蓝天桌面\",\"description\":\"Windows XP (Luna) 复古主题:蓝色渐变窗口条带窗口按钮、米色状态栏(大写/数字/滚动指示灯)、侧边栏任务栏上的绿色「开始」按钮、资源管理器风格树行和 Bliss 蓝天桌面,全局直角。\",\"tags\":[\"retro\",\"xp\",\"luna\",\"windows\",\"start-button\"],\"accent\":\"#316ac5\",\"bodyAttr\":\"data-dsh-xp\",\"package\":\"@linxin666/dsh-client-ui-skin-xp\",\"order\":6},\n\n {\"id\":\"dragon-heir\",\"name\":\"龙的传人\",\"nameEn\":\"Dragon Heir\",\"author\":\"dsh-web-ui\",\"tagline\":\"不屈龙魂 · 万里长城双主题 · 朱砂龙印\",\"description\":\"龙的传人 — 一面是不屈龙魂(墨龙穿云、朱砂印章、不屈锋芒),一面是万里长城(青黛山色、金晖镀墙、苍茫暮色)。亮暗主题各自配一幅画与一枚龙印 favicon,面板半透明磨砂,让画透出来。\",\"tags\":[\"dragon\",\"loong\",\"chinese\",\"ink-wash\",\"great-wall\",\"dual-theme\"],\"accent\":\"#c3272b\",\"bodyAttr\":\"data-dsh-dragon-heir\",\"package\":\"@linxin666/dsh-client-ui-skin-dragon-heir\",\"order\":7},\n\n {\"id\":\"minecraft\",\"name\":\"Minecraft 方块世界\",\"nameEn\":\"Minecraft Voxel\",\"author\":\"dsh-web-ui\",\"tagline\":\"动态全景天空盒 · 方块按钮 · 告示牌输入框\",\"description\":\"复刻《我的世界》主界面氛围的方块皮肤:程序化绘制的像素全景天空盒(方块山、像素云、方块树、草方块地面)在身后缓慢旋转,界面浮在石板上;按钮还原 MC 菜单按钮(灰石板、悬停变黄、按下下沉),输入框做成带钉子的木告示牌。\",\"tags\":[\"minecraft\",\"voxel\",\"pixel\",\"game\",\"panorama\",\"skybox\"],\"accent\":\"#7cbd4b\",\"bodyAttr\":\"data-dsh-minecraft\",\"package\":\"@linxin666/dsh-client-ui-skin-minecraft\",\"order\":8},\n\n {\"id\":\"trading\",\"name\":\"交易终端\",\"nameEn\":\"Trading Terminal\",\"author\":\"dsh-web-ui\",\"tagline\":\"实时行情跑马灯 · 长桥港美股行情 · 红涨绿跌交易终端\",\"description\":\"结合 dsh-fun-ticker 行情跑马灯与 dsh-longbridge 港美股行情的炒股皮肤:顶栏滚动 A股/港股/美股/指数/加密/外汇报价(装 fun-ticker 后跟随你的自选列表),状态栏展示长桥行情快照与 A股/港股/美股交易时段,写代码也像盯盘。\",\"tags\":[\"stock\",\"trading\",\"ticker\",\"live\",\"terminal\",\"longbridge\"],\"accent\":\"#f23645\",\"bodyAttr\":\"data-dsh-trading\",\"package\":\"@linxin666/dsh-client-ui-skin-trading\",\"order\":9},\n\n {\"id\":\"miku\",\"name\":\"初音未来 · 电子歌姬\",\"nameEn\":\"Hatsune Miku\",\"author\":\"涂山苏苏\",\"tagline\":\"蓝紫双马尾 · 01 编号 · 音符波形 · 电子歌姬主题\",\"description\":\"以世界第一的虚拟歌姬初音未来为灵感的主题皮肤:蓝紫洋红渐变贯穿全局,音符与声波曲线点缀在半透明面板之间,标题栏与状态栏带有 01 编号徽标与音乐波形,半透明毛玻璃面板透出背景图——沉浸式电子歌姬氛围。\",\"tags\":[\"miku\",\"vocaloid\",\"blue\",\"music\",\"idol\",\"waveform\"],\"accent\":\"#2e9bff\",\"bodyAttr\":\"data-dsh-miku\",\"package\":\"@linxin666/dsh-client-ui-skin-miku\",\"order\":10},\n\n]\n","/**\n * Boot-manifest readiness checks for the one-click apply flow.\n *\n * The host half writes the skin patch synchronously, but the web app's boot\n * graph (the `window.__DSH_BOOT__` JSON inside the served HTML) is\n * regenerated asynchronously by the config watcher. A page reloaded right\n * after the patch write can therefore boot into the previous skin. These\n * helpers let the frontend poll the served document until the manifest\n * actually reflects the target before reloading.\n * @module @linxin666/dsh-client-ui-skin-center/manifest\n */\n\n/** Bundle URL pattern of any skin entry in the boot manifest. */\nconst SKIN_BUNDLE_URL = /\\/plugins\\/@linxin666\\/dsh-client-ui-skin-(?!center)[a-z0-9-]+\\/client\\.js/\n\n/**\n * Whether a served GUI document's boot manifest enables the given skin.\n * A `null` target means the stock look: no skin bundle URL may be present\n * (the skin-center plugin's own bundle always loads and is excluded).\n * @param documentHtml - the served GUI document (contains the boot JSON).\n * @param target - skin id, or `null` for the stock look.\n * @returns whether the manifest already enables the target.\n */\nexport function manifestHasSkin(documentHtml: string, target: string | null): boolean {\n if (target === null) return !SKIN_BUNDLE_URL.test(documentHtml)\n return documentHtml.includes(`/plugins/@linxin666/dsh-client-ui-skin-${target}/client.js`)\n}\n","/**\n * Try-on engine for the in-GUI skin center.\n *\n * A skin's client bundle is executed through the REAL module system, not a\n * shim and not eval: the host route `/api/skin-center/bundle/<id>` serves\n * the skin's prebuilt `lib/client.js` as a same-origin script (mirroring\n * the kernel's own defaultLoadBundle — see dsh-client-modules), and its\n * body calls `window.__ModuleLoader__.load({id, factory})`, which only\n * REGISTERS the factory. `window.__DSH_MODULES__.import(package)` (the\n * kernel's ClientModuleSystem, contract C5/C6) then materializes it — which\n * auto-injects the skin's CSS `<style data-plugin>` tag — and\n * `surface.apply(miniCtx)` mounts the skin exactly as the fiber system\n * would, returning a full disposer. That makes try-on and its teardown the\n * real code paths, with no CSP `unsafe-eval` dependence and no startup\n * cost: the ~700KB of embedded art base64 is only parsed when a skin is\n * actually tried on.\n *\n * Mutual exclusion: the GUI never hosts two skins at once. The currently\n * ACTIVE skin is owned by its own cordis fiber (its disposer is not\n * reachable), so try-on retracts the active skin's visual writes by recipe:\n * remove its body attribute (its stylesheet goes inert), clear the\n * body-level backdrop inline styles (blue-fantasy's whale art), detach only\n * known skin chrome body children (title/status bars marked `data-skin-chrome`\n * or carrying the skin's body attribute, leaving other plugins' portals and\n * toasts in place), and neutralize known global-rule leaks (xp's sidebar\n * taskbar/start). Everything is snapshotted and restored on exit in original\n * order. The active skin's own fiber is never touched, so exiting try-on\n * returns the page to exactly the pre-try-on state.\n *\n * A ghost MutationObserver may survive retraction (blue-fantasy re-writes\n * its backdrop on theme flips), so during try-on a neutralizing observer\n * re-clears the backdrop props whenever `data-ds-dark-theme` changes.\n */\n\nimport { SKIN_CENTER_ENTRIES, type SkinCenterEntry } from './generated/skins.ts'\n\n/** Body-level backdrop properties skins may write inline (blue-fantasy). */\nconst BACKDROP_PROPS = [\n 'background-image',\n 'background-position',\n 'background-size',\n 'background-attachment',\n 'background-repeat',\n] as const\n\n/**\n * Per-skin neutralization CSS: rules that hide visual leaks whose styles\n * are NOT scoped under the skin's body attribute (they live on app elements\n * the skin touches, so detaching chrome cannot remove them). Matched by\n * css-module class substring, which is stable across rebuilds.\n */\nconst NEUTRALIZE_CSS: Record<string, string> = {\n // xp styles its start button + taskbar strip in the sidebar footer with\n // top-level (unscoped) rules; its apply() removes them on dispose, but\n // during try-on its ghost observer would re-add them, so hide instead.\n xp: [\n `[data-pane='sidebar'] [class*='xpTaskbar']{background:transparent!important;border-top:none!important;box-shadow:none!important}`,\n `[data-pane='sidebar'] [class*='xpStart']{display:none!important}`,\n ].join(''),\n}\n\n/** The window surfaces the boot protocol installs (manifest.ts contract). */\ninterface SkinCenterWindow {\n __DSH_BOOT__?: { entries?: Array<{ id: string }> }\n __DSH_MODULES__?: {\n import(specifier: string): Promise<unknown>\n invalidate(id: string): void\n }\n}\n\n/** Host base path of the skin bundle route (registered by src/routes.ts). */\nconst BUNDLE_ROUTE = '/api/skin-center/bundle'\n\n/**\n * Execute one skin's client bundle as a real same-origin script, mirroring\n * the kernel's own defaultLoadBundle (dsh-client-modules): the script body\n * calls `window.__ModuleLoader__.load({id, factory})`, which only registers\n * the factory — materialization is the caller's separate `import` step. No\n * eval: try-on works under any CSP that allows same-origin scripts (the\n * shell itself loads plugin bundles this way), and a failed fetch rejects\n * so the caller can restore the active skin instead of leaving it retracted.\n * @param url - same-origin bundle URL.\n * @returns a promise resolving once the script executed.\n */\nfunction loadBundleScript(url: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const el = document.createElement('script')\n el.async = true\n el.src = url\n el.addEventListener('load', () => {\n el.remove()\n resolve()\n }, { once: true })\n el.addEventListener('error', () => {\n el.remove()\n reject(new Error(`skin-center: bundle script ${url} failed to load`))\n }, { once: true })\n document.head.append(el)\n })\n}\n\n/** Read the page's composed boot-graph entry ids (only enabled plugins appear). */\nfunction bootEntryIds(): string[] {\n const boot = (window as SkinCenterWindow).__DSH_BOOT__\n return boot?.entries?.map(entry => entry.id) ?? []\n}\n\n/** The skin package currently ACTIVE in the boot graph, if it is one of ours. */\nexport function activeSkinEntry(): SkinCenterEntry | undefined {\n const ids = new Set(bootEntryIds())\n return SKIN_CENTER_ENTRIES.find(entry => ids.has(entry.package))\n}\n\n/**\n * Whether a direct body child is skin chrome owned by `skin`: marked with the\n * `data-skin-chrome` marker (minecraft/dragon-heir) or carrying the skin's\n * scoping body attribute. Everything else — other plugins' portals, toasts and\n * overlays appended to body — is left alone.\n */\nfunction isSkinChrome(el: Element, skin: SkinCenterEntry | null): boolean {\n if (el.hasAttribute('data-skin-chrome')) return true\n return skin !== null && el.hasAttribute(skin.bodyAttr)\n}\n\n/** Minimal ctx the skins' apply() needs: cordis effect lifecycle only. */\ninterface MiniCtx {\n effect(callback: () => () => void, label?: string): () => void\n /**\n * Service reads are answered with undefined: try-on is a pure-DOM stage,\n * so optional service access (e.g. ths reading the connection handle)\n * degrades to its fallback instead of throwing mid-apply.\n */\n get(key: string): unknown\n /** Hidden handle for the controller: run the disposers in reverse order. */\n __disposeAll(): void\n}\n\nfunction miniCtx(): MiniCtx {\n const disposers: Array<() => void> = []\n return {\n effect(callback) {\n disposers.push(callback())\n return () => {}\n },\n get() {\n return undefined\n },\n __disposeAll(): void {\n for (const dispose of disposers.reverse()) dispose()\n },\n }\n}\n\n/** Snapshot of the active skin's visual writes, restored on try-on exit. */\ninterface ActiveVisuals {\n skin: SkinCenterEntry | null\n /** Attribute value before retraction (null = attribute absent). */\n bodyAttr: string | null\n /** body inline style before retraction (null = none). */\n bodyStyle: string | null\n /** Skin chrome elements detached from body, re-inserted at their anchors. */\n detached: Array<{ el: HTMLElement; anchor: Node | null }>\n /** Neutralizes ghost backdrop writes while a try-on is live. */\n clearObserver: MutationObserver | null\n /** Hides global-rule leaks of the active skin (xp taskbar). */\n neutralizeStyle: HTMLStyleElement | null\n}\n\n/**\n * One live try-on session: owns the tried-on skin's disposer plus the\n * captured active-skin visuals, and restores everything on exit.\n */\nexport class TryOnController {\n private session: {\n /** The tried-on skin, or null when trying on the official stock look. */\n entry: SkinCenterEntry | null\n dispose: () => void\n active: ActiveVisuals\n } | null = null\n\n /**\n * Generation counter. A newer try-on or exit increments it, so an in-flight\n * `tryOn` (awaiting the real bundle load) can detect it was superseded and\n * drop only what it mounted instead of clobbering the newer session.\n */\n private epoch = 0\n\n /**\n * One package can be requested again before its first script load finishes\n * (for example A -> B -> A). Share that materialization so two script tags\n * never race to register the same module factory.\n */\n private readonly pendingModules = new Map<string, Promise<(ctx: unknown) => unknown>>()\n\n /**\n * Package selected by the newest async request. A superseded request for the\n * same package must not invalidate the module/style now owned by that newer\n * request when their shared load settles.\n */\n private requestedPackage: string | null = null\n\n /**\n * Loads one skin's client bundle so its factory registers on the page's\n * `__ModuleLoader__`. Defaults to a same-origin script tag from the host\n * route `/api/skin-center/bundle/<id>`; tests inject a stub.\n */\n private readonly loadBundle: (entry: SkinCenterEntry) => Promise<void>\n\n constructor(options: { loadBundle?: (entry: SkinCenterEntry) => Promise<void> } = {}) {\n this.loadBundle = options.loadBundle ?? (entry => loadBundleScript(`${BUNDLE_ROUTE}/${encodeURIComponent(entry.id)}`))\n }\n /** The skin currently being tried on, if any. */\n get trying(): SkinCenterEntry | null {\n return this.session?.entry ?? null\n }\n\n /** Whether the official stock look (no skin) is being tried on. */\n get tryingOfficial(): boolean {\n return this.session !== null && this.session.entry === null\n }\n\n /**\n * Start trying on `entry` (replaces any live session).\n *\n * When another skin is already being tried on, keep it mounted while the\n * next bundle loads. Once the target is ready, tear down the old preview and\n * mount the new one against the SAME captured active-skin snapshot. This\n * avoids the expensive preview -> active -> preview round trip and prevents\n * a flash of the active skin between consecutive try-ons.\n * @returns whether this request mounted the target (false when superseded).\n */\n async tryOn(entry: SkinCenterEntry): Promise<boolean> {\n if (entry.package === activeSkinEntry()?.package) return false\n const epoch = ++this.epoch\n this.requestedPackage = entry.package\n\n let apply: (ctx: unknown) => unknown\n try {\n // Keep either the active skin or the current preview visible while the\n // target script downloads, parses and materializes its module.\n apply = await this.loadModuleOnce(entry)\n } catch (error) {\n if (this.shouldCleanupRequest(entry, epoch)) this.cleanupModule(entry)\n throw error\n }\n if (epoch !== this.epoch) {\n if (this.shouldCleanupRequest(entry, epoch)) this.cleanupModule(entry)\n return false\n }\n\n const previous = this.session\n let active: ActiveVisuals\n if (previous === null) {\n active = this.captureAndRetractActive()\n } else {\n // Transfer ownership of the original active-skin snapshot directly to\n // the next preview; restoring and immediately retracting it is wasted\n // DOM work and is the visible pause reported during chained try-ons.\n this.session = null\n previous.dispose()\n if (previous.entry !== null) this.cleanupModule(previous.entry)\n active = previous.active\n }\n\n let dispose: () => void\n try {\n dispose = this.applyLoaded(entry, apply)\n } catch (error) {\n if (epoch === this.epoch) this.restoreActive(active)\n throw error\n }\n this.session = { entry, dispose, active }\n return true\n }\n\n /**\n * Try on the official stock look: retract the active skin's visual writes\n * (same recipe as a skin try-on) and mount nothing. Exiting restores the\n * active skin exactly like any other try-on session.\n */\n tryOnOfficial(): void {\n if (activeSkinEntry() === null) return\n this.epoch += 1\n this.requestedPackage = null\n const previous = this.session\n if (previous !== null) {\n // A live preview already owns the captured active visuals. Dispose only\n // that preview and reuse the snapshot instead of restoring and retracting\n // the active skin in two consecutive layout-heavy operations.\n this.session = null\n previous.dispose()\n if (previous.entry !== null) this.cleanupModule(previous.entry)\n this.session = { entry: null, dispose: () => {}, active: previous.active }\n return\n }\n const active: ActiveVisuals = this.captureAndRetractActive()\n this.session = { entry: null, dispose: () => {}, active }\n }\n\n /** Exit the live session: dispose the tried-on skin, then restore the active skin. */\n exit(): void {\n // Also invalidate an in-flight initial/chained load. The visible session\n // may already be null by the time a user exits, but its late completion\n // must never remount a skin after cancellation.\n this.epoch += 1\n this.requestedPackage = null\n const session = this.session\n if (session === null) return\n this.session = null\n session.dispose()\n if (session.entry !== null) this.cleanupModule(session.entry)\n this.restoreActive(session.active)\n }\n\n /** Share one materialization while repeated requests for a package overlap. */\n private loadModuleOnce(entry: SkinCenterEntry): Promise<(ctx: unknown) => unknown> {\n const existing = this.pendingModules.get(entry.package)\n if (existing !== undefined) return existing\n\n const pending = this.loadModule(entry)\n this.pendingModules.set(entry.package, pending)\n void pending.then(\n () => {\n if (this.pendingModules.get(entry.package) === pending) this.pendingModules.delete(entry.package)\n },\n () => {\n if (this.pendingModules.get(entry.package) === pending) this.pendingModules.delete(entry.package)\n },\n )\n return pending\n }\n\n /** Whether this request still owns cleanup of the package module/style. */\n private shouldCleanupRequest(entry: SkinCenterEntry, epoch: number): boolean {\n return epoch === this.epoch || this.requestedPackage !== entry.package\n }\n\n /** Execute + materialize the target skin through the real loader. */\n private async loadModule(entry: SkinCenterEntry): Promise<(ctx: unknown) => unknown> {\n const modules = (window as SkinCenterWindow).__DSH_MODULES__\n if (modules === undefined) throw new Error('skin-center: window.__DSH_MODULES__ missing')\n // This try-on session owns the module record for the package for its\n // whole life (a try-on of the ACTIVE skin is rejected above, and the GUI\n // never hosts two skins at once). Drop any factory a crashed earlier\n // session left behind so the script below registers a fresh one — the\n // duplicate-registration retry dance is unnecessary with the real\n // loader, whose load() throws on a duplicate id.\n modules.invalidate(entry.package)\n await this.loadBundle(entry)\n const surface = await modules.import(entry.package)\n const apply = (surface as { apply?: (ctx: unknown) => unknown }).apply\n if (typeof apply !== 'function') {\n throw new Error(`skin-center: \"${entry.package}\" client bundle exports no apply`)\n }\n return apply\n }\n\n /** Apply a module that has already been loaded while the active skin was visible. */\n private applyLoaded(entry: SkinCenterEntry, apply: (ctx: unknown) => unknown): () => void {\n const ctx = miniCtx()\n try {\n apply(ctx)\n } catch (error) {\n // A skin that throws mid-apply leaves partial writes behind (body\n // attribute, chrome, style tag) and never registers its disposer —\n // the tryOn catch only restores the ACTIVE skin. Roll the residue back\n // here so a crashed try-on can never bleed into the next one.\n this.cleanupModule(entry)\n document.body.removeAttribute(entry.bodyAttr)\n for (const el of [...document.body.children] as HTMLElement[]) {\n if (isSkinChrome(el, entry)) el.remove()\n }\n throw error\n }\n return ctx.__disposeAll\n }\n\n /** Drop the tried-on module record + its injected style tag. */\n private cleanupModule(entry: SkinCenterEntry): void {\n const modules = (window as SkinCenterWindow).__DSH_MODULES__\n modules?.invalidate(entry.package)\n for (const el of document.querySelectorAll(`style[data-plugin=${JSON.stringify(entry.package)}]`)) {\n el.remove()\n }\n }\n\n /**\n * Snapshot the active skin's visual writes and retract them so the tried-on\n * skin can take over the whole surface.\n */\n private captureAndRetractActive(): ActiveVisuals {\n const skin = activeSkinEntry() ?? null\n const body = document.body\n const bodyAttr = skin === null ? null : body.getAttribute(skin.bodyAttr)\n if (skin !== null && bodyAttr !== null) body.removeAttribute(skin.bodyAttr)\n\n const bodyStyle = body.getAttribute('style')\n for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop)\n\n // Detach only known skin chrome (marker/bodyAttr), leaving other\n // plugins' portals/toasts in place; remember each element's next sibling\n // so restore re-inserts in the original order.\n const children = [...body.children] as HTMLElement[]\n const chrome = new Set<HTMLElement>()\n for (const el of children) {\n if (el.id !== 'root' && isSkinChrome(el, skin)) chrome.add(el)\n }\n const detached: Array<{ el: HTMLElement; anchor: Node | null }> = []\n for (let i = 0; i < children.length; i++) {\n const el = children[i]\n if (!chrome.has(el)) continue\n let anchor: Node | null = null\n for (let j = i + 1; j < children.length; j++) {\n if (!chrome.has(children[j])) { anchor = children[j]; break }\n }\n detached.push({ el, anchor })\n }\n for (const { el } of detached) el.remove()\n\n // Neutralize a surviving ghost observer (blue-fantasy's backdrop\n // re-writer) across theme flips during the try-on.\n const clearObserver = new MutationObserver(() => {\n for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop)\n })\n clearObserver.observe(body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })\n\n const neutralizeCss = skin === null ? undefined : NEUTRALIZE_CSS[skin.id]\n const neutralizeStyle = neutralizeCss === undefined ? null : this.injectStyle(neutralizeCss)\n\n return { skin, bodyAttr, bodyStyle, detached, clearObserver, neutralizeStyle }\n }\n\n /** Restore the active skin's captured visual state. */\n private restoreActive(active: ActiveVisuals): void {\n const body = document.body\n if (active.skin !== null && active.bodyAttr !== null) {\n body.setAttribute(active.skin.bodyAttr, active.bodyAttr)\n }\n if (active.bodyStyle !== null) {\n body.setAttribute('style', active.bodyStyle)\n } else {\n body.removeAttribute('style')\n }\n for (const { el, anchor } of active.detached) {\n body.insertBefore(el, anchor !== null && anchor.parentNode === body ? anchor : null)\n }\n active.clearObserver?.disconnect()\n active.neutralizeStyle?.remove()\n }\n\n private injectStyle(css: string): HTMLStyleElement {\n const tag = document.createElement('style')\n tag.dataset.skinCenterNeutralize = ''\n tag.textContent = css\n document.head.append(tag)\n return tag\n }\n}\n","/**\n * The skin-center plugin card: one disclosure card inside the Web UI plugin\n * group (插件配置 → Web UI 插件), listing every installed skin plus the\n * official stock look. Live try-on executes the real bundle inside the GUI\n * (light/dark preview, full restore on exit); Apply is one click — the host\n * half runs `dsh-skin use` through /api/skin-center/apply, the config\n * watcher hot-reloads the patch, and the page reloads into the new skin.\n * Copy rides the standard `t` seat; the theme preview control drives the\n * official theme service (persisted, same as the Appearance row).\n */\nimport { useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'\nimport { SKIN_CENTER_ENTRIES, type SkinCenterEntry } from './generated/skins.ts'\nimport { manifestHasSkin } from './manifest.ts'\nimport type { SkinBackgroundHandle } from './background.ts'\nimport { activeSkinEntry, TryOnController } from './try-on.ts'\nimport css from './skin-center.module.css'\n\n/** Business face the skin-center apply() injects into the card. */\nexport interface SkinCenterInjected {\n controller: TryOnController\n theme: {\n getTheme(): ThemeSnapshot\n subscribe(listener: () => void): () => void\n setTheme(id: 'light' | 'dark'): void\n }\n /** Background occluder over the shared skin-background namespace. */\n background: SkinBackgroundHandle\n}\n\n/** Plugin-card component props: group-item runtime share + locale seat + injected face. */\nexport type SkinCenterComponentProps =\n PropsRuntime<'web-ui.plugin.item'> & PropsLocale<'skinCenter'> & SkinCenterInjected\n\n/** The apply target of the official stock-look card. */\nconst OFFICIAL = 'official'\n\n/** Skin ids that read the background-scrim variable and paint a backdrop. */\nconst BACKDROP_SKIN_IDS = new Set(['blue-fantasy', 'whale-song'])\n\n/**\n * Render the skin-center card: a disclosure header naming the plugin, with\n * the skin list (official default + every installed skin; try-on / theme\n * preview / one-click apply) inside its body.\n * @param props - card props.\n * @returns the plugin card.\n */\nexport function SkinCenter({ t, controller, theme, background }: SkinCenterComponentProps) {\n const snapshot = useSyncExternalStore(theme.subscribe, theme.getTheme)\n const opacity = useSyncExternalStore(background.subscribe, background.opacity)\n const activePackage = activeSkinEntry()?.package\n const activeId = activeSkinEntry()?.id\n const backdropActive = activeId !== undefined && BACKDROP_SKIN_IDS.has(activeId)\n const [open, setOpen] = useState(false)\n const [tryingId, setTryingId] = useState<string | null>(null)\n const [tryingOfficial, setTryingOfficial] = useState(false)\n const [loadingId, setLoadingId] = useState<string | null>(null)\n const [applying, setApplying] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n // Unmount guard for the confirmation poll: once the card is gone, the\n // pending timers must stop and no reload / setState may fire.\n const mounted = useRef(false)\n // Latest-click-wins token for async bundle loads. A chained try-on or exit\n // invalidates older completions so a slow bundle can never overwrite the\n // UI state chosen by a newer click.\n const tryOnRequest = useRef(0)\n useEffect(() => {\n mounted.current = true\n return () => { mounted.current = false }\n }, [])\n\n const tryOn = (entry: SkinCenterEntry): void => {\n if (loadingId === entry.id) return\n const request = ++tryOnRequest.current\n setError(null)\n setLoadingId(entry.id)\n void controller.tryOn(entry)\n .then(mountedTarget => {\n if (!mounted.current || request !== tryOnRequest.current || !mountedTarget) return\n setLoadingId(null)\n setTryingId(entry.id)\n setTryingOfficial(false)\n })\n .catch(() => {\n if (!mounted.current || request !== tryOnRequest.current) return\n // A load failure keeps the previous preview mounted; a mount failure\n // restores the original active skin. Mirror the controller's actual\n // session instead of blindly clearing a preview that may still exist.\n setLoadingId(null)\n setError(t('tryOnError'))\n setTryingId(controller.trying?.id ?? null)\n setTryingOfficial(controller.tryingOfficial)\n })\n }\n\n const tryOnOfficial = (): void => {\n ++tryOnRequest.current\n setError(null)\n setLoadingId(null)\n try {\n controller.tryOnOfficial()\n } catch {\n setError(t('tryOnError'))\n setTryingOfficial(false)\n return\n }\n setTryingId(null)\n setTryingOfficial(true)\n }\n\n const exitTryOn = (): void => {\n ++tryOnRequest.current\n controller.exit()\n setLoadingId(null)\n setTryingId(null)\n setTryingOfficial(false)\n }\n\n /**\n * Poll the host state until the config watcher reports the target active\n * (the patch write lands before the watcher re-applies it), or time out.\n * @param target - skin id, or `official` for the stock look.\n * @returns whether the target became active within the poll budget.\n */\n const confirmActive = (target: string): Promise<boolean> =>\n new Promise(resolve => {\n const expected = target === OFFICIAL ? 'none' : target\n let tries = 0\n const tick = (): void => {\n if (!mounted.current) {\n resolve(false)\n return\n }\n tries += 1\n void fetch('/api/skin-center/state')\n .then(async response => {\n const payload = await response.json().catch(() => null) as { ok?: boolean; active?: string } | null\n if (response.ok && payload?.ok === true && payload.active === expected) {\n resolve(true)\n return\n }\n if (tries >= 20 || !mounted.current) resolve(false)\n else window.setTimeout(tick, 250)\n })\n .catch(() => {\n if (tries >= 20 || !mounted.current) resolve(false)\n else window.setTimeout(tick, 250)\n })\n }\n tick()\n })\n\n /**\n * Poll the served GUI document until the boot manifest actually enables\n * the target (the config watcher regenerates it asynchronously after the\n * patch write — reloading earlier boots the page into the previous skin),\n * or time out.\n * @param target - skin id, or `official` for the stock look.\n * @returns whether the manifest caught up within the poll budget.\n */\n const manifestReady = (target: string): Promise<boolean> =>\n new Promise(resolve => {\n const expected = target === OFFICIAL ? null : target\n let tries = 0\n const tick = (): void => {\n if (!mounted.current) {\n resolve(false)\n return\n }\n tries += 1\n void fetch(window.location.href, { cache: 'no-store' })\n .then(async response => {\n const html = await response.text().catch(() => null)\n if (html !== null && manifestHasSkin(html, expected)) {\n resolve(true)\n return\n }\n if (tries >= 40 || !mounted.current) resolve(false)\n else window.setTimeout(tick, 500)\n })\n .catch(() => {\n if (tries >= 40 || !mounted.current) resolve(false)\n else window.setTimeout(tick, 500)\n })\n }\n tick()\n })\n\n /**\n * One-click apply: the host half runs `dsh-skin use <target>` (or\n * `use official`), the config watcher hot-reloads the patch within\n * seconds, then this page reloads to pick up the new boot graph. The\n * reload waits for both the patch (state poll) and the regenerated boot\n * manifest (manifest poll) so the page never boots into the old skin.\n * @param target - skin id, or `official` for the stock look.\n */\n const applySkin = (target: string): void => {\n setError(null)\n setApplying(target)\n const body = target === OFFICIAL ? { official: true } : { skin: target }\n void fetch('/api/skin-center/apply', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n .then(async response => {\n const payload = await response.json().catch(() => null) as { ok?: boolean; error?: string } | null\n if (!response.ok || payload?.ok !== true) {\n throw new Error(payload?.error ?? `HTTP ${response.status}`)\n }\n setApplying(null)\n // Patch written; reload only once the watcher reports the target\n // active AND the boot manifest caught up, so the page never boots\n // into the old skin.\n void confirmActive(target).then(confirmed => {\n if (!mounted.current) return\n if (!confirmed) {\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('appliedUnconfirmed')} — ${command}`)\n return\n }\n void manifestReady(target).then(ready => {\n if (!mounted.current) return\n if (ready) {\n window.location.reload()\n } else {\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('appliedUnconfirmed')} — ${command}`)\n }\n })\n })\n })\n .catch((cause: unknown) => {\n setApplying(null)\n const detail = cause instanceof Error ? cause.message : String(cause)\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('applyFailed')} (${detail}) — ${command}`)\n })\n }\n\n const dark = snapshot.active.colorScheme === 'dark'\n\n /** One row: try-on control + apply button. Shared by the official card and every skin card. */\n const actionButtons = (opts: {\n key: string\n isActive: boolean\n isTrying: boolean\n onTryOn: () => void\n applyLabel: string\n }): ReactNode => (\n <div className={css.actions}>\n {opts.isActive ? (\n <button type=\"button\" className={`${css.button} ${css.buttonGhost}`} disabled>\n {t('tryOn')}\n </button>\n ) : opts.isTrying ? (\n <button type=\"button\" className={`${css.button} ${css.buttonPrimary}`} onClick={exitTryOn}>\n {t('exitTryOn')}\n </button>\n ) : (\n <button\n type=\"button\"\n className={`${css.button} ${css.buttonPrimary}`}\n disabled={loadingId === opts.key}\n onClick={opts.onTryOn}\n >\n {loadingId === opts.key ? t('loading') : t('tryOn')}\n </button>\n )}\n <button\n type=\"button\"\n className={css.button}\n disabled={applying !== null || loadingId !== null}\n onClick={() => { applySkin(opts.key) }}\n >\n {applying === opts.key ? t('applying') : opts.applyLabel}\n </button>\n </div>\n )\n\n return (\n <li className={open ? `${css.pluginCard} ${css.pluginCardOpen}` : css.pluginCard}>\n <button\n type=\"button\"\n className={css.cardHeader}\n aria-expanded={open}\n aria-label={`${t(open ? 'collapse' : 'expand')}: ${t('title')}`}\n onClick={() => { setOpen(current => !current) }}\n >\n <span className={css.headText}>\n <span className={css.pluginName}>\n {t('title')}\n <span className={css.titleBadge}>{String(SKIN_CENTER_ENTRIES.length)}</span>\n </span>\n <span className={css.cardDescription} title={t('cardDescription')}>{t('cardDescription')}</span>\n </span>\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 14 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron}\n >\n <path\n 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\"\n fill=\"currentColor\"\n />\n </svg>\n </button>\n\n {open\n ? (\n <div className={css.cardBody}>\n <div className={css.head}>\n <div className={css.intro} title={t('intro')}>{t('intro')}</div>\n <div className={css.themeRow}>\n <span className={css.themeLabel}>{t('theme')}</span>\n <button\n type=\"button\"\n className={`${css.themeButton} ${dark ? '' : css.themeButtonActive}`}\n onClick={() => { theme.setTheme('light') }}\n >\n {t('themeLight')}\n </button>\n <button\n type=\"button\"\n className={`${css.themeButton} ${dark ? css.themeButtonActive : ''}`}\n onClick={() => { theme.setTheme('dark') }}\n >\n {t('themeDark')}\n </button>\n </div>\n </div>\n\n <div className={css.backgroundRow}>\n <div className={css.backgroundHead}>\n <span className={css.backgroundLabel}>{t('backgroundOpacity')}</span>\n <span className={css.backgroundValue} aria-hidden=\"true\">{opacity}%</span>\n </div>\n <input\n id=\"skin-center-background-opacity\"\n className={css.backgroundRange}\n type=\"range\"\n min=\"0\"\n max=\"100\"\n step=\"5\"\n value={opacity}\n aria-valuetext={`${opacity}%`}\n aria-label={t('backgroundOpacity')}\n onChange={(event) => { background.set(Number(event.target.value)) }}\n />\n <p className={backdropActive ? css.backgroundHint : css.backgroundHintMuted}>\n {backdropActive ? t('backgroundHint') : t('backgroundHintInert')}\n </p>\n </div>\n\n {error !== null && <div className={css.error}>{error}</div>}\n\n <div className={css.list}>\n {(() => {\n const isActive = activePackage === undefined\n const isTrying = tryingOfficial\n const badge = isActive ? t('active') : isTrying ? t('tryingOn') : null\n return (\n <div className={css.card} key={OFFICIAL}>\n <div className={css.cardHead}>\n <span className={css.swatch} style={{ background: '#98a1ab' }} aria-hidden=\"true\" />\n <span className={css.cardName} title={t('official')}>{t('official')}</span>\n {badge !== null && (\n <span className={`${css.badge} ${isActive ? css.badgeActive : css.badgeTrying}`}>\n {badge}\n </span>\n )}\n </div>\n <div className={css.cardTagline} title={t('officialTagline')}>{t('officialTagline')}</div>\n {actionButtons({\n key: OFFICIAL,\n isActive,\n isTrying,\n onTryOn: tryOnOfficial,\n applyLabel: t('restore'),\n })}\n </div>\n )\n })()}\n\n {SKIN_CENTER_ENTRIES.map(entry => {\n const isActive = entry.package === activePackage\n const isTrying = entry.id === tryingId\n const badge = isActive ? t('active') : isTrying ? t('tryingOn') : null\n return (\n <div className={css.card} key={entry.id}>\n <div className={css.cardHead}>\n <span className={css.swatch} style={{ background: entry.accent }} aria-hidden=\"true\" />\n <span className={css.cardName} title={entry.nameEn}>{entry.nameEn}</span>\n {badge !== null && (\n <span className={`${css.badge} ${isActive ? css.badgeActive : css.badgeTrying}`}>\n {badge}\n </span>\n )}\n </div>\n <div className={css.cardTagline} title={entry.tagline}>{entry.tagline}</div>\n {actionButtons({\n key: entry.id,\n isActive,\n isTrying,\n onTryOn: () => { tryOn(entry) },\n applyLabel: t('apply'),\n })}\n </div>\n )\n })}\n </div>\n </div>\n )\n : null}\n </li>\n )\n}","/**\n * Background-scrim handle for the skin center: binds the `skin-background`\n * settings namespace and applies the chosen occlusion to the page's backdrop.\n *\n * Application is a CSS variable on `document.body`\n * (--dsw-skin-scrim), which backdrop-painting skins (blue-fantasy /\n * whale-song) read inside their setBackdrop() so the veil stays in sync across\n * theme flips and try-on restores. The official stock look paints no backdrop,\n * so the variable is inert there — the value still persists so it is ready for\n * the next backdrop skin.\n *\n * Values are 0-100 (0 = no extra veil, 100 = fully obscured); they are written\n * through as a 0..1 alpha for the CSS variable. Dragging the control applies\n * instantly (live) and persists through the settings scope.\n */\nimport type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-runtime/client'\n\n/** The namespace string the Host registers (mirrors src/index.ts). */\nexport const SKIN_BACKGROUND_NS = 'skin-background'\n\n/** Field of the background value inside the namespace section. */\nexport const OPACITY_FIELD = 'backgroundOpacity'\n\n/** CSS custom property written to document.body and read by backdrop skins. */\nexport const SCRIM_VAR = '--dsw-skin-scrim'\n\n/** Default occlusion (0 = no extra veil) when the section carries none. */\nexport const DEFAULT_OPACITY = 0\n\n/** The face the skin-center card injects for the background control. */\nexport interface SkinBackgroundHandle {\n /** Current occlusion 0-100 (also the getSnapshot seat for useSyncExternalStore). */\n opacity(): number\n /** Observe a change in the applied value. */\n subscribe(listener: () => void): () => void\n /** Apply + persist a new occlusion. */\n set(opacity: number): void\n}\n\n/**\n * Own the skin-background scope: read the latest occlusion, apply it to the\n * body CSS variable instantly, and persist changes through the settings scope.\n */\nexport class BackgroundController implements SkinBackgroundHandle {\n private value = DEFAULT_OPACITY\n private readonly listeners = new Set<() => void>()\n private readonly scope: SettingsScope<{ backgroundOpacity?: number }>\n\n /**\n * @param scope - the bound skin-background settings scope.\n */\n constructor(scope: SettingsScope<{ backgroundOpacity?: number }>) {\n this.scope = scope\n this.value = this.read()\n this.apply()\n scope.subscribe(() => {\n this.value = this.read()\n this.apply()\n this.publish()\n })\n }\n\n opacity(): number { return this.value }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n set(opacity: number): void {\n const clamped = Math.max(0, Math.min(100, Math.round(opacity)))\n this.value = clamped\n this.apply()\n this.publish()\n // Persist: queue the write on the settings scope. Failures are silent —\n // the live value is already applied, and the write drains on reconnect.\n void this.scope.set(OPACITY_FIELD, clamped)\n }\n\n /** The effective section value, clamped 0-100, defaulting to 0. */\n private read(): number {\n const snapshot: SettingsScopeSnapshot<{ backgroundOpacity?: number }> = this.scope.getSnapshot()\n const raw = snapshot.value?.backgroundOpacity\n if (typeof raw !== 'number' || !Number.isFinite(raw)) return DEFAULT_OPACITY\n return Math.max(0, Math.min(100, raw))\n }\n\n /** Write the current occlusion onto the body CSS variable (0..1 alpha). */\n private apply(): void {\n document.body.style.setProperty(SCRIM_VAR, String(this.value / 100))\n }\n\n private publish(): void {\n for (const listener of this.listeners) listener()\n }\n}\n","/**\n * Skin-center locale dictionaries. The plugin-card name, its description,\n * and every control of the in-GUI skin center is localized through the\n * standard `t` seat.\n */\n\n/** Copy keys owned by this plugin. */\nexport type SkinCenterKey =\n | 'title'\n | 'cardDescription'\n | 'expand'\n | 'collapse'\n | 'intro'\n | 'official'\n | 'officialTagline'\n | 'active'\n | 'tryingOn'\n | 'tryOn'\n | 'loading'\n | 'exitTryOn'\n | 'apply'\n | 'applying'\n | 'restore'\n | 'applyFailed'\n | 'appliedUnconfirmed'\n | 'theme'\n | 'themeLight'\n | 'themeDark'\n | 'tryOnError'\n | 'backgroundOpacity'\n | 'backgroundHint'\n | 'backgroundHintInert'\n\nexport const en: Record<SkinCenterKey, string> = {\n title: 'Skin Center',\n cardDescription: 'Try on any installed skin live in the GUI — exit restores instantly, applying persists in one click.',\n expand: 'Expand',\n collapse: 'Collapse',\n intro: 'Try on any skin live — it takes effect instantly, exit restores the current look. Apply persists it across restarts.',\n official: 'Official default',\n officialTagline: 'The stock DSH look with no skin applied.',\n active: 'Active',\n tryingOn: 'Trying on',\n tryOn: 'Try on',\n loading: 'Loading…',\n exitTryOn: 'Exit try-on',\n apply: 'Apply',\n applying: 'Applying…',\n restore: 'Restore',\n applyFailed: 'Apply failed',\n appliedUnconfirmed: 'Applied, but the change has not been confirmed — refresh the page if the skin did not switch',\n theme: 'Theme preview',\n themeLight: 'Light',\n themeDark: 'Dark',\n tryOnError: 'Try-on failed — see console',\n backgroundOpacity: 'Background occlusion',\n backgroundHint: 'Instantly veils the backdrop behind the panels — higher values obscure the art to help you focus.',\n backgroundHintInert: 'Only applies to skins that paint a backdrop (Blue Fantasy / Whale Song). Applies to the official default automatically once such a skin is active.',\n}\n\nexport const zh: Record<SkinCenterKey, string> = {\n title: '皮肤中心',\n cardDescription: '在 GUI 内即时试穿任意皮肤,退出即完全还原;应用一键完成并自动刷新。',\n expand: '展开',\n collapse: '收起',\n intro: '任意皮肤可即时试穿,退出即完全还原;「应用」一键持久化,页面自动刷新生效。',\n official: '官方默认',\n officialTagline: '还原 DSH 官方默认外观,不应用任何皮肤。',\n active: '当前激活',\n tryingOn: '试穿中',\n tryOn: '试穿',\n loading: '加载中…',\n exitTryOn: '退出试穿',\n apply: '应用',\n applying: '应用中…',\n restore: '恢复默认',\n applyFailed: '应用失败',\n appliedUnconfirmed: '已写入配置但尚未确认生效——若皮肤未切换请手动刷新页面',\n theme: '主题预览',\n themeLight: '亮色',\n themeDark: '暗色',\n tryOnError: '试穿失败,详见控制台',\n backgroundOpacity: '背景遮挡',\n backgroundHint: '即时为面板背后的背景加遮罩——数值越高越能弱化插画,帮你集中注意力。',\n backgroundHintInert: '仅对带背景图插画的皮肤(蓝色幻想 / 鲸吟)生效;官方默认无背景图,该滑块对这些皮肤自动生效。',\n}\n","/**\n * In-GUI skin center, browser half: registers the Skins plugin card into the\n * Web UI plugin group (`web-ui.plugin.item`, declared by the web-ui-settings\n * group card under 插件配置) and provides the try-on controller + official\n * theme handle to it. The card lists every installed skin (embedded\n * registry), tries it on live inside the GUI, exits with a full restore, and\n * copies the one-command apply. The plugin writes only DOM and the settings\n * ledger — no services, no events, no model access.\n */\nimport type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { SkinCenter, type SkinCenterInjected } from './SkinCenter.tsx'\nimport { BackgroundController, SKIN_BACKGROUND_NS } from './background.ts'\nimport { en, zh, type SkinCenterKey } from './locales.ts'\nimport { TryOnController } from './try-on.ts'\n\nexport type { SkinCenterComponentProps, SkinCenterInjected } from './SkinCenter.tsx'\nexport { TryOnController } from './try-on.ts'\n\n/** Locale namespace owned by this plugin. */\nexport const NS = 'skinCenter'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The skin-center card's copy. */\n skinCenter: SkinCenterKey\n }\n\n interface SlotMap {\n /**\n * The child slot the Web UI plugin group declares; this card registers\n * into the group instead of the top-level `settings.plugin.item` list.\n * Spelled here with the same shape so this package can register without\n * depending on the sibling UI package.\n */\n 'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }\n }\n}\n\n/** Owner share of a plugin card (the group card supplies nothing). */\nexport interface SettingsPluginItemOwnerProps {\n /** Marker field: card owner props are intentionally empty. */\n children?: never\n}\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n /**\n * Optional rc.6 compatibility binder provided by dsh-web-ui-settings;\n * absent when that group plugin is not installed, so callers fall back to\n * the official settings scope.\n */\n webUiSettings?: { bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S> }\n }\n}\n\n\n/** Required services: slots + locale (plugin card), theme (preview toggle), and settingsScope + its transport (background scrim). */\nexport const inject = ['slots', 'locale', 'theme', 'settingsScope', 'connection', 'remote']\n\n/**\n * Register the skin-center dictionaries, the body scope attribute, and the\n * Skins plugin card inside the Web UI plugin group.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-skin-center: dictionaries')\n\n // The card's own styles scope under this attribute so they keep applying\n // during try-on (when the active skin's attribute is retracted).\n ctx.effect(() => {\n document.body.dataset.dshSkinCenter = ''\n return () => { delete document.body.dataset.dshSkinCenter }\n }, 'ui-skin-center: body scope')\n\n const theme = ctx.get('theme') as ThemeRuntime\n const controller = new TryOnController()\n // Background occluder over the shared skin-background namespace. The scope\n // is bound to this plugin's fiber, so it is torn down with the card.\n const binder = ctx.get('webUiSettings') ?? ctx.settingsScope\n const backgroundScope = binder.bind<{ backgroundOpacity?: number }>({ namespace: SKIN_BACKGROUND_NS })\n const background = new BackgroundController(backgroundScope)\n const injected = (): SkinCenterInjected => ({\n controller,\n theme: {\n getTheme: () => theme.getTheme(),\n subscribe: listener => ctx.on('theme/change', listener),\n setTheme: id => theme.setTheme(id),\n },\n background: {\n opacity: () => background.opacity(),\n subscribe: listener => background.subscribe(listener),\n set: opacity => background.set(opacity),\n },\n })\n\n ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({\n name: 'web-ui.plugin.item',\n id: 'skins',\n order: 110,\n locale: NS,\n inject: injected,\n }, SkinCenter))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["css"],"sources":["../../../packages/skins/skin-center/src/client/generated/skins.ts","../../../packages/skins/skin-center/src/client/manifest.ts","../../../packages/skins/skin-center/src/client/try-on.ts","../../../packages/skins/skin-center/src/client/SkinCenter.tsx","../../../packages/skins/skin-center/src/client/background.ts","../../../packages/skins/skin-center/src/client/locales.ts","../../../packages/skins/skin-center/src/client/index.ts"],"sourcesContent":["/**\n * AUTO-GENERATED by scripts/skin-center-bundles — do not edit by hand.\n * Skin registry for the GUI skin center: gallery metadata only. Client\n * bundle text is NOT embedded here — try-on fetches each skin's\n * prebuilt lib/client.js on demand from the host route\n * /api/skin-center/bundle/<id> (a same-origin script executed by the\n * page's own module loader; no eval, and the GUI never parses the\n * embedded art base64 at startup).\n * Regenerate with `node scripts/skin-center-bundles`.\n */\n\n/** One registry entry: gallery metadata for the skin center card. */\nexport interface SkinCenterEntry {\n /** Skin id (packages/skins/<id>/). */\n id: string\n /** Display name (zh). */\n name: string\n /** Display name (en). */\n nameEn: string\n /** One-line tagline. */\n tagline: string\n /** Accent color for the swatch. */\n accent: string\n /** Author credit. */\n author?: string\n /** Longer description. */\n description?: string\n /** Tag list. */\n tags?: string[]\n /** Body attribute that scopes the skin styles. */\n bodyAttr: string\n /** Plugin package name (the boot-graph entry id when active). */\n package: string\n /** Gallery sort order (packages/skins/<name>/skin.json). */\n order?: number\n}\n\n/** Every skin, ordered by packages/skins/<name>/skin.json `order`. */\nexport const SKIN_CENTER_ENTRIES: readonly SkinCenterEntry[] = [\n {\"id\":\"blue-fantasy\",\"name\":\"蓝色幻想\",\"nameEn\":\"Blue Fantasy\",\"author\":\"powerdog996(DreamSkin 社区)· dsh-web-ui 适配\",\"tagline\":\"鲸鱼插画背景 · periwinkle 靛蓝调色板 · 半透明面板\",\"description\":\"DreamSkin「DeepSeek-鲸鱼娘」Codex 桌面主题的 dsh 适配:鲸鱼插画背景垫在半透明面板之下,遮罩随亮/暗主题实时切换,periwinkle 靛蓝色调重映射到全部 dsh token。\",\"tags\":[\"dreamskin\",\"whale\",\"indigo\",\"art\",\"translucent\"],\"accent\":\"#4a5fa8\",\"bodyAttr\":\"data-dsh-blue-fantasy\",\"package\":\"@linxin666/dsh-client-ui-skin-blue-fantasy\",\"order\":1},\n\n {\"id\":\"whale-song\",\"name\":\"鲸吟\",\"nameEn\":\"Whale Song\",\"author\":\"dsh-web-ui\",\"tagline\":\"深海鲸语女神背景 · 冰蓝海洋调色板 · 金色细线点缀\",\"description\":\"《鲸吟》— 深海鲸语女神主题:无文字纯氛围背景画(蓝发女神与鲸群居左、冰蓝星座网格与金线点缀、右侧大量留白)垫在半透明面板之下,遮罩随亮/暗主题实时切换,冰蓝/浅青/深海军蓝/钴蓝冷色体系重映射到全部 dsh token,暗色变体为深海夜航调。\",\"tags\":[\"whale\",\"ocean\",\"ice-blue\",\"goddess\",\"art\",\"translucent\"],\"accent\":\"#4d8fd4\",\"bodyAttr\":\"data-dsh-whale-song\",\"package\":\"@linxin666/dsh-client-ui-skin-whale-song\",\"order\":2},\n\n {\"id\":\"harbor\",\"name\":\"夕港\",\"nameEn\":\"Harbor\",\"author\":\"moeblack\",\"tagline\":\"暮光蓝港 · 日落橙辉 · 半透明夜色面板\",\"description\":\"《夕港》黄昏港口主题:动漫少女黄昏港口背景(暮光蓝天空渐入日落橙)垫在半透明面板之下,遮罩随亮/暗主题实时切换,深暮蓝 #141a2e 底与日落橙 #ff9d5c 主色重映射到 dsh token,亮色是薄暮纱、暗色是深海夜航纱,同一幅画两种读法。\",\"tags\":[\"harbor\",\"dusk\",\"twilight\",\"sunset\",\"amber\",\"art\",\"translucent\"],\"accent\":\"#ff9d5c\",\"bodyAttr\":\"data-dsh-harbor\",\"package\":\"@linxin666/dsh-client-ui-skin-harbor\",\"order\":3},\n\n {\"id\":\"qq98\",\"name\":\"QQ2008 怀旧版\",\"nameEn\":\"QQ2008 Retro\",\"author\":\"dsh-web-ui\",\"tagline\":\"水晶蓝桌面 · 玻璃深蓝标题栏 · 戴围巾企鹅\",\"description\":\"dsh web ui 家族收录的第一个皮肤:QQ2008 水晶蓝年代。深蓝渐变桌面、玻璃质感标题栏、浅蓝状态栏和圆角高光控件,配一只戴围巾的企鹅。\",\"tags\":[\"retro\",\"qq\",\"2008\",\"crystal-blue\",\"nostalgia\"],\"accent\":\"#2b7cd9\",\"bodyAttr\":\"data-dsh-retro\",\"package\":\"@linxin666/dsh-client-ui-skin-qq98\",\"order\":4},\n\n {\"id\":\"ths\",\"name\":\"同花顺风格\",\"nameEn\":\"Tonghuashun Trading\",\"author\":\"dsh-web-ui\",\"tagline\":\"品牌红标题栏 · 实时行情状态栏 · 灰蓝数据终端\",\"description\":\"同花顺风格炒股主题:品牌红标题栏带上证指数行情签,状态栏红涨绿跌,自选股风格的侧边栏和交易终端面板,写代码也像盯盘。\",\"tags\":[\"stock\",\"trading\",\"terminal\",\"red\"],\"accent\":\"#e60012\",\"bodyAttr\":\"data-dsh-ths\",\"package\":\"@linxin666/dsh-client-ui-skin-ths\",\"order\":5},\n\n {\"id\":\"xp\",\"name\":\"Windows XP (Luna)\",\"nameEn\":\"Windows XP Luna\",\"author\":\"dsh-web-ui\",\"tagline\":\"Luna 蓝窗口条 · 绿色开始按钮 · Bliss 蓝天桌面\",\"description\":\"Windows XP (Luna) 复古主题:蓝色渐变窗口条带窗口按钮、米色状态栏(大写/数字/滚动指示灯)、侧边栏任务栏上的绿色「开始」按钮、资源管理器风格树行和 Bliss 蓝天桌面,全局直角。\",\"tags\":[\"retro\",\"xp\",\"luna\",\"windows\",\"start-button\"],\"accent\":\"#316ac5\",\"bodyAttr\":\"data-dsh-xp\",\"package\":\"@linxin666/dsh-client-ui-skin-xp\",\"order\":6},\n\n {\"id\":\"dragon-heir\",\"name\":\"龙的传人\",\"nameEn\":\"Dragon Heir\",\"author\":\"dsh-web-ui\",\"tagline\":\"不屈龙魂 · 万里长城双主题 · 朱砂龙印\",\"description\":\"龙的传人 — 一面是不屈龙魂(墨龙穿云、朱砂印章、不屈锋芒),一面是万里长城(青黛山色、金晖镀墙、苍茫暮色)。亮暗主题各自配一幅画与一枚龙印 favicon,面板半透明磨砂,让画透出来。\",\"tags\":[\"dragon\",\"loong\",\"chinese\",\"ink-wash\",\"great-wall\",\"dual-theme\"],\"accent\":\"#c3272b\",\"bodyAttr\":\"data-dsh-dragon-heir\",\"package\":\"@linxin666/dsh-client-ui-skin-dragon-heir\",\"order\":7},\n\n {\"id\":\"minecraft\",\"name\":\"Minecraft 方块世界\",\"nameEn\":\"Minecraft Voxel\",\"author\":\"dsh-web-ui\",\"tagline\":\"动态全景天空盒 · 方块按钮 · 告示牌输入框\",\"description\":\"复刻《我的世界》主界面氛围的方块皮肤:程序化绘制的像素全景天空盒(方块山、像素云、方块树、草方块地面)在身后缓慢旋转,界面浮在石板上;按钮还原 MC 菜单按钮(灰石板、悬停变黄、按下下沉),输入框做成带钉子的木告示牌。\",\"tags\":[\"minecraft\",\"voxel\",\"pixel\",\"game\",\"panorama\",\"skybox\"],\"accent\":\"#7cbd4b\",\"bodyAttr\":\"data-dsh-minecraft\",\"package\":\"@linxin666/dsh-client-ui-skin-minecraft\",\"order\":8},\n\n {\"id\":\"trading\",\"name\":\"交易终端\",\"nameEn\":\"Trading Terminal\",\"author\":\"dsh-web-ui\",\"tagline\":\"实时行情跑马灯 · 长桥港美股行情 · 红涨绿跌交易终端\",\"description\":\"结合 dsh-fun-ticker 行情跑马灯与 dsh-longbridge 港美股行情的炒股皮肤:顶栏滚动 A股/港股/美股/指数/加密/外汇报价(装 fun-ticker 后跟随你的自选列表),状态栏展示长桥行情快照与 A股/港股/美股交易时段,写代码也像盯盘。\",\"tags\":[\"stock\",\"trading\",\"ticker\",\"live\",\"terminal\",\"longbridge\"],\"accent\":\"#f23645\",\"bodyAttr\":\"data-dsh-trading\",\"package\":\"@linxin666/dsh-client-ui-skin-trading\",\"order\":9},\n\n {\"id\":\"miku\",\"name\":\"初音未来 · 电子歌姬\",\"nameEn\":\"Hatsune Miku\",\"author\":\"涂山苏苏\",\"tagline\":\"蓝紫双马尾 · 01 编号 · 音符波形 · 电子歌姬主题\",\"description\":\"以世界第一的虚拟歌姬初音未来为灵感的主题皮肤:蓝紫洋红渐变贯穿全局,音符与声波曲线点缀在半透明面板之间,标题栏与状态栏带有 01 编号徽标与音乐波形,半透明毛玻璃面板透出背景图——沉浸式电子歌姬氛围。\",\"tags\":[\"miku\",\"vocaloid\",\"blue\",\"music\",\"idol\",\"waveform\"],\"accent\":\"#2e9bff\",\"bodyAttr\":\"data-dsh-miku\",\"package\":\"@linxin666/dsh-client-ui-skin-miku\",\"order\":10},\n\n]\n","/**\n * Boot-manifest readiness checks for the one-click apply flow.\n *\n * The host half writes the skin patch synchronously, but the web app's boot\n * graph (the `window.__DSH_BOOT__` JSON inside the served HTML) is\n * regenerated asynchronously by the config watcher. A page reloaded right\n * after the patch write can therefore boot into the previous skin. These\n * helpers let the frontend poll the served document until the manifest\n * actually reflects the target before reloading.\n * @module @linxin666/dsh-client-ui-skin-center/manifest\n */\n\n/** Bundle URL pattern of any skin entry in the boot manifest. */\nconst SKIN_BUNDLE_URL = /\\/plugins\\/@linxin666\\/dsh-client-ui-skin-(?!center)[a-z0-9-]+\\/client\\.js/\n\n/**\n * Whether a served GUI document's boot manifest enables the given skin.\n * A `null` target means the stock look: no skin bundle URL may be present\n * (the skin-center plugin's own bundle always loads and is excluded).\n * @param documentHtml - the served GUI document (contains the boot JSON).\n * @param target - skin id, or `null` for the stock look.\n * @returns whether the manifest already enables the target.\n */\nexport function manifestHasSkin(documentHtml: string, target: string | null): boolean {\n if (target === null) return !SKIN_BUNDLE_URL.test(documentHtml)\n return documentHtml.includes(`/plugins/@linxin666/dsh-client-ui-skin-${target}/client.js`)\n}\n","/**\n * Try-on engine for the in-GUI skin center.\n *\n * A skin's client bundle is executed through the REAL module system, not a\n * shim and not eval: the host route `/api/skin-center/bundle/<id>` serves\n * the skin's prebuilt `lib/client.js` as a same-origin script (mirroring\n * the kernel's own defaultLoadBundle — see dsh-client-modules), and its\n * body calls `window.__ModuleLoader__.load({id, factory})`, which only\n * REGISTERS the factory. `window.__DSH_MODULES__.import(package)` (the\n * kernel's ClientModuleSystem, contract C5/C6) then materializes it — which\n * auto-injects the skin's CSS `<style data-plugin>` tag — and\n * `surface.apply(miniCtx)` mounts the skin exactly as the fiber system\n * would, returning a full disposer. That makes try-on and its teardown the\n * real code paths, with no CSP `unsafe-eval` dependence and no startup\n * cost: the ~700KB of embedded art base64 is only parsed when a skin is\n * actually tried on.\n *\n * Mutual exclusion: the GUI never hosts two skins at once. The currently\n * ACTIVE skin is owned by its own cordis fiber (its disposer is not\n * reachable), so try-on retracts the active skin's visual writes by recipe:\n * remove its body attribute (its stylesheet goes inert), clear the\n * body-level backdrop inline styles (blue-fantasy's whale art), detach only\n * known skin chrome body children (title/status bars marked `data-skin-chrome`\n * or carrying the skin's body attribute, leaving other plugins' portals and\n * toasts in place), and neutralize known global-rule leaks (xp's sidebar\n * taskbar/start). Everything is snapshotted and restored on exit in original\n * order. The active skin's own fiber is never touched, so exiting try-on\n * returns the page to exactly the pre-try-on state.\n *\n * A ghost MutationObserver may survive retraction (blue-fantasy re-writes\n * its backdrop on theme flips), so during try-on a neutralizing observer\n * re-clears the backdrop props whenever `data-ds-dark-theme` changes.\n */\n\nimport { SKIN_CENTER_ENTRIES, type SkinCenterEntry } from './generated/skins.ts'\n\n/** Body-level backdrop properties skins may write inline (blue-fantasy). */\nconst BACKDROP_PROPS = [\n 'background-image',\n 'background-position',\n 'background-size',\n 'background-attachment',\n 'background-repeat',\n] as const\n\n/**\n * Per-skin neutralization CSS: rules that hide visual leaks whose styles\n * are NOT scoped under the skin's body attribute (they live on app elements\n * the skin touches, so detaching chrome cannot remove them). Matched by\n * css-module class substring, which is stable across rebuilds.\n */\nconst NEUTRALIZE_CSS: Record<string, string> = {\n // xp styles its start button + taskbar strip in the sidebar footer with\n // top-level (unscoped) rules; its apply() removes them on dispose, but\n // during try-on its ghost observer would re-add them, so hide instead.\n xp: [\n `[data-pane='sidebar'] [class*='xpTaskbar']{background:transparent!important;border-top:none!important;box-shadow:none!important}`,\n `[data-pane='sidebar'] [class*='xpStart']{display:none!important}`,\n ].join(''),\n}\n\n/** The window surfaces the boot protocol installs (manifest.ts contract). */\ninterface SkinCenterWindow {\n __DSH_BOOT__?: { entries?: Array<{ id: string }> }\n __DSH_MODULES__?: {\n import(specifier: string): Promise<unknown>\n invalidate(id: string): void\n }\n}\n\n/** Host base path of the skin bundle route (registered by src/routes.ts). */\nconst BUNDLE_ROUTE = '/api/skin-center/bundle'\n\n/**\n * Execute one skin's client bundle as a real same-origin script, mirroring\n * the kernel's own defaultLoadBundle (dsh-client-modules): the script body\n * calls `window.__ModuleLoader__.load({id, factory})`, which only registers\n * the factory — materialization is the caller's separate `import` step. No\n * eval: try-on works under any CSP that allows same-origin scripts (the\n * shell itself loads plugin bundles this way), and a failed fetch rejects\n * so the caller can restore the active skin instead of leaving it retracted.\n * @param url - same-origin bundle URL.\n * @returns a promise resolving once the script executed.\n */\nfunction loadBundleScript(url: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const el = document.createElement('script')\n el.async = true\n el.src = url\n el.addEventListener('load', () => {\n el.remove()\n resolve()\n }, { once: true })\n el.addEventListener('error', () => {\n el.remove()\n reject(new Error(`skin-center: bundle script ${url} failed to load`))\n }, { once: true })\n document.head.append(el)\n })\n}\n\n/** Read the page's composed boot-graph entry ids (only enabled plugins appear). */\nfunction bootEntryIds(): string[] {\n const boot = (window as SkinCenterWindow).__DSH_BOOT__\n return boot?.entries?.map(entry => entry.id) ?? []\n}\n\n/** The skin package currently ACTIVE in the boot graph, if it is one of ours. */\nexport function activeSkinEntry(): SkinCenterEntry | undefined {\n const ids = new Set(bootEntryIds())\n return SKIN_CENTER_ENTRIES.find(entry => ids.has(entry.package))\n}\n\n/**\n * Whether a direct body child is skin chrome owned by `skin`: marked with the\n * `data-skin-chrome` marker (minecraft/dragon-heir) or carrying the skin's\n * scoping body attribute. Everything else — other plugins' portals, toasts and\n * overlays appended to body — is left alone.\n */\nfunction isSkinChrome(el: Element, skin: SkinCenterEntry | null): boolean {\n if (el.hasAttribute('data-skin-chrome')) return true\n return skin !== null && el.hasAttribute(skin.bodyAttr)\n}\n\n/** Minimal ctx the skins' apply() needs: cordis effect lifecycle only. */\ninterface MiniCtx {\n effect(callback: () => () => void, label?: string): () => void\n /**\n * Service reads are answered with undefined: try-on is a pure-DOM stage,\n * so optional service access (e.g. ths reading the connection handle)\n * degrades to its fallback instead of throwing mid-apply.\n */\n get(key: string): unknown\n /** Hidden handle for the controller: run the disposers in reverse order. */\n __disposeAll(): void\n}\n\nfunction miniCtx(): MiniCtx {\n const disposers: Array<() => void> = []\n return {\n effect(callback) {\n disposers.push(callback())\n return () => {}\n },\n get() {\n return undefined\n },\n __disposeAll(): void {\n for (const dispose of disposers.reverse()) dispose()\n },\n }\n}\n\n/** Snapshot of the active skin's visual writes, restored on try-on exit. */\ninterface ActiveVisuals {\n skin: SkinCenterEntry | null\n /** Attribute value before retraction (null = attribute absent). */\n bodyAttr: string | null\n /** body inline style before retraction (null = none). */\n bodyStyle: string | null\n /** Skin chrome elements detached from body, re-inserted at their anchors. */\n detached: Array<{ el: HTMLElement; anchor: Node | null }>\n /** Neutralizes ghost backdrop writes while a try-on is live. */\n clearObserver: MutationObserver | null\n /** Hides global-rule leaks of the active skin (xp taskbar). */\n neutralizeStyle: HTMLStyleElement | null\n}\n\n/**\n * One live try-on session: owns the tried-on skin's disposer plus the\n * captured active-skin visuals, and restores everything on exit.\n */\nexport class TryOnController {\n private session: {\n /** The tried-on skin, or null when trying on the official stock look. */\n entry: SkinCenterEntry | null\n dispose: () => void\n active: ActiveVisuals\n } | null = null\n\n /**\n * Generation counter. A newer try-on or exit increments it, so an in-flight\n * `tryOn` (awaiting the real bundle load) can detect it was superseded and\n * drop only what it mounted instead of clobbering the newer session.\n */\n private epoch = 0\n\n /**\n * One package can be requested again before its first script load finishes\n * (for example A -> B -> A). Share that materialization so two script tags\n * never race to register the same module factory.\n */\n private readonly pendingModules = new Map<string, Promise<(ctx: unknown) => unknown>>()\n\n /**\n * Package selected by the newest async request. A superseded request for the\n * same package must not invalidate the module/style now owned by that newer\n * request when their shared load settles.\n */\n private requestedPackage: string | null = null\n\n /**\n * Loads one skin's client bundle so its factory registers on the page's\n * `__ModuleLoader__`. Defaults to a same-origin script tag from the host\n * route `/api/skin-center/bundle/<id>`; tests inject a stub.\n */\n private readonly loadBundle: (entry: SkinCenterEntry) => Promise<void>\n\n constructor(options: { loadBundle?: (entry: SkinCenterEntry) => Promise<void> } = {}) {\n this.loadBundle = options.loadBundle ?? (entry => loadBundleScript(`${BUNDLE_ROUTE}/${encodeURIComponent(entry.id)}`))\n }\n /** The skin currently being tried on, if any. */\n get trying(): SkinCenterEntry | null {\n return this.session?.entry ?? null\n }\n\n /** Whether the official stock look (no skin) is being tried on. */\n get tryingOfficial(): boolean {\n return this.session !== null && this.session.entry === null\n }\n\n /**\n * Start trying on `entry` (replaces any live session).\n *\n * When another skin is already being tried on, keep it mounted while the\n * next bundle loads. Once the target is ready, tear down the old preview and\n * mount the new one against the SAME captured active-skin snapshot. This\n * avoids the expensive preview -> active -> preview round trip and prevents\n * a flash of the active skin between consecutive try-ons.\n * @returns whether this request mounted the target (false when superseded).\n */\n async tryOn(entry: SkinCenterEntry): Promise<boolean> {\n if (entry.package === activeSkinEntry()?.package) return false\n const epoch = ++this.epoch\n this.requestedPackage = entry.package\n\n let apply: (ctx: unknown) => unknown\n try {\n // Keep either the active skin or the current preview visible while the\n // target script downloads, parses and materializes its module.\n apply = await this.loadModuleOnce(entry)\n } catch (error) {\n if (this.shouldCleanupRequest(entry, epoch)) this.cleanupModule(entry)\n throw error\n }\n if (epoch !== this.epoch) {\n if (this.shouldCleanupRequest(entry, epoch)) this.cleanupModule(entry)\n return false\n }\n\n const previous = this.session\n let active: ActiveVisuals\n if (previous === null) {\n active = this.captureAndRetractActive()\n } else {\n // Transfer ownership of the original active-skin snapshot directly to\n // the next preview; restoring and immediately retracting it is wasted\n // DOM work and is the visible pause reported during chained try-ons.\n this.session = null\n previous.dispose()\n if (previous.entry !== null) this.cleanupModule(previous.entry)\n active = previous.active\n }\n\n let dispose: () => void\n try {\n dispose = this.applyLoaded(entry, apply)\n } catch (error) {\n if (epoch === this.epoch) this.restoreActive(active)\n throw error\n }\n this.session = { entry, dispose, active }\n return true\n }\n\n /**\n * Try on the official stock look: retract the active skin's visual writes\n * (same recipe as a skin try-on) and mount nothing. Exiting restores the\n * active skin exactly like any other try-on session.\n */\n tryOnOfficial(): void {\n if (activeSkinEntry() === null) return\n this.epoch += 1\n this.requestedPackage = null\n const previous = this.session\n if (previous !== null) {\n // A live preview already owns the captured active visuals. Dispose only\n // that preview and reuse the snapshot instead of restoring and retracting\n // the active skin in two consecutive layout-heavy operations.\n this.session = null\n previous.dispose()\n if (previous.entry !== null) this.cleanupModule(previous.entry)\n this.session = { entry: null, dispose: () => {}, active: previous.active }\n return\n }\n const active: ActiveVisuals = this.captureAndRetractActive()\n this.session = { entry: null, dispose: () => {}, active }\n }\n\n /** Exit the live session: dispose the tried-on skin, then restore the active skin. */\n exit(): void {\n // Also invalidate an in-flight initial/chained load. The visible session\n // may already be null by the time a user exits, but its late completion\n // must never remount a skin after cancellation.\n this.epoch += 1\n this.requestedPackage = null\n const session = this.session\n if (session === null) return\n this.session = null\n session.dispose()\n if (session.entry !== null) this.cleanupModule(session.entry)\n this.restoreActive(session.active)\n }\n\n /** Share one materialization while repeated requests for a package overlap. */\n private loadModuleOnce(entry: SkinCenterEntry): Promise<(ctx: unknown) => unknown> {\n const existing = this.pendingModules.get(entry.package)\n if (existing !== undefined) return existing\n\n const pending = this.loadModule(entry)\n this.pendingModules.set(entry.package, pending)\n void pending.then(\n () => {\n if (this.pendingModules.get(entry.package) === pending) this.pendingModules.delete(entry.package)\n },\n () => {\n if (this.pendingModules.get(entry.package) === pending) this.pendingModules.delete(entry.package)\n },\n )\n return pending\n }\n\n /** Whether this request still owns cleanup of the package module/style. */\n private shouldCleanupRequest(entry: SkinCenterEntry, epoch: number): boolean {\n return epoch === this.epoch || this.requestedPackage !== entry.package\n }\n\n /** Execute + materialize the target skin through the real loader. */\n private async loadModule(entry: SkinCenterEntry): Promise<(ctx: unknown) => unknown> {\n const modules = (window as SkinCenterWindow).__DSH_MODULES__\n if (modules === undefined) throw new Error('skin-center: window.__DSH_MODULES__ missing')\n // This try-on session owns the module record for the package for its\n // whole life (a try-on of the ACTIVE skin is rejected above, and the GUI\n // never hosts two skins at once). Drop any factory a crashed earlier\n // session left behind so the script below registers a fresh one — the\n // duplicate-registration retry dance is unnecessary with the real\n // loader, whose load() throws on a duplicate id.\n modules.invalidate(entry.package)\n await this.loadBundle(entry)\n const surface = await modules.import(entry.package)\n const apply = (surface as { apply?: (ctx: unknown) => unknown }).apply\n if (typeof apply !== 'function') {\n throw new Error(`skin-center: \"${entry.package}\" client bundle exports no apply`)\n }\n return apply\n }\n\n /** Apply a module that has already been loaded while the active skin was visible. */\n private applyLoaded(entry: SkinCenterEntry, apply: (ctx: unknown) => unknown): () => void {\n const ctx = miniCtx()\n try {\n apply(ctx)\n } catch (error) {\n // A skin that throws mid-apply leaves partial writes behind (body\n // attribute, chrome, style tag) and never registers its disposer —\n // the tryOn catch only restores the ACTIVE skin. Roll the residue back\n // here so a crashed try-on can never bleed into the next one.\n this.cleanupModule(entry)\n document.body.removeAttribute(entry.bodyAttr)\n for (const el of [...document.body.children] as HTMLElement[]) {\n if (isSkinChrome(el, entry)) el.remove()\n }\n throw error\n }\n return ctx.__disposeAll\n }\n\n /** Drop the tried-on module record + its injected style tag. */\n private cleanupModule(entry: SkinCenterEntry): void {\n const modules = (window as SkinCenterWindow).__DSH_MODULES__\n modules?.invalidate(entry.package)\n for (const el of document.querySelectorAll(`style[data-plugin=${JSON.stringify(entry.package)}]`)) {\n el.remove()\n }\n }\n\n /**\n * Snapshot the active skin's visual writes and retract them so the tried-on\n * skin can take over the whole surface.\n */\n private captureAndRetractActive(): ActiveVisuals {\n const skin = activeSkinEntry() ?? null\n const body = document.body\n const bodyAttr = skin === null ? null : body.getAttribute(skin.bodyAttr)\n if (skin !== null && bodyAttr !== null) body.removeAttribute(skin.bodyAttr)\n\n const bodyStyle = body.getAttribute('style')\n for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop)\n\n // Detach only known skin chrome (marker/bodyAttr), leaving other\n // plugins' portals/toasts in place; remember each element's next sibling\n // so restore re-inserts in the original order.\n const children = [...body.children] as HTMLElement[]\n const chrome = new Set<HTMLElement>()\n for (const el of children) {\n if (el.id !== 'root' && isSkinChrome(el, skin)) chrome.add(el)\n }\n const detached: Array<{ el: HTMLElement; anchor: Node | null }> = []\n for (let i = 0; i < children.length; i++) {\n const el = children[i]\n if (!chrome.has(el)) continue\n let anchor: Node | null = null\n for (let j = i + 1; j < children.length; j++) {\n if (!chrome.has(children[j])) { anchor = children[j]; break }\n }\n detached.push({ el, anchor })\n }\n for (const { el } of detached) el.remove()\n\n // Neutralize a surviving ghost observer (blue-fantasy's backdrop\n // re-writer) across theme flips during the try-on.\n const clearObserver = new MutationObserver(() => {\n for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop)\n })\n clearObserver.observe(body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })\n\n const neutralizeCss = skin === null ? undefined : NEUTRALIZE_CSS[skin.id]\n const neutralizeStyle = neutralizeCss === undefined ? null : this.injectStyle(neutralizeCss)\n\n return { skin, bodyAttr, bodyStyle, detached, clearObserver, neutralizeStyle }\n }\n\n /** Restore the active skin's captured visual state. */\n private restoreActive(active: ActiveVisuals): void {\n const body = document.body\n if (active.skin !== null && active.bodyAttr !== null) {\n body.setAttribute(active.skin.bodyAttr, active.bodyAttr)\n }\n if (active.bodyStyle !== null) {\n body.setAttribute('style', active.bodyStyle)\n } else {\n body.removeAttribute('style')\n }\n for (const { el, anchor } of active.detached) {\n body.insertBefore(el, anchor !== null && anchor.parentNode === body ? anchor : null)\n }\n active.clearObserver?.disconnect()\n active.neutralizeStyle?.remove()\n }\n\n private injectStyle(css: string): HTMLStyleElement {\n const tag = document.createElement('style')\n tag.dataset.skinCenterNeutralize = ''\n tag.textContent = css\n document.head.append(tag)\n return tag\n }\n}\n","/**\n * The skin-center card: rendered as the content of a first-level settings\n * section, listing every installed skin plus the official stock look. Live\n * try-on executes the real bundle inside the GUI (light/dark preview, full\n * restore on exit); Apply is one click — the host half runs `dsh-skin use`\n * through /api/skin-center/apply, the config watcher hot-reloads the patch,\n * and the page reloads into the new skin. Copy rides the standard `t` seat;\n * the theme preview control drives the official theme service (persisted,\n * same as the Appearance row).\n */\nimport { useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'\nimport { SKIN_CENTER_ENTRIES, type SkinCenterEntry } from './generated/skins.ts'\nimport { manifestHasSkin } from './manifest.ts'\nimport type { SkinBackgroundHandle } from './background.ts'\nimport { activeSkinEntry, TryOnController } from './try-on.ts'\nimport css from './skin-center.module.css'\n\n/** Business face the skin-center apply() injects into the card. */\nexport interface SkinCenterInjected {\n controller: TryOnController\n theme: {\n getTheme(): ThemeSnapshot\n subscribe(listener: () => void): () => void\n setTheme(id: 'light' | 'dark'): void\n }\n /** Background occluder over the shared skin-background namespace. */\n background: SkinBackgroundHandle\n}\n\n/** Plugin-card component props: locale seat + injected face. */\nexport type SkinCenterComponentProps =\n PropsLocale<'skinCenter'> & SkinCenterInjected\n\n/** The apply target of the official stock-look card. */\nconst OFFICIAL = 'official'\n\n/** Skin ids that read the background-scrim variable and paint a backdrop. */\nconst BACKDROP_SKIN_IDS = new Set(['blue-fantasy', 'whale-song'])\n\n/**\n * Render the skin-center card: a static header naming the plugin, with the\n * always-visible skin list (official default + every installed skin; try-on /\n * theme preview / one-click apply) rendered below it.\n * @param props - card props.\n * @returns the plugin card.\n */\nexport function SkinCenter({ t, controller, theme, background }: SkinCenterComponentProps) {\n const snapshot = useSyncExternalStore(theme.subscribe, theme.getTheme)\n const enabled = useSyncExternalStore(background.subscribe, background.enabled)\n const opacity = useSyncExternalStore(background.subscribe, background.opacity)\n const blurEmpty = useSyncExternalStore(background.subscribe, background.blurEmpty)\n const blurContent = useSyncExternalStore(background.subscribe, background.blurContent)\n const activePackage = activeSkinEntry()?.package\n const activeId = activeSkinEntry()?.id\n const backdropActive = activeId !== undefined && BACKDROP_SKIN_IDS.has(activeId)\n const [tryingId, setTryingId] = useState<string | null>(null)\n const [tryingOfficial, setTryingOfficial] = useState(false)\n const [loadingId, setLoadingId] = useState<string | null>(null)\n const [applying, setApplying] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n // Unmount guard for the confirmation poll: once the card is gone, the\n // pending timers must stop and no reload / setState may fire.\n const mounted = useRef(false)\n // Latest-click-wins token for async bundle loads. A chained try-on or exit\n // invalidates older completions so a slow bundle can never overwrite the\n // UI state chosen by a newer click.\n const tryOnRequest = useRef(0)\n useEffect(() => {\n mounted.current = true\n return () => { mounted.current = false }\n }, [])\n\n const tryOn = (entry: SkinCenterEntry): void => {\n if (loadingId === entry.id) return\n const request = ++tryOnRequest.current\n setError(null)\n setLoadingId(entry.id)\n void controller.tryOn(entry)\n .then(mountedTarget => {\n if (!mounted.current || request !== tryOnRequest.current || !mountedTarget) return\n setLoadingId(null)\n setTryingId(entry.id)\n setTryingOfficial(false)\n })\n .catch(() => {\n if (!mounted.current || request !== tryOnRequest.current) return\n // A load failure keeps the previous preview mounted; a mount failure\n // restores the original active skin. Mirror the controller's actual\n // session instead of blindly clearing a preview that may still exist.\n setLoadingId(null)\n setError(t('tryOnError'))\n setTryingId(controller.trying?.id ?? null)\n setTryingOfficial(controller.tryingOfficial)\n })\n }\n\n const tryOnOfficial = (): void => {\n ++tryOnRequest.current\n setError(null)\n setLoadingId(null)\n try {\n controller.tryOnOfficial()\n } catch {\n setError(t('tryOnError'))\n setTryingOfficial(false)\n return\n }\n setTryingId(null)\n setTryingOfficial(true)\n }\n\n const exitTryOn = (): void => {\n ++tryOnRequest.current\n controller.exit()\n setLoadingId(null)\n setTryingId(null)\n setTryingOfficial(false)\n }\n\n /**\n * Poll the host state until the config watcher reports the target active\n * (the patch write lands before the watcher re-applies it), or time out.\n * @param target - skin id, or `official` for the stock look.\n * @returns whether the target became active within the poll budget.\n */\n const confirmActive = (target: string): Promise<boolean> =>\n new Promise(resolve => {\n const expected = target === OFFICIAL ? 'none' : target\n let tries = 0\n const tick = (): void => {\n if (!mounted.current) {\n resolve(false)\n return\n }\n tries += 1\n void fetch('/api/skin-center/state')\n .then(async response => {\n const payload = await response.json().catch(() => null) as { ok?: boolean; active?: string } | null\n if (response.ok && payload?.ok === true && payload.active === expected) {\n resolve(true)\n return\n }\n if (tries >= 20 || !mounted.current) resolve(false)\n else window.setTimeout(tick, 250)\n })\n .catch(() => {\n if (tries >= 20 || !mounted.current) resolve(false)\n else window.setTimeout(tick, 250)\n })\n }\n tick()\n })\n\n /**\n * Poll the served GUI document until the boot manifest actually enables\n * the target (the config watcher regenerates it asynchronously after the\n * patch write — reloading earlier boots the page into the previous skin),\n * or time out.\n * @param target - skin id, or `official` for the stock look.\n * @returns whether the manifest caught up within the poll budget.\n */\n const manifestReady = (target: string): Promise<boolean> =>\n new Promise(resolve => {\n const expected = target === OFFICIAL ? null : target\n let tries = 0\n const tick = (): void => {\n if (!mounted.current) {\n resolve(false)\n return\n }\n tries += 1\n void fetch(window.location.href, { cache: 'no-store' })\n .then(async response => {\n const html = await response.text().catch(() => null)\n if (html !== null && manifestHasSkin(html, expected)) {\n resolve(true)\n return\n }\n if (tries >= 40 || !mounted.current) resolve(false)\n else window.setTimeout(tick, 500)\n })\n .catch(() => {\n if (tries >= 40 || !mounted.current) resolve(false)\n else window.setTimeout(tick, 500)\n })\n }\n tick()\n })\n\n /**\n * One-click apply: the host half runs `dsh-skin use <target>` (or\n * `use official`), the config watcher hot-reloads the patch within\n * seconds, then this page reloads to pick up the new boot graph. The\n * reload waits for both the patch (state poll) and the regenerated boot\n * manifest (manifest poll) so the page never boots into the old skin.\n * @param target - skin id, or `official` for the stock look.\n */\n const applySkin = (target: string): void => {\n setError(null)\n setApplying(target)\n const body = target === OFFICIAL ? { official: true } : { skin: target }\n void fetch('/api/skin-center/apply', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n .then(async response => {\n const payload = await response.json().catch(() => null) as { ok?: boolean; error?: string } | null\n if (!response.ok || payload?.ok !== true) {\n throw new Error(payload?.error ?? `HTTP ${response.status}`)\n }\n setApplying(null)\n // Patch written; reload only once the watcher reports the target\n // active AND the boot manifest caught up, so the page never boots\n // into the old skin.\n void confirmActive(target).then(confirmed => {\n if (!mounted.current) return\n if (!confirmed) {\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('appliedUnconfirmed')} — ${command}`)\n return\n }\n void manifestReady(target).then(ready => {\n if (!mounted.current) return\n if (ready) {\n window.location.reload()\n } else {\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('appliedUnconfirmed')} — ${command}`)\n }\n })\n })\n })\n .catch((cause: unknown) => {\n setApplying(null)\n const detail = cause instanceof Error ? cause.message : String(cause)\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('applyFailed')} (${detail}) — ${command}`)\n })\n }\n\n const dark = snapshot.active.colorScheme === 'dark'\n\n /** One row: try-on control + apply button. Shared by the official card and every skin card. */\n const actionButtons = (opts: {\n key: string\n isActive: boolean\n isTrying: boolean\n onTryOn: () => void\n applyLabel: string\n }): ReactNode => (\n <div className={css.actions}>\n {opts.isActive ? (\n <button type=\"button\" className={`${css.button} ${css.buttonGhost}`} disabled>\n {t('tryOn')}\n </button>\n ) : opts.isTrying ? (\n <button type=\"button\" className={`${css.button} ${css.buttonPrimary}`} onClick={exitTryOn}>\n {t('exitTryOn')}\n </button>\n ) : (\n <button\n type=\"button\"\n className={`${css.button} ${css.buttonPrimary}`}\n disabled={loadingId === opts.key}\n onClick={opts.onTryOn}\n >\n {loadingId === opts.key ? t('loading') : t('tryOn')}\n </button>\n )}\n <button\n type=\"button\"\n className={css.button}\n disabled={applying !== null || loadingId !== null}\n onClick={() => { applySkin(opts.key) }}\n >\n {applying === opts.key ? t('applying') : opts.applyLabel}\n </button>\n </div>\n )\n\n return (\n <li className={css.pluginCard}>\n <div className={css.cardHeaderStatic}>\n <span className={css.headText}>\n <span className={css.pluginName}>\n {t('title')}\n <span className={css.titleBadge}>{String(SKIN_CENTER_ENTRIES.length)}</span>\n </span>\n <span className={css.cardDescription} title={t('cardDescription')}>{t('cardDescription')}</span>\n </span>\n </div>\n\n <div className={css.cardBody}>\n <div className={css.enableRow}>\n <span className={css.enableLabel} title={t('enabled')}>{t('enabled')}</span>\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={enabled}\n aria-label={t('enabled')}\n className={enabled ? css.switch + ' ' + css.switchOn : css.switch}\n onClick={() => { background.setEnabled(!enabled) }}\n >\n <span className={css.switchThumb} />\n </button>\n <p className={css.enableHint}>{t('enabledHint')}</p>\n </div>\n {enabled\n ? (\n <>\n <div className={css.head}>\n <div className={css.intro} title={t('intro')}>{t('intro')}</div>\n <div className={css.themeRow}>\n <span className={css.themeLabel}>{t('theme')}</span>\n <button\n type=\"button\"\n className={`${css.themeButton} ${dark ? '' : css.themeButtonActive}`}\n onClick={() => { theme.setTheme('light') }}\n >\n {t('themeLight')}\n </button>\n <button\n type=\"button\"\n className={`${css.themeButton} ${dark ? css.themeButtonActive : ''}`}\n onClick={() => { theme.setTheme('dark') }}\n >\n {t('themeDark')}\n </button>\n </div>\n </div>\n\n <div className={css.backgroundRow}>\n <div className={css.backgroundHead}>\n <span className={css.backgroundLabel}>{t('backgroundOpacity')}</span>\n <span className={css.backgroundValue} aria-hidden=\"true\">{opacity}%</span>\n </div>\n <input\n id=\"skin-center-background-opacity\"\n className={css.backgroundRange}\n type=\"range\"\n min=\"0\"\n max=\"100\"\n step=\"5\"\n value={opacity}\n aria-valuetext={`${opacity}%`}\n aria-label={t('backgroundOpacity')}\n onChange={(event) => { background.set(Number(event.target.value)) }}\n />\n <p className={backdropActive ? css.backgroundHint : css.backgroundHintMuted}>\n {backdropActive ? t('backgroundHint') : t('backgroundHintInert')}\n </p>\n </div>\n <div className={css.backgroundRow}>\n <div className={css.backgroundHead}>\n <span className={css.backgroundLabel}>{t('backgroundBlurEmpty')}</span>\n <span className={css.backgroundValue} aria-hidden=\"true\">{blurEmpty}px</span>\n </div>\n <input\n id=\"skin-center-background-blur-empty\"\n className={css.backgroundRange}\n type=\"range\"\n min=\"0\"\n max=\"20\"\n step=\"1\"\n value={blurEmpty}\n aria-valuetext={`${blurEmpty}px`}\n aria-label={t('backgroundBlurEmpty')}\n onChange={(event) => { background.setBlurEmpty(Number(event.target.value)) }}\n />\n <div className={css.backgroundHead}>\n <span className={css.backgroundLabel}>{t('backgroundBlurContent')}</span>\n <span className={css.backgroundValue} aria-hidden=\"true\">{blurContent}px</span>\n </div>\n <input\n id=\"skin-center-background-blur-content\"\n className={css.backgroundRange}\n type=\"range\"\n min=\"0\"\n max=\"20\"\n step=\"1\"\n value={blurContent}\n aria-valuetext={`${blurContent}px`}\n aria-label={t('backgroundBlurContent')}\n onChange={(event) => { background.setBlurContent(Number(event.target.value)) }}\n />\n <p className={backdropActive ? css.backgroundHint : css.backgroundHintMuted}>\n {backdropActive ? t('backgroundBlurHint') : t('backgroundBlurInert')}\n </p>\n </div>\n\n\n {error !== null && <div className={css.error}>{error}</div>}\n\n <div className={css.list}>\n {(() => {\n const isActive = activePackage === undefined\n const isTrying = tryingOfficial\n const badge = isActive ? t('active') : isTrying ? t('tryingOn') : null\n return (\n <div className={css.card} key={OFFICIAL}>\n <div className={css.cardHead}>\n <span className={css.swatch} style={{ background: '#98a1ab' }} aria-hidden=\"true\" />\n <span className={css.cardName} title={t('official')}>{t('official')}</span>\n {badge !== null && (\n <span className={`${css.badge} ${isActive ? css.badgeActive : css.badgeTrying}`}>\n {badge}\n </span>\n )}\n </div>\n <div className={css.cardTagline} title={t('officialTagline')}>{t('officialTagline')}</div>\n {actionButtons({\n key: OFFICIAL,\n isActive,\n isTrying,\n onTryOn: tryOnOfficial,\n applyLabel: t('restore'),\n })}\n </div>\n )\n })()}\n\n {SKIN_CENTER_ENTRIES.map(entry => {\n const isActive = entry.package === activePackage\n const isTrying = entry.id === tryingId\n const badge = isActive ? t('active') : isTrying ? t('tryingOn') : null\n return (\n <div className={css.card} key={entry.id}>\n <div className={css.cardHead}>\n <span className={css.swatch} style={{ background: entry.accent }} aria-hidden=\"true\" />\n <span className={css.cardName} title={entry.nameEn}>{entry.nameEn}</span>\n {badge !== null && (\n <span className={`${css.badge} ${isActive ? css.badgeActive : css.badgeTrying}`}>\n {badge}\n </span>\n )}\n </div>\n <div className={css.cardTagline} title={entry.tagline}>{entry.tagline}</div>\n {actionButtons({\n key: entry.id,\n isActive,\n isTrying,\n onTryOn: () => { tryOn(entry) },\n applyLabel: t('apply'),\n })}\n </div>\n )\n })}\n </div>\n </>\n )\n : (\n <p className={css.offNote} role=\"status\">{t('offNote')}</p>\n )}\n </div>\n </li>\n )\n}\n\n/** Props the settings section binds for the skin-center card page. */\nexport type SkinCenterSectionProps =\n PropsRuntime<'settings.section'>\n & PropsLocale<'skinCenter'>\n & SkinCenterInjected\n\n/** Render the skin-center card as a first-level settings page. */\nexport function SkinCenterSection(props: SkinCenterSectionProps): ReactNode {\n const { t, controller, theme, background } = props\n return (\n <ul className={css.sectionList}>\n <SkinCenter t={t} controller={controller} theme={theme} background={background} />\n </ul>\n )\n}\n","/**\n * Background-scrim handle for the skin center: binds the `skin-background`\n * settings namespace and applies the chosen occlusion to the page's backdrop,\n * plus an optional per-state Gaussian blur of that backdrop.\n *\n * Occlusion is a CSS variable on `document.body` (--dsw-skin-scrim), which\n * backdrop-painting skins (blue-fantasy / whale-song) read inside their\n * setBackdrop() so the veil stays in sync across theme flips and try-on\n * restores. The official stock look paints no backdrop, so the variable is\n * inert there — the value still persists so it is ready for the next backdrop\n * skin.\n *\n * The Gaussian blur targets the same painted backdrop through a fixed child\n * of `document.body` using backdrop-filter: it samples the body's own\n * background painted behind it. Separate strengths apply to the empty\n * conversation and the conversation-with-content states, detected from the\n * shell's stable message-row class suffixes (hash prefix varies, suffix is\n * stable). When the active blur is 0 no element exists, so there is no GPU\n * cost. Blur never changes the occlusion behavior above.\n *\n * Occlusion values are 0-100 (0 = no extra veil, 100 = fully obscured); they\n * are written through as a 0..1 alpha for the CSS variable. Blur values are\n * 0-20 px. Dragging the controls applies instantly (live) and persists\n * through the settings scope.\n */\nimport type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-runtime/client'\n\n/** The namespace string the Host registers (mirrors src/index.ts). */\nexport const SKIN_BACKGROUND_NS = 'skin-background'\n\n/** Field of the background value inside the namespace section. */\nexport const OPACITY_FIELD = 'backgroundOpacity'\n\n/** Field of the empty-conversation backdrop blur inside the namespace section. */\nexport const BLUR_EMPTY_FIELD = 'backgroundBlurEmpty'\n\n/** Field of the with-content backdrop blur inside the namespace section. */\nexport const BLUR_CONTENT_FIELD = 'backgroundBlurContent'\n\n/** CSS custom property written to document.body and read by backdrop skins. */\nexport const SCRIM_VAR = '--dsw-skin-scrim'\n\n/** Default occlusion (0 = no extra veil) when the section carries none. */\nexport const DEFAULT_OPACITY = 0\n\n/** Default blur (0 = disabled) when the section carries none. */\nexport const DEFAULT_BLUR = 0\n\n/** The face the skin-center card injects for the background control. */\nexport interface SkinBackgroundHandle {\n /** Current master switch (true when the plugin is on). */\n enabled(): boolean\n /** Toggle + persist the master switch. */\n setEnabled(value: boolean): void\n /** Current occlusion 0-100 (also the getSnapshot seat for useSyncExternalStore). */\n opacity(): number\n /** Current empty-conversation backdrop blur 0-20 px. */\n blurEmpty(): number\n /** Current with-content backdrop blur 0-20 px. */\n blurContent(): number\n /** Observe a change in the applied values. */\n subscribe(listener: () => void): () => void\n /** Apply + persist a new occlusion. */\n set(opacity: number): void\n /** Apply + persist a new empty-conversation backdrop blur (0-20 px). */\n setBlurEmpty(value: number): void\n /** Apply + persist a new with-content backdrop blur (0-20 px). */\n setBlurContent(value: number): void\n /** Tear down the blur element and MutationObserver. */\n dispose(): void\n}\n\n/**\n * Selector for a conversation message row inside the shell's center column.\n * The `data-pane=\"conversation\"` attribute is stamped by the dsh-web-ui-all\n * compat shim on the center column; the _userRow / _compactionRow /\n * _contextRow / _turnErrorRow suffixes are the official shell's CSS-module\n * hashed message-row classes (hash prefix varies, suffix is stable). Stable\n * like the repo's compat shim, not hash-dependent.\n */\nconst CONVERSATION_CONTENT_SELECTOR = [\n '[data-pane=\"conversation\"] [class*=\"_userRow\"]',\n '[data-pane=\"conversation\"] [class*=\"_compactionRow\"]',\n '[data-pane=\"conversation\"] [class*=\"_contextRow\"]',\n '[data-pane=\"conversation\"] [class*=\"_turnErrorRow\"]',\n].join(', ')\n\n/**\n * Own the skin-background scope: read the latest occlusion + blur strengths,\n * apply them to the body instantly, and persist changes through the settings\n * scope.\n */\nexport class BackgroundController implements SkinBackgroundHandle {\n private enabledValue = true\n private opacityValue = DEFAULT_OPACITY\n private blurEmptyValue = DEFAULT_BLUR\n private blurContentValue = DEFAULT_BLUR\n private readonly listeners = new Set<() => void>()\n private readonly scope: SettingsScope<{\n enabled?: boolean\n backgroundOpacity?: number\n backgroundBlurEmpty?: number\n backgroundBlurContent?: number\n }>\n /** The fixed backdrop-filter element, present only while active blur > 0. */\n private blurElement: HTMLDivElement | null = null\n /** The body MutationObserver, installed lazily once a blur is active. */\n private observer: MutationObserver | null = null\n /** Pending requestAnimationFrame id for a coalesced recheck. */\n private rafId: number | null = null\n /** Guard: after dispose no scheduled work may reinstall anything. */\n private disposed = false\n\n /**\n * @param scope - the bound skin-background settings scope.\n */\n constructor(scope: SettingsScope<{\n enabled?: boolean\n backgroundOpacity?: number\n backgroundBlurEmpty?: number\n backgroundBlurContent?: number\n }>) {\n this.scope = scope\n this.enabledValue = this.readEnabled()\n this.opacityValue = this.readOpacity()\n this.blurEmptyValue = this.readBlur(BLUR_EMPTY_FIELD)\n this.blurContentValue = this.readBlur(BLUR_CONTENT_FIELD)\n this.applyOcclusion()\n this.syncBlur()\n scope.subscribe(() => {\n this.enabledValue = this.readEnabled()\n this.opacityValue = this.readOpacity()\n this.blurEmptyValue = this.readBlur(BLUR_EMPTY_FIELD)\n this.blurContentValue = this.readBlur(BLUR_CONTENT_FIELD)\n this.applyOcclusion()\n this.syncBlur()\n this.publish()\n })\n }\n\n enabled(): boolean { return this.enabledValue }\n\n setEnabled(value: boolean): void {\n this.enabledValue = value\n this.applyOcclusion()\n this.syncBlur()\n this.publish()\n void this.scope.set('enabled', value)\n }\n\n opacity(): number { return this.opacityValue }\n\n blurEmpty(): number { return this.blurEmptyValue }\n\n blurContent(): number { return this.blurContentValue }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n set(opacity: number): void {\n const clamped = Math.max(0, Math.min(100, Math.round(opacity)))\n this.opacityValue = clamped\n this.applyOcclusion()\n this.publish()\n // Persist: queue the write on the settings scope. Failures are silent —\n // the live value is already applied, and the write drains on reconnect.\n void this.scope.set(OPACITY_FIELD, clamped)\n }\n\n setBlurEmpty(value: number): void {\n const clamped = this.clampBlur(value)\n this.blurEmptyValue = clamped\n this.ensureObserver()\n this.syncBlur()\n this.publish()\n void this.scope.set(BLUR_EMPTY_FIELD, clamped)\n }\n\n setBlurContent(value: number): void {\n const clamped = this.clampBlur(value)\n this.blurContentValue = clamped\n this.ensureObserver()\n this.syncBlur()\n this.publish()\n void this.scope.set(BLUR_CONTENT_FIELD, clamped)\n }\n\n dispose(): void {\n this.disposed = true\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId)\n this.rafId = null\n }\n this.removeBlurElement()\n if (this.observer !== null) {\n this.observer.disconnect()\n this.observer = null\n }\n }\n\n /** The effective master-switch section value, defaulting to true when absent. */\n private readEnabled(): boolean {\n const snapshot: SettingsScopeSnapshot<{ enabled?: boolean }> = this.scope.getSnapshot()\n const raw = snapshot.value?.enabled\n return typeof raw !== 'boolean' ? true : raw\n }\n\n /** The effective occlusion section value, clamped 0-100, defaulting to 0. */\n private readOpacity(): number {\n const snapshot: SettingsScopeSnapshot<{ backgroundOpacity?: number }> = this.scope.getSnapshot()\n const raw = snapshot.value?.backgroundOpacity\n if (typeof raw !== 'number' || !Number.isFinite(raw)) return DEFAULT_OPACITY\n return Math.max(0, Math.min(100, raw))\n }\n\n /** The effective blur section value for one field, clamped 0-20, defaulting to 0. */\n private readBlur(field: 'backgroundBlurEmpty' | 'backgroundBlurContent'): number {\n const snapshot: SettingsScopeSnapshot<{\n backgroundBlurEmpty?: number\n backgroundBlurContent?: number\n }> = this.scope.getSnapshot()\n const raw = snapshot.value?.[field]\n if (typeof raw !== 'number' || !Number.isFinite(raw)) return DEFAULT_BLUR\n return this.clampBlur(raw)\n }\n\n private clampBlur(value: number): number {\n return Math.max(0, Math.min(20, Math.round(value)))\n }\n\n /** Write the current occlusion onto the body CSS variable (0..1 alpha). */\n private applyOcclusion(): void {\n if (!this.enabledValue) {\n document.body.style.removeProperty(SCRIM_VAR)\n return\n }\n document.body.style.setProperty(SCRIM_VAR, String(this.opacityValue / 100))\n }\n\n /**\n * Apply the active blur: empty or with-content strength depending on the\n * conversation state. A value > 0 ensures the fixed blur element exists\n * with the matching backdrop-filter; 0 removes it.\n */\n private syncBlur(): void {\n if (this.disposed) return\n if (!this.enabledValue) {\n this.removeBlurElement()\n return\n }\n this.ensureObserver()\n const active = this.hasConversationContent() ? this.blurContentValue : this.blurEmptyValue\n if (active > 0) this.ensureBlurElement(active)\n else this.removeBlurElement()\n }\n\n /** True when the conversation pane hosts at least one message row. */\n private hasConversationContent(): boolean {\n return document.querySelector(CONVERSATION_CONTENT_SELECTOR) !== null\n }\n\n /** Create (if needed) and size the fixed backdrop-filter element. */\n private ensureBlurElement(active: number): void {\n if (this.blurElement === null) {\n const element = document.createElement('div')\n element.style.position = 'fixed'\n element.style.inset = '0'\n element.style.zIndex = '-1'\n element.style.pointerEvents = 'none'\n element.setAttribute('aria-hidden', 'true')\n this.blurElement = element\n document.body.appendChild(element)\n }\n const blur = 'blur(' + active + 'px)'\n this.blurElement.style.backdropFilter = blur\n // Safari: the vendor-prefixed form is only reachable via setProperty.\n this.blurElement.style.setProperty('-webkit-backdrop-filter', blur)\n }\n\n /** Remove the fixed blur element, if present. */\n private removeBlurElement(): void {\n if (this.blurElement === null) return\n this.blurElement.remove()\n this.blurElement = null\n }\n\n /**\n * Install the MutationObserver on document.body only when either blur\n * field is active, so a fully-disabled blur never pays the observation\n * cost. Runs lazily on the first non-zero set.\n */\n private ensureObserver(): void {\n if (this.disposed || this.observer !== null) return\n if (this.blurEmptyValue <= 0 && this.blurContentValue <= 0) return\n this.observer = new MutationObserver(() => this.scheduleRecheck())\n this.observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['class'],\n })\n }\n\n /** Coalesce burst mutations into one rAF-delayed recheck. */\n private scheduleRecheck(): void {\n if (this.disposed || this.rafId !== null) return\n this.rafId = requestAnimationFrame(() => {\n this.rafId = null\n if (this.disposed) return\n this.syncBlur()\n })\n }\n\n private publish(): void {\n for (const listener of this.listeners) listener()\n }\n}\n","/**\n * Skin-center locale dictionaries. The plugin-card name, its description,\n * and every control of the in-GUI skin center is localized through the\n * standard `t` seat.\n */\n\n/** Copy keys owned by this plugin. */\nexport type SkinCenterKey =\n | 'title'\n | 'cardDescription'\n | 'enabled'\n | 'enabledHint'\n | 'offNote'\n | 'intro'\n | 'official'\n | 'officialTagline'\n | 'active'\n | 'tryingOn'\n | 'tryOn'\n | 'loading'\n | 'exitTryOn'\n | 'apply'\n | 'applying'\n | 'restore'\n | 'applyFailed'\n | 'appliedUnconfirmed'\n | 'theme'\n | 'themeLight'\n | 'themeDark'\n | 'tryOnError'\n | 'backgroundOpacity'\n | 'backgroundBlurEmpty'\n | 'backgroundBlurContent'\n | 'backgroundBlurHint'\n | 'backgroundBlurInert'\n | 'backgroundHint'\n | 'backgroundHintInert'\n\nexport const en: Record<SkinCenterKey, string> = {\n title: 'Skin Center',\n cardDescription: 'Try on any installed skin live in the GUI — exit restores instantly, applying persists in one click.',\n enabled: 'Enable skin center',\n enabledHint: 'When off, try-on, apply and background controls are disabled; turn it back on to resume.',\n offNote: 'The skin center is turned off.',\n intro: 'Try on any skin live — it takes effect instantly, exit restores the current look. Apply persists it across restarts.',\n official: 'Official default',\n officialTagline: 'The stock DSH look with no skin applied.',\n active: 'Active',\n tryingOn: 'Trying on',\n tryOn: 'Try on',\n loading: 'Loading…',\n exitTryOn: 'Exit try-on',\n apply: 'Apply',\n applying: 'Applying…',\n restore: 'Restore',\n applyFailed: 'Apply failed',\n appliedUnconfirmed: 'Applied, but the change has not been confirmed — refresh the page if the skin did not switch',\n theme: 'Theme preview',\n themeLight: 'Light',\n themeDark: 'Dark',\n tryOnError: 'Try-on failed — see console',\n backgroundOpacity: 'Background occlusion',\n backgroundBlurEmpty: 'Blur when empty',\n backgroundBlurContent: 'Blur with content',\n backgroundBlurHint: 'Applies a separate Gaussian blur to the backdrop for the empty conversation and the conversation with content; 0 disables.',\n backgroundBlurInert: 'Visible only with skins that paint a backdrop; the official default has none.',\n backgroundHint: 'Instantly veils the backdrop behind the panels — higher values obscure the art to help you focus.',\n backgroundHintInert: 'Only applies to skins that paint a backdrop (Blue Fantasy / Whale Song). Applies to the official default automatically once such a skin is active.',\n}\n\nexport const zh: Record<SkinCenterKey, string> = {\n title: '皮肤中心',\n cardDescription: '在 GUI 内即时试穿任意皮肤,退出即完全还原;应用一键完成并自动刷新。',\n enabled: '启用皮肤中心',\n enabledHint: '关闭后停用试穿、应用与背景控件,重新打开即恢复。',\n offNote: '皮肤中心已关闭。',\n intro: '任意皮肤可即时试穿,退出即完全还原;「应用」一键持久化,页面自动刷新生效。',\n official: '官方默认',\n officialTagline: '还原 DSH 官方默认外观,不应用任何皮肤。',\n active: '当前激活',\n tryingOn: '试穿中',\n tryOn: '试穿',\n loading: '加载中…',\n exitTryOn: '退出试穿',\n apply: '应用',\n applying: '应用中…',\n restore: '恢复默认',\n applyFailed: '应用失败',\n appliedUnconfirmed: '已写入配置但尚未确认生效——若皮肤未切换请手动刷新页面',\n theme: '主题预览',\n themeLight: '亮色',\n themeDark: '暗色',\n tryOnError: '试穿失败,详见控制台',\n backgroundOpacity: '背景遮挡',\n backgroundBlurEmpty: '空对话背景模糊',\n backgroundBlurContent: '有对话背景模糊',\n backgroundBlurHint: '对话为空与有内容时分别应用不同的背景高斯模糊强度,0 为关闭。',\n backgroundBlurInert: '仅对带背景图插画的皮肤可见;官方默认无背景图。',\n backgroundHint: '即时为面板背后的背景加遮罩——数值越高越能弱化插画,帮你集中注意力。',\n backgroundHintInert: '仅对带背景图插画的皮肤(蓝色幻想 / 鲸吟)生效;官方默认无背景图,该滑块对这些皮肤自动生效。',\n}\n","/**\n * In-GUI skin center, browser half: registers the Skin Center as a first-level\n * settings section (`settings.section`) and provides the try-on controller +\n * official theme handle to it. The section lists every installed skin\n * (embedded registry), tries it on live inside the GUI, exits with a full\n * restore, and applies the chosen skin in one click. The plugin writes only\n * DOM and the settings ledger — no services, no events, no model access.\n */\nimport type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { SkinCenterSection, type SkinCenterInjected } from './SkinCenter.tsx'\nimport { BackgroundController, SKIN_BACKGROUND_NS } from './background.ts'\nimport { en, zh, type SkinCenterKey } from './locales.ts'\nimport { TryOnController } from './try-on.ts'\n\nexport type { SkinCenterComponentProps, SkinCenterInjected } from './SkinCenter.tsx'\nexport { TryOnController } from './try-on.ts'\n\n/** Locale namespace owned by this plugin. */\nexport const NS = 'skinCenter'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The skin-center card's copy. */\n skinCenter: SkinCenterKey\n }\n}\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n /**\n * Optional rc.6 compatibility binder provided by dsh-web-ui-settings;\n * absent when that group plugin is not installed, so callers fall back to\n * the official settings scope.\n */\n webUiSettings?: { bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S> }\n }\n}\n\n\n/** Required services: slots + locale (plugin card), theme (preview toggle), and settingsScope + its transport (background scrim). */\nexport const inject = ['slots', 'locale', 'theme', 'settingsScope', 'connection', 'remote']\n\n/**\n * Register the skin-center dictionaries, the body scope attribute, and the\n * Skin Center as a first-level settings section.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-skin-center: dictionaries')\n\n // The card's own styles scope under this attribute so they keep applying\n // during try-on (when the active skin's attribute is retracted).\n ctx.effect(() => {\n document.body.dataset.dshSkinCenter = ''\n return () => { delete document.body.dataset.dshSkinCenter }\n }, 'ui-skin-center: body scope')\n\n const theme = ctx.get('theme') as ThemeRuntime\n const controller = new TryOnController()\n // Background occluder over the shared skin-background namespace. The scope\n // is bound to this plugin's fiber, so it is torn down with the card.\n const binder = ctx.get('webUiSettings') ?? ctx.settingsScope\n const backgroundScope = binder.bind<{\n enabled?: boolean\n backgroundOpacity?: number\n backgroundBlurEmpty?: number\n backgroundBlurContent?: number\n }>({ namespace: SKIN_BACKGROUND_NS })\n const background = new BackgroundController(backgroundScope)\n // Tear the blur element + observer down when this plugin's fiber goes away.\n ctx.effect(() => () => background.dispose(), 'ui-skin-center: background dispose')\n const injected = (): SkinCenterInjected => ({\n controller,\n theme: {\n getTheme: () => theme.getTheme(),\n subscribe: listener => ctx.on('theme/change', listener),\n setTheme: id => theme.setTheme(id),\n },\n background: {\n enabled: () => background.enabled(),\n setEnabled: value => background.setEnabled(value),\n opacity: () => background.opacity(),\n blurEmpty: () => background.blurEmpty(),\n blurContent: () => background.blurContent(),\n subscribe: listener => background.subscribe(listener),\n set: opacity => background.set(opacity),\n setBlurEmpty: value => background.setBlurEmpty(value),\n setBlurContent: value => background.setBlurContent(value),\n dispose: () => background.dispose(),\n },\n })\n\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'skin-center',\n order: 120,\n label: () => ctx.locale.bind('skinCenter')('title'),\n locale: 'skinCenter',\n inject: injected,\n }, SkinCenterSection))\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -1149,8 +1149,16 @@ const inject = ["webServer"];
1149
1149
  * scope without depending on this Host package.
1150
1150
  */
1151
1151
  const SKIN_BACKGROUND_NAMESPACE = settingsNamespace("skin-background");
1152
- /** Runtime schema for SkinBackgroundConfig. */
1153
- const SkinBackgroundConfigSchema = z.object({ backgroundOpacity: z.number().min(0).max(100).step(5).default(0) });
1152
+ /**
1153
+ * Runtime schema for SkinBackgroundConfig. Persists the master switch
1154
+ * (`enabled`) alongside the background strength fields.
1155
+ */
1156
+ const SkinBackgroundConfigSchema = z.object({
1157
+ enabled: z.boolean().default(true),
1158
+ backgroundOpacity: z.number().min(0).max(100).step(5).default(0),
1159
+ backgroundBlurEmpty: z.number().min(0).max(20).step(1).default(0),
1160
+ backgroundBlurContent: z.number().min(0).max(20).step(1).default(0)
1161
+ });
1154
1162
  /**
1155
1163
  * Register the skin-center API routes.
1156
1164
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-skin-center",
3
3
  "description": "In-GUI skin center for the dsh web GUI: lists the official default plus every installed skin (qq98 / ths / xp / blue-fantasy / dragon-heir / minecraft), tries it on live inside the real GUI (executes the actual client bundle, light/dark preview), exits with a full restore, and applies in one click through the host /api/skin-center API (dsh-skin use, hot-reloaded, no restart)",
4
- "version": "0.1.17",
4
+ "version": "0.1.18",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {