dsh-wx-skin 0.1.0 → 0.1.2
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 +13 -11
- package/lib/client.js +79 -23
- package/lib/client.js.map +1 -1
- package/lib/index.js +133 -4
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +27 -7
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
8
|
-

|
|
9
9
|
|
|
10
10
|
> 上图为本插件实际效果演示(深蓝渐变壁纸透过半透明表面)。`assets/demo.html` 是自包含的演示源,可直接用浏览器打开预览。
|
|
11
11
|
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
- **效果调节**:
|
|
21
21
|
- 暗化 0–80%(黑色遮罩,保证文字可读);
|
|
22
22
|
- 模糊 0–24px(背景毛玻璃);
|
|
23
|
-
- **透出
|
|
23
|
+
- **透出 0–100%**(表面不透明度,越低背景越明显,默认 72%;可读性由「暗化」配合)。
|
|
24
24
|
- **启用开关 + 恢复默认**,一键关闭皮肤。
|
|
25
|
-
-
|
|
25
|
+
- **持久化**:设置同时存于 `localStorage` 与宿主副本 `~/.dsh/dsh-wx-skin.settings.json`,刷新、重启、桌面端换端口后均自动恢复。
|
|
26
26
|
- **明暗主题适配**:随 `body[data-ds-dark-theme]` 自动切换两套半透明配色。
|
|
27
27
|
- **完全独立**:纯浏览器端 client 插件,不修改 DSH 仓库,不影响主界面与其它插件。
|
|
28
28
|
|
|
@@ -37,19 +37,19 @@
|
|
|
37
37
|
插件已发布到 npm,一条命令装齐(在 **DSH 源码 checkout 目录**执行):
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
|
-
pnpm dsh plugin --profile web add dsh-wx-skin@0.1.
|
|
40
|
+
pnpm dsh plugin --profile web add dsh-wx-skin@0.1.2
|
|
41
41
|
# 若 `dsh` 已加入 PATH,也可直接:
|
|
42
|
-
dsh plugin --profile web add dsh-wx-skin@0.1.
|
|
42
|
+
dsh plugin --profile web add dsh-wx-skin@0.1.2
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
### 方式二:使用发布包(tarball)
|
|
46
46
|
|
|
47
|
-
下载 `dsh-wx-skin-0.1.
|
|
47
|
+
下载 `dsh-wx-skin-0.1.2.tgz`(或通过 GitHub Releases 获取):
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
50
|
pnpm dsh plugin --profile web add file:<tgz 的绝对路径>
|
|
51
51
|
# 例如:
|
|
52
|
-
pnpm dsh plugin --profile web add file:C:/Users/you/Downloads/dsh-wx-skin-0.1.
|
|
52
|
+
pnpm dsh plugin --profile web add file:C:/Users/you/Downloads/dsh-wx-skin-0.1.2.tgz
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
### 方式三:克隆源码构建(开发者)
|
|
@@ -104,6 +104,7 @@ pnpm dsh plugin --profile web remove dsh-wx-skin
|
|
|
104
104
|
- **背景层**:注入全屏 `div[data-wx-skin-layer]`(`position: fixed; z-index: 0; pointer-events: none`),并将应用根 `#root` 抬到 `z-index: 1`。⚠️ 实测 **`z-index: -1` 的 fixed 图层在 DSH shell 中不绘制**(落在 canvas 背景之下),这是早期版本"能选图但背景不显示"的根因,故采用 `z-index: 0` + `#root` 抬升方案。
|
|
105
105
|
- **半透明表面**:以独立 `<style>` + `!important` 覆盖十余个 alias 表面 token(`--dsw-alias-bg-*`、`--dsw-specific-*`、`--dsw-alias-markdown-*` 等),明暗两套值;透明度由 `--wx-skin-surface` 变量统一控制(不依赖 `color-mix()`,任意现代浏览器可用)。
|
|
106
106
|
- **图片管线**:canvas 解码 → 编码为 JPEG data URL;原始分辨率 ≤4096px 时保持原样,更大或超出浏览器存储容量时静默缩小;编码结果做有效性校验,异常自动降档重编——**永不因图片大小报错**。
|
|
107
|
+
- **宿主持久化**:`src/index.ts` 通过 `webServer` 服务注册 `/dsh-wx-skin/load` 与 `/dsh-wx-skin/save` 两条回环路由,把设置原子写入 DSH home(`$DSH_HOME` 或 `~/.dsh`)下的 `dsh-wx-skin.settings.json`;桌面端每次启动端口变化、localStorage 清空时,客户端自动从宿主副本恢复——背景图片跨会话不丢失。
|
|
107
108
|
- **不依赖 `ctx.theme` 服务**:皮肤完全独立于 DSH 主题系统,关闭时样式惰性、默认主题不受影响。
|
|
108
109
|
|
|
109
110
|
---
|
|
@@ -115,7 +116,7 @@ npm install # 安装依赖
|
|
|
115
116
|
npm run typecheck # tsc 类型检查(宿主 + 客户端)
|
|
116
117
|
npm run test # vitest 单元测试(skin-store / image-pipeline)
|
|
117
118
|
npm run build # tsc 宿主 lib + tsdown client bundle
|
|
118
|
-
npm pack # 产出发布包 dsh-wx-skin
|
|
119
|
+
npm pack # 产出发布包 dsh-wx-skin-<version>.tgz
|
|
119
120
|
```
|
|
120
121
|
|
|
121
122
|
改完源码:`npm run build` → 重启 `dsh web` → 刷新页面。
|
|
@@ -130,13 +131,14 @@ dsh-wx-skin/
|
|
|
130
131
|
│ ├── demo.png # README 效果截图
|
|
131
132
|
│ └── demo.html # 自包含演示页(浏览器直接打开)
|
|
132
133
|
├── src/
|
|
133
|
-
│ ├── index.ts #
|
|
134
|
+
│ ├── index.ts # 宿主半区:设置持久化路由(webServer → ~/.dsh)
|
|
134
135
|
│ ├── core/types.ts # 共享类型(SkinSettings)
|
|
135
136
|
│ └── client/
|
|
136
137
|
│ ├── index.ts # 浏览器半区入口(apply)
|
|
137
138
|
│ ├── mount.tsx # 侧栏入口 + 弹出面板的 DOM 注入(自愈挂载)
|
|
138
139
|
│ ├── SkinPanel.tsx # 设置面板(选图 / URL / 预设 / 滑杆 / 恢复默认)
|
|
139
|
-
│ ├── skin-store.ts #
|
|
140
|
+
│ ├── skin-store.ts # 纯逻辑:默认值、localStorage、预设、CSS 变量映射
|
|
141
|
+
│ ├── skin-host.ts # 宿主副本读写(loopback fetch,可注入)
|
|
140
142
|
│ ├── image-pipeline.ts # 图片 → 降采样 → data URL(依赖注入,可测)
|
|
141
143
|
│ ├── skin-dom.ts # 全局样式 / 背景层 / 应用到文档
|
|
142
144
|
│ ├── global-skin-css.ts # 皮肤全局样式表(背景层 + 半透明表面 + 明暗适配)
|
|
@@ -151,7 +153,7 @@ dsh-wx-skin/
|
|
|
151
153
|
## 📋 兼容性与注意事项
|
|
152
154
|
|
|
153
155
|
- **DSH 版本**:插件为纯浏览器端 + 单个 cordis 行,对 DSH 版本不敏感;但侧栏入口依赖 shell 的 DOM 结构(`[data-pane="sidebar"]` / `[class*="logoRow"]` / `[class*="newSession"]`)。若 DSH 版本变更导致入口未出现,只会记录日志、不影响 GUI,可反馈后调整选择器。
|
|
154
|
-
- **存储**:本地图片以 data URL 存于 `localStorage`(
|
|
156
|
+
- **存储**:本地图片以 data URL 存于 `localStorage`(浏览器缓存/回退),并镜像到宿主副本 `~/.dsh/dsh-wx-skin.settings.json`(跨端口持久化);超大图会自动缩放至可持久化尺寸。
|
|
155
157
|
- **格式**:仅接受位图(PNG / JPEG / WebP / GIF / BMP);SVG 等矢量格式不支持(canvas 管线只处理位图,安全可预测)。
|
|
156
158
|
- **依赖 `#root`**:皮肤通过将应用根 `#root` 抬升到 `z-index: 1` 使背景层位于应用之下,请确保 shell 的挂载根仍为 `#root`(DSH 默认如此)。
|
|
157
159
|
|
package/lib/client.js
CHANGED
|
@@ -146,8 +146,6 @@ window.__ModuleLoader__.load({
|
|
|
146
146
|
];
|
|
147
147
|
const MAX_DIM = .8;
|
|
148
148
|
const MAX_BLUR = 24;
|
|
149
|
-
/** Surface opacity bounds (0.5..1). */
|
|
150
|
-
const MIN_SURFACE = .5;
|
|
151
149
|
const DEFAULT_SURFACE = .72;
|
|
152
150
|
/** Fresh defaults for a clean install. */
|
|
153
151
|
const DEFAULT_SETTINGS = Object.freeze({
|
|
@@ -209,7 +207,7 @@ window.__ModuleLoader__.load({
|
|
|
209
207
|
preset: str(o.preset),
|
|
210
208
|
dim: clamp(num(o.dim, DEFAULT_SETTINGS.dim), 0, MAX_DIM),
|
|
211
209
|
blur: clamp(num(o.blur, DEFAULT_SETTINGS.blur), 0, MAX_BLUR),
|
|
212
|
-
surface: clamp(num(o.surface, DEFAULT_SETTINGS.surface),
|
|
210
|
+
surface: clamp(num(o.surface, DEFAULT_SETTINGS.surface), 0, 1)
|
|
213
211
|
};
|
|
214
212
|
}
|
|
215
213
|
/** Read + sanitize the persisted settings, or the defaults. */
|
|
@@ -231,6 +229,11 @@ window.__ModuleLoader__.load({
|
|
|
231
229
|
return false;
|
|
232
230
|
}
|
|
233
231
|
}
|
|
232
|
+
/** Whether the settings equal the pristine defaults (used to skip seeding the
|
|
233
|
+
* host copy from localStorage on the first run after upgrade). */
|
|
234
|
+
function isDefaultSettings(settings) {
|
|
235
|
+
return settings.enabled === DEFAULT_SETTINGS.enabled && settings.source === DEFAULT_SETTINGS.source && settings.imageDataUrl === DEFAULT_SETTINGS.imageDataUrl && settings.url === DEFAULT_SETTINGS.url && settings.preset === DEFAULT_SETTINGS.preset && settings.dim === DEFAULT_SETTINGS.dim && settings.blur === DEFAULT_SETTINGS.blur && settings.surface === DEFAULT_SETTINGS.surface;
|
|
236
|
+
}
|
|
234
237
|
/**
|
|
235
238
|
* Project the settings onto the CSS custom properties the skin layer reads.
|
|
236
239
|
* Returns an empty map when the skin is disabled (the applier then retracts
|
|
@@ -264,31 +267,31 @@ window.__ModuleLoader__.load({
|
|
|
264
267
|
document.head.appendChild(tag);
|
|
265
268
|
}
|
|
266
269
|
var skin_module_css_default = {
|
|
267
|
-
"urlRow": "_2J2Nra_urlRow",
|
|
268
270
|
"section": "_2J2Nra_section",
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"stagedThumb": "_2J2Nra_stagedThumb",
|
|
271
|
+
"fileInput": "_2J2Nra_fileInput",
|
|
272
|
+
"panel": "_2J2Nra_panel",
|
|
273
|
+
"swatch": "_2J2Nra_swatch",
|
|
273
274
|
"urlInput": "_2J2Nra_urlInput",
|
|
274
|
-
"
|
|
275
|
-
"button": "_2J2Nra_button",
|
|
275
|
+
"header": "_2J2Nra_header",
|
|
276
276
|
"sliderRow": "_2J2Nra_sliderRow",
|
|
277
|
-
"
|
|
278
|
-
"entry": "_2J2Nra_entry",
|
|
279
|
-
"close": "_2J2Nra_close",
|
|
277
|
+
"slider": "_2J2Nra_slider",
|
|
280
278
|
"footer": "_2J2Nra_footer",
|
|
281
|
-
"
|
|
279
|
+
"close": "_2J2Nra_close",
|
|
280
|
+
"preview": "_2J2Nra_preview",
|
|
281
|
+
"buttonPrimary": "_2J2Nra_buttonPrimary",
|
|
282
|
+
"errorText": "_2J2Nra_errorText",
|
|
282
283
|
"toggleRow": "_2J2Nra_toggleRow",
|
|
283
|
-
"stagedRow": "_2J2Nra_stagedRow",
|
|
284
284
|
"currentBadge": "_2J2Nra_currentBadge",
|
|
285
|
+
"entry": "_2J2Nra_entry",
|
|
285
286
|
"swatchActive": "_2J2Nra_swatchActive",
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"swatch": "_2J2Nra_swatch",
|
|
287
|
+
"entryLabel": "_2J2Nra_entryLabel",
|
|
288
|
+
"button": "_2J2Nra_button",
|
|
289
|
+
"stagedThumb": "_2J2Nra_stagedThumb",
|
|
290
|
+
"presets": "_2J2Nra_presets",
|
|
291
291
|
"sliderLabel": "_2J2Nra_sliderLabel",
|
|
292
|
+
"urlRow": "_2J2Nra_urlRow",
|
|
293
|
+
"stagedRow": "_2J2Nra_stagedRow",
|
|
294
|
+
"entryIcon": "_2J2Nra_entryIcon",
|
|
292
295
|
"stagedLabel": "_2J2Nra_stagedLabel"
|
|
293
296
|
};
|
|
294
297
|
//#endregion
|
|
@@ -568,7 +571,7 @@ window.__ModuleLoader__.load({
|
|
|
568
571
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
569
572
|
className: skin_module_css_default.slider,
|
|
570
573
|
type: "range",
|
|
571
|
-
min:
|
|
574
|
+
min: 0,
|
|
572
575
|
max: 100,
|
|
573
576
|
step: 1,
|
|
574
577
|
value: Math.round(settings.surface * 100),
|
|
@@ -596,6 +599,35 @@ window.__ModuleLoader__.load({
|
|
|
596
599
|
});
|
|
597
600
|
}
|
|
598
601
|
//#endregion
|
|
602
|
+
//#region src/client/skin-host.ts
|
|
603
|
+
/** HTTP route names served by the host half. */
|
|
604
|
+
const LOAD_PATH = "/dsh-wx-skin/load";
|
|
605
|
+
const SAVE_PATH = "/dsh-wx-skin/save";
|
|
606
|
+
/** Read the durable settings from the host, or null when unavailable. */
|
|
607
|
+
async function hostLoad(fetchImpl = globalThis.fetch) {
|
|
608
|
+
try {
|
|
609
|
+
const res = await fetchImpl(LOAD_PATH);
|
|
610
|
+
if (!res.ok) return null;
|
|
611
|
+
const body = await res.json();
|
|
612
|
+
if (body?.ok !== true || body.settings === null || body.settings === void 0 || typeof body.settings !== "object") return null;
|
|
613
|
+
return sanitizeSettings(body.settings);
|
|
614
|
+
} catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/** Persist the settings to the host; false when it could not (never throws). */
|
|
619
|
+
async function hostSave(settings, fetchImpl = globalThis.fetch) {
|
|
620
|
+
try {
|
|
621
|
+
return (await fetchImpl(SAVE_PATH, {
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers: { "content-type": "application/json" },
|
|
624
|
+
body: JSON.stringify({ settings })
|
|
625
|
+
})).ok;
|
|
626
|
+
} catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
//#endregion
|
|
599
631
|
//#region src/client/global-skin-css.ts
|
|
600
632
|
/**
|
|
601
633
|
* dsh-wx-skin — global skin stylesheet, injected once as a <style> tag by
|
|
@@ -834,17 +866,28 @@ html[data-wx-skin-active] button[data-wx-skin-entry] {
|
|
|
834
866
|
const applier = new SkinApplier();
|
|
835
867
|
let latest = loadSettings();
|
|
836
868
|
applier.apply(latest);
|
|
869
|
+
let lastSentHost;
|
|
870
|
+
const syncHost = () => {
|
|
871
|
+
const serialized = JSON.stringify(latest);
|
|
872
|
+
if (lastSentHost === serialized) return;
|
|
873
|
+
lastSentHost = serialized;
|
|
874
|
+
hostSave(latest);
|
|
875
|
+
};
|
|
876
|
+
const persist = () => {
|
|
877
|
+
saveSettings(latest);
|
|
878
|
+
syncHost();
|
|
879
|
+
};
|
|
837
880
|
let saveTimer;
|
|
838
881
|
const flushSave = () => {
|
|
839
882
|
if (saveTimer === void 0) return;
|
|
840
883
|
window.clearTimeout(saveTimer);
|
|
841
884
|
saveTimer = void 0;
|
|
842
|
-
|
|
885
|
+
persist();
|
|
843
886
|
};
|
|
844
887
|
const scheduleSave = () => {
|
|
845
888
|
if (saveTimer !== void 0) window.clearTimeout(saveTimer);
|
|
846
889
|
saveTimer = window.setTimeout(() => {
|
|
847
|
-
|
|
890
|
+
persist();
|
|
848
891
|
}, 200);
|
|
849
892
|
};
|
|
850
893
|
const entry = document.createElement("button");
|
|
@@ -859,6 +902,19 @@ html[data-wx-skin-active] button[data-wx-skin-entry] {
|
|
|
859
902
|
};
|
|
860
903
|
setEntryActive(latest.enabled);
|
|
861
904
|
disposers.push(mountEntrySelfHealing(entry));
|
|
905
|
+
(async () => {
|
|
906
|
+
try {
|
|
907
|
+
const host = await hostLoad();
|
|
908
|
+
if (host !== null) {
|
|
909
|
+
latest = host;
|
|
910
|
+
applier.apply(host);
|
|
911
|
+
setEntryActive(host.enabled);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const local = loadSettings();
|
|
915
|
+
if (!isDefaultSettings(local)) hostSave(local);
|
|
916
|
+
} catch {}
|
|
917
|
+
})();
|
|
862
918
|
const panelHost = document.createElement("div");
|
|
863
919
|
panelHost.setAttribute(PANEL_ATTR, "");
|
|
864
920
|
panelHost.style.cssText = `position: fixed; z-index: 1000; width: ${PANEL_WIDTH}px;`;
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["css","css"],"sources":["../src/client/image-pipeline.ts","../src/client/skin-store.ts","../src/client/SkinPanel.tsx","../src/client/global-skin-css.ts","../src/client/skin-dom.ts","../src/client/mount.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * dsh-wx-skin — local-image pipeline: read a picked file, downscale it to a\n * bounded canvas, and re-encode it to a data URL. The browser implementation\n * is injected through `PipelineEnv` so tests drive it with a fake source; the\n * pure `fileToDataUrl` logic is environment-agnostic.\n *\n * There is intentionally no input-size or output-size limit: any image the\n * canvas can decode is accepted (no \"too large\" rejection). The only bound is\n * the downscale to `maxEdge`, which keeps the resulting data URL small enough\n * to persist while preserving a sharp full-screen background.\n * @module dsh-wx-skin/client/image-pipeline\n */\n\n/** A decodable raster image held by the pipeline. */\nexport interface ImageSource {\n width: number\n height: number\n /** Draw the source scaled to w×h into a fresh canvas and encode it. */\n render(w: number, h: number, type: string, quality: number): string\n /** Release any held resources (object URLs, …). */\n release(): void\n}\n\n/** The browser environment the pipeline runs against. */\nexport interface PipelineEnv {\n /** Decode a file into an ImageSource. Throws when the file is not readable. */\n readImage(file: Blob): Promise<ImageSource>\n}\n\n/** Error the pipeline throws with a stable machine-readable code. */\nexport class ImagePipelineError extends Error {\n constructor(\n public readonly code: 'unsupported' | 'decode' | 'encode',\n message: string,\n ) {\n super(message)\n this.name = 'ImagePipelineError'\n }\n}\n\n/** Tuning knobs for the encode. */\nexport interface DownscaleOptions {\n /**\n * Optional explicit cap on the longest edge of the first encode (px).\n * Omitted by default — the image keeps its original resolution up to\n * {@link DISPLAY_MAX_EDGE}; larger images are downscaled for display (a\n * background never needs more than ~4K, and oversized canvases silently\n * produce empty data URLs, which is exactly the \"selected but not shown\"\n * failure). Picking a file is never limited by size.\n */\n maxEdge?: number\n /** JPEG quality (0..1). Default 0.82. */\n quality?: number\n /**\n * Data-URL length budget (chars). When the encoded result exceeds this,\n * the pipeline silently re-encodes at smaller sizes so the skin stays\n * persistable; it never throws. Default 4_000_000 (safe under the ~5 MB\n * localStorage quota).\n */\n storageBudget?: number\n}\n\n/**\n * Display cap for the encoded background: images larger than this (per side)\n * are downscaled so the canvas stays inside every browser's limits and the\n * data URL stays small enough to persist. 4096 px is sharper than any screen\n * needs for a background.\n */\nexport const DISPLAY_MAX_EDGE = 4096\n\n/** Raster formats the pipeline accepts (canvas can re-encode these safely). */\nexport const ALLOWED_TYPES = [\n 'image/png', 'image/jpeg', 'image/jpg', 'image/pjpeg', 'image/x-png',\n 'image/webp', 'image/gif', 'image/bmp', 'image/x-ms-bmp',\n] as const\n\n/** Default browser environment: object URL + <img> decode + canvas encode. */\nexport const browserEnv: PipelineEnv = {\n async readImage(file: Blob): Promise<ImageSource> {\n const url = URL.createObjectURL(file)\n try {\n const image = await loadImage(url)\n return {\n width: image.naturalWidth,\n height: image.naturalHeight,\n render(w, h, type, quality) {\n const canvas = document.createElement('canvas')\n canvas.width = w\n canvas.height = h\n const ctx = canvas.getContext('2d')\n if (ctx === null) throw new Error('canvas 2d context unavailable')\n ctx.drawImage(image, 0, 0, w, h)\n return canvas.toDataURL(type, quality)\n },\n release() {\n URL.revokeObjectURL(url)\n },\n }\n } catch (error) {\n URL.revokeObjectURL(url)\n throw error\n }\n },\n}\n\n/** Load an <img> from a src, resolving on decode. */\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const image = new Image()\n image.onload = () => resolve(image)\n image.onerror = () => reject(new Error('image load failed'))\n image.src = src\n })\n}\n\n/** Convert a Blob file into a JPEG data URL. Picking is never limited by file\n * size; the encode keeps the original resolution up to the display cap and\n * silently re-encodes smaller when the canvas would be oversized or the data\n * URL would not persist — it never throws a size error. */\nexport async function fileToDataUrl(\n file: Blob,\n env: PipelineEnv = browserEnv,\n options: DownscaleOptions = {},\n): Promise<string> {\n const quality = options.quality ?? 0.82\n const storageBudget = options.storageBudget ?? 4_000_000\n\n if (typeof (file as File).type === 'string' && (file as File).type !== '') {\n const type = (file as File).type.toLowerCase()\n if (type === 'image/svg+xml') {\n throw new ImagePipelineError('unsupported', '不支持 SVG 矢量图,请选择 PNG / JPEG / WebP / GIF / BMP 位图。')\n }\n if (!type.startsWith('image/')) {\n throw new ImagePipelineError('unsupported', `不支持的文件类型“${type}”,请选择 PNG / JPEG / WebP / GIF / BMP 图片。`)\n }\n // image/* beyond the raster whitelist (e.g. avif) proceeds to decode —\n // a codec the canvas cannot read surfaces as a clear \"decode\" error.\n }\n\n let source: ImageSource\n try {\n source = await env.readImage(file)\n } catch (error) {\n throw new ImagePipelineError('decode', '无法读取图片:文件已损坏或不是有效的图片。')\n }\n\n try {\n if (!Number.isFinite(source.width) || !Number.isFinite(source.height) || source.width <= 0 || source.height <= 0) {\n throw new ImagePipelineError('decode', '无法解析图片尺寸,文件可能已损坏。')\n }\n // First attempt: original resolution when it fits the display cap,\n // otherwise downscaled so the canvas stays inside browser limits.\n const cap = options.maxEdge ?? Math.min(Math.max(source.width, source.height), DISPLAY_MAX_EDGE)\n const first = sizeFor(source, cap)\n let data = encode(source, first.w, first.h, quality)\n // Fallback: never reject — when the encode came back invalid (oversized\n // canvases can silently yield empty data URLs) or too big to persist,\n // re-encode smaller. The last attempt always returns.\n if (!isValidDataUrl(data) || data.length > storageBudget) {\n for (const edge of [2560, 1920, 1280]) {\n if (Math.max(first.w, first.h) <= edge) continue\n const next = sizeFor(source, edge)\n data = encode(source, next.w, next.h, Math.max(0.6, quality - 0.1))\n if (isValidDataUrl(data) && data.length <= storageBudget) break\n }\n }\n return data\n } finally {\n try {\n source.release()\n } catch {\n // A release failure must not mask the primary outcome.\n }\n }\n}\n\n/** True when the encoder produced a real image payload (not an empty `data:,`). */\nfunction isValidDataUrl(data: string): boolean {\n return data.startsWith('data:image/') && data.length > 64\n}\n\n/** Bounded target size: original dimensions, or downscaled to fit `edge`. */\nfunction sizeFor(source: ImageSource, edge: number | undefined): { w: number; h: number } {\n if (edge === undefined) return { w: Math.max(1, Math.round(source.width)), h: Math.max(1, Math.round(source.height)) }\n const scale = Math.min(1, edge / Math.max(source.width, source.height))\n return {\n w: Math.max(1, Math.round(source.width * scale)),\n h: Math.max(1, Math.round(source.height * scale)),\n }\n}\n\n/** Draw + encode one attempt; encode failures are wrapped with a stable code. */\nfunction encode(source: ImageSource, w: number, h: number, quality: number): string {\n try {\n return source.render(w, h, 'image/jpeg', quality)\n } catch (error) {\n throw new ImagePipelineError('encode', '图片编码失败,请换一张图片。')\n }\n}\n","/**\n * dsh-wx-skin — pure skin settings state: defaults, sanitization, persistence\n * (localStorage), preset catalog, and the CSS-variable projection consumed by\n * the DOM applier. No DOM access here — tests exercise these functions with a\n * fake storage.\n * @module dsh-wx-skin/client/skin-store\n */\nimport type { SkinSettings, SkinSource } from '../core/types.ts'\n\n/** localStorage key holding the durable settings JSON. */\nexport const STORAGE_KEY = 'dsh-wx-skin.settings'\n\n/** Document attribute that gates the global skin CSS (set on documentElement). */\nexport const ACTIVE_ATTR = 'data-wx-skin-active'\n\n/** Attribute on the injected background layer div. */\nexport const LAYER_ATTR = 'data-wx-skin-layer'\n\n/** Attribute on the injected sidebar entry button. */\nexport const ENTRY_ATTR = 'data-wx-skin-entry'\n\n/** CSS custom properties the applier writes on documentElement. */\nexport const SKIN_CSS_VARS = [\n '--wx-skin-bg-image',\n '--wx-skin-bg-color',\n '--wx-skin-scrim',\n '--wx-skin-blur',\n '--wx-skin-surface',\n] as const\n\n/** Clamp a number into [min, max]. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nconst SOURCES: readonly SkinSource[] = ['image', 'url', 'preset', 'none']\n\nconst MAX_DIM = 0.8\nconst MAX_BLUR = 24\n/** Surface opacity bounds (0.5..1). */\nexport const MIN_SURFACE = 0.5\nexport const MAX_SURFACE = 1\nexport const DEFAULT_SURFACE = 0.72\n\n/** Fresh defaults for a clean install. */\nexport const DEFAULT_SETTINGS: SkinSettings = Object.freeze({\n enabled: false,\n source: 'none',\n imageDataUrl: null,\n url: null,\n preset: null,\n dim: 0.35,\n blur: 0,\n surface: DEFAULT_SURFACE,\n})\n\n/** A selectable preset skin: solid color or gradient CSS value. */\nexport interface SkinPreset {\n id: string\n label: string\n /** CSS background value (solid color or linear-gradient). */\n value: string\n}\n\n/** Built-in presets — immediate effect without picking an image. */\nexport const PRESETS: readonly SkinPreset[] = Object.freeze([\n { id: 'color-ink', label: '墨蓝', value: '#1f2a44' },\n { id: 'color-slate', label: '石板', value: '#3b4252' },\n { id: 'color-sand', label: '暖沙', value: '#e9e2d0' },\n { id: 'grad-sunset', label: '落日渐变', value: 'linear-gradient(135deg, #ff9a8b, #ff6a88 45%, #ff99ac)' },\n { id: 'grad-ocean', label: '深海渐变', value: 'linear-gradient(135deg, #0f2027, #203a43 55%, #2c5364)' },\n { id: 'grad-aurora', label: '极光渐变', value: 'linear-gradient(135deg, #43cea2, #185a9d 60%, #9cecfb)' },\n])\n\n/** Coerce an unknown persisted value into a valid SkinSettings. */\nexport function sanitizeSettings(raw: unknown): SkinSettings {\n const o = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>\n const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null)\n const num = (v: unknown, fallback: number): number => {\n const n = typeof v === 'number' && Number.isFinite(v) ? v : fallback\n return n\n }\n const source: SkinSource = SOURCES.includes(o.source as SkinSource) ? (o.source as SkinSource) : 'none'\n return {\n enabled: o.enabled === true,\n source,\n imageDataUrl: str(o.imageDataUrl),\n url: str(o.url),\n preset: str(o.preset),\n dim: clamp(num(o.dim, DEFAULT_SETTINGS.dim), 0, MAX_DIM),\n blur: clamp(num(o.blur, DEFAULT_SETTINGS.blur), 0, MAX_BLUR),\n surface: clamp(num(o.surface, DEFAULT_SETTINGS.surface), MIN_SURFACE, MAX_SURFACE),\n }\n}\n\n/** Read + sanitize the persisted settings, or the defaults. */\nexport function loadSettings(storage: Pick<Storage, 'getItem'> = globalThis.localStorage): SkinSettings {\n try {\n const text = storage.getItem(STORAGE_KEY)\n if (text === null) return { ...DEFAULT_SETTINGS }\n return sanitizeSettings(JSON.parse(text))\n } catch {\n return { ...DEFAULT_SETTINGS }\n }\n}\n\n/** Persist settings; quota errors are swallowed (the caller surfaces a message). */\nexport function saveSettings(settings: SkinSettings, storage: Pick<Storage, 'setItem'> = globalThis.localStorage): boolean {\n try {\n storage.setItem(STORAGE_KEY, JSON.stringify(settings))\n return true\n } catch {\n return false\n }\n}\n\n/** Remove the persisted settings (restores defaults on next boot). */\nexport function clearSettings(storage: Pick<Storage, 'removeItem'> = globalThis.localStorage): void {\n try {\n storage.removeItem(STORAGE_KEY)\n } catch {\n // Ignore.\n }\n}\n\n/**\n * Project the settings onto the CSS custom properties the skin layer reads.\n * Returns an empty map when the skin is disabled (the applier then retracts\n * every variable).\n */\nexport function cssVariables(settings: SkinSettings): Partial<Record<(typeof SKIN_CSS_VARS)[number], string>> {\n if (!settings.enabled) return {}\n const vars: Partial<Record<(typeof SKIN_CSS_VARS)[number], string>> = {}\n if (settings.source === 'image' && settings.imageDataUrl !== null) {\n vars['--wx-skin-bg-image'] = `url(\"${settings.imageDataUrl}\")`\n } else if (settings.source === 'url' && settings.url !== null) {\n vars['--wx-skin-bg-image'] = `url(\"${escapeCssUrl(settings.url)}\")`\n } else if (settings.source === 'preset' && settings.preset !== null) {\n // A gradient goes to background-image; a solid color to background-color.\n if (settings.preset.includes('gradient')) vars['--wx-skin-bg-image'] = settings.preset\n else vars['--wx-skin-bg-color'] = settings.preset\n }\n vars['--wx-skin-scrim'] = `rgba(0, 0, 0, ${settings.dim.toFixed(3)})`\n vars['--wx-skin-blur'] = `${Math.round(settings.blur)}px`\n vars['--wx-skin-surface'] = settings.surface.toFixed(3)\n return vars\n}\n\n/** Escape quotes so a user-supplied URL cannot break out of `url(\"...\")`. */\nexport function escapeCssUrl(url: string): string {\n return url.replace(/[\"\\\\\\n\\r]/g, char => `\\\\${char}`)\n}\n","/**\n * dsh-wx-skin — the popover panel: enable toggle, background preview, local\n * image picker (file input → downscale → data URL), URL input, presets, dim /\n * blur sliders, and reset. The component is controlled: every change calls\n * `commit` (mount persists + applies to the document), keeping the panel and\n * the live skin in lockstep.\n * @module dsh-wx-skin/client/skin-panel\n */\nimport { useRef, useState, type ChangeEvent } from 'react'\nimport type { SkinSettings } from '../core/types.ts'\nimport { ImagePipelineError, fileToDataUrl } from './image-pipeline.ts'\nimport { DEFAULT_SURFACE, MIN_SURFACE, MAX_SURFACE, PRESETS } from './skin-store.ts'\nimport css from './skin.module.css'\n\nexport interface SkinPanelProps {\n /** Settings as loaded at open time. */\n initial: SkinSettings\n /** Persist + apply a settings change (mount wires storage + DOM). */\n commit: (next: SkinSettings) => void\n /** Close the popover. */\n onClose: () => void\n}\n\n/** Describe the currently active source for the status badge. */\nfunction describe(settings: SkinSettings): string {\n if (!settings.enabled) return '未启用'\n if (settings.source === 'image') return '图片(本地)'\n if (settings.source === 'url') return '图片(URL)'\n if (settings.source === 'preset') return '预设'\n return '未启用'\n}\n\n/** The popover skin settings panel. */\nexport function SkinPanel({ initial, commit, onClose }: SkinPanelProps): JSX.Element {\n const [settings, setSettings] = useState<SkinSettings>(initial)\n const [urlInput, setUrlInput] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n /** A just-picked local image, processed but not yet applied — the user\n * clicks 「应用」 to commit it (explicit two-step flow). */\n const [staged, setStaged] = useState<string | null>(null)\n const fileRef = useRef<HTMLInputElement | null>(null)\n\n const setAndCommit = (next: SkinSettings): void => {\n setSettings(next)\n setError(null)\n commit(next)\n }\n\n const onPickFile = async (event: ChangeEvent<HTMLInputElement>): Promise<void> => {\n const file = event.target.files?.[0]\n event.target.value = ''\n if (file === undefined) return\n setBusy(true)\n setError(null)\n try {\n const dataUrl = await fileToDataUrl(file)\n setStaged(dataUrl)\n } catch (err) {\n setStaged(null)\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n setBusy(false)\n }\n }\n\n /** Commit the staged local image to the skin. */\n const onApplyImage = (): void => {\n if (staged === null) return\n setAndCommit({\n ...settings,\n enabled: true,\n source: 'image',\n imageDataUrl: staged,\n url: null,\n preset: null,\n })\n setStaged(null)\n }\n\n const onApplyUrl = (): void => {\n const url = urlInput.trim()\n if (url === '') {\n setError('请输入图片 URL(http:// 或 https:// 开头)。')\n return\n }\n setAndCommit({\n ...settings,\n enabled: true,\n source: 'url',\n url,\n imageDataUrl: null,\n preset: null,\n })\n setUrlInput('')\n setStaged(null)\n }\n\n const onPreset = (value: string): void => {\n setAndCommit({\n ...settings,\n enabled: true,\n source: 'preset',\n preset: value,\n imageDataUrl: null,\n url: null,\n })\n }\n\n const onReset = (): void => {\n setStaged(null)\n setAndCommit({\n ...settings,\n enabled: false,\n source: 'none',\n imageDataUrl: null,\n url: null,\n preset: null,\n dim: 0.35,\n blur: 0,\n surface: DEFAULT_SURFACE,\n })\n }\n\n const previewStyle = settings.enabled\n ? { background: 'var(--wx-skin-bg-image, none) center / cover no-repeat, var(--wx-skin-bg-color, transparent)' }\n : undefined\n\n return (\n <div className={css.panel} role=\"dialog\" aria-label=\"皮肤设置\">\n <div className={css.header}>\n <span>皮肤</span>\n <button type=\"button\" className={css.close} aria-label=\"关闭\" onClick={onClose}>×</button>\n </div>\n\n <label className={css.toggleRow}>\n <input\n type=\"checkbox\"\n checked={settings.enabled}\n onChange={e => setAndCommit({ ...settings, enabled: e.target.checked })}\n />\n <span>启用皮肤</span>\n </label>\n\n <div className={css.preview} style={previewStyle}>\n {!settings.enabled && <span>未启用</span>}\n </div>\n <div className={css.currentBadge}>当前:{describe(settings)}</div>\n\n <div className={css.section}>选择图片</div>\n <div className={css.urlRow}>\n <button\n type=\"button\"\n className={css.buttonPrimary}\n disabled={busy}\n onClick={() => fileRef.current?.click()}\n >\n {busy ? '处理中…' : '选择图片'}\n </button>\n <input\n ref={fileRef}\n className={css.fileInput}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/bmp\"\n onChange={(e) => void onPickFile(e)}\n />\n </div>\n {staged !== null && (\n <div className={css.stagedRow}>\n <div\n className={css.stagedThumb}\n style={{ backgroundImage: `url(\"${staged}\")`, backgroundSize: 'cover', backgroundPosition: 'center' }}\n />\n <span className={css.stagedLabel}>已选择图片</span>\n <button type=\"button\" className={css.buttonPrimary} onClick={onApplyImage}>应用</button>\n </div>\n )}\n\n <div className={css.section}>图片 URL</div>\n <div className={css.urlRow}>\n <input\n className={css.urlInput}\n value={urlInput}\n placeholder=\"https://…/image.jpg\"\n onChange={e => setUrlInput(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter') onApplyUrl() }}\n />\n <button type=\"button\" className={css.button} onClick={onApplyUrl}>应用</button>\n </div>\n\n <div className={css.section}>预设</div>\n <div className={css.presets}>\n {PRESETS.map(preset => (\n <button\n key={preset.id}\n type=\"button\"\n className={settings.source === 'preset' && settings.preset === preset.value\n ? `${css.swatch} ${css.swatchActive}`\n : css.swatch}\n title={preset.label}\n style={{ background: preset.value }}\n onClick={() => onPreset(preset.value)}\n />\n ))}\n </div>\n\n <div className={css.section}>效果</div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>暗化 {Math.round(settings.dim * 100)}%</span>\n <input\n className={css.slider}\n type=\"range\"\n min={0}\n max={80}\n step={5}\n value={Math.round(settings.dim * 100)}\n onChange={e => setAndCommit({ ...settings, dim: Number(e.target.value) / 100 })}\n />\n </div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>模糊 {Math.round(settings.blur)}px</span>\n <input\n className={css.slider}\n type=\"range\"\n min={0}\n max={24}\n step={1}\n value={Math.round(settings.blur)}\n onChange={e => setAndCommit({ ...settings, blur: Number(e.target.value) })}\n />\n </div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>透出 {Math.round(settings.surface * 100)}%</span>\n <input\n className={css.slider}\n type=\"range\"\n min={MIN_SURFACE * 100}\n max={MAX_SURFACE * 100}\n step={1}\n value={Math.round(settings.surface * 100)}\n title=\"表面不透明度越低,背景图片越明显\"\n onChange={e => setAndCommit({ ...settings, surface: Number(e.target.value) / 100 })}\n />\n </div>\n\n {error !== null && <div className={css.errorText}>{error}</div>}\n\n <div className={css.footer}>\n <button type=\"button\" className={css.button} onClick={onReset}>恢复默认</button>\n </div>\n </div>\n )\n}\n","/**\n * dsh-wx-skin — global skin stylesheet, injected once as a <style> tag by\n * mount. Everything is gated on the `data-wx-skin-active` attribute on\n * documentElement, which the DOM applier toggles; with the attribute absent\n * the sheet is inert and the default theme is untouched.\n *\n * Design: a full-viewport fixed layer paints the chosen image/color, and the\n * alias surface tokens are overridden with translucent values so the\n * background shows through while text stays readable. The light/dark palette\n * branches ride `body[data-ds-dark-theme]`, exactly like the theme's own\n * token sheets.\n *\n * Stacking: measured on the real DSH shell, a `position: fixed` layer with\n * `z-index: -1` never paints (it lands behind the canvas background), while a\n * positive z-index layer paints fine. So the layer sits at `z-index: 0` and\n * the app root `#root` is lifted to `z-index: 1` — the layer renders behind\n * the app and shows through its translucent surfaces.\n *\n * Translucency: every surface token is `rgb(<static palette literal> /\n * var(--wx-skin-surface))` — a user-adjustable surface opacity (default 0.72)\n * — with no `color-mix()` dependency, so it works on any modern browser.\n * @module dsh-wx-skin/client/global-skin-css\n */\n\nexport const GLOBAL_SKIN_CSS = String.raw`\n/* ---- background layer (paints behind the app, above the canvas) ---- */\nhtml[data-wx-skin-active] div[data-wx-skin-layer] {\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image: var(--wx-skin-bg-image, none);\n background-color: var(--wx-skin-bg-color, transparent);\n background-position: center;\n background-size: cover;\n background-repeat: no-repeat;\n filter: blur(var(--wx-skin-blur, 0px));\n}\n\n/* dim / scrim overlay between the image and the translucent surfaces */\nhtml[data-wx-skin-active] div[data-wx-skin-layer]::after {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--wx-skin-scrim, transparent);\n}\n\n/* keep the canvas neutral so nothing opaque hides the layer */\nhtml[data-wx-skin-active] body {\n background: transparent !important;\n}\n\n/* lift the app above the layer; its translucent surfaces let it show through */\nhtml[data-wx-skin-active] #root {\n position: relative !important;\n z-index: 1 !important;\n}\n\n/* ---- translucent surfaces: light palette ---- */\nhtml[data-wx-skin-active] body {\n --dsw-alias-bg-base: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-1: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-2: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-3: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-overlay: rgb(233 236 242 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-module-platform: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-multi-select: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-sidebar-fill: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-input-major: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-selector: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-tip: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble: rgb(237 243 254 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble-highlight: rgb(211 226 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block-banner: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-inline-code: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-placeholder: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-tag: rgb(241 243 245 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-citation: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;\n}\n\n/* ---- translucent surfaces: dark palette ---- */\nhtml[data-wx-skin-active] body[data-ds-dark-theme] {\n --dsw-alias-bg-base: rgb(21 21 23 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-1: rgb(35 35 36 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-2: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-3: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-overlay: rgb(97 102 107 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-module-platform: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-multi-select: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-sidebar-fill: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-input-major: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-selector: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-tip: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble-highlight: rgb(67 69 74 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block-banner: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-inline-code: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-placeholder: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-tag: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-citation: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n}\n\n/* ---- sidebar entry active indicator ---- */\nhtml[data-wx-skin-active] button[data-wx-skin-entry] {\n color: var(--dsw-alias-state-business-primary, #4176e6);\n}\n`\n","/**\n * dsh-wx-skin — DOM applier: owns the injected global stylesheet, the\n * background layer element, and the projection of skin settings onto the\n * document (attribute + CSS variables). Pure DOM writes; retracts only what\n * it wrote, so the default theme is untouched when the skin is off.\n * @module dsh-wx-skin/client/skin-dom\n */\nimport type { SkinSettings } from '../core/types.ts'\nimport { GLOBAL_SKIN_CSS } from './global-skin-css.ts'\nimport { ACTIVE_ATTR, LAYER_ATTR, SKIN_CSS_VARS, cssVariables } from './skin-store.ts'\n\n/** Identity of the injected global <style> tag (idempotent injection key). */\nexport const GLOBAL_STYLE_ID = 'dsh-wx-skin/global'\n\n/** Inject the global skin stylesheet once. Safe to call repeatedly. */\nexport function ensureGlobalCss(): void {\n if (document.querySelector(`style[data-plugin-css=\"${GLOBAL_STYLE_ID}\"]`) !== null) return\n const tag = document.createElement('style')\n tag.dataset.pluginCss = GLOBAL_STYLE_ID\n tag.textContent = GLOBAL_SKIN_CSS\n document.head.appendChild(tag)\n}\n\n/** Ensure the full-viewport background layer exists and return it. */\nexport function ensureLayer(): HTMLDivElement {\n let layer = document.querySelector<HTMLDivElement>(`div[${LAYER_ATTR}]`)\n if (layer === null) {\n layer = document.createElement('div')\n layer.setAttribute(LAYER_ATTR, '')\n document.body.appendChild(layer)\n }\n return layer\n}\n\n/**\n * Apply skin settings to the document: toggle the gating attribute on\n * documentElement and write/retract the four CSS variables every time (so a\n * disabled skin leaves no residue).\n */\nexport class SkinApplier {\n apply(settings: SkinSettings): void {\n const root = document.documentElement\n if (settings.enabled) root.setAttribute(ACTIVE_ATTR, '')\n else root.removeAttribute(ACTIVE_ATTR)\n const vars = cssVariables(settings)\n for (const name of SKIN_CSS_VARS) {\n const value = vars[name]\n if (value === undefined) root.style.removeProperty(name)\n else root.style.setProperty(name, value)\n }\n }\n}\n","/**\n * dsh-wx-skin — DOM mounting: inject the global stylesheet and background\n * layer, place the sidebar 「皮肤」 entry (self-healing across shell\n * re-renders), and toggle an anchored popover with the settings panel. All\n * mount failures are logged, never thrown (the shell fails boot when a plugin\n * apply throws). Persists settings with a short debounce so slider drags do\n * not hammer localStorage with the image data URL.\n * @module dsh-wx-skin/client/mount\n */\nimport { createRoot, type Root } from 'react-dom/client'\nimport type { SkinSettings } from '../core/types.ts'\nimport { SkinPanel } from './SkinPanel.tsx'\nimport { ENTRY_ATTR, loadSettings, saveSettings } from './skin-store.ts'\nimport { SkinApplier, ensureGlobalCss, ensureLayer } from './skin-dom.ts'\nimport css from './skin.module.css'\n\nconst PANEL_ATTR = 'data-wx-skin-panel'\nconst PANEL_WIDTH = 300\n\nconst ICON = '<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"8\" cy=\"8\" r=\"6\"/><path d=\"M8 2v2.2M8 11.8V14M2 8h2.2M11.8 8H14M4 4l1.6 1.6M10.4 10.4 12 12M12 4l-1.6 1.6M5.6 10.4 4 12\"/></svg>'\n\n/** Locate the sidebar shell root, or undefined while not yet mounted. */\nfunction sidebarRoot(): HTMLElement | undefined {\n const column = document.querySelector<HTMLElement>('[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]')\n if (column === null) return undefined\n const logoOwner = column.querySelector<HTMLElement>('[class*=\"logoRow\"]')?.parentElement\n return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)\n}\n\n/** The New Session button (nested in the logo row on current shells). */\nfunction newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {\n const nested = root.querySelector<HTMLButtonElement>('button[class*=\"newSession\"]')\n if (nested !== null) return nested\n for (const child of root.children) {\n if (child.tagName === 'BUTTON') return child as HTMLButtonElement\n }\n return undefined\n}\n\n/** Keep the entry in the sidebar, re-placing it across shell re-renders. */\nfunction mountEntrySelfHealing(entry: HTMLButtonElement): () => void {\n let root: HTMLElement | undefined\n let placed = false\n let rootObserver: MutationObserver | undefined\n\n const tryPlace = (): void => {\n if (root !== undefined && !root.isConnected) {\n rootObserver?.disconnect()\n root = undefined\n placed = false\n }\n if (placed) {\n if (document.body.contains(entry)) return\n rootObserver?.disconnect()\n root = undefined\n placed = false\n }\n root ??= sidebarRoot()\n if (root === undefined) return\n const anchor = newSessionButton(root)\n if (anchor === undefined) return\n if (entry.parentElement !== root) root.insertBefore(entry, anchor.nextElementSibling)\n placed = true\n if (rootObserver === undefined) {\n rootObserver = new MutationObserver(() => {\n if (root === undefined || !root.isConnected) {\n placed = false\n tryPlace()\n return\n }\n if (!root.contains(entry)) tryPlace()\n })\n rootObserver.observe(root, { childList: true, subtree: true })\n }\n }\n\n const waitObserver = new MutationObserver(() => { tryPlace() })\n waitObserver.observe(document.body, { childList: true, subtree: true })\n tryPlace()\n\n return () => {\n waitObserver.disconnect()\n rootObserver?.disconnect()\n entry.remove()\n }\n}\n\n/**\n * Mount the skin feature into the web shell.\n * @returns disposer tearing down the entry, popover, layer, and stylesheet.\n */\nexport function mountSkin(): () => void {\n const disposers: Array<() => void> = []\n ensureGlobalCss()\n ensureLayer()\n\n const applier = new SkinApplier()\n let latest = loadSettings()\n applier.apply(latest)\n\n // Debounced persistence (slider drags must not rewrite the image data URL\n // on every tick).\n let saveTimer: number | undefined\n const flushSave = (): void => {\n if (saveTimer === undefined) return\n window.clearTimeout(saveTimer)\n saveTimer = undefined\n saveSettings(latest)\n }\n const scheduleSave = (): void => {\n if (saveTimer !== undefined) window.clearTimeout(saveTimer)\n saveTimer = window.setTimeout(() => { saveSettings(latest) }, 200)\n }\n\n // Sidebar entry.\n const entry = document.createElement('button')\n entry.type = 'button'\n entry.setAttribute(ENTRY_ATTR, '')\n entry.className = css.entry\n entry.setAttribute('aria-label', '皮肤')\n entry.innerHTML = `<span class=\"${css.entryIcon}\">${ICON}</span><span class=\"${css.entryLabel}\">皮肤</span>`\n const setEntryActive = (active: boolean): void => {\n if (active) entry.dataset.active = 'true'\n else delete entry.dataset.active\n }\n setEntryActive(latest.enabled)\n disposers.push(mountEntrySelfHealing(entry))\n\n // Popover host + React root.\n const panelHost = document.createElement('div')\n panelHost.setAttribute(PANEL_ATTR, '')\n panelHost.style.cssText = `position: fixed; z-index: 1000; width: ${PANEL_WIDTH}px;`\n let root: Root | undefined\n\n const positionPopover = (): void => {\n const rect = entry.getBoundingClientRect()\n const left = Math.max(8, Math.min(rect.right + 10, window.innerWidth - PANEL_WIDTH - 8))\n const top = Math.max(8, rect.top)\n panelHost.style.left = `${left}px`\n panelHost.style.top = `${top}px`\n }\n\n const closePanel = (): void => {\n if (root === undefined) return\n flushSave()\n root.unmount()\n root = undefined\n panelHost.remove()\n }\n\n const openPanel = (): void => {\n if (root !== undefined) return\n positionPopover()\n document.body.appendChild(panelHost)\n root = createRoot(panelHost)\n root.render(<SkinPanel\n initial={latest}\n onClose={closePanel}\n commit={(next) => {\n latest = next\n applier.apply(next)\n setEntryActive(next.enabled)\n scheduleSave()\n }}\n />)\n }\n\n const togglePanel = (): void => { if (root !== undefined) closePanel(); else openPanel() }\n entry.addEventListener('click', togglePanel)\n\n // Close on outside click, Escape, or window resize.\n const onDocMouseDown = (event: MouseEvent): void => {\n if (root === undefined) return\n const target = event.target as Node | null\n if (target !== null && (panelHost.contains(target) || entry.contains(target))) return\n closePanel()\n }\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && root !== undefined) closePanel()\n }\n const onResize = (): void => { if (root !== undefined) closePanel() }\n const onBeforeUnload = (): void => flushSave()\n document.addEventListener('mousedown', onDocMouseDown)\n window.addEventListener('keydown', onKeyDown)\n window.addEventListener('resize', onResize)\n window.addEventListener('beforeunload', onBeforeUnload)\n disposers.push(() => {\n document.removeEventListener('mousedown', onDocMouseDown)\n window.removeEventListener('keydown', onKeyDown)\n window.removeEventListener('resize', onResize)\n window.removeEventListener('beforeunload', onBeforeUnload)\n })\n\n return () => {\n closePanel()\n for (const dispose of disposers.splice(0)) dispose()\n document.querySelector<HTMLElement>(`div[data-wx-skin-layer]`)?.remove()\n document.querySelector<HTMLElement>(`style[data-plugin-css=\"dsh-wx-skin/global\"]`)?.remove()\n applier.apply({ ...latest, enabled: false })\n }\n}\n","/**\n * dsh-wx-skin — browser half entry. Mounts the skin feature (sidebar entry +\n * settings popover + background layer). All failures are logged, never thrown.\n * @module dsh-wx-skin/client\n */\nimport { mountSkin } from './mount.tsx'\n\n/** No hard service dependencies — the skin only touches the DOM. */\nexport const inject: string[] = []\n\n/** Apply the browser half. */\nexport function apply(): void {\n try {\n mountSkin()\n } catch (error) {\n // A failing external plugin must never take the GUI down.\n console.error('[dsh-wx-skin] mount failed:', error)\n }\n}\n"],"mappings":";;;;;;;;;;;EA8BA,IAAa,qBAAb,cAAwC,MAAM;GAE1B;GADlB,YACE,MACA,SACA;IACA,MAAM,OAAO;IAHG,KAAA,OAAA;IAIhB,KAAK,OAAO;GACd;EACF;;EAuCA,MAAa,aAA0B,EACrC,MAAM,UAAU,MAAkC;GAChD,MAAM,MAAM,IAAI,gBAAgB,IAAI;GACpC,IAAI;IACF,MAAM,QAAQ,MAAM,UAAU,GAAG;IACjC,OAAO;KACL,OAAO,MAAM;KACb,QAAQ,MAAM;KACd,OAAO,GAAG,GAAG,MAAM,SAAS;MAC1B,MAAM,SAAS,SAAS,cAAc,QAAQ;MAC9C,OAAO,QAAQ;MACf,OAAO,SAAS;MAChB,MAAM,MAAM,OAAO,WAAW,IAAI;MAClC,IAAI,QAAQ,MAAM,MAAM,IAAI,MAAM,+BAA+B;MACjE,IAAI,UAAU,OAAO,GAAG,GAAG,GAAG,CAAC;MAC/B,OAAO,OAAO,UAAU,MAAM,OAAO;KACvC;KACA,UAAU;MACR,IAAI,gBAAgB,GAAG;KACzB;IACF;GACF,SAAS,OAAO;IACd,IAAI,gBAAgB,GAAG;IACvB,MAAM;GACR;EACF,EACF;;EAGA,SAAS,UAAU,KAAwC;GACzD,OAAO,IAAI,SAAS,SAAS,WAAW;IACtC,MAAM,QAAQ,IAAI,MAAM;IACxB,MAAM,eAAe,QAAQ,KAAK;IAClC,MAAM,gBAAgB,uBAAO,IAAI,MAAM,mBAAmB,CAAC;IAC3D,MAAM,MAAM;GACd,CAAC;EACH;;;;;EAMA,eAAsB,cACpB,MACA,MAAmB,YACnB,UAA4B,CAAC,GACZ;GACjB,MAAM,UAAU,QAAQ,WAAW;GACnC,MAAM,gBAAgB,QAAQ,iBAAiB;GAE/C,IAAI,OAAQ,KAAc,SAAS,YAAa,KAAc,SAAS,IAAI;IACzE,MAAM,OAAQ,KAAc,KAAK,YAAY;IAC7C,IAAI,SAAS,iBACX,MAAM,IAAI,mBAAmB,eAAe,mDAAmD;IAEjG,IAAI,CAAC,KAAK,WAAW,QAAQ,GAC3B,MAAM,IAAI,mBAAmB,eAAe,YAAY,KAAK,wCAAwC;GAIzG;GAEA,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,IAAI,UAAU,IAAI;GACnC,SAAS,OAAO;IACd,MAAM,IAAI,mBAAmB,UAAU,uBAAuB;GAChE;GAEA,IAAI;IACF,IAAI,CAAC,OAAO,SAAS,OAAO,KAAK,KAAK,CAAC,OAAO,SAAS,OAAO,MAAM,KAAK,OAAO,SAAS,KAAK,OAAO,UAAU,GAC7G,MAAM,IAAI,mBAAmB,UAAU,mBAAmB;IAI5D,MAAM,MAAM,QAAQ,WAAW,KAAK,IAAI,KAAK,IAAI,OAAO,OAAO,OAAO,MAAM,GAAA,IAAmB;IAC/F,MAAM,QAAQ,QAAQ,QAAQ,GAAG;IACjC,IAAI,OAAO,OAAO,QAAQ,MAAM,GAAG,MAAM,GAAG,OAAO;IAInD,IAAI,CAAC,eAAe,IAAI,KAAK,KAAK,SAAS,eACzC,KAAK,MAAM,QAAQ;KAAC;KAAM;KAAM;IAAI,GAAG;KACrC,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM;KACxC,MAAM,OAAO,QAAQ,QAAQ,IAAI;KACjC,OAAO,OAAO,QAAQ,KAAK,GAAG,KAAK,GAAG,KAAK,IAAI,IAAK,UAAU,EAAG,CAAC;KAClE,IAAI,eAAe,IAAI,KAAK,KAAK,UAAU,eAAe;IAC5D;IAEF,OAAO;GACT,UAAU;IACR,IAAI;KACF,OAAO,QAAQ;IACjB,QAAQ,CAER;GACF;EACF;;EAGA,SAAS,eAAe,MAAuB;GAC7C,OAAO,KAAK,WAAW,aAAa,KAAK,KAAK,SAAS;EACzD;;EAGA,SAAS,QAAQ,QAAqB,MAAoD;GACxF,IAAI,SAAS,KAAA,GAAW,OAAO;IAAE,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,KAAK,CAAC;IAAG,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,MAAM,CAAC;GAAE;GACrH,MAAM,QAAQ,KAAK,IAAI,GAAG,OAAO,KAAK,IAAI,OAAO,OAAO,OAAO,MAAM,CAAC;GACtE,OAAO;IACL,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,KAAK,CAAC;IAC/C,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,SAAS,KAAK,CAAC;GAClD;EACF;;EAGA,SAAS,OAAO,QAAqB,GAAW,GAAW,SAAyB;GAClF,IAAI;IACF,OAAO,OAAO,OAAO,GAAG,GAAG,cAAc,OAAO;GAClD,SAAS,OAAO;IACd,MAAM,IAAI,mBAAmB,UAAU,gBAAgB;GACzD;EACF;;;;EC5LA,MAAa,cAAc;;EAG3B,MAAa,cAAc;;EAG3B,MAAa,aAAa;;EAG1B,MAAa,aAAa;;EAG1B,MAAa,gBAAgB;GAC3B;GACA;GACA;GACA;GACA;EACF;;EAGA,SAAgB,MAAM,OAAe,KAAa,KAAqB;GACrE,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;EAC3C;EAEA,MAAM,UAAiC;GAAC;GAAS;GAAO;GAAU;EAAM;EAExE,MAAM,UAAU;EAChB,MAAM,WAAW;;EAEjB,MAAa,cAAc;EAE3B,MAAa,kBAAkB;;EAG/B,MAAa,mBAAiC,OAAO,OAAO;GAC1D,SAAS;GACT,QAAQ;GACR,cAAc;GACd,KAAK;GACL,QAAQ;GACR,KAAK;GACL,MAAM;GACN,SAAS;EACX,CAAC;;EAWD,MAAa,UAAiC,OAAO,OAAO;GAC1D;IAAE,IAAI;IAAa,OAAO;IAAM,OAAO;GAAU;GACjD;IAAE,IAAI;IAAe,OAAO;IAAM,OAAO;GAAU;GACnD;IAAE,IAAI;IAAc,OAAO;IAAM,OAAO;GAAU;GAClD;IAAE,IAAI;IAAe,OAAO;IAAQ,OAAO;GAAyD;GACpG;IAAE,IAAI;IAAc,OAAO;IAAQ,OAAO;GAAyD;GACnG;IAAE,IAAI;IAAe,OAAO;IAAQ,OAAO;GAAyD;EACtG,CAAC;;EAGD,SAAgB,iBAAiB,KAA4B;GAC3D,MAAM,IAAK,OAAO,QAAQ,YAAY,QAAQ,OAAO,MAAM,CAAC;GAC5D,MAAM,OAAO,MAA+B,OAAO,MAAM,YAAY,MAAM,KAAK,IAAI;GACpF,MAAM,OAAO,GAAY,aAA6B;IAEpD,OADU,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI;GAE9D;GACA,MAAM,SAAqB,QAAQ,SAAS,EAAE,MAAoB,IAAK,EAAE,SAAwB;GACjG,OAAO;IACL,SAAS,EAAE,YAAY;IACvB;IACA,cAAc,IAAI,EAAE,YAAY;IAChC,KAAK,IAAI,EAAE,GAAG;IACd,QAAQ,IAAI,EAAE,MAAM;IACpB,KAAK,MAAM,IAAI,EAAE,KAAK,iBAAiB,GAAG,GAAG,GAAG,OAAO;IACvD,MAAM,MAAM,IAAI,EAAE,MAAM,iBAAiB,IAAI,GAAG,GAAG,QAAQ;IAC3D,SAAS,MAAM,IAAI,EAAE,SAAS,iBAAiB,OAAO,GAAG,aAAA,CAAwB;GACnF;EACF;;EAGA,SAAgB,aAAa,UAAoC,WAAW,cAA4B;GACtG,IAAI;IACF,MAAM,OAAO,QAAQ,QAAQ,WAAW;IACxC,IAAI,SAAS,MAAM,OAAO,EAAE,GAAG,iBAAiB;IAChD,OAAO,iBAAiB,KAAK,MAAM,IAAI,CAAC;GAC1C,QAAQ;IACN,OAAO,EAAE,GAAG,iBAAiB;GAC/B;EACF;;EAGA,SAAgB,aAAa,UAAwB,UAAoC,WAAW,cAAuB;GACzH,IAAI;IACF,QAAQ,QAAQ,aAAa,KAAK,UAAU,QAAQ,CAAC;IACrD,OAAO;GACT,QAAQ;IACN,OAAO;GACT;EACF;;;;;;EAgBA,SAAgB,aAAa,UAAiF;GAC5G,IAAI,CAAC,SAAS,SAAS,OAAO,CAAC;GAC/B,MAAM,OAAgE,CAAC;GACvE,IAAI,SAAS,WAAW,WAAW,SAAS,iBAAiB,MAC3D,KAAK,wBAAwB,QAAQ,SAAS,aAAa;QACtD,IAAI,SAAS,WAAW,SAAS,SAAS,QAAQ,MACvD,KAAK,wBAAwB,QAAQ,aAAa,SAAS,GAAG,EAAE;QAC3D,IAAI,SAAS,WAAW,YAAY,SAAS,WAAW,MAE7D,IAAI,SAAS,OAAO,SAAS,UAAU,GAAG,KAAK,wBAAwB,SAAS;QAC3E,KAAK,wBAAwB,SAAS;GAE7C,KAAK,qBAAqB,iBAAiB,SAAS,IAAI,QAAQ,CAAC,EAAE;GACnE,KAAK,oBAAoB,GAAG,KAAK,MAAM,SAAS,IAAI,EAAE;GACtD,KAAK,uBAAuB,SAAS,QAAQ,QAAQ,CAAC;GACtD,OAAO;EACT;;EAGA,SAAgB,aAAa,KAAqB;GAChD,OAAO,IAAI,QAAQ,eAAc,SAAQ,KAAK,MAAM;EACtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC/HA,SAAS,SAAS,UAAgC;GAChD,IAAI,CAAC,SAAS,SAAS,OAAO;GAC9B,IAAI,SAAS,WAAW,SAAS,OAAO;GACxC,IAAI,SAAS,WAAW,OAAO,OAAO;GACtC,IAAI,SAAS,WAAW,UAAU,OAAO;GACzC,OAAO;EACT;;EAGA,SAAgB,UAAU,EAAE,SAAS,QAAQ,WAAwC;GACnF,MAAM,CAAC,UAAU,gBAAA,GAAA,MAAA,SAAA,CAAsC,OAAO;GAC9D,MAAM,CAAC,UAAU,gBAAA,GAAA,MAAA,SAAA,CAAwB,EAAE;GAC3C,MAAM,CAAC,MAAM,YAAA,GAAA,MAAA,SAAA,CAAoB,KAAK;GACtC,MAAM,CAAC,OAAO,aAAA,GAAA,MAAA,SAAA,CAAoC,IAAI;;;GAGtD,MAAM,CAAC,QAAQ,cAAA,GAAA,MAAA,SAAA,CAAqC,IAAI;GACxD,MAAM,WAAA,GAAA,MAAA,OAAA,CAA0C,IAAI;GAEpD,MAAM,gBAAgB,SAA6B;IACjD,YAAY,IAAI;IAChB,SAAS,IAAI;IACb,OAAO,IAAI;GACb;GAEA,MAAM,aAAa,OAAO,UAAwD;IAChF,MAAM,OAAO,MAAM,OAAO,QAAQ;IAClC,MAAM,OAAO,QAAQ;IACrB,IAAI,SAAS,KAAA,GAAW;IACxB,QAAQ,IAAI;IACZ,SAAS,IAAI;IACb,IAAI;KACF,MAAM,UAAU,MAAM,cAAc,IAAI;KACxC,UAAU,OAAO;IACnB,SAAS,KAAK;KACZ,UAAU,IAAI;KACd,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;IAC3D,UAAU;KACR,QAAQ,KAAK;IACf;GACF;;GAGA,MAAM,qBAA2B;IAC/B,IAAI,WAAW,MAAM;IACrB,aAAa;KACX,GAAG;KACH,SAAS;KACT,QAAQ;KACR,cAAc;KACd,KAAK;KACL,QAAQ;IACV,CAAC;IACD,UAAU,IAAI;GAChB;GAEA,MAAM,mBAAyB;IAC7B,MAAM,MAAM,SAAS,KAAK;IAC1B,IAAI,QAAQ,IAAI;KACd,SAAS,mCAAmC;KAC5C;IACF;IACA,aAAa;KACX,GAAG;KACH,SAAS;KACT,QAAQ;KACR;KACA,cAAc;KACd,QAAQ;IACV,CAAC;IACD,YAAY,EAAE;IACd,UAAU,IAAI;GAChB;GAEA,MAAM,YAAY,UAAwB;IACxC,aAAa;KACX,GAAG;KACH,SAAS;KACT,QAAQ;KACR,QAAQ;KACR,cAAc;KACd,KAAK;IACP,CAAC;GACH;GAEA,MAAM,gBAAsB;IAC1B,UAAU,IAAI;IACd,aAAa;KACX,GAAG;KACH,SAAS;KACT,QAAQ;KACR,cAAc;KACd,KAAK;KACL,QAAQ;KACR,KAAK;KACL,MAAM;KACN,SAAS;IACX,CAAC;GACH;GAEA,MAAM,eAAe,SAAS,UAC1B,EAAE,YAAY,+FAA+F,IAC7G,KAAA;GAEJ,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAWA,wBAAI;IAAO,MAAK;IAAS,cAAW;cAApD;KACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAM,KAAQ,CAAA,GACd,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OAAQ,MAAK;OAAS,WAAWA,wBAAI;OAAO,cAAW;OAAK,SAAS;iBAAS;MAAS,CAAA,CACpF;;KAEL,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;MAAO,WAAWA,wBAAI;gBAAtB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,MAAK;OACL,SAAS,SAAS;OAClB,WAAU,MAAK,aAAa;QAAE,GAAG;QAAU,SAAS,EAAE,OAAO;OAAQ,CAAC;MACvE,CAAA,GACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAM,OAAU,CAAA,CACX;;KAEP,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;MAAS,OAAO;gBACjC,CAAC,SAAS,WAAW,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAM,MAAS,CAAA;KAClC,CAAA;KACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAApB,CAAkC,OAAI,SAAS,QAAQ,CAAO;;KAE9D,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAAS;KAAS,CAAA;KACtC,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OACE,MAAK;OACL,WAAWA,wBAAI;OACf,UAAU;OACV,eAAe,QAAQ,SAAS,MAAM;iBAErC,OAAO,SAAS;MACX,CAAA,GACR,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,KAAK;OACL,WAAWA,wBAAI;OACf,MAAK;OACL,QAAO;OACP,WAAW,MAAM,KAAK,WAAW,CAAC;MACnC,CAAA,CACE;;KACJ,WAAW,QACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAApB;OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;QACE,WAAWA,wBAAI;QACf,OAAO;SAAE,iBAAiB,QAAQ,OAAO;SAAK,gBAAgB;SAAS,oBAAoB;QAAS;OACrG,CAAA;OACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QAAM,WAAWA,wBAAI;kBAAa;OAAW,CAAA;OAC7C,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;QAAQ,MAAK;QAAS,WAAWA,wBAAI;QAAe,SAAS;kBAAc;OAAU,CAAA;MAClF;;KAGP,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAAS;KAAW,CAAA;KACxC,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,WAAWA,wBAAI;OACf,OAAO;OACP,aAAY;OACZ,WAAU,MAAK,YAAY,EAAE,OAAO,KAAK;OACzC,YAAW,MAAK;QAAE,IAAI,EAAE,QAAQ,SAAS,WAAW;OAAE;MACvD,CAAA,GACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OAAQ,MAAK;OAAS,WAAWA,wBAAI;OAAQ,SAAS;iBAAY;MAAU,CAAA,CACzE;;KAEL,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAAS;KAAO,CAAA;KACpC,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBACjB,QAAQ,KAAI,WACX,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OAEE,MAAK;OACL,WAAW,SAAS,WAAW,YAAY,SAAS,WAAW,OAAO,QAClE,GAAGA,wBAAI,OAAO,GAAGA,wBAAI,iBACrBA,wBAAI;OACR,OAAO,OAAO;OACd,OAAO,EAAE,YAAY,OAAO,MAAM;OAClC,eAAe,SAAS,OAAO,KAAK;MACrC,GARM,OAAO,EAQb,CACF;KACE,CAAA;KAEL,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAAS;KAAO,CAAA;KACpC,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;OAAM,WAAWA,wBAAI;iBAArB;QAAkC;QAAI,KAAK,MAAM,SAAS,MAAM,GAAG;QAAE;OAAO;UAC5E,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,WAAWA,wBAAI;OACf,MAAK;OACL,KAAK;OACL,KAAK;OACL,MAAM;OACN,OAAO,KAAK,MAAM,SAAS,MAAM,GAAG;OACpC,WAAU,MAAK,aAAa;QAAE,GAAG;QAAU,KAAK,OAAO,EAAE,OAAO,KAAK,IAAI;OAAI,CAAC;MAC/E,CAAA,CACE;;KACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;OAAM,WAAWA,wBAAI;iBAArB;QAAkC;QAAI,KAAK,MAAM,SAAS,IAAI;QAAE;OAAQ;UACxE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,WAAWA,wBAAI;OACf,MAAK;OACL,KAAK;OACL,KAAK;OACL,MAAM;OACN,OAAO,KAAK,MAAM,SAAS,IAAI;OAC/B,WAAU,MAAK,aAAa;QAAE,GAAG;QAAU,MAAM,OAAO,EAAE,OAAO,KAAK;OAAE,CAAC;MAC1E,CAAA,CACE;;KACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;OAAM,WAAWA,wBAAI;iBAArB;QAAkC;QAAI,KAAK,MAAM,SAAS,UAAU,GAAG;QAAE;OAAO;UAChF,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,WAAWA,wBAAI;OACf,MAAK;OACL,KAAK,cAAc;OACnB,KAAA;OACA,MAAM;OACN,OAAO,KAAK,MAAM,SAAS,UAAU,GAAG;OACxC,OAAM;OACN,WAAU,MAAK,aAAa;QAAE,GAAG;QAAU,SAAS,OAAO,EAAE,OAAO,KAAK,IAAI;OAAI,CAAC;MACnF,CAAA,CACE;;KAEJ,UAAU,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAAY;KAAW,CAAA;KAE9D,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAWA,wBAAI;gBAClB,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OAAQ,MAAK;OAAS,WAAWA,wBAAI;OAAQ,SAAS;iBAAS;MAAY,CAAA;KACxE,CAAA;IACF;;EAET;;;;;;;;;;;;;;;;;;;;;;;;;;ECpOA,MAAa,kBAAkB,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECZzC,MAAa,kBAAkB;;EAG/B,SAAgB,kBAAwB;GACtC,IAAI,SAAS,cAAc,6CAA6C,MAAM,MAAM;GACpF,MAAM,MAAM,SAAS,cAAc,OAAO;GAC1C,IAAI,QAAQ,YAAY;GACxB,IAAI,cAAc;GAClB,SAAS,KAAK,YAAY,GAAG;EAC/B;;EAGA,SAAgB,cAA8B;GAC5C,IAAI,QAAQ,SAAS,cAA8B,OAAO,WAAW,EAAE;GACvE,IAAI,UAAU,MAAM;IAClB,QAAQ,SAAS,cAAc,KAAK;IACpC,MAAM,aAAa,YAAY,EAAE;IACjC,SAAS,KAAK,YAAY,KAAK;GACjC;GACA,OAAO;EACT;;;;;;EAOA,IAAa,cAAb,MAAyB;GACvB,MAAM,UAA8B;IAClC,MAAM,OAAO,SAAS;IACtB,IAAI,SAAS,SAAS,KAAK,aAAa,aAAa,EAAE;SAClD,KAAK,gBAAgB,WAAW;IACrC,MAAM,OAAO,aAAa,QAAQ;IAClC,KAAK,MAAM,QAAQ,eAAe;KAChC,MAAM,QAAQ,KAAK;KACnB,IAAI,UAAU,KAAA,GAAW,KAAK,MAAM,eAAe,IAAI;UAClD,KAAK,MAAM,YAAY,MAAM,KAAK;IACzC;GACF;EACF;;;;;;;;;;;;ECnCA,MAAM,aAAa;EACnB,MAAM,cAAc;EAEpB,MAAM,OAAO;;EAGb,SAAS,cAAuC;GAC9C,MAAM,SAAS,SAAS,cAA2B,kDAA8C;GACjG,IAAI,WAAW,MAAM,OAAO,KAAA;GAE5B,OADkB,OAAO,cAA2B,sBAAoB,CAAC,EAAE,iBACtD,OAAO;EAC9B;;EAGA,SAAS,iBAAiB,MAAkD;GAC1E,MAAM,SAAS,KAAK,cAAiC,+BAA6B;GAClF,IAAI,WAAW,MAAM,OAAO;GAC5B,KAAK,MAAM,SAAS,KAAK,UACvB,IAAI,MAAM,YAAY,UAAU,OAAO;EAG3C;;EAGA,SAAS,sBAAsB,OAAsC;GACnE,IAAI;GACJ,IAAI,SAAS;GACb,IAAI;GAEJ,MAAM,iBAAuB;IAC3B,IAAI,SAAS,KAAA,KAAa,CAAC,KAAK,aAAa;KAC3C,cAAc,WAAW;KACzB,OAAO,KAAA;KACP,SAAS;IACX;IACA,IAAI,QAAQ;KACV,IAAI,SAAS,KAAK,SAAS,KAAK,GAAG;KACnC,cAAc,WAAW;KACzB,OAAO,KAAA;KACP,SAAS;IACX;IACA,SAAS,YAAY;IACrB,IAAI,SAAS,KAAA,GAAW;IACxB,MAAM,SAAS,iBAAiB,IAAI;IACpC,IAAI,WAAW,KAAA,GAAW;IAC1B,IAAI,MAAM,kBAAkB,MAAM,KAAK,aAAa,OAAO,OAAO,kBAAkB;IACpF,SAAS;IACT,IAAI,iBAAiB,KAAA,GAAW;KAC9B,eAAe,IAAI,uBAAuB;MACxC,IAAI,SAAS,KAAA,KAAa,CAAC,KAAK,aAAa;OAC3C,SAAS;OACT,SAAS;OACT;MACF;MACA,IAAI,CAAC,KAAK,SAAS,KAAK,GAAG,SAAS;KACtC,CAAC;KACD,aAAa,QAAQ,MAAM;MAAE,WAAW;MAAM,SAAS;KAAK,CAAC;IAC/D;GACF;GAEA,MAAM,eAAe,IAAI,uBAAuB;IAAE,SAAS;GAAE,CAAC;GAC9D,aAAa,QAAQ,SAAS,MAAM;IAAE,WAAW;IAAM,SAAS;GAAK,CAAC;GACtE,SAAS;GAET,aAAa;IACX,aAAa,WAAW;IACxB,cAAc,WAAW;IACzB,MAAM,OAAO;GACf;EACF;;;;;EAMA,SAAgB,YAAwB;GACtC,MAAM,YAA+B,CAAC;GACtC,gBAAgB;GAChB,YAAY;GAEZ,MAAM,UAAU,IAAI,YAAY;GAChC,IAAI,SAAS,aAAa;GAC1B,QAAQ,MAAM,MAAM;GAIpB,IAAI;GACJ,MAAM,kBAAwB;IAC5B,IAAI,cAAc,KAAA,GAAW;IAC7B,OAAO,aAAa,SAAS;IAC7B,YAAY,KAAA;IACZ,aAAa,MAAM;GACrB;GACA,MAAM,qBAA2B;IAC/B,IAAI,cAAc,KAAA,GAAW,OAAO,aAAa,SAAS;IAC1D,YAAY,OAAO,iBAAiB;KAAE,aAAa,MAAM;IAAE,GAAG,GAAG;GACnE;GAGA,MAAM,QAAQ,SAAS,cAAc,QAAQ;GAC7C,MAAM,OAAO;GACb,MAAM,aAAa,YAAY,EAAE;GACjC,MAAM,YAAYC,wBAAI;GACtB,MAAM,aAAa,cAAc,IAAI;GACrC,MAAM,YAAY,gBAAgBA,wBAAI,UAAU,IAAI,KAAK,sBAAsBA,wBAAI,WAAW;GAC9F,MAAM,kBAAkB,WAA0B;IAChD,IAAI,QAAQ,MAAM,QAAQ,SAAS;SAC9B,OAAO,MAAM,QAAQ;GAC5B;GACA,eAAe,OAAO,OAAO;GAC7B,UAAU,KAAK,sBAAsB,KAAK,CAAC;GAG3C,MAAM,YAAY,SAAS,cAAc,KAAK;GAC9C,UAAU,aAAa,YAAY,EAAE;GACrC,UAAU,MAAM,UAAU,0CAA0C,YAAY;GAChF,IAAI;GAEJ,MAAM,wBAA8B;IAClC,MAAM,OAAO,MAAM,sBAAsB;IACzC,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,IAAI,OAAO,aAAa,cAAc,CAAC,CAAC;IACvF,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK,GAAG;IAChC,UAAU,MAAM,OAAO,GAAG,KAAK;IAC/B,UAAU,MAAM,MAAM,GAAG,IAAI;GAC/B;GAEA,MAAM,mBAAyB;IAC7B,IAAI,SAAS,KAAA,GAAW;IACxB,UAAU;IACV,KAAK,QAAQ;IACb,OAAO,KAAA;IACP,UAAU,OAAO;GACnB;GAEA,MAAM,kBAAwB;IAC5B,IAAI,SAAS,KAAA,GAAW;IACxB,gBAAgB;IAChB,SAAS,KAAK,YAAY,SAAS;IACnC,QAAA,GAAA,iBAAA,WAAA,CAAkB,SAAS;IAC3B,KAAK,OAAO,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;KACV,SAAS;KACT,SAAS;KACT,SAAS,SAAS;MAChB,SAAS;MACT,QAAQ,MAAM,IAAI;MAClB,eAAe,KAAK,OAAO;MAC3B,aAAa;KACf;IACD,CAAA,CAAC;GACJ;GAEA,MAAM,oBAA0B;IAAE,IAAI,SAAS,KAAA,GAAW,WAAW;SAAQ,UAAU;GAAE;GACzF,MAAM,iBAAiB,SAAS,WAAW;GAG3C,MAAM,kBAAkB,UAA4B;IAClD,IAAI,SAAS,KAAA,GAAW;IACxB,MAAM,SAAS,MAAM;IACrB,IAAI,WAAW,SAAS,UAAU,SAAS,MAAM,KAAK,MAAM,SAAS,MAAM,IAAI;IAC/E,WAAW;GACb;GACA,MAAM,aAAa,UAA+B;IAChD,IAAI,MAAM,QAAQ,YAAY,SAAS,KAAA,GAAW,WAAW;GAC/D;GACA,MAAM,iBAAuB;IAAE,IAAI,SAAS,KAAA,GAAW,WAAW;GAAE;GACpE,MAAM,uBAA6B,UAAU;GAC7C,SAAS,iBAAiB,aAAa,cAAc;GACrD,OAAO,iBAAiB,WAAW,SAAS;GAC5C,OAAO,iBAAiB,UAAU,QAAQ;GAC1C,OAAO,iBAAiB,gBAAgB,cAAc;GACtD,UAAU,WAAW;IACnB,SAAS,oBAAoB,aAAa,cAAc;IACxD,OAAO,oBAAoB,WAAW,SAAS;IAC/C,OAAO,oBAAoB,UAAU,QAAQ;IAC7C,OAAO,oBAAoB,gBAAgB,cAAc;GAC3D,CAAC;GAED,aAAa;IACX,WAAW;IACX,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;IACnD,SAAS,cAA2B,yBAAyB,CAAC,EAAE,OAAO;IACvE,SAAS,cAA2B,6CAA6C,CAAC,EAAE,OAAO;IAC3F,QAAQ,MAAM;KAAE,GAAG;KAAQ,SAAS;IAAM,CAAC;GAC7C;EACF;;;;;;;;;EChMA,MAAa,SAAmB,CAAC;;EAGjC,SAAgB,QAAc;GAC5B,IAAI;IACF,UAAU;GACZ,SAAS,OAAO;IAEd,QAAQ,MAAM,+BAA+B,KAAK;GACpD;EACF"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["css","css"],"sources":["../src/client/image-pipeline.ts","../src/client/skin-store.ts","../src/client/SkinPanel.tsx","../src/client/skin-host.ts","../src/client/global-skin-css.ts","../src/client/skin-dom.ts","../src/client/mount.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * dsh-wx-skin — local-image pipeline: read a picked file, downscale it to a\n * bounded canvas, and re-encode it to a data URL. The browser implementation\n * is injected through `PipelineEnv` so tests drive it with a fake source; the\n * pure `fileToDataUrl` logic is environment-agnostic.\n *\n * There is intentionally no input-size or output-size limit: any image the\n * canvas can decode is accepted (no \"too large\" rejection). The only bound is\n * the downscale to `maxEdge`, which keeps the resulting data URL small enough\n * to persist while preserving a sharp full-screen background.\n * @module dsh-wx-skin/client/image-pipeline\n */\n\n/** A decodable raster image held by the pipeline. */\nexport interface ImageSource {\n width: number\n height: number\n /** Draw the source scaled to w×h into a fresh canvas and encode it. */\n render(w: number, h: number, type: string, quality: number): string\n /** Release any held resources (object URLs, …). */\n release(): void\n}\n\n/** The browser environment the pipeline runs against. */\nexport interface PipelineEnv {\n /** Decode a file into an ImageSource. Throws when the file is not readable. */\n readImage(file: Blob): Promise<ImageSource>\n}\n\n/** Error the pipeline throws with a stable machine-readable code. */\nexport class ImagePipelineError extends Error {\n constructor(\n public readonly code: 'unsupported' | 'decode' | 'encode',\n message: string,\n ) {\n super(message)\n this.name = 'ImagePipelineError'\n }\n}\n\n/** Tuning knobs for the encode. */\nexport interface DownscaleOptions {\n /**\n * Optional explicit cap on the longest edge of the first encode (px).\n * Omitted by default — the image keeps its original resolution up to\n * {@link DISPLAY_MAX_EDGE}; larger images are downscaled for display (a\n * background never needs more than ~4K, and oversized canvases silently\n * produce empty data URLs, which is exactly the \"selected but not shown\"\n * failure). Picking a file is never limited by size.\n */\n maxEdge?: number\n /** JPEG quality (0..1). Default 0.82. */\n quality?: number\n /**\n * Data-URL length budget (chars). When the encoded result exceeds this,\n * the pipeline silently re-encodes at smaller sizes so the skin stays\n * persistable; it never throws. Default 4_000_000 (safe under the ~5 MB\n * localStorage quota).\n */\n storageBudget?: number\n}\n\n/**\n * Display cap for the encoded background: images larger than this (per side)\n * are downscaled so the canvas stays inside every browser's limits and the\n * data URL stays small enough to persist. 4096 px is sharper than any screen\n * needs for a background.\n */\nexport const DISPLAY_MAX_EDGE = 4096\n\n/** Raster formats the pipeline accepts (canvas can re-encode these safely). */\nexport const ALLOWED_TYPES = [\n 'image/png', 'image/jpeg', 'image/jpg', 'image/pjpeg', 'image/x-png',\n 'image/webp', 'image/gif', 'image/bmp', 'image/x-ms-bmp',\n] as const\n\n/** Default browser environment: object URL + <img> decode + canvas encode. */\nexport const browserEnv: PipelineEnv = {\n async readImage(file: Blob): Promise<ImageSource> {\n const url = URL.createObjectURL(file)\n try {\n const image = await loadImage(url)\n return {\n width: image.naturalWidth,\n height: image.naturalHeight,\n render(w, h, type, quality) {\n const canvas = document.createElement('canvas')\n canvas.width = w\n canvas.height = h\n const ctx = canvas.getContext('2d')\n if (ctx === null) throw new Error('canvas 2d context unavailable')\n ctx.drawImage(image, 0, 0, w, h)\n return canvas.toDataURL(type, quality)\n },\n release() {\n URL.revokeObjectURL(url)\n },\n }\n } catch (error) {\n URL.revokeObjectURL(url)\n throw error\n }\n },\n}\n\n/** Load an <img> from a src, resolving on decode. */\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const image = new Image()\n image.onload = () => resolve(image)\n image.onerror = () => reject(new Error('image load failed'))\n image.src = src\n })\n}\n\n/** Convert a Blob file into a JPEG data URL. Picking is never limited by file\n * size; the encode keeps the original resolution up to the display cap and\n * silently re-encodes smaller when the canvas would be oversized or the data\n * URL would not persist — it never throws a size error. */\nexport async function fileToDataUrl(\n file: Blob,\n env: PipelineEnv = browserEnv,\n options: DownscaleOptions = {},\n): Promise<string> {\n const quality = options.quality ?? 0.82\n const storageBudget = options.storageBudget ?? 4_000_000\n\n if (typeof (file as File).type === 'string' && (file as File).type !== '') {\n const type = (file as File).type.toLowerCase()\n if (type === 'image/svg+xml') {\n throw new ImagePipelineError('unsupported', '不支持 SVG 矢量图,请选择 PNG / JPEG / WebP / GIF / BMP 位图。')\n }\n if (!type.startsWith('image/')) {\n throw new ImagePipelineError('unsupported', `不支持的文件类型“${type}”,请选择 PNG / JPEG / WebP / GIF / BMP 图片。`)\n }\n // image/* beyond the raster whitelist (e.g. avif) proceeds to decode —\n // a codec the canvas cannot read surfaces as a clear \"decode\" error.\n }\n\n let source: ImageSource\n try {\n source = await env.readImage(file)\n } catch (error) {\n throw new ImagePipelineError('decode', '无法读取图片:文件已损坏或不是有效的图片。')\n }\n\n try {\n if (!Number.isFinite(source.width) || !Number.isFinite(source.height) || source.width <= 0 || source.height <= 0) {\n throw new ImagePipelineError('decode', '无法解析图片尺寸,文件可能已损坏。')\n }\n // First attempt: original resolution when it fits the display cap,\n // otherwise downscaled so the canvas stays inside browser limits.\n const cap = options.maxEdge ?? Math.min(Math.max(source.width, source.height), DISPLAY_MAX_EDGE)\n const first = sizeFor(source, cap)\n let data = encode(source, first.w, first.h, quality)\n // Fallback: never reject — when the encode came back invalid (oversized\n // canvases can silently yield empty data URLs) or too big to persist,\n // re-encode smaller. The last attempt always returns.\n if (!isValidDataUrl(data) || data.length > storageBudget) {\n for (const edge of [2560, 1920, 1280]) {\n if (Math.max(first.w, first.h) <= edge) continue\n const next = sizeFor(source, edge)\n data = encode(source, next.w, next.h, Math.max(0.6, quality - 0.1))\n if (isValidDataUrl(data) && data.length <= storageBudget) break\n }\n }\n return data\n } finally {\n try {\n source.release()\n } catch {\n // A release failure must not mask the primary outcome.\n }\n }\n}\n\n/** True when the encoder produced a real image payload (not an empty `data:,`). */\nfunction isValidDataUrl(data: string): boolean {\n return data.startsWith('data:image/') && data.length > 64\n}\n\n/** Bounded target size: original dimensions, or downscaled to fit `edge`. */\nfunction sizeFor(source: ImageSource, edge: number | undefined): { w: number; h: number } {\n if (edge === undefined) return { w: Math.max(1, Math.round(source.width)), h: Math.max(1, Math.round(source.height)) }\n const scale = Math.min(1, edge / Math.max(source.width, source.height))\n return {\n w: Math.max(1, Math.round(source.width * scale)),\n h: Math.max(1, Math.round(source.height * scale)),\n }\n}\n\n/** Draw + encode one attempt; encode failures are wrapped with a stable code. */\nfunction encode(source: ImageSource, w: number, h: number, quality: number): string {\n try {\n return source.render(w, h, 'image/jpeg', quality)\n } catch (error) {\n throw new ImagePipelineError('encode', '图片编码失败,请换一张图片。')\n }\n}\n","/**\n * dsh-wx-skin — pure skin settings state: defaults, sanitization, persistence\n * (localStorage), preset catalog, and the CSS-variable projection consumed by\n * the DOM applier. No DOM access here — tests exercise these functions with a\n * fake storage.\n * @module dsh-wx-skin/client/skin-store\n */\nimport type { SkinSettings, SkinSource } from '../core/types.ts'\n\n/** localStorage key holding the durable settings JSON. */\nexport const STORAGE_KEY = 'dsh-wx-skin.settings'\n\n/** Document attribute that gates the global skin CSS (set on documentElement). */\nexport const ACTIVE_ATTR = 'data-wx-skin-active'\n\n/** Attribute on the injected background layer div. */\nexport const LAYER_ATTR = 'data-wx-skin-layer'\n\n/** Attribute on the injected sidebar entry button. */\nexport const ENTRY_ATTR = 'data-wx-skin-entry'\n\n/** CSS custom properties the applier writes on documentElement. */\nexport const SKIN_CSS_VARS = [\n '--wx-skin-bg-image',\n '--wx-skin-bg-color',\n '--wx-skin-scrim',\n '--wx-skin-blur',\n '--wx-skin-surface',\n] as const\n\n/** Clamp a number into [min, max]. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nconst SOURCES: readonly SkinSource[] = ['image', 'url', 'preset', 'none']\n\nconst MAX_DIM = 0.8\nconst MAX_BLUR = 24\n/** Surface opacity bounds (0..1 — fully adjustable; readability is handled by the dim slider). */\nexport const MIN_SURFACE = 0\nexport const MAX_SURFACE = 1\nexport const DEFAULT_SURFACE = 0.72\n\n/** Fresh defaults for a clean install. */\nexport const DEFAULT_SETTINGS: SkinSettings = Object.freeze({\n enabled: false,\n source: 'none',\n imageDataUrl: null,\n url: null,\n preset: null,\n dim: 0.35,\n blur: 0,\n surface: DEFAULT_SURFACE,\n})\n\n/** A selectable preset skin: solid color or gradient CSS value. */\nexport interface SkinPreset {\n id: string\n label: string\n /** CSS background value (solid color or linear-gradient). */\n value: string\n}\n\n/** Built-in presets — immediate effect without picking an image. */\nexport const PRESETS: readonly SkinPreset[] = Object.freeze([\n { id: 'color-ink', label: '墨蓝', value: '#1f2a44' },\n { id: 'color-slate', label: '石板', value: '#3b4252' },\n { id: 'color-sand', label: '暖沙', value: '#e9e2d0' },\n { id: 'grad-sunset', label: '落日渐变', value: 'linear-gradient(135deg, #ff9a8b, #ff6a88 45%, #ff99ac)' },\n { id: 'grad-ocean', label: '深海渐变', value: 'linear-gradient(135deg, #0f2027, #203a43 55%, #2c5364)' },\n { id: 'grad-aurora', label: '极光渐变', value: 'linear-gradient(135deg, #43cea2, #185a9d 60%, #9cecfb)' },\n])\n\n/** Coerce an unknown persisted value into a valid SkinSettings. */\nexport function sanitizeSettings(raw: unknown): SkinSettings {\n const o = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>\n const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null)\n const num = (v: unknown, fallback: number): number => {\n const n = typeof v === 'number' && Number.isFinite(v) ? v : fallback\n return n\n }\n const source: SkinSource = SOURCES.includes(o.source as SkinSource) ? (o.source as SkinSource) : 'none'\n return {\n enabled: o.enabled === true,\n source,\n imageDataUrl: str(o.imageDataUrl),\n url: str(o.url),\n preset: str(o.preset),\n dim: clamp(num(o.dim, DEFAULT_SETTINGS.dim), 0, MAX_DIM),\n blur: clamp(num(o.blur, DEFAULT_SETTINGS.blur), 0, MAX_BLUR),\n surface: clamp(num(o.surface, DEFAULT_SETTINGS.surface), MIN_SURFACE, MAX_SURFACE),\n }\n}\n\n/** Read + sanitize the persisted settings, or the defaults. */\nexport function loadSettings(storage: Pick<Storage, 'getItem'> = globalThis.localStorage): SkinSettings {\n try {\n const text = storage.getItem(STORAGE_KEY)\n if (text === null) return { ...DEFAULT_SETTINGS }\n return sanitizeSettings(JSON.parse(text))\n } catch {\n return { ...DEFAULT_SETTINGS }\n }\n}\n\n/** Persist settings; quota errors are swallowed (the caller surfaces a message). */\nexport function saveSettings(settings: SkinSettings, storage: Pick<Storage, 'setItem'> = globalThis.localStorage): boolean {\n try {\n storage.setItem(STORAGE_KEY, JSON.stringify(settings))\n return true\n } catch {\n return false\n }\n}\n\n/** Remove the persisted settings (restores defaults on next boot). */\nexport function clearSettings(storage: Pick<Storage, 'removeItem'> = globalThis.localStorage): void {\n try {\n storage.removeItem(STORAGE_KEY)\n } catch {\n // Ignore.\n }\n}\n\n/** Whether the settings equal the pristine defaults (used to skip seeding the\n * host copy from localStorage on the first run after upgrade). */\nexport function isDefaultSettings(settings: SkinSettings): boolean {\n return settings.enabled === DEFAULT_SETTINGS.enabled\n && settings.source === DEFAULT_SETTINGS.source\n && settings.imageDataUrl === DEFAULT_SETTINGS.imageDataUrl\n && settings.url === DEFAULT_SETTINGS.url\n && settings.preset === DEFAULT_SETTINGS.preset\n && settings.dim === DEFAULT_SETTINGS.dim\n && settings.blur === DEFAULT_SETTINGS.blur\n && settings.surface === DEFAULT_SETTINGS.surface\n}\n\n/**\n * Project the settings onto the CSS custom properties the skin layer reads.\n * Returns an empty map when the skin is disabled (the applier then retracts\n * every variable).\n */\nexport function cssVariables(settings: SkinSettings): Partial<Record<(typeof SKIN_CSS_VARS)[number], string>> {\n if (!settings.enabled) return {}\n const vars: Partial<Record<(typeof SKIN_CSS_VARS)[number], string>> = {}\n if (settings.source === 'image' && settings.imageDataUrl !== null) {\n vars['--wx-skin-bg-image'] = `url(\"${settings.imageDataUrl}\")`\n } else if (settings.source === 'url' && settings.url !== null) {\n vars['--wx-skin-bg-image'] = `url(\"${escapeCssUrl(settings.url)}\")`\n } else if (settings.source === 'preset' && settings.preset !== null) {\n // A gradient goes to background-image; a solid color to background-color.\n if (settings.preset.includes('gradient')) vars['--wx-skin-bg-image'] = settings.preset\n else vars['--wx-skin-bg-color'] = settings.preset\n }\n vars['--wx-skin-scrim'] = `rgba(0, 0, 0, ${settings.dim.toFixed(3)})`\n vars['--wx-skin-blur'] = `${Math.round(settings.blur)}px`\n vars['--wx-skin-surface'] = settings.surface.toFixed(3)\n return vars\n}\n\n/** Escape quotes so a user-supplied URL cannot break out of `url(\"...\")`. */\nexport function escapeCssUrl(url: string): string {\n return url.replace(/[\"\\\\\\n\\r]/g, char => `\\\\${char}`)\n}\n","/**\n * dsh-wx-skin — the popover panel: enable toggle, background preview, local\n * image picker (file input → downscale → data URL), URL input, presets, dim /\n * blur sliders, and reset. The component is controlled: every change calls\n * `commit` (mount persists + applies to the document), keeping the panel and\n * the live skin in lockstep.\n * @module dsh-wx-skin/client/skin-panel\n */\nimport { useRef, useState, type ChangeEvent } from 'react'\nimport type { SkinSettings } from '../core/types.ts'\nimport { ImagePipelineError, fileToDataUrl } from './image-pipeline.ts'\nimport { DEFAULT_SURFACE, MIN_SURFACE, MAX_SURFACE, PRESETS } from './skin-store.ts'\nimport css from './skin.module.css'\n\nexport interface SkinPanelProps {\n /** Settings as loaded at open time. */\n initial: SkinSettings\n /** Persist + apply a settings change (mount wires storage + DOM). */\n commit: (next: SkinSettings) => void\n /** Close the popover. */\n onClose: () => void\n}\n\n/** Describe the currently active source for the status badge. */\nfunction describe(settings: SkinSettings): string {\n if (!settings.enabled) return '未启用'\n if (settings.source === 'image') return '图片(本地)'\n if (settings.source === 'url') return '图片(URL)'\n if (settings.source === 'preset') return '预设'\n return '未启用'\n}\n\n/** The popover skin settings panel. */\nexport function SkinPanel({ initial, commit, onClose }: SkinPanelProps): JSX.Element {\n const [settings, setSettings] = useState<SkinSettings>(initial)\n const [urlInput, setUrlInput] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n /** A just-picked local image, processed but not yet applied — the user\n * clicks 「应用」 to commit it (explicit two-step flow). */\n const [staged, setStaged] = useState<string | null>(null)\n const fileRef = useRef<HTMLInputElement | null>(null)\n\n const setAndCommit = (next: SkinSettings): void => {\n setSettings(next)\n setError(null)\n commit(next)\n }\n\n const onPickFile = async (event: ChangeEvent<HTMLInputElement>): Promise<void> => {\n const file = event.target.files?.[0]\n event.target.value = ''\n if (file === undefined) return\n setBusy(true)\n setError(null)\n try {\n const dataUrl = await fileToDataUrl(file)\n setStaged(dataUrl)\n } catch (err) {\n setStaged(null)\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n setBusy(false)\n }\n }\n\n /** Commit the staged local image to the skin. */\n const onApplyImage = (): void => {\n if (staged === null) return\n setAndCommit({\n ...settings,\n enabled: true,\n source: 'image',\n imageDataUrl: staged,\n url: null,\n preset: null,\n })\n setStaged(null)\n }\n\n const onApplyUrl = (): void => {\n const url = urlInput.trim()\n if (url === '') {\n setError('请输入图片 URL(http:// 或 https:// 开头)。')\n return\n }\n setAndCommit({\n ...settings,\n enabled: true,\n source: 'url',\n url,\n imageDataUrl: null,\n preset: null,\n })\n setUrlInput('')\n setStaged(null)\n }\n\n const onPreset = (value: string): void => {\n setAndCommit({\n ...settings,\n enabled: true,\n source: 'preset',\n preset: value,\n imageDataUrl: null,\n url: null,\n })\n }\n\n const onReset = (): void => {\n setStaged(null)\n setAndCommit({\n ...settings,\n enabled: false,\n source: 'none',\n imageDataUrl: null,\n url: null,\n preset: null,\n dim: 0.35,\n blur: 0,\n surface: DEFAULT_SURFACE,\n })\n }\n\n const previewStyle = settings.enabled\n ? { background: 'var(--wx-skin-bg-image, none) center / cover no-repeat, var(--wx-skin-bg-color, transparent)' }\n : undefined\n\n return (\n <div className={css.panel} role=\"dialog\" aria-label=\"皮肤设置\">\n <div className={css.header}>\n <span>皮肤</span>\n <button type=\"button\" className={css.close} aria-label=\"关闭\" onClick={onClose}>×</button>\n </div>\n\n <label className={css.toggleRow}>\n <input\n type=\"checkbox\"\n checked={settings.enabled}\n onChange={e => setAndCommit({ ...settings, enabled: e.target.checked })}\n />\n <span>启用皮肤</span>\n </label>\n\n <div className={css.preview} style={previewStyle}>\n {!settings.enabled && <span>未启用</span>}\n </div>\n <div className={css.currentBadge}>当前:{describe(settings)}</div>\n\n <div className={css.section}>选择图片</div>\n <div className={css.urlRow}>\n <button\n type=\"button\"\n className={css.buttonPrimary}\n disabled={busy}\n onClick={() => fileRef.current?.click()}\n >\n {busy ? '处理中…' : '选择图片'}\n </button>\n <input\n ref={fileRef}\n className={css.fileInput}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/bmp\"\n onChange={(e) => void onPickFile(e)}\n />\n </div>\n {staged !== null && (\n <div className={css.stagedRow}>\n <div\n className={css.stagedThumb}\n style={{ backgroundImage: `url(\"${staged}\")`, backgroundSize: 'cover', backgroundPosition: 'center' }}\n />\n <span className={css.stagedLabel}>已选择图片</span>\n <button type=\"button\" className={css.buttonPrimary} onClick={onApplyImage}>应用</button>\n </div>\n )}\n\n <div className={css.section}>图片 URL</div>\n <div className={css.urlRow}>\n <input\n className={css.urlInput}\n value={urlInput}\n placeholder=\"https://…/image.jpg\"\n onChange={e => setUrlInput(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter') onApplyUrl() }}\n />\n <button type=\"button\" className={css.button} onClick={onApplyUrl}>应用</button>\n </div>\n\n <div className={css.section}>预设</div>\n <div className={css.presets}>\n {PRESETS.map(preset => (\n <button\n key={preset.id}\n type=\"button\"\n className={settings.source === 'preset' && settings.preset === preset.value\n ? `${css.swatch} ${css.swatchActive}`\n : css.swatch}\n title={preset.label}\n style={{ background: preset.value }}\n onClick={() => onPreset(preset.value)}\n />\n ))}\n </div>\n\n <div className={css.section}>效果</div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>暗化 {Math.round(settings.dim * 100)}%</span>\n <input\n className={css.slider}\n type=\"range\"\n min={0}\n max={80}\n step={5}\n value={Math.round(settings.dim * 100)}\n onChange={e => setAndCommit({ ...settings, dim: Number(e.target.value) / 100 })}\n />\n </div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>模糊 {Math.round(settings.blur)}px</span>\n <input\n className={css.slider}\n type=\"range\"\n min={0}\n max={24}\n step={1}\n value={Math.round(settings.blur)}\n onChange={e => setAndCommit({ ...settings, blur: Number(e.target.value) })}\n />\n </div>\n <div className={css.sliderRow}>\n <span className={css.sliderLabel}>透出 {Math.round(settings.surface * 100)}%</span>\n <input\n className={css.slider}\n type=\"range\"\n min={MIN_SURFACE * 100}\n max={MAX_SURFACE * 100}\n step={1}\n value={Math.round(settings.surface * 100)}\n title=\"表面不透明度越低,背景图片越明显\"\n onChange={e => setAndCommit({ ...settings, surface: Number(e.target.value) / 100 })}\n />\n </div>\n\n {error !== null && <div className={css.errorText}>{error}</div>}\n\n <div className={css.footer}>\n <button type=\"button\" className={css.button} onClick={onReset}>恢复默认</button>\n </div>\n </div>\n )\n}\n","/**\n * dsh-wx-skin — host persistence client: loads/saves the durable settings\n * through the host half's loopback routes (/dsh-wx-skin/load,\n * /dsh-wx-skin/save), which store the copy in the DSH home directory —\n * independent of the current origin, so the background image survives the\n * desktop app's per-launch port changes.\n *\n * Module is kept free of DOM access and takes an injectable fetch so unit\n * tests exercise it without a page. Every function degrades to a safe result\n * (null / false) and never throws; the caller keeps its localStorage\n * fallback.\n * @module dsh-wx-skin/client/skin-host\n */\nimport type { SkinSettings } from '../core/types.ts'\nimport { sanitizeSettings } from './skin-store.ts'\n\n/** HTTP route names served by the host half. */\nexport const LOAD_PATH = '/dsh-wx-skin/load'\nexport const SAVE_PATH = '/dsh-wx-skin/save'\n\n/** Read the durable settings from the host, or null when unavailable. */\nexport async function hostLoad(fetchImpl: typeof fetch = globalThis.fetch): Promise<SkinSettings | null> {\n try {\n const res = await fetchImpl(LOAD_PATH)\n if (!res.ok) return null\n const body = await res.json() as { ok?: unknown; settings?: unknown }\n // The host only ever writes an object; any other shape means no usable\n // durable copy → the caller falls back to localStorage/defaults.\n if (body?.ok !== true\n || body.settings === null\n || body.settings === undefined\n || typeof body.settings !== 'object') return null\n return sanitizeSettings(body.settings)\n } catch {\n return null\n }\n}\n\n/** Persist the settings to the host; false when it could not (never throws). */\nexport async function hostSave(settings: SkinSettings, fetchImpl: typeof fetch = globalThis.fetch): Promise<boolean> {\n try {\n const res = await fetchImpl(SAVE_PATH, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ settings }),\n })\n return res.ok\n } catch {\n return false\n }\n}\n","/**\n * dsh-wx-skin — global skin stylesheet, injected once as a <style> tag by\n * mount. Everything is gated on the `data-wx-skin-active` attribute on\n * documentElement, which the DOM applier toggles; with the attribute absent\n * the sheet is inert and the default theme is untouched.\n *\n * Design: a full-viewport fixed layer paints the chosen image/color, and the\n * alias surface tokens are overridden with translucent values so the\n * background shows through while text stays readable. The light/dark palette\n * branches ride `body[data-ds-dark-theme]`, exactly like the theme's own\n * token sheets.\n *\n * Stacking: measured on the real DSH shell, a `position: fixed` layer with\n * `z-index: -1` never paints (it lands behind the canvas background), while a\n * positive z-index layer paints fine. So the layer sits at `z-index: 0` and\n * the app root `#root` is lifted to `z-index: 1` — the layer renders behind\n * the app and shows through its translucent surfaces.\n *\n * Translucency: every surface token is `rgb(<static palette literal> /\n * var(--wx-skin-surface))` — a user-adjustable surface opacity (default 0.72)\n * — with no `color-mix()` dependency, so it works on any modern browser.\n * @module dsh-wx-skin/client/global-skin-css\n */\n\nexport const GLOBAL_SKIN_CSS = String.raw`\n/* ---- background layer (paints behind the app, above the canvas) ---- */\nhtml[data-wx-skin-active] div[data-wx-skin-layer] {\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image: var(--wx-skin-bg-image, none);\n background-color: var(--wx-skin-bg-color, transparent);\n background-position: center;\n background-size: cover;\n background-repeat: no-repeat;\n filter: blur(var(--wx-skin-blur, 0px));\n}\n\n/* dim / scrim overlay between the image and the translucent surfaces */\nhtml[data-wx-skin-active] div[data-wx-skin-layer]::after {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--wx-skin-scrim, transparent);\n}\n\n/* keep the canvas neutral so nothing opaque hides the layer */\nhtml[data-wx-skin-active] body {\n background: transparent !important;\n}\n\n/* lift the app above the layer; its translucent surfaces let it show through */\nhtml[data-wx-skin-active] #root {\n position: relative !important;\n z-index: 1 !important;\n}\n\n/* ---- translucent surfaces: light palette ---- */\nhtml[data-wx-skin-active] body {\n --dsw-alias-bg-base: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-1: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-2: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-3: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-overlay: rgb(233 236 242 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-module-platform: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-multi-select: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-sidebar-fill: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-input-major: rgb(255 255 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-selector: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-tip: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble: rgb(237 243 254 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble-highlight: rgb(211 226 255 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block-banner: rgb(249 250 251 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-inline-code: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-placeholder: rgb(245 246 247 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-tag: rgb(241 243 245 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-citation: rgb(235 238 242 / var(--wx-skin-surface, 0.72)) !important;\n}\n\n/* ---- translucent surfaces: dark palette ---- */\nhtml[data-wx-skin-active] body[data-ds-dark-theme] {\n --dsw-alias-bg-base: rgb(21 21 23 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-1: rgb(35 35 36 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-2: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-layer-3: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-overlay: rgb(97 102 107 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-module-platform: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-bg-multi-select: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-sidebar-fill: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-input-major: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-selector: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-tip: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-specific-bubble-highlight: rgb(67 69 74 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block: rgb(27 27 28 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-code-block-banner: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-inline-code: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-placeholder: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-tag: rgb(44 44 46 / var(--wx-skin-surface, 0.72)) !important;\n --dsw-alias-markdown-citation: rgb(53 54 56 / var(--wx-skin-surface, 0.72)) !important;\n}\n\n/* ---- sidebar entry active indicator ---- */\nhtml[data-wx-skin-active] button[data-wx-skin-entry] {\n color: var(--dsw-alias-state-business-primary, #4176e6);\n}\n`\n","/**\n * dsh-wx-skin — DOM applier: owns the injected global stylesheet, the\n * background layer element, and the projection of skin settings onto the\n * document (attribute + CSS variables). Pure DOM writes; retracts only what\n * it wrote, so the default theme is untouched when the skin is off.\n * @module dsh-wx-skin/client/skin-dom\n */\nimport type { SkinSettings } from '../core/types.ts'\nimport { GLOBAL_SKIN_CSS } from './global-skin-css.ts'\nimport { ACTIVE_ATTR, LAYER_ATTR, SKIN_CSS_VARS, cssVariables } from './skin-store.ts'\n\n/** Identity of the injected global <style> tag (idempotent injection key). */\nexport const GLOBAL_STYLE_ID = 'dsh-wx-skin/global'\n\n/** Inject the global skin stylesheet once. Safe to call repeatedly. */\nexport function ensureGlobalCss(): void {\n if (document.querySelector(`style[data-plugin-css=\"${GLOBAL_STYLE_ID}\"]`) !== null) return\n const tag = document.createElement('style')\n tag.dataset.pluginCss = GLOBAL_STYLE_ID\n tag.textContent = GLOBAL_SKIN_CSS\n document.head.appendChild(tag)\n}\n\n/** Ensure the full-viewport background layer exists and return it. */\nexport function ensureLayer(): HTMLDivElement {\n let layer = document.querySelector<HTMLDivElement>(`div[${LAYER_ATTR}]`)\n if (layer === null) {\n layer = document.createElement('div')\n layer.setAttribute(LAYER_ATTR, '')\n document.body.appendChild(layer)\n }\n return layer\n}\n\n/**\n * Apply skin settings to the document: toggle the gating attribute on\n * documentElement and write/retract the four CSS variables every time (so a\n * disabled skin leaves no residue).\n */\nexport class SkinApplier {\n apply(settings: SkinSettings): void {\n const root = document.documentElement\n if (settings.enabled) root.setAttribute(ACTIVE_ATTR, '')\n else root.removeAttribute(ACTIVE_ATTR)\n const vars = cssVariables(settings)\n for (const name of SKIN_CSS_VARS) {\n const value = vars[name]\n if (value === undefined) root.style.removeProperty(name)\n else root.style.setProperty(name, value)\n }\n }\n}\n","/**\n * dsh-wx-skin — DOM mounting: inject the global stylesheet and background\n * layer, place the sidebar 「皮肤」 entry (self-healing across shell\n * re-renders), and toggle an anchored popover with the settings panel. All\n * mount failures are logged, never thrown (the shell fails boot when a plugin\n * apply throws). Persists settings with a short debounce so slider drags do\n * not hammer localStorage with the image data URL.\n * @module dsh-wx-skin/client/mount\n */\nimport { createRoot, type Root } from 'react-dom/client'\nimport type { SkinSettings } from '../core/types.ts'\nimport { SkinPanel } from './SkinPanel.tsx'\nimport { ENTRY_ATTR, isDefaultSettings, loadSettings, saveSettings } from './skin-store.ts'\nimport { hostLoad, hostSave } from './skin-host.ts'\nimport { SkinApplier, ensureGlobalCss, ensureLayer } from './skin-dom.ts'\nimport css from './skin.module.css'\n\nconst PANEL_ATTR = 'data-wx-skin-panel'\nconst PANEL_WIDTH = 300\n\nconst ICON = '<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"8\" cy=\"8\" r=\"6\"/><path d=\"M8 2v2.2M8 11.8V14M2 8h2.2M11.8 8H14M4 4l1.6 1.6M10.4 10.4 12 12M12 4l-1.6 1.6M5.6 10.4 4 12\"/></svg>'\n\n/** Locate the sidebar shell root, or undefined while not yet mounted. */\nfunction sidebarRoot(): HTMLElement | undefined {\n const column = document.querySelector<HTMLElement>('[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]')\n if (column === null) return undefined\n const logoOwner = column.querySelector<HTMLElement>('[class*=\"logoRow\"]')?.parentElement\n return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)\n}\n\n/** The New Session button (nested in the logo row on current shells). */\nfunction newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {\n const nested = root.querySelector<HTMLButtonElement>('button[class*=\"newSession\"]')\n if (nested !== null) return nested\n for (const child of root.children) {\n if (child.tagName === 'BUTTON') return child as HTMLButtonElement\n }\n return undefined\n}\n\n/** Keep the entry in the sidebar, re-placing it across shell re-renders. */\nfunction mountEntrySelfHealing(entry: HTMLButtonElement): () => void {\n let root: HTMLElement | undefined\n let placed = false\n let rootObserver: MutationObserver | undefined\n\n const tryPlace = (): void => {\n if (root !== undefined && !root.isConnected) {\n rootObserver?.disconnect()\n root = undefined\n placed = false\n }\n if (placed) {\n if (document.body.contains(entry)) return\n rootObserver?.disconnect()\n root = undefined\n placed = false\n }\n root ??= sidebarRoot()\n if (root === undefined) return\n const anchor = newSessionButton(root)\n if (anchor === undefined) return\n if (entry.parentElement !== root) root.insertBefore(entry, anchor.nextElementSibling)\n placed = true\n if (rootObserver === undefined) {\n rootObserver = new MutationObserver(() => {\n if (root === undefined || !root.isConnected) {\n placed = false\n tryPlace()\n return\n }\n if (!root.contains(entry)) tryPlace()\n })\n rootObserver.observe(root, { childList: true, subtree: true })\n }\n }\n\n const waitObserver = new MutationObserver(() => { tryPlace() })\n waitObserver.observe(document.body, { childList: true, subtree: true })\n tryPlace()\n\n return () => {\n waitObserver.disconnect()\n rootObserver?.disconnect()\n entry.remove()\n }\n}\n\n/**\n * Mount the skin feature into the web shell.\n * @returns disposer tearing down the entry, popover, layer, and stylesheet.\n */\nexport function mountSkin(): () => void {\n const disposers: Array<() => void> = []\n ensureGlobalCss()\n ensureLayer()\n\n const applier = new SkinApplier()\n let latest = loadSettings()\n applier.apply(latest)\n\n // Debounced persistence (slider drags must not rewrite the image data URL\n // on every tick). Writes both the localStorage cache/fallback and the\n // durable host copy (deduped so an unchanged settings blob is not re-POSTed).\n let lastSentHost: string | undefined\n const syncHost = (): void => {\n const serialized = JSON.stringify(latest)\n if (lastSentHost === serialized) return\n lastSentHost = serialized\n void hostSave(latest)\n }\n const persist = (): void => {\n saveSettings(latest)\n syncHost()\n }\n let saveTimer: number | undefined\n const flushSave = (): void => {\n if (saveTimer === undefined) return\n window.clearTimeout(saveTimer)\n saveTimer = undefined\n persist()\n }\n const scheduleSave = (): void => {\n if (saveTimer !== undefined) window.clearTimeout(saveTimer)\n saveTimer = window.setTimeout(() => { persist() }, 200)\n }\n\n // Sidebar entry.\n const entry = document.createElement('button')\n entry.type = 'button'\n entry.setAttribute(ENTRY_ATTR, '')\n entry.className = css.entry\n entry.setAttribute('aria-label', '皮肤')\n entry.innerHTML = `<span class=\"${css.entryIcon}\">${ICON}</span><span class=\"${css.entryLabel}\">皮肤</span>`\n const setEntryActive = (active: boolean): void => {\n if (active) entry.dataset.active = 'true'\n else delete entry.dataset.active\n }\n setEntryActive(latest.enabled)\n disposers.push(mountEntrySelfHealing(entry))\n\n // Server-side durability: the desktop app changes origin — and therefore its\n // localStorage bucket — on every launch, so the durable copy lives in the\n // DSH home via the host half. Sync once at mount: adopt the host copy when\n // present, else seed it from localStorage on the first run after upgrade.\n // Never throws into the GUI; any host failure keeps the localStorage path.\n void (async () => {\n try {\n const host = await hostLoad()\n if (host !== null) {\n latest = host\n applier.apply(host)\n setEntryActive(host.enabled)\n return\n }\n const local = loadSettings()\n if (!isDefaultSettings(local)) void hostSave(local)\n } catch {\n // Swallow — the skin must never take the shell down.\n }\n })()\n\n // Popover host + React root.\n const panelHost = document.createElement('div')\n panelHost.setAttribute(PANEL_ATTR, '')\n panelHost.style.cssText = `position: fixed; z-index: 1000; width: ${PANEL_WIDTH}px;`\n let root: Root | undefined\n\n const positionPopover = (): void => {\n const rect = entry.getBoundingClientRect()\n const left = Math.max(8, Math.min(rect.right + 10, window.innerWidth - PANEL_WIDTH - 8))\n const top = Math.max(8, rect.top)\n panelHost.style.left = `${left}px`\n panelHost.style.top = `${top}px`\n }\n\n const closePanel = (): void => {\n if (root === undefined) return\n flushSave()\n root.unmount()\n root = undefined\n panelHost.remove()\n }\n\n const openPanel = (): void => {\n if (root !== undefined) return\n positionPopover()\n document.body.appendChild(panelHost)\n root = createRoot(panelHost)\n root.render(<SkinPanel\n initial={latest}\n onClose={closePanel}\n commit={(next) => {\n latest = next\n applier.apply(next)\n setEntryActive(next.enabled)\n scheduleSave()\n }}\n />)\n }\n\n const togglePanel = (): void => { if (root !== undefined) closePanel(); else openPanel() }\n entry.addEventListener('click', togglePanel)\n\n // Close on outside click, Escape, or window resize.\n const onDocMouseDown = (event: MouseEvent): void => {\n if (root === undefined) return\n const target = event.target as Node | null\n if (target !== null && (panelHost.contains(target) || entry.contains(target))) return\n closePanel()\n }\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && root !== undefined) closePanel()\n }\n const onResize = (): void => { if (root !== undefined) closePanel() }\n const onBeforeUnload = (): void => flushSave()\n document.addEventListener('mousedown', onDocMouseDown)\n window.addEventListener('keydown', onKeyDown)\n window.addEventListener('resize', onResize)\n window.addEventListener('beforeunload', onBeforeUnload)\n disposers.push(() => {\n document.removeEventListener('mousedown', onDocMouseDown)\n window.removeEventListener('keydown', onKeyDown)\n window.removeEventListener('resize', onResize)\n window.removeEventListener('beforeunload', onBeforeUnload)\n })\n\n return () => {\n closePanel()\n for (const dispose of disposers.splice(0)) dispose()\n document.querySelector<HTMLElement>(`div[data-wx-skin-layer]`)?.remove()\n document.querySelector<HTMLElement>(`style[data-plugin-css=\"dsh-wx-skin/global\"]`)?.remove()\n applier.apply({ ...latest, enabled: false })\n }\n}\n","/**\n * dsh-wx-skin — browser half entry. Mounts the skin feature (sidebar entry +\n * settings popover + background layer). All failures are logged, never thrown.\n * @module dsh-wx-skin/client\n */\nimport { mountSkin } from './mount.tsx'\n\n/** No hard service dependencies — the skin only touches the DOM. */\nexport const inject: string[] = []\n\n/** Apply the browser half. */\nexport function apply(): void {\n try {\n mountSkin()\n } catch (error) {\n // A failing external plugin must never take the GUI down.\n console.error('[dsh-wx-skin] mount failed:', error)\n }\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,138 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
1
4
|
export const name = 'dsh-wx-skin';
|
|
5
|
+
/** Services required before the routes can be registered. */
|
|
6
|
+
export const inject = ['webServer'];
|
|
7
|
+
/** Route paths served by the host half (outside /api, mirrors /dsh-wx-* conventions). */
|
|
8
|
+
export const LOAD_ROUTE = '/dsh-wx-skin/load';
|
|
9
|
+
export const SAVE_ROUTE = '/dsh-wx-skin/save';
|
|
10
|
+
/** Upper bound on a settings payload — image data URLs can reach ~MBs. */
|
|
11
|
+
const MAX_SAVE_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const LOOPBACK_ORIGINS = ['http://127.0.0.1', 'http://localhost', 'http://[::1]'];
|
|
13
|
+
/** The dsh home directory ($DSH_HOME, else ~/.dsh). */
|
|
14
|
+
function resolveDshHome() {
|
|
15
|
+
const env = process.env.DSH_HOME;
|
|
16
|
+
if (env !== undefined && env.trim() !== '')
|
|
17
|
+
return env;
|
|
18
|
+
return join(homedir(), '.dsh');
|
|
19
|
+
}
|
|
20
|
+
/** Durable settings file path — one shared copy per machine. */
|
|
21
|
+
function settingsPath() {
|
|
22
|
+
return join(resolveDshHome(), 'dsh-wx-skin.settings.json');
|
|
23
|
+
}
|
|
24
|
+
/** Whether a request may reach the host routes: loopback origin only. */
|
|
25
|
+
function isLoopbackRequest(req) {
|
|
26
|
+
const origin = req.headers.origin;
|
|
27
|
+
if (origin === undefined)
|
|
28
|
+
return true; // same-origin fetch omits Origin
|
|
29
|
+
return LOOPBACK_ORIGINS.some((base) => origin === base || origin.startsWith(`${base}:`));
|
|
30
|
+
}
|
|
31
|
+
/** Send a JSON response. */
|
|
32
|
+
function sendJson(res, status, body) {
|
|
33
|
+
const payload = JSON.stringify(body);
|
|
34
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
35
|
+
res.end(payload);
|
|
36
|
+
}
|
|
37
|
+
/** Read a bounded JSON body to a plain object, or null when absent/invalid/oversized. */
|
|
38
|
+
async function readJsonBody(req, maxBytes) {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let size = 0;
|
|
41
|
+
for await (const chunk of req) {
|
|
42
|
+
const buffer = chunk;
|
|
43
|
+
size += buffer.byteLength;
|
|
44
|
+
if (size > maxBytes)
|
|
45
|
+
return null;
|
|
46
|
+
chunks.push(buffer);
|
|
47
|
+
}
|
|
48
|
+
if (chunks.length === 0)
|
|
49
|
+
return {};
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
52
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Common method + loopback guard for every host route. */
|
|
59
|
+
function guard(req, res, method) {
|
|
60
|
+
if (req.method !== method) {
|
|
61
|
+
sendJson(res, 405, { ok: false, error: 'method not allowed' });
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (!isLoopbackRequest(req)) {
|
|
65
|
+
sendJson(res, 403, { ok: false, error: 'forbidden' });
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
2
70
|
/**
|
|
3
|
-
* Apply the host half:
|
|
4
|
-
* @param
|
|
71
|
+
* Apply the host half: register the skin-settings persistence routes.
|
|
72
|
+
* @param ctx - the Cordis context.
|
|
5
73
|
*/
|
|
6
|
-
export function apply(
|
|
7
|
-
|
|
74
|
+
export function apply(ctx) {
|
|
75
|
+
const webServer = ctx.get('webServer');
|
|
76
|
+
if (webServer === undefined) {
|
|
77
|
+
ctx.logger.warn('[dsh-wx-skin] webServer service unavailable; host persistence disabled (localStorage fallback)');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
ctx.effect(() => {
|
|
81
|
+
const disposers = [];
|
|
82
|
+
const path = settingsPath();
|
|
83
|
+
// GET /dsh-wx-skin/load — read the durable copy (raw; client sanitizes).
|
|
84
|
+
disposers.push(webServer.register({
|
|
85
|
+
kind: 'exact',
|
|
86
|
+
path: LOAD_ROUTE,
|
|
87
|
+
handler: (req, res) => {
|
|
88
|
+
if (!guard(req, res, 'GET'))
|
|
89
|
+
return;
|
|
90
|
+
let settings = null;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
93
|
+
settings = parsed?.settings ?? null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
settings = null; // absent or corrupt → client falls back to localStorage/defaults
|
|
97
|
+
}
|
|
98
|
+
sendJson(res, 200, { ok: true, settings });
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
// POST /dsh-wx-skin/save — atomically persist the settings blob.
|
|
102
|
+
disposers.push(webServer.register({
|
|
103
|
+
kind: 'exact',
|
|
104
|
+
path: SAVE_ROUTE,
|
|
105
|
+
handler: async (req, res) => {
|
|
106
|
+
if (!guard(req, res, 'POST'))
|
|
107
|
+
return;
|
|
108
|
+
const body = await readJsonBody(req, MAX_SAVE_BYTES);
|
|
109
|
+
if (body === null) {
|
|
110
|
+
sendJson(res, 413, { ok: false, error: 'invalid or oversized body' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const settings = body.settings;
|
|
114
|
+
if (typeof settings !== 'object' || settings === null) {
|
|
115
|
+
sendJson(res, 400, { ok: false, error: 'missing settings payload' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
120
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
121
|
+
writeFileSync(tmp, JSON.stringify({ settings }), 'utf8');
|
|
122
|
+
renameSync(tmp, path);
|
|
123
|
+
sendJson(res, 200, { ok: true });
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
ctx.logger.warn('[dsh-wx-skin] save failed: %s', message);
|
|
128
|
+
sendJson(res, 500, { ok: false, error: message });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
return () => {
|
|
133
|
+
for (const dispose of disposers.splice(0))
|
|
134
|
+
dispose();
|
|
135
|
+
};
|
|
136
|
+
}, 'dsh-wx-skin: persistence routes');
|
|
8
137
|
}
|
|
9
138
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-wx-skin — host half:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* dsh-wx-skin — host half: server-side persistence for the skin settings.
|
|
3
|
+
*
|
|
4
|
+
* The browser half keeps a copy of the settings in localStorage, which is
|
|
5
|
+
* scoped to the page origin. The desktop app (dsh-wx-desktop) launches
|
|
6
|
+
* `dsh web --port 0`, so the OS assigns a fresh port — and therefore a fresh
|
|
7
|
+
* origin and an empty localStorage bucket — on every launch, which used to
|
|
8
|
+
* wipe the background image. These two webServer routes move the durable copy
|
|
9
|
+
* of the settings into the DSH home directory, where it survives any origin
|
|
10
|
+
* change and is shared by every client on this machine (browser GUI at :3080
|
|
11
|
+
* and the desktop app alike):
|
|
12
|
+
*
|
|
13
|
+
* GET /dsh-wx-skin/load → { ok, settings }
|
|
14
|
+
* POST /dsh-wx-skin/save → { ok }
|
|
15
|
+
*
|
|
16
|
+
* The schema lives in the browser half (sanitizeSettings); the host stores
|
|
17
|
+
* and returns the raw JSON blob it receives. Routes are loopback-only and the
|
|
18
|
+
* file is written atomically (temp + rename) so concurrent clients cannot
|
|
19
|
+
* corrupt it. When the webServer service is unavailable the host half stays a
|
|
20
|
+
* no-op and the client falls back to its localStorage behavior.
|
|
6
21
|
* @module dsh-wx-skin
|
|
7
22
|
*/
|
|
8
23
|
import type { Context } from '@deepseek-ai/cordis';
|
|
9
24
|
export declare const name = "dsh-wx-skin";
|
|
25
|
+
/** Services required before the routes can be registered. */
|
|
26
|
+
export declare const inject: string[];
|
|
27
|
+
/** Route paths served by the host half (outside /api, mirrors /dsh-wx-* conventions). */
|
|
28
|
+
export declare const LOAD_ROUTE = "/dsh-wx-skin/load";
|
|
29
|
+
export declare const SAVE_ROUTE = "/dsh-wx-skin/save";
|
|
10
30
|
/**
|
|
11
|
-
* Apply the host half:
|
|
12
|
-
* @param
|
|
31
|
+
* Apply the host half: register the skin-settings persistence routes.
|
|
32
|
+
* @param ctx - the Cordis context.
|
|
13
33
|
*/
|
|
14
|
-
export declare function apply(
|
|
34
|
+
export declare function apply(ctx: Context): void;
|
|
15
35
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,eAAO,MAAM,IAAI,gBAAgB,CAAA;AAEjC,6DAA6D;AAC7D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC,yFAAyF;AACzF,eAAO,MAAM,UAAU,sBAAsB,CAAA;AAC7C,eAAO,MAAM,UAAU,sBAAsB,CAAA;AA0E7C;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA8DxC"}
|