@linxin666/dsh-client-ui-skin-center 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,10 @@
21
21
  写入 `~/.dsh/cordis.patch.yml` 后由 DSH 配置 watcher 秒级热载入,页面自动刷新生效——
22
22
  **无需重启 dsh web,无需复制命令**。应用失败时错误提示里附带终端兜底命令。
23
23
  host 依赖 `dsh-skin` CLI 在 PATH(`~/.local/bin/dsh-skin`,仓库 `scripts/dsh-skin`)。
24
+ Windows 兼容性:`dsh-skin` 的 harness home 取 `$DSH_HOME`(缺省 `~/.dsh`),仓库根目录从
25
+ 脚本自身位置推导(可用 `DSH_SKIN_REPO` 覆盖),不依赖 `$HOME` 与固定路径;Windows 用户
26
+ 自建 `dsh-skin.cmd` 包装(内容 `node scripts/dsh-skin %*`)放入 PATH 目录即可。host 端在
27
+ Windows 上经 shell 调用该命令——`execFile` 无法直接启动裸名 `.cmd`。
24
28
 
25
29
  ## 安装(官方 plugin bundle 方式)
26
30
 
package/lib/client.js CHANGED
@@ -143,6 +143,46 @@ window.__ModuleLoader__.load({
143
143
  "bodyAttr": "data-dsh-whale-song",
144
144
  "package": "@linxin666/dsh-client-ui-skin-whale-song",
145
145
  "order": 7
146
+ },
147
+ {
148
+ "id": "trading",
149
+ "name": "交易终端",
150
+ "nameEn": "Trading Terminal",
151
+ "author": "dsh-web-ui",
152
+ "tagline": "实时行情跑马灯 · 长桥港美股行情 · 红涨绿跌交易终端",
153
+ "description": "结合 dsh-fun-ticker 行情跑马灯与 dsh-longbridge 港美股行情的炒股皮肤:顶栏滚动 A股/港股/美股/指数/加密/外汇报价(装 fun-ticker 后跟随你的自选列表),状态栏展示长桥行情快照与 A股/港股/美股交易时段,写代码也像盯盘。",
154
+ "tags": [
155
+ "stock",
156
+ "trading",
157
+ "ticker",
158
+ "live",
159
+ "terminal",
160
+ "longbridge"
161
+ ],
162
+ "accent": "#f23645",
163
+ "bodyAttr": "data-dsh-trading",
164
+ "package": "@linxin666/dsh-client-ui-skin-trading",
165
+ "order": 8
166
+ },
167
+ {
168
+ "id": "miku",
169
+ "name": "初音未来 · 电子歌姬",
170
+ "nameEn": "Hatsune Miku",
171
+ "author": "涂山苏苏",
172
+ "tagline": "蓝紫双马尾 · 01 编号 · 音符波形 · 电子歌姬主题",
173
+ "description": "以世界第一的虚拟歌姬初音未来为灵感的主题皮肤:蓝紫洋红渐变贯穿全局,音符与声波曲线点缀在半透明面板之间,标题栏与状态栏带有 01 编号徽标与音乐波形,半透明毛玻璃面板透出背景图——沉浸式电子歌姬氛围。",
174
+ "tags": [
175
+ "miku",
176
+ "vocaloid",
177
+ "blue",
178
+ "music",
179
+ "idol",
180
+ "waveform"
181
+ ],
182
+ "accent": "#2e9bff",
183
+ "bodyAttr": "data-dsh-miku",
184
+ "package": "@linxin666/dsh-client-ui-skin-miku",
185
+ "order": 9
146
186
  }
147
187
  ];
