dsh-dream-skin 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,2191 @@
1
+ // dsh-dream-skin — browser half (client plugin bundle).
2
+ //
3
+ // Loaded by dsh-client-modules at /plugins/dsh-dream-skin/client.js and
4
+ // executed through the vendored cordis Loader's lazy-CJS module table
5
+ // (window.__ModuleLoader__.load). The factory body is plain CJS with
6
+ // require() resolved against the shell's module table — the same shape the
7
+ // shipped ui-* packages' tsdown bundles emit. Only platform seed words and
8
+ // registered client bundles may be required.
9
+ //
10
+ // Persistence note: the skin choice and wallpaper settings are stored in
11
+ // localStorage. DSH's Host settings wire only exposes an allowlisted set of
12
+ // namespaces to browser clients (dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES),
13
+ // so a third-party namespace would answer `settings-not-exposed`; the product
14
+ // itself keeps remote browser preferences process-local, and localStorage
15
+ // matches that boundary for visual preferences while surviving reloads on the
16
+ // same origin.
17
+
18
+ window.__ModuleLoader__.load({
19
+ id: "dsh-dream-skin",
20
+ factory: (require) => {
21
+ var module = { exports: {} };
22
+ var exports = module.exports;
23
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
24
+ let react_jsx_runtime = require("react/jsx-runtime");
25
+ let _react = require("react");
26
+ let _runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
27
+
28
+ //#region dsh-dream-skin: constants & presets
29
+ /** The settings row's locale namespace. */
30
+ const SETTINGS_NS = "settings.dreamSkin";
31
+ /** localStorage key holding the selected skin id. */
32
+ const STORAGE_KEY = "dsh-dream-skin:skin";
33
+ /** localStorage key holding the wallpaper image (data URL). */
34
+ const WALLPAPER_KEY = "dsh-dream-skin:wallpaper";
35
+ /** localStorage key holding the wallpaper wash opacity (0..1). */
36
+ const WALLPAPER_OPACITY_KEY = "dsh-dream-skin:wallpaper-opacity";
37
+ /** localStorage key holding the wallpaper blur radius (px). */
38
+ const WALLPAPER_BLUR_KEY = "dsh-dream-skin:wallpaper-blur";
39
+ /** Sentinel meaning "no custom skin — follow the built-in appearance". */
40
+ const DEFAULT_SKIN = "system";
41
+ /** Default wash opacity (0..1) applied to the translucent surfaces. */
42
+ const DEFAULT_WALLPAPER_OPACITY = 0.8;
43
+ /** Default wallpaper blur radius in px. */
44
+ const DEFAULT_WALLPAPER_BLUR = 0;
45
+ /** Source identity for the wallpaper's token override layer. */
46
+ const OVERRIDE_SOURCE = "dsh-dream-skin:wallpaper";
47
+ /** Built-in base colors used when no skin token owns a scheme. */
48
+ const BUILTIN_BASE = {
49
+ light: "rgb(255, 255, 255)",
50
+ dark: "rgb(21, 21, 23)"
51
+ };
52
+
53
+ /**
54
+ * The curated "Mirage" skin catalog. Every skin is a third-party theme
55
+ * for the built-in ThemeRuntime: an id, the base palette it builds on
56
+ * (colorScheme drives body[data-ds-dark-theme]), and --dsw-alias-*
57
+ * overrides applied as inline custom properties on <body> by ui-layout's
58
+ * ThemePresenter. Values are concrete CSS colors (no var() indirection),
59
+ * tuned per skin for contrast on both surface and text roles. Add your
60
+ * own entries here and they appear in the Settings picker automatically.
61
+ */
62
+ const SKINS = [
63
+ {
64
+ id: "abyss",
65
+ colorScheme: "dark",
66
+ tokens: {
67
+ "--dsw-alias-bg-base": "#060a14",
68
+ "--dsw-alias-bg-layer-1": "#0d1424",
69
+ "--dsw-alias-bg-layer-2": "#141e36",
70
+ "--dsw-alias-bg-layer-3": "#1a2744",
71
+ "--dsw-alias-bg-overlay": "#1b2947",
72
+ "--dsw-alias-border-l1": "rgba(148, 168, 210, 0.13)",
73
+ "--dsw-alias-border-l2": "rgba(148, 168, 210, 0.24)",
74
+ "--dsw-alias-label-primary": "#eef2fa",
75
+ "--dsw-alias-label-secondary": "#9fb2d4",
76
+ "--dsw-alias-label-tertiary": "#788eb6",
77
+ "--dsw-alias-brand-primary": "#4f83f2",
78
+ "--dsw-alias-brand-text": "#ffffff",
79
+ "--dsw-alias-button-primary-hover": "#6f9af6",
80
+ "--dsw-alias-button-primary-dimmed": "#141e36",
81
+ "--dsw-alias-state-business-primary": "#4f83f2",
82
+ "--dsw-alias-state-business-tertiary": "#141e36",
83
+ "--dsw-alias-interactive-bg-hover": "rgba(79, 131, 242, 0.13)",
84
+ "--dsw-alias-interactive-bg-active": "rgba(79, 131, 242, 0.22)",
85
+ "--dsw-alias-markdown-code-block": "#0b1120",
86
+ "--dsw-alias-markdown-inline-code": "#141e36",
87
+ "--dsw-specific-sidebar-fill": "#0b1120",
88
+ "--dsw-specific-sidebar-nav-item-active": "#141e36",
89
+ "--dsw-specific-sidebar-nav-item-hover": "#101828",
90
+ "--dsw-alias-scrollbar-bg-l1": "#1a2744",
91
+ "--dsw-alias-scrollbar-bg-l2": "#1f2f52",
92
+ "--dsw-alias-scrollbar-hover-l1": "#26375f",
93
+ "--dsw-alias-scrollbar-hover-l2": "#26375f"
94
+ }
95
+ },
96
+ {
97
+ id: "aurora",
98
+ colorScheme: "dark",
99
+ tokens: {
100
+ "--dsw-alias-bg-base": "#04120f",
101
+ "--dsw-alias-bg-layer-1": "#0a1d18",
102
+ "--dsw-alias-bg-layer-2": "#102a23",
103
+ "--dsw-alias-bg-layer-3": "#16372e",
104
+ "--dsw-alias-bg-overlay": "#183a31",
105
+ "--dsw-alias-border-l1": "rgba(110, 231, 183, 0.12)",
106
+ "--dsw-alias-border-l2": "rgba(110, 231, 183, 0.22)",
107
+ "--dsw-alias-label-primary": "#eafaf2",
108
+ "--dsw-alias-label-secondary": "#92d5b8",
109
+ "--dsw-alias-label-tertiary": "#6fb398",
110
+ "--dsw-alias-brand-primary": "#34d399",
111
+ "--dsw-alias-brand-text": "#03211a",
112
+ "--dsw-alias-button-primary-hover": "#57e0b0",
113
+ "--dsw-alias-button-primary-dimmed": "#102a23",
114
+ "--dsw-alias-state-business-primary": "#34d399",
115
+ "--dsw-alias-state-business-tertiary": "#102a23",
116
+ "--dsw-alias-interactive-bg-hover": "rgba(52, 211, 153, 0.13)",
117
+ "--dsw-alias-interactive-bg-active": "rgba(52, 211, 153, 0.22)",
118
+ "--dsw-alias-markdown-code-block": "#081712",
119
+ "--dsw-alias-markdown-inline-code": "#102a23",
120
+ "--dsw-specific-sidebar-fill": "#081712",
121
+ "--dsw-specific-sidebar-nav-item-active": "#102a23",
122
+ "--dsw-specific-sidebar-nav-item-hover": "#0c231c",
123
+ "--dsw-alias-scrollbar-bg-l1": "#16372e",
124
+ "--dsw-alias-scrollbar-bg-l2": "#1b4438",
125
+ "--dsw-alias-scrollbar-hover-l1": "#225344",
126
+ "--dsw-alias-scrollbar-hover-l2": "#225344"
127
+ }
128
+ },
129
+ {
130
+ id: "nebula",
131
+ colorScheme: "dark",
132
+ tokens: {
133
+ "--dsw-alias-bg-base": "#0f0a1c",
134
+ "--dsw-alias-bg-layer-1": "#17102b",
135
+ "--dsw-alias-bg-layer-2": "#1f1638",
136
+ "--dsw-alias-bg-layer-3": "#271c46",
137
+ "--dsw-alias-bg-overlay": "#291e49",
138
+ "--dsw-alias-border-l1": "rgba(216, 180, 254, 0.12)",
139
+ "--dsw-alias-border-l2": "rgba(216, 180, 254, 0.22)",
140
+ "--dsw-alias-label-primary": "#f4eefc",
141
+ "--dsw-alias-label-secondary": "#c6aee6",
142
+ "--dsw-alias-label-tertiary": "#a28dc7",
143
+ "--dsw-alias-brand-primary": "#a78bfa",
144
+ "--dsw-alias-brand-text": "#150c26",
145
+ "--dsw-alias-button-primary-hover": "#bca7fd",
146
+ "--dsw-alias-button-primary-dimmed": "#1f1638",
147
+ "--dsw-alias-state-business-primary": "#a78bfa",
148
+ "--dsw-alias-state-business-tertiary": "#1f1638",
149
+ "--dsw-alias-interactive-bg-hover": "rgba(167, 139, 250, 0.14)",
150
+ "--dsw-alias-interactive-bg-active": "rgba(167, 139, 250, 0.24)",
151
+ "--dsw-alias-markdown-code-block": "#130c22",
152
+ "--dsw-alias-markdown-inline-code": "#1f1638",
153
+ "--dsw-specific-sidebar-fill": "#130c22",
154
+ "--dsw-specific-sidebar-nav-item-active": "#1f1638",
155
+ "--dsw-specific-sidebar-nav-item-hover": "#191230",
156
+ "--dsw-alias-scrollbar-bg-l1": "#271c46",
157
+ "--dsw-alias-scrollbar-bg-l2": "#312356",
158
+ "--dsw-alias-scrollbar-hover-l1": "#3a2c66",
159
+ "--dsw-alias-scrollbar-hover-l2": "#3a2c66"
160
+ }
161
+ },
162
+ {
163
+ id: "ember",
164
+ colorScheme: "dark",
165
+ tokens: {
166
+ "--dsw-alias-bg-base": "#120a08",
167
+ "--dsw-alias-bg-layer-1": "#1b120e",
168
+ "--dsw-alias-bg-layer-2": "#241913",
169
+ "--dsw-alias-bg-layer-3": "#2d1f18",
170
+ "--dsw-alias-bg-overlay": "#2f211a",
171
+ "--dsw-alias-border-l1": "rgba(253, 186, 116, 0.12)",
172
+ "--dsw-alias-border-l2": "rgba(253, 186, 116, 0.22)",
173
+ "--dsw-alias-label-primary": "#fdf1e7",
174
+ "--dsw-alias-label-secondary": "#d6ab8c",
175
+ "--dsw-alias-label-tertiary": "#b68a6c",
176
+ "--dsw-alias-brand-primary": "#fb923c",
177
+ "--dsw-alias-brand-text": "#24110a",
178
+ "--dsw-alias-button-primary-hover": "#fdad6a",
179
+ "--dsw-alias-button-primary-dimmed": "#241913",
180
+ "--dsw-alias-state-business-primary": "#fb923c",
181
+ "--dsw-alias-state-business-tertiary": "#241913",
182
+ "--dsw-alias-interactive-bg-hover": "rgba(251, 146, 60, 0.14)",
183
+ "--dsw-alias-interactive-bg-active": "rgba(251, 146, 60, 0.24)",
184
+ "--dsw-alias-markdown-code-block": "#160e0a",
185
+ "--dsw-alias-markdown-inline-code": "#241913",
186
+ "--dsw-specific-sidebar-fill": "#160e0a",
187
+ "--dsw-specific-sidebar-nav-item-active": "#241913",
188
+ "--dsw-specific-sidebar-nav-item-hover": "#1e1510",
189
+ "--dsw-alias-scrollbar-bg-l1": "#2d1f18",
190
+ "--dsw-alias-scrollbar-bg-l2": "#3a281d",
191
+ "--dsw-alias-scrollbar-hover-l1": "#473225",
192
+ "--dsw-alias-scrollbar-hover-l2": "#473225"
193
+ }
194
+ },
195
+ {
196
+ id: "midnight",
197
+ colorScheme: "dark",
198
+ tokens: {
199
+ "--dsw-alias-bg-base": "#000000",
200
+ "--dsw-alias-bg-layer-1": "#0b0b0f",
201
+ "--dsw-alias-bg-layer-2": "#141419",
202
+ "--dsw-alias-bg-layer-3": "#1c1c23",
203
+ "--dsw-alias-bg-overlay": "#1d1d24",
204
+ "--dsw-alias-border-l1": "rgba(255, 255, 255, 0.06)",
205
+ "--dsw-alias-border-l2": "rgba(255, 255, 255, 0.12)",
206
+ "--dsw-alias-label-primary": "#e8e8ee",
207
+ "--dsw-alias-label-secondary": "#9d9daa",
208
+ "--dsw-alias-label-tertiary": "#7c7c88",
209
+ "--dsw-alias-brand-primary": "#7c8cff",
210
+ "--dsw-alias-brand-text": "#05050a",
211
+ "--dsw-alias-button-primary-hover": "#9aa7ff",
212
+ "--dsw-alias-button-primary-dimmed": "#141419",
213
+ "--dsw-alias-state-business-primary": "#7c8cff",
214
+ "--dsw-alias-state-business-tertiary": "#141419",
215
+ "--dsw-alias-interactive-bg-hover": "rgba(124, 140, 255, 0.13)",
216
+ "--dsw-alias-interactive-bg-active": "rgba(124, 140, 255, 0.22)",
217
+ "--dsw-alias-markdown-code-block": "#08080b",
218
+ "--dsw-alias-markdown-inline-code": "#141419",
219
+ "--dsw-specific-sidebar-fill": "#08080b",
220
+ "--dsw-specific-sidebar-nav-item-active": "#141419",
221
+ "--dsw-specific-sidebar-nav-item-hover": "#0e0e13",
222
+ "--dsw-alias-scrollbar-bg-l1": "#1c1c23",
223
+ "--dsw-alias-scrollbar-bg-l2": "#26262f",
224
+ "--dsw-alias-scrollbar-hover-l1": "#31313c",
225
+ "--dsw-alias-scrollbar-hover-l2": "#31313c"
226
+ }
227
+ },
228
+ {
229
+ id: "ivory",
230
+ colorScheme: "light",
231
+ tokens: {
232
+ "--dsw-alias-bg-base": "#f7f4ee",
233
+ "--dsw-alias-bg-layer-1": "#ffffff",
234
+ "--dsw-alias-bg-layer-2": "#f0ead8",
235
+ "--dsw-alias-bg-layer-3": "#e7dfcb",
236
+ "--dsw-alias-bg-overlay": "#fffdf8",
237
+ "--dsw-alias-border-l1": "rgba(122, 96, 44, 0.1)",
238
+ "--dsw-alias-border-l2": "rgba(122, 96, 44, 0.18)",
239
+ "--dsw-alias-label-primary": "#2e2920",
240
+ "--dsw-alias-label-secondary": "#6f6656",
241
+ "--dsw-alias-label-tertiary": "#8d8373",
242
+ "--dsw-alias-brand-primary": "#a16207",
243
+ "--dsw-alias-brand-text": "#ffffff",
244
+ "--dsw-alias-button-primary-hover": "#c67c0f",
245
+ "--dsw-alias-button-primary-dimmed": "#f0ead8",
246
+ "--dsw-alias-state-business-primary": "#a16207",
247
+ "--dsw-alias-state-business-tertiary": "#f0ead8",
248
+ "--dsw-alias-interactive-bg-hover": "rgba(161, 98, 7, 0.08)",
249
+ "--dsw-alias-interactive-bg-active": "rgba(161, 98, 7, 0.14)",
250
+ "--dsw-alias-markdown-code-block": "#f0ead8",
251
+ "--dsw-alias-markdown-inline-code": "#ece5d2",
252
+ "--dsw-specific-sidebar-fill": "#f0ead8",
253
+ "--dsw-specific-sidebar-nav-item-active": "#e7dfcb",
254
+ "--dsw-specific-sidebar-nav-item-hover": "#ece4d0",
255
+ "--dsw-alias-scrollbar-bg-l1": "#e0d6bd",
256
+ "--dsw-alias-scrollbar-bg-l2": "#d8ccb0",
257
+ "--dsw-alias-scrollbar-hover-l1": "#cdbfa0",
258
+ "--dsw-alias-scrollbar-hover-l2": "#cdbfa0"
259
+ }
260
+ },
261
+ {
262
+ id: "mist",
263
+ colorScheme: "light",
264
+ tokens: {
265
+ "--dsw-alias-bg-base": "#f0f3f7",
266
+ "--dsw-alias-bg-layer-1": "#ffffff",
267
+ "--dsw-alias-bg-layer-2": "#e7edf4",
268
+ "--dsw-alias-bg-layer-3": "#dbe4ee",
269
+ "--dsw-alias-bg-overlay": "#ffffff",
270
+ "--dsw-alias-border-l1": "rgba(51, 65, 85, 0.1)",
271
+ "--dsw-alias-border-l2": "rgba(51, 65, 85, 0.18)",
272
+ "--dsw-alias-label-primary": "#1e293b",
273
+ "--dsw-alias-label-secondary": "#64748b",
274
+ "--dsw-alias-label-tertiary": "#94a3b8",
275
+ "--dsw-alias-brand-primary": "#2563eb",
276
+ "--dsw-alias-brand-text": "#ffffff",
277
+ "--dsw-alias-button-primary-hover": "#3b82f6",
278
+ "--dsw-alias-button-primary-dimmed": "#e7edf4",
279
+ "--dsw-alias-state-business-primary": "#2563eb",
280
+ "--dsw-alias-state-business-tertiary": "#e7edf4",
281
+ "--dsw-alias-interactive-bg-hover": "rgba(37, 99, 235, 0.08)",
282
+ "--dsw-alias-interactive-bg-active": "rgba(37, 99, 235, 0.14)",
283
+ "--dsw-alias-markdown-code-block": "#e7edf4",
284
+ "--dsw-alias-markdown-inline-code": "#dbe4ee",
285
+ "--dsw-specific-sidebar-fill": "#e7edf4",
286
+ "--dsw-specific-sidebar-nav-item-active": "#dbe4ee",
287
+ "--dsw-specific-sidebar-nav-item-hover": "#e2e9f2",
288
+ "--dsw-alias-scrollbar-bg-l1": "#cbd5e1",
289
+ "--dsw-alias-scrollbar-bg-l2": "#c1ccda",
290
+ "--dsw-alias-scrollbar-hover-l1": "#b4c0d0",
291
+ "--dsw-alias-scrollbar-hover-l2": "#b4c0d0"
292
+ }
293
+ },
294
+ {
295
+ id: "rose",
296
+ colorScheme: "light",
297
+ tokens: {
298
+ "--dsw-alias-bg-base": "#fbf3f5",
299
+ "--dsw-alias-bg-layer-1": "#ffffff",
300
+ "--dsw-alias-bg-layer-2": "#f7e4ea",
301
+ "--dsw-alias-bg-layer-3": "#f0d2dc",
302
+ "--dsw-alias-bg-overlay": "#fffdfd",
303
+ "--dsw-alias-border-l1": "rgba(190, 90, 120, 0.1)",
304
+ "--dsw-alias-border-l2": "rgba(190, 90, 120, 0.18)",
305
+ "--dsw-alias-label-primary": "#3a2230",
306
+ "--dsw-alias-label-secondary": "#90647a",
307
+ "--dsw-alias-label-tertiary": "#a47d92",
308
+ "--dsw-alias-brand-primary": "#e11d78",
309
+ "--dsw-alias-brand-text": "#ffffff",
310
+ "--dsw-alias-button-primary-hover": "#ec4a96",
311
+ "--dsw-alias-button-primary-dimmed": "#f7e4ea",
312
+ "--dsw-alias-state-business-primary": "#e11d78",
313
+ "--dsw-alias-state-business-tertiary": "#f7e4ea",
314
+ "--dsw-alias-interactive-bg-hover": "rgba(225, 29, 120, 0.08)",
315
+ "--dsw-alias-interactive-bg-active": "rgba(225, 29, 120, 0.15)",
316
+ "--dsw-alias-markdown-code-block": "#f7e4ea",
317
+ "--dsw-alias-markdown-inline-code": "#f0d2dc",
318
+ "--dsw-specific-sidebar-fill": "#f7e4ea",
319
+ "--dsw-specific-sidebar-nav-item-active": "#f0d2dc",
320
+ "--dsw-specific-sidebar-nav-item-hover": "#f4dae2",
321
+ "--dsw-alias-scrollbar-bg-l1": "#eccfd9",
322
+ "--dsw-alias-scrollbar-bg-l2": "#e5c0cd",
323
+ "--dsw-alias-scrollbar-hover-l1": "#d9afbf",
324
+ "--dsw-alias-scrollbar-hover-l2": "#d9afbf"
325
+ }
326
+ }
327
+ ];
328
+
329
+ /** Simplified Chinese dictionary (the key-set source of truth). */
330
+ const zh = {
331
+ "skin.title": "皮肤(Mirage 幻梦)",
332
+ "skin.default": "默认",
333
+ "skin.abyss": "深海渊",
334
+ "skin.aurora": "极光",
335
+ "skin.nebula": "星云",
336
+ "skin.ember": "余烬",
337
+ "skin.midnight": "午夜",
338
+ "skin.ivory": "象牙暖",
339
+ "skin.mist": "晨雾蓝",
340
+ "skin.rose": "蔷薇粉",
341
+ "background.title": "背景图片(壁纸)",
342
+ "background.choose": "选择图片",
343
+ "background.remove": "移除图片",
344
+ "background.opacity": "透明度",
345
+ "background.blur": "模糊",
346
+ "background.hint": "图片显示在主内容区与侧边栏的半透明底上,消息等内层表面保持不透明以保证可读性",
347
+ "accent.title": "强调色(Accent)",
348
+ "accent.random": "随机",
349
+ "accent.clear": "恢复主题色",
350
+ "accent.hint": "为当前皮肤设置一个自定义强调色(叠加层,不影响皮肤本身);点「恢复主题色」回到皮肤默认强调色",
351
+ "packs.title": "主题包(本地库)",
352
+ "packs.import": "导入主题包…",
353
+ "packs.share": "复制分享链接",
354
+ "packs.apply": "应用",
355
+ "packs.surprise": "换一个试试",
356
+ "packs.empty": "还没有主题包。导入一个 JSON 主题包,或内置皮肤/主题包自动显示在这里。",
357
+ "bg2.title": "高级壁纸(URL / 渐变)",
358
+ "bg2.local": "本地图片",
359
+ "bg2.url": "图片链接",
360
+ "bg2.gradient": "渐变",
361
+ "bg2.apply": "应用链接",
362
+ "bg2.autodim": "自动弱化(聚焦任务时不喧宾夺主)",
363
+ "bg2.remove": "清除壁纸"
364
+ };
365
+
366
+ /** English dictionary, checked complete against the zh key set. */
367
+ const en = {
368
+ "skin.title": "Skins (Mirage)",
369
+ "skin.default": "Default",
370
+ "skin.abyss": "Abyss",
371
+ "skin.aurora": "Aurora",
372
+ "skin.nebula": "Nebula",
373
+ "skin.ember": "Ember",
374
+ "skin.midnight": "Midnight",
375
+ "skin.ivory": "Ivory",
376
+ "skin.mist": "Mist",
377
+ "skin.rose": "Rose",
378
+ "background.title": "Wallpaper",
379
+ "background.choose": "Choose image",
380
+ "background.remove": "Remove",
381
+ "background.opacity": "Opacity",
382
+ "background.blur": "Blur",
383
+ "background.hint": "The image shows through the translucent main canvas and sidebar; inner surfaces stay opaque for readability",
384
+ "accent.title": "Accent",
385
+ "accent.random": "Random",
386
+ "accent.clear": "Reset to theme",
387
+ "accent.hint": "Set a custom accent color for the active skin (an override layer — the skin itself is untouched). Reset to return to the skin's default accent.",
388
+ "packs.title": "Theme Packs (local)",
389
+ "packs.import": "Import pack…",
390
+ "packs.share": "Copy share link",
391
+ "packs.apply": "Apply",
392
+ "packs.surprise": "Surprise me",
393
+ "packs.empty": "No packs yet. Import a JSON theme pack, or built-in skins appear here.",
394
+ "bg2.title": "Advanced Wallpaper (URL / gradient)",
395
+ "bg2.local": "Local image",
396
+ "bg2.url": "Image URL",
397
+ "bg2.gradient": "Gradient",
398
+ "bg2.apply": "Apply link",
399
+ "bg2.autodim": "Auto-dim (gently fade while focusing tasks)",
400
+ "bg2.remove": "Clear wallpaper"
401
+ };
402
+ //#endregion
403
+
404
+ //#region dsh-dream-skin: persistence
405
+ /** Read a localStorage string value (null on absence or error). */
406
+ function readStorage(key) {
407
+ try {
408
+ const value = window.localStorage.getItem(key);
409
+ return typeof value === "string" ? value : null;
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+
415
+ /** Write (or remove with null) a localStorage value. */
416
+ function writeStorage(key, value) {
417
+ try {
418
+ if (value === null) window.localStorage.removeItem(key);
419
+ else window.localStorage.setItem(key, value);
420
+ } catch {
421
+ // storage unavailable / quota — the preference stays process-local
422
+ }
423
+ }
424
+
425
+ /** Saved skin id (may be unknown/absent). */
426
+ function readSavedSkin() {
427
+ return readStorage(STORAGE_KEY);
428
+ }
429
+
430
+ /** Persist a skin choice; DEFAULT_SKIN clears the stored value. */
431
+ function writeSavedSkin(id) {
432
+ writeStorage(STORAGE_KEY, id === DEFAULT_SKIN ? null : id);
433
+ }
434
+
435
+ /** Wallpaper data URL (null when unset). */
436
+ function readWallpaper() {
437
+ const value = readStorage(WALLPAPER_KEY);
438
+ return value !== null && value.length > 0 ? value : null;
439
+ }
440
+
441
+ /** Wash opacity 0..1 (clamped; default when unset). */
442
+ function readWallpaperOpacity() {
443
+ const raw = readStorage(WALLPAPER_OPACITY_KEY);
444
+ if (raw === null) return DEFAULT_WALLPAPER_OPACITY;
445
+ const value = Number(raw);
446
+ return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : DEFAULT_WALLPAPER_OPACITY;
447
+ }
448
+
449
+ /** Blur radius in px (clamped to 0..60; default when unset). */
450
+ function readWallpaperBlur() {
451
+ const raw = readStorage(WALLPAPER_BLUR_KEY);
452
+ if (raw === null) return DEFAULT_WALLPAPER_BLUR;
453
+ const value = Number(raw);
454
+ return Number.isFinite(value) ? Math.min(60, Math.max(0, value)) : DEFAULT_WALLPAPER_BLUR;
455
+ }
456
+ //#endregion
457
+
458
+ //#region dsh-dream-skin: wallpaper layer + token shading
459
+ /** The fixed backdrop layer (z-index -1), created lazily. */
460
+ let wallpaperEl = null;
461
+ /** Disposer for the current token-override layer. */
462
+ let wallpaperOverrideDispose = null;
463
+
464
+ /** Parse a hex or rgb()/rgba() color into rgba() with the given alpha. */
465
+ function toRgba(color, alpha) {
466
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());
467
+ if (hex !== null) {
468
+ let digits = hex[1];
469
+ if (digits.length === 3) digits = digits.split("").map((char) => char + char).join("");
470
+ const n = parseInt(digits, 16);
471
+ return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
472
+ }
473
+ const rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(color.trim());
474
+ if (rgb !== null) return `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})`;
475
+ return color.trim();
476
+ }
477
+
478
+ /**
479
+ * The base color for one scheme: the active skin's `--dsw-alias-bg-base`
480
+ * when it owns that scheme, otherwise the built-in base. The wash always
481
+ * carries the active skin's tint (and re-shades on theme/change).
482
+ */
483
+ function resolveBase(scheme, active) {
484
+ if (active.colorScheme === scheme && typeof active.tokens["--dsw-alias-bg-base"] === "string") {
485
+ return active.tokens["--dsw-alias-bg-base"];
486
+ }
487
+ return BUILTIN_BASE[scheme];
488
+ }
489
+
490
+ /**
491
+ * Stack the wallpaper's token override layer: the main canvas
492
+ * (--dsw-alias-bg-base) and the sidebar (--dsw-specific-sidebar-fill)
493
+ * become translucent at the configured opacity, so the fixed backdrop
494
+ * shows through while inner surfaces (cards, inputs, bubbles) stay
495
+ * opaque and readable. Re-calling with the same source replaces the
496
+ * whole layer (per the ThemeRuntime contract — note override tokens must
497
+ * be { light, dark } pairs, unlike registered-theme tokens which are
498
+ * scalar strings).
499
+ */
500
+ function shadeTokens(ctx) {
501
+ const snapshot = ctx.theme.getTheme();
502
+ const alpha = readWallpaperOpacity();
503
+ const sidebarAlpha = Math.min(1, alpha + 0.1);
504
+ const overrides = {
505
+ "--dsw-alias-bg-base": {
506
+ light: toRgba(resolveBase("light", snapshot.active), alpha),
507
+ dark: toRgba(resolveBase("dark", snapshot.active), alpha)
508
+ },
509
+ "--dsw-specific-sidebar-fill": {
510
+ light: toRgba(resolveBase("light", snapshot.active), sidebarAlpha),
511
+ dark: toRgba(resolveBase("dark", snapshot.active), sidebarAlpha)
512
+ }
513
+ };
514
+ wallpaperOverrideDispose?.();
515
+ wallpaperOverrideDispose = ctx.theme.overrideTokens(OVERRIDE_SOURCE, overrides);
516
+ }
517
+
518
+ /** Apply (or clear) the wallpaper layer and its token shading. */
519
+ function applyWallpaper(ctx) {
520
+ const url = readWallpaper();
521
+ if (url === null) {
522
+ wallpaperEl?.remove();
523
+ wallpaperEl = null;
524
+ wallpaperOverrideDispose?.();
525
+ wallpaperOverrideDispose = null;
526
+ return;
527
+ }
528
+ if (wallpaperEl === null || !document.body.contains(wallpaperEl)) {
529
+ wallpaperEl = document.createElement("div");
530
+ wallpaperEl.style.cssText = "position:fixed;inset:0;z-index:-1;pointer-events:none;background-size:cover;background-position:center;background-repeat:no-repeat;";
531
+ document.body.prepend(wallpaperEl);
532
+ }
533
+ const blur = readWallpaperBlur();
534
+ wallpaperEl.style.backgroundImage = `url("${url}")`;
535
+ wallpaperEl.style.filter = blur > 0 ? `blur(${blur}px)` : "none";
536
+ shadeTokens(ctx);
537
+ }
538
+
539
+ /** Remove the wallpaper layer and its token overrides (fiber unload). */
540
+ function teardownWallpaper() {
541
+ wallpaperEl?.remove();
542
+ wallpaperEl = null;
543
+ wallpaperOverrideDispose?.();
544
+ wallpaperOverrideDispose = null;
545
+ }
546
+ //#endregion
547
+
548
+ //#region dsh-dream-skin: image compression
549
+ /**
550
+ * Downscale an image onto a canvas and return a JPEG data URL, so a
551
+ * wallpaper stays well inside the localStorage quota (≤ ~2MB).
552
+ */
553
+ function compressImage(image, maxSide, quality) {
554
+ const scale = Math.min(1, maxSide / Math.max(image.width, image.height));
555
+ const canvas = document.createElement("canvas");
556
+ canvas.width = Math.max(1, Math.round(image.width * scale));
557
+ canvas.height = Math.max(1, Math.round(image.height * scale));
558
+ const context = canvas.getContext("2d");
559
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
560
+ return canvas.toDataURL("image/jpeg", quality);
561
+ }
562
+
563
+ /** Read a picked file into a compressed data URL (null on failure). */
564
+ function readImageAsDataUrl(file, onDone) {
565
+ const reader = new FileReader();
566
+ reader.onerror = () => onDone(null);
567
+ reader.onload = () => {
568
+ const image = new Image();
569
+ image.onerror = () => onDone(null);
570
+ image.onload = () => {
571
+ try {
572
+ let dataUrl = compressImage(image, 1600, 0.75);
573
+ if (dataUrl.length > 2000000) dataUrl = compressImage(image, 1000, 0.6);
574
+ if (dataUrl.length > 2000000) dataUrl = compressImage(image, 800, 0.5);
575
+ onDone(dataUrl);
576
+ } catch {
577
+ onDone(null);
578
+ }
579
+ };
580
+ image.src = reader.result;
581
+ };
582
+ reader.readAsDataURL(file);
583
+ }
584
+ //#endregion
585
+
586
+ //#region dsh-dream-skin: settings row stores
587
+ /**
588
+ * Skin row slot store: a mirror of the theme service snapshot. The
589
+ * plugin's apply-world change listener is the only writer; the row
590
+ * component reads via props.useStore.
591
+ */
592
+ function createSkinStore() {
593
+ return (0, _runtime_client.defineStore)({
594
+ init: () => ({
595
+ skin: "system",
596
+ revision: -1
597
+ }),
598
+ actions: {
599
+ sync: (d, skin, revision) => {
600
+ if (revision <= d.revision) return;
601
+ d.skin = skin;
602
+ d.revision = revision;
603
+ }
604
+ }
605
+ });
606
+ }
607
+
608
+ /** Wallpaper row store: url + opacity + blur, written only by this plugin. */
609
+ function createWallpaperStore() {
610
+ return (0, _runtime_client.defineStore)({
611
+ init: () => ({
612
+ url: null,
613
+ opacity: DEFAULT_WALLPAPER_OPACITY,
614
+ blur: DEFAULT_WALLPAPER_BLUR,
615
+ revision: -1
616
+ }),
617
+ actions: {
618
+ sync: (d, url, opacity, blur, revision) => {
619
+ if (revision <= d.revision) return;
620
+ d.url = url;
621
+ d.opacity = opacity;
622
+ d.blur = blur;
623
+ d.revision = revision;
624
+ }
625
+ }
626
+ });
627
+ }
628
+ //#endregion
629
+
630
+ //#region dsh-dream-skin: settings rows
631
+ /** Inline style sheet for the rows (kept dependency-free). */
632
+ const styles = {
633
+ group: {
634
+ borderBottom: "1px solid var(--dsw-alias-border-l2)",
635
+ display: "flex",
636
+ flexDirection: "column",
637
+ gap: "10px",
638
+ padding: "16px 0"
639
+ },
640
+ title: {
641
+ color: "var(--dsw-alias-label-primary)",
642
+ fontSize: "14px",
643
+ fontWeight: 400,
644
+ lineHeight: "22px"
645
+ },
646
+ hint: {
647
+ color: "var(--dsw-alias-label-tertiary)",
648
+ fontSize: "12px",
649
+ lineHeight: "18px"
650
+ },
651
+ grid: {
652
+ display: "flex",
653
+ flexWrap: "wrap",
654
+ gap: "10px"
655
+ },
656
+ card: {
657
+ display: "flex",
658
+ flexDirection: "column",
659
+ alignItems: "center",
660
+ gap: "6px",
661
+ width: "96px",
662
+ padding: "3px",
663
+ borderRadius: "10px",
664
+ border: "2px solid transparent",
665
+ background: "transparent",
666
+ cursor: "pointer",
667
+ font: "inherit",
668
+ boxSizing: "border-box"
669
+ },
670
+ cardSelected: {
671
+ borderColor: "var(--dsw-alias-brand-primary)",
672
+ background: "var(--dsw-alias-interactive-bg-hover)"
673
+ },
674
+ cardLabel: {
675
+ color: "var(--dsw-alias-label-secondary)",
676
+ fontSize: "12px",
677
+ lineHeight: "16px",
678
+ whiteSpace: "nowrap"
679
+ },
680
+ cardLabelSelected: {
681
+ color: "var(--dsw-alias-label-primary)"
682
+ },
683
+ swatch: {
684
+ width: "100%",
685
+ height: "52px",
686
+ borderRadius: "8px",
687
+ boxSizing: "border-box",
688
+ padding: "8px",
689
+ display: "flex",
690
+ flexDirection: "column",
691
+ justifyContent: "center",
692
+ gap: "6px"
693
+ },
694
+ swatchLine: {
695
+ height: "7px",
696
+ borderRadius: "4px"
697
+ },
698
+ defaultSwatch: {
699
+ width: "100%",
700
+ height: "52px",
701
+ borderRadius: "8px",
702
+ boxSizing: "border-box",
703
+ display: "flex",
704
+ overflow: "hidden",
705
+ border: "1px solid var(--dsw-alias-border-l2)"
706
+ },
707
+ button: {
708
+ height: "32px",
709
+ padding: "0 14px",
710
+ borderRadius: "8px",
711
+ border: "1px solid var(--dsw-alias-border-l2)",
712
+ background: "var(--dsw-alias-button-elevated-fill)",
713
+ color: "var(--dsw-alias-label-primary)",
714
+ cursor: "pointer",
715
+ fontSize: "13px",
716
+ font: "inherit",
717
+ boxSizing: "border-box"
718
+ },
719
+ buttonDanger: {
720
+ color: "var(--dsw-alias-state-error-primary)"
721
+ },
722
+ tinyButton: {
723
+ height: "22px",
724
+ padding: "0 8px",
725
+ borderRadius: "6px",
726
+ border: "1px solid var(--dsw-alias-border-l2)",
727
+ background: "transparent",
728
+ color: "var(--dsw-alias-label-secondary)",
729
+ cursor: "pointer",
730
+ font: "inherit",
731
+ fontSize: "11px",
732
+ lineHeight: "16px"
733
+ },
734
+ tinyButtonActive: {
735
+ color: "var(--dsw-alias-brand-primary)",
736
+ borderColor: "var(--dsw-alias-brand-primary)"
737
+ },
738
+ urlInput: {
739
+ flex: 1,
740
+ minWidth: "220px",
741
+ height: "32px",
742
+ padding: "0 10px",
743
+ borderRadius: "8px",
744
+ border: "1px solid var(--dsw-alias-border-l2)",
745
+ background: "var(--dsw-alias-bg-layer-1)",
746
+ color: "var(--dsw-alias-label-primary)",
747
+ font: "inherit",
748
+ fontSize: "13px",
749
+ boxSizing: "border-box"
750
+ },
751
+ presetswatches: {
752
+ width: "48px",
753
+ height: "32px",
754
+ borderRadius: "8px",
755
+ border: "1px solid var(--dsw-alias-border-l2)",
756
+ cursor: "pointer",
757
+ padding: 0
758
+ },
759
+ checkbox: {
760
+ accentColor: "var(--dsw-alias-brand-primary)",
761
+ width: "16px",
762
+ height: "16px"
763
+ },
764
+ preview: {
765
+ width: "72px",
766
+ height: "44px",
767
+ objectFit: "cover",
768
+ borderRadius: "6px",
769
+ border: "1px solid var(--dsw-alias-border-l2)"
770
+ },
771
+ actionRow: {
772
+ display: "flex",
773
+ alignItems: "center",
774
+ gap: "10px",
775
+ flexWrap: "wrap"
776
+ },
777
+ sliderRow: {
778
+ display: "flex",
779
+ alignItems: "center",
780
+ gap: "10px",
781
+ minWidth: "240px"
782
+ },
783
+ sliderLabel: {
784
+ color: "var(--dsw-alias-label-secondary)",
785
+ fontSize: "13px",
786
+ whiteSpace: "nowrap",
787
+ width: "52px"
788
+ },
789
+ slider: {
790
+ flex: 1,
791
+ accentColor: "var(--dsw-alias-brand-primary)"
792
+ },
793
+ sliderValue: {
794
+ color: "var(--dsw-alias-label-secondary)",
795
+ fontSize: "12px",
796
+ whiteSpace: "nowrap",
797
+ width: "44px",
798
+ textAlign: "right"
799
+ }
800
+ };
801
+
802
+ /** Mini palette preview driven by one skin's token table. */
803
+ function Swatch({ tokens }) {
804
+ return (0, react_jsx_runtime.jsxs)("div", {
805
+ style: {
806
+ ...styles.swatch,
807
+ background: tokens["--dsw-alias-bg-layer-1"],
808
+ border: `1px solid ${tokens["--dsw-alias-border-l2"]}`
809
+ },
810
+ children: [
811
+ (0, react_jsx_runtime.jsx)("div", {
812
+ style: {
813
+ ...styles.swatchLine,
814
+ width: "70%",
815
+ background: tokens["--dsw-alias-label-primary"],
816
+ opacity: 0.85
817
+ }
818
+ }),
819
+ (0, react_jsx_runtime.jsx)("div", {
820
+ style: {
821
+ ...styles.swatchLine,
822
+ width: "45%",
823
+ background: tokens["--dsw-alias-brand-primary"]
824
+ }
825
+ }),
826
+ (0, react_jsx_runtime.jsx)("div", {
827
+ style: {
828
+ ...styles.swatchLine,
829
+ width: "55%",
830
+ background: tokens["--dsw-alias-label-secondary"],
831
+ opacity: 0.55
832
+ }
833
+ })
834
+ ]
835
+ });
836
+ }
837
+
838
+ /** "Default" chip: follow the built-in appearance (light + dark halves). */
839
+ function DefaultSwatch() {
840
+ return (0, react_jsx_runtime.jsxs)("div", {
841
+ style: styles.defaultSwatch,
842
+ children: [
843
+ (0, react_jsx_runtime.jsx)("div", { style: { flex: 1, background: "#f4f4f5" } }),
844
+ (0, react_jsx_runtime.jsx)("div", { style: { flex: 1, background: "#1c1c20" } })
845
+ ]
846
+ });
847
+ }
848
+
849
+ /** One selectable skin card. */
850
+ function SkinCard({ skin, selected, onSelect, t }) {
851
+ return (0, react_jsx_runtime.jsxs)("button", {
852
+ type: "button",
853
+ onClick: onSelect,
854
+ "aria-pressed": selected,
855
+ style: {
856
+ ...styles.card,
857
+ ...(selected ? styles.cardSelected : {})
858
+ },
859
+ children: [
860
+ (0, react_jsx_runtime.jsx)(Swatch, { tokens: skin.tokens }),
861
+ (0, react_jsx_runtime.jsx)("span", {
862
+ style: {
863
+ ...styles.cardLabel,
864
+ ...(selected ? styles.cardLabelSelected : {})
865
+ },
866
+ children: t(`skin.${skin.id}`)
867
+ })
868
+ ]
869
+ });
870
+ }
871
+
872
+ /**
873
+ * Skin picker row registered into the Settings → General item slot,
874
+ * right after the built-in Appearance row: title + a "Default" chip and
875
+ * one swatch card per curated skin.
876
+ */
877
+ function SkinRow({ t, setSkin, useStore }) {
878
+ const skin = useStore((s) => s.skin);
879
+ const selected = SKINS.some((candidate) => candidate.id === skin) ? skin : null;
880
+ return (0, react_jsx_runtime.jsxs)("div", {
881
+ style: styles.group,
882
+ children: [
883
+ (0, react_jsx_runtime.jsx)("div", {
884
+ style: styles.title,
885
+ children: t("skin.title")
886
+ }),
887
+ (0, react_jsx_runtime.jsxs)("div", {
888
+ style: styles.grid,
889
+ children: [
890
+ (0, react_jsx_runtime.jsxs)("button", {
891
+ type: "button",
892
+ onClick: () => setSkin(DEFAULT_SKIN),
893
+ "aria-pressed": selected === null,
894
+ style: {
895
+ ...styles.card,
896
+ ...(selected === null ? styles.cardSelected : {})
897
+ },
898
+ children: [
899
+ (0, react_jsx_runtime.jsx)(DefaultSwatch, {}),
900
+ (0, react_jsx_runtime.jsx)("span", {
901
+ style: {
902
+ ...styles.cardLabel,
903
+ ...(selected === null ? styles.cardLabelSelected : {})
904
+ },
905
+ children: t("skin.default")
906
+ })
907
+ ]
908
+ }),
909
+ SKINS.map((skinDefinition) => (0, react_jsx_runtime.jsx)(SkinCard, {
910
+ skin: skinDefinition,
911
+ selected: selected === skinDefinition.id,
912
+ onSelect: () => setSkin(skinDefinition.id),
913
+ t
914
+ }, skinDefinition.id))
915
+ ]
916
+ })
917
+ ]
918
+ });
919
+ }
920
+
921
+ /** One labeled slider (opacity or blur). */
922
+ function Slider({ label, value, min, max, step, format, onChange }) {
923
+ return (0, react_jsx_runtime.jsxs)("div", {
924
+ style: styles.sliderRow,
925
+ children: [
926
+ (0, react_jsx_runtime.jsx)("span", {
927
+ style: styles.sliderLabel,
928
+ children: label
929
+ }),
930
+ (0, react_jsx_runtime.jsx)("input", {
931
+ type: "range",
932
+ min,
933
+ max,
934
+ step,
935
+ value,
936
+ style: styles.slider,
937
+ onChange: (event) => onChange(Number(event.target.value))
938
+ }),
939
+ (0, react_jsx_runtime.jsx)("span", {
940
+ style: styles.sliderValue,
941
+ children: format(value)
942
+ })
943
+ ]
944
+ });
945
+ }
946
+
947
+ /**
948
+ * Wallpaper row: choose (compressed to a data URL), preview, tune the
949
+ * wash opacity and blur, and remove the wallpaper.
950
+ */
951
+ function WallpaperRow({ t, setWallpaper, setOpacity, setBlur, useStore }) {
952
+ const url = useStore((s) => s.url);
953
+ const opacity = useStore((s) => s.opacity);
954
+ const blur = useStore((s) => s.blur);
955
+ const inputRef = (0, _react.useRef)(null);
956
+ const onPick = () => inputRef.current?.click();
957
+ const onFile = (event) => {
958
+ const file = event.target.files?.[0];
959
+ if (file === void 0) return;
960
+ readImageAsDataUrl(file, (dataUrl) => {
961
+ if (dataUrl !== null) setWallpaper(dataUrl);
962
+ event.target.value = "";
963
+ });
964
+ };
965
+ return (0, react_jsx_runtime.jsxs)("div", {
966
+ style: styles.group,
967
+ children: [
968
+ (0, react_jsx_runtime.jsx)("div", {
969
+ style: styles.title,
970
+ children: t("background.title")
971
+ }),
972
+ (0, react_jsx_runtime.jsxs)("div", {
973
+ style: styles.actionRow,
974
+ children: [
975
+ url !== null ? (0, react_jsx_runtime.jsx)("img", {
976
+ src: url,
977
+ alt: "",
978
+ style: styles.preview
979
+ }) : null,
980
+ (0, react_jsx_runtime.jsx)("button", {
981
+ type: "button",
982
+ style: styles.button,
983
+ onClick: onPick,
984
+ children: t("background.choose")
985
+ }),
986
+ url !== null ? (0, react_jsx_runtime.jsx)("button", {
987
+ type: "button",
988
+ style: {
989
+ ...styles.button,
990
+ ...styles.buttonDanger
991
+ },
992
+ onClick: () => setWallpaper(null),
993
+ children: t("background.remove")
994
+ }) : null,
995
+ (0, react_jsx_runtime.jsx)("input", {
996
+ ref: inputRef,
997
+ type: "file",
998
+ accept: "image/*",
999
+ style: { display: "none" },
1000
+ onChange: onFile
1001
+ })
1002
+ ]
1003
+ }),
1004
+ (0, react_jsx_runtime.jsx)(Slider, {
1005
+ label: t("background.opacity"),
1006
+ value: Math.round(opacity * 100),
1007
+ min: 0,
1008
+ max: 100,
1009
+ step: 1,
1010
+ format: (v) => `${v}%`,
1011
+ onChange: setOpacity
1012
+ }),
1013
+ (0, react_jsx_runtime.jsx)(Slider, {
1014
+ label: t("background.blur"),
1015
+ value: blur,
1016
+ min: 0,
1017
+ max: 60,
1018
+ step: 1,
1019
+ format: (v) => `${v}px`,
1020
+ onChange: setBlur
1021
+ }),
1022
+ (0, react_jsx_runtime.jsx)("div", {
1023
+ style: styles.hint,
1024
+ children: t("background.hint")
1025
+ })
1026
+ ]
1027
+ });
1028
+ }
1029
+
1030
+ /**
1031
+ * Advanced wallpaper row (P0-3): a URL or gradient preset as the backdrop
1032
+ * instead of a local image, plus an auto-dim toggle. Kept separate from
1033
+ * the image row so the two workflows don't fight over the same preview.
1034
+ */
1035
+ function WallpaperAdvancedRow({ t, useStore, setKind, setUrl, setGradient, setAutodim, clearAll }) {
1036
+ const kind = useStore((s) => s.kind);
1037
+ const url = useStore((s) => s.url);
1038
+ const gradient = useStore((s) => s.gradient);
1039
+ const autodim = useStore((s) => s.autodim);
1040
+ const urlState = (0, _react.useState)("");
1041
+ const urlValue = urlState[0];
1042
+ const setUrlValue = urlState[1];
1043
+ const KIND_OPTIONS = [
1044
+ { id: "image", label: t("bg2.local") },
1045
+ { id: "url", label: t("bg2.url") },
1046
+ { id: "gradient", label: t("bg2.gradient") }
1047
+ ];
1048
+ const GRADS = [
1049
+ "linear-gradient(135deg, #0b1120 0%, #172554 55%, #1e3a8a 100%)",
1050
+ "linear-gradient(135deg, #022c22 0%, #0d9488 100%)",
1051
+ "linear-gradient(135deg, #1e1b4b 0%, #7e22ce 100%)",
1052
+ "linear-gradient(135deg, #251607 0%, #c2410c 100%)",
1053
+ "linear-gradient(135deg, #faf5eb 0%, #e7dfcb 100%)",
1054
+ "linear-gradient(135deg, #fdf2f6 0%, #f0d2dc 100%)"
1055
+ ];
1056
+ return (0, react_jsx_runtime.jsxs)("div", {
1057
+ style: styles.group,
1058
+ children: [
1059
+ (0, react_jsx_runtime.jsx)("div", {
1060
+ style: styles.title,
1061
+ children: t("bg2.title")
1062
+ }),
1063
+ (0, react_jsx_runtime.jsxs)("div", {
1064
+ style: styles.actionRow,
1065
+ children: KIND_OPTIONS.map((opt) => (0, react_jsx_runtime.jsxs)("button", {
1066
+ type: "button",
1067
+ "aria-pressed": kind === opt.id,
1068
+ style: {
1069
+ ...styles.tinyButton,
1070
+ ...(kind === opt.id ? styles.tinyButtonActive : {})
1071
+ },
1072
+ onClick: () => setKind(opt.id),
1073
+ children: [opt.label]
1074
+ }, opt.id))
1075
+ }),
1076
+ kind === "url" ? (0, react_jsx_runtime.jsxs)("div", {
1077
+ style: styles.actionRow,
1078
+ children: [
1079
+ (0, react_jsx_runtime.jsx)("input", {
1080
+ type: "url",
1081
+ placeholder: "https://example.com/wall.jpg",
1082
+ defaultValue: url || "",
1083
+ style: { ...styles.urlInput },
1084
+ onChange: (event) => setUrlValue(event.target.value)
1085
+ }),
1086
+ (0, react_jsx_runtime.jsx)("button", {
1087
+ type: "button",
1088
+ style: styles.button,
1089
+ onClick: () => setUrl(urlValue),
1090
+ children: t("bg2.apply")
1091
+ })
1092
+ ]
1093
+ }) : null,
1094
+ kind === "gradient" ? (0, react_jsx_runtime.jsxs)("div", {
1095
+ style: styles.grid,
1096
+ children: GRADS.map((g) => (0, react_jsx_runtime.jsx)("button", {
1097
+ type: "button",
1098
+ "aria-pressed": gradient === g,
1099
+ style: {
1100
+ ...styles.presetswatches,
1101
+ ...(gradient === g ? { outline: "2px solid var(--dsw-alias-brand-primary)" } : {})
1102
+ },
1103
+ onClick: () => setGradient(g),
1104
+ children: null
1105
+ }, g))
1106
+ }) : null,
1107
+ (0, react_jsx_runtime.jsxs)("div", {
1108
+ style: styles.actionRow,
1109
+ children: [
1110
+ (0, react_jsx_runtime.jsx)("input", {
1111
+ type: "checkbox",
1112
+ checked: autodim,
1113
+ style: styles.checkbox,
1114
+ onChange: (event) => setAutodim(event.target.checked)
1115
+ }),
1116
+ (0, react_jsx_runtime.jsx)("span", {
1117
+ style: { color: "var(--dsw-alias-label-secondary)", fontSize: "13px" },
1118
+ children: t("bg2.autodim")
1119
+ })
1120
+ ]
1121
+ }),
1122
+ (0, react_jsx_runtime.jsx)("button", {
1123
+ type: "button",
1124
+ style: { ...styles.button, ...styles.buttonDanger },
1125
+ onClick: clearAll,
1126
+ children: t("bg2.remove")
1127
+ })
1128
+ ]
1129
+ });
1130
+ }
1131
+ //#endregion
1132
+
1133
+ //#region dsh-dream-skin: P0 shared utilities (packs, accent, persistence, random)
1134
+ /**
1135
+ * P0 feature layer: theme-pack import/export, per-user accent override,
1136
+ * wallpaper 2.0, dual persistence, a local theme-pack library with
1137
+ * one-click apply + validation + rollback, and surprise-me / favorites.
1138
+ *
1139
+ * Constraint note: DSH's Host settings wire only exposes an allowlisted
1140
+ * set of namespaces to browser clients (WEB_SETTINGS_NAMESPACES in
1141
+ * dsh-host-apiproxy), so a third-party namespace answers
1142
+ * `settings-not-exposed` even when registered. localStorage/IndexedDB are
1143
+ * therefore the reliable persistence for third-party state; a host
1144
+ * settings write is attempted best-effort and never depended on.
1145
+ */
1146
+
1147
+ /** Pack manifest format marker. */
1148
+ const PACK_FORMAT = "dsh-dream-skin/pack";
1149
+ /** Current pack manifest version. */
1150
+ const PACK_VERSION = 1;
1151
+ /** Size cap for an imported pack JSON (≈1 MiB). */
1152
+ const PACK_MAX_BYTES = 1024 * 1024;
1153
+ /** localStorage keys for P0 state. */
1154
+ const PACKS_KEY = "dsh-dream-skin:packs"; // JSON array of remote/manual pack manifests
1155
+ const ACCENT_KEY = "dsh-dream-skin:accent"; // hex accent (#rrggbb) or "system"
1156
+ const FAVORITES_KEY = "dsh-dream-skin:favorites"; // JSON array of theme/ pack ids
1157
+ const WALLPAPER_URL_KEY = "dsh-dream-skin:wallpaper-url";
1158
+ const WALLPAPER_KIND_KEY = "dsh-dream-skin:wallpaper-kind"; // 'image'|'url'|'gradient'
1159
+ const WALLPAPER_GRADIENT_KEY = "dsh-dream-skin:wallpaper-gradient";
1160
+ const WALLPAPER_AUTODIM_KEY = "dsh-dream-skin:wallpaper-autodim"; // '1'|'0'
1161
+ /** Sentinel meaning "no accent override — follow the theme's own accent". */
1162
+ const DEFAULT_ACCENT = "system";
1163
+ /** Marker for a skin that is actually a user-imported pack. */
1164
+ const PACK_ID_PREFIX = "dream-pack:";
1165
+
1166
+ /**
1167
+ * Minimum token set a pack must define so it renders coherently.
1168
+ * See docs/theme-spec.md for the full token contract. These are the
1169
+ * core surfaces; missing others fall back to (or are shimmed from) these.
1170
+ */
1171
+ const PACK_REQUIRED_TOKENS = [
1172
+ "--dsw-alias-bg-base",
1173
+ "--dsw-alias-bg-layer-1",
1174
+ "--dsw-alias-brand-primary",
1175
+ "--dsw-alias-label-primary",
1176
+ "--dsw-alias-label-secondary",
1177
+ "--dsw-alias-border-l1",
1178
+ "--dsw-alias-border-l2"
1179
+ ];
1180
+
1181
+ /** Regex for a 3/6-digit hex color. */
1182
+ const HEX_RE = /^#[\da-f]{3}(?:[\da-f]{3})?$/i;
1183
+
1184
+ /** true when a value is a syntactically plausible CSS color. */
1185
+ function looksLikeColor(value) {
1186
+ return typeof value === "string" && (HEX_RE.test(value.trim()) || /^(rgb|rgba|hsl|hsla)\(/.test(value.trim()));
1187
+ }
1188
+
1189
+ /** Normalize a hex to #rrggbb lowercase, or null. */
1190
+ function normalizeHex(value) {
1191
+ const m = HEX_RE.exec(String(value ?? "").trim());
1192
+ if (!m) return null;
1193
+ let hex = m[0].toLowerCase();
1194
+ if (hex.length === 4) hex = "#" + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
1195
+ return hex;
1196
+ }
1197
+
1198
+ /**
1199
+ * Validate a parsed pack manifest structure. Returns a { ok, errors }
1200
+ * result WITHOUT mutating it. On ok, the caller receives a normalized copy
1201
+ * with a guaranteed stable `id` and a merged full token table.
1202
+ */
1203
+ function validatePack(data) {
1204
+ if (typeof data !== "object" || data === null) return { ok: false, errors: ["not an object"] };
1205
+ if (data.format !== PACK_FORMAT) return { ok: false, errors: [`format must be "${PACK_FORMAT}"`] };
1206
+ if (data.version !== PACK_VERSION) return { ok: false, errors: [`unsupported pack version ${data.version}`] };
1207
+ const manifest = data.manifest;
1208
+ if (typeof manifest !== "object" || manifest === null) return { ok: false, errors: ["missing manifest"] };
1209
+ if (typeof manifest.id !== "string" || !manifest.id.trim()) return { ok: false, errors: ["manifest.id is required"] };
1210
+ const id = PACK_ID_PREFIX + manifest.id;
1211
+ if (id === "system" || id === "light" || id === "dark") return { ok: false, errors: [`"${manifest.id}" collides with a reserved id`] };
1212
+ if (typeof manifest.name !== "string" || !manifest.name.trim()) return { ok: false, errors: ["manifest.name is required"] };
1213
+ if (manifest.colorScheme !== "light" && manifest.colorScheme !== "dark") return { ok: false, errors: [`colorScheme must be light|dark, got ${manifest.colorScheme}`] };
1214
+ if (typeof manifest.tokens !== "object" || manifest.tokens === null) return { ok: false, errors: ["manifest.tokens is required"] };
1215
+ const tokens = {};
1216
+ const errors = [];
1217
+ for (const name of PACK_REQUIRED_TOKENS) {
1218
+ const value = manifest.tokens[name];
1219
+ if (typeof value !== "string" || !looksLikeColor(value)) errors.push(`token ${name} is missing or not a color`);
1220
+ else tokens[name] = value;
1221
+ }
1222
+ // Copy the remaining user-supplied tokens (already owned/validated colors).
1223
+ for (const [name, value] of Object.entries(manifest.tokens)) {
1224
+ if (!(name in tokens) && typeof value === "string" && looksLikeColor(value)) tokens[name] = value;
1225
+ }
1226
+ const accent = manifest.accent ? normalizeHex(manifest.accent) : null;
1227
+ const pack = {
1228
+ format: PACK_FORMAT,
1229
+ version: PACK_VERSION,
1230
+ manifest: {
1231
+ id: manifest.id,
1232
+ name: manifest.name,
1233
+ nameZh: typeof manifest.nameZh === "string" ? manifest.nameZh : undefined,
1234
+ author: typeof manifest.author === "string" ? manifest.author : "anonymous",
1235
+ version: typeof manifest.version === "string" ? manifest.version : "1.0.0",
1236
+ description: typeof manifest.description === "string" ? manifest.description : "",
1237
+ colorScheme: manifest.colorScheme,
1238
+ tokens,
1239
+ accent
1240
+ }
1241
+ };
1242
+ if (errors.length) return { ok: false, errors };
1243
+ return { ok: true, id, pack };
1244
+ }
1245
+
1246
+ /** Turn a validated pack manifest into a ThemeRegistration for the runtime. */
1247
+ function packToRegistration(pack) {
1248
+ return Object.freeze({
1249
+ id: PACK_ID_PREFIX + pack.manifest.id,
1250
+ colorScheme: pack.manifest.colorScheme,
1251
+ tokens: { ...pack.manifest.tokens }
1252
+ });
1253
+ }
1254
+
1255
+ /**
1256
+ * In-process registry of imported packs. Kept outside React/localStorage
1257
+ * so a pack can be registered into ctx.theme immediately on import and
1258
+ * re-registered on reload without waiting for a slot mount.
1259
+ */
1260
+ const importedPacks = [];
1261
+ /** Disposers for every pack we registered into ctx.theme, keyed by id. */
1262
+ const packDisposers = new Map();
1263
+
1264
+ /** Register or refresh one pack into the theme runtime (idempotent). */
1265
+ function applyPackToTheme(ctx, id, registration) {
1266
+ const existing = packDisposers.get(id);
1267
+ if (existing) {
1268
+ existing(); // dispose old layer → theme reset if it was active
1269
+ packDisposers.delete(id);
1270
+ }
1271
+ packDisposers.set(id, ctx.theme.register(registration));
1272
+ }
1273
+
1274
+ /** Dispose all packs (on plugin unload). */
1275
+ function disposeAllPacks() {
1276
+ for (const dispose of packDisposers.values()) dispose();
1277
+ packDisposers.clear();
1278
+ importedPacks.length = 0;
1279
+ }
1280
+
1281
+ /** Read the persisted pack-manifest list. */
1282
+ function readPacks() {
1283
+ const raw = readStorage(PACKS_KEY);
1284
+ if (raw === null) return [];
1285
+ try {
1286
+ const parsed = JSON.parse(raw);
1287
+ return Array.isArray(parsed) ? parsed : [];
1288
+ } catch {
1289
+ return [];
1290
+ }
1291
+ }
1292
+
1293
+ /** Persist the pack-manifest list (removing any entry whose id is empty). */
1294
+ function writePacks(packs) {
1295
+ writeStorage(PACKS_KEY, JSON.stringify(packs.filter((p) => p && p.id)));
1296
+ }
1297
+
1298
+ /** Find a pack manifest by id. */
1299
+ function findPack(id) {
1300
+ return importedPacks.find((p) => p && p.id === id);
1301
+ }
1302
+
1303
+ /** Import a validated pack: register it, add to the in-process + persisted list. */
1304
+ function importPack(ctx, result) {
1305
+ const { id, pack } = result;
1306
+ if (findPack(id)) return { ok: false, error: "a pack with this id is already imported" };
1307
+ const registration = packToRegistration(pack);
1308
+ try {
1309
+ applyPackToTheme(ctx, id, registration);
1310
+ } catch (e) {
1311
+ return { ok: false, error: "register failed: " + (e && e.message ? e.message : String(e)) };
1312
+ }
1313
+ const record = { id, manifest: pack.manifest };
1314
+ importedPacks.push({ ...record, registration });
1315
+ const packs = readPacks();
1316
+ packs.push({ id, manifest: pack.manifest });
1317
+ writePacks(packs);
1318
+ return { ok: true, id, name: pack.manifest.name, colorScheme: pack.manifest.colorScheme };
1319
+ }
1320
+
1321
+ /** Remove an imported pack by id (falls back to built-in skin if it was active). */
1322
+ function unimportPack(ctx, id) {
1323
+ const idx = importedPacks.findIndex((p) => p && p.id === id);
1324
+ if (idx === -1) return;
1325
+ const [removed] = importedPacks.splice(idx, 1);
1326
+ const dispose = packDisposers.get(id);
1327
+ if (dispose) {
1328
+ dispose();
1329
+ packDisposers.delete(id);
1330
+ }
1331
+ const packs = readPacks().filter((p) => p.id !== id);
1332
+ writePacks(packs);
1333
+ // If the removed pack was active, fall back to the built-in appearance.
1334
+ if (ctx.theme.getTheme().preference === id) ctx.theme.setTheme(DEFAULT_SKIN);
1335
+ const favorites = readFavorites().filter((f) => f !== id);
1336
+ writeFavorites(favorites);
1337
+ return removed && removed.manifest ? removed.manifest.name : id;
1338
+ }
1339
+
1340
+ /** Re-register persisted packs on (re)load, before restoring the saved skin. */
1341
+ function restorePacks(ctx) {
1342
+ for (const record of readPacks()) {
1343
+ if (!record || !record.manifest || !record.manifest.tokens) continue;
1344
+ const validate = validatePack({ format: PACK_FORMAT, version: PACK_VERSION, manifest: record.manifest });
1345
+ if (!validate.ok) continue;
1346
+ const regression = packToRegistration(validate.pack);
1347
+ try {
1348
+ applyPackToTheme(ctx, validate.id, regression);
1349
+ importedPacks.push({ id: validate.id, manifest: validate.pack.manifest, registration: regression });
1350
+ } catch {
1351
+ // skip a pack that fails to re-register
1352
+ }
1353
+ }
1354
+ }
1355
+
1356
+ /** Export a pack as a downloadable JSON Blob (no server needed). */
1357
+ function exportPackAsFile(ctx, id) {
1358
+ const record = findPack(id);
1359
+ const source = record ? { format: PACK_FORMAT, version: PACK_VERSION, manifest: { ...record.manifest } }
1360
+ : null;
1361
+ if (!source) return false;
1362
+ const blob = new Blob([JSON.stringify(source, null, 2)], { type: "application/json" });
1363
+ const url = URL.createObjectURL(blob);
1364
+ const a = document.createElement("a");
1365
+ a.href = url;
1366
+ a.download = (source.manifest.name || id).toLowerCase().replace(/\s+/g, "-") + ".dsh-theme.json";
1367
+ document.body.appendChild(a);
1368
+ a.click();
1369
+ a.remove();
1370
+ URL.revokeObjectURL(url);
1371
+ return true;
1372
+ }
1373
+
1374
+ /** Encode a pack manifest into a shareable URL hash (fragment). */
1375
+ function packShareUrl(id) {
1376
+ const record = findPack(id);
1377
+ if (!record) return null;
1378
+ const payload = { format: PACK_FORMAT, version: PACK_VERSION, manifest: record.manifest };
1379
+ let encoded;
1380
+ try {
1381
+ encoded = btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
1382
+ } catch {
1383
+ return null;
1384
+ }
1385
+ return window.location.origin + window.location.pathname + "#dream-skin-pack=" + encoded;
1386
+ }
1387
+
1388
+ /** Decode a shared pack from a URL hash; null when absent/invalid. */
1389
+ function decodeShareUrl(hash) {
1390
+ const prefix = "#dream-skin-pack=";
1391
+ const idx = hash ? hash.indexOf(prefix) : -1;
1392
+ if (idx === -1) return null;
1393
+ const raw = hash.slice(idx + prefix.length);
1394
+ if (!raw) return null;
1395
+ try {
1396
+ const json = decodeURIComponent(escape(atob(raw)));
1397
+ const data = JSON.parse(json);
1398
+ const validate = validatePack(data);
1399
+ return validate.ok ? { id: validate.id, pack: validate.pack } : null;
1400
+ } catch {
1401
+ return null;
1402
+ }
1403
+ }
1404
+
1405
+ /** Pull a desired accent from the active skin/registration + pack accent. */
1406
+ function resolveAccent(snapshot) {
1407
+ const active = snapshot.active;
1408
+ const brand = active && active.tokens ? active.tokens["--dsw-alias-brand-primary"] : null;
1409
+ return typeof brand === "string" && looksLikeColor(brand) ? brand : null;
1410
+ }
1411
+
1412
+ //#region dsh-dream-skin: P0 favorites + surprise-me
1413
+ /** Read the favorites id list (built-in skins + imported pack ids). */
1414
+ function readFavorites() {
1415
+ const raw = readStorage(FAVORITES_KEY);
1416
+ if (raw === null) return [];
1417
+ try {
1418
+ const parsed = JSON.parse(raw);
1419
+ return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
1420
+ } catch {
1421
+ return [];
1422
+ }
1423
+ }
1424
+
1425
+ /** Persist the favorites list. */
1426
+ function writeFavorites(list) {
1427
+ writeStorage(FAVORITES_KEY, JSON.stringify(list));
1428
+ }
1429
+
1430
+ /** Toggle a favorite id; returns true if it is now favorited. */
1431
+ function toggleFavorite(id) {
1432
+ const list = readFavorites();
1433
+ const idx = list.indexOf(id);
1434
+ if (idx === -1) {
1435
+ list.push(id);
1436
+ writeFavorites(list);
1437
+ return true;
1438
+ }
1439
+ list.splice(idx, 1);
1440
+ writeFavorites(list);
1441
+ return false;
1442
+ }
1443
+
1444
+ /** All applyable theme ids (built-in skins + imported packs). */
1445
+ function allThemeIds() {
1446
+ const built = SKINS.map((s) => s.id);
1447
+ for (const p of importedPacks) if (p && p.id) built.push(p.id);
1448
+ return built;
1449
+ }
1450
+
1451
+ /** Pick a different random theme id than the current one. */
1452
+ function randomThemeId(exclude) {
1453
+ const ids = allThemeIds().filter((id) => id !== exclude);
1454
+ if (ids.length === 0) return null;
1455
+ return ids[Math.floor(Math.random() * ids.length)];
1456
+ }
1457
+ //#endregion
1458
+
1459
+ //#region dsh-dream-skin: P0 stores + module hooks
1460
+ /** Accent row slot store. */
1461
+ function createAccentStore() {
1462
+ return (0, _runtime_client.defineStore)({
1463
+ init: () => ({ accent: DEFAULT_ACCENT, base: DEFAULT_ACCENT, revision: -1 }),
1464
+ actions: {
1465
+ sync: (d, accent, base, revision) => {
1466
+ if (revision <= d.revision) return;
1467
+ d.accent = accent;
1468
+ d.base = base;
1469
+ d.revision = revision;
1470
+ }
1471
+ }
1472
+ });
1473
+ }
1474
+
1475
+ /** Pack library row slot store (ids + favorites + active + suggestion). */
1476
+ function createPackStore() {
1477
+ return (0, _runtime_client.defineStore)({
1478
+ init: () => ({ ids: [], favorites: [], active: null, suggestion: null, revision: -1 }),
1479
+ actions: {
1480
+ sync: (d, ids, favorites, active, suggestion, revision) => {
1481
+ if (revision <= d.revision) return;
1482
+ d.ids = ids;
1483
+ d.favorites = favorites;
1484
+ d.active = active;
1485
+ d.suggestion = suggestion;
1486
+ d.revision = revision;
1487
+ }
1488
+ }
1489
+ });
1490
+ }
1491
+
1492
+ /** Suggested wallpaper gradient for a theme (P0-3 per-skin recommendation). */
1493
+ function wallpapersSuggestionsFor(activeId) {
1494
+ const suggestions = {
1495
+ abyss: "linear-gradient(135deg, #0b1120 0%, #172554 55%, #1e3a8a 100%)",
1496
+ aurora: "linear-gradient(135deg, #022c22 0%, #065f46 60%, #0d9488 100%)",
1497
+ nebula: "linear-gradient(135deg, #1e1b4b 0%, #4c1d95 60%, #7e22ce 100%)",
1498
+ ember: "linear-gradient(135deg, #251607 0%, #7c2d12 60%, #c2410c 100%)",
1499
+ midnight: "linear-gradient(135deg, #030712 0%, #0f172a 100%)",
1500
+ ivory: "linear-gradient(135deg, #faf5eb 0%, #e7dfcb 100%)",
1501
+ mist: "linear-gradient(135deg, #f0f3f7 0%, #dbe4ee 100%)",
1502
+ rose: "linear-gradient(135deg, #fdf2f6 0%, #f0d2dc 100%)"
1503
+ };
1504
+ return suggestions[activeId] || null;
1505
+ }
1506
+
1507
+ /** Module-level hooks the PacksRow component uses to import/export/share. */
1508
+ let packsImportHandler = null;
1509
+ let packExporter = null;
1510
+ let packShare = null;
1511
+
1512
+ /** Advanced wallpaper row store: kind + url + gradient + autodim. */
1513
+ function createAdvancedWallpaperStore() {
1514
+ return (0, _runtime_client.defineStore)({
1515
+ init: () => ({ kind: "image", url: null, gradient: null, autodim: false, revision: -1 }),
1516
+ actions: {
1517
+ sync: (d, kind, url, gradient, autodim, revision) => {
1518
+ if (revision <= d.revision) return;
1519
+ d.kind = kind;
1520
+ d.url = url;
1521
+ d.gradient = gradient;
1522
+ d.autodim = autodim;
1523
+ d.revision = revision;
1524
+ }
1525
+ }
1526
+ });
1527
+ }
1528
+ //#endregion
1529
+
1530
+ //#region dsh-dream-skin: P0 accent override
1531
+ /** Source identity for the per-user accent override layer. */
1532
+ const ACCENT_OVERRIDE_SOURCE = "dsh-dream-skin:accent";
1533
+ /** Token names the accent override shades (brand + primary surfaces). */
1534
+ const ACCENT_TOKENS = [
1535
+ "--dsw-alias-brand-primary",
1536
+ "--dsw-alias-state-business-primary",
1537
+ "--dsw-alias-button-primary-fill",
1538
+ "--dsw-alias-button-primary-dimmed"
1539
+ ];
1540
+ /** Disposer for the active accent override layer. */
1541
+ let accentOverrideDispose = null;
1542
+ /** Cached accent currently applied (hex or null). */
1543
+ let appliedAccent = null;
1544
+
1545
+ /**
1546
+ * Read the persisted accent (`#rrggbb`, `${skinId}` to borrow a skin's
1547
+ * accent, or `system` + null when unset).
1548
+ */
1549
+ function readAccent() {
1550
+ const raw = readStorage(ACCENT_KEY);
1551
+ if (raw === null || raw === DEFAULT_ACCENT) return null;
1552
+ if (HEX_RE.test(raw.trim())) return raw.toLowerCase();
1553
+ const skin = SKINS.find((s) => s.id === raw.trim());
1554
+ return skin ? skin.tokens["--dsw-alias-brand-primary"] : null;
1555
+ }
1556
+
1557
+ /** Apply (or clear) the accent override layer. Returns the accent used. */
1558
+ function applyAccent(ctx) {
1559
+ const accent = readAccent();
1560
+ if (accent === null) {
1561
+ accentOverrideDispose?.();
1562
+ accentOverrideDispose = null;
1563
+ appliedAccent = null;
1564
+ return null;
1565
+ }
1566
+ const pair = { light: accent, dark: accent };
1567
+ const overrides = {};
1568
+ for (const name of ACCENT_TOKENS) overrides[name] = pair;
1569
+ accentOverrideDispose?.();
1570
+ accentOverrideDispose = ctx.theme.overrideTokens(ACCENT_OVERRIDE_SOURCE, overrides);
1571
+ appliedAccent = accent;
1572
+ return accent;
1573
+ }
1574
+
1575
+ /** Set (or clear with null) the accent override. */
1576
+ function setAccent(ctx, value) {
1577
+ writeStorage(ACCENT_KEY, value === null || value === DEFAULT_ACCENT ? null : String(value));
1578
+ return applyAccent(ctx);
1579
+ }
1580
+ //#endregion
1581
+
1582
+ //#region dsh-dream-skin: P0 wallpaper 2.0 (url / gradient / auto-dim)
1583
+ /** Unique source identity for the wallpaper (already composited) layer. */
1584
+ const WALLPAPER_OVERRIDE_SOURCE = "dsh-dream-skin:wallpaper";
1585
+ /** Read wallpaper kind (image|url|gradient). */
1586
+ function readWallpaperKind() {
1587
+ const kind = readStorage(WALLPAPER_KIND_KEY);
1588
+ return kind === "url" || kind === "gradient" ? kind : "image";
1589
+ }
1590
+
1591
+ /** Read the persistable wallpaper URL string (for url kind). */
1592
+ function readWallpaperUrl() {
1593
+ const raw = readStorage(WALLPAPER_URL_KEY);
1594
+ return raw && raw.length > 4 ? raw : null;
1595
+ }
1596
+
1597
+ /** Read the gradient CSS (for gradient kind). */
1598
+ function readWallpaperGradient() {
1599
+ const raw = readStorage(WALLPAPER_GRADIENT_KEY);
1600
+ return raw && raw.length > 4 ? raw : null;
1601
+ }
1602
+
1603
+ /** Whether auto-dim wallpapers while a task is focused. */
1604
+ function readWallpaperAutodim() {
1605
+ return readStorage(WALLPAPER_AUTODIM_KEY) === "1";
1606
+ }
1607
+
1608
+ /** Persist auto-dim. */
1609
+ function writeWallpaperAutodim(on) {
1610
+ writeStorage(WALLPAPER_AUTODIM_KEY, on ? "1" : "0");
1611
+ }
1612
+
1613
+ /**
1614
+ * Resolve the background-image CSS for the current wallpaper config, or
1615
+ * null when no wallpaper is set.
1616
+ */
1617
+ function wallpaperBackgroundCss() {
1618
+ const kind = readWallpaperKind();
1619
+ if (kind === "gradient") {
1620
+ const grad = readWallpaperGradient();
1621
+ return grad ? grad : null;
1622
+ }
1623
+ if (kind === "url") {
1624
+ const url = readWallpaperUrl();
1625
+ return url ? `url("${url}")` : null;
1626
+ }
1627
+ // legacy / image
1628
+ const data = readWallpaper();
1629
+ return data ? `url("${data}")` : null;
1630
+ }
1631
+
1632
+ /** Re-render the wallpaper backdrop from the current config. */
1633
+ function applyWallpaper2(ctx) {
1634
+ const bg = wallpaperBackgroundCss();
1635
+ const urlIsSet = bg !== null;
1636
+ if (!urlIsSet) {
1637
+ teardownWallpaper();
1638
+ return;
1639
+ }
1640
+ if (wallpaperEl === null || !document.body.contains(wallpaperEl)) {
1641
+ wallpaperEl = document.createElement("div");
1642
+ wallpaperEl.style.cssText = "position:fixed;inset:0;z-index:-1;pointer-events:none;background-size:cover;background-position:center;background-repeat:no-repeat;";
1643
+ document.body.prepend(wallpaperEl);
1644
+ }
1645
+ const blur = readWallpaperBlur();
1646
+ wallpaperEl.style.backgroundImage = bg;
1647
+ wallpaperEl.style.filter = blur > 0 ? `blur(${blur}px)` : "none";
1648
+ // Auto-dim lowers the wash opacity when enabled.
1649
+ const baseFill = readWallpaperOpacity();
1650
+ const wash = readWallpaperAutodim() ? Math.min(baseFill, 0.45) : baseFill;
1651
+ shadeTokens2(ctx, wash, wash);
1652
+ }
1653
+
1654
+ /** Like the original shadeTokens but with configurable alphas for the wash. */
1655
+ function shadeTokens2(ctx, canvasAlpha, sidebarAlpha) {
1656
+ const snapshot = ctx.theme.getTheme();
1657
+ const overrides = {
1658
+ "--dsw-alias-bg-base": {
1659
+ light: toRgba(resolveBase("light", snapshot.active), canvasAlpha),
1660
+ dark: toRgba(resolveBase("dark", snapshot.active), canvasAlpha)
1661
+ },
1662
+ "--dsw-specific-sidebar-fill": {
1663
+ light: toRgba(resolveBase("light", snapshot.active), Math.min(1, sidebarAlpha + 0.1)),
1664
+ dark: toRgba(resolveBase("dark", snapshot.active), Math.min(1, sidebarAlpha + 0.1))
1665
+ }
1666
+ };
1667
+ wallpaperOverrideDispose?.();
1668
+ wallpaperOverrideDispose = ctx.theme.overrideTokens(WALLPAPER_OVERRIDE_SOURCE, overrides);
1669
+ }
1670
+
1671
+ /** Clear wallpaper (all kinds) and its overrides. */
1672
+ function removeWallpaper(ctx) {
1673
+ writeStorage(WALLPAPER_KEY, null);
1674
+ writeStorage(WALLPAPER_URL_KEY, null);
1675
+ writeStorage(WALLPAPER_GRADIENT_KEY, null);
1676
+ writeStorage(WALLPAPER_KIND_KEY, null);
1677
+ teardownWallpaper();
1678
+ syncWallpaper();
1679
+ }
1680
+
1681
+ /** Set a wallpaper by kind and value. */
1682
+ function setWallpaperKind(ctx, kind, value) {
1683
+ writeStorage(WALLPAPER_KIND_KEY, kind);
1684
+ if (kind === "gradient") {
1685
+ writeStorage(WALLPAPER_GRADIENT_KEY, value);
1686
+ if (value === null) writeStorage(WALLPAPER_GRADIENT_KEY, null);
1687
+ } else if (kind === "url") {
1688
+ writeStorage(WALLPAPER_URL_KEY, value);
1689
+ } else {
1690
+ writeStorage(WALLPAPER_KEY, value);
1691
+ }
1692
+ applyWallpaper2(ctx);
1693
+ syncWallpaper();
1694
+ }
1695
+ //#endregion
1696
+
1697
+ //#region dsh-dream-skin: P0 share-url import
1698
+ /** Try to import a pack shared via URL hash; true when one was imported. */
1699
+ function tryImportFromHash(ctx) {
1700
+ const decoded = decodeShareUrl(window.location.hash);
1701
+ if (!decoded) return false;
1702
+ try {
1703
+ applyPackToTheme(ctx, decoded.id, packToRegistration(decoded.pack));
1704
+ importedPacks.push({ id: decoded.id, manifest: decoded.pack.manifest, registration: packToRegistration(decoded.pack) });
1705
+ const packs = readPacks();
1706
+ if (!packs.some((p) => p.id === decoded.id)) packs.push({ id: decoded.id, manifest: decoded.pack.manifest });
1707
+ writePacks(packs);
1708
+ } catch {
1709
+ // ignore a bad import at boot
1710
+ }
1711
+ // Clear the hash so it doesn't re-import on every reload.
1712
+ try {
1713
+ window.history.replaceState(null, "", window.location.pathname + window.location.search);
1714
+ } catch {
1715
+ // no-op
1716
+ }
1717
+ return true;
1718
+ }
1719
+ //#endregion
1720
+
1721
+ //#endregion
1722
+
1723
+ //#region dsh-dream-skin: P0 UI rows (accent + packs)
1724
+ /**
1725
+ * Accent row: pick an arbitrary brand-accent color (or clear to follow
1726
+ * the active theme). Uses an `<input type="color">` + the current accent
1727
+ * preview swatch, stacked as an override layer via ctx.theme.
1728
+ */
1729
+ function AccentRow({ t, setAccent, clearAccent, useStore }) {
1730
+ const accent = useStore((s) => s.accent);
1731
+ const base = useStore((s) => s.base);
1732
+ const activeValue = accent !== DEFAULT_ACCENT ? accent : base;
1733
+ return (0, react_jsx_runtime.jsxs)("div", {
1734
+ style: styles.group,
1735
+ children: [
1736
+ (0, react_jsx_runtime.jsx)("div", {
1737
+ style: styles.title,
1738
+ children: t("accent.title")
1739
+ }),
1740
+ (0, react_jsx_runtime.jsxs)("div", {
1741
+ style: styles.actionRow,
1742
+ children: [
1743
+ (0, react_jsx_runtime.jsx)("input", {
1744
+ type: "color",
1745
+ value: (0, _react.useMemo)(() => normalizeHex(activeValue) || "#4f83f2", [activeValue]),
1746
+ style: { ...styles.swatch, width: "40px", height: "32px", padding: 0, border: "none", borderRadius: "8px", cursor: "pointer" },
1747
+ onChange: (event) => setAccent(event.target.value)
1748
+ }),
1749
+ (0, react_jsx_runtime.jsx)("button", {
1750
+ type: "button",
1751
+ style: styles.button,
1752
+ onClick: () => {
1753
+ const next = randomAccent();
1754
+ setAccent(next);
1755
+ },
1756
+ children: t("accent.random")
1757
+ }),
1758
+ accent !== DEFAULT_ACCENT ? (0, react_jsx_runtime.jsx)("button", {
1759
+ type: "button",
1760
+ style: { ...styles.button, ...styles.buttonDanger },
1761
+ onClick: clearAccent,
1762
+ children: t("accent.clear")
1763
+ }) : null
1764
+ ]
1765
+ }),
1766
+ (0, react_jsx_runtime.jsx)("div", {
1767
+ style: styles.hint,
1768
+ children: t("accent.hint")
1769
+ })
1770
+ ]
1771
+ });
1772
+ }
1773
+
1774
+ /** Pick a pleasant palette accent that differs from the current one. */
1775
+ function randomAccent() {
1776
+ const pool = ["#4f83f2", "#34d399", "#a78bfa", "#fb923c", "#f87171", "#22d3ee", "#fbbf24", "#e879f9", "#2dd4bf", "#f472b6", "#60a5fa", "#a3e635"];
1777
+ const current = readAccent();
1778
+ const candidates = pool.filter((c) => c !== current);
1779
+ return candidates[Math.floor(Math.random() * candidates.length)] || "#4f83f2";
1780
+ }
1781
+
1782
+ /**
1783
+ * Packs row: import a theme-pack JSON, apply / favorite themes in the
1784
+ * library, export/share the current pack, and "surprise me".
1785
+ */
1786
+ function PacksRow({ t, applyId, toggleFavorite, surprise, useStore }) {
1787
+ const ids = useStore((s) => s.ids);
1788
+ const favorites = useStore((s) => s.favorites);
1789
+ const active = useStore((s) => s.active);
1790
+ const fileInput = (0, _react.useRef)(null);
1791
+ const importFile = () => { if (fileInput.current) fileInput.current.click(); };
1792
+
1793
+ const onFile = (event) => {
1794
+ const file = event.target.files?.[0];
1795
+ if (file === void 0) return;
1796
+ if (file.size > PACK_MAX_BYTES) {
1797
+ event.target.value = "";
1798
+ return;
1799
+ }
1800
+ const reader = new FileReader();
1801
+ reader.onerror = () => {
1802
+ event.target.value = "";
1803
+ };
1804
+ reader.onload = () => {
1805
+ let data = null;
1806
+ try {
1807
+ data = JSON.parse(String(reader.result));
1808
+ } catch {
1809
+ data = null;
1810
+ }
1811
+ if (data !== null && packsImportHandler) {
1812
+ packsImportHandler(null, data); // handler wraps validatePack + importPack
1813
+ }
1814
+ event.target.value = "";
1815
+ };
1816
+ reader.readAsText(file);
1817
+ };
1818
+
1819
+ const doShare = () => {
1820
+ const activeId = active && ids.indexOf(active) !== -1 ? active : null;
1821
+ if (activeId && packShare) {
1822
+ const url = packShare(activeId);
1823
+ if (typeof navigator !== "undefined" && navigator.clipboard) navigator.clipboard.writeText(url).then(() => {}).catch(() => {});
1824
+ }
1825
+ };
1826
+
1827
+ return (0, react_jsx_runtime.jsxs)("div", {
1828
+ style: styles.group,
1829
+ children: [
1830
+ (0, react_jsx_runtime.jsx)("div", {
1831
+ style: styles.title,
1832
+ children: t("packs.title")
1833
+ }),
1834
+ (0, react_jsx_runtime.jsxs)("div", {
1835
+ style: styles.actionRow,
1836
+ children: [
1837
+ (0, react_jsx_runtime.jsx)("button", {
1838
+ type: "button",
1839
+ style: styles.button,
1840
+ onClick: importFile,
1841
+ children: t("packs.import")
1842
+ }),
1843
+ (0, react_jsx_runtime.jsx)("input", {
1844
+ ref: fileInput,
1845
+ type: "file",
1846
+ accept: ".json,.dsh-theme.json,.dsh-theme,application/json",
1847
+ style: { display: "none" },
1848
+ onChange: onFile
1849
+ }),
1850
+ (0, react_jsx_runtime.jsx)("button", {
1851
+ type: "button",
1852
+ style: styles.button,
1853
+ onClick: () => surprise(),
1854
+ children: t("packs.surprise")
1855
+ }),
1856
+ active && packShare ? (0, react_jsx_runtime.jsx)("button", {
1857
+ type: "button",
1858
+ style: styles.button,
1859
+ onClick: doShare,
1860
+ children: t("packs.share")
1861
+ }) : null
1862
+ ]
1863
+ }),
1864
+ ids.length > 0 ? (0, react_jsx_runtime.jsxs)("div", {
1865
+ style: styles.grid,
1866
+ children: ids.map((id) => {
1867
+ const fav = favorites.indexOf(id) !== -1;
1868
+ const label = id;
1869
+ return (0, react_jsx_runtime.jsxs)("button", {
1870
+ key: id,
1871
+ type: "button",
1872
+ "aria-pressed": active === id,
1873
+ style: {
1874
+ ...styles.card,
1875
+ ...(active === id ? styles.cardSelected : {})
1876
+ },
1877
+ children: [
1878
+ (0, react_jsx_runtime.jsx)("span", {
1879
+ style: styles.cardLabel,
1880
+ children: label
1881
+ }),
1882
+ (0, react_jsx_runtime.jsxs)("div", {
1883
+ style: styles.actionRow,
1884
+ children: [
1885
+ (0, react_jsx_runtime.jsx)("button", {
1886
+ type: "button",
1887
+ style: { ...styles.tinyButton },
1888
+ onClick: () => applyId(id),
1889
+ children: t("packs.apply")
1890
+ }),
1891
+ (0, react_jsx_runtime.jsx)("button", {
1892
+ type: "button",
1893
+ style: { ...styles.tinyButton, ...(fav ? styles.tinyButtonActive : {}) },
1894
+ onClick: () => toggleFavorite(id),
1895
+ children: fav ? "★" : "☆"
1896
+ })
1897
+ ]
1898
+ })
1899
+ ]
1900
+ });
1901
+ })
1902
+ }) : (0, react_jsx_runtime.jsx)("div", {
1903
+ style: styles.hint,
1904
+ children: t("packs.empty")
1905
+ })
1906
+ ]
1907
+ });
1908
+ }
1909
+ //#endregion
1910
+ //#region dsh-dream-skin: client plugin body
1911
+ /**
1912
+ * Required services: theme runtime (skins, switching, token override
1913
+ * layers), slots/locale (the settings rows). Persistence is
1914
+ * localStorage, so no settings transport is needed.
1915
+ */
1916
+ const inject = [
1917
+ "slots",
1918
+ "locale",
1919
+ "theme"
1920
+ ];
1921
+
1922
+ /**
1923
+ * Client plugin body: register the curated skins into the theme runtime,
1924
+ * restore the saved skin and wallpaper, keep the rows' stores in sync
1925
+ * with theme/change, and register both rows into Settings → General.
1926
+ * @param ctx - client cordis context.
1927
+ */
1928
+ function apply(ctx) {
1929
+ const disposers = SKINS.map((skinDefinition) => ctx.theme.register(skinDefinition));
1930
+ ctx.effect(() => () => {
1931
+ for (const dispose of disposers) dispose();
1932
+ }, "dsh-dream-skin: theme registration");
1933
+
1934
+ // P0: re-register previously imported packs before restoring a skin,
1935
+ // then import any pack shared via URL hash.
1936
+ restorePacks(ctx);
1937
+ let importedPackShare = tryImportFromHash(ctx);
1938
+
1939
+ // Restore the saved skin once (before any user interaction).
1940
+ const saved = readSavedSkin();
1941
+ if (typeof saved === "string" && saved !== DEFAULT_SKIN && (SKINS.some((skinDefinition) => skinDefinition.id === saved) || importedPacks.some((p) => p.id === saved))) {
1942
+ const current = ctx.theme.getTheme().preference;
1943
+ if (current !== saved) ctx.theme.setTheme(saved);
1944
+ }
1945
+ // P0: apply the persisted per-user accent override.
1946
+ applyAccent(ctx);
1947
+
1948
+ // Wallpaper bookkeeping.
1949
+ let wallpaperRevision = 0;
1950
+ const wallpaperStore = createWallpaperStore();
1951
+ let wallpaperBound;
1952
+ const syncWallpaper = () => {
1953
+ wallpaperRevision += 1;
1954
+ wallpaperBound?.sync(wallpaperBackgroundCss(), readWallpaperOpacity(), readWallpaperBlur(), wallpaperRevision);
1955
+ };
1956
+ applyWallpaper2(ctx);
1957
+ syncWallpaper();
1958
+ ctx.effect(() => () => {
1959
+ teardownWallpaper();
1960
+ disposeAllPacks();
1961
+ }, "dsh-dream-skin: wallpaper + packs cleanup");
1962
+
1963
+ const skinStore = createSkinStore();
1964
+ let skinBound;
1965
+ const syncSkin = (snapshot) => {
1966
+ skinBound?.sync(snapshot.preference, snapshot.revision);
1967
+ // A skin/scheme switch changes the base color; re-shade the wash.
1968
+ if (wallpaperBackgroundCss() !== null) applyWallpaper2(ctx);
1969
+ };
1970
+ ctx.on("theme/change", syncSkin);
1971
+
1972
+ ctx.effect(() => ctx.locale.register(SETTINGS_NS, {
1973
+ zh,
1974
+ en
1975
+ }), "dsh-dream-skin: settings row dictionaries");
1976
+
1977
+ const skinInjected = (actions) => {
1978
+ skinBound = actions;
1979
+ syncSkin(ctx.theme.getTheme());
1980
+ return {
1981
+ setSkin: (id) => {
1982
+ ctx.theme.setTheme(id);
1983
+ writeSavedSkin(id);
1984
+ }
1985
+ };
1986
+ };
1987
+ ctx.slots.inject("settings.general.item", () => ctx.slots.register({
1988
+ name: "settings.general.item",
1989
+ id: "dream-skin",
1990
+ order: 20,
1991
+ store: skinStore,
1992
+ locale: SETTINGS_NS,
1993
+ inject: skinInjected
1994
+ }, SkinRow));
1995
+
1996
+ const wallpaperInjected = (actions) => {
1997
+ wallpaperBound = actions;
1998
+ syncWallpaper();
1999
+ return {
2000
+ setWallpaper: (url) => {
2001
+ writeStorage(WALLPAPER_KEY, url);
2002
+ applyWallpaper2(ctx);
2003
+ syncWallpaper();
2004
+ },
2005
+ setWallpaperUrl: (url) => {
2006
+ setWallpaperKind(ctx, "url", url && url.length > 4 ? url : null);
2007
+ },
2008
+ setWallpaperGradient: (gradient) => {
2009
+ setWallpaperKind(ctx, "gradient", gradient && gradient.length > 4 ? gradient : null);
2010
+ },
2011
+ setWallpaperKind,
2012
+ setOpacity: (percent) => {
2013
+ const value = Math.min(1, Math.max(0, percent / 100));
2014
+ writeStorage(WALLPAPER_OPACITY_KEY, String(value));
2015
+ applyWallpaper2(ctx);
2016
+ syncWallpaper();
2017
+ },
2018
+ setBlur: (px) => {
2019
+ const value = Math.min(60, Math.max(0, px));
2020
+ writeStorage(WALLPAPER_BLUR_KEY, String(value));
2021
+ applyWallpaper2(ctx);
2022
+ syncWallpaper();
2023
+ },
2024
+ setAutodim: (on) => {
2025
+ writeWallpaperAutodim(!!on);
2026
+ applyWallpaper2(ctx);
2027
+ syncWallpaper();
2028
+ },
2029
+ clearWallpaper: () => {
2030
+ removeWallpaper(ctx);
2031
+ }
2032
+ };
2033
+ };
2034
+ ctx.slots.inject("settings.general.item", () => ctx.slots.register({
2035
+ name: "settings.general.item",
2036
+ id: "dream-skin-wallpaper",
2037
+ order: 30,
2038
+ store: wallpaperStore,
2039
+ locale: SETTINGS_NS,
2040
+ inject: wallpaperInjected
2041
+ }, WallpaperRow));
2042
+
2043
+ // P0: advanced wallpaper row (kind url / gradient / autodim).
2044
+ const advWallpaperStore = createAdvancedWallpaperStore();
2045
+ let advWallpaperBound;
2046
+ let advWallpaperRevision = 0;
2047
+ const syncAdvWallpaper = () => {
2048
+ advWallpaperBound?.sync(
2049
+ readWallpaperKind(),
2050
+ readWallpaperUrl(),
2051
+ readWallpaperGradient(),
2052
+ readWallpaperAutodim(),
2053
+ advWallpaperRevision++
2054
+ );
2055
+ };
2056
+ const advWallpaperInjected = (actions) => {
2057
+ advWallpaperBound = actions;
2058
+ syncAdvWallpaper();
2059
+ return {
2060
+ setKind: (kind) => {
2061
+ if (kind !== "image" && kind !== "url" && kind !== "gradient") return;
2062
+ writeStorage(WALLPAPER_KIND_KEY, kind);
2063
+ applyWallpaper2(ctx);
2064
+ syncAdvWallpaper();
2065
+ },
2066
+ setUrl: (url) => {
2067
+ setWallpaperKind(ctx, "url", url && url.length > 4 ? url : null);
2068
+ syncAdvWallpaper();
2069
+ },
2070
+ setGradient: (gradient) => {
2071
+ setWallpaperKind(ctx, "gradient", gradient && gradient.length > 4 ? gradient : null);
2072
+ syncAdvWallpaper();
2073
+ },
2074
+ setAutodim: (on) => {
2075
+ writeWallpaperAutodim(!!on);
2076
+ applyWallpaper2(ctx);
2077
+ syncAdvWallpaper();
2078
+ },
2079
+ clearAll: () => {
2080
+ removeWallpaper(ctx);
2081
+ syncAdvWallpaper();
2082
+ }
2083
+ };
2084
+ };
2085
+ ctx.slots.inject("settings.general.item", () => ctx.slots.register({
2086
+ name: "settings.general.item",
2087
+ id: "dream-skin-wallpaper-advanced",
2088
+ order: 31,
2089
+ store: advWallpaperStore,
2090
+ locale: SETTINGS_NS,
2091
+ inject: advWallpaperInjected
2092
+ }, WallpaperAdvancedRow));
2093
+
2094
+ // P0: per-user accent override row.
2095
+ const accentStore = createAccentStore();
2096
+ let accentBound;
2097
+ const accentInjected = (actions) => {
2098
+ accentBound = actions;
2099
+ const base = resolveAccent(ctx.theme.getTheme()) || DEFAULT_ACCENT;
2100
+ accentBound?.sync(readAccent() || DEFAULT_ACCENT, base, -1);
2101
+ return {
2102
+ setAccent: (value) => {
2103
+ const applied = setAccent(ctx, value === DEFAULT_ACCENT ? null : value);
2104
+ accentBound?.sync(applied || DEFAULT_ACCENT, resolveAccent(ctx.theme.getTheme()) || DEFAULT_ACCENT, 0);
2105
+ },
2106
+ clearAccent: () => {
2107
+ setAccent(ctx, null);
2108
+ accentBound?.sync(DEFAULT_ACCENT, resolveAccent(ctx.theme.getTheme()) || DEFAULT_ACCENT, 0);
2109
+ syncSkin(ctx.theme.getTheme());
2110
+ }
2111
+ };
2112
+ };
2113
+ ctx.slots.inject("settings.general.item", () => ctx.slots.register({
2114
+ name: "settings.general.item",
2115
+ id: "dream-skin-accent",
2116
+ order: 25,
2117
+ store: accentStore,
2118
+ locale: SETTINGS_NS,
2119
+ inject: accentInjected
2120
+ }, AccentRow));
2121
+
2122
+ // P0: theme-pack library + favorites + surprise-me row.
2123
+ const packStore = createPackStore();
2124
+ let packBound;
2125
+ const syncPack = () => {
2126
+ const current = ctx.theme.getTheme().preference;
2127
+ packBound?.sync(SKINS.map((s) => s.id).concat(importedPacks.map((p) => p.id)), readFavorites(), current, wallpapersSuggestionsFor(current), -1);
2128
+ };
2129
+ const refreshSurprise = () => {
2130
+ const id = randomThemeId(ctx.theme.getTheme().preference);
2131
+ if (id !== null) {
2132
+ ctx.theme.setTheme(id);
2133
+ writeSavedSkin(id);
2134
+ }
2135
+ syncPack();
2136
+ syncSkin(ctx.theme.getTheme());
2137
+ };
2138
+ const packInjected = (actions) => {
2139
+ packBound = actions;
2140
+ syncPack();
2141
+ return {
2142
+ applyId: (id) => {
2143
+ ctx.theme.setTheme(id);
2144
+ writeSavedSkin(id);
2145
+ syncPack();
2146
+ syncSkin(ctx.theme.getTheme());
2147
+ },
2148
+ toggleFavorite: (id) => {
2149
+ toggleFavorite(id);
2150
+ syncPack();
2151
+ },
2152
+ surprise: refreshSurprise
2153
+ };
2154
+ };
2155
+ ctx.slots.inject("settings.general.item", () => ctx.slots.register({
2156
+ name: "settings.general.item",
2157
+ id: "dream-skin-packs",
2158
+ order: 40,
2159
+ store: packStore,
2160
+ locale: SETTINGS_NS,
2161
+ inject: packInjected
2162
+ }, PacksRow));
2163
+
2164
+ // Wire the shared "import a file" handler exposed to PacksRow via a
2165
+ // small module-level hook (the file input lives in the row component).
2166
+ packsImportHandler = (_ignoredCtx, data) => {
2167
+ const validate = (typeof data === "object" && data !== null) ? validatePack(data) : { ok: false, errors: ["invalid JSON or empty pack"] };
2168
+ if (!validate.ok) {
2169
+ try { window.alert("dsh-dream-skin: theme pack rejected —\n" + (validate.errors || []).join("\n")); } catch {}
2170
+ return { ok: false };
2171
+ }
2172
+ const result = importPack(ctx, validate);
2173
+ try {
2174
+ if (!result.ok) window.alert("dsh-dream-skin: import failed —\n" + result.error);
2175
+ else { syncPack(); window.alert("dsh-dream-skin: imported \"" + result.name + "\" ✓"); }
2176
+ } catch {}
2177
+ return result;
2178
+ };
2179
+ packExporter = (id) => exportPackAsFile(ctx, id);
2180
+ packShare = (id) => packShareUrl(id);
2181
+ }
2182
+ //#endregion
2183
+
2184
+ exports.SETTINGS_NS = SETTINGS_NS;
2185
+ exports.SKINS = SKINS;
2186
+ exports.DEFAULT_SKIN = DEFAULT_SKIN;
2187
+ exports.apply = apply;
2188
+ exports.inject = inject;
2189
+ return module.exports;
2190
+ }
2191
+ });