deepseek-harness-background 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,1116 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "deepseek-harness-background",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/background-css.ts
10
+ /**
11
+ * Background CSS — the stylesheet injected once into the document when the
12
+ * plugin first runs. It defines the behind-body wallpaper layer, the scrim,
13
+ * and the frosted-glass panels, all driven by CSS variables the painter writes
14
+ * onto `document.body`. Kept in a single idempotent `<style data-plugin-css>`
15
+ * tag (mirrors the reference `dsh-wallpaper-engine` bundle).
16
+ */
17
+ /** Unique id stamped on the injected style element (dedup key). */
18
+ const BACKGROUND_CSS_TAG = "deepseek-harness-background/styles";
19
+ /** Body attribute the painter sets while a wallpaper is active. */
20
+ const ACTIVE_ATTR = "data-dsh-bg";
21
+ /**
22
+ * The backdrop CSS. The wallpaper is a fixed `z-index:-2` element (below the
23
+ * scrim at `-1`, below the UI). While active, the official surface tokens are
24
+ * made transparent (so the wallpaper shows through every column) and the
25
+ * opaque input / bubble surfaces turn into translucent frosted glass.
26
+ * Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`)
27
+ * and CSS-module SUFFIX conventions (`_bubble`) — the same stable anchors the
28
+ * reference engine uses, since hashed prefixes change across shell rebuilds.
29
+ */
30
+ const BACKGROUND_CSS = `
31
+ .dsh-bg-layer {
32
+ position: fixed; inset: 0; z-index: -2; overflow: hidden; pointer-events: none;
33
+ }
34
+ .dsh-bg-layer .dsh-bg-image {
35
+ width: 100%; height: 100%; display: block; border: 0; background: transparent;
36
+ object-fit: var(--bg-object-fit, cover);
37
+ opacity: var(--bg-opacity, 1);
38
+ filter: blur(var(--bg-wallpaper-blur, 0px));
39
+ /* Blur thins the edges; scale the image up to hide the fringe. */
40
+ transform: scale(var(--bg-wallpaper-scale, 1));
41
+ transform-origin: center;
42
+ }
43
+ /* Scrim is theme-aware: a WHITE veil in the light scheme (it lifts the art
44
+ toward the near-white surface so dark text keeps contrast) and a BLACK veil
45
+ in the dark scheme (it dims the art so light text keeps contrast). */
46
+ .dsh-bg-scrim {
47
+ position: fixed; inset: 0; z-index: -1; pointer-events: none;
48
+ background: rgba(255, 255, 255, var(--bg-scrim, 0.25));
49
+ }
50
+ body[data-ds-dark-theme] .dsh-bg-scrim {
51
+ background: rgba(0, 0, 0, var(--bg-scrim, 0.25));
52
+ }
53
+
54
+ /* Active: make the app frame + sidebar background transparent so all columns
55
+ share the same wallpaper + scrim backdrop. */
56
+ body[data-dsh-bg] {
57
+ --dsw-alias-bg-base: transparent;
58
+ --dsw-specific-sidebar-fill: transparent;
59
+ }
60
+
61
+ /* Frosted glass over the opaque conversation surfaces (composer + bubbles).
62
+ The surface translucency is driven by --dsw-specific-input-major /
63
+ --dsw-specific-bubble, which the painter writes (theme-aware) so that a
64
+ fully-opaque panel restores the official surfaces. The blur here comes
65
+ from --bg-glass-blur; a faint top-weighted specular sheen makes the
66
+ translucent surfaces read as wet glass. */
67
+ body[data-dsh-bg] [data-composer-card],
68
+ body[data-dsh-bg] [class*="_bubble"] {
69
+ background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.05) 38%, rgba(255, 255, 255, 0.02));
70
+ -webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.6)) brightness(var(--bg-glass-brightness, 1.04)) contrast(1.01);
71
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.6)) brightness(var(--bg-glass-brightness, 1.04)) contrast(1.01);
72
+ box-shadow:
73
+ inset 0 1px 0 rgba(255, 255, 255, var(--bg-glass-highlight, 0.32)),
74
+ inset 0 -1px 0 rgba(255, 255, 255, 0.08),
75
+ inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
76
+ 0 12px 40px rgba(0, 0, 0, var(--bg-glass-shadow, 0.12));
77
+ }
78
+ `;
79
+ /**
80
+ * Inject the backdrop stylesheet once. Safe to call repeatedly; a second call
81
+ * is a no-op (dedup key on the style tag).
82
+ */
83
+ function injectBackgroundCss() {
84
+ if (document.querySelector(`style[data-plugin-css="deepseek-harness-background/styles"]`)) return;
85
+ const tag = document.createElement("style");
86
+ tag.dataset.plugin = "deepseek-harness-background";
87
+ tag.dataset.pluginCss = BACKGROUND_CSS_TAG;
88
+ tag.textContent = BACKGROUND_CSS;
89
+ document.head.appendChild(tag);
90
+ }
91
+ //#endregion
92
+ //#region src/settings.ts
93
+ /** Browser-facing base path of the background API (shared with the host routes). */
94
+ const BACKGROUND_API_PREFIX = "/api/bg-wallpaper";
95
+ /** Maximum scrim opacity (a nearly opaque veil keeps text legible over any art). */
96
+ const SCRIM_MAX = .95;
97
+ /** Default scrim strength applied over user art (a light readability veil). */
98
+ const DEFAULT_SCRIM = .25;
99
+ /** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
100
+ const DEFAULT_PANEL_OPACITY = .15;
101
+ /** Default background rendering mode. */
102
+ const DEFAULT_FIT = "cover";
103
+ /** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
104
+ const FIT_MODES = ["cover", "contain"];
105
+ //#endregion
106
+ //#region src/client/backdrop.ts
107
+ /**
108
+ * Background painter — renders the user background behind the whole app frame:
109
+ * a fixed `z-index:-2` wallpaper element plus a `z-index:-1` scrim on
110
+ * `document.body`, driven by the `data-dsh-bg` body attribute and a set of
111
+ * CSS variables. It also applies the frosted-glass surface tokens so the
112
+ * wallpaper shows through translucent input / bubble panels.
113
+ *
114
+ * Retraction discipline mirrors the reference skin plugins: every property /
115
+ * DOM node this painter owns is restored to its prior state on dispose, and
116
+ * the `data-ds-dark-theme` observer is disconnected.
117
+ */
118
+ /** Class names the layer and scrim elements carry. */
119
+ const LAYER_CLASS = "dsh-bg-layer";
120
+ const IMAGE_CLASS = "dsh-bg-image";
121
+ const SCRIM_CLASS = "dsh-bg-scrim";
122
+ /** Surface tokens this painter overwrites (restored on dispose). */
123
+ const SURFACE_TOKENS = ["--dsw-specific-input-major", "--dsw-specific-bubble"];
124
+ /** Fraction of the official white surface alpha kept at the most transparent panel setting. */
125
+ const GLASS_MIN_ALPHA = .05;
126
+ /**
127
+ * Resolve the effective background image url for a section.
128
+ * @param settings - resolved background section.
129
+ * @returns the image url, or '' when no source is set.
130
+ */
131
+ function backgroundImageUrl(settings, uploadBase) {
132
+ if (settings.uploadId) return `${uploadBase}/image/${settings.uploadId}`;
133
+ return settings.url;
134
+ }
135
+ /**
136
+ * Manage the behind-body wallpaper + scrim layers and the glass surface.
137
+ */
138
+ var BackgroundPainter = class {
139
+ layer = null;
140
+ img = null;
141
+ scrim = null;
142
+ observer;
143
+ active = false;
144
+ settings;
145
+ savedTokens = /* @__PURE__ */ new Map();
146
+ savedVars = /* @__PURE__ */ new Map();
147
+ /** Remember + hijack a property only once so dispose restores the original. */
148
+ rememberOnce(prop) {
149
+ if (this.savedTokens.has(prop) || this.savedVars.has(prop)) return;
150
+ const value = document.body.style.getPropertyValue(prop);
151
+ if (SURFACE_TOKENS.includes(prop)) this.savedTokens.set(prop, value);
152
+ else this.savedVars.set(prop, value);
153
+ }
154
+ /**
155
+ * Apply the latest settings. Idempotent.
156
+ * @param settings - resolved background section.
157
+ * @param uploadBase - the API base for resolving uploads.
158
+ */
159
+ apply(settings, uploadBase) {
160
+ this.settings = settings;
161
+ if (!settings.enabled) {
162
+ this.dispose();
163
+ return;
164
+ }
165
+ injectBackgroundCss();
166
+ this.rememberOnce("--dsw-alias-bg-base");
167
+ this.rememberOnce("--dsw-specific-sidebar-fill");
168
+ document.body.style.setProperty("--dsw-alias-bg-base", "transparent");
169
+ document.body.style.setProperty("--dsw-specific-sidebar-fill", "transparent");
170
+ const url = backgroundImageUrl(settings, uploadBase);
171
+ if (url === "") {
172
+ this.applySurfaceGlass(settings, true);
173
+ this.active = false;
174
+ this.removeLayers();
175
+ document.body.removeAttribute(ACTIVE_ATTR);
176
+ return;
177
+ }
178
+ if (!this.layer) {
179
+ this.layer = document.createElement("div");
180
+ this.layer.className = LAYER_CLASS;
181
+ this.img = document.createElement("img");
182
+ this.img.className = IMAGE_CLASS;
183
+ this.img.alt = "";
184
+ this.layer.appendChild(this.img);
185
+ document.body.appendChild(this.layer);
186
+ }
187
+ if (this.img && this.img.src !== url) this.img.src = url;
188
+ if (!this.scrim) {
189
+ this.scrim = document.createElement("div");
190
+ this.scrim.className = SCRIM_CLASS;
191
+ document.body.appendChild(this.scrim);
192
+ }
193
+ document.body.setAttribute(ACTIVE_ATTR, "on");
194
+ this.active = true;
195
+ document.body.style;
196
+ this.setVar("--bg-object-fit", settings.fit);
197
+ this.setVar("--bg-opacity", String(settings.opacity));
198
+ this.setVar("--bg-wallpaper-blur", `${settings.wallpaperBlur}px`);
199
+ this.setVar("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
200
+ this.setVar("--bg-scrim", String(settings.scrim));
201
+ this.setVar("--bg-glass-blur", `${settings.blur}px`);
202
+ this.setVar("--bg-glass-saturate", String(1.15 + settings.blur * .03));
203
+ this.applySurfaceGlass(settings, false);
204
+ if (!this.observer) {
205
+ this.observer = new MutationObserver(() => this.applySurfaceGlass(this.settings, false));
206
+ this.observer.observe(document.body, {
207
+ attributes: true,
208
+ attributeFilter: ["data-ds-dark-theme"]
209
+ });
210
+ }
211
+ }
212
+ /** Write one owned CSS variable onto body. */
213
+ setVar(name, value) {
214
+ this.rememberOnce(name);
215
+ document.body.style.setProperty(name, value);
216
+ }
217
+ /**
218
+ * Update one effect knob without re-running the full apply. This is the hot
219
+ * path for slider drags (many events per second): only the single CSS
220
+ * variable changes, the DOM layers are untouched.
221
+ * @param key - the knob to update.
222
+ * @param value - its new value in its canonical unit.
223
+ */
224
+ setKnob(key, value) {
225
+ if (this.settings === void 0 || !this.settings.enabled) return;
226
+ switch (key) {
227
+ case "opacity":
228
+ this.setVar("--bg-opacity", String(value));
229
+ break;
230
+ case "scrim":
231
+ this.setVar("--bg-scrim", String(value));
232
+ break;
233
+ case "blur":
234
+ this.setVar("--bg-glass-blur", `${value}px`);
235
+ this.setVar("--bg-glass-saturate", String(1.15 + Number(value) * .03));
236
+ break;
237
+ case "wallpaperBlur":
238
+ this.setVar("--bg-wallpaper-blur", `${value}px`);
239
+ this.setVar("--bg-wallpaper-scale", (1 + Number(value) * .006).toFixed(4));
240
+ break;
241
+ case "fit": this.setVar("--bg-object-fit", String(value));
242
+ }
243
+ }
244
+ /**
245
+ * Apply the translucent-glass surface tokens (or restore the official ones
246
+ * when the panel is fully opaque / there is no background).
247
+ * @param settings - resolved section (for panelOpacity).
248
+ * @param forceRestore - true to restore official surfaces regardless (used
249
+ * when no source is set, so leftover glass tokens never linger).
250
+ */
251
+ applySurfaceGlass(settings, forceRestore) {
252
+ const s = document.body.style;
253
+ const inDark = document.body.dataset.dsDarkTheme !== void 0;
254
+ for (const token of SURFACE_TOKENS) this.rememberOnce(token);
255
+ const restore = () => {
256
+ for (const token of SURFACE_TOKENS) {
257
+ const original = this.savedTokens.get(token);
258
+ if (original !== void 0 && original !== "") s.setProperty(token, original);
259
+ else s.removeProperty(token);
260
+ }
261
+ };
262
+ if (forceRestore || settings === void 0 || settings.panelOpacity >= 1) {
263
+ restore();
264
+ this.setVar("--bg-glass-blur", "0px");
265
+ return;
266
+ }
267
+ const alpha = GLASS_MIN_ALPHA + settings.panelOpacity * .85;
268
+ const clamped = Math.max(0, Math.min(.9, alpha));
269
+ const factor = inDark ? .4 : .8;
270
+ s.setProperty("--dsw-specific-input-major", `rgba(255, 255, 255, ${(clamped * factor).toFixed(3)})`);
271
+ s.setProperty("--dsw-specific-bubble", `rgba(255, 255, 255, ${(clamped * factor * .8).toFixed(3)})`);
272
+ }
273
+ /** Remove the wallpaper + scrim layers and clear the active attribute. */
274
+ removeLayers() {
275
+ if (this.layer) {
276
+ this.layer.remove();
277
+ this.layer = null;
278
+ this.img = null;
279
+ }
280
+ if (this.scrim) {
281
+ this.scrim.remove();
282
+ this.scrim = null;
283
+ }
284
+ }
285
+ /** Restore every owned property and remove the layers. */
286
+ dispose() {
287
+ this.observer?.disconnect();
288
+ this.observer = void 0;
289
+ this.removeLayers();
290
+ const s = document.body.style;
291
+ document.body.removeAttribute(ACTIVE_ATTR);
292
+ for (const [prop, value] of this.savedTokens) s.setProperty(prop, value);
293
+ for (const [prop, value] of this.savedVars) s.setProperty(prop, value);
294
+ this.savedTokens.clear();
295
+ this.savedVars.clear();
296
+ this.active = false;
297
+ this.settings = void 0;
298
+ }
299
+ /** @returns whether a wallpaper is currently rendered. */
300
+ isActive() {
301
+ return this.active;
302
+ }
303
+ };
304
+ /** Module-level painter singleton shared by the settings row preview and the live apply. */
305
+ const backgroundPainter = new BackgroundPainter();
306
+ /**
307
+ * Apply the given settings through the shared painter (idempotent). Convenience
308
+ * used by the settings row for live previews and by the plugin apply to render
309
+ * the persisted section.
310
+ * @param settings - settings to apply (may be an unsaved draft for preview).
311
+ */
312
+ function paintBackground(settings) {
313
+ backgroundPainter.apply(settings, BACKGROUND_API_PREFIX);
314
+ }
315
+ /**
316
+ * Update one effect knob on the live backdrop without re-running the full
317
+ * apply — the hot path for slider drags (see BackgroundPainter.setKnob).
318
+ * @param key - the knob to update.
319
+ * @param value - its new value.
320
+ */
321
+ function paintBackgroundKnob(key, value) {
322
+ backgroundPainter.setKnob(key, value);
323
+ }
324
+ /** Preview variables the settings-surface preview card consumes. */
325
+ const PREVIEW_VARS = [
326
+ "--bg-opacity",
327
+ "--bg-scrim",
328
+ "--bg-object-fit",
329
+ "--bg-wallpaper-blur",
330
+ "--bg-wallpaper-scale",
331
+ "--bg-glass-blur",
332
+ "--bg-glass-saturate",
333
+ "--bg-preview-glass"
334
+ ];
335
+ /**
336
+ * Push the effect knobs onto one element (the settings-surface preview card)
337
+ * so dragging a slider updates the card without touching the live backdrop.
338
+ * The card's CSS reads the same variable names as the painter writes on body.
339
+ * @param el - the preview element.
340
+ * @param settings - the draft settings to render (may be unsaved).
341
+ */
342
+ function paintPreviewSurface(el, settings) {
343
+ const s = el.style;
344
+ s.setProperty("--bg-object-fit", settings.fit);
345
+ s.setProperty("--bg-opacity", String(settings.opacity));
346
+ s.setProperty("--bg-scrim", String(settings.scrim));
347
+ s.setProperty("--bg-wallpaper-blur", `${settings.wallpaperBlur}px`);
348
+ s.setProperty("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
349
+ const inDark = document.body.dataset.dsDarkTheme !== void 0;
350
+ if (settings.panelOpacity >= 1) {
351
+ s.setProperty("--bg-glass-blur", "0px");
352
+ s.setProperty("--bg-preview-glass", "var(--dsw-alias-bg-layer-1)");
353
+ return;
354
+ }
355
+ s.setProperty("--bg-glass-blur", `${settings.blur}px`);
356
+ s.setProperty("--bg-glass-saturate", String(1.15 + settings.blur * .03));
357
+ const alpha = GLASS_MIN_ALPHA + settings.panelOpacity * .85;
358
+ const clamped = Math.max(0, Math.min(.9, alpha));
359
+ const factor = inDark ? .4 : .8;
360
+ s.setProperty("--bg-preview-glass", `rgba(255, 255, 255, ${(clamped * factor * .8).toFixed(3)})`);
361
+ }
362
+ /** Remove every preview variable from an element (restores CSS defaults). */
363
+ function clearPreviewSurface(el) {
364
+ for (const name of PREVIEW_VARS) el.style.removeProperty(name);
365
+ }
366
+ //#endregion
367
+ //#region src/client/locales.ts
368
+ /** Chinese copy. */
369
+ const zh = {
370
+ "background.title": "自定义背景",
371
+ "background.description": "为整个应用界面设置背景图片,可调不透明度、遮罩、面板透明与毛玻璃模糊",
372
+ "background.upload": "上传图片",
373
+ "background.uploading": "上传中…",
374
+ "background.uploadMinetypes": "支持 JPG / PNG / WebP / GIF",
375
+ "background.urlPlaceholder": "或粘贴图片 http(s) 链接后回车",
376
+ "background.applyUrl": "应用链接",
377
+ "background.enabled": "背景已启用",
378
+ "background.source": "图片",
379
+ "background.effects": "效果",
380
+ "background.noPreview": "上传图片或粘贴链接后,此处实时预览效果",
381
+ "background.previewGlass": "毛玻璃",
382
+ "background.opacity": "不透明度",
383
+ "background.scrim": "遮罩",
384
+ "background.panelOpacity": "面板不透明度",
385
+ "background.blur": "毛玻璃模糊",
386
+ "background.wallpaperBlur": "壁纸模糊",
387
+ "background.fit": "填充方式",
388
+ "background.cover": "铺满",
389
+ "background.contain": "完整",
390
+ "background.clear": "清除背景",
391
+ "background.uploadFailed": "上传失败,请重试",
392
+ "background.saveFailed": "保存失败,请重试"
393
+ };
394
+ /** English copy. */
395
+ const en = {
396
+ "background.title": "Custom Background",
397
+ "background.description": "Set a background image behind the whole app surface with adjustable opacity, scrim, panel transparency and glass blur",
398
+ "background.upload": "Upload image",
399
+ "background.uploading": "Uploading…",
400
+ "background.uploadMinetypes": "JPG / PNG / WebP / GIF",
401
+ "background.urlPlaceholder": "Or paste an image http(s) link and press Enter",
402
+ "background.applyUrl": "Apply URL",
403
+ "background.enabled": "Background on",
404
+ "background.source": "Image",
405
+ "background.effects": "Effects",
406
+ "background.noPreview": "Upload an image or paste a link to preview live here",
407
+ "background.previewGlass": "Glass",
408
+ "background.opacity": "Opacity",
409
+ "background.scrim": "Scrim",
410
+ "background.panelOpacity": "Panel opacity",
411
+ "background.blur": "Glass blur",
412
+ "background.wallpaperBlur": "Wallpaper blur",
413
+ "background.fit": "Fit",
414
+ "background.cover": "Cover",
415
+ "background.contain": "Contain",
416
+ "background.clear": "Clear background",
417
+ "background.uploadFailed": "Upload failed, try again",
418
+ "background.saveFailed": "Save failed, try again"
419
+ };
420
+ //#endregion
421
+ //#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
422
+ function r(e) {
423
+ var t, f, n = "";
424
+ if ("string" == typeof e || "number" == typeof e) n += e;
425
+ else if ("object" == typeof e) if (Array.isArray(e)) {
426
+ var o = e.length;
427
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
428
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
429
+ return n;
430
+ }
431
+ function clsx() {
432
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
433
+ return n;
434
+ }
435
+ //#endregion
436
+ //#region src/client/settings-client.ts
437
+ /**
438
+ * Observable settings client (getSnapshot/subscribe — the uSES currency).
439
+ * Methods are arrow properties so React's useSyncExternalStore can call them
440
+ * without an owning `this`.
441
+ */
442
+ var SettingsClient = class {
443
+ snapshot = { status: "loading" };
444
+ listeners = /* @__PURE__ */ new Set();
445
+ /** Monotonic request id; the latest save wins even when POSTs interleave. */
446
+ saveSeq = 0;
447
+ /** Pending save promise (used by the row's flush-on-unmount). */
448
+ pending;
449
+ /** @returns the current sync snapshot (stable reference until the next change). */
450
+ getSnapshot = () => this.snapshot;
451
+ /** Observe snapshot replacements. @returns the disposer removing this listener. */
452
+ subscribe = (listener) => {
453
+ this.listeners.add(listener);
454
+ return () => {
455
+ this.listeners.delete(listener);
456
+ };
457
+ };
458
+ notify() {
459
+ for (const listener of this.listeners) listener();
460
+ }
461
+ /** Await any in-flight save (used before teardown so edits are not dropped). */
462
+ async flush() {
463
+ await this.pending;
464
+ }
465
+ /** Fetch the section from the host. */
466
+ async load() {
467
+ try {
468
+ const response = await fetch(`${BACKGROUND_API_PREFIX}/settings`);
469
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
470
+ const body = await response.json();
471
+ if (!body.ok || body.value === void 0) throw new Error("unexpected payload");
472
+ this.snapshot = {
473
+ status: "ready",
474
+ value: body.value
475
+ };
476
+ } catch {
477
+ this.snapshot = { status: "error" };
478
+ }
479
+ this.notify();
480
+ }
481
+ /**
482
+ * Persist the section through the host route. Concurrent calls are
483
+ * serialized by a request sequence: an older save that resolves after a
484
+ * newer one must not clobber the newer snapshot with its stale value.
485
+ * @param section - the complete next section.
486
+ * @returns whether the host accepted the write.
487
+ */
488
+ async save(section) {
489
+ const seq = ++this.saveSeq;
490
+ const run = async () => {
491
+ try {
492
+ const response = await fetch(`${BACKGROUND_API_PREFIX}/settings`, {
493
+ method: "POST",
494
+ headers: { "content-type": "application/json" },
495
+ body: JSON.stringify(section)
496
+ });
497
+ const body = await response.json();
498
+ if (!response.ok || !body.ok || body.value === void 0) return false;
499
+ if (seq !== this.saveSeq) return true;
500
+ this.snapshot = {
501
+ status: "ready",
502
+ value: body.value
503
+ };
504
+ this.notify();
505
+ return true;
506
+ } catch {
507
+ return false;
508
+ }
509
+ };
510
+ const promise = run();
511
+ this.pending = promise;
512
+ return promise;
513
+ }
514
+ /**
515
+ * Upload a local image file; on success stores its id into the snapshot and
516
+ * returns the resolved background url (or null on failure).
517
+ * @param file - the chosen image file.
518
+ * @returns the resolved image url, or null.
519
+ */
520
+ async upload(file) {
521
+ try {
522
+ const response = await fetch(`${BACKGROUND_API_PREFIX}/upload`, {
523
+ method: "POST",
524
+ headers: { "content-type": file.type },
525
+ body: file
526
+ });
527
+ const body = await response.json();
528
+ if (!response.ok || !body.ok || body.id === void 0 || body.url === void 0) return null;
529
+ return body.url ?? null;
530
+ } catch {
531
+ return null;
532
+ }
533
+ }
534
+ };
535
+ /** Shared client for the painter and the settings row. */
536
+ const settingsClient = new SettingsClient();
537
+ //#endregion
538
+ //#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
539
+ const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;height:132px;position:relative;overflow:hidden}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.6));border:1px solid #ffffff38;border-radius:10px;justify-content:center;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;box-shadow:inset 0 1px #ffffff4d}.-ozqdq_previewGlassText{color:#fff;opacity:.8;text-shadow:0 1px 2px #00000059;white-space:nowrap;font-size:11px;line-height:1}.-ozqdq_previewEmpty{color:var(--dsw-alias-label-tertiary);justify-content:center;align-items:center;font-size:12px;line-height:1.5;display:flex;position:absolute;inset:0}.-ozqdq_section{flex-direction:column;gap:8px;display:flex}.-ozqdq_sectionLabel{color:var(--dsw-alias-label-tertiary);letter-spacing:.01em;font-size:12px;font-weight:500;line-height:18px}.-ozqdq_sourceRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.-ozqdq_urlRow{align-items:center;gap:8px;display:flex}.-ozqdq_urlInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);min-width:0;height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;flex:1;padding:0 10px;font-size:13px;line-height:1.5}.-ozqdq_urlInput::placeholder{color:var(--dsw-alias-label-dimmed)}.-ozqdq_urlInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.-ozqdq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.-ozqdq_enabledBadge{white-space:nowrap;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.-ozqdq_btn{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:14px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;line-height:18px;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}.-ozqdq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.-ozqdq_btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.-ozqdq_btn:disabled{opacity:.45;cursor:default}.-ozqdq_btnPrimary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.-ozqdq_btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.-ozqdq_btnDanger{color:var(--dsw-alias-label-error)}.-ozqdq_btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.-ozqdq_sliderRow{align-items:center;gap:10px;display:flex}.-ozqdq_sliderLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_slider{-webkit-appearance:none;appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:18px;margin:0}.-ozqdq_slider::-webkit-slider-runnable-track{background:linear-gradient(90deg, var(--dsw-alias-brand-primary) var(--bg-fill,0%), var(--dsw-alias-border-l3) var(--bg-fill,0%));border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-track{background:var(--dsw-alias-border-l3);border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:2px;height:4px}.-ozqdq_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%;margin-top:-5px}.-ozqdq_slider::-moz-range-thumb{border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%}.-ozqdq_slider:hover::-webkit-slider-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:hover::-moz-range-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:active::-webkit-slider-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:active::-moz-range-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:focus-visible{outline:none}.-ozqdq_slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_slider:focus-visible::-moz-range-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_sliderValue{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:0 0 52px;font-size:13px;line-height:1.5}.-ozqdq_segRow{align-items:center;gap:10px;display:flex}.-ozqdq_segLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_segGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex:1;gap:2px;min-width:0;padding:2px;display:inline-flex}.-ozqdq_segBtn{height:24px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;flex:1;padding:0 16px;font-size:12px;line-height:24px}.-ozqdq_segBtn:hover:not(.-ozqdq_segActive){color:var(--dsw-alias-label-primary)}.-ozqdq_segActive{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-weight:500}.-ozqdq_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5}.-ozqdq_clearRow{align-items:center;gap:10px;display:flex}@media (prefers-reduced-motion:reduce){.-ozqdq_btn{transition:none}}";
540
+ const tagId = "deepseek-harness-background/SettingsRow.module.css";
541
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
542
+ const tag = document.createElement("style");
543
+ tag.dataset.plugin = "deepseek-harness-background";
544
+ tag.dataset.pluginCss = tagId;
545
+ tag.textContent = css;
546
+ document.head.appendChild(tag);
547
+ }
548
+ var SettingsRow_module_css_default = {
549
+ "titleRow": "-ozqdq_titleRow",
550
+ "clearBtn": "-ozqdq_clearBtn",
551
+ "preview": "-ozqdq_preview",
552
+ "previewGlass": "-ozqdq_previewGlass",
553
+ "header": "-ozqdq_header",
554
+ "previewEmpty": "-ozqdq_previewEmpty",
555
+ "sectionLabel": "-ozqdq_sectionLabel",
556
+ "btnDanger": "-ozqdq_btnDanger",
557
+ "sliderRow": "-ozqdq_sliderRow",
558
+ "segGroup": "-ozqdq_segGroup",
559
+ "clearRow": "-ozqdq_clearRow",
560
+ "slider": "-ozqdq_slider",
561
+ "sliderValue": "-ozqdq_sliderValue",
562
+ "hint": "-ozqdq_hint",
563
+ "segLabel": "-ozqdq_segLabel",
564
+ "segBtn": "-ozqdq_segBtn",
565
+ "sliderLabel": "-ozqdq_sliderLabel",
566
+ "segRow": "-ozqdq_segRow",
567
+ "desc": "-ozqdq_desc",
568
+ "urlRow": "-ozqdq_urlRow",
569
+ "previewImg": "-ozqdq_previewImg",
570
+ "segActive": "-ozqdq_segActive",
571
+ "error": "-ozqdq_error",
572
+ "title": "-ozqdq_title",
573
+ "urlInput": "-ozqdq_urlInput",
574
+ "btn": "-ozqdq_btn",
575
+ "btnPrimary": "-ozqdq_btnPrimary",
576
+ "previewGlassText": "-ozqdq_previewGlassText",
577
+ "enabledBadge": "-ozqdq_enabledBadge",
578
+ "row": "-ozqdq_row",
579
+ "previewScrim": "-ozqdq_previewScrim",
580
+ "section": "-ozqdq_section",
581
+ "sourceRow": "-ozqdq_sourceRow"
582
+ };
583
+ //#endregion
584
+ //#region src/client/SettingsRow.tsx
585
+ /**
586
+ * Background settings row — the editable form rendered inside the General
587
+ * settings section (`settings.general.item` slot), in the same visual area as
588
+ * the official Appearance row. Layout mirrors the reference wallpaper engine
589
+ * picker (preview surface + grouped sections), styled with the harness alias
590
+ * tokens so it reads as part of the settings chrome.
591
+ *
592
+ * Interaction model (fixes the previous jank):
593
+ * - Sliders STEP in 5% (or 2px/5px) increments, so dragging snaps between
594
+ * discrete values instead of firing dozens of sub-percent updates.
595
+ * - While dragging, only the CSS variables change: the slider holds its own
596
+ * state (no parent re-render) and the preview card repaints in isolation.
597
+ * React's onChange maps to the native `input` event and would fire on every
598
+ * step, so a commit is deferred to release (pointerup / keyup / blur) and
599
+ * debounced — one POST per gesture, never per tick.
600
+ * - A `lastSavedRef` distinguishes our own saves from external changes, so
601
+ * the snapshot sync can never clobber a draft the user is still editing.
602
+ */
603
+ /** Defaults mirror the host schema (also the pre-load draft fallback). */
604
+ const DEFAULTS = {
605
+ enabled: false,
606
+ uploadId: "",
607
+ url: "",
608
+ opacity: 1,
609
+ scrim: DEFAULT_SCRIM,
610
+ panelOpacity: DEFAULT_PANEL_OPACITY,
611
+ blur: 16,
612
+ wallpaperBlur: 0,
613
+ fit: DEFAULT_FIT
614
+ };
615
+ /** Per-field slider geometry: 5% steps for ratios, fine px steps for radii so
616
+ * the two blur sliders feel smooth near small values. Bounds come from the
617
+ * shared settings constants so the schema boundary and the UI cannot drift. */
618
+ const SLIDER_SPEC = {
619
+ opacity: {
620
+ min: 0,
621
+ max: 1,
622
+ step: .05,
623
+ unit: "percent"
624
+ },
625
+ scrim: {
626
+ min: 0,
627
+ max: SCRIM_MAX,
628
+ step: .05,
629
+ unit: "percent"
630
+ },
631
+ panelOpacity: {
632
+ min: 0,
633
+ max: 1,
634
+ step: .05,
635
+ unit: "percent"
636
+ },
637
+ blur: {
638
+ min: 0,
639
+ max: 40,
640
+ step: 1,
641
+ unit: "px"
642
+ },
643
+ wallpaperBlur: {
644
+ min: 0,
645
+ max: 60,
646
+ step: 2,
647
+ unit: "px"
648
+ }
649
+ };
650
+ /** Derive the bare upload id from a resolve url like /api/bg-wallpaper/image/<id>. */
651
+ function stripId(url) {
652
+ const prefix = `${BACKGROUND_API_PREFIX}/image/`;
653
+ return url.startsWith(prefix) ? url.slice(prefix.length) : "";
654
+ }
655
+ /** Read the settings store (uSES-friendly). */
656
+ function readSnapshot() {
657
+ return settingsClient.getSnapshot();
658
+ }
659
+ /** Field-wise equality for distinguishing our own saves from external changes. */
660
+ function sameSettings(a, b) {
661
+ return a.enabled === b.enabled && a.uploadId === b.uploadId && a.url === b.url && a.opacity === b.opacity && a.scrim === b.scrim && a.panelOpacity === b.panelOpacity && a.blur === b.blur && a.wallpaperBlur === b.wallpaperBlur && a.fit === b.fit;
662
+ }
663
+ /**
664
+ * A stepped range control. While dragging it re-renders only itself: the value
665
+ * is local state, `onLive(name, value)` repaints (CSS variables, no React
666
+ * state), and `onCommit()` fires once on release through a debounced save.
667
+ */
668
+ function ControlSlider(props) {
669
+ const { name, label, value, onLive, onCommit } = props;
670
+ const spec = SLIDER_SPEC[name];
671
+ const [local, setLocal] = (0, react.useState)(value);
672
+ const localRef = (0, react.useRef)(value);
673
+ const dragging = (0, react.useRef)(false);
674
+ const onLiveRef = (0, react.useRef)(onLive);
675
+ const onCommitRef = (0, react.useRef)(onCommit);
676
+ onLiveRef.current = onLive;
677
+ onCommitRef.current = onCommit;
678
+ (0, react.useEffect)(() => {
679
+ if (!dragging.current && localRef.current !== value) {
680
+ localRef.current = value;
681
+ setLocal(value);
682
+ }
683
+ }, [value]);
684
+ const handleLive = (e) => {
685
+ dragging.current = true;
686
+ const v = Number(e.currentTarget.value);
687
+ localRef.current = v;
688
+ setLocal(v);
689
+ syncTrackFill(e.currentTarget);
690
+ onLiveRef.current(name, v);
691
+ };
692
+ const handleCommit = () => {
693
+ if (!dragging.current) return;
694
+ dragging.current = false;
695
+ onCommitRef.current();
696
+ };
697
+ const text = spec.unit === "percent" ? `${Math.round(local * 100)}%` : `${local}px`;
698
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
699
+ className: SettingsRow_module_css_default.sliderRow,
700
+ children: [
701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
702
+ className: SettingsRow_module_css_default.sliderLabel,
703
+ children: label
704
+ }),
705
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
706
+ type: "range",
707
+ className: SettingsRow_module_css_default.slider,
708
+ ref: (el) => {
709
+ if (el) syncTrackFill(el);
710
+ },
711
+ min: spec.min,
712
+ max: spec.max,
713
+ step: spec.step,
714
+ value: String(local),
715
+ "aria-label": label,
716
+ onInput: handleLive,
717
+ onPointerUp: handleCommit,
718
+ onKeyUp: (e) => {
719
+ if (e.key.startsWith("Arrow") || e.key === "Home" || e.key === "End" || e.key === "PageUp" || e.key === "PageDown") handleCommit();
720
+ },
721
+ onBlur: handleCommit
722
+ }),
723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
724
+ className: SettingsRow_module_css_default.sliderValue,
725
+ children: text
726
+ })
727
+ ]
728
+ });
729
+ }
730
+ /** Write the filled-track fraction (%) of a range input into its --bg-fill
731
+ * variable (webkit has no range-progress pseudo-element; moz draws its own). */
732
+ function syncTrackFill(el) {
733
+ const min = Number(el.min);
734
+ const max = Number(el.max);
735
+ const value = Number(el.value);
736
+ const pct = max === min ? 0 : (value - min) / (max - min) * 100;
737
+ el.style.setProperty("--bg-fill", `${pct}%`);
738
+ }
739
+ /**
740
+ * The background settings row. A draft mirrors the persisted section; dragging
741
+ * repaints the preview card and the live backdrop through CSS variables, and a
742
+ * release commits once through the plugin's own host route.
743
+ */
744
+ function BackgroundSettingsRow({ t }) {
745
+ const snapshot = (0, react.useSyncExternalStore)(settingsClient.subscribe, readSnapshot);
746
+ const [draft, setDraft] = (0, react.useState)(DEFAULTS);
747
+ const [uploading, setUploading] = (0, react.useState)(false);
748
+ const [urlText, setUrlText] = (0, react.useState)("");
749
+ const [error, setError] = (0, react.useState)("");
750
+ const fileInput = (0, react.useRef)(null);
751
+ const previewRef = (0, react.useRef)(null);
752
+ const draftRef = (0, react.useRef)(DEFAULTS);
753
+ const editingRef = (0, react.useRef)(false);
754
+ const lastSavedRef = (0, react.useRef)(null);
755
+ const dirtyRef = (0, react.useRef)(false);
756
+ const commitTimer = (0, react.useRef)(void 0);
757
+ const editingTimer = (0, react.useRef)(void 0);
758
+ /** Repaint the live backdrop + preview card from one draft (no React state). */
759
+ const paintLive = (0, react.useCallback)((next) => {
760
+ paintBackground(next);
761
+ if (previewRef.current) paintPreviewSurface(previewRef.current, next);
762
+ }, []);
763
+ /** Mark the row as being edited; clears after a quiet 600ms window. */
764
+ const markEditing = (0, react.useCallback)(() => {
765
+ editingRef.current = true;
766
+ clearTimeout(editingTimer.current);
767
+ editingTimer.current = window.setTimeout(() => {
768
+ editingRef.current = false;
769
+ }, 600);
770
+ }, []);
771
+ /** Slider live handler (stable identity for the memoized sliders). */
772
+ const onLive = (0, react.useCallback)((key, value) => {
773
+ const next = {
774
+ ...draftRef.current,
775
+ [key]: value
776
+ };
777
+ draftRef.current = next;
778
+ markEditing();
779
+ if (key === "panelOpacity") paintBackground(next);
780
+ else paintBackgroundKnob(key, value);
781
+ if (previewRef.current) paintPreviewSurface(previewRef.current, next);
782
+ }, [markEditing]);
783
+ /** Persist one section through the host route and adopt it on success. */
784
+ const saveNow = (0, react.useCallback)(async (next) => {
785
+ dirtyRef.current = true;
786
+ const ok = await settingsClient.save(next);
787
+ if (ok) dirtyRef.current = false;
788
+ setError(ok ? "" : t("background.saveFailed"));
789
+ if (ok) {
790
+ lastSavedRef.current = next;
791
+ draftRef.current = next;
792
+ setDraft(next);
793
+ if (!next.uploadId) setUrlText(next.url);
794
+ }
795
+ }, [t]);
796
+ /** Debounced commit: one POST per drag gesture, from the latest draft. */
797
+ const onCommit = (0, react.useCallback)(() => {
798
+ clearTimeout(commitTimer.current);
799
+ commitTimer.current = window.setTimeout(() => {
800
+ saveNow(draftRef.current);
801
+ }, 120);
802
+ }, [saveNow]);
803
+ (0, react.useEffect)(() => {
804
+ if (snapshot.status !== "ready" || snapshot.value === void 0) return;
805
+ const saved = lastSavedRef.current;
806
+ if ((saved === null || !sameSettings(saved, snapshot.value)) && !editingRef.current) {
807
+ lastSavedRef.current = snapshot.value;
808
+ draftRef.current = snapshot.value;
809
+ setDraft(snapshot.value);
810
+ setUrlText(snapshot.value.uploadId ? "" : snapshot.value.url);
811
+ if (previewRef.current) paintPreviewSurface(previewRef.current, snapshot.value);
812
+ }
813
+ }, [snapshot]);
814
+ (0, react.useEffect)(() => () => {
815
+ clearTimeout(commitTimer.current);
816
+ clearTimeout(editingTimer.current);
817
+ if (dirtyRef.current) settingsClient.flush();
818
+ }, []);
819
+ const handleFile = async (file) => {
820
+ if (!file) return;
821
+ markEditing();
822
+ setUploading(true);
823
+ setError("");
824
+ try {
825
+ const url = await settingsClient.upload(file);
826
+ if (url === null) {
827
+ setError(t("background.uploadFailed"));
828
+ return;
829
+ }
830
+ const next = {
831
+ ...draftRef.current,
832
+ uploadId: stripId(url),
833
+ url: "",
834
+ enabled: true
835
+ };
836
+ draftRef.current = next;
837
+ setDraft(next);
838
+ setUrlText("");
839
+ paintLive(next);
840
+ await saveNow(next);
841
+ } finally {
842
+ setUploading(false);
843
+ }
844
+ };
845
+ const handleUrlSubmit = () => {
846
+ const url = urlText.trim();
847
+ if (!/^https?:\/\//i.test(url)) {
848
+ setError(t("background.saveFailed"));
849
+ return;
850
+ }
851
+ markEditing();
852
+ const next = {
853
+ ...draftRef.current,
854
+ uploadId: "",
855
+ url,
856
+ enabled: true
857
+ };
858
+ draftRef.current = next;
859
+ setDraft(next);
860
+ paintLive(next);
861
+ saveNow(next);
862
+ };
863
+ const handleClear = () => {
864
+ const next = {
865
+ ...draftRef.current,
866
+ uploadId: "",
867
+ url: "",
868
+ enabled: false
869
+ };
870
+ draftRef.current = next;
871
+ setDraft(next);
872
+ setUrlText("");
873
+ if (previewRef.current) clearPreviewSurface(previewRef.current);
874
+ paintBackground(next);
875
+ saveNow(next);
876
+ };
877
+ const handleFit = (mode) => {
878
+ markEditing();
879
+ const next = {
880
+ ...draftRef.current,
881
+ fit: mode
882
+ };
883
+ draftRef.current = next;
884
+ setDraft(next);
885
+ paintLive(next);
886
+ saveNow(next);
887
+ };
888
+ const sourceUrl = draft.uploadId ? `${BACKGROUND_API_PREFIX}/image/${draft.uploadId}` : draft.url;
889
+ const hasImage = sourceUrl !== "";
890
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
891
+ className: SettingsRow_module_css_default.row,
892
+ children: [
893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
894
+ className: SettingsRow_module_css_default.header,
895
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
896
+ className: SettingsRow_module_css_default.titleRow,
897
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
898
+ className: SettingsRow_module_css_default.title,
899
+ children: t("background.title")
900
+ }), hasImage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
901
+ type: "button",
902
+ className: clsx(SettingsRow_module_css_default.btn, SettingsRow_module_css_default.btnDanger, SettingsRow_module_css_default.clearBtn),
903
+ onClick: handleClear,
904
+ children: t("background.clear")
905
+ })]
906
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
907
+ className: SettingsRow_module_css_default.desc,
908
+ children: t("background.description")
909
+ })]
910
+ }),
911
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
912
+ className: SettingsRow_module_css_default.preview,
913
+ ref: previewRef,
914
+ children: hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
916
+ className: SettingsRow_module_css_default.previewImg,
917
+ src: sourceUrl,
918
+ alt: "",
919
+ draggable: false
920
+ }),
921
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: SettingsRow_module_css_default.previewScrim }),
922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
923
+ className: SettingsRow_module_css_default.previewGlass,
924
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
925
+ className: SettingsRow_module_css_default.previewGlassText,
926
+ children: t("background.previewGlass")
927
+ })
928
+ })
929
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
930
+ className: SettingsRow_module_css_default.previewEmpty,
931
+ children: t("background.noPreview")
932
+ })
933
+ }),
934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
935
+ className: SettingsRow_module_css_default.section,
936
+ children: [
937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
938
+ className: SettingsRow_module_css_default.sectionLabel,
939
+ children: t("background.source")
940
+ }),
941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
942
+ className: SettingsRow_module_css_default.sourceRow,
943
+ children: [
944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
945
+ type: "file",
946
+ ref: fileInput,
947
+ hidden: true,
948
+ accept: "image/jpeg,image/png,image/webp,image/gif",
949
+ onChange: (e) => {
950
+ handleFile(e.currentTarget.files?.[0]);
951
+ e.currentTarget.value = "";
952
+ }
953
+ }),
954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
955
+ type: "button",
956
+ className: clsx(SettingsRow_module_css_default.btn, SettingsRow_module_css_default.btnPrimary),
957
+ disabled: uploading,
958
+ onClick: () => fileInput.current?.click(),
959
+ children: uploading ? t("background.uploading") : t("background.upload")
960
+ }),
961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
962
+ className: SettingsRow_module_css_default.hint,
963
+ children: t("background.uploadMinetypes")
964
+ }),
965
+ draft.uploadId && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
966
+ className: SettingsRow_module_css_default.enabledBadge,
967
+ children: ["✓ ", t("background.enabled")]
968
+ })
969
+ ]
970
+ }),
971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
972
+ className: SettingsRow_module_css_default.urlRow,
973
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
974
+ className: SettingsRow_module_css_default.urlInput,
975
+ value: urlText,
976
+ placeholder: t("background.urlPlaceholder"),
977
+ onKeyDown: (e) => {
978
+ if (e.key === "Enter") handleUrlSubmit();
979
+ },
980
+ onChange: (e) => setUrlText(e.currentTarget.value)
981
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
982
+ type: "button",
983
+ className: SettingsRow_module_css_default.btn,
984
+ disabled: !urlText.trim(),
985
+ onClick: handleUrlSubmit,
986
+ children: t("background.applyUrl")
987
+ })]
988
+ })
989
+ ]
990
+ }),
991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
992
+ className: SettingsRow_module_css_default.section,
993
+ children: [
994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
995
+ className: SettingsRow_module_css_default.sectionLabel,
996
+ children: t("background.effects")
997
+ }),
998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
999
+ name: "opacity",
1000
+ label: t("background.opacity"),
1001
+ value: draft.opacity,
1002
+ onLive,
1003
+ onCommit
1004
+ }),
1005
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
1006
+ name: "scrim",
1007
+ label: t("background.scrim"),
1008
+ value: draft.scrim,
1009
+ onLive,
1010
+ onCommit
1011
+ }),
1012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
1013
+ name: "panelOpacity",
1014
+ label: t("background.panelOpacity"),
1015
+ value: draft.panelOpacity,
1016
+ onLive,
1017
+ onCommit
1018
+ }),
1019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
1020
+ name: "blur",
1021
+ label: t("background.blur"),
1022
+ value: draft.blur,
1023
+ onLive,
1024
+ onCommit
1025
+ }),
1026
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ControlSlider, {
1027
+ name: "wallpaperBlur",
1028
+ label: t("background.wallpaperBlur"),
1029
+ value: draft.wallpaperBlur,
1030
+ onLive,
1031
+ onCommit
1032
+ }),
1033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1034
+ className: SettingsRow_module_css_default.segRow,
1035
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1036
+ className: SettingsRow_module_css_default.segLabel,
1037
+ children: t("background.fit")
1038
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1039
+ className: SettingsRow_module_css_default.segGroup,
1040
+ role: "group",
1041
+ "aria-label": t("background.fit"),
1042
+ children: FIT_MODES.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1043
+ type: "button",
1044
+ className: clsx(SettingsRow_module_css_default.segBtn, draft.fit === mode && SettingsRow_module_css_default.segActive),
1045
+ onClick: () => handleFit(mode),
1046
+ children: t(mode === "cover" ? "background.cover" : "background.contain")
1047
+ }, mode))
1048
+ })]
1049
+ })
1050
+ ]
1051
+ }),
1052
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1053
+ className: SettingsRow_module_css_default.error,
1054
+ role: "alert",
1055
+ children: error
1056
+ }),
1057
+ hasImage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1058
+ className: SettingsRow_module_css_default.clearRow,
1059
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1060
+ type: "button",
1061
+ className: clsx(SettingsRow_module_css_default.btn, SettingsRow_module_css_default.btnDanger),
1062
+ onClick: handleClear,
1063
+ children: t("background.clear")
1064
+ })
1065
+ })
1066
+ ]
1067
+ });
1068
+ }
1069
+ //#endregion
1070
+ //#region src/client/index.ts
1071
+ /**
1072
+ * Service injections this entry declares: the `slots` (runtime) and `locale`
1073
+ * services only — the settings surface talks to the plugin's own host route
1074
+ * over plain same-origin fetch, so no settings/connection service is needed.
1075
+ */
1076
+ const inject = ["slots", "locale"];
1077
+ /**
1078
+ * Client plugin body: paint the persisted background live and register the
1079
+ * General-settings row.
1080
+ * @param ctx - client cordis context (slots/locale injected).
1081
+ */
1082
+ function apply(ctx) {
1083
+ const paint = () => {
1084
+ const snapshot = settingsClient.getSnapshot();
1085
+ const value = snapshot.status === "ready" ? snapshot.value : void 0;
1086
+ if (value === void 0 || !value.enabled) {
1087
+ backgroundPainter.dispose();
1088
+ return;
1089
+ }
1090
+ paintBackground(value);
1091
+ };
1092
+ const unsubscribe = settingsClient.subscribe(paint);
1093
+ settingsClient.load();
1094
+ ctx.effect(() => ctx.locale.register("ui-background", {
1095
+ zh,
1096
+ en
1097
+ }), "deepseek-harness-background: dictionaries");
1098
+ ctx.slots.inject("settings.general.item", () => ctx.slots.register({
1099
+ name: "settings.general.item",
1100
+ id: "deepseek-harness-background",
1101
+ order: 600,
1102
+ locale: "ui-background"
1103
+ }, BackgroundSettingsRow));
1104
+ ctx.effect(() => () => {
1105
+ unsubscribe();
1106
+ backgroundPainter.dispose();
1107
+ }, "deepseek-harness-background: background surface");
1108
+ }
1109
+ //#endregion
1110
+ exports.apply = apply;
1111
+ exports.inject = inject;
1112
+ return module.exports;
1113
+ }
1114
+ });
1115
+
1116
+ //# sourceMappingURL=client.js.map