148
188
  //#endregion
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/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\":\"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\":1},\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\":2},\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\":3},\n\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\":4},\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\":5},\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\":6},\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\":7},\n\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 * 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 /** Start trying on `entry` (replaces any live session). */\n async tryOn(entry: SkinCenterEntry): Promise<void> {\n if (entry.package === activeSkinEntry()?.package) return\n this.exit()\n const epoch = ++this.epoch\n\n const active: ActiveVisuals = this.captureAndRetractActive()\n let dispose: (() => void) | undefined\n try {\n dispose = await this.loadAndApply(entry)\n } catch (error) {\n if (epoch === this.epoch) this.restoreActive(active)\n throw error\n }\n if (epoch !== this.epoch) {\n // Superseded while loading (a newer try-on or exit started): drop only\n // what this attempt mounted — the newer operation owns the surface and\n // captured the active-skin visuals it needs on exit.\n this.cleanupModule(entry)\n dispose()\n return\n }\n this.session = { entry, dispose, active }\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.exit()\n this.epoch += 1\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 const session = this.session\n if (session === null) return\n this.epoch += 1\n this.session = null\n session.dispose()\n if (session.entry !== null) this.cleanupModule(session.entry)\n this.restoreActive(session.active)\n }\n\n /** Execute + materialize + mount the target skin through the real loader. */\n private async loadAndApply(entry: SkinCenterEntry): Promise<() => void> {\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 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 { 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 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 [applying, setApplying] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n const tryOn = (entry: SkinCenterEntry): void => {\n setError(null)\n void controller.tryOn(entry)\n .then(() => {\n setTryingId(entry.id)\n setTryingOfficial(false)\n })\n .catch(() => {\n // The controller may have torn down a previous session before the\n // load failed; reset both flags so no stale \"trying on\" lingers.\n setError(t('tryOnError'))\n setTryingId(null)\n setTryingOfficial(false)\n })\n }\n\n const tryOnOfficial = (): void => {\n setError(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 controller.exit()\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 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) resolve(false)\n else window.setTimeout(tick, 250)\n })\n .catch(() => {\n if (tries >= 20) resolve(false)\n else window.setTimeout(tick, 250)\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.\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, so the page never boots into the old skin.\n void confirmActive(target).then(confirmed => {\n if (confirmed) {\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 .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 type=\"button\" className={`${css.button} ${css.buttonPrimary}`} onClick={opts.onTryOn}>\n {t('tryOn')}\n </button>\n )}\n <button\n type=\"button\"\n className={css.button}\n disabled={applying !== 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 <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 <span className={open ? css.chevronOpen : css.chevron}>▾</span>\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","/**\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 | '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 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 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 } 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\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 backgroundScope = ctx.settingsScope.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/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\":\"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\":1},\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\":2},\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\":3},\n\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\":4},\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\":5},\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\":6},\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\":7},\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\":8},\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\":9},\n\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 * 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 /** Start trying on `entry` (replaces any live session). */\n async tryOn(entry: SkinCenterEntry): Promise<void> {\n if (entry.package === activeSkinEntry()?.package) return\n this.exit()\n const epoch = ++this.epoch\n\n const active: ActiveVisuals = this.captureAndRetractActive()\n let dispose: (() => void) | undefined\n try {\n dispose = await this.loadAndApply(entry)\n } catch (error) {\n if (epoch === this.epoch) this.restoreActive(active)\n throw error\n }\n if (epoch !== this.epoch) {\n // Superseded while loading (a newer try-on or exit started): drop only\n // what this attempt mounted — the newer operation owns the surface and\n // captured the active-skin visuals it needs on exit.\n this.cleanupModule(entry)\n dispose()\n return\n }\n this.session = { entry, dispose, active }\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.exit()\n this.epoch += 1\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 const session = this.session\n if (session === null) return\n this.epoch += 1\n this.session = null\n session.dispose()\n if (session.entry !== null) this.cleanupModule(session.entry)\n this.restoreActive(session.active)\n }\n\n /** Execute + materialize + mount the target skin through the real loader. */\n private async loadAndApply(entry: SkinCenterEntry): Promise<() => void> {\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 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 { 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 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 [applying, setApplying] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n const tryOn = (entry: SkinCenterEntry): void => {\n setError(null)\n void controller.tryOn(entry)\n .then(() => {\n setTryingId(entry.id)\n setTryingOfficial(false)\n })\n .catch(() => {\n // The controller may have torn down a previous session before the\n // load failed; reset both flags so no stale \"trying on\" lingers.\n setError(t('tryOnError'))\n setTryingId(null)\n setTryingOfficial(false)\n })\n }\n\n const tryOnOfficial = (): void => {\n setError(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 controller.exit()\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 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) resolve(false)\n else window.setTimeout(tick, 250)\n })\n .catch(() => {\n if (tries >= 20) resolve(false)\n else window.setTimeout(tick, 250)\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.\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, so the page never boots into the old skin.\n void confirmActive(target).then(confirmed => {\n if (confirmed) {\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 .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 type=\"button\" className={`${css.button} ${css.buttonPrimary}`} onClick={opts.onTryOn}>\n {t('tryOn')}\n </button>\n )}\n <button\n type=\"button\"\n className={css.button}\n disabled={applying !== 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 <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 <span className={open ? css.chevronOpen : css.chevron}>▾</span>\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","/**\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 | '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 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 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 } 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\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 backgroundScope = ctx.settingsScope.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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"}
package/lib/index.js CHANGED
@@ -2,8 +2,8 @@ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-sett
2
2
  import z from "schemastery";
3
3
  import { lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
5
  import { homedir } from "node:os";
6
+ import { fileURLToPath } from "node:url";
7
7
  //#region src/skin-switch.ts
8
8
  /**
9
9
  * In-process skin switching for the skin center — the official `dsh-skin use`
@@ -22,8 +22,27 @@ import { homedir } from "node:os";
22
22
  * dictionary, so adding a skin needs no code change here.
23
23
  * @module @linxin666/dsh-client-ui-skin-center/skin-switch
24
24
  */
25
- /** Repo layout: skin bundles live at packages/skins/<id>. */
26
- const SKINS_DIR$1 = fileURLToPath(new URL("../../../skins/", import.meta.url));
25
+ /**
26
+ * Resolve the directory that holds the skin packages (each a dir carrying a
27
+ * skin.json). Both install layouts resolve with new URL('../../', ...):
28
+ * - monorepo: packages/skins/skin-center/lib/../../ -> packages/skins/
29
+ * - npm install: node_modules/@linxin666/dsh-client-ui-skin-center/lib/../..
30
+ * -> node_modules/@linxin666/ (the scoped dir holding dsh-client-ui-skin-*)
31
+ * The legacy '../../../skins/' spelling (which pointed at node_modules/skins/
32
+ * under npm — the ENOENT of zhu1090093659/dsh-web-ui#21/#33/#34) is kept as a
33
+ * fallback candidate, and DSH_SKINS_DIR overrides everything (tests use it).
34
+ */
35
+ function resolveSkinsDir() {
36
+ const fromEnv = process.env.DSH_SKINS_DIR;
37
+ if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
38
+ const candidates = [fileURLToPath(new URL("../../", import.meta.url)), fileURLToPath(new URL("../../../skins/", import.meta.url))];
39
+ for (const candidate of candidates) try {
40
+ for (const dir of readdirSync(candidate)) if (statSync(join(candidate, dir, "skin.json"), { throwIfNoEntry: false })) return candidate;
41
+ } catch {}
42
+ return candidates[0];
43
+ }
44
+ /** The skin-package root for this install (see resolveSkinsDir). */
45
+ const SKINS_DIR = resolveSkinsDir();
27
46
  /** Managed patch-section delimiters (the CLI's SINGLE authority boundaries). */
28
47
  const MANAGED_START = "# --- dsh-skin managed (auto-generated; do not edit) ---";
29
48
  const MANAGED_END = "# --- end dsh-skin managed ---";
@@ -33,11 +52,12 @@ const DEFAULT_PROFILE = process.env.DSH_SKIN_PROFILE ?? "web";
33
52
  * Parse the switch-relevant fields of one skin.json. Returns null for
34
53
  * anything that is not a valid skin so it is simply skipped — never walking
35
54
  * outside the skins tree (the id is validated before any path use).
36
- * @param dir - directory name under packages/skins/.
55
+ * @param dir - directory name under the skins root.
56
+ * @param skinsDir - the skins root (defaults to the resolved install layout).
37
57
  */
38
- function readSkinMeta(dir) {
58
+ function readSkinMeta(dir, skinsDir = SKINS_DIR) {
39
59
  try {
40
- const meta = JSON.parse(readFileSync(join(SKINS_DIR$1, dir, "skin.json"), "utf8"));
60
+ const meta = JSON.parse(readFileSync(join(skinsDir, dir, "skin.json"), "utf8"));
41
61
  if (typeof meta !== "object" || meta === null) return null;
42
62
  const record = meta;
43
63
  if (typeof record.id !== "string" || !/^[a-z0-9-]+$/.test(record.id)) return null;
@@ -58,21 +78,31 @@ function readSkinMeta(dir) {
58
78
  }
59
79
  }
60
80
  /**
61
- * Derive the skin registry from packages/skins/<id>/skin.json — the single
81
+ * Derive the skin registry from each skin dir's skin.json — the single
62
82
  * source of truth (skin.json already carries package/wiring.id/bundleWired).
63
83
  * Replaces the CLI's hand-maintained SKINS dictionary, so adding a skin
64
- * needs no code change here.
84
+ * needs no code change here. Directories without a readable skin.json
85
+ * (the skin-center manager itself, non-skin packages in the npm scoped dir)
86
+ * are skipped. The root is injectable so tests can point at either install
87
+ * layout.
88
+ * @param skinsDir - the skins root (defaults to the resolved install layout).
65
89
  * @returns skin id -> switch metadata.
66
90
  */
67
- function loadRegistry() {
91
+ function loadRegistry(skinsDir = SKINS_DIR) {
68
92
  const out = {};
69
- for (const dir of readdirSync(SKINS_DIR$1)) {
70
- const meta = readSkinMeta(dir);
93
+ let entries;
94
+ try {
95
+ entries = readdirSync(skinsDir);
96
+ } catch {
97
+ return out;
98
+ }
99
+ for (const dir of entries) {
100
+ const meta = readSkinMeta(dir, skinsDir);
71
101
  if (meta === null || meta.wiring === void 0 || meta.package === void 0) continue;
72
102
  out[meta.id] = {
73
103
  pkg: meta.package,
74
104
  id: meta.wiring.id,
75
- dir: join(SKINS_DIR$1, dir),
105
+ dir: join(skinsDir, dir),
76
106
  bundleWired: meta.wiring.bundleWired === true
77
107
  };
78
108
  }
@@ -184,9 +214,18 @@ function writePatchAtomic(filePath, next) {
184
214
  renameSync(tmp, filePath);
185
215
  }
186
216
  /**
187
- * Make the profile node_modules symlink for a skin. Returns true when a new
188
- * link was created, false when it already pointed at the skin dir. Refuses
189
- * to touch a non-symlink target (that path is not ours to clobber).
217
+ * Make the profile node_modules link for a skin. Returns true when a new
218
+ * link was created, false when the target was already resolvable.
219
+ *
220
+ * A target that already resolves (a REAL installed directory, e.g. the npm
221
+ * layout where the skin package sits at node_modules/@linxin666/..., or a
222
+ * symlink/junction pointing at the skin dir) is left untouched — there is
223
+ * nothing to link. Only an existing link pointing elsewhere is refreshed.
224
+ * A plain FILE target is still refused (that path is not ours to clobber).
225
+ *
226
+ * On win32 the link falls back to a directory junction (absolute target) when
227
+ * symlink creation fails with a privilege error, so no Developer Mode or
228
+ * elevation is required (zhu1090093659/dsh-web-ui#24).
190
229
  * @param entry - the skin switch entry.
191
230
  * @param profileModulesDir - the profile's node_modules dir.
192
231
  */
@@ -196,13 +235,19 @@ function ensureSymlink(entry, profileModulesDir) {
196
235
  try {
197
236
  stat = lstatSync(target);
198
237
  } catch {}
199
- if (stat) {
200
- if (!stat.isSymbolicLink()) throw new Error(`${target} exists and is not a symlink — refusing to touch it`);
238
+ if (stat) if (stat.isSymbolicLink()) {
201
239
  if (readlinkSync(target) === entry.dir) return false;
202
240
  unlinkSync(target);
203
- }
241
+ } else if (stat.isDirectory()) return false;
242
+ else throw new Error(`${target} exists and is not a symlink or directory — refusing to touch it`);
204
243
  mkdirSync(dirname(target), { recursive: true });
205
- symlinkSync(entry.dir, target);
244
+ try {
245
+ symlinkSync(entry.dir, target);
246
+ } catch (error) {
247
+ const code = error?.code;
248
+ if (process.platform === "win32" && typeof code === "string" && SYMLINK_PRIVILEGE_CODES.includes(code)) symlinkSync(entry.dir, target, "junction");
249
+ else throw error;
250
+ }
206
251
  return true;
207
252
  }
208
253
  /** Windows/privilege code points where symlinkSync fails. */
@@ -236,7 +281,8 @@ function symlinkFriendly(caller, fn) {
236
281
  function checkInstalled(entry, profileModulesDir) {
237
282
  let ok = false;
238
283
  try {
239
- ok = lstatSync(join(profileModulesDir, entry.pkg)).isSymbolicLink();
284
+ const stat = lstatSync(join(profileModulesDir, entry.pkg));
285
+ ok = stat.isSymbolicLink() || stat.isDirectory();
240
286
  } catch {}
241
287
  return ok ? null : `${entry.pkg} 未安装到 profile;先用 dsh-skin install ${entry.id.replace(/^ui-skin-/, "")}(或 dsh plugin --profile ${DEFAULT_PROFILE} add ${entry.dir})安装,否则加载会失败。`;
242
288
  }
@@ -435,12 +481,12 @@ function postRoute(path, run) {
435
481
  }
436
482
  };
437
483
  }
438
- /** Repo layout: skin bundles live at packages/skins/<id>/lib/client.js. */
439
- const SKINS_DIR = fileURLToPath(new URL("../../../skins/", import.meta.url));
440
484
  /**
441
- * Map skin id -> directory under packages/skins/, scanned from each
485
+ * Map skin id -> directory under the skins root, scanned from each
442
486
  * skin.json. The id is validated against this map (never used as a raw
443
- * path) so the bundle route cannot be walked off the skins tree.
487
+ * path) so the bundle route cannot be walked off the skins tree. The root
488
+ * resolves per install layout (monorepo packages/skins/, npm
489
+ * node_modules/@linxin666/) — see skin-switch resolveSkinsDir.
444
490
  * @returns skin id -> directory name.
445
491
  */
446
492
  function skinDirectories() {
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.2",
4
+ "version": "0.1.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {