dsh-chat-background 0.0.0-stage → 1.0.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/client.js ADDED
@@ -0,0 +1,3229 @@
1
+ /**
2
+ * dsh-chat-background v1.0.0
3
+ * Chat background image, sidebar veil, composer strip, task progress bar and all-session token total for the DeepSeek Harness (DSH) Web GUI.
4
+ * @author LittleLemonJam
5
+ * @license MIT
6
+ * @homepage https://github.com/LittleLemonJam/dsh-chat-background
7
+ *
8
+ * Generated by scripts/build.mjs from src/ — do not edit this file by hand.
9
+ */
10
+ (() => {
11
+ // src/constants.js
12
+ var LIST_URL = "/chat-backgrounds/list";
13
+ var FILE_URL = "/chat-backgrounds/files/";
14
+ var FOLDER_URL = "/chat-backgrounds/folder";
15
+ var TOKEN_URL = "/chat-backgrounds/token-total";
16
+ var FOLDER_HEADER = "x-dsh-chat-background";
17
+ var FOLDER_HEADER_VALUE = "1";
18
+ var SELECTION_KEY = "dsh-chat-background:selection";
19
+ var MODE_KEY = "dsh-chat-background:mode";
20
+ var ROTATION_INTERVAL_KEY = "dsh-chat-background:rotation-interval";
21
+ var ROTATION_DURATION_KEY = "dsh-chat-background:rotation-duration";
22
+ var ROTATION_EFFECT_KEY = "dsh-chat-background:rotation-effect";
23
+ var ROTATION_GROUP_KEY = "dsh-chat-background:rotation-group";
24
+ var ROTATION_ORDER_KEY = "dsh-chat-background:rotation-order";
25
+ var SCRIM_KEY = "dsh-chat-background:scrim";
26
+ var VEIL_KEY = "dsh-chat-background:veil";
27
+ var SCRIM_LEFT_KEY = "dsh-chat-background:scrim-left";
28
+ var SCRIM_MAIN_KEY = "dsh-chat-background:scrim-main";
29
+ var SCRIM_RIGHT_KEY = "dsh-chat-background:scrim-right";
30
+ var STRIP_KEY = "dsh-chat-background:strip";
31
+ var STRIP_COLOR_KEY = "dsh-chat-background:strip-color";
32
+ var PROGRESS_KEY = "dsh-chat-background:progress";
33
+ var PROGRESS_COLOR_KEY = "dsh-chat-background:progress-color";
34
+ var TOKEN_KEY = "dsh-chat-background:token";
35
+ var TOKEN_SIZE_KEY = "dsh-chat-background:token-size";
36
+ var ACTIVE_ATTRIBUTE = "data-dsh-chat-background";
37
+ var LIFT_ATTRIBUTE = "data-dsh-chat-background-lift";
38
+ var SIDEBAR_COLUMN_ATTRIBUTE = "data-dsh-chat-background-column";
39
+ var MAIN_COLUMN_ATTRIBUTE = "data-dsh-chat-background-main";
40
+ var RIGHT_COLUMN_ATTRIBUTE = "data-dsh-chat-background-right";
41
+ var RIGHT_PANEL_ATTRIBUTE = "data-dsh-chat-background-right-panel";
42
+ var RIGHT_PANEL_FULL_ATTRIBUTE = "data-dsh-chat-background-right-panel-full";
43
+ var LAYOUT_FRAME_ATTRIBUTE = "data-dsh-chat-background-frame";
44
+ var FRAME_ANIMATING_ATTRIBUTE = "data-dsh-chat-background-frame-animating";
45
+ var RIGHT_FULLSCREEN_ATTRIBUTE = "data-dsh-chat-background-right-fullscreen";
46
+ var NO_ANIMATION_ATTRIBUTE = "data-dsh-chat-background-no-animation";
47
+ var BACKGROUND_LAYER_ATTRIBUTE = "data-dsh-chat-background-layer";
48
+ var STRIP_ATTRIBUTE = "data-dsh-chat-strip";
49
+ var TOKEN_NODE_ATTRIBUTE = "data-dsh-token-total";
50
+ var CSS_TAG_ID = "dsh-chat-background/background.css";
51
+ var DYNAMIC_TAG_ID = "dsh-chat-background/measured.css";
52
+ var INLINE_PROPERTIES = [
53
+ "--dsh-chat-background-image",
54
+ "--dsh-chat-background-scrim-left",
55
+ "--dsh-chat-background-scrim-main",
56
+ "--dsh-chat-background-scrim-right",
57
+ "--dsh-chat-strip-bg",
58
+ "--dsh-progress-color",
59
+ "--dsh-token-size"
60
+ ];
61
+ var SCRIM_MIN = 0;
62
+ var SCRIM_MAX = 100;
63
+ var SCRIM_DEFAULT = 58;
64
+ var VEIL_MIN = 0;
65
+ var VEIL_MAX = 90;
66
+ var VEIL_DEFAULT = 45;
67
+ var TOKEN_SIZE_MIN = 10;
68
+ var TOKEN_SIZE_MAX = 20;
69
+ var TOKEN_SIZE_DEFAULT = 14;
70
+ var STRIP_DEFAULT_HEX = "#f2f3f5";
71
+ var PROGRESS_DEFAULT_HEX = "#4176e6";
72
+ var ROTATION_INTERVAL_MIN = 5;
73
+ var ROTATION_INTERVAL_MAX = 300;
74
+ var ROTATION_INTERVAL_DEFAULT = 30;
75
+ var ROTATION_DURATION_MIN = 0;
76
+ var ROTATION_DURATION_MAX = 3;
77
+ var ROTATION_DURATION_DEFAULT = 0.6;
78
+ var ROTATION_EFFECTS = ["fade", "fade-through", "none"];
79
+ var ROTATION_EFFECT_DEFAULT = "fade";
80
+ var ROTATION_EVENT = "dsh-chat-background:selection-changed";
81
+ var ROTATION_LAYER_ATTRIBUTE = "data-dsh-chat-background-rotate-layer";
82
+ var PROGRESS_DEFAULT_TOKEN = "--dsw-alias-brand-primary";
83
+ var TOKEN_POLL_MS = 6e4;
84
+ var SECTION_ID = "chat-background";
85
+ var LOCALE_NS = "chat-background";
86
+ var SECTION_ORDER = 5;
87
+ var PROGRESS_DOCK_ORDER = 12;
88
+ var ACCEPTED_IMAGE_EXTENSIONS = [".png", ".apng", ".jpg", ".jpeg", ".webp", ".gif", ".avif", ".bmp", ".svg"];
89
+
90
+ // src/diagnostics.js
91
+ var state = { startedAt: Date.now() };
92
+ function recordDiagnostics(patch) {
93
+ state = { ...state, ...patch };
94
+ }
95
+ function diagnosticsSnapshot() {
96
+ return {
97
+ version: true ? "1.0.0" : "unknown",
98
+ updatedAt: Date.now(),
99
+ ...state
100
+ };
101
+ }
102
+
103
+ // src/lib/layout.js
104
+ function isFullscreenPanel(panel, viewport) {
105
+ return panel.width >= viewport.width - 2 && panel.height >= viewport.height - 2 && Math.abs(panel.left) <= 1 && Math.abs(panel.top) <= 1;
106
+ }
107
+
108
+ // src/lib/color.js
109
+ function cssColorToHex(value) {
110
+ const raw = String(value ?? "").trim();
111
+ if (raw === "") return null;
112
+ const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/iu.exec(raw);
113
+ if (short !== null) return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`;
114
+ if (/^#[0-9a-f]{6}$/iu.test(raw)) return raw;
115
+ const parts = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/iu.exec(raw);
116
+ if (parts === null) return null;
117
+ const channel = (text) => Number.parseInt(text, 10).toString(16).padStart(2, "0");
118
+ return `#${channel(parts[1])}${channel(parts[2])}${channel(parts[3])}`;
119
+ }
120
+
121
+ // src/settings.js
122
+ var installed = null;
123
+ function installSettings(value) {
124
+ installed = value;
125
+ }
126
+ function settings() {
127
+ if (installed === null) throw new Error("dsh-chat-background: settings were read before the plugin applied");
128
+ return installed;
129
+ }
130
+
131
+ // src/strip.js
132
+ var cachedInputCard = null;
133
+ var cachedInputCardSource = "none";
134
+ var cachedUsageRow = null;
135
+ var cachedUsageRowStable = false;
136
+ var stripResizeObserver = null;
137
+ var observedComposerWrapper = null;
138
+ function ensureComposerSizing(wrapper) {
139
+ if (typeof ResizeObserver === "undefined") return;
140
+ if (observedComposerWrapper === wrapper && stripResizeObserver !== null) return;
141
+ if (stripResizeObserver === null) stripResizeObserver = new ResizeObserver(() => renderComposerStrip());
142
+ else stripResizeObserver.disconnect();
143
+ stripResizeObserver.observe(wrapper);
144
+ observedComposerWrapper = wrapper;
145
+ }
146
+ function disconnectComposerSizing() {
147
+ if (stripResizeObserver !== null) {
148
+ stripResizeObserver.disconnect();
149
+ stripResizeObserver = null;
150
+ observedComposerWrapper = null;
151
+ }
152
+ cachedInputCard = null;
153
+ cachedInputCardSource = "none";
154
+ cachedUsageRow = null;
155
+ cachedUsageRowStable = false;
156
+ }
157
+ function findInputCard(wrapper) {
158
+ if (cachedInputCard !== null && cachedInputCard !== wrapper && !wrapper.contains(cachedInputCard)) {
159
+ cachedInputCard = null;
160
+ }
161
+ if (cachedInputCard !== null) return cachedInputCard;
162
+ const explicit = wrapper.querySelector("[data-composer-card]");
163
+ if (explicit !== null && explicit.getBoundingClientRect().width > 0) {
164
+ cachedInputCard = explicit;
165
+ cachedInputCardSource = "data-composer-card";
166
+ return explicit;
167
+ }
168
+ let best = null;
169
+ let bestArea = 0;
170
+ for (const candidate of wrapper.querySelectorAll("*")) {
171
+ const hasEditor = candidate.matches('textarea, [contenteditable="true"], [data-composer-input]') || candidate.querySelector('textarea, [contenteditable="true"], [data-composer-input]') !== null;
172
+ if (!hasEditor) continue;
173
+ const style = getComputedStyle(candidate);
174
+ if (style.backgroundColor === "rgba(0, 0, 0, 0)" && style.backgroundImage === "none") continue;
175
+ const rect = candidate.getBoundingClientRect();
176
+ const area = rect.width * rect.height;
177
+ if (rect.width === 0 || area <= bestArea) continue;
178
+ bestArea = area;
179
+ best = candidate;
180
+ }
181
+ if (best !== null) {
182
+ cachedInputCard = best;
183
+ cachedInputCardSource = "heuristic";
184
+ }
185
+ return best ?? wrapper;
186
+ }
187
+ function findComposerWrapper() {
188
+ const barSlot = document.querySelector('[data-slot="conversation.composer.bar"]');
189
+ const primary = barSlot === null ? null : barSlot.firstElementChild;
190
+ if (primary !== null) return primary;
191
+ const card = document.querySelector("[data-composer-card]");
192
+ const dock = document.querySelector("[data-composer-dock]");
193
+ if (card === null || dock === null) return null;
194
+ let node = dock.parentElement;
195
+ while (node !== null && !node.contains(card)) node = node.parentElement;
196
+ return node === null || node === document.body ? null : node;
197
+ }
198
+ function rectOf(element) {
199
+ const own = element.getBoundingClientRect();
200
+ if (own.width > 0 && own.height > 0) return own;
201
+ const pills = [...element.querySelectorAll("[data-composer-stat]")].map((pill) => pill.getBoundingClientRect()).filter((rect) => rect.width > 0 && rect.height > 0);
202
+ if (pills.length === 0) return own;
203
+ const left = Math.min(...pills.map((rect) => rect.left));
204
+ const top = Math.min(...pills.map((rect) => rect.top));
205
+ const right = Math.max(...pills.map((rect) => rect.right));
206
+ const bottom = Math.max(...pills.map((rect) => rect.bottom));
207
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
208
+ }
209
+ function isUsageRowShape(candidate, wrapperRect) {
210
+ const rect = rectOf(candidate);
211
+ if (rect.height === 0 || rect.height > 40) return false;
212
+ if (rect.width < 120 || rect.width > 900) return false;
213
+ return rect.top >= wrapperRect.top - 2 && rect.bottom <= wrapperRect.bottom + 80;
214
+ }
215
+ function findStableUsageRowCandidates(wrapper) {
216
+ const dock = wrapper.querySelector("[data-composer-dock]");
217
+ if (dock === null) return [];
218
+ const pills = [...dock.querySelectorAll("[data-composer-stat]")];
219
+ if (pills.length === 0) return [];
220
+ let row = pills[0].parentElement;
221
+ while (row !== null && row !== dock && (!pills.every((pill) => row.contains(pill)) || row.children.length > 4)) {
222
+ row = row.parentElement;
223
+ }
224
+ return row !== null && row !== dock ? [row, dock] : [dock];
225
+ }
226
+ function findUsageRow(wrapper) {
227
+ if (cachedUsageRow !== null && !cachedUsageRow.isConnected) cachedUsageRow = null;
228
+ if (cachedUsageRow !== null && cachedUsageRowStable && cachedUsageRow.querySelector("[data-composer-stat]") === null) {
229
+ cachedUsageRow = null;
230
+ }
231
+ if (cachedUsageRow !== null) return cachedUsageRow;
232
+ const wrapperRect = wrapper.getBoundingClientRect();
233
+ for (const candidate of findStableUsageRowCandidates(wrapper)) {
234
+ if (!isUsageRowShape(candidate, wrapperRect)) continue;
235
+ cachedUsageRow = candidate;
236
+ cachedUsageRowStable = true;
237
+ return candidate;
238
+ }
239
+ const counts = /\d+\s*轮[\s\S]{0,14}?步|tok\/s/u;
240
+ const metric = /tok\/s|缓存命中|上下文/u;
241
+ let best = null;
242
+ let bestTop = Number.NEGATIVE_INFINITY;
243
+ for (const candidate of wrapper.querySelectorAll("div, span, p, section, footer")) {
244
+ if (candidate.children.length < 2 || candidate.children.length > 4) continue;
245
+ const text = (candidate.textContent ?? "").trim();
246
+ if (text.length === 0 || text.length > 80) continue;
247
+ if (text.includes("\n")) continue;
248
+ if (!counts.test(text) || !metric.test(text)) continue;
249
+ if (!isUsageRowShape(candidate, wrapperRect)) continue;
250
+ const childTexts = [...candidate.children].map((child) => (child.textContent ?? "").trim()).filter((value) => value !== "");
251
+ if (childTexts.length < 2 || childTexts.includes(text)) continue;
252
+ const rect = candidate.getBoundingClientRect();
253
+ if (rect.top > bestTop) {
254
+ bestTop = rect.top;
255
+ best = candidate;
256
+ }
257
+ }
258
+ cachedUsageRow = best;
259
+ cachedUsageRowStable = false;
260
+ return best;
261
+ }
262
+ function renderComposerStrip() {
263
+ const existing = document.querySelector(`[${STRIP_ATTRIBUTE}]`);
264
+ const active2 = document.documentElement.hasAttribute(ACTIVE_ATTRIBUTE);
265
+ const wrapper = findComposerWrapper();
266
+ if (!settings().switches.strip.read() || !active2 || wrapper === null) {
267
+ if (existing !== null) existing.remove();
268
+ recordDiagnostics({
269
+ strip: { visible: false, wrapper: wrapper !== null, active: active2, enabled: settings().switches.strip.read() }
270
+ });
271
+ return null;
272
+ }
273
+ ensureComposerSizing(wrapper);
274
+ const row = findUsageRow(wrapper);
275
+ if (row === null) {
276
+ if (existing !== null) existing.remove();
277
+ recordDiagnostics({ strip: { visible: false, wrapper: true, reason: "no usage row" } });
278
+ return null;
279
+ }
280
+ const card = findInputCard(wrapper);
281
+ const wrapperRect = wrapper.getBoundingClientRect();
282
+ const cardRect = card.getBoundingClientRect();
283
+ const rowRect = rectOf(row);
284
+ let element = existing;
285
+ if (element === null) {
286
+ element = document.createElement("div");
287
+ element.setAttribute(STRIP_ATTRIBUTE, "");
288
+ element.setAttribute("aria-hidden", "true");
289
+ }
290
+ if (element.parentElement !== wrapper) wrapper.appendChild(element);
291
+ const top = Math.max(cardRect.bottom, rowRect.top - 4) - wrapperRect.top;
292
+ const height = rowRect.bottom + 4 - wrapperRect.top - top;
293
+ const nextLeft = `${Math.round(cardRect.left - wrapperRect.left)}px`;
294
+ const nextTop = `${Math.round(top)}px`;
295
+ const nextWidth = `${Math.round(cardRect.width)}px`;
296
+ const nextHeight = `${Math.round(Math.max(height, 0))}px`;
297
+ if (element.style.left !== nextLeft) element.style.left = nextLeft;
298
+ if (element.style.top !== nextTop) element.style.top = nextTop;
299
+ if (element.style.width !== nextWidth) element.style.width = nextWidth;
300
+ if (element.style.height !== nextHeight) element.style.height = nextHeight;
301
+ recordDiagnostics({
302
+ strip: {
303
+ visible: true,
304
+ width: Math.round(cardRect.width),
305
+ height: Math.round(Math.max(height, 0)),
306
+ inputCard: cachedInputCardSource,
307
+ usageRow: cachedUsageRowStable ? "stable-hooks" : "wording-fallback"
308
+ }
309
+ });
310
+ return element;
311
+ }
312
+
313
+ // src/react.js
314
+ var binding = null;
315
+ function bindReact(require2) {
316
+ if (binding !== null) return binding;
317
+ const React = require2("react");
318
+ binding = { React, h: React.createElement };
319
+ return binding;
320
+ }
321
+ function getReact() {
322
+ if (binding === null) throw new Error("dsh-chat-background: React was not bound before first render");
323
+ return binding.React;
324
+ }
325
+ function h(type, props, ...children) {
326
+ return getReact().createElement(type, props, ...children);
327
+ }
328
+ function useState(initialState) {
329
+ return getReact().useState(initialState);
330
+ }
331
+ function useEffect(effect, dependencies) {
332
+ return getReact().useEffect(effect, dependencies);
333
+ }
334
+ function useLayoutEffect(effect, dependencies) {
335
+ return getReact().useLayoutEffect(effect, dependencies);
336
+ }
337
+ function useRef(initialValue) {
338
+ return getReact().useRef(initialValue);
339
+ }
340
+ function useId() {
341
+ const React = getReact();
342
+ return typeof React.useId === "function" ? React.useId() : "";
343
+ }
344
+
345
+ // src/i18n.js
346
+ var zh = {
347
+ none: "无(纯色背景)",
348
+ choose: "选择图片",
349
+ open: "打开文件夹",
350
+ refresh: "重新扫描",
351
+ emptyBefore: "文件夹还是空的,把图片放进 ",
352
+ emptyAfter: " 后点「重新扫描」",
353
+ loading: "读取中…",
354
+ failed: "读取背景文件夹失败",
355
+ openFailed: "打开文件夹失败",
356
+ truncated: "图片太多,只显示最近的 {shown} 张",
357
+ scrimLeftTitle: "左侧栏遮罩层",
358
+ scrimMainTitle: "主窗口遮罩层",
359
+ scrimRightTitle: "右侧栏遮罩层",
360
+ stripTitle: "输入栏下方背景条",
361
+ stripDescription: "在输入栏下方加一条与输入栏等长的背景条,把下面那排用量数据包起来(仅在有背景图时显示)",
362
+ stripColorTitle: "背景条颜色",
363
+ stripColorDescription: "这条背景条的底色;点色块打开调色盘,也可以恢复默认",
364
+ reset: "恢复默认",
365
+ progressTitle: "任务进度条",
366
+ progressDescription: "显示当前任务的完成进度:有任务清单时按清单完成度,否则按本轮已完成的步骤粗略估算;空闲时隐藏",
367
+ progressRunning: "正在执行…",
368
+ progressRunningHint: "粗略估算:本轮已完成 {steps} 步",
369
+ progressColorTitle: "进度条颜色",
370
+ progressColorDescription: "进度条填充色;点色块打开调色盘,也可以恢复默认",
371
+ tokenTitle: "Token 总用量",
372
+ tokenDescription: "在左下角用户标上方显示所有会话(含已归档)的 token 总用量",
373
+ tokenLabel: "Token总用量:",
374
+ tokenHint: "全部会话累计:{exact} token(含缓存读取;{sessions}/{all} 个会话有用量)",
375
+ tokenSizeTitle: "Token 字号",
376
+ tokenSizeDescription: "这一行的字号,10–20 px",
377
+ modeTitle: "背景显示方式",
378
+ modeFixed: "固定背景",
379
+ modeRotation: "轮换背景",
380
+ rotationEffectTitle: "轮换设置",
381
+ effectType: "特效方式",
382
+ effectFade: "淡入淡出",
383
+ effectFadeThrough: "渐变过渡",
384
+ effectNone: "无特效",
385
+ effectDuration: "切换时长",
386
+ rotationInterval: "轮换间隔",
387
+ rotationGroupTitle: "轮换分组",
388
+ rotationGroupAll: "全选",
389
+ rotationGroupNone: "清空",
390
+ rotationGroupEmpty: "背景库为空",
391
+ dragHandle: "拖动排序",
392
+ nav: "页面设置",
393
+ tabBackground: "背景",
394
+ tabUi: "UI",
395
+ percent: "%"
396
+ };
397
+ var en = {
398
+ none: "None (plain background)",
399
+ choose: "Choose image",
400
+ open: "Open folder",
401
+ refresh: "Rescan",
402
+ emptyBefore: "The folder is empty — put images in ",
403
+ emptyAfter: " and press Rescan",
404
+ loading: "Loading…",
405
+ failed: "Could not read the background folder",
406
+ openFailed: "Could not open the folder",
407
+ truncated: "Too many images; showing the most recent {shown}",
408
+ scrimLeftTitle: "Left sidebar scrim layer",
409
+ scrimMainTitle: "Main window scrim layer",
410
+ scrimRightTitle: "Right sidebar scrim layer",
411
+ stripTitle: "Bar under the input",
412
+ stripDescription: "Adds a bar as wide as the input, wrapping the usage row below it (shown while a background image is set)",
413
+ stripColorTitle: "Bar colour",
414
+ stripColorDescription: "Fill of that bar; click the swatch for a colour picker, or reset it",
415
+ reset: "Reset",
416
+ progressTitle: "Task progress bar",
417
+ progressDescription: "Shows how far the current task has come: the todo list when the agent keeps one, otherwise a rough estimate from the steps this turn has taken; hidden while idle",
418
+ progressRunning: "Working…",
419
+ progressRunningHint: "Rough estimate: {steps} steps taken this turn",
420
+ progressColorTitle: "Progress colour",
421
+ progressColorDescription: "Fill of the progress bar; click the swatch to open a colour picker, or reset it",
422
+ tokenTitle: "Total token usage",
423
+ tokenDescription: "Shows the token total of every session (archived included) above the account row",
424
+ tokenLabel: "Total tokens: ",
425
+ tokenHint: "All sessions: {exact} tokens (cache reads included; {sessions}/{all} sessions carry usage)",
426
+ tokenSizeTitle: "Token font size",
427
+ tokenSizeDescription: "Font size of that line, 10–20 px",
428
+ modeTitle: "Background display",
429
+ modeFixed: "Fixed image",
430
+ modeRotation: "Rotate images",
431
+ rotationEffectTitle: "Rotation settings",
432
+ effectType: "Effect",
433
+ effectFade: "Fade",
434
+ effectFadeThrough: "Fade through",
435
+ effectNone: "None",
436
+ effectDuration: "Duration",
437
+ rotationInterval: "Interval",
438
+ rotationGroupTitle: "Rotation group",
439
+ rotationGroupAll: "Select all",
440
+ rotationGroupNone: "Clear",
441
+ rotationGroupEmpty: "The background library is empty",
442
+ dragHandle: "Drag to reorder",
443
+ nav: "Page settings",
444
+ tabBackground: "Background",
445
+ tabUi: "UI",
446
+ percent: "%"
447
+ };
448
+ function copyNow() {
449
+ if (typeof document === "undefined") return en;
450
+ const documentLanguage = (document.documentElement.lang || "").toLowerCase();
451
+ const browserLanguage = (typeof navigator === "undefined" ? "" : navigator.language || "").toLowerCase();
452
+ if (documentLanguage.startsWith("zh")) return zh;
453
+ if (documentLanguage === "" && browserLanguage.startsWith("zh")) return zh;
454
+ return en;
455
+ }
456
+ var translate = null;
457
+ var unsubscribeLocale = null;
458
+ function installTranslator({ translate: boundTranslate, subscribe = null }) {
459
+ translate = boundTranslate;
460
+ unsubscribeLocale = typeof subscribe === "function" ? subscribe : null;
461
+ }
462
+ function copyValue(key) {
463
+ if (translate !== null) {
464
+ try {
465
+ const text = translate(key);
466
+ if (typeof text === "string" && text !== "" && text !== key) return text;
467
+ } catch {
468
+ }
469
+ }
470
+ return copyNow()[key] ?? key;
471
+ }
472
+ var copyView = new Proxy(
473
+ {},
474
+ {
475
+ get: (_target, key) => copyValue(String(key))
476
+ }
477
+ );
478
+ var copyListeners = /* @__PURE__ */ new Set();
479
+ var copyObserver = null;
480
+ function subscribeDomLanguage(listener) {
481
+ if (typeof MutationObserver === "undefined") return () => {
482
+ };
483
+ if (copyObserver === null) {
484
+ copyObserver = new MutationObserver(() => {
485
+ for (const entry of copyListeners) entry();
486
+ });
487
+ copyObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
488
+ }
489
+ copyListeners.add(listener);
490
+ return () => {
491
+ copyListeners.delete(listener);
492
+ if (copyListeners.size === 0 && copyObserver !== null) {
493
+ copyObserver.disconnect();
494
+ copyObserver = null;
495
+ }
496
+ };
497
+ }
498
+ function useCopy() {
499
+ const [, forceRender] = useState(0);
500
+ useEffect(() => {
501
+ const listener = () => {
502
+ forceRender((generation) => generation + 1);
503
+ };
504
+ const unsubscribe = unsubscribeLocale !== null ? unsubscribeLocale(listener) : subscribeDomLanguage(listener);
505
+ return () => {
506
+ unsubscribe();
507
+ };
508
+ }, []);
509
+ return copyView;
510
+ }
511
+
512
+ // src/lib/format.js
513
+ function formatCompactTokens(value) {
514
+ if (!Number.isFinite(value) || value <= 0) return "0";
515
+ if (value < 1e3) return String(Math.round(value));
516
+ if (value < 1e6) return `${(value / 1e3).toFixed(value < 1e4 ? 1 : 0)}K`;
517
+ if (value < 1e9) return `${(value / 1e6).toFixed(value < 1e7 ? 1 : 0)}M`;
518
+ return `${(value / 1e9).toFixed(2)}B`;
519
+ }
520
+
521
+ // src/lib/clamp.js
522
+ function clampToRange(value, minimum, maximum) {
523
+ return Math.min(maximum, Math.max(minimum, Math.round(value)));
524
+ }
525
+ function parsePercent(raw, fallback, minimum, maximum) {
526
+ if (raw === null || raw === "") return fallback;
527
+ const value = Number(raw);
528
+ if (!Number.isFinite(value)) return fallback;
529
+ return clampToRange(value, minimum, maximum);
530
+ }
531
+
532
+ // src/storage.js
533
+ var valueCache = /* @__PURE__ */ new Map();
534
+ var storageListener = null;
535
+ function installStorageListener() {
536
+ if (storageListener !== null) return;
537
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
538
+ storageListener = () => {
539
+ valueCache.clear();
540
+ };
541
+ window.addEventListener("storage", storageListener);
542
+ }
543
+ function disposeStorageCache() {
544
+ if (storageListener !== null && typeof window !== "undefined" && typeof window.removeEventListener === "function") {
545
+ window.removeEventListener("storage", storageListener);
546
+ }
547
+ storageListener = null;
548
+ valueCache.clear();
549
+ }
550
+ function readString(key) {
551
+ installStorageListener();
552
+ if (valueCache.has(key)) return valueCache.get(key);
553
+ try {
554
+ const raw = window.localStorage.getItem(key);
555
+ const value = raw === null || raw === "" ? null : raw;
556
+ valueCache.set(key, value);
557
+ return value;
558
+ } catch {
559
+ return null;
560
+ }
561
+ }
562
+ function writeString(key, value) {
563
+ valueCache.set(key, value);
564
+ try {
565
+ if (value === null) window.localStorage.removeItem(key);
566
+ else window.localStorage.setItem(key, value);
567
+ } catch {
568
+ }
569
+ }
570
+ function readPercent(key, fallback, minimum, maximum) {
571
+ return parsePercent(readString(key), fallback, minimum, maximum);
572
+ }
573
+ function readSelection() {
574
+ const raw = readString(SELECTION_KEY);
575
+ if (raw === null) return null;
576
+ if (raw.includes("/") || raw.includes("\\") || raw.includes("\0")) return null;
577
+ const lowered = raw.toLowerCase();
578
+ if (!ACCEPTED_IMAGE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) return null;
579
+ return raw;
580
+ }
581
+ function createSwitch({ key, onChange }) {
582
+ const listeners = /* @__PURE__ */ new Set();
583
+ const read = () => readString(key) !== "off";
584
+ const write = (next) => {
585
+ writeString(key, next ? null : "off");
586
+ if (typeof onChange === "function") onChange(next);
587
+ for (const listener of listeners) listener(next);
588
+ };
589
+ const useEnabled = () => {
590
+ const [enabled, setLocal] = useState(read);
591
+ useEffect(() => {
592
+ const listener = (next) => {
593
+ setLocal(next);
594
+ };
595
+ listeners.add(listener);
596
+ setLocal(read());
597
+ return () => {
598
+ listeners.delete(listener);
599
+ };
600
+ }, []);
601
+ return enabled;
602
+ };
603
+ return { read, write, useEnabled };
604
+ }
605
+ function createColorSetting({ key, property, fallback, translucent = false, defaultToken = null }) {
606
+ const read = () => {
607
+ const raw = readString(key);
608
+ return raw !== null && /^#[0-9a-f]{6}$/iu.test(raw) ? raw : null;
609
+ };
610
+ const shownDefault = () => {
611
+ if (defaultToken === null) return fallback;
612
+ const scope = document.body ?? document.documentElement;
613
+ const fromBody = getComputedStyle(scope).getPropertyValue(defaultToken);
614
+ const fromRoot = getComputedStyle(document.documentElement).getPropertyValue(defaultToken);
615
+ return cssColorToHex(fromBody) ?? cssColorToHex(fromRoot) ?? fallback;
616
+ };
617
+ const apply = () => {
618
+ const hex = read();
619
+ if (hex === null) document.documentElement.style.removeProperty(property);
620
+ else document.documentElement.style.setProperty(property, translucent ? `${hex}eb` : hex);
621
+ };
622
+ const useColor = (onChange) => {
623
+ const [color, setColor] = useState(() => read() ?? shownDefault());
624
+ const change = (next) => {
625
+ setColor(next);
626
+ writeString(key, next);
627
+ apply();
628
+ if (typeof onChange === "function") onChange(next);
629
+ };
630
+ const reset = () => {
631
+ setColor(shownDefault());
632
+ writeString(key, null);
633
+ apply();
634
+ if (typeof onChange === "function") onChange();
635
+ };
636
+ return { color, change, reset };
637
+ };
638
+ return { read, apply, useColor };
639
+ }
640
+ function createStripColorSetting() {
641
+ return createColorSetting({
642
+ key: STRIP_COLOR_KEY,
643
+ property: "--dsh-chat-strip-bg",
644
+ fallback: STRIP_DEFAULT_HEX,
645
+ translucent: true
646
+ });
647
+ }
648
+ function createProgressColorSetting() {
649
+ return createColorSetting({
650
+ key: PROGRESS_COLOR_KEY,
651
+ property: "--dsh-progress-color",
652
+ fallback: PROGRESS_DEFAULT_HEX,
653
+ /* Unset means the bar paints the theme's brand token, so the swatch shows
654
+ * that colour rather than a blue the bar never uses. */
655
+ defaultToken: PROGRESS_DEFAULT_TOKEN
656
+ });
657
+ }
658
+
659
+ // src/token-total.js
660
+ var tokenValue = null;
661
+ function resetTokenTotal() {
662
+ tokenValue = null;
663
+ }
664
+ var cachedAnchor = null;
665
+ function readTokenSize() {
666
+ return readPercent(TOKEN_SIZE_KEY, TOKEN_SIZE_DEFAULT, TOKEN_SIZE_MIN, TOKEN_SIZE_MAX);
667
+ }
668
+ function applyTokenSize() {
669
+ document.documentElement.style.setProperty("--dsh-token-size", `${readTokenSize()}px`);
670
+ }
671
+ function refreshTokenTotal() {
672
+ return fetch(TOKEN_URL, { headers: { accept: "application/json" } }).then((response) => response.ok ? response.json() : null).then((payload) => {
673
+ if (payload !== null && typeof payload === "object") {
674
+ tokenValue = payload;
675
+ recordDiagnostics({
676
+ token: {
677
+ status: "ok",
678
+ total: payload.total,
679
+ source: payload.source ?? "unknown",
680
+ sessions: payload.sessions,
681
+ at: Date.now()
682
+ }
683
+ });
684
+ renderTokenTotal();
685
+ } else {
686
+ recordDiagnostics({ token: { status: "http-error", at: Date.now() } });
687
+ }
688
+ return payload;
689
+ }).catch((error) => {
690
+ recordDiagnostics({ token: { status: "network-error", error: String(error), at: Date.now() } });
691
+ return null;
692
+ });
693
+ }
694
+ function findAccountAnchor() {
695
+ if (cachedAnchor !== null && cachedAnchor.parent.isConnected && (cachedAnchor.before === null || cachedAnchor.before.isConnected)) {
696
+ return cachedAnchor;
697
+ }
698
+ cachedAnchor = null;
699
+ const candidates = [];
700
+ const account = document.querySelector('[class*="settingsArea"]');
701
+ if (account !== null && account.parentElement !== null) {
702
+ candidates.push({ parent: account.parentElement, before: account });
703
+ }
704
+ const settingsSlot = document.querySelector('[data-slot="sidebar.settings"]');
705
+ const settingsRow = settingsSlot === null ? null : settingsSlot.parentElement;
706
+ if (settingsRow !== null && settingsRow.parentElement !== null) {
707
+ candidates.push({ parent: settingsRow.parentElement, before: settingsRow });
708
+ }
709
+ const foot = document.querySelector('[class*="footArea"]');
710
+ if (foot !== null) candidates.push({ parent: foot, before: foot.lastElementChild });
711
+ for (const candidate of candidates) {
712
+ const rect = candidate.parent.getBoundingClientRect();
713
+ const inSidebarFooter = rect.width > 100 && rect.left < window.innerWidth * 0.5;
714
+ const nearBottom = rect.bottom > window.innerHeight * 0.6;
715
+ if (inSidebarFooter && nearBottom) {
716
+ cachedAnchor = candidate;
717
+ return candidate;
718
+ }
719
+ }
720
+ return null;
721
+ }
722
+ function renderTokenTotal() {
723
+ const copy = copyNow();
724
+ const existing = document.querySelector(`[${TOKEN_NODE_ATTRIBUTE}]`);
725
+ if (!settings().switches.token.read()) {
726
+ if (existing !== null) existing.remove();
727
+ return null;
728
+ }
729
+ const anchor = findAccountAnchor();
730
+ if (anchor === null) {
731
+ if (existing !== null) existing.remove();
732
+ return null;
733
+ }
734
+ let element = existing;
735
+ if (element === null) {
736
+ element = document.createElement("div");
737
+ element.className = "dshcb-token-total";
738
+ element.setAttribute(TOKEN_NODE_ATTRIBUTE, "");
739
+ element.setAttribute("aria-live", "polite");
740
+ }
741
+ if (element.parentElement !== anchor.parent || element.nextElementSibling !== anchor.before) {
742
+ anchor.parent.insertBefore(element, anchor.before);
743
+ }
744
+ const account = anchor.parent.querySelector('[data-slot="settings.launcher"] button') ?? anchor.parent.querySelector('button[aria-label="账号菜单"]') ?? anchor.parent.querySelector('button[aria-label="Account menu"]');
745
+ if (account !== null) {
746
+ const accountPadding = Number.parseFloat(getComputedStyle(account).paddingLeft) || 0;
747
+ const target = account.getBoundingClientRect().left + accountPadding;
748
+ const left = element.getBoundingClientRect().left;
749
+ if (target > 0 && left > 0) {
750
+ const nextPadding = String(Math.max(0, Math.round(target - left))) + "px";
751
+ if (element.style.paddingLeft !== nextPadding) element.style.paddingLeft = nextPadding;
752
+ }
753
+ }
754
+ const total = tokenValue !== null && typeof tokenValue.total === "number" ? tokenValue.total : null;
755
+ const missing = tokenValue !== null && tokenValue.missing === true;
756
+ const text = copy.tokenLabel + (total === null ? "…" : missing ? "—" : formatCompactTokens(total));
757
+ if (element.textContent !== text) element.textContent = text;
758
+ if (tokenValue !== null) {
759
+ const hint = copy.tokenHint.replaceAll("{exact}", String(tokenValue.total)).replaceAll("{sessions}", String(tokenValue.countedSessions ?? 0)).replaceAll("{all}", String(tokenValue.sessions ?? 0));
760
+ if (element.title !== hint) element.title = hint;
761
+ }
762
+ return element;
763
+ }
764
+
765
+ // src/background.js
766
+ function ensureBackgroundLayer(name) {
767
+ if (document.body === null || name === null) return null;
768
+ let layer2 = document.querySelector("[" + BACKGROUND_LAYER_ATTRIBUTE + "]");
769
+ if (layer2 === null || layer2.tagName !== "IMG") {
770
+ layer2?.remove();
771
+ layer2 = document.createElement("img");
772
+ layer2.className = "dshcb-background-layer";
773
+ layer2.setAttribute(BACKGROUND_LAYER_ATTRIBUTE, "");
774
+ layer2.setAttribute("aria-hidden", "true");
775
+ layer2.setAttribute("alt", "");
776
+ layer2.setAttribute("decoding", "async");
777
+ layer2.setAttribute("draggable", "false");
778
+ document.body.appendChild(layer2);
779
+ }
780
+ const src = FILE_URL + encodeURIComponent(name);
781
+ if (layer2.getAttribute("src") !== src) layer2.setAttribute("src", src);
782
+ return layer2;
783
+ }
784
+ function removeBackgroundLayer() {
785
+ for (const element of document.querySelectorAll("[" + BACKGROUND_LAYER_ATTRIBUTE + "]")) element.remove();
786
+ }
787
+ var THEME_BASE_TOKEN = "--dsw-alias-bg-base";
788
+ function baseSurfaceIsDark() {
789
+ try {
790
+ const scope = document.body ?? document.documentElement;
791
+ const hex = cssColorToHex(getComputedStyle(scope).getPropertyValue(THEME_BASE_TOKEN));
792
+ if (hex === null) return null;
793
+ const channel = (start) => Number.parseInt(hex.slice(start, start + 2), 16) / 255;
794
+ const luminance = 0.2126 * channel(1) + 0.7152 * channel(3) + 0.0722 * channel(5);
795
+ return luminance < 0.45;
796
+ } catch {
797
+ return null;
798
+ }
799
+ }
800
+ function isDark() {
801
+ if (document.body === null) return false;
802
+ if (document.body.hasAttribute("data-ds-dark-theme")) return true;
803
+ return baseSurfaceIsDark() === true;
804
+ }
805
+ function readRegionScrim(key, legacyKey, legacyDefault) {
806
+ const legacy = readPercent(legacyKey, legacyDefault, VEIL_MIN, VEIL_MAX);
807
+ return readPercent(key, legacy, SCRIM_MIN, SCRIM_MAX);
808
+ }
809
+ function readLeftScrim() {
810
+ return readRegionScrim(SCRIM_LEFT_KEY, VEIL_KEY, VEIL_DEFAULT);
811
+ }
812
+ function readMainScrim() {
813
+ return readRegionScrim(SCRIM_MAIN_KEY, SCRIM_KEY, SCRIM_DEFAULT);
814
+ }
815
+ function readRightScrim() {
816
+ return readRegionScrim(SCRIM_RIGHT_KEY, VEIL_KEY, VEIL_DEFAULT);
817
+ }
818
+ function layer(alpha) {
819
+ const rgb = isDark() ? "9,11,15" : "250,250,250";
820
+ const value = (alpha / 100).toFixed(3);
821
+ return `linear-gradient(rgba(${rgb},${value}), rgba(${rgb},${value}))`;
822
+ }
823
+ function cssString(name) {
824
+ return `"${String(name).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
825
+ }
826
+ function isOpaque(color) {
827
+ if (color === "" || color === "transparent") return false;
828
+ const match = /^rgba?\(([^)]+)\)$/iu.exec(color);
829
+ if (match === null) return color !== "transparent";
830
+ const parts = match[1].split(",").map((part) => Number(part.trim()));
831
+ return parts.length < 4 || parts[3] > 0.05;
832
+ }
833
+ var painterMarks = /* @__PURE__ */ new Set();
834
+ var cachedAnchor2 = null;
835
+ var cachedContainer = null;
836
+ function conversationAnchor() {
837
+ if (cachedAnchor2 !== null && cachedAnchor2.isConnected) return cachedAnchor2;
838
+ cachedAnchor2 = document.querySelector('[data-slot="main.conversation"]');
839
+ if (cachedAnchor2 === null) cachedAnchor2 = document.querySelector('[data-slot="main"]');
840
+ return cachedAnchor2;
841
+ }
842
+ function conversationContainer() {
843
+ const anchor = conversationAnchor();
844
+ if (anchor === null) return null;
845
+ if (cachedContainer !== null && cachedContainer.isConnected && (cachedContainer === anchor || anchor.contains(cachedContainer))) {
846
+ return cachedContainer;
847
+ }
848
+ cachedContainer = anchor.firstElementChild ?? anchor;
849
+ return cachedContainer;
850
+ }
851
+ function markedPainters() {
852
+ for (const element of [...painterMarks]) {
853
+ if (!element.isConnected) painterMarks.delete(element);
854
+ }
855
+ return [...painterMarks];
856
+ }
857
+ function clearPainters() {
858
+ for (const element of painterMarks) element.removeAttribute(LIFT_ATTRIBUTE);
859
+ painterMarks.clear();
860
+ }
861
+ function markColumn(attribute, slotName) {
862
+ const outlet = document.querySelector(`[data-slot="${slotName}"]`);
863
+ const column = outlet === null ? null : outlet.parentElement;
864
+ if (column !== null && !column.hasAttribute(attribute)) column.setAttribute(attribute, "");
865
+ }
866
+ function clearColumn(attribute) {
867
+ for (const element of document.querySelectorAll(`[${attribute}]`)) element.removeAttribute(attribute);
868
+ }
869
+ function markSidebarColumn() {
870
+ const outlet = document.querySelector('[data-slot="sidebar"]');
871
+ const column = outlet === null ? null : outlet.parentElement;
872
+ if (column !== null) column.setAttribute(SIDEBAR_COLUMN_ATTRIBUTE, "");
873
+ }
874
+ function markMainColumn() {
875
+ markColumn(MAIN_COLUMN_ATTRIBUTE, "main");
876
+ }
877
+ function markRightColumn() {
878
+ markColumn(RIGHT_COLUMN_ATTRIBUTE, "rightbar");
879
+ }
880
+ var fullscreenGraceUntil = 0;
881
+ var fullscreenTimer = 0;
882
+ var fullscreenSuppressed = false;
883
+ var noAnimationTimer = 0;
884
+ function hasExitFullscreenControl() {
885
+ return document.querySelector('button[aria-label="退出全屏"], button[aria-label="Exit fullscreen"]') !== null;
886
+ }
887
+ function scheduleFullscreenRecheck() {
888
+ if (fullscreenTimer !== 0) return;
889
+ const delay = Math.max(60, fullscreenGraceUntil - Date.now() + 20);
890
+ fullscreenTimer = window.setTimeout(() => {
891
+ fullscreenTimer = 0;
892
+ applyBackground(readSelection());
893
+ }, delay);
894
+ }
895
+ var frameAnimTimer = 0;
896
+ var panelObserver = null;
897
+ var observedPanel = null;
898
+ function observeRightPanel(panel) {
899
+ if (typeof ResizeObserver === "undefined" || observedPanel === panel) return;
900
+ panelObserver?.disconnect();
901
+ observedPanel = panel;
902
+ panelObserver = new ResizeObserver(() => {
903
+ flushBackground();
904
+ });
905
+ panelObserver.observe(panel);
906
+ }
907
+ function clearFullscreenLayer() {
908
+ const root = document.documentElement;
909
+ root.removeAttribute(RIGHT_FULLSCREEN_ATTRIBUTE);
910
+ root.style.removeProperty("--dsh-chat-background-full-left");
911
+ root.style.removeProperty("--dsh-chat-background-full-width");
912
+ for (const element of document.querySelectorAll(`[${RIGHT_PANEL_FULL_ATTRIBUTE}]`)) {
913
+ element.removeAttribute(RIGHT_PANEL_FULL_ATTRIBUTE);
914
+ }
915
+ fullscreenGraceUntil = 0;
916
+ }
917
+ function silenceTransitions() {
918
+ const root = document.documentElement;
919
+ root.setAttribute(NO_ANIMATION_ATTRIBUTE, "");
920
+ if (noAnimationTimer !== 0) window.clearTimeout(noAnimationTimer);
921
+ noAnimationTimer = window.setTimeout(() => {
922
+ noAnimationTimer = 0;
923
+ root.removeAttribute(NO_ANIMATION_ATTRIBUTE);
924
+ }, 450);
925
+ }
926
+ function suppressFullscreenForExit() {
927
+ fullscreenSuppressed = true;
928
+ clearFullscreenLayer();
929
+ silenceTransitions();
930
+ applyBackground(readSelection());
931
+ }
932
+ function markRightPanel() {
933
+ if (observedPanel !== null && !observedPanel.isConnected) {
934
+ panelObserver?.disconnect();
935
+ panelObserver = null;
936
+ observedPanel = null;
937
+ }
938
+ const outlet = document.querySelector('[data-slot="rightbar"]');
939
+ const column = outlet === null ? null : outlet.parentElement;
940
+ const panel = outlet === null ? null : outlet.querySelector('[class*="panel"]');
941
+ const root = document.documentElement;
942
+ const labelFull = hasExitFullscreenControl();
943
+ if (!labelFull) fullscreenSuppressed = false;
944
+ if (labelFull) fullscreenGraceUntil = Math.max(fullscreenGraceUntil, Date.now() + 300);
945
+ let measured = false;
946
+ if (panel !== null) {
947
+ const box = panel.getBoundingClientRect();
948
+ measured = isFullscreenPanel(
949
+ { width: box.width, height: box.height, left: box.left, top: box.top },
950
+ { width: window.innerWidth, height: window.innerHeight }
951
+ );
952
+ }
953
+ const full = !fullscreenSuppressed && (labelFull || measured || Date.now() < fullscreenGraceUntil);
954
+ if (full) {
955
+ if (!root.hasAttribute(RIGHT_FULLSCREEN_ATTRIBUTE)) root.setAttribute(RIGHT_FULLSCREEN_ATTRIBUTE, "");
956
+ } else {
957
+ root.removeAttribute(RIGHT_FULLSCREEN_ATTRIBUTE);
958
+ fullscreenGraceUntil = 0;
959
+ }
960
+ if (full && !labelFull) scheduleFullscreenRecheck();
961
+ if (panel === null) return;
962
+ if (!panel.hasAttribute(RIGHT_PANEL_ATTRIBUTE)) panel.setAttribute(RIGHT_PANEL_ATTRIBUTE, "");
963
+ const setFull = (element) => {
964
+ if (full && !element.hasAttribute(RIGHT_PANEL_FULL_ATTRIBUTE)) element.setAttribute(RIGHT_PANEL_FULL_ATTRIBUTE, "");
965
+ else if (!full && element.hasAttribute(RIGHT_PANEL_FULL_ATTRIBUTE))
966
+ element.removeAttribute(RIGHT_PANEL_FULL_ATTRIBUTE);
967
+ };
968
+ setFull(panel);
969
+ if (column !== null) setFull(column);
970
+ if (full) {
971
+ const leftOutlet = document.querySelector('[data-slot="sidebar"]');
972
+ const leftColumn = leftOutlet === null ? null : leftOutlet.parentElement;
973
+ const leftWidth = leftColumn === null ? 0 : leftColumn.getBoundingClientRect().width;
974
+ const columnLeft = column === null ? 0 : column.getBoundingClientRect().left;
975
+ const offset = Math.round(leftWidth - columnLeft);
976
+ const width = Math.round(window.innerWidth - leftWidth);
977
+ root.style.setProperty("--dsh-chat-background-full-left", offset + "px");
978
+ root.style.setProperty("--dsh-chat-background-full-width", width + "px");
979
+ } else if (!full) {
980
+ root.style.removeProperty("--dsh-chat-background-full-left");
981
+ root.style.removeProperty("--dsh-chat-background-full-width");
982
+ }
983
+ observeRightPanel(panel);
984
+ }
985
+ function animateLayoutFrame() {
986
+ const outlet = document.querySelector('[data-slot="main"]');
987
+ const column = outlet === null ? null : outlet.parentElement;
988
+ const frame = column === null ? null : column.parentElement;
989
+ if (frame === null) return;
990
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
991
+ if (!reduced) frame.setAttribute(FRAME_ANIMATING_ATTRIBUTE, "");
992
+ if (frameAnimTimer !== 0) window.clearTimeout(frameAnimTimer);
993
+ frameAnimTimer = window.setTimeout(() => {
994
+ frameAnimTimer = 0;
995
+ frame.removeAttribute(FRAME_ANIMATING_ATTRIBUTE);
996
+ }, 340);
997
+ }
998
+ function installRightbarGestures() {
999
+ let labelFull = hasExitFullscreenControl();
1000
+ const observer = new MutationObserver(() => {
1001
+ const next = hasExitFullscreenControl();
1002
+ if (next === labelFull) return;
1003
+ labelFull = next;
1004
+ flushBackground();
1005
+ });
1006
+ observer.observe(document.documentElement, {
1007
+ subtree: true,
1008
+ childList: true,
1009
+ attributes: true,
1010
+ attributeFilter: ["aria-label"]
1011
+ });
1012
+ const handler = (event) => {
1013
+ const target = event.target instanceof Element ? event.target.closest("button[aria-label]") : null;
1014
+ if (target === null) return;
1015
+ const label = target.getAttribute("aria-label") ?? "";
1016
+ if (/打开右侧边栏|收起右侧边栏|open right sidebar|collapse right sidebar/i.test(label)) {
1017
+ animateLayoutFrame();
1018
+ return;
1019
+ }
1020
+ if (target.closest('[data-slot="rightbar"]') === null) return;
1021
+ if (/退出全屏|exit fullscreen/i.test(label)) {
1022
+ suppressFullscreenForExit();
1023
+ return;
1024
+ }
1025
+ if (/^全屏$|^fullscreen$/i.test(label)) {
1026
+ if (document.querySelector("[data-dsh-chat-background-right-panel]") === null) return;
1027
+ fullscreenSuppressed = false;
1028
+ silenceTransitions();
1029
+ fullscreenGraceUntil = Date.now() + 700;
1030
+ if (fullscreenTimer !== 0) window.clearTimeout(fullscreenTimer);
1031
+ fullscreenTimer = window.setTimeout(() => {
1032
+ fullscreenTimer = 0;
1033
+ applyBackground(readSelection());
1034
+ }, 720);
1035
+ applyBackground(readSelection());
1036
+ }
1037
+ };
1038
+ document.addEventListener("click", handler, { capture: true });
1039
+ return () => {
1040
+ document.removeEventListener("click", handler, { capture: true });
1041
+ observer.disconnect();
1042
+ fullscreenSuppressed = false;
1043
+ if (noAnimationTimer !== 0) window.clearTimeout(noAnimationTimer);
1044
+ noAnimationTimer = 0;
1045
+ document.documentElement.removeAttribute(NO_ANIMATION_ATTRIBUTE);
1046
+ if (frameAnimTimer !== 0) window.clearTimeout(frameAnimTimer);
1047
+ frameAnimTimer = 0;
1048
+ if (fullscreenTimer !== 0) window.clearTimeout(fullscreenTimer);
1049
+ fullscreenTimer = 0;
1050
+ fullscreenGraceUntil = 0;
1051
+ };
1052
+ }
1053
+ function markLayoutFrame() {
1054
+ const outlet = document.querySelector('[data-slot="main"]');
1055
+ const column = outlet === null ? null : outlet.parentElement;
1056
+ const frame = column === null ? null : column.parentElement;
1057
+ if (frame !== null && !frame.hasAttribute(LAYOUT_FRAME_ATTRIBUTE)) frame.setAttribute(LAYOUT_FRAME_ATTRIBUTE, "");
1058
+ }
1059
+ function markRegionColumns() {
1060
+ markSidebarColumn();
1061
+ markMainColumn();
1062
+ markRightColumn();
1063
+ markRightPanel();
1064
+ markLayoutFrame();
1065
+ }
1066
+ function clearRegionColumns() {
1067
+ clearColumn(SIDEBAR_COLUMN_ATTRIBUTE);
1068
+ clearColumn(MAIN_COLUMN_ATTRIBUTE);
1069
+ clearColumn(RIGHT_COLUMN_ATTRIBUTE);
1070
+ clearColumn(RIGHT_PANEL_ATTRIBUTE);
1071
+ clearColumn(RIGHT_PANEL_FULL_ATTRIBUTE);
1072
+ clearColumn(LAYOUT_FRAME_ATTRIBUTE);
1073
+ }
1074
+ function nudgeNativeTitlebar(active2) {
1075
+ if (document.body === null) return;
1076
+ document.body.style.setProperty("--dsh-chat-background-native", active2 ? "1" : "0");
1077
+ }
1078
+ function syncDynamicRules(elements) {
1079
+ if (typeof document === "undefined") return;
1080
+ let tag = document.querySelector(`style[data-plugin-css=${cssString(DYNAMIC_TAG_ID)}]`);
1081
+ if (tag === null) {
1082
+ tag = document.createElement("style");
1083
+ tag.dataset.plugin = "dsh-chat-background";
1084
+ tag.dataset.pluginCss = DYNAMIC_TAG_ID;
1085
+ document.head.appendChild(tag);
1086
+ }
1087
+ const selectors = [];
1088
+ for (const element of elements) {
1089
+ if (element === document.body || element === document.documentElement) {
1090
+ selectors.push(element === document.body ? "body" : "html");
1091
+ continue;
1092
+ }
1093
+ const tokens = [...element.classList].filter((token) => token !== "");
1094
+ for (const token of tokens) selectors.push(`html[data-dsh-chat-background] [class~=${cssString(token)}]`);
1095
+ }
1096
+ const next = selectors.length === 0 ? "" : `${selectors.join(",\n")} { background-color: transparent !important; }`;
1097
+ if (tag.textContent !== next) tag.textContent = next;
1098
+ }
1099
+ function markPainters() {
1100
+ const anchor = conversationAnchor();
1101
+ if (anchor === null) return [];
1102
+ const start = anchor.firstElementChild ?? anchor;
1103
+ const candidates = [];
1104
+ for (let element = start; element !== null && element !== document.documentElement; element = element.parentElement) {
1105
+ if (isOpaque(window.getComputedStyle(element).backgroundColor)) candidates.push(element);
1106
+ }
1107
+ for (const element of candidates) {
1108
+ if (painterMarks.has(element)) continue;
1109
+ painterMarks.add(element);
1110
+ element.setAttribute(LIFT_ATTRIBUTE, "");
1111
+ }
1112
+ if (candidates.length > 0) syncDynamicRules([candidates[0]]);
1113
+ return candidates;
1114
+ }
1115
+ var applied = { selection: void 0, dark: null, scrimLeft: null, scrimMain: null, scrimRight: null };
1116
+ var markSignature = -1;
1117
+ var markContainer = null;
1118
+ var flushHandle = 0;
1119
+ var flushIsTimeout = false;
1120
+ function applyBackground(selection) {
1121
+ const root = document.documentElement;
1122
+ const next = selection === null || selection === "" ? null : selection;
1123
+ const dark = isDark();
1124
+ const scrimLeft = readLeftScrim();
1125
+ const scrimMain = readMainScrim();
1126
+ const scrimRight = readRightScrim();
1127
+ const signature = document.querySelectorAll("[data-slot]").length;
1128
+ const container = conversationContainer();
1129
+ const painters = markedPainters();
1130
+ const needsPaint = signature !== markSignature || container !== markContainer || painters.length === 0;
1131
+ const sameVars = applied.selection === next && applied.dark === dark && applied.scrimLeft === scrimLeft && applied.scrimMain === scrimMain && applied.scrimRight === scrimRight;
1132
+ if (next === null) {
1133
+ if (sameVars && !root.hasAttribute(ACTIVE_ATTRIBUTE)) return;
1134
+ for (const property of INLINE_PROPERTIES) root.style.removeProperty(property);
1135
+ root.removeAttribute(ACTIVE_ATTRIBUTE);
1136
+ removeBackgroundLayer();
1137
+ root.removeAttribute(RIGHT_FULLSCREEN_ATTRIBUTE);
1138
+ root.style.removeProperty("--dsh-chat-background-full-left");
1139
+ root.style.removeProperty("--dsh-chat-background-full-width");
1140
+ clearPainters();
1141
+ clearRegionColumns();
1142
+ syncDynamicRules([]);
1143
+ nudgeNativeTitlebar(false);
1144
+ applied = { selection: null, dark, scrimLeft, scrimMain, scrimRight };
1145
+ markSignature = -1;
1146
+ markContainer = null;
1147
+ recordDiagnostics({ background: { active: false } });
1148
+ return;
1149
+ }
1150
+ markRegionColumns();
1151
+ ensureBackgroundLayer(next);
1152
+ if (sameVars && !needsPaint) return;
1153
+ if (needsPaint) {
1154
+ markSignature = signature;
1155
+ markContainer = container;
1156
+ markPainters();
1157
+ }
1158
+ recordDiagnostics({
1159
+ background: {
1160
+ active: true,
1161
+ painted: markedPainters().length,
1162
+ conversation: container !== null,
1163
+ dark,
1164
+ scrim: { left: scrimLeft, main: scrimMain, right: scrimRight },
1165
+ markers: {
1166
+ rightPanel: document.querySelector("[" + RIGHT_PANEL_ATTRIBUTE + "]") !== null,
1167
+ layoutFrame: document.querySelector("[" + LAYOUT_FRAME_ATTRIBUTE + "]") !== null,
1168
+ rightPanelFull: document.querySelector("[" + RIGHT_PANEL_ATTRIBUTE + "][" + RIGHT_PANEL_FULL_ATTRIBUTE + "]") !== null
1169
+ }
1170
+ }
1171
+ });
1172
+ if (!sameVars) {
1173
+ root.style.setProperty("--dsh-chat-background-image", 'url("' + FILE_URL + encodeURIComponent(next) + '")');
1174
+ root.style.setProperty("--dsh-chat-background-scrim-left", layer(scrimLeft));
1175
+ root.style.setProperty("--dsh-chat-background-scrim-main", layer(scrimMain));
1176
+ root.style.setProperty("--dsh-chat-background-scrim-right", layer(scrimRight));
1177
+ root.setAttribute(ACTIVE_ATTRIBUTE, next);
1178
+ applied = { selection: next, dark, scrimLeft, scrimMain, scrimRight };
1179
+ nudgeNativeTitlebar(true);
1180
+ }
1181
+ }
1182
+ function flushBackground() {
1183
+ if (flushHandle !== 0) return;
1184
+ const run = () => {
1185
+ flushHandle = 0;
1186
+ applyBackground(readSelection());
1187
+ renderComposerStrip();
1188
+ renderTokenTotal();
1189
+ };
1190
+ if (typeof window.requestAnimationFrame === "function") {
1191
+ flushIsTimeout = false;
1192
+ flushHandle = window.requestAnimationFrame(run);
1193
+ } else {
1194
+ flushIsTimeout = true;
1195
+ flushHandle = window.setTimeout(run, 16);
1196
+ }
1197
+ }
1198
+ function teardownVisuals() {
1199
+ if (flushHandle !== 0) {
1200
+ if (flushIsTimeout) window.clearTimeout(flushHandle);
1201
+ else window.cancelAnimationFrame(flushHandle);
1202
+ flushHandle = 0;
1203
+ }
1204
+ const root = document.documentElement;
1205
+ root.removeAttribute(ACTIVE_ATTRIBUTE);
1206
+ removeBackgroundLayer();
1207
+ root.removeAttribute(RIGHT_FULLSCREEN_ATTRIBUTE);
1208
+ root.style.removeProperty("--dsh-chat-background-full-left");
1209
+ root.style.removeProperty("--dsh-chat-background-full-width");
1210
+ for (const property of INLINE_PROPERTIES) root.style.removeProperty(property);
1211
+ document.body?.style.removeProperty("--dsh-chat-background-native");
1212
+ panelObserver?.disconnect();
1213
+ panelObserver = null;
1214
+ observedPanel = null;
1215
+ fullscreenGraceUntil = 0;
1216
+ fullscreenSuppressed = false;
1217
+ if (noAnimationTimer !== 0) window.clearTimeout(noAnimationTimer);
1218
+ noAnimationTimer = 0;
1219
+ root.removeAttribute(NO_ANIMATION_ATTRIBUTE);
1220
+ if (fullscreenTimer !== 0) window.clearTimeout(fullscreenTimer);
1221
+ fullscreenTimer = 0;
1222
+ if (frameAnimTimer !== 0) window.clearTimeout(frameAnimTimer);
1223
+ frameAnimTimer = 0;
1224
+ clearPainters();
1225
+ clearRegionColumns();
1226
+ syncDynamicRules([]);
1227
+ document.querySelector(`style[data-plugin-css=${cssString(DYNAMIC_TAG_ID)}]`)?.remove();
1228
+ document.querySelector(`[${STRIP_ATTRIBUTE}]`)?.remove();
1229
+ document.querySelector(`[${TOKEN_NODE_ATTRIBUTE}]`)?.remove();
1230
+ cachedAnchor2 = null;
1231
+ cachedContainer = null;
1232
+ markSignature = -1;
1233
+ markContainer = null;
1234
+ applied = { selection: void 0, dark: null, scrimLeft: null, scrimMain: null, scrimRight: null };
1235
+ }
1236
+
1237
+ // src/lib/zoom.js
1238
+ var LINE_HEIGHT = 16;
1239
+ var MAX_STEP = 120;
1240
+ var WHEEL_SENSITIVITY = 15e-4;
1241
+ function normalizeWheelDelta(deltaY, deltaMode, viewportHeight) {
1242
+ const unit = deltaMode === 1 ? LINE_HEIGHT : deltaMode === 2 ? Math.max(1, viewportHeight) : 1;
1243
+ return Math.max(-MAX_STEP, Math.min(MAX_STEP, deltaY * unit));
1244
+ }
1245
+ function wheelZoomFactor(delta) {
1246
+ return Math.exp(-delta * WHEEL_SENSITIVITY);
1247
+ }
1248
+
1249
+ // src/image-zoom.js
1250
+ var ZOOM_ATTRIBUTE = "data-dsh-image-zoom";
1251
+ var MIN_SCALE = 0.4;
1252
+ var MAX_SCALE = 8;
1253
+ var originals = /* @__PURE__ */ new WeakMap();
1254
+ var injectingWheel = false;
1255
+ function isViewerElement(element) {
1256
+ const style = getComputedStyle(element);
1257
+ if (style.position !== "fixed" && style.position !== "absolute") return false;
1258
+ const rect = element.getBoundingClientRect();
1259
+ return rect.width * rect.height >= window.innerWidth * window.innerHeight * 0.4;
1260
+ }
1261
+ function findLightboxImage(target) {
1262
+ if (target.closest(".dshcb-section") !== null) return null;
1263
+ const dialog = target.closest('[role="dialog"][aria-modal="true"]');
1264
+ if (dialog === null || !isViewerElement(dialog)) return null;
1265
+ for (const child of dialog.children) {
1266
+ if (child.tagName === "IMG") return child;
1267
+ if (child.tagName !== "DIV" || !isViewerElement(child)) continue;
1268
+ for (const grandchild of child.children) {
1269
+ if (grandchild.tagName === "IMG") return grandchild;
1270
+ }
1271
+ }
1272
+ return null;
1273
+ }
1274
+ function readTransform(image) {
1275
+ const style = getComputedStyle(image);
1276
+ if (style.transform === "none" || style.transform === "") {
1277
+ return { scale: 1, tx: 0, ty: 0 };
1278
+ }
1279
+ const matrix = new DOMMatrixReadOnly(style.transform);
1280
+ const scale = Math.hypot(matrix.a, matrix.b) || 1;
1281
+ return { scale, tx: matrix.e, ty: matrix.f };
1282
+ }
1283
+ function zoomImage(image, event) {
1284
+ const before = readTransform(image);
1285
+ const delta = normalizeWheelDelta(event.deltaY, event.deltaMode, window.innerHeight);
1286
+ const nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, before.scale * wheelZoomFactor(delta)));
1287
+ if (nextScale === before.scale) return;
1288
+ const rect = image.getBoundingClientRect();
1289
+ const baseLeft = rect.left - before.tx;
1290
+ const baseTop = rect.top - before.ty;
1291
+ const localX = (event.clientX - rect.left) / before.scale;
1292
+ const localY = (event.clientY - rect.top) / before.scale;
1293
+ const tx = event.clientX - baseLeft - nextScale * localX;
1294
+ const ty = event.clientY - baseTop - nextScale * localY;
1295
+ if (!originals.has(image)) {
1296
+ originals.set(image, { transform: image.style.transform, origin: image.style.transformOrigin });
1297
+ }
1298
+ image.style.transformOrigin = "0 0";
1299
+ image.style.transform = `translate(${tx.toFixed(2)}px, ${ty.toFixed(2)}px) scale(${nextScale.toFixed(4)})`;
1300
+ image.setAttribute(ZOOM_ATTRIBUTE, nextScale.toFixed(2));
1301
+ }
1302
+ function resetImage(image) {
1303
+ const original = originals.get(image);
1304
+ originals.delete(image);
1305
+ if (original !== void 0 && original.transform !== "") image.style.transform = original.transform;
1306
+ else image.style.removeProperty("transform");
1307
+ if (original !== void 0 && original.origin !== "") image.style.transformOrigin = original.origin;
1308
+ else image.style.removeProperty("transform-origin");
1309
+ image.removeAttribute(ZOOM_ATTRIBUTE);
1310
+ }
1311
+ function translateSidebarWheel(scrollport, event) {
1312
+ if (event.ctrlKey || injectingWheel) return false;
1313
+ injectingWheel = true;
1314
+ try {
1315
+ scrollport.dispatchEvent(
1316
+ new WheelEvent("wheel", {
1317
+ deltaX: event.deltaX,
1318
+ deltaY: event.deltaY,
1319
+ deltaMode: event.deltaMode,
1320
+ ctrlKey: true,
1321
+ clientX: event.clientX,
1322
+ clientY: event.clientY,
1323
+ bubbles: true,
1324
+ cancelable: true
1325
+ })
1326
+ );
1327
+ } finally {
1328
+ injectingWheel = false;
1329
+ }
1330
+ return true;
1331
+ }
1332
+ function onWheel(event) {
1333
+ const target = event.target instanceof Element ? event.target : null;
1334
+ if (target === null) return;
1335
+ const scrollport = target.closest("[data-document-zoom-scrollport]");
1336
+ if (scrollport !== null && target.closest("[data-image-preview]") !== null) {
1337
+ if (translateSidebarWheel(scrollport, event)) {
1338
+ event.preventDefault();
1339
+ event.stopPropagation();
1340
+ }
1341
+ return;
1342
+ }
1343
+ const image = findLightboxImage(target);
1344
+ if (image === null) return;
1345
+ event.preventDefault();
1346
+ event.stopPropagation();
1347
+ zoomImage(image, event);
1348
+ }
1349
+ function onDoubleClick(event) {
1350
+ const target = event.target instanceof Element ? event.target : null;
1351
+ if (target === null) return;
1352
+ const image = findLightboxImage(target);
1353
+ if (image === null || !originals.has(image)) return;
1354
+ resetImage(image);
1355
+ event.preventDefault();
1356
+ event.stopPropagation();
1357
+ }
1358
+ function installImageZoom() {
1359
+ window.addEventListener("wheel", onWheel, { capture: true, passive: false });
1360
+ window.addEventListener("dblclick", onDoubleClick, { capture: true });
1361
+ return () => {
1362
+ window.removeEventListener("wheel", onWheel, { capture: true });
1363
+ window.removeEventListener("dblclick", onDoubleClick, { capture: true });
1364
+ };
1365
+ }
1366
+
1367
+ // src/progress.js
1368
+ function ProgressView({ copy, enabled, percent, runningHint, estimating, idle = false }) {
1369
+ recordDiagnostics({ progress: { enabled, idle, percent, estimating, visible: enabled && !idle } });
1370
+ if (!enabled || idle) return null;
1371
+ const track = {
1372
+ className: "dshcb-progress-track",
1373
+ role: "progressbar",
1374
+ "aria-valuemin": "0",
1375
+ "aria-valuemax": "100",
1376
+ "aria-valuenow": String(percent),
1377
+ "aria-label": copy.progressTitle,
1378
+ title: estimating ? runningHint : copy.progressTitle
1379
+ };
1380
+ if (estimating) track["data-running"] = "";
1381
+ return h(
1382
+ "div",
1383
+ { className: "dshcb-progress-row" },
1384
+ h(
1385
+ "span",
1386
+ { className: "dshcb-progress-value", title: estimating ? copy.progressRunning : void 0 },
1387
+ String(percent) + copy.percent
1388
+ ),
1389
+ h("div", track, h("div", { className: "dshcb-progress-fill", style: { width: `${percent}%` } }))
1390
+ );
1391
+ }
1392
+ function ProgressDockLive({ useProjection, useSession }) {
1393
+ const copy = useCopy();
1394
+ const enabled = settings().switches.progress.useEnabled();
1395
+ const todos = useProjection("todos");
1396
+ const stats = useProjection("sessionStats");
1397
+ const running = useSession((snapshot) => snapshot !== void 0 && snapshot !== null && snapshot.running === true) === true;
1398
+ const steps = stats !== null && typeof stats === "object" && typeof stats.steps === "number" ? stats.steps : 0;
1399
+ const baseline = useRef(null);
1400
+ useEffect(() => {
1401
+ baseline.current = running ? baseline.current ?? steps : null;
1402
+ }, [running, steps]);
1403
+ const turnSteps = running && baseline.current !== null ? Math.max(0, steps - baseline.current) : 0;
1404
+ const list = Array.isArray(todos) ? todos.filter((item) => item !== null && typeof item === "object") : [];
1405
+ if (list.length > 0) {
1406
+ const done = list.filter((item) => item.status === "completed").length;
1407
+ return h(ProgressView, {
1408
+ copy,
1409
+ enabled,
1410
+ percent: Math.round(done / list.length * 100),
1411
+ runningHint: void 0,
1412
+ estimating: false
1413
+ });
1414
+ }
1415
+ if (running) {
1416
+ const percent = turnSteps <= 0 ? 4 : Math.min(92, Math.round(turnSteps / (turnSteps + 6) * 100));
1417
+ return h(ProgressView, {
1418
+ copy,
1419
+ enabled,
1420
+ percent,
1421
+ runningHint: copy.progressRunningHint.replace("{steps}", String(turnSteps)),
1422
+ estimating: true
1423
+ });
1424
+ }
1425
+ return h(ProgressView, { copy, enabled, percent: 100, runningHint: void 0, estimating: false, idle: true });
1426
+ }
1427
+ function ProgressDockStatic() {
1428
+ const copy = useCopy();
1429
+ const enabled = settings().switches.progress.useEnabled();
1430
+ return h(ProgressView, { copy, enabled, percent: 100, runningHint: void 0, estimating: false, idle: true });
1431
+ }
1432
+ function ProgressDock(props) {
1433
+ const live = typeof props.useProjection === "function" && typeof props.useSession === "function";
1434
+ return live ? h(ProgressDockLive, props) : h(ProgressDockStatic);
1435
+ }
1436
+
1437
+ // src/lib/rotation.js
1438
+ function pickNextImage(candidates, current) {
1439
+ if (candidates.length === 0) return null;
1440
+ const index = current === null ? -1 : candidates.indexOf(current);
1441
+ return candidates[(index + 1) % candidates.length];
1442
+ }
1443
+ function normalizeRotationInterval(value) {
1444
+ return Math.round(clampToRange(Number(value), ROTATION_INTERVAL_MIN, ROTATION_INTERVAL_MAX));
1445
+ }
1446
+ function normalizeRotationDuration(value) {
1447
+ const number = Number(value);
1448
+ if (!Number.isFinite(number)) return 0;
1449
+ return Math.round(Math.min(ROTATION_DURATION_MAX, Math.max(ROTATION_DURATION_MIN, number)) * 10) / 10;
1450
+ }
1451
+ function selectRotationGroup(stored, available) {
1452
+ return stored.filter((name) => available.includes(name));
1453
+ }
1454
+ function moveRotationImage(order, from, to) {
1455
+ const fromIndex = order.indexOf(from);
1456
+ const toIndex = order.indexOf(to);
1457
+ if (fromIndex < 0 || toIndex < 0 || from === to) return [...order];
1458
+ const next = [...order];
1459
+ next.splice(fromIndex, 1);
1460
+ const target = next.indexOf(to);
1461
+ next.splice(fromIndex < toIndex ? target + 1 : target, 0, from);
1462
+ return next;
1463
+ }
1464
+
1465
+ // src/rotation.js
1466
+ var LAYER_CLASS = "dshcb-rotate-layer";
1467
+ var timer = 0;
1468
+ var layerTimers = [];
1469
+ var request = null;
1470
+ var installed2 = false;
1471
+ function readRotationMode() {
1472
+ return readString(MODE_KEY) === "rotation";
1473
+ }
1474
+ function readRotationInterval() {
1475
+ const raw = Number.parseFloat(readString(ROTATION_INTERVAL_KEY) ?? "");
1476
+ return normalizeRotationInterval(Number.isFinite(raw) ? raw : ROTATION_INTERVAL_DEFAULT);
1477
+ }
1478
+ function readRotationDuration() {
1479
+ const raw = Number.parseFloat(readString(ROTATION_DURATION_KEY) ?? "");
1480
+ return normalizeRotationDuration(Number.isFinite(raw) ? raw : ROTATION_DURATION_DEFAULT);
1481
+ }
1482
+ function readRotationEffect() {
1483
+ const raw = readString(ROTATION_EFFECT_KEY);
1484
+ return ROTATION_EFFECTS.includes(raw) ? raw : ROTATION_EFFECT_DEFAULT;
1485
+ }
1486
+ function readRotationGroup() {
1487
+ const raw = readString(ROTATION_GROUP_KEY);
1488
+ if (raw === null) return [];
1489
+ try {
1490
+ const parsed = JSON.parse(raw);
1491
+ if (!Array.isArray(parsed)) return [];
1492
+ return parsed.filter((value) => typeof value === "string");
1493
+ } catch {
1494
+ return [];
1495
+ }
1496
+ }
1497
+ function scheduleLayer(fn, delay) {
1498
+ const id = window.setTimeout(() => {
1499
+ layerTimers = layerTimers.filter((entry) => entry !== id);
1500
+ fn();
1501
+ }, delay);
1502
+ layerTimers.push(id);
1503
+ }
1504
+ function removeLayer() {
1505
+ for (const element of document.querySelectorAll(`[${ROTATION_LAYER_ATTRIBUTE}]`)) element.remove();
1506
+ }
1507
+ function clearTransient() {
1508
+ for (const id of layerTimers) window.clearTimeout(id);
1509
+ layerTimers = [];
1510
+ request?.abort?.();
1511
+ request = null;
1512
+ removeLayer();
1513
+ }
1514
+ function commit(name) {
1515
+ writeString(SELECTION_KEY, name);
1516
+ applyBackground(name);
1517
+ window.dispatchEvent(new CustomEvent(ROTATION_EVENT, { detail: { name } }));
1518
+ }
1519
+ function themeBaseColour() {
1520
+ const value = getComputedStyle(document.body ?? document.documentElement).getPropertyValue("--dsw-alias-bg-base").trim();
1521
+ if (value !== "") return value;
1522
+ return document.body?.hasAttribute("data-ds-dark-theme") === true ? "rgb(9, 11, 15)" : "rgb(250, 250, 250)";
1523
+ }
1524
+ function createImageLayer(name) {
1525
+ const layer2 = document.createElement("img");
1526
+ layer2.className = LAYER_CLASS;
1527
+ layer2.setAttribute(ROTATION_LAYER_ATTRIBUTE, "");
1528
+ layer2.setAttribute("aria-hidden", "true");
1529
+ layer2.setAttribute("alt", "");
1530
+ layer2.setAttribute("draggable", "false");
1531
+ layer2.src = FILE_URL + encodeURIComponent(name);
1532
+ document.body.appendChild(layer2);
1533
+ return layer2;
1534
+ }
1535
+ function createColorLayer(color) {
1536
+ const layer2 = document.createElement("div");
1537
+ layer2.className = LAYER_CLASS;
1538
+ layer2.setAttribute(ROTATION_LAYER_ATTRIBUTE, "");
1539
+ layer2.setAttribute("aria-hidden", "true");
1540
+ layer2.style.background = color;
1541
+ document.body.appendChild(layer2);
1542
+ return layer2;
1543
+ }
1544
+ function fadeTransition(previous, next, duration) {
1545
+ const layer2 = createImageLayer(previous);
1546
+ layer2.style.opacity = "1";
1547
+ commit(next);
1548
+ void layer2.offsetWidth;
1549
+ layer2.style.transition = `opacity ${String(duration)}s ease`;
1550
+ layer2.style.opacity = "0";
1551
+ scheduleLayer(removeLayer, duration * 1e3 + 80);
1552
+ }
1553
+ function fadeThroughTransition(previous, next, duration) {
1554
+ const half = Math.max(40, duration * 1e3 / 2);
1555
+ const layer2 = createColorLayer(themeBaseColour());
1556
+ layer2.style.opacity = "0";
1557
+ void layer2.offsetWidth;
1558
+ layer2.style.transition = `opacity ${String(half / 1e3)}s ease`;
1559
+ layer2.style.opacity = "1";
1560
+ scheduleLayer(() => {
1561
+ commit(next);
1562
+ layer2.style.transition = `opacity ${String(half / 1e3)}s ease`;
1563
+ layer2.style.opacity = "0";
1564
+ scheduleLayer(removeLayer, half + 80);
1565
+ }, half);
1566
+ void previous;
1567
+ }
1568
+ function transitionTo(next) {
1569
+ const previous = readString(SELECTION_KEY);
1570
+ if (next === null || next === previous) return;
1571
+ const ref = document.documentElement.getAttribute(RIGHT_FULLSCREEN_ATTRIBUTE) !== null;
1572
+ const duration = readRotationDuration();
1573
+ const effect = readRotationEffect();
1574
+ if (ref || duration <= 0 || effect === "none" || previous === null) {
1575
+ commit(next);
1576
+ return;
1577
+ }
1578
+ if (effect === "fade-through") fadeThroughTransition(previous, next, duration);
1579
+ else fadeTransition(previous, next, duration);
1580
+ }
1581
+ async function loadImages() {
1582
+ request?.abort?.();
1583
+ const controller = new AbortController();
1584
+ request = controller;
1585
+ try {
1586
+ const response = await fetch(LIST_URL, { headers: { accept: "application/json" }, signal: controller.signal });
1587
+ if (!response.ok) return [];
1588
+ const payload = await response.json();
1589
+ return Array.isArray(payload.images) ? payload.images.map((image) => image.name) : [];
1590
+ } catch {
1591
+ return [];
1592
+ } finally {
1593
+ if (request === controller) request = null;
1594
+ }
1595
+ }
1596
+ async function tick(advance) {
1597
+ if (!installed2 || !readRotationMode()) return;
1598
+ const names = await loadImages();
1599
+ if (!installed2 || !readRotationMode()) return;
1600
+ const selected = readRotationGroup();
1601
+ const candidates = selected.length === 0 ? names : selectRotationGroup(selected, names);
1602
+ if (candidates.length === 0) return;
1603
+ const current = readString(SELECTION_KEY);
1604
+ if (current === null || !candidates.includes(current)) {
1605
+ transitionTo(candidates[0]);
1606
+ } else if (advance) {
1607
+ transitionTo(pickNextImage(candidates, current));
1608
+ }
1609
+ }
1610
+ function schedule() {
1611
+ window.clearTimeout(timer);
1612
+ timer = 0;
1613
+ if (!installed2 || !readRotationMode()) return;
1614
+ const interval = readRotationInterval();
1615
+ timer = window.setTimeout(() => {
1616
+ timer = 0;
1617
+ void tick(true).finally(() => {
1618
+ schedule();
1619
+ });
1620
+ }, interval * 1e3);
1621
+ }
1622
+ function refreshRotation({ immediate = false } = {}) {
1623
+ window.clearTimeout(timer);
1624
+ timer = 0;
1625
+ if (!installed2) return;
1626
+ if (!readRotationMode()) {
1627
+ clearTransient();
1628
+ return;
1629
+ }
1630
+ if (immediate || readString(SELECTION_KEY) === null) {
1631
+ void tick(false).finally(() => {
1632
+ schedule();
1633
+ });
1634
+ } else {
1635
+ schedule();
1636
+ }
1637
+ }
1638
+ function installRotation() {
1639
+ installed2 = true;
1640
+ refreshRotation({ immediate: true });
1641
+ return () => {
1642
+ installed2 = false;
1643
+ window.clearTimeout(timer);
1644
+ timer = 0;
1645
+ clearTransient();
1646
+ };
1647
+ }
1648
+
1649
+ // src/rows.js
1650
+ function Row({ title, description, children, disabled, status }) {
1651
+ const titleId = useId();
1652
+ const descriptionId = useId();
1653
+ const hasDescription = typeof description === "string" && description !== "";
1654
+ return h(
1655
+ "div",
1656
+ {
1657
+ className: "dshcb-row",
1658
+ "data-disabled": disabled ? "" : void 0,
1659
+ role: "group",
1660
+ "aria-labelledby": titleId === "" ? void 0 : titleId,
1661
+ "aria-describedby": hasDescription ? descriptionId : void 0
1662
+ },
1663
+ h(
1664
+ "div",
1665
+ { className: "dshcb-text" },
1666
+ h("div", { className: "dshcb-title", id: titleId === "" ? void 0 : titleId }, title),
1667
+ hasDescription ? h("div", { className: "dshcb-desc", id: descriptionId }, description) : null,
1668
+ status
1669
+ ),
1670
+ h("div", { className: "dshcb-control", "aria-describedby": hasDescription ? descriptionId : void 0 }, children)
1671
+ );
1672
+ }
1673
+ function BackgroundRow() {
1674
+ const copy = useCopy();
1675
+ const [images, setImages] = useState(null);
1676
+ const [folder, setFolder] = useState("");
1677
+ const [truncated, setTruncated] = useState(false);
1678
+ const [error, setError] = useState(null);
1679
+ const [busy, setBusy] = useState(false);
1680
+ const [selection, setSelection] = useState(() => readSelection());
1681
+ const alive = useRef(true);
1682
+ const request2 = useRef(null);
1683
+ const load = () => {
1684
+ request2.current?.abort?.();
1685
+ const controller = new AbortController();
1686
+ request2.current = controller;
1687
+ setBusy(true);
1688
+ fetch(LIST_URL, { headers: { accept: "application/json" }, signal: controller.signal }).then((response) => {
1689
+ if (!response.ok) throw new Error("HTTP " + String(response.status));
1690
+ return response.json();
1691
+ }).then((payload) => {
1692
+ if (!alive.current) return;
1693
+ const list = Array.isArray(payload.images) ? payload.images : [];
1694
+ setImages(list);
1695
+ setFolder(typeof payload.folder === "string" ? payload.folder : "");
1696
+ setTruncated(payload.truncated === true);
1697
+ setError(null);
1698
+ recordDiagnostics({
1699
+ list: { status: "ok", images: list.length, truncated: payload.truncated === true, at: Date.now() }
1700
+ });
1701
+ }).catch((failure) => {
1702
+ if (failure?.name === "AbortError") return;
1703
+ recordDiagnostics({ list: { status: "error", error: String(failure), at: Date.now() } });
1704
+ if (alive.current) setError("failed");
1705
+ }).finally(() => {
1706
+ if (alive.current && request2.current === controller) setBusy(false);
1707
+ });
1708
+ };
1709
+ useEffect(() => {
1710
+ alive.current = true;
1711
+ load();
1712
+ const onRotated = (event) => {
1713
+ const name = event?.detail?.name;
1714
+ if (typeof name === "string") setSelection(name);
1715
+ };
1716
+ window.addEventListener(ROTATION_EVENT, onRotated);
1717
+ return () => {
1718
+ alive.current = false;
1719
+ request2.current?.abort?.();
1720
+ window.removeEventListener(ROTATION_EVENT, onRotated);
1721
+ };
1722
+ }, []);
1723
+ const choose = (name) => {
1724
+ const next = name === "" ? null : name;
1725
+ writeString(SELECTION_KEY, next);
1726
+ setSelection(next);
1727
+ applyBackground(next);
1728
+ };
1729
+ const openFolder = () => {
1730
+ fetch(FOLDER_URL, {
1731
+ method: "POST",
1732
+ headers: { [FOLDER_HEADER]: FOLDER_HEADER_VALUE }
1733
+ }).then((response) => {
1734
+ if (!response.ok) throw new Error("HTTP " + String(response.status));
1735
+ if (alive.current) setError(null);
1736
+ }).catch(() => {
1737
+ if (alive.current) setError("openFailed");
1738
+ });
1739
+ };
1740
+ const current = images === null ? null : images.find((image) => image.name === selection) || null;
1741
+ const available = images === null ? [] : images;
1742
+ const errorText = error === null ? null : copy[error];
1743
+ const notices = [];
1744
+ if (errorText !== null) notices.push(h("div", { key: "error", className: "dshcb-error", role: "alert" }, errorText));
1745
+ if (errorText === null && images === null) {
1746
+ notices.push(h("div", { key: "loading", className: "dshcb-notice" }, copy.loading));
1747
+ }
1748
+ if (errorText === null && images !== null && available.length === 0) {
1749
+ notices.push(
1750
+ h(
1751
+ "div",
1752
+ { key: "empty", className: "dshcb-notice" },
1753
+ copy.emptyBefore,
1754
+ h("code", null, folder === "" ? "~/.dsh/backgrounds" : folder),
1755
+ copy.emptyAfter
1756
+ )
1757
+ );
1758
+ }
1759
+ if (errorText === null && images !== null && truncated) {
1760
+ notices.push(
1761
+ h(
1762
+ "div",
1763
+ { key: "truncated", className: "dshcb-notice" },
1764
+ copy.truncated.replace("{shown}", String(available.length))
1765
+ )
1766
+ );
1767
+ }
1768
+ const status = notices.length === 0 ? null : h("div", { className: "dshcb-status" }, notices);
1769
+ return h(
1770
+ "div",
1771
+ { className: "dshcb-background-block" },
1772
+ h(
1773
+ "div",
1774
+ {
1775
+ className: "dshcb-bg-preview",
1776
+ "aria-hidden": true,
1777
+ style: selection === null ? void 0 : { backgroundImage: 'url("' + FILE_URL + encodeURIComponent(selection) + '")' }
1778
+ },
1779
+ selection === null ? h("span", { className: "dshcb-bg-empty" }, copy.none) : null
1780
+ ),
1781
+ h(
1782
+ "div",
1783
+ { className: "dshcb-bg-file" },
1784
+ h(
1785
+ "select",
1786
+ {
1787
+ className: "dshcb-select dshcb-bg-select",
1788
+ value: selection === null ? "" : selection,
1789
+ disabled: busy,
1790
+ "aria-label": copy.choose,
1791
+ onChange: (event) => {
1792
+ choose(event.target.value);
1793
+ }
1794
+ },
1795
+ h("option", { value: "" }, copy.none),
1796
+ available.map((image) => h("option", { key: image.name, value: image.name }, image.name)),
1797
+ selection !== null && current === null ? h("option", { key: selection, value: selection }, selection + " (missing)") : null
1798
+ ),
1799
+ h("button", { type: "button", className: "dshcb-button", onClick: openFolder }, copy.open),
1800
+ h("button", { type: "button", className: "dshcb-button", onClick: load, disabled: busy }, copy.refresh)
1801
+ ),
1802
+ status
1803
+ );
1804
+ }
1805
+ function ModeRow() {
1806
+ const copy = useCopy();
1807
+ const [mode, setMode] = useState(() => readRotationMode() ? "rotation" : "fixed");
1808
+ const change = (next) => {
1809
+ setMode(next);
1810
+ writeString(MODE_KEY, next);
1811
+ refreshRotation({ immediate: next === "rotation" });
1812
+ };
1813
+ return h(
1814
+ Row,
1815
+ { title: copy.modeTitle },
1816
+ h(
1817
+ "div",
1818
+ { className: "dshcb-rotation-controls" },
1819
+ h(
1820
+ "label",
1821
+ { className: "dshcb-control-field" },
1822
+ h("span", { className: "dshcb-control-caption" }, copy.modeTitle),
1823
+ h(
1824
+ "select",
1825
+ {
1826
+ className: "dshcb-select",
1827
+ value: mode,
1828
+ "aria-label": copy.modeTitle,
1829
+ onChange: (event) => change(event.target.value)
1830
+ },
1831
+ h("option", { value: "fixed" }, copy.modeFixed),
1832
+ h("option", { value: "rotation" }, copy.modeRotation)
1833
+ )
1834
+ )
1835
+ )
1836
+ );
1837
+ }
1838
+ function RotationEffectRow() {
1839
+ const copy = useCopy();
1840
+ const [effect, setEffect] = useState(() => readRotationEffect());
1841
+ const [duration, setDuration] = useState(() => readRotationDuration());
1842
+ const [interval, setIntervalValue] = useState(() => readRotationInterval());
1843
+ const changeEffect = (next) => {
1844
+ setEffect(next);
1845
+ writeString(ROTATION_EFFECT_KEY, next);
1846
+ refreshRotation();
1847
+ };
1848
+ const changeDuration = (next) => {
1849
+ const value = normalizeRotationDuration(next);
1850
+ setDuration(value);
1851
+ writeString(ROTATION_DURATION_KEY, String(value));
1852
+ refreshRotation();
1853
+ };
1854
+ const changeInterval = (next) => {
1855
+ const value = normalizeRotationInterval(next);
1856
+ setIntervalValue(value);
1857
+ writeString(ROTATION_INTERVAL_KEY, String(value));
1858
+ refreshRotation();
1859
+ };
1860
+ return h(
1861
+ Row,
1862
+ { title: copy.rotationEffectTitle },
1863
+ h(
1864
+ "div",
1865
+ { className: "dshcb-rotation-controls" },
1866
+ h(
1867
+ "label",
1868
+ { className: "dshcb-control-field" },
1869
+ h("span", { className: "dshcb-control-caption" }, copy.effectType),
1870
+ h(
1871
+ "select",
1872
+ {
1873
+ className: "dshcb-select",
1874
+ value: effect,
1875
+ "aria-label": copy.effectType,
1876
+ onChange: (event) => changeEffect(event.target.value)
1877
+ },
1878
+ h("option", { value: "fade" }, copy.effectFade),
1879
+ h("option", { value: "fade-through" }, copy.effectFadeThrough),
1880
+ h("option", { value: "none" }, copy.effectNone)
1881
+ )
1882
+ ),
1883
+ h(
1884
+ "label",
1885
+ { className: "dshcb-control-field" },
1886
+ h("span", { className: "dshcb-control-caption" }, copy.effectDuration),
1887
+ h("input", {
1888
+ type: "range",
1889
+ className: "dshcb-range",
1890
+ min: String(ROTATION_DURATION_MIN),
1891
+ max: String(ROTATION_DURATION_MAX),
1892
+ step: "0.1",
1893
+ value: String(duration),
1894
+ "aria-label": copy.effectDuration,
1895
+ onChange: (event) => changeDuration(event.target.value)
1896
+ }),
1897
+ h("span", { className: "dshcb-value" }, duration.toFixed(1) + "s")
1898
+ ),
1899
+ h(
1900
+ "label",
1901
+ { className: "dshcb-control-field" },
1902
+ h("span", { className: "dshcb-control-caption" }, copy.rotationInterval),
1903
+ h("input", {
1904
+ type: "range",
1905
+ className: "dshcb-range",
1906
+ min: String(ROTATION_INTERVAL_MIN),
1907
+ max: String(ROTATION_INTERVAL_MAX),
1908
+ step: "5",
1909
+ value: String(interval),
1910
+ "aria-label": copy.rotationInterval,
1911
+ onChange: (event) => changeInterval(event.target.value)
1912
+ }),
1913
+ h("span", { className: "dshcb-value" }, String(interval) + "s")
1914
+ )
1915
+ )
1916
+ );
1917
+ }
1918
+ function RotationGroupRow() {
1919
+ const copy = useCopy();
1920
+ const [images, setImages] = useState(null);
1921
+ const [selected, setSelected] = useState(() => readRotationGroup());
1922
+ const [order, setOrder] = useState(() => {
1923
+ const raw = readString(ROTATION_ORDER_KEY);
1924
+ if (raw === null) return [];
1925
+ try {
1926
+ const parsed = JSON.parse(raw);
1927
+ return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
1928
+ } catch {
1929
+ return [];
1930
+ }
1931
+ });
1932
+ const [dragging, setDragging] = useState(null);
1933
+ const alive = useRef(true);
1934
+ const listRef = useRef(null);
1935
+ const flipFrom = useRef(null);
1936
+ useEffect(() => {
1937
+ alive.current = true;
1938
+ const controller = new AbortController();
1939
+ fetch(LIST_URL, { headers: { accept: "application/json" }, signal: controller.signal }).then((response) => {
1940
+ if (!response.ok) throw new Error("HTTP " + String(response.status));
1941
+ return response.json();
1942
+ }).then((payload) => {
1943
+ if (!alive.current) return;
1944
+ const names = Array.isArray(payload.images) ? payload.images.map((image) => image.name) : [];
1945
+ setImages(names);
1946
+ setOrder((current) => {
1947
+ const saved = current.filter((name) => names.includes(name));
1948
+ return [...saved, ...names.filter((name) => !saved.includes(name))];
1949
+ });
1950
+ setSelected((current) => current.filter((name) => names.includes(name)));
1951
+ }).catch(() => {
1952
+ if (alive.current) setImages([]);
1953
+ });
1954
+ return () => {
1955
+ alive.current = false;
1956
+ controller.abort();
1957
+ };
1958
+ }, []);
1959
+ const writeSelected = (next) => {
1960
+ setSelected(next);
1961
+ writeString(ROTATION_GROUP_KEY, JSON.stringify(order.filter((name) => next.includes(name))));
1962
+ refreshRotation({ immediate: true });
1963
+ };
1964
+ const writeOrder = (next) => {
1965
+ const list2 = listRef.current;
1966
+ if (list2 !== null) {
1967
+ const rects = /* @__PURE__ */ new Map();
1968
+ for (const item of list2.querySelectorAll(".dshcb-group-item")) {
1969
+ rects.set(item.getAttribute("data-name"), item.getBoundingClientRect());
1970
+ }
1971
+ flipFrom.current = rects;
1972
+ }
1973
+ setOrder(next);
1974
+ writeString(ROTATION_ORDER_KEY, JSON.stringify(next));
1975
+ writeString(ROTATION_GROUP_KEY, JSON.stringify(next.filter((name) => selected.includes(name))));
1976
+ refreshRotation({ immediate: true });
1977
+ };
1978
+ useLayoutEffect(() => {
1979
+ const previous = flipFrom.current;
1980
+ flipFrom.current = null;
1981
+ const list2 = listRef.current;
1982
+ if (previous === null || list2 === null) return;
1983
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true) return;
1984
+ for (const item of list2.querySelectorAll(".dshcb-group-item")) {
1985
+ const before = previous.get(item.getAttribute("data-name"));
1986
+ if (before === void 0) continue;
1987
+ const after = item.getBoundingClientRect();
1988
+ const delta = before.top - after.top;
1989
+ if (Math.abs(delta) < 1) continue;
1990
+ item.animate([{ transform: `translateY(${String(delta)}px)` }, { transform: "translateY(0)" }], {
1991
+ duration: 220,
1992
+ easing: "cubic-bezier(.2, .8, .2, 1)"
1993
+ });
1994
+ }
1995
+ }, [order]);
1996
+ const toggle = (name) => {
1997
+ writeSelected(selected.includes(name) ? selected.filter((entry) => entry !== name) : [...selected, name]);
1998
+ };
1999
+ const available = images ?? [];
2000
+ const displayNames = order.filter((name) => available.includes(name));
2001
+ const move = (from, to) => {
2002
+ if (from === null || from === to) return;
2003
+ writeOrder(moveRotationImage(order, from, to));
2004
+ };
2005
+ const list = images === null ? h("div", { className: "dshcb-notice" }, copy.loading) : available.length === 0 ? h("div", { className: "dshcb-notice" }, copy.rotationGroupEmpty) : h(
2006
+ "div",
2007
+ { className: "dshcb-group-list", ref: listRef },
2008
+ displayNames.map((name) => {
2009
+ const checked = selected.includes(name);
2010
+ return h(
2011
+ "div",
2012
+ {
2013
+ key: name,
2014
+ className: "dshcb-group-item",
2015
+ "data-name": name,
2016
+ "data-selected": checked ? "" : void 0,
2017
+ "data-dragging": dragging === name ? "" : void 0,
2018
+ onDragOver: (event) => {
2019
+ if (dragging !== null) event.preventDefault();
2020
+ },
2021
+ onDrop: (event) => {
2022
+ event.preventDefault();
2023
+ move(dragging, name);
2024
+ setDragging(null);
2025
+ }
2026
+ },
2027
+ h("input", {
2028
+ type: "checkbox",
2029
+ checked,
2030
+ "aria-label": name,
2031
+ onChange: () => toggle(name)
2032
+ }),
2033
+ h("img", {
2034
+ className: "dshcb-group-thumb",
2035
+ src: FILE_URL + encodeURIComponent(name),
2036
+ alt: "",
2037
+ loading: "lazy",
2038
+ decoding: "async"
2039
+ }),
2040
+ h("span", { className: "dshcb-group-name" }, name),
2041
+ h(
2042
+ "span",
2043
+ {
2044
+ className: "dshcb-drag-handle",
2045
+ draggable: true,
2046
+ title: copy.dragHandle,
2047
+ "aria-label": copy.dragHandle,
2048
+ onDragStart: (event) => {
2049
+ event.dataTransfer.setData("text/plain", name);
2050
+ event.dataTransfer.effectAllowed = "move";
2051
+ setDragging(name);
2052
+ },
2053
+ onDragEnd: () => setDragging(null),
2054
+ onClick: (event) => event.preventDefault()
2055
+ },
2056
+ "☰"
2057
+ )
2058
+ );
2059
+ })
2060
+ );
2061
+ return h(
2062
+ Row,
2063
+ { title: copy.rotationGroupTitle },
2064
+ h(
2065
+ "div",
2066
+ { className: "dshcb-rotation-controls" },
2067
+ list,
2068
+ h(
2069
+ "div",
2070
+ { className: "dshcb-group-actions" },
2071
+ h(
2072
+ "button",
2073
+ {
2074
+ type: "button",
2075
+ className: "dshcb-button",
2076
+ disabled: available.length === 0,
2077
+ onClick: () => writeSelected(available)
2078
+ },
2079
+ copy.rotationGroupAll
2080
+ ),
2081
+ h(
2082
+ "button",
2083
+ {
2084
+ type: "button",
2085
+ className: "dshcb-button",
2086
+ disabled: selected.length === 0,
2087
+ onClick: () => writeSelected([])
2088
+ },
2089
+ copy.rotationGroupNone
2090
+ )
2091
+ )
2092
+ )
2093
+ );
2094
+ }
2095
+ function SwitchRow({ setting, title, description }) {
2096
+ const enabled = setting.useEnabled();
2097
+ return h(
2098
+ Row,
2099
+ { title, description },
2100
+ h("button", {
2101
+ type: "button",
2102
+ role: "switch",
2103
+ "aria-checked": enabled ? "true" : "false",
2104
+ "aria-label": title,
2105
+ className: "dshcb-switch",
2106
+ onClick: () => {
2107
+ setting.write(!enabled);
2108
+ }
2109
+ })
2110
+ );
2111
+ }
2112
+ function ColorRow({ setting, title, description }) {
2113
+ const copy = useCopy();
2114
+ const { color, change, reset } = setting.useColor();
2115
+ return h(
2116
+ Row,
2117
+ { title, description },
2118
+ h("input", {
2119
+ type: "color",
2120
+ className: "dshcb-color",
2121
+ value: color,
2122
+ "aria-label": title,
2123
+ onChange: (event) => {
2124
+ change(event.target.value);
2125
+ }
2126
+ }),
2127
+ h(
2128
+ "button",
2129
+ {
2130
+ type: "button",
2131
+ className: "dshcb-button",
2132
+ onClick: () => {
2133
+ reset();
2134
+ }
2135
+ },
2136
+ copy.reset
2137
+ )
2138
+ );
2139
+ }
2140
+ function ActiveSliderRow({ storageKey, minimum, maximum, fallback, title, description, disabled, unit, after }) {
2141
+ const copy = useCopy();
2142
+ const [value, setValue] = useState(() => readPercent(storageKey, fallback, minimum, maximum));
2143
+ const change = (next) => {
2144
+ const parsed = Number(next);
2145
+ const clamped = Number.isFinite(parsed) ? clampToRange(parsed, minimum, maximum) : value;
2146
+ setValue(clamped);
2147
+ writeString(storageKey, String(clamped));
2148
+ if (typeof after === "function") after(clamped);
2149
+ else applyBackground(readSelection());
2150
+ };
2151
+ return h(
2152
+ Row,
2153
+ { title, description, disabled },
2154
+ h("input", {
2155
+ type: "range",
2156
+ className: "dshcb-range",
2157
+ min: String(minimum),
2158
+ max: String(maximum),
2159
+ step: "1",
2160
+ value: String(value),
2161
+ disabled: disabled === true,
2162
+ "aria-label": title,
2163
+ onChange: (event) => {
2164
+ change(Number(event.target.value));
2165
+ }
2166
+ }),
2167
+ h("span", { className: "dshcb-value" }, String(value) + (unit ?? copy.percent))
2168
+ );
2169
+ }
2170
+ function ScrimLeftRow() {
2171
+ const copy = useCopy();
2172
+ return h(ActiveSliderRow, {
2173
+ storageKey: SCRIM_LEFT_KEY,
2174
+ minimum: SCRIM_MIN,
2175
+ maximum: SCRIM_MAX,
2176
+ fallback: readPercent(VEIL_KEY, VEIL_DEFAULT, VEIL_MIN, VEIL_MAX),
2177
+ title: copy.scrimLeftTitle
2178
+ });
2179
+ }
2180
+ function ScrimMainRow() {
2181
+ const copy = useCopy();
2182
+ return h(ActiveSliderRow, {
2183
+ storageKey: SCRIM_MAIN_KEY,
2184
+ minimum: SCRIM_MIN,
2185
+ maximum: SCRIM_MAX,
2186
+ fallback: readPercent(SCRIM_KEY, SCRIM_DEFAULT, SCRIM_MIN, SCRIM_MAX),
2187
+ title: copy.scrimMainTitle
2188
+ });
2189
+ }
2190
+ function ScrimRightRow() {
2191
+ const copy = useCopy();
2192
+ return h(ActiveSliderRow, {
2193
+ storageKey: SCRIM_RIGHT_KEY,
2194
+ minimum: SCRIM_MIN,
2195
+ maximum: SCRIM_MAX,
2196
+ fallback: readPercent(VEIL_KEY, VEIL_DEFAULT, VEIL_MIN, VEIL_MAX),
2197
+ title: copy.scrimRightTitle
2198
+ });
2199
+ }
2200
+ function StripRow() {
2201
+ const copy = useCopy();
2202
+ return h(SwitchRow, {
2203
+ setting: settings().switches.strip,
2204
+ title: copy.stripTitle,
2205
+ description: copy.stripDescription
2206
+ });
2207
+ }
2208
+ function StripColorRow() {
2209
+ const copy = useCopy();
2210
+ return h(ColorRow, {
2211
+ setting: settings().colors.strip,
2212
+ title: copy.stripColorTitle,
2213
+ description: copy.stripColorDescription
2214
+ });
2215
+ }
2216
+ function ProgressRow() {
2217
+ const copy = useCopy();
2218
+ return h(SwitchRow, {
2219
+ setting: settings().switches.progress,
2220
+ title: copy.progressTitle,
2221
+ description: copy.progressDescription
2222
+ });
2223
+ }
2224
+ function ProgressColorRow() {
2225
+ const copy = useCopy();
2226
+ return h(ColorRow, {
2227
+ setting: settings().colors.progress,
2228
+ title: copy.progressColorTitle,
2229
+ description: copy.progressColorDescription
2230
+ });
2231
+ }
2232
+ function TokenRow() {
2233
+ const copy = useCopy();
2234
+ return h(SwitchRow, {
2235
+ setting: settings().switches.token,
2236
+ title: copy.tokenTitle,
2237
+ description: copy.tokenDescription
2238
+ });
2239
+ }
2240
+ function TokenSizeRow() {
2241
+ const copy = useCopy();
2242
+ return h(ActiveSliderRow, {
2243
+ storageKey: TOKEN_SIZE_KEY,
2244
+ minimum: TOKEN_SIZE_MIN,
2245
+ maximum: TOKEN_SIZE_MAX,
2246
+ fallback: TOKEN_SIZE_DEFAULT,
2247
+ title: copy.tokenSizeTitle,
2248
+ description: copy.tokenSizeDescription,
2249
+ unit: "px",
2250
+ after: () => {
2251
+ applyTokenSize();
2252
+ }
2253
+ });
2254
+ }
2255
+ var BACKGROUND_ROWS = [
2256
+ BackgroundRow,
2257
+ ModeRow,
2258
+ RotationEffectRow,
2259
+ RotationGroupRow,
2260
+ ScrimLeftRow,
2261
+ ScrimMainRow,
2262
+ ScrimRightRow
2263
+ ];
2264
+ var UI_GROUPS = [
2265
+ [StripRow, StripColorRow],
2266
+ [ProgressRow, ProgressColorRow],
2267
+ [TokenRow, TokenSizeRow]
2268
+ ];
2269
+ function UiCard({ rows }) {
2270
+ return h(
2271
+ "div",
2272
+ { className: "dshcb-card" },
2273
+ rows.map((RowComponent, index) => h(RowComponent, { key: String(index) }))
2274
+ );
2275
+ }
2276
+ function PageSection() {
2277
+ const copy = useCopy();
2278
+ const [tab, setTab] = useState("background");
2279
+ const tabs = [
2280
+ { id: "background", label: copy.tabBackground },
2281
+ { id: "ui", label: copy.tabUi }
2282
+ ];
2283
+ const panelId = useId();
2284
+ const tabId = (id) => "dshcb-tab-" + id;
2285
+ return h(
2286
+ "div",
2287
+ { className: "dshcb-section" },
2288
+ h(
2289
+ "div",
2290
+ { className: "dshcb-tabs", role: "tablist" },
2291
+ tabs.map(
2292
+ (entry) => h(
2293
+ "button",
2294
+ {
2295
+ key: entry.id,
2296
+ type: "button",
2297
+ role: "tab",
2298
+ id: tabId(entry.id),
2299
+ "aria-controls": panelId,
2300
+ tabIndex: tab === entry.id ? 0 : -1,
2301
+ className: "dshcb-tab" + (tab === entry.id ? " dshcb-tab-active" : ""),
2302
+ "aria-selected": tab === entry.id ? "true" : "false",
2303
+ onClick: () => setTab(entry.id),
2304
+ onKeyDown: (event) => {
2305
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
2306
+ event.preventDefault();
2307
+ setTab(tab === "background" ? "ui" : "background");
2308
+ }
2309
+ }
2310
+ },
2311
+ entry.label
2312
+ )
2313
+ )
2314
+ ),
2315
+ h(
2316
+ "div",
2317
+ { className: "dshcb-tab-panel", role: "tabpanel", id: panelId, "aria-labelledby": tabId(tab) },
2318
+ tab === "background" ? BACKGROUND_ROWS.map((RowComponent, index) => h(RowComponent, { key: String(index) })) : UI_GROUPS.map((rows, index) => h(UiCard, { key: String(index), rows }))
2319
+ )
2320
+ );
2321
+ }
2322
+
2323
+ // src/styles.js
2324
+ var active = `html[${ACTIVE_ATTRIBUTE}]`;
2325
+ var css = `
2326
+ /* ---- background image ---------------------------------------------------- */
2327
+
2328
+ ${active} {
2329
+ background-color: transparent !important;
2330
+ }
2331
+
2332
+ /* The image lives on its own fixed element, not on <html>: Chromium renders
2333
+ * animated GIF/WebP/APNG backgrounds on the root element as a static first
2334
+ * frame. */
2335
+ .dshcb-background-layer {
2336
+ position: fixed;
2337
+ inset: 0;
2338
+ z-index: -1;
2339
+ width: 100%;
2340
+ height: 100%;
2341
+ pointer-events: none;
2342
+ display: block;
2343
+ object-fit: cover;
2344
+ object-position: center center;
2345
+ }
2346
+
2347
+ /* Temporary rotation layer: painted above the <html> background but below
2348
+ * the app columns and their scrims. The manager sets the image/colour and
2349
+ * animates opacity inline. */
2350
+ .dshcb-rotate-layer {
2351
+ position: fixed;
2352
+ inset: 0;
2353
+ z-index: -1;
2354
+ width: 100%;
2355
+ height: 100%;
2356
+ pointer-events: none;
2357
+ display: block;
2358
+ object-fit: cover;
2359
+ object-position: center center;
2360
+ }
2361
+
2362
+ /* Rotation settings block. The mode/effect/group controls stack inside
2363
+ * the normal row control column so they line up with every other row. */
2364
+ .dshcb-rotation-controls {
2365
+ display: flex;
2366
+ flex-direction: column;
2367
+ gap: 8px;
2368
+ min-width: 260px;
2369
+ max-width: 340px;
2370
+ }
2371
+ .dshcb-control-field {
2372
+ display: flex;
2373
+ align-items: center;
2374
+ gap: 8px;
2375
+ font-size: 12px;
2376
+ }
2377
+ .dshcb-control-field > .dshcb-control-caption {
2378
+ flex: 0 0 auto;
2379
+ color: var(--dsw-alias-label-secondary);
2380
+ }
2381
+ .dshcb-control-field > .dshcb-select {
2382
+ flex: 1 1 auto;
2383
+ min-width: 0;
2384
+ }
2385
+ .dshcb-control-field > .dshcb-range {
2386
+ flex: 1 1 auto;
2387
+ min-width: 80px;
2388
+ }
2389
+ .dshcb-control-field > .dshcb-value {
2390
+ flex: 0 0 auto;
2391
+ min-width: 42px;
2392
+ text-align: end;
2393
+ }
2394
+ .dshcb-group-list {
2395
+ display: flex;
2396
+ flex-direction: column;
2397
+ gap: 2px;
2398
+ max-height: 128px;
2399
+ overflow-y: auto;
2400
+ overscroll-behavior: contain;
2401
+ }
2402
+ .dshcb-group-item {
2403
+ display: flex;
2404
+ align-items: center;
2405
+ gap: 6px;
2406
+ flex: none;
2407
+ height: 24px;
2408
+ max-width: 100%;
2409
+ padding: 0 2px;
2410
+ border-radius: 4px;
2411
+ font-size: 12px;
2412
+ }
2413
+ .dshcb-group-item[data-dragging] {
2414
+ opacity: .5;
2415
+ will-change: transform;
2416
+ }
2417
+ .dshcb-group-thumb {
2418
+ flex: 0 0 auto;
2419
+ width: 20px;
2420
+ height: 20px;
2421
+ border-radius: 3px;
2422
+ object-fit: cover;
2423
+ background: var(--dsw-alias-interactive-bg-hover);
2424
+ }
2425
+ .dshcb-group-name {
2426
+ flex: 1 1 auto;
2427
+ min-width: 0;
2428
+ overflow: hidden;
2429
+ text-overflow: ellipsis;
2430
+ white-space: nowrap;
2431
+ }
2432
+ .dshcb-drag-handle {
2433
+ flex: 0 0 auto;
2434
+ padding: 0 2px;
2435
+ color: var(--dsw-alias-label-tertiary);
2436
+ cursor: grab;
2437
+ font-size: 14px;
2438
+ line-height: 1;
2439
+ user-select: none;
2440
+ }
2441
+ .dshcb-group-actions {
2442
+ display: flex;
2443
+ gap: 8px;
2444
+ }
2445
+
2446
+ /* Scrims are independent per region (left sidebar / main column / right
2447
+ * sidebar); each column is marked by the plugin and paints its own layer at
2448
+ * z-index -1 inside an isolated stacking context. There is deliberately no
2449
+ * window-wide pseudo-element: it could not leave the sidebars untouched. */
2450
+
2451
+ /* Measured containers: any ancestor of the conversation that paints an opaque
2452
+ * base surface is marked by markPainters(R11-01) and cleared here. */
2453
+ ${active} [${LIFT_ATTRIBUTE}] {
2454
+ background-color: transparent !important;
2455
+ }
2456
+
2457
+ /* ---- sidebar ------------------------------------------------------------- */
2458
+
2459
+ /* The sidebar column and the sidebar slot's own root are two opaque surfaces of
2460
+ * the app frame. The column is marked by markSidebarColumn() through the
2461
+ * [data-slot=sidebar] outlet's parent, so these rules no longer depend on
2462
+ * the hashed CSS-module class name; the class selector stays as a last-resort
2463
+ * fallback for a composition where the slot outlet is missing. Both clear
2464
+ * background-image too: macOS paints the column with a gradient (R2 F2). */
2465
+ ${active} [${SIDEBAR_COLUMN_ATTRIBUTE}],
2466
+ ${active} [${SIDEBAR_COLUMN_ATTRIBUTE}] > [data-slot="sidebar"] > *,
2467
+ ${active} [class*="sidebarCol"] {
2468
+ background-color: transparent !important;
2469
+ background-image: none !important;
2470
+ }
2471
+
2472
+ /* The three painted columns get their own stacking context so a scrim layer
2473
+ * can sit at z-index -1: above the image, below every in-flow surface. A
2474
+ * positioned pseudo-element needs a positioned containing block, so each
2475
+ * column gets position: relative as well (isolation alone is not enough). */
2476
+ ${active} [${SIDEBAR_COLUMN_ATTRIBUTE}],
2477
+ ${active} [${MAIN_COLUMN_ATTRIBUTE}],
2478
+ ${active} [${RIGHT_COLUMN_ATTRIBUTE}],
2479
+ ${active} [class*="sidebarCol"] {
2480
+ position: relative !important;
2481
+ isolation: isolate !important;
2482
+ }
2483
+
2484
+ /* Left sidebar scrim. */
2485
+ ${active} [${SIDEBAR_COLUMN_ATTRIBUTE}]::before,
2486
+ ${active} [class*="sidebarCol"]::before {
2487
+ content: "";
2488
+ position: absolute;
2489
+ inset: 0;
2490
+ z-index: -1;
2491
+ pointer-events: none;
2492
+ background: var(--dsh-chat-background-scrim-left);
2493
+ }
2494
+
2495
+ /* Main conversation column scrim. */
2496
+ ${active} [${MAIN_COLUMN_ATTRIBUTE}]::after {
2497
+ content: "";
2498
+ position: absolute;
2499
+ inset: 0;
2500
+ z-index: -1;
2501
+ pointer-events: none;
2502
+ background: var(--dsh-chat-background-scrim-main);
2503
+ }
2504
+
2505
+ /* Normal size: the right column carries the scrim like the other columns, so
2506
+ * collapsing the sidebar shrinks the scrim with it. */
2507
+ ${active} [${RIGHT_COLUMN_ATTRIBUTE}]::after {
2508
+ content: "";
2509
+ position: absolute;
2510
+ inset: 0;
2511
+ z-index: -1;
2512
+ pointer-events: none;
2513
+ background: var(--dsh-chat-background-scrim-right);
2514
+ }
2515
+ ${active} [${RIGHT_COLUMN_ATTRIBUTE}][${RIGHT_PANEL_FULL_ATTRIBUTE}]::after {
2516
+ display: none;
2517
+ }
2518
+
2519
+ /* Full-window preview: the panel itself paints the image + one right scrim,
2520
+ * covering the conversation underneath instead of letting it show through
2521
+ * (R11-01). The marker is only present while the panel is full-window, so a
2522
+ * collapsing normal sidebar never leaves a scrim rectangle behind. */
2523
+ /* Fullscreen means "the main window becomes the right sidebar": keep the left
2524
+ * sidebar in place, hide the conversation column, and let the panel cover the
2525
+ * rest. The panel is repositioned through the fullscreen offset variables so
2526
+ * it starts at the left sidebar edge instead of covering the whole window. */
2527
+ ${active}[${RIGHT_FULLSCREEN_ATTRIBUTE}] [${MAIN_COLUMN_ATTRIBUTE}] {
2528
+ visibility: hidden !important;
2529
+ }
2530
+
2531
+ ${active} [${RIGHT_PANEL_ATTRIBUTE}][${RIGHT_PANEL_FULL_ATTRIBUTE}] {
2532
+ left: var(--dsh-chat-background-full-left, 0px) !important;
2533
+ right: auto !important;
2534
+ width: var(--dsh-chat-background-full-width, 100vw) !important;
2535
+ background-color: transparent !important;
2536
+ background-image: var(--dsh-chat-background-scrim-right), var(--dsh-chat-background-image) !important;
2537
+ background-size: auto, cover !important;
2538
+ background-position: center, center !important;
2539
+ background-repeat: no-repeat !important;
2540
+ background-attachment: fixed !important;
2541
+ }
2542
+
2543
+ /* While switching fullscreen, freeze the rightbar panel/contents for a few
2544
+ * frames so the app cannot animate the transition (R16-01). */
2545
+ ${active}[${NO_ANIMATION_ATTRIBUTE}] [data-slot="rightbar"] *,
2546
+ ${active}[${NO_ANIMATION_ATTRIBUTE}] [${RIGHT_PANEL_ATTRIBUTE}] {
2547
+ transition: none !important;
2548
+ animation: none !important;
2549
+ }
2550
+
2551
+ /* The layout frame gets a transition only for the duration of an open/collapse
2552
+ * click; it is not left on, otherwise dragging the sidebar edge would lag. The
2553
+ * timing matches the app content transform (0.3s cubic-bezier(.4,0,.2,1)). */
2554
+ ${active} [${LAYOUT_FRAME_ATTRIBUTE}][${FRAME_ANIMATING_ATTRIBUTE}] {
2555
+ transition: grid-template-columns 300ms cubic-bezier(.4, 0, .2, 1) !important;
2556
+ }
2557
+
2558
+ /* The session list's own bottom fade ends on the fixed sidebar fill
2559
+ * (linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill))),
2560
+ * which becomes a light band above the account row once the sidebar is
2561
+ * transparent. Re-point its end colour at the veil so it still fades out
2562
+ * instead of leaving an edge.
2563
+ *
2564
+ * Scoped to the workspace slot inside the sidebar: a bare [class*="fade"] also
2565
+ * matches the conversation turn-rail masks and group-body masks, and painting a
2566
+ * background on those leaves visible bars (BG-03). */
2567
+ ${active} [data-slot="sidebar.workspaces"] [class*="fade"],
2568
+ ${active} [${SIDEBAR_COLUMN_ATTRIBUTE}] [class*="fade"] {
2569
+ background: linear-gradient(180deg, transparent, var(--dsh-chat-background-scrim-left, transparent)) !important;
2570
+ }
2571
+
2572
+ /* Windows desktop draws a 40px title-bar fill on the app frame's ::before
2573
+ * (html[data-windows-titlebar] is set by the preload); a measured
2574
+ * background-color cannot see a pseudo-element, so clear it directly. */
2575
+ ${active}[data-windows-titlebar] [class*="frame"]::before {
2576
+ background: transparent !important;
2577
+ }
2578
+
2579
+ /* The desktop preload samples a hidden probe span whose background is
2580
+ * var(--dsw-specific-sidebar-fill) and sends it to Electron as the native
2581
+ * title-bar overlay colour. Overriding only that probe (never the token
2582
+ * itself) makes the window-control strip transparent without changing any
2583
+ * app surface that uses the token. appending a body custom property in
2584
+ * background.js nudges the preload into re-sending the colour. */
2585
+ ${active} body > span[style*="dsw-specific-sidebar-fill"] {
2586
+ background-color: transparent !important;
2587
+ }
2588
+
2589
+ /* Accessibility: when the user asks for reduced transparency or the system runs
2590
+ * forced-colors, the honest fallback is the plain theme rather than a scrim the
2591
+ * user asked not to have. */
2592
+ @media (forced-colors: active) {
2593
+ ${active} {
2594
+ background-image: none !important;
2595
+ }
2596
+ ${active} [${SIDEBAR_COLUMN_ATTRIBUTE}]::before,
2597
+ ${active} [${MAIN_COLUMN_ATTRIBUTE}]::after,
2598
+ ${active} [${RIGHT_COLUMN_ATTRIBUTE}]::after {
2599
+ display: none;
2600
+ }
2601
+ }
2602
+ @media (prefers-reduced-motion: reduce) {
2603
+ ${active} [${LAYOUT_FRAME_ATTRIBUTE}] {
2604
+ transition: none !important;
2605
+ }
2606
+ }
2607
+ @media (prefers-reduced-transparency: reduce) {
2608
+ ${active} {
2609
+ background-image: none !important;
2610
+ }
2611
+ ${active} [${SIDEBAR_COLUMN_ATTRIBUTE}]::before,
2612
+ ${active} [${MAIN_COLUMN_ATTRIBUTE}]::after,
2613
+ ${active} [${RIGHT_COLUMN_ATTRIBUTE}]::after {
2614
+ display: none;
2615
+ }
2616
+ }
2617
+
2618
+ /* The right sidebar is its own tab host with an opaque section surface; it is
2619
+ * cleared through the stable rightbar slot outlet so the image also shows
2620
+ * there (the slot outlet is display: contents; the section is its DOM
2621
+ * descendant). Content cards inside keep their own surfaces for readability. */
2622
+ ${active} [data-slot="rightbar"] [class*="tabHost"] {
2623
+ background-color: transparent !important;
2624
+ background-image: none !important;
2625
+ }
2626
+
2627
+ /* The conversation's composer seat carries a fade pad
2628
+ * (linear-gradient(180deg, transparent 0px, var(--dsw-alias-bg-base) 36px),
2629
+ * sticky at the bottom), which leaves a white band under the composer. Clear
2630
+ * it while a background is applied so the image reaches the bottom. */
2631
+ ${active} :is([class*="composerSeat"], [class*="composerFade"], [class*="dockFade"]) {
2632
+ background-image: none !important;
2633
+ }
2634
+
2635
+ /* The bar under the composer: absolutely positioned inside the composer
2636
+ * wrapper, one layer below every child of that wrapper, so it paints over the
2637
+ * background image and under the usage row. isolation on the wrapper is what
2638
+ * lets the negative layer stop there; it creates a stacking context only,
2639
+ * never a containing block for fixed-position descendants. */
2640
+ ${active} [data-slot="conversation.composer.bar"] > * {
2641
+ position: relative !important;
2642
+ isolation: isolate !important;
2643
+ }
2644
+
2645
+ /* Both plugin colours default per theme in the stylesheet; a picked colour
2646
+ * arrives as an inline property on <html> and replaces them. */
2647
+ ${active} {
2648
+ --dsh-chat-strip-bg: rgba(242, 243, 245, 0.92);
2649
+ }
2650
+
2651
+ /* The theme service puts data-ds-dark-theme on <body>; <html> cannot be a
2652
+ * descendant of <body>, so the override is published on the body and inherited
2653
+ * down to the injected bar. A selector of the form
2654
+ * body[data-ds-dark-theme] html[...] can never match (BG-01). */
2655
+ ${active} body[data-ds-dark-theme] {
2656
+ --dsh-chat-strip-bg: rgba(38, 41, 48, 0.92);
2657
+ }
2658
+
2659
+ ${active} [${STRIP_ATTRIBUTE}] {
2660
+ position: absolute;
2661
+ z-index: -1;
2662
+ pointer-events: none;
2663
+ border-radius: 12px;
2664
+ background: var(--dsh-chat-strip-bg);
2665
+ }
2666
+
2667
+ /* Settings surfaces stay readable: the section shell paints the app base.
2668
+ *
2669
+ * Deliberately limited to the settings entries: a broader shell.overlay > *
2670
+ * rule would paint an opaque background on every floating layer other plugins
2671
+ * register there (BG-15). */
2672
+ ${active} :is([data-slot="settings.section"], [data-slot="settings.header"], [data-slot="settings.shell"]) {
2673
+ background-color: var(--dsw-alias-bg-base) !important;
2674
+ }
2675
+
2676
+ /* ---- settings rows ------------------------------------------------------- */
2677
+
2678
+ /* Inline styles cannot express :hover, focus, switch states or a track fill, so
2679
+ * the rows keep their layout in this plugin stylesheet. */
2680
+
2681
+ /* Settings page shell: one column of rows, like the built-in sections. The last
2682
+ * row drops its divider so the page does not end on a line. */
2683
+ .dshcb-section {
2684
+ display: flex;
2685
+ flex-direction: column;
2686
+ width: 100%;
2687
+ }
2688
+ .dshcb-section > :last-child {
2689
+ border-bottom: none;
2690
+ }
2691
+
2692
+ /* Sub-navigation mirrors the app’s pill tabs: the active tab is a filled
2693
+ * brand pill, the others stay quiet text buttons. */
2694
+ .dshcb-tabs {
2695
+ display: flex;
2696
+ align-items: center;
2697
+ gap: 6px;
2698
+ padding: 2px 0 14px;
2699
+ }
2700
+ .dshcb-tab {
2701
+ height: 32px;
2702
+ padding: 0 14px;
2703
+ border: none;
2704
+ border-radius: 8px;
2705
+ background: transparent;
2706
+ color: var(--dsw-alias-label-secondary);
2707
+ font: inherit;
2708
+ font-size: 13px;
2709
+ line-height: 20px;
2710
+ cursor: pointer;
2711
+ }
2712
+ .dshcb-tab:hover:not(.dshcb-tab-active) {
2713
+ background: var(--dsw-alias-bg-layer-1);
2714
+ }
2715
+ .dshcb-tab-active {
2716
+ background: var(--dsw-alias-brand-primary);
2717
+ color: var(--dsw-alias-label-primary-inverted, #fff);
2718
+ }
2719
+
2720
+ /* One card per component on the UI tab, matching the app’s bordered
2721
+ * summary cards: 1px hairline and a large radius. */
2722
+ .dshcb-card {
2723
+ margin-top: 12px;
2724
+ border: 1px solid var(--dsw-alias-border-l2);
2725
+ border-radius: var(--dsw-radius-lg, 12px);
2726
+ padding: 0 16px;
2727
+ }
2728
+ .dshcb-card .dshcb-row {
2729
+ padding: 14px 0;
2730
+ }
2731
+ .dshcb-card .dshcb-row:last-child {
2732
+ border-bottom: none;
2733
+ }
2734
+ .dshcb-tab-panel > .dshcb-row:last-child {
2735
+ border-bottom: none;
2736
+ }
2737
+ .dshcb-status {
2738
+ display: flex;
2739
+ flex-direction: column;
2740
+ gap: 4px;
2741
+ }
2742
+
2743
+ .dshcb-background-block {
2744
+ display: flex;
2745
+ flex-direction: column;
2746
+ gap: 12px;
2747
+ padding: 16px 0;
2748
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
2749
+ }
2750
+ .dshcb-bg-preview {
2751
+ display: flex;
2752
+ align-items: center;
2753
+ justify-content: center;
2754
+ width: 100%;
2755
+ height: 180px;
2756
+ border-radius: var(--dsw-radius-md, 8px);
2757
+ background-color: var(--dsw-alias-bg-layer-1);
2758
+ background-position: center;
2759
+ background-size: cover;
2760
+ background-repeat: no-repeat;
2761
+ overflow: hidden;
2762
+ }
2763
+ .dshcb-bg-empty {
2764
+ color: var(--dsw-alias-label-tertiary);
2765
+ font-size: 13px;
2766
+ }
2767
+ .dshcb-bg-file {
2768
+ display: flex;
2769
+ align-items: center;
2770
+ gap: 8px;
2771
+ }
2772
+ .dshcb-bg-select {
2773
+ flex: 1;
2774
+ max-width: none;
2775
+ min-width: 0;
2776
+ }
2777
+ .dshcb-bg-file .dshcb-select {
2778
+ flex: 1;
2779
+ max-width: none;
2780
+ }
2781
+
2782
+ .dshcb-row {
2783
+ display: flex;
2784
+ align-items: center;
2785
+ justify-content: space-between;
2786
+ gap: 24px;
2787
+ padding: 16px 0;
2788
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
2789
+ }
2790
+ .dshcb-text {
2791
+ display: flex;
2792
+ flex-direction: column;
2793
+ flex: 1;
2794
+ gap: 4px;
2795
+ min-width: 0;
2796
+ }
2797
+ .dshcb-title {
2798
+ color: var(--dsw-alias-label-primary);
2799
+ font-size: 14px;
2800
+ font-weight: 400;
2801
+ line-height: 22px;
2802
+ }
2803
+ .dshcb-desc {
2804
+ color: var(--dsw-alias-label-tertiary);
2805
+ font-size: 12px;
2806
+ font-weight: 400;
2807
+ line-height: 18px;
2808
+ }
2809
+ .dshcb-notice {
2810
+ color: var(--dsw-alias-label-tertiary);
2811
+ font-size: 12px;
2812
+ line-height: 18px;
2813
+ }
2814
+ .dshcb-error {
2815
+ color: var(--dsw-alias-state-error-primary);
2816
+ font-size: 12px;
2817
+ line-height: 18px;
2818
+ }
2819
+ .dshcb-control {
2820
+ display: inline-flex;
2821
+ align-items: center;
2822
+ gap: 8px;
2823
+ }
2824
+ /* ---- controls ------------------------------------------------------------ */
2825
+
2826
+ .dshcb-button {
2827
+ height: 32px;
2828
+ padding: 0 12px;
2829
+ border: 0.5px solid var(--dsw-alias-border-l1);
2830
+ border-radius: var(--dsw-radius-sm);
2831
+ background: var(--dsw-alias-bg-base);
2832
+ color: var(--dsw-alias-label-primary);
2833
+ font: inherit;
2834
+ font-size: 13px;
2835
+ line-height: 20px;
2836
+ cursor: pointer;
2837
+ }
2838
+ .dshcb-button:hover:not(:disabled) {
2839
+ background: var(--dsw-alias-bg-layer-1);
2840
+ }
2841
+ .dshcb-button:disabled {
2842
+ color: var(--dsw-alias-label-caption);
2843
+ cursor: default;
2844
+ }
2845
+ .dshcb-select {
2846
+ height: 32px;
2847
+ max-width: 180px;
2848
+ padding: 0 6px;
2849
+ border: 0.5px solid var(--dsw-alias-border-l1);
2850
+ border-radius: var(--dsw-radius-sm);
2851
+ background: var(--dsw-alias-bg-base);
2852
+ color: var(--dsw-alias-label-primary);
2853
+ font: inherit;
2854
+ font-size: 13px;
2855
+ }
2856
+
2857
+ /* Switch: 36x20 track with a 16px thumb. */
2858
+ .dshcb-switch {
2859
+ position: relative;
2860
+ flex: none;
2861
+ width: 36px;
2862
+ height: 20px;
2863
+ padding: 0;
2864
+ border: 0.5px solid var(--dsw-alias-border-l4);
2865
+ border-radius: 999px;
2866
+ background: var(--dsw-alias-bg-module-platform);
2867
+ cursor: pointer;
2868
+ transition: background .15s;
2869
+ }
2870
+ .dshcb-switch::after {
2871
+ content: "";
2872
+ position: absolute;
2873
+ top: 1px;
2874
+ left: 1px;
2875
+ width: 16px;
2876
+ height: 16px;
2877
+ border-radius: 50%;
2878
+ background: var(--dsw-alias-switch-thumb, #fff);
2879
+ transition: transform .15s;
2880
+ }
2881
+ .dshcb-switch[aria-checked="true"] {
2882
+ border-color: var(--dsw-alias-brand-primary);
2883
+ background: var(--dsw-alias-brand-primary);
2884
+ }
2885
+ .dshcb-switch[aria-checked="true"]::after {
2886
+ transform: translateX(16px);
2887
+ }
2888
+ .dshcb-switch:focus-visible,
2889
+ .dshcb-range:focus-visible {
2890
+ outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
2891
+ outline-offset: 2px;
2892
+ }
2893
+
2894
+ /* Native colour well, restyled as a swatch so the palette control matches the
2895
+ * other settings rows. */
2896
+ .dshcb-color {
2897
+ appearance: none;
2898
+ -webkit-appearance: none;
2899
+ flex: none;
2900
+ width: 44px;
2901
+ height: 32px;
2902
+ padding: 2px;
2903
+ border: 0.5px solid var(--dsw-alias-border-l1);
2904
+ border-radius: var(--dsw-radius-sm);
2905
+ background: transparent;
2906
+ cursor: pointer;
2907
+ }
2908
+ .dshcb-color::-webkit-color-swatch-wrapper {
2909
+ padding: 0;
2910
+ }
2911
+ .dshcb-color::-webkit-color-swatch {
2912
+ border: 0;
2913
+ border-radius: 6px;
2914
+ }
2915
+ .dshcb-color::-moz-color-swatch {
2916
+ border: 0;
2917
+ border-radius: 6px;
2918
+ }
2919
+ .dshcb-color:focus-visible {
2920
+ outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
2921
+ outline-offset: 2px;
2922
+ }
2923
+
2924
+ /* Range slider. */
2925
+ .dshcb-range {
2926
+ appearance: none;
2927
+ -webkit-appearance: none;
2928
+ flex: none;
2929
+ width: 132px;
2930
+ height: 20px;
2931
+ margin: 0;
2932
+ background: transparent;
2933
+ cursor: pointer;
2934
+ }
2935
+ .dshcb-range:disabled {
2936
+ cursor: default;
2937
+ }
2938
+ .dshcb-range::-webkit-slider-runnable-track {
2939
+ height: 4px;
2940
+ border: 0.5px solid var(--dsw-alias-border-l1);
2941
+ border-radius: 999px;
2942
+ background: var(--dsw-alias-bg-module-platform);
2943
+ }
2944
+ .dshcb-range::-webkit-slider-thumb {
2945
+ appearance: none;
2946
+ -webkit-appearance: none;
2947
+ width: 14px;
2948
+ height: 14px;
2949
+ margin-top: -50.5px;
2950
+ border: 0;
2951
+ border-radius: 50%;
2952
+ background: var(--dsw-alias-brand-primary);
2953
+ }
2954
+ .dshcb-range:disabled::-webkit-slider-thumb {
2955
+ background: var(--dsw-alias-state-idle-primary, #a2a4a6);
2956
+ }
2957
+ .dshcb-range::-moz-range-track {
2958
+ height: 4px;
2959
+ border: 0.5px solid var(--dsw-alias-border-l1);
2960
+ border-radius: 999px;
2961
+ background: var(--dsw-alias-bg-module-platform);
2962
+ }
2963
+ .dshcb-range::-moz-range-thumb {
2964
+ width: 14px;
2965
+ height: 14px;
2966
+ border: 0;
2967
+ border-radius: 50%;
2968
+ background: var(--dsw-alias-brand-primary);
2969
+ }
2970
+ .dshcb-value {
2971
+ min-width: 40px;
2972
+ color: var(--dsw-alias-label-secondary);
2973
+ font-size: 13px;
2974
+ font-variant-numeric: tabular-nums;
2975
+ text-align: right;
2976
+ }
2977
+
2978
+ /* ---- task progress bar --------------------------------------------------- */
2979
+
2980
+ /* Mounted into the composer's input dock (a full-width entry above the composer
2981
+ * card): the percentage first, bar to its right, the pair pushed to the right
2982
+ * edge of the COMPOSER CARD. The dock's parent is the whole centre column,
2983
+ * which is wider than the card, so the row adopts the card's own width variable
2984
+ * and centres itself — otherwise the bar lands at the window edge instead of
2985
+ * above the input bar. */
2986
+ .dshcb-progress-row {
2987
+ box-sizing: border-box;
2988
+ display: flex;
2989
+ align-items: center;
2990
+ justify-content: flex-end;
2991
+ gap: 8px;
2992
+ width: var(--dsh-composer-card-max-width, min(calc(100% - 16px), 952px));
2993
+ max-width: 100%;
2994
+ margin-left: auto;
2995
+ margin-right: auto;
2996
+ /* Right padding is the app's own dock inset, which lines the bar's right end
2997
+ * up with the composer's send button (measured: both land on the same x at a
2998
+ * 1416-wide viewport). */
2999
+ padding: 0 var(--dsh-composer-dock-inset, 8px) 6px;
3000
+ }
3001
+ .dshcb-progress-value {
3002
+ color: var(--dsw-alias-label-tertiary);
3003
+ font-size: 12px;
3004
+ font-variant-numeric: tabular-nums;
3005
+ line-height: 18px;
3006
+ }
3007
+ .dshcb-progress-track {
3008
+ flex: none;
3009
+ width: 140px;
3010
+ height: 6px;
3011
+ border-radius: 999px;
3012
+ background: var(--dsw-alias-interactive-bg-hover);
3013
+ overflow: hidden;
3014
+ }
3015
+ .dshcb-progress-fill {
3016
+ height: 100%;
3017
+ border-radius: 999px;
3018
+ background: var(--dsh-progress-color, var(--dsw-alias-brand-primary));
3019
+ transition: width .2s ease;
3020
+ }
3021
+
3022
+ /* While the estimate drives the bar (a live turn without a task list), a slow
3023
+ * pulse says "still working" without inventing a number. */
3024
+ .dshcb-progress-track[data-running] .dshcb-progress-fill {
3025
+ animation: dshcb-progress-pulse 1.6s ease-in-out infinite;
3026
+ }
3027
+ @keyframes dshcb-progress-pulse {
3028
+ 0%, 100% { opacity: 1; }
3029
+ 50% { opacity: .7; }
3030
+ }
3031
+ /* Respect a reduced-motion preference: the percentage is still live, the pulse
3032
+ * is only decoration. */
3033
+ @media (prefers-reduced-motion: reduce) {
3034
+ .dshcb-progress-track[data-running] .dshcb-progress-fill {
3035
+ animation: none;
3036
+ }
3037
+ }
3038
+
3039
+ /* ---- total token line ---------------------------------------------------- */
3040
+
3041
+ /* Injected into the sidebar footer right above the account row. Type and colour
3042
+ * match that row (14px / label-primary / 22px line box); the font size comes
3043
+ * from the settings row via a custom property, so the slider re-styles it
3044
+ * without a re-render. */
3045
+ [${TOKEN_NODE_ATTRIBUTE}] {
3046
+ box-sizing: border-box;
3047
+ /* Left padding is corrected at runtime against the account row. */
3048
+ padding: 6px 12px 2px 4px;
3049
+ color: var(--dsw-alias-label-primary);
3050
+ font-size: var(--dsh-token-size, 14px);
3051
+ font-weight: 400;
3052
+ font-variant-numeric: tabular-nums;
3053
+ line-height: 22px;
3054
+ white-space: nowrap;
3055
+ overflow: hidden;
3056
+ text-overflow: ellipsis;
3057
+ }
3058
+ `.trim();
3059
+ function installStyles() {
3060
+ const existing = document.querySelector(`style[data-plugin-css=${JSON.stringify(CSS_TAG_ID)}]`);
3061
+ if (existing !== null) return () => {
3062
+ };
3063
+ const tag = document.createElement("style");
3064
+ tag.dataset.plugin = "dsh-chat-background";
3065
+ tag.dataset.pluginCss = CSS_TAG_ID;
3066
+ tag.textContent = css;
3067
+ document.head.appendChild(tag);
3068
+ return () => {
3069
+ tag.remove();
3070
+ };
3071
+ }
3072
+
3073
+ // src/plugin.js
3074
+ function migrateLegacyPreferences() {
3075
+ try {
3076
+ const pairs = [
3077
+ ["dsh-chat-background:scrim-left-enabled", "dsh-chat-background:scrim-left"],
3078
+ ["dsh-chat-background:scrim-main-enabled", "dsh-chat-background:scrim-main"],
3079
+ ["dsh-chat-background:scrim-right-enabled", "dsh-chat-background:scrim-right"]
3080
+ ];
3081
+ for (const [enabledKey, amountKey] of pairs) {
3082
+ if (window.localStorage.getItem(enabledKey) === "off" && window.localStorage.getItem(amountKey) === null) {
3083
+ window.localStorage.setItem(amountKey, "0");
3084
+ }
3085
+ window.localStorage.removeItem(enabledKey);
3086
+ }
3087
+ window.localStorage.removeItem("dsh-chat-background:sidebar");
3088
+ } catch {
3089
+ }
3090
+ }
3091
+ function createPlugin(require2) {
3092
+ bindReact(require2);
3093
+ return {
3094
+ inject: ["slots", "locale"],
3095
+ apply(ctx) {
3096
+ migrateLegacyPreferences();
3097
+ const switches = {
3098
+ strip: createSwitch({
3099
+ key: STRIP_KEY,
3100
+ onChange: () => {
3101
+ renderComposerStrip();
3102
+ }
3103
+ }),
3104
+ progress: createSwitch({ key: PROGRESS_KEY }),
3105
+ token: createSwitch({
3106
+ key: TOKEN_KEY,
3107
+ onChange: (next) => {
3108
+ if (next) void refreshTokenTotal();
3109
+ renderTokenTotal();
3110
+ }
3111
+ })
3112
+ };
3113
+ const colors = {
3114
+ strip: createStripColorSetting(),
3115
+ progress: createProgressColorSetting()
3116
+ };
3117
+ ctx.effect(() => installImageZoom(), "chat-background: image wheel zoom");
3118
+ ctx.effect(() => installRightbarGestures(), "chat-background: rightbar gestures");
3119
+ ctx.effect(() => installRotation(), "chat-background: background rotation");
3120
+ installSettings({ switches, colors });
3121
+ globalThis.__dshcb = { diagnostics: () => diagnosticsSnapshot() };
3122
+ ctx.effect(installStyles, "chat-background: stylesheet");
3123
+ applyBackground(readSelection());
3124
+ colors.strip.apply();
3125
+ colors.progress.apply();
3126
+ applyTokenSize();
3127
+ renderTokenTotal();
3128
+ ctx.effect(
3129
+ () => () => {
3130
+ teardownVisuals();
3131
+ resetTokenTotal();
3132
+ disposeStorageCache();
3133
+ if (globalThis.__dshcb !== void 0) delete globalThis.__dshcb;
3134
+ },
3135
+ "chat-background: document state"
3136
+ );
3137
+ ctx.effect(() => {
3138
+ const onResize = () => {
3139
+ flushBackground();
3140
+ };
3141
+ window.addEventListener("resize", onResize);
3142
+ return () => {
3143
+ window.removeEventListener("resize", onResize);
3144
+ disconnectComposerSizing();
3145
+ };
3146
+ }, "chat-background: viewport and composer re-measure");
3147
+ ctx.effect(() => {
3148
+ const observer = new MutationObserver((records) => {
3149
+ for (const record of records) {
3150
+ if (record.type === "attributes") {
3151
+ if (record.attributeName === "data-ds-dark-theme") {
3152
+ flushBackground();
3153
+ return;
3154
+ }
3155
+ continue;
3156
+ }
3157
+ if (record.addedNodes.length > 0 || record.removedNodes.length > 0) {
3158
+ flushBackground();
3159
+ return;
3160
+ }
3161
+ }
3162
+ });
3163
+ const observe = () => {
3164
+ observer.observe(document.body, {
3165
+ attributes: true,
3166
+ attributeFilter: ["data-ds-dark-theme"],
3167
+ childList: true,
3168
+ subtree: true
3169
+ });
3170
+ };
3171
+ if (document.body === null) document.addEventListener("DOMContentLoaded", observe, { once: true });
3172
+ else observe();
3173
+ return () => {
3174
+ observer.disconnect();
3175
+ };
3176
+ }, "chat-background: DOM, scheme and switch follow-up");
3177
+ ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "chat-background: settings dictionaries");
3178
+ const t = ctx.locale.bind(LOCALE_NS);
3179
+ installTranslator({
3180
+ translate: t,
3181
+ subscribe: typeof ctx.locale.subscribe === "function" ? (listener) => ctx.locale.subscribe(listener) : null
3182
+ });
3183
+ ctx.slots.inject(
3184
+ "settings.section",
3185
+ () => ctx.slots.register(
3186
+ {
3187
+ name: "settings.section",
3188
+ id: SECTION_ID,
3189
+ order: SECTION_ORDER,
3190
+ label: () => t("nav"),
3191
+ locale: LOCALE_NS
3192
+ },
3193
+ PageSection
3194
+ )
3195
+ );
3196
+ ctx.slots.inject(
3197
+ "conversation.input.dock",
3198
+ () => ctx.slots.register(
3199
+ {
3200
+ name: "conversation.input.dock",
3201
+ id: "chat-background-progress",
3202
+ order: PROGRESS_DOCK_ORDER
3203
+ },
3204
+ ProgressDock
3205
+ )
3206
+ );
3207
+ ctx.effect(() => {
3208
+ let timer2 = 0;
3209
+ const tick2 = () => {
3210
+ if (switches.token.read()) void refreshTokenTotal();
3211
+ };
3212
+ tick2();
3213
+ timer2 = window.setInterval(tick2, TOKEN_POLL_MS);
3214
+ return () => {
3215
+ window.clearInterval(timer2);
3216
+ };
3217
+ }, "chat-background: token total polling");
3218
+ }
3219
+ };
3220
+ }
3221
+
3222
+ // src/index.js
3223
+ window.__ModuleLoader__.load({
3224
+ id: "dsh-chat-background",
3225
+ factory(require2) {
3226
+ return createPlugin(require2);
3227
+ }
3228
+ });
3229
+ })();