@linxin666/dsh-client-ui-skin-center 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,866 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@linxin666/dsh-client-ui-skin-center",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/generated/skins.ts
10
+ /** Every skin, ordered by packages/skins/<name>/skin.json `order`. */
11
+ const SKIN_CENTER_ENTRIES = [
12
+ {
13
+ "id": "qq98",
14
+ "name": "QQ2008 怀旧版",
15
+ "nameEn": "QQ2008 Retro",
16
+ "author": "dsh-web-ui",
17
+ "tagline": "水晶蓝桌面 · 玻璃深蓝标题栏 · 戴围巾企鹅",
18
+ "description": "dsh web ui 家族收录的第一个皮肤:QQ2008 水晶蓝年代。深蓝渐变桌面、玻璃质感标题栏、浅蓝状态栏和圆角高光控件,配一只戴围巾的企鹅。",
19
+ "tags": [
20
+ "retro",
21
+ "qq",
22
+ "2008",
23
+ "crystal-blue",
24
+ "nostalgia"
25
+ ],
26
+ "accent": "#2b7cd9",
27
+ "bodyAttr": "data-dsh-retro",
28
+ "package": "@linxin666/dsh-client-ui-skin-qq98",
29
+ "order": 1
30
+ },
31
+ {
32
+ "id": "ths",
33
+ "name": "同花顺风格",
34
+ "nameEn": "Tonghuashun Trading",
35
+ "author": "dsh-web-ui",
36
+ "tagline": "品牌红标题栏 · 实时行情状态栏 · 灰蓝数据终端",
37
+ "description": "同花顺风格炒股主题:品牌红标题栏带上证指数行情签,状态栏红涨绿跌,自选股风格的侧边栏和交易终端面板,写代码也像盯盘。",
38
+ "tags": [
39
+ "stock",
40
+ "trading",
41
+ "terminal",
42
+ "red"
43
+ ],
44
+ "accent": "#e60012",
45
+ "bodyAttr": "data-dsh-ths",
46
+ "package": "@linxin666/dsh-client-ui-skin-ths",
47
+ "order": 2
48
+ },
49
+ {
50
+ "id": "xp",
51
+ "name": "Windows XP (Luna)",
52
+ "nameEn": "Windows XP Luna",
53
+ "author": "dsh-web-ui",
54
+ "tagline": "Luna 蓝窗口条 · 绿色开始按钮 · Bliss 蓝天桌面",
55
+ "description": "Windows XP (Luna) 复古主题:蓝色渐变窗口条带窗口按钮、米色状态栏(大写/数字/滚动指示灯)、侧边栏任务栏上的绿色「开始」按钮、资源管理器风格树行和 Bliss 蓝天桌面,全局直角。",
56
+ "tags": [
57
+ "retro",
58
+ "xp",
59
+ "luna",
60
+ "windows",
61
+ "start-button"
62
+ ],
63
+ "accent": "#316ac5",
64
+ "bodyAttr": "data-dsh-xp",
65
+ "package": "@linxin666/dsh-client-ui-skin-xp",
66
+ "order": 3
67
+ },
68
+ {
69
+ "id": "blue-fantasy",
70
+ "name": "蓝色幻想",
71
+ "nameEn": "Blue Fantasy",
72
+ "author": "powerdog996(DreamSkin 社区)· dsh-web-ui 适配",
73
+ "tagline": "鲸鱼插画背景 · periwinkle 靛蓝调色板 · 半透明面板",
74
+ "description": "DreamSkin「DeepSeek-鲸鱼娘」Codex 桌面主题的 dsh 适配:鲸鱼插画背景垫在半透明面板之下,遮罩随亮/暗主题实时切换,periwinkle 靛蓝色调重映射到全部 dsh token。",
75
+ "tags": [
76
+ "dreamskin",
77
+ "whale",
78
+ "indigo",
79
+ "art",
80
+ "translucent"
81
+ ],
82
+ "accent": "#4a5fa8",
83
+ "bodyAttr": "data-dsh-blue-fantasy",
84
+ "package": "@linxin666/dsh-client-ui-skin-blue-fantasy",
85
+ "order": 4
86
+ },
87
+ {
88
+ "id": "dragon-heir",
89
+ "name": "龙的传人",
90
+ "nameEn": "Dragon Heir",
91
+ "author": "dsh-web-ui",
92
+ "tagline": "不屈龙魂 · 万里长城双主题 · 朱砂龙印",
93
+ "description": "龙的传人 — 一面是不屈龙魂(墨龙穿云、朱砂印章、不屈锋芒),一面是万里长城(青黛山色、金晖镀墙、苍茫暮色)。亮暗主题各自配一幅画与一枚龙印 favicon,面板半透明磨砂,让画透出来。",
94
+ "tags": [
95
+ "dragon",
96
+ "loong",
97
+ "chinese",
98
+ "ink-wash",
99
+ "great-wall",
100
+ "dual-theme"
101
+ ],
102
+ "accent": "#c3272b",
103
+ "bodyAttr": "data-dsh-dragon-heir",
104
+ "package": "@linxin666/dsh-client-ui-skin-dragon-heir",
105
+ "order": 5
106
+ },
107
+ {
108
+ "id": "minecraft",
109
+ "name": "Minecraft 方块世界",
110
+ "nameEn": "Minecraft Voxel",
111
+ "author": "dsh-web-ui",
112
+ "tagline": "动态全景天空盒 · 方块按钮 · 告示牌输入框",
113
+ "description": "复刻《我的世界》主界面氛围的方块皮肤:程序化绘制的像素全景天空盒(方块山、像素云、方块树、草方块地面)在身后缓慢旋转,界面浮在石板上;按钮还原 MC 菜单按钮(灰石板、悬停变黄、按下下沉),输入框做成带钉子的木告示牌。",
114
+ "tags": [
115
+ "minecraft",
116
+ "voxel",
117
+ "pixel",
118
+ "game",
119
+ "panorama",
120
+ "skybox"
121
+ ],
122
+ "accent": "#7cbd4b",
123
+ "bodyAttr": "data-dsh-minecraft",
124
+ "package": "@linxin666/dsh-client-ui-skin-minecraft",
125
+ "order": 6
126
+ },
127
+ {
128
+ "id": "whale-song",
129
+ "name": "鲸吟",
130
+ "nameEn": "Whale Song",
131
+ "author": "dsh-web-ui",
132
+ "tagline": "深海鲸语女神背景 · 冰蓝海洋调色板 · 金色细线点缀",
133
+ "description": "《鲸吟》— 深海鲸语女神主题:无文字纯氛围背景画(蓝发女神与鲸群居左、冰蓝星座网格与金线点缀、右侧大量留白)垫在半透明面板之下,遮罩随亮/暗主题实时切换,冰蓝/浅青/深海军蓝/钴蓝冷色体系重映射到全部 dsh token,暗色变体为深海夜航调。",
134
+ "tags": [
135
+ "whale",
136
+ "ocean",
137
+ "ice-blue",
138
+ "goddess",
139
+ "art",
140
+ "translucent"
141
+ ],
142
+ "accent": "#4d8fd4",
143
+ "bodyAttr": "data-dsh-whale-song",
144
+ "package": "@linxin666/dsh-client-ui-skin-whale-song",
145
+ "order": 7
146
+ }
147
+ ];
148
+ //#endregion
149
+ //#region src/client/try-on.ts
150
+ /**
151
+ * Try-on engine for the in-GUI skin center.
152
+ *
153
+ * A skin's client bundle is executed through the REAL module system, not a
154
+ * shim and not eval: the host route `/api/skin-center/bundle/<id>` serves
155
+ * the skin's prebuilt `lib/client.js` as a same-origin script (mirroring
156
+ * the kernel's own defaultLoadBundle — see dsh-client-modules), and its
157
+ * body calls `window.__ModuleLoader__.load({id, factory})`, which only
158
+ * REGISTERS the factory. `window.__DSH_MODULES__.import(package)` (the
159
+ * kernel's ClientModuleSystem, contract C5/C6) then materializes it — which
160
+ * auto-injects the skin's CSS `<style data-plugin>` tag — and
161
+ * `surface.apply(miniCtx)` mounts the skin exactly as the fiber system
162
+ * would, returning a full disposer. That makes try-on and its teardown the
163
+ * real code paths, with no CSP `unsafe-eval` dependence and no startup
164
+ * cost: the ~700KB of embedded art base64 is only parsed when a skin is
165
+ * actually tried on.
166
+ *
167
+ * Mutual exclusion: the GUI never hosts two skins at once. The currently
168
+ * ACTIVE skin is owned by its own cordis fiber (its disposer is not
169
+ * reachable), so try-on retracts the active skin's visual writes by recipe:
170
+ * remove its body attribute (its stylesheet goes inert), clear the
171
+ * body-level backdrop inline styles (blue-fantasy's whale art), detach only
172
+ * known skin chrome body children (title/status bars marked `data-skin-chrome`
173
+ * or carrying the skin's body attribute, leaving other plugins' portals and
174
+ * toasts in place), and neutralize known global-rule leaks (xp's sidebar
175
+ * taskbar/start). Everything is snapshotted and restored on exit in original
176
+ * order. The active skin's own fiber is never touched, so exiting try-on
177
+ * returns the page to exactly the pre-try-on state.
178
+ *
179
+ * A ghost MutationObserver may survive retraction (blue-fantasy re-writes
180
+ * its backdrop on theme flips), so during try-on a neutralizing observer
181
+ * re-clears the backdrop props whenever `data-ds-dark-theme` changes.
182
+ */
183
+ /** Body-level backdrop properties skins may write inline (blue-fantasy). */
184
+ const BACKDROP_PROPS = [
185
+ "background-image",
186
+ "background-position",
187
+ "background-size",
188
+ "background-attachment",
189
+ "background-repeat"
190
+ ];
191
+ /**
192
+ * Per-skin neutralization CSS: rules that hide visual leaks whose styles
193
+ * are NOT scoped under the skin's body attribute (they live on app elements
194
+ * the skin touches, so detaching chrome cannot remove them). Matched by
195
+ * css-module class substring, which is stable across rebuilds.
196
+ */
197
+ const NEUTRALIZE_CSS = { xp: [`[data-pane='sidebar'] [class*='xpTaskbar']{background:transparent!important;border-top:none!important;box-shadow:none!important}`, `[data-pane='sidebar'] [class*='xpStart']{display:none!important}`].join("") };
198
+ /** Host base path of the skin bundle route (registered by src/routes.ts). */
199
+ const BUNDLE_ROUTE = "/api/skin-center/bundle";
200
+ /**
201
+ * Execute one skin's client bundle as a real same-origin script, mirroring
202
+ * the kernel's own defaultLoadBundle (dsh-client-modules): the script body
203
+ * calls `window.__ModuleLoader__.load({id, factory})`, which only registers
204
+ * the factory — materialization is the caller's separate `import` step. No
205
+ * eval: try-on works under any CSP that allows same-origin scripts (the
206
+ * shell itself loads plugin bundles this way), and a failed fetch rejects
207
+ * so the caller can restore the active skin instead of leaving it retracted.
208
+ * @param url - same-origin bundle URL.
209
+ * @returns a promise resolving once the script executed.
210
+ */
211
+ function loadBundleScript(url) {
212
+ return new Promise((resolve, reject) => {
213
+ const el = document.createElement("script");
214
+ el.async = true;
215
+ el.src = url;
216
+ el.addEventListener("load", () => {
217
+ el.remove();
218
+ resolve();
219
+ }, { once: true });
220
+ el.addEventListener("error", () => {
221
+ el.remove();
222
+ reject(/* @__PURE__ */ new Error(`skin-center: bundle script ${url} failed to load`));
223
+ }, { once: true });
224
+ document.head.append(el);
225
+ });
226
+ }
227
+ /** Read the page's composed boot-graph entry ids (only enabled plugins appear). */
228
+ function bootEntryIds() {
229
+ return window.__DSH_BOOT__?.entries?.map((entry) => entry.id) ?? [];
230
+ }
231
+ /** The skin package currently ACTIVE in the boot graph, if it is one of ours. */
232
+ function activeSkinEntry() {
233
+ const ids = new Set(bootEntryIds());
234
+ return SKIN_CENTER_ENTRIES.find((entry) => ids.has(entry.package));
235
+ }
236
+ /**
237
+ * Whether a direct body child is skin chrome owned by `skin`: marked with the
238
+ * `data-skin-chrome` marker (minecraft/dragon-heir) or carrying the skin's
239
+ * scoping body attribute. Everything else — other plugins' portals, toasts and
240
+ * overlays appended to body — is left alone.
241
+ */
242
+ function isSkinChrome(el, skin) {
243
+ if (el.hasAttribute("data-skin-chrome")) return true;
244
+ return skin !== null && el.hasAttribute(skin.bodyAttr);
245
+ }
246
+ function miniCtx() {
247
+ const disposers = [];
248
+ return {
249
+ effect(callback) {
250
+ disposers.push(callback());
251
+ return () => {};
252
+ },
253
+ get() {},
254
+ __disposeAll() {
255
+ for (const dispose of disposers.reverse()) dispose();
256
+ }
257
+ };
258
+ }
259
+ /**
260
+ * One live try-on session: owns the tried-on skin's disposer plus the
261
+ * captured active-skin visuals, and restores everything on exit.
262
+ */
263
+ var TryOnController = class {
264
+ session = null;
265
+ /**
266
+ * Generation counter. A newer try-on or exit increments it, so an in-flight
267
+ * `tryOn` (awaiting the real bundle load) can detect it was superseded and
268
+ * drop only what it mounted instead of clobbering the newer session.
269
+ */
270
+ epoch = 0;
271
+ /**
272
+ * Loads one skin's client bundle so its factory registers on the page's
273
+ * `__ModuleLoader__`. Defaults to a same-origin script tag from the host
274
+ * route `/api/skin-center/bundle/<id>`; tests inject a stub.
275
+ */
276
+ loadBundle;
277
+ constructor(options = {}) {
278
+ this.loadBundle = options.loadBundle ?? ((entry) => loadBundleScript(`${BUNDLE_ROUTE}/${encodeURIComponent(entry.id)}`));
279
+ }
280
+ /** The skin currently being tried on, if any. */
281
+ get trying() {
282
+ return this.session?.entry ?? null;
283
+ }
284
+ /** Whether the official stock look (no skin) is being tried on. */
285
+ get tryingOfficial() {
286
+ return this.session !== null && this.session.entry === null;
287
+ }
288
+ /** Start trying on `entry` (replaces any live session). */
289
+ async tryOn(entry) {
290
+ if (entry.package === activeSkinEntry()?.package) return;
291
+ this.exit();
292
+ const epoch = ++this.epoch;
293
+ const active = this.captureAndRetractActive();
294
+ let dispose;
295
+ try {
296
+ dispose = await this.loadAndApply(entry);
297
+ } catch (error) {
298
+ if (epoch === this.epoch) this.restoreActive(active);
299
+ throw error;
300
+ }
301
+ if (epoch !== this.epoch) {
302
+ this.cleanupModule(entry);
303
+ dispose();
304
+ return;
305
+ }
306
+ this.session = {
307
+ entry,
308
+ dispose,
309
+ active
310
+ };
311
+ }
312
+ /**
313
+ * Try on the official stock look: retract the active skin's visual writes
314
+ * (same recipe as a skin try-on) and mount nothing. Exiting restores the
315
+ * active skin exactly like any other try-on session.
316
+ */
317
+ tryOnOfficial() {
318
+ if (activeSkinEntry() === null) return;
319
+ this.exit();
320
+ this.epoch += 1;
321
+ const active = this.captureAndRetractActive();
322
+ this.session = {
323
+ entry: null,
324
+ dispose: () => {},
325
+ active
326
+ };
327
+ }
328
+ /** Exit the live session: dispose the tried-on skin, then restore the active skin. */
329
+ exit() {
330
+ const session = this.session;
331
+ if (session === null) return;
332
+ this.epoch += 1;
333
+ this.session = null;
334
+ session.dispose();
335
+ if (session.entry !== null) this.cleanupModule(session.entry);
336
+ this.restoreActive(session.active);
337
+ }
338
+ /** Execute + materialize + mount the target skin through the real loader. */
339
+ async loadAndApply(entry) {
340
+ const modules = window.__DSH_MODULES__;
341
+ if (modules === void 0) throw new Error("skin-center: window.__DSH_MODULES__ missing");
342
+ modules.invalidate(entry.package);
343
+ await this.loadBundle(entry);
344
+ const apply = (await modules.import(entry.package)).apply;
345
+ if (typeof apply !== "function") throw new Error(`skin-center: "${entry.package}" client bundle exports no apply`);
346
+ const ctx = miniCtx();
347
+ try {
348
+ apply(ctx);
349
+ } catch (error) {
350
+ this.cleanupModule(entry);
351
+ document.body.removeAttribute(entry.bodyAttr);
352
+ for (const el of [...document.body.children]) if (isSkinChrome(el, entry)) el.remove();
353
+ throw error;
354
+ }
355
+ return ctx.__disposeAll;
356
+ }
357
+ /** Drop the tried-on module record + its injected style tag. */
358
+ cleanupModule(entry) {
359
+ window.__DSH_MODULES__?.invalidate(entry.package);
360
+ for (const el of document.querySelectorAll(`style[data-plugin=${JSON.stringify(entry.package)}]`)) el.remove();
361
+ }
362
+ /**
363
+ * Snapshot the active skin's visual writes and retract them so the tried-on
364
+ * skin can take over the whole surface.
365
+ */
366
+ captureAndRetractActive() {
367
+ const skin = activeSkinEntry() ?? null;
368
+ const body = document.body;
369
+ const bodyAttr = skin === null ? null : body.getAttribute(skin.bodyAttr);
370
+ if (skin !== null && bodyAttr !== null) body.removeAttribute(skin.bodyAttr);
371
+ const bodyStyle = body.getAttribute("style");
372
+ for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop);
373
+ const children = [...body.children];
374
+ const chrome = /* @__PURE__ */ new Set();
375
+ for (const el of children) if (el.id !== "root" && isSkinChrome(el, skin)) chrome.add(el);
376
+ const detached = [];
377
+ for (let i = 0; i < children.length; i++) {
378
+ const el = children[i];
379
+ if (!chrome.has(el)) continue;
380
+ let anchor = null;
381
+ for (let j = i + 1; j < children.length; j++) if (!chrome.has(children[j])) {
382
+ anchor = children[j];
383
+ break;
384
+ }
385
+ detached.push({
386
+ el,
387
+ anchor
388
+ });
389
+ }
390
+ for (const { el } of detached) el.remove();
391
+ const clearObserver = new MutationObserver(() => {
392
+ for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop);
393
+ });
394
+ clearObserver.observe(body, {
395
+ attributes: true,
396
+ attributeFilter: ["data-ds-dark-theme"]
397
+ });
398
+ const neutralizeCss = skin === null ? void 0 : NEUTRALIZE_CSS[skin.id];
399
+ return {
400
+ skin,
401
+ bodyAttr,
402
+ bodyStyle,
403
+ detached,
404
+ clearObserver,
405
+ neutralizeStyle: neutralizeCss === void 0 ? null : this.injectStyle(neutralizeCss)
406
+ };
407
+ }
408
+ /** Restore the active skin's captured visual state. */
409
+ restoreActive(active) {
410
+ const body = document.body;
411
+ if (active.skin !== null && active.bodyAttr !== null) body.setAttribute(active.skin.bodyAttr, active.bodyAttr);
412
+ if (active.bodyStyle !== null) body.setAttribute("style", active.bodyStyle);
413
+ else body.removeAttribute("style");
414
+ for (const { el, anchor } of active.detached) body.insertBefore(el, anchor !== null && anchor.parentNode === body ? anchor : null);
415
+ active.clearObserver?.disconnect();
416
+ active.neutralizeStyle?.remove();
417
+ }
418
+ injectStyle(css) {
419
+ const tag = document.createElement("style");
420
+ tag.dataset.skinCenterNeutralize = "";
421
+ tag.textContent = css;
422
+ document.head.append(tag);
423
+ return tag;
424
+ }
425
+ };
426
+ //#endregion
427
+ //#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/skins/skin-center/src/client/skin-center.module.css.mjs
428
+ const css = "body[data-dsh-skin-center] .K3rRTW_pluginCard{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:8px;list-style:none;overflow:hidden}body[data-dsh-skin-center] .K3rRTW_cardHeader{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;padding:11px 14px;transition:background .12s;display:flex}body[data-dsh-skin-center] .K3rRTW_cardHeader:hover{background:var(--dsw-alias-bg-layer-1,#f1f5f9)}body[data-dsh-skin-center] .K3rRTW_cardHeader:active{background:var(--dsw-alias-bg-layer-3,#e6ecf4)}body[data-dsh-skin-center] .K3rRTW_cardHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_headText{flex-direction:column;flex:1;gap:3px;min-width:0;display:flex}body[data-dsh-skin-center] .K3rRTW_pluginName{color:var(--dsw-alias-label-primary,#172a45);align-items:baseline;gap:8px;font-size:13.5px;font-weight:600;display:flex}body[data-dsh-skin-center] .K3rRTW_cardDescription{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.4}body[data-dsh-skin-center] .K3rRTW_chevron,body[data-dsh-skin-center] .K3rRTW_chevronOpen{color:var(--dsw-alias-label-secondary,#6b7280);flex:none;margin-left:10px;font-size:12px;transition:transform .12s}body[data-dsh-skin-center] .K3rRTW_chevronOpen{transform:rotate(180deg)}body[data-dsh-skin-center] .K3rRTW_cardBody{border-top:1px solid var(--dsw-alias-border-l1,#e2e8f0);flex-direction:column;gap:12px;padding:12px 14px 14px;display:flex}body[data-dsh-skin-center] .K3rRTW_head{flex-direction:column;gap:6px;display:flex}body[data-dsh-skin-center] .K3rRTW_titleBadge{color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;font-weight:500}body[data-dsh-skin-center] .K3rRTW_intro{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12.5px;line-height:1.55}body[data-dsh-skin-center] .K3rRTW_themeRow{align-items:center;gap:8px;margin-top:2px;display:flex}body[data-dsh-skin-center] .K3rRTW_themeLabel{color:var(--dsw-alias-label-secondary,#6b7280);margin-right:2px;font-size:12px}body[data-dsh-skin-center] .K3rRTW_themeButton{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:6px;padding:5px 10px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .K3rRTW_themeButton:hover{border-color:var(--dsw-alias-border-l4,#94a3b8)}body[data-dsh-skin-center] .K3rRTW_themeButton:active{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_themeButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_themeButtonActive{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_list{flex-direction:column;gap:10px;display:flex}body[data-dsh-skin-center] .K3rRTW_card{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:10px;flex-direction:column;gap:8px;padding:12px 14px;display:flex}body[data-dsh-skin-center] .K3rRTW_cardHead{align-items:center;gap:10px;min-width:0;display:flex}body[data-dsh-skin-center] .K3rRTW_swatch{width:14px;height:14px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);border-radius:50%;flex:none}body[data-dsh-skin-center] .K3rRTW_cardName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:13.5px;font-weight:600;overflow:hidden}body[data-dsh-skin-center] .K3rRTW_cardTagline{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.45}body[data-dsh-skin-center] .K3rRTW_badge{letter-spacing:.02em;border-radius:999px;flex:none;min-width:0;margin-left:auto;padding:2px 8px;font-size:11px;font-weight:600}body[data-dsh-skin-center] .K3rRTW_badgeActive{color:var(--dsw-alias-state-success-primary,#0f6b3a);background:var(--dsw-alias-state-success-tertiary,#dcf3e5)}body[data-dsh-skin-center] .K3rRTW_badgeTrying{color:var(--dsw-alias-brand-primary,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e2edfc)}body[data-dsh-skin-center] .K3rRTW_actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}body[data-dsh-skin-center] .K3rRTW_button{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:7px;padding:6px 12px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .K3rRTW_button:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2b7cd9);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_button:active:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_buttonPrimary{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-fill,#2b7cd9);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .K3rRTW_buttonPrimary:hover:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .K3rRTW_buttonPrimary:active:not(:disabled),body[data-dsh-skin-center] .K3rRTW_buttonPrimary:focus-visible:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_buttonGhost{background:0 0;border-color:#0000}body[data-dsh-skin-center] .K3rRTW_button:disabled{opacity:.55;cursor:default}body[data-dsh-skin-center] .K3rRTW_error{color:var(--dsw-alias-state-error-primary,#b42318);font-size:12px}@media (prefers-reduced-motion:reduce){body[data-dsh-skin-center] .K3rRTW_cardHeader,body[data-dsh-skin-center] .K3rRTW_themeButton,body[data-dsh-skin-center] .K3rRTW_button,body[data-dsh-skin-center] .K3rRTW_chevron,body[data-dsh-skin-center] .K3rRTW_chevronOpen{transition:none}}";
429
+ const tagId = "@linxin666/dsh-client-ui-skin-center/skin-center.module.css";
430
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
431
+ const tag = document.createElement("style");
432
+ tag.dataset.plugin = "@linxin666/dsh-client-ui-skin-center";
433
+ tag.dataset.pluginCss = tagId;
434
+ tag.textContent = css;
435
+ document.head.appendChild(tag);
436
+ }
437
+ var skin_center_module_css_default = {
438
+ "actions": "K3rRTW_actions",
439
+ "badge": "K3rRTW_badge",
440
+ "badgeActive": "K3rRTW_badgeActive",
441
+ "badgeTrying": "K3rRTW_badgeTrying",
442
+ "button": "K3rRTW_button",
443
+ "buttonGhost": "K3rRTW_buttonGhost",
444
+ "buttonPrimary": "K3rRTW_buttonPrimary",
445
+ "card": "K3rRTW_card",
446
+ "cardBody": "K3rRTW_cardBody",
447
+ "cardDescription": "K3rRTW_cardDescription",
448
+ "cardHead": "K3rRTW_cardHead",
449
+ "cardHeader": "K3rRTW_cardHeader",
450
+ "cardName": "K3rRTW_cardName",
451
+ "cardTagline": "K3rRTW_cardTagline",
452
+ "chevron": "K3rRTW_chevron",
453
+ "chevronOpen": "K3rRTW_chevronOpen",
454
+ "error": "K3rRTW_error",
455
+ "head": "K3rRTW_head",
456
+ "headText": "K3rRTW_headText",
457
+ "intro": "K3rRTW_intro",
458
+ "list": "K3rRTW_list",
459
+ "pluginCard": "K3rRTW_pluginCard",
460
+ "pluginName": "K3rRTW_pluginName",
461
+ "swatch": "K3rRTW_swatch",
462
+ "themeButton": "K3rRTW_themeButton",
463
+ "themeButtonActive": "K3rRTW_themeButtonActive",
464
+ "themeLabel": "K3rRTW_themeLabel",
465
+ "themeRow": "K3rRTW_themeRow",
466
+ "titleBadge": "K3rRTW_titleBadge"
467
+ };
468
+ //#endregion
469
+ //#region src/client/SkinCenter.tsx
470
+ /**
471
+ * The skin-center plugin card: one disclosure card inside the Web UI plugin
472
+ * group (插件配置 → Web UI 插件), listing every installed skin plus the
473
+ * official stock look. Live try-on executes the real bundle inside the GUI
474
+ * (light/dark preview, full restore on exit); Apply is one click — the host
475
+ * half runs `dsh-skin use` through /api/skin-center/apply, the config
476
+ * watcher hot-reloads the patch, and the page reloads into the new skin.
477
+ * Copy rides the standard `t` seat; the theme preview control drives the
478
+ * official theme service (persisted, same as the Appearance row).
479
+ */
480
+ /** The apply target of the official stock-look card. */
481
+ const OFFICIAL = "official";
482
+ /**
483
+ * Render the skin-center card: a disclosure header naming the plugin, with
484
+ * the skin list (official default + every installed skin; try-on / theme
485
+ * preview / one-click apply) inside its body.
486
+ * @param props - card props.
487
+ * @returns the plugin card.
488
+ */
489
+ function SkinCenter({ t, controller, theme }) {
490
+ const snapshot = (0, react.useSyncExternalStore)(theme.subscribe, theme.getTheme);
491
+ const activePackage = activeSkinEntry()?.package;
492
+ const [open, setOpen] = (0, react.useState)(false);
493
+ const [tryingId, setTryingId] = (0, react.useState)(null);
494
+ const [tryingOfficial, setTryingOfficial] = (0, react.useState)(false);
495
+ const [applying, setApplying] = (0, react.useState)(null);
496
+ const [error, setError] = (0, react.useState)(null);
497
+ const tryOn = (entry) => {
498
+ setError(null);
499
+ controller.tryOn(entry).then(() => {
500
+ setTryingId(entry.id);
501
+ setTryingOfficial(false);
502
+ }).catch(() => {
503
+ setError(t("tryOnError"));
504
+ setTryingId(null);
505
+ setTryingOfficial(false);
506
+ });
507
+ };
508
+ const tryOnOfficial = () => {
509
+ setError(null);
510
+ try {
511
+ controller.tryOnOfficial();
512
+ } catch {
513
+ setError(t("tryOnError"));
514
+ setTryingOfficial(false);
515
+ return;
516
+ }
517
+ setTryingId(null);
518
+ setTryingOfficial(true);
519
+ };
520
+ const exitTryOn = () => {
521
+ controller.exit();
522
+ setTryingId(null);
523
+ setTryingOfficial(false);
524
+ };
525
+ /**
526
+ * Poll the host state until the config watcher reports the target active
527
+ * (the patch write lands before the watcher re-applies it), or time out.
528
+ * @param target - skin id, or `official` for the stock look.
529
+ * @returns whether the target became active within the poll budget.
530
+ */
531
+ const confirmActive = (target) => new Promise((resolve) => {
532
+ const expected = target === OFFICIAL ? "none" : target;
533
+ let tries = 0;
534
+ const tick = () => {
535
+ tries += 1;
536
+ fetch("/api/skin-center/state").then(async (response) => {
537
+ const payload = await response.json().catch(() => null);
538
+ if (response.ok && payload?.ok === true && payload.active === expected) {
539
+ resolve(true);
540
+ return;
541
+ }
542
+ if (tries >= 20) resolve(false);
543
+ else window.setTimeout(tick, 250);
544
+ }).catch(() => {
545
+ if (tries >= 20) resolve(false);
546
+ else window.setTimeout(tick, 250);
547
+ });
548
+ };
549
+ tick();
550
+ });
551
+ /**
552
+ * One-click apply: the host half runs `dsh-skin use <target>` (or
553
+ * `use official`), the config watcher hot-reloads the patch within
554
+ * seconds, then this page reloads to pick up the new boot graph.
555
+ * @param target - skin id, or `official` for the stock look.
556
+ */
557
+ const applySkin = (target) => {
558
+ setError(null);
559
+ setApplying(target);
560
+ fetch("/api/skin-center/apply", {
561
+ method: "POST",
562
+ headers: { "content-type": "application/json" },
563
+ body: JSON.stringify(target === OFFICIAL ? { official: true } : { skin: target })
564
+ }).then(async (response) => {
565
+ const payload = await response.json().catch(() => null);
566
+ if (!response.ok || payload?.ok !== true) throw new Error(payload?.error ?? `HTTP ${response.status}`);
567
+ setApplying(null);
568
+ confirmActive(target).then((confirmed) => {
569
+ if (confirmed) window.location.reload();
570
+ else {
571
+ const command = target === OFFICIAL ? "dsh-skin use official" : `dsh-skin use ${target}`;
572
+ setError(`${t("appliedUnconfirmed")} — ${command}`);
573
+ }
574
+ });
575
+ }).catch((cause) => {
576
+ setApplying(null);
577
+ const detail = cause instanceof Error ? cause.message : String(cause);
578
+ const command = target === OFFICIAL ? "dsh-skin use official" : `dsh-skin use ${target}`;
579
+ setError(`${t("applyFailed")} (${detail}) — ${command}`);
580
+ });
581
+ };
582
+ const dark = snapshot.active.colorScheme === "dark";
583
+ /** One row: try-on control + apply button. Shared by the official card and every skin card. */
584
+ const actionButtons = (opts) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
585
+ className: skin_center_module_css_default.actions,
586
+ children: [opts.isActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
587
+ type: "button",
588
+ className: `${skin_center_module_css_default.button} ${skin_center_module_css_default.buttonGhost}`,
589
+ disabled: true,
590
+ children: t("tryOn")
591
+ }) : opts.isTrying ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
592
+ type: "button",
593
+ className: `${skin_center_module_css_default.button} ${skin_center_module_css_default.buttonPrimary}`,
594
+ onClick: exitTryOn,
595
+ children: t("exitTryOn")
596
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
597
+ type: "button",
598
+ className: `${skin_center_module_css_default.button} ${skin_center_module_css_default.buttonPrimary}`,
599
+ onClick: opts.onTryOn,
600
+ children: t("tryOn")
601
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
602
+ type: "button",
603
+ className: skin_center_module_css_default.button,
604
+ disabled: applying !== null,
605
+ onClick: () => {
606
+ applySkin(opts.key);
607
+ },
608
+ children: applying === opts.key ? t("applying") : opts.applyLabel
609
+ })]
610
+ });
611
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
612
+ className: skin_center_module_css_default.pluginCard,
613
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
614
+ type: "button",
615
+ className: skin_center_module_css_default.cardHeader,
616
+ "aria-expanded": open,
617
+ "aria-label": `${t(open ? "collapse" : "expand")}: ${t("title")}`,
618
+ onClick: () => {
619
+ setOpen((current) => !current);
620
+ },
621
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
622
+ className: skin_center_module_css_default.headText,
623
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
624
+ className: skin_center_module_css_default.pluginName,
625
+ children: [t("title"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
626
+ className: skin_center_module_css_default.titleBadge,
627
+ children: String(SKIN_CENTER_ENTRIES.length)
628
+ })]
629
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
630
+ className: skin_center_module_css_default.cardDescription,
631
+ title: t("cardDescription"),
632
+ children: t("cardDescription")
633
+ })]
634
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
635
+ className: open ? skin_center_module_css_default.chevronOpen : skin_center_module_css_default.chevron,
636
+ children: "▾"
637
+ })]
638
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
639
+ className: skin_center_module_css_default.cardBody,
640
+ children: [
641
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
642
+ className: skin_center_module_css_default.head,
643
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
644
+ className: skin_center_module_css_default.intro,
645
+ title: t("intro"),
646
+ children: t("intro")
647
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
648
+ className: skin_center_module_css_default.themeRow,
649
+ children: [
650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
651
+ className: skin_center_module_css_default.themeLabel,
652
+ children: t("theme")
653
+ }),
654
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
655
+ type: "button",
656
+ className: `${skin_center_module_css_default.themeButton} ${dark ? "" : skin_center_module_css_default.themeButtonActive}`,
657
+ onClick: () => {
658
+ theme.setTheme("light");
659
+ },
660
+ children: t("themeLight")
661
+ }),
662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
663
+ type: "button",
664
+ className: `${skin_center_module_css_default.themeButton} ${dark ? skin_center_module_css_default.themeButtonActive : ""}`,
665
+ onClick: () => {
666
+ theme.setTheme("dark");
667
+ },
668
+ children: t("themeDark")
669
+ })
670
+ ]
671
+ })]
672
+ }),
673
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
674
+ className: skin_center_module_css_default.error,
675
+ children: error
676
+ }),
677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
678
+ className: skin_center_module_css_default.list,
679
+ children: [(() => {
680
+ const isActive = activePackage === void 0;
681
+ const isTrying = tryingOfficial;
682
+ const badge = isActive ? t("active") : isTrying ? t("tryingOn") : null;
683
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
684
+ className: skin_center_module_css_default.card,
685
+ children: [
686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
687
+ className: skin_center_module_css_default.cardHead,
688
+ children: [
689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
690
+ className: skin_center_module_css_default.swatch,
691
+ style: { background: "#98a1ab" },
692
+ "aria-hidden": "true"
693
+ }),
694
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
695
+ className: skin_center_module_css_default.cardName,
696
+ title: t("official"),
697
+ children: t("official")
698
+ }),
699
+ badge !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
700
+ className: `${skin_center_module_css_default.badge} ${isActive ? skin_center_module_css_default.badgeActive : skin_center_module_css_default.badgeTrying}`,
701
+ children: badge
702
+ })
703
+ ]
704
+ }),
705
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
706
+ className: skin_center_module_css_default.cardTagline,
707
+ title: t("officialTagline"),
708
+ children: t("officialTagline")
709
+ }),
710
+ actionButtons({
711
+ key: OFFICIAL,
712
+ isActive,
713
+ isTrying,
714
+ onTryOn: tryOnOfficial,
715
+ applyLabel: t("restore")
716
+ })
717
+ ]
718
+ }, OFFICIAL);
719
+ })(), SKIN_CENTER_ENTRIES.map((entry) => {
720
+ const isActive = entry.package === activePackage;
721
+ const isTrying = entry.id === tryingId;
722
+ const badge = isActive ? t("active") : isTrying ? t("tryingOn") : null;
723
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
724
+ className: skin_center_module_css_default.card,
725
+ children: [
726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
727
+ className: skin_center_module_css_default.cardHead,
728
+ children: [
729
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
730
+ className: skin_center_module_css_default.swatch,
731
+ style: { background: entry.accent },
732
+ "aria-hidden": "true"
733
+ }),
734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
735
+ className: skin_center_module_css_default.cardName,
736
+ title: entry.nameEn,
737
+ children: entry.nameEn
738
+ }),
739
+ badge !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
740
+ className: `${skin_center_module_css_default.badge} ${isActive ? skin_center_module_css_default.badgeActive : skin_center_module_css_default.badgeTrying}`,
741
+ children: badge
742
+ })
743
+ ]
744
+ }),
745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
746
+ className: skin_center_module_css_default.cardTagline,
747
+ title: entry.tagline,
748
+ children: entry.tagline
749
+ }),
750
+ actionButtons({
751
+ key: entry.id,
752
+ isActive,
753
+ isTrying,
754
+ onTryOn: () => {
755
+ tryOn(entry);
756
+ },
757
+ applyLabel: t("apply")
758
+ })
759
+ ]
760
+ }, entry.id);
761
+ })]
762
+ })
763
+ ]
764
+ }) : null]
765
+ });
766
+ }
767
+ //#endregion
768
+ //#region src/client/locales.ts
769
+ const en = {
770
+ title: "Skin Center",
771
+ cardDescription: "Try on any installed skin live in the GUI — exit restores instantly, applying persists in one click.",
772
+ expand: "Expand",
773
+ collapse: "Collapse",
774
+ intro: "Try on any skin live — it takes effect instantly, exit restores the current look. Apply persists it across restarts.",
775
+ official: "Official default",
776
+ officialTagline: "The stock DSH look with no skin applied.",
777
+ active: "Active",
778
+ tryingOn: "Trying on",
779
+ tryOn: "Try on",
780
+ exitTryOn: "Exit try-on",
781
+ apply: "Apply",
782
+ applying: "Applying…",
783
+ restore: "Restore",
784
+ applyFailed: "Apply failed",
785
+ appliedUnconfirmed: "Applied, but the change has not been confirmed — refresh the page if the skin did not switch",
786
+ theme: "Theme preview",
787
+ themeLight: "Light",
788
+ themeDark: "Dark",
789
+ tryOnError: "Try-on failed — see console"
790
+ };
791
+ const zh = {
792
+ title: "皮肤中心",
793
+ cardDescription: "在 GUI 内即时试穿任意皮肤,退出即完全还原;应用一键完成并自动刷新。",
794
+ expand: "展开",
795
+ collapse: "收起",
796
+ intro: "任意皮肤可即时试穿,退出即完全还原;「应用」一键持久化,页面自动刷新生效。",
797
+ official: "官方默认",
798
+ officialTagline: "还原 DSH 官方默认外观,不应用任何皮肤。",
799
+ active: "当前激活",
800
+ tryingOn: "试穿中",
801
+ tryOn: "试穿",
802
+ exitTryOn: "退出试穿",
803
+ apply: "应用",
804
+ applying: "应用中…",
805
+ restore: "恢复默认",
806
+ applyFailed: "应用失败",
807
+ appliedUnconfirmed: "已写入配置但尚未确认生效——若皮肤未切换请手动刷新页面",
808
+ theme: "主题预览",
809
+ themeLight: "亮色",
810
+ themeDark: "暗色",
811
+ tryOnError: "试穿失败,详见控制台"
812
+ };
813
+ //#endregion
814
+ //#region src/client/index.ts
815
+ /** Locale namespace owned by this plugin. */
816
+ const NS = "skinCenter";
817
+ /** Required services: slots + locale (plugin card) and theme (preview toggle). */
818
+ const inject = [
819
+ "slots",
820
+ "locale",
821
+ "theme"
822
+ ];
823
+ /**
824
+ * Register the skin-center dictionaries, the body scope attribute, and the
825
+ * Skins plugin card inside the Web UI plugin group.
826
+ * @param ctx - client root context.
827
+ */
828
+ function apply(ctx) {
829
+ ctx.effect(() => ctx.locale.register(NS, {
830
+ zh,
831
+ en
832
+ }), "ui-skin-center: dictionaries");
833
+ ctx.effect(() => {
834
+ document.body.dataset.dshSkinCenter = "";
835
+ return () => {
836
+ delete document.body.dataset.dshSkinCenter;
837
+ };
838
+ }, "ui-skin-center: body scope");
839
+ const theme = ctx.get("theme");
840
+ const controller = new TryOnController();
841
+ const injected = () => ({
842
+ controller,
843
+ theme: {
844
+ getTheme: () => theme.getTheme(),
845
+ subscribe: (listener) => ctx.on("theme/change", listener),
846
+ setTheme: (id) => theme.setTheme(id)
847
+ }
848
+ });
849
+ ctx.slots.inject("web-ui.plugin.item", () => ctx.slots.register({
850
+ name: "web-ui.plugin.item",
851
+ id: "skins",
852
+ order: 110,
853
+ locale: NS,
854
+ inject: injected
855
+ }, SkinCenter));
856
+ }
857
+ //#endregion
858
+ exports.NS = NS;
859
+ exports.TryOnController = TryOnController;
860
+ exports.apply = apply;
861
+ exports.inject = inject;
862
+ return module.exports;
863
+ }
864
+ });
865
+
866
+ //# sourceMappingURL=client.js.map