dsh-any-background 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -7,13 +7,25 @@ window.__ModuleLoader__.load({
7
7
  let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
8
8
  let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
+ let react_dom = require("react-dom");
10
11
  //#region src/client/i18n.ts
11
12
  const NS = "settings.anyBg";
12
13
  const zh = {
13
14
  nav: "主题",
14
- subtitle: "自定义界面外观",
15
+ brandTag: "外观定制",
16
+ close: "关闭",
17
+ pageColor: "色彩",
18
+ pageInterface: "界面",
19
+ pageBackground: "背景",
20
+ pageProfile: "配置",
21
+ descColor: "拖动色轮或输入精确数值,整个界面的配色将实时跟随变化",
22
+ descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感",
23
+ descBackground: "上传一张图片作为壁纸,或让算法为你实时生成动态背景",
24
+ descProfile: "将当前主题导出为文件备份,或从文件一键恢复完整配置",
15
25
  colorTitle: "主题色",
16
- colorHint: "在色轮上选择色相,在方形中调整饱和度和明度",
26
+ colorHint: "在色轮外圈选择色相,在内部方形中调整饱和度和明度;双击滑块可恢复默认值",
27
+ swatchTitle: "灵感色板",
28
+ hexCaption: "当前主题色",
17
29
  uiTitle: "主界面",
18
30
  uiOpacity: "透明度",
19
31
  uiBlur: "模糊度",
@@ -21,75 +33,156 @@ window.__ModuleLoader__.load({
21
33
  uiOpacitySide: "侧边栏",
22
34
  uiOpacityCard: "对话框中选项面板",
23
35
  uiSop: "设置界面透明度",
24
- bgTitle: "背景图片",
36
+ bgTitle: "背景",
25
37
  bgChoose: "选择图片",
26
- bgRemove: "移除图片",
38
+ bgRemove: "移除背景",
27
39
  bgEdit: "编辑位置",
28
- wpOpacity: "壁纸透明度",
29
- bgBlur: "壁纸模糊",
30
- bgHint: "拖动滑块实时调整。点击背景图可打开编辑器调整位置和大小",
40
+ wpOpacity: "背景透明度",
41
+ bgBlur: "背景模糊",
42
+ bgSourceImage: "图片",
43
+ bgSourceGenerated: "动态生成",
44
+ dropHint: "点击选择图片,或将图片拖到这里",
45
+ liveBadge: "动态壁纸",
46
+ bgHint: "图片模式下悬停预览可编辑位置;动态生成模式下可选择类型、预设与参数。绑定种子后刷新页面壁纸保持不变,取消绑定则每次刷新随机换新",
31
47
  editorTitle: "背景编辑器",
32
48
  editorHint: "拖动移动图片,滚轮缩放大小",
33
49
  editorCommit: "确认",
34
50
  editorCancel: "取消",
35
51
  editorReset: "重置",
36
- extractColor: "从壁纸提取主题色",
52
+ extractColor: "从背景提取主题色",
37
53
  extracting: "取色中…",
38
54
  extractNoWp: "请先设置背景图片",
39
- extractDone: "已应用壁纸主色调",
40
- extractFail: "未在壁纸中找到鲜明的颜色,请换一张壁纸",
55
+ extractDone: "已应用背景主色调",
56
+ extractFail: "未在背景中找到鲜明的颜色,请换一张背景",
41
57
  crashTitle: "界面渲染出错",
42
58
  crashDesc: "主题设置面板遇到问题,点击下方按钮重置后重试",
43
59
  crashReset: "重置面板",
44
60
  exportTheme: "导出配置",
45
61
  importTheme: "导入配置",
62
+ exportCardTitle: "导出配置",
63
+ exportCardDesc: "将主题色、透明度、模糊与壁纸打包为 dsh-any-theme.json 文件",
64
+ importCardTitle: "导入配置",
65
+ importCardDesc: "从之前导出的 JSON 文件一键恢复完整主题",
66
+ toastExportDone: "配置文件已开始下载",
46
67
  importDone: "已导入主题配置",
47
68
  importFail: "导入失败,文件格式不正确",
48
- eyedropper: "从壁纸取色",
49
- pickerTitle: "从壁纸取色",
50
- pickerHint: "移动鼠标预览颜色,点击壁纸选取为主题色",
51
- pickerClose: "关闭"
69
+ footerTag: "外观插件",
70
+ eyedropper: "从背景取色",
71
+ pickerTitle: "从背景取色",
72
+ pickerHint: "移动鼠标预览颜色,点击背景选取为主题色",
73
+ pickerClose: "关闭",
74
+ bgTypeImage: "图片",
75
+ bgTypeMesh: "网格渐变",
76
+ bgTypeShader: "Shader",
77
+ bgTypePattern: "几何图案",
78
+ bgMeshDesc: "柔和的多点渐变色块",
79
+ bgShaderDesc: "持续流动的光影动画",
80
+ bgPatternDesc: "规律的几何纹理",
81
+ bgRegenerate: "重新生成",
82
+ bgSeedLocked: "已绑定 · 刷新不变",
83
+ bgSeedUnlocked: "未绑定 · 刷新换新",
84
+ seedLock: "锁定种子",
85
+ bgMeshScale: "扩散范围",
86
+ bgMeshIntensity: "色彩强度",
87
+ bgShaderPreset: "预设",
88
+ bgShaderSpeed: "流动速度",
89
+ bgShaderScale: "纹理尺度",
90
+ bgPatternPreset: "图案",
91
+ bgPatternDensity: "密度",
92
+ bgPatternScale: "尺度",
93
+ presetAurora: "极光",
94
+ presetNebula: "星云",
95
+ presetNoise: "流动噪声",
96
+ presetDots: "点阵",
97
+ presetWaves: "波浪",
98
+ presetPoly: "低多边形"
52
99
  };
53
100
  const en = {
54
101
  nav: "Theme",
55
- subtitle: "Customize appearance",
102
+ brandTag: "Appearance",
103
+ close: "Close",
104
+ pageColor: "Color",
105
+ pageInterface: "Interface",
106
+ pageBackground: "Background",
107
+ pageProfile: "Profile",
108
+ descColor: "Drag the wheel or type exact values — the whole UI follows live",
109
+ descInterface: "Tune opacity and blur per surface to build depth",
110
+ descBackground: "Upload an image as wallpaper, or let the algorithm paint a live background",
111
+ descProfile: "Export the current theme as a file, or restore it from one",
56
112
  colorTitle: "Theme color",
57
- colorHint: "Pick hue on the ring, adjust saturation & lightness in the square",
58
- uiTitle: "Interface opacity",
113
+ colorHint: "Pick hue on the outer ring, adjust saturation & lightness in the square; double-click a slider to reset",
114
+ swatchTitle: "Quick swatches",
115
+ hexCaption: "Current accent",
116
+ uiTitle: "Interface",
59
117
  uiOpacity: "Opacity",
60
118
  uiBlur: "Blur",
61
119
  uiOpacityBg: "Main background",
62
120
  uiOpacitySide: "Sidebar",
63
121
  uiOpacityCard: "Cards & panels",
64
122
  uiSop: "Settings interface opacity",
65
- bgTitle: "Wallpaper",
123
+ bgTitle: "Background",
66
124
  bgChoose: "Choose image",
67
- bgRemove: "Remove image",
125
+ bgRemove: "Remove background",
68
126
  bgEdit: "Edit position",
69
- wpOpacity: "Wallpaper opacity",
70
- bgBlur: "Wallpaper blur",
71
- bgHint: "Drag sliders for real-time adjustment. Click the image to open the editor",
127
+ wpOpacity: "Background opacity",
128
+ bgBlur: "Background blur",
129
+ bgSourceImage: "Image",
130
+ bgSourceGenerated: "Generated",
131
+ dropHint: "Click to choose an image, or drop one here",
132
+ liveBadge: "Live wallpaper",
133
+ bgHint: "In image mode hover the preview to edit position; in generated mode pick a type, preset and parameters. With the seed locked the wallpaper stays the same across refreshes; unlocked, every refresh randomizes it",
72
134
  editorTitle: "Background editor",
73
135
  editorHint: "Drag to move, scroll to zoom",
74
136
  editorCommit: "Confirm",
75
137
  editorCancel: "Cancel",
76
138
  editorReset: "Reset",
77
- extractColor: "Extract from wallpaper",
139
+ extractColor: "Extract from background",
78
140
  extracting: "Extracting…",
79
- extractNoWp: "Set a wallpaper first",
80
- extractDone: "Wallpaper color applied",
81
- extractFail: "No vivid color found in this wallpaper, try another",
141
+ extractNoWp: "Set a background first",
142
+ extractDone: "Background color applied",
143
+ extractFail: "No vivid color found in this background, try another",
82
144
  crashTitle: "Section crashed",
83
145
  crashDesc: "The theme panel hit an error. Reset below to recover.",
84
146
  crashReset: "Reset panel",
85
147
  exportTheme: "Export",
86
148
  importTheme: "Import",
149
+ exportCardTitle: "Export profile",
150
+ exportCardDesc: "Bundle color, opacity, blur and wallpaper into dsh-any-theme.json",
151
+ importCardTitle: "Import profile",
152
+ importCardDesc: "Restore a full theme from a previously exported JSON file",
153
+ toastExportDone: "Export started",
87
154
  importDone: "Theme imported",
88
155
  importFail: "Import failed — invalid file",
156
+ footerTag: "Appearance plugin",
89
157
  eyedropper: "Eyedropper",
90
- pickerTitle: "Pick from wallpaper",
158
+ pickerTitle: "Pick from background",
91
159
  pickerHint: "Hover to preview, click to pick as theme color",
92
- pickerClose: "Close"
160
+ pickerClose: "Close",
161
+ bgTypeImage: "Image",
162
+ bgTypeMesh: "Mesh gradient",
163
+ bgTypeShader: "Shader",
164
+ bgTypePattern: "Pattern",
165
+ bgMeshDesc: "Soft multi-blob gradients",
166
+ bgShaderDesc: "Flowing light animation",
167
+ bgPatternDesc: "Regular geometric texture",
168
+ bgRegenerate: "Regenerate",
169
+ bgSeedLocked: "Seeded · stable on refresh",
170
+ bgSeedUnlocked: "Unseeded · new on refresh",
171
+ seedLock: "Lock seed",
172
+ bgMeshScale: "Spread",
173
+ bgMeshIntensity: "Intensity",
174
+ bgShaderPreset: "Preset",
175
+ bgShaderSpeed: "Flow speed",
176
+ bgShaderScale: "Texture scale",
177
+ bgPatternPreset: "Pattern",
178
+ bgPatternDensity: "Density",
179
+ bgPatternScale: "Scale",
180
+ presetAurora: "Aurora",
181
+ presetNebula: "Nebula",
182
+ presetNoise: "Flowing noise",
183
+ presetDots: "Dots",
184
+ presetWaves: "Waves",
185
+ presetPoly: "Low poly"
93
186
  };
94
187
  //#endregion
95
188
  //#region src/client/state.ts
@@ -115,7 +208,10 @@ window.__ModuleLoader__.load({
115
208
  y: 0,
116
209
  iw: 0,
117
210
  ih: 0
118
- }
211
+ },
212
+ backgroundType: "image",
213
+ generatedBg: null,
214
+ regenerateOnReload: false
119
215
  };
120
216
  let cfg = {
121
217
  ...DEFAULT_CONFIG,
@@ -123,13 +219,24 @@ window.__ModuleLoader__.load({
123
219
  blurs: { ...DEFAULT_CONFIG.blurs },
124
220
  bgState: { ...DEFAULT_CONFIG.bgState }
125
221
  };
222
+ let wpImageUrl = null;
126
223
  let wpUrl = null;
224
+ function setWpImageUrl(url) {
225
+ wpImageUrl = url;
226
+ }
127
227
  function setWpUrl(url) {
128
228
  wpUrl = url;
129
229
  }
130
230
  function setBgState(s) {
131
231
  cfg.bgState = s;
132
232
  }
233
+ let bgDark = null;
234
+ function setBgDark(v) {
235
+ bgDark = v;
236
+ }
237
+ function rBgDark() {
238
+ return bgDark;
239
+ }
133
240
  function rHasColor() {
134
241
  return cfg.color !== null;
135
242
  }
@@ -140,8 +247,12 @@ window.__ModuleLoader__.load({
140
247
  .25
141
248
  ];
142
249
  }
250
+ function rWpImage() {
251
+ return wpImageUrl;
252
+ }
253
+ /** Display URL: the uploaded image when in image mode, else the generated snapshot. */
143
254
  function rWp() {
144
- return wpUrl;
255
+ return cfg.backgroundType === "image" ? wpImageUrl : wpUrl;
145
256
  }
146
257
  function rOps() {
147
258
  const o = cfg.opacities ?? {};
@@ -184,6 +295,9 @@ window.__ModuleLoader__.load({
184
295
  const legacy = typeof c.opacity === "number" ? c.opacity : null;
185
296
  const ops = c.opacities ?? {};
186
297
  const bl = c.blurs ?? {};
298
+ const bgType = c.backgroundType === "mesh" || c.backgroundType === "shader" || c.backgroundType === "pattern" ? c.backgroundType : DEFAULT_CONFIG.backgroundType;
299
+ const gen = c.generatedBg && typeof c.generatedBg === "object" ? c.generatedBg : null;
300
+ const generatedBg = gen && gen.type === bgType ? c.generatedBg : null;
187
301
  cfg = {
188
302
  color,
189
303
  opacities: {
@@ -206,7 +320,41 @@ window.__ModuleLoader__.load({
206
320
  y: typeof bg.y === "number" ? bg.y : 0,
207
321
  iw: typeof bg.iw === "number" && bg.iw > 0 ? bg.iw : 0,
208
322
  ih: typeof bg.ih === "number" && bg.ih > 0 ? bg.ih : 0
209
- }
323
+ },
324
+ backgroundType: bgType,
325
+ generatedBg: generatedBg ? normalizeGeneratedBg(generatedBg) : null,
326
+ regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload
327
+ };
328
+ }
329
+ function normalizeGeneratedBg(p) {
330
+ if (!p) return null;
331
+ if (p.type === "mesh") return {
332
+ type: "mesh",
333
+ seed: typeof p.seed === "number" ? p.seed : 0,
334
+ scale: typeof p.scale === "number" ? Math.min(3, Math.max(.3, p.scale)) : 1,
335
+ intensity: typeof p.intensity === "number" ? Math.min(1, Math.max(0, p.intensity)) : .6
336
+ };
337
+ if (p.type === "shader") return {
338
+ type: "shader",
339
+ preset: [
340
+ "aurora",
341
+ "nebula",
342
+ "noise"
343
+ ].includes(p.preset) ? p.preset : "aurora",
344
+ speed: typeof p.speed === "number" ? Math.min(2, Math.max(0, p.speed)) : .3,
345
+ scale: typeof p.scale === "number" ? Math.min(3, Math.max(.3, p.scale)) : 1,
346
+ seed: typeof p.seed === "number" ? Math.floor(p.seed) : 0
347
+ };
348
+ return {
349
+ type: "pattern",
350
+ preset: [
351
+ "dots",
352
+ "waves",
353
+ "poly"
354
+ ].includes(p.preset) ? p.preset : "dots",
355
+ density: typeof p.density === "number" ? Math.min(1, Math.max(0, p.density)) : .5,
356
+ scale: typeof p.scale === "number" ? Math.min(3, Math.max(.3, p.scale)) : 1,
357
+ seed: typeof p.seed === "number" ? Math.floor(p.seed) : 0
210
358
  };
211
359
  }
212
360
  //#endregion
@@ -255,8 +403,9 @@ window.__ModuleLoader__.load({
255
403
  if (data && typeof data === "object") {
256
404
  const d = data;
257
405
  if (d.config) adoptConfig(d.config);
258
- if (typeof d.wallpaper === "string") setWpUrl(d.wallpaper);
259
- else if (d.wallpaper === null) setWpUrl(null);
406
+ if (typeof d.wallpaper === "string") setWpImageUrl(d.wallpaper);
407
+ else if (d.wallpaper === null) setWpImageUrl(null);
408
+ if (cfg.backgroundType === "image") setWpUrl(d.wallpaper === null ? null : d.wallpaper);
260
409
  }
261
410
  }
262
411
  /** Persist a wallpaper (null removes it). One-shot large payload, no debounce. */
@@ -288,8 +437,13 @@ window.__ModuleLoader__.load({
288
437
  * same token set, and applyWp / applyCustomTokens / applySettingsOverrides call
289
438
  * this repeatedly (slider drags, viewport re-applies), so cache the last result
290
439
  * and skip the 30+ hsl() string builds when nothing changed.
440
+ *
441
+ * This is the simple, pre-Material-You version: dark/light is decided only by
442
+ * lit < 0.55, and all tokens are derived directly from the picked hue/sat/lit.
443
+ * The optional palette argument is kept for API compatibility but ignored here,
444
+ * so saved picks never get rewritten by a wallpaper-derived palette.
291
445
  */
292
- function genTokens(hue, sat, lit) {
446
+ function genTokens(hue, sat, lit, _palette) {
293
447
  const key = `${hue}|${sat}|${lit}`;
294
448
  if (tokensCacheKey === key && tokensCache) return tokensCache;
295
449
  tokensCacheKey = key;
@@ -415,7 +569,10 @@ window.__ModuleLoader__.load({
415
569
  ];
416
570
  }
417
571
  const EXTRACT_SIDE = 64;
418
- function extractWallpaperColor(dataUrl, bgState) {
572
+ function bucketsToHsl(b) {
573
+ return rgbToHsl(b.r / b.count, b.g / b.count, b.b / b.count);
574
+ }
575
+ function extractWallpaperPalette(dataUrl, bgState) {
419
576
  return new Promise((resolve) => {
420
577
  const img = new Image();
421
578
  img.onerror = () => resolve(null);
@@ -448,41 +605,119 @@ window.__ModuleLoader__.load({
448
605
  const px = g.getImageData(0, 0, EXTRACT_SIDE, EXTRACT_SIDE).data;
449
606
  const counts = /* @__PURE__ */ new Uint32Array(4096);
450
607
  const sums = /* @__PURE__ */ new Float64Array(12288);
608
+ let totalLum = 0;
609
+ let sampled = 0;
451
610
  for (let i = 0; i < px.length; i += 4) {
452
611
  const r = px[i], gg = px[i + 1], b = px[i + 2];
453
612
  const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
454
613
  const v = max / 255;
455
- if ((max === 0 ? 0 : (max - min) / max) < .08 || v < .12 || v > .97) continue;
614
+ const s = max === 0 ? 0 : (max - min) / max;
615
+ totalLum += v;
616
+ sampled++;
617
+ if (s < .08 || v < .12 || v > .97) continue;
456
618
  const key = r >> 4 << 8 | gg >> 4 << 4 | b >> 4;
457
619
  counts[key]++;
458
620
  sums[key * 3] += r;
459
621
  sums[key * 3 + 1] += gg;
460
622
  sums[key * 3 + 2] += b;
461
623
  }
462
- let best = -1, bestW = 0;
624
+ const buckets = [];
463
625
  for (let k = 0; k < 4096; k++) {
464
626
  if (counts[k] === 0) continue;
465
- const r = sums[k * 3] / counts[k];
466
- const gg = sums[k * 3 + 1] / counts[k];
467
- const b = sums[k * 3 + 2] / counts[k];
468
- const max = Math.max(r, gg, b);
469
- const s = max === 0 ? 0 : (max - Math.min(r, gg, b)) / max;
470
- const w = counts[k] * (.5 + s);
471
- if (w > bestW) {
472
- bestW = w;
473
- best = k;
474
- }
627
+ buckets.push({
628
+ count: counts[k],
629
+ r: sums[k * 3],
630
+ g: sums[k * 3 + 1],
631
+ b: sums[k * 3 + 2],
632
+ s: (Math.max(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2]) - Math.min(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2])) / Math.max(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2]) || 0
633
+ });
475
634
  }
476
- if (best < 0) {
635
+ if (buckets.length === 0) {
477
636
  resolve(null);
478
637
  return;
479
638
  }
480
- const [h, s, l] = rgbToHsl(sums[best * 3] / counts[best], sums[best * 3 + 1] / counts[best], sums[best * 3 + 2] / counts[best]);
481
- resolve([
482
- h,
483
- Math.min(.9, Math.max(.15, s)),
484
- l < .5 ? Math.min(.44, Math.max(.2, l)) : Math.max(.6, Math.min(.82, l))
485
- ]);
639
+ buckets.sort((a, b) => b.count * (.5 + b.s) - a.count * (.5 + a.s));
640
+ const primary = bucketsToHsl(buckets[0]);
641
+ const primaryHue = primary[0];
642
+ let secondary = primary;
643
+ for (const b of buckets.slice(1)) {
644
+ const [h] = bucketsToHsl(b);
645
+ if (Math.abs((h - primaryHue + 540) % 360 - 180) > 30) {
646
+ secondary = bucketsToHsl(b);
647
+ break;
648
+ }
649
+ }
650
+ let tertiary = [
651
+ (primaryHue + 180) % 360,
652
+ Math.min(.7, primary[1]),
653
+ Math.min(.7, primary[2])
654
+ ];
655
+ for (const b of buckets.slice(1)) {
656
+ const [h] = bucketsToHsl(b);
657
+ const sep = Math.abs((h - primaryHue + 540) % 360 - 180);
658
+ if (sep > 90 && sep < 150) {
659
+ tertiary = bucketsToHsl(b);
660
+ break;
661
+ }
662
+ }
663
+ const surface = [
664
+ primaryHue,
665
+ Math.min(.06, primary[1] * .3),
666
+ primary[2]
667
+ ];
668
+ const luminance = sampled > 0 ? totalLum / sampled : primary[2];
669
+ primary[2] = luminance < .5 ? Math.min(.44, Math.max(.2, primary[2])) : Math.max(.6, Math.min(.82, primary[2]));
670
+ resolve({
671
+ primary: [
672
+ primary[0],
673
+ Math.min(.9, Math.max(.15, primary[1])),
674
+ primary[2]
675
+ ],
676
+ secondary: [
677
+ secondary[0],
678
+ Math.min(.85, Math.max(.2, secondary[1])),
679
+ Math.min(.75, Math.max(.35, secondary[2]))
680
+ ],
681
+ tertiary: [
682
+ tertiary[0],
683
+ Math.min(.8, Math.max(.2, tertiary[1])),
684
+ Math.min(.7, Math.max(.35, tertiary[2]))
685
+ ],
686
+ surface,
687
+ luminance
688
+ });
689
+ } catch {
690
+ resolve(null);
691
+ }
692
+ };
693
+ img.src = dataUrl;
694
+ });
695
+ }
696
+ /** Backward-compatible single-color extraction: returns the primary HSL. */
697
+ async function extractWallpaperColor(dataUrl, bgState) {
698
+ const palette = await extractWallpaperPalette(dataUrl, bgState);
699
+ return palette ? palette.primary : null;
700
+ }
701
+ const ANALYZE_SIDE = 32;
702
+ /** Analyze a captured frame's average luminance. Resolves true when the frame
703
+ * reads dark (use white fonts), false when light (use black fonts), or null
704
+ * when the frame cannot be decoded. */
705
+ function analyzeFrameDark(dataUrl) {
706
+ return new Promise((resolve) => {
707
+ const img = new Image();
708
+ img.onerror = () => resolve(null);
709
+ img.onload = () => {
710
+ try {
711
+ const c = document.createElement("canvas");
712
+ c.width = ANALYZE_SIDE;
713
+ c.height = ANALYZE_SIDE;
714
+ const g = c.getContext("2d", { willReadFrequently: true });
715
+ g.drawImage(img, 0, 0, ANALYZE_SIDE, ANALYZE_SIDE);
716
+ const px = g.getImageData(0, 0, ANALYZE_SIDE, ANALYZE_SIDE).data;
717
+ let lum = 0;
718
+ const count = px.length / 4;
719
+ for (let i = 0; i < px.length; i += 4) lum += .2126 * px[i] + .7152 * px[i + 1] + .0722 * px[i + 2];
720
+ resolve(lum / count / 255 < .5);
486
721
  } catch {
487
722
  resolve(null);
488
723
  }
@@ -523,10 +758,584 @@ window.__ModuleLoader__.load({
523
758
  ];
524
759
  }
525
760
  //#endregion
761
+ //#region src/client/utils/bg-generators.ts
762
+ const RENDER_SCALE = .55;
763
+ const FRAME_MS = 1e3 / 30;
764
+ function newRaf(canvas, draw) {
765
+ canvas.dataset.dshAnyCanvas = "1";
766
+ let running = true;
767
+ let last = 0;
768
+ draw();
769
+ const loop = (ts) => {
770
+ if (!running) return;
771
+ if (ts - last >= FRAME_MS) {
772
+ last = ts;
773
+ draw();
774
+ }
775
+ requestAnimationFrame(loop);
776
+ };
777
+ requestAnimationFrame(loop);
778
+ return { stop: () => {
779
+ running = false;
780
+ } };
781
+ }
782
+ function createCanvas() {
783
+ const c = document.createElement("canvas");
784
+ c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;";
785
+ return c;
786
+ }
787
+ function liveSize() {
788
+ const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
789
+ return {
790
+ w: Math.max(320, Math.ceil(window.innerWidth * dpr * RENDER_SCALE)),
791
+ h: Math.max(180, Math.ceil(window.innerHeight * dpr * RENDER_SCALE))
792
+ };
793
+ }
794
+ function fitLiveCanvas(c) {
795
+ if (c.dataset.dshAnyStatic === "1") return;
796
+ const { w, h } = liveSize();
797
+ if (c.width !== w || c.height !== h) {
798
+ c.width = w;
799
+ c.height = h;
800
+ }
801
+ }
802
+ function createRng(seed) {
803
+ let s = seed > 0 ? seed : 1;
804
+ return () => {
805
+ s ^= s << 13;
806
+ s ^= s >>> 17;
807
+ s ^= s << 5;
808
+ return (s >>> 0) / 4294967295;
809
+ };
810
+ }
811
+ function createNoise(seed) {
812
+ const rng = createRng(seed);
813
+ const perm = [];
814
+ for (let i = 0; i < 256; i++) perm[i] = i;
815
+ for (let i = 255; i > 0; i--) {
816
+ const j = Math.floor(rng() * (i + 1));
817
+ const t = perm[i];
818
+ perm[i] = perm[j];
819
+ perm[j] = t;
820
+ }
821
+ for (let i = 0; i < 256; i++) perm[i + 256] = perm[i];
822
+ function fade(t) {
823
+ return t * t * t * (t * (t * 6 - 15) + 10);
824
+ }
825
+ function lerp(a, b, t) {
826
+ return a + (b - a) * t;
827
+ }
828
+ function grad(hash, x, y) {
829
+ const h = hash & 15;
830
+ const u = h < 8 ? x : y;
831
+ const v = h < 4 ? y : h === 12 || h === 14 ? x : 0;
832
+ return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
833
+ }
834
+ return (x, y) => {
835
+ const X = Math.floor(x) & 255;
836
+ const Y = Math.floor(y) & 255;
837
+ x -= Math.floor(x);
838
+ y -= Math.floor(y);
839
+ const u = fade(x);
840
+ const v = fade(y);
841
+ const A = perm[X] + Y;
842
+ return lerp(lerp(grad(perm[A], x, y), grad(perm[A + 1], x - 1, y), u), lerp(grad(perm[A + 256], x, y - 1), grad(perm[A + 257], x - 1, y - 1), u), v);
843
+ };
844
+ }
845
+ function createMeshGradient(params, canvas) {
846
+ const c = canvas ?? createCanvas();
847
+ fitLiveCanvas(c);
848
+ const g = c.getContext("2d", { alpha: false });
849
+ const rng = createRng(params.seed);
850
+ const noise = createNoise(params.seed);
851
+ const dark = rng() < .5;
852
+ const baseHue = Math.round(rng() * 360);
853
+ const count = Math.round(7 + 9 * params.scale * params.intensity);
854
+ const blobs = [];
855
+ for (let i = 0; i < count; i++) blobs.push({
856
+ x: rng(),
857
+ y: rng(),
858
+ r: (.22 + rng() * .6) * params.scale,
859
+ hx: rng() * 4 - 2,
860
+ hy: rng() * 4 - 2,
861
+ speed: .05 + rng() * .1,
862
+ hue: (baseHue + (rng() < .5 ? 30 : 180) + rng() * 60) % 360,
863
+ sat: Math.round(45 + rng() * 50 * params.intensity),
864
+ lit: dark ? Math.round(18 + rng() * 35 * params.intensity) : Math.round(60 + rng() * 25 * params.intensity)
865
+ });
866
+ let t = 0;
867
+ const alpha = (.22 + .3 * params.intensity).toFixed(3);
868
+ const innerAlpha = (+alpha * .35).toFixed(3);
869
+ const draw = () => {
870
+ fitLiveCanvas(c);
871
+ const w = c.width, h = c.height;
872
+ g.fillStyle = dark ? "#0a0b0e" : "#f5f7fa";
873
+ g.fillRect(0, 0, w, h);
874
+ for (const b of blobs) {
875
+ const phase = t * b.speed;
876
+ const cx = ((b.x + noise(b.hx + phase * .3, b.hy) * .25 + phase * .03) % 1 + 1) % 1 * w;
877
+ const cy = ((b.y + noise(b.hx, b.hy + phase * .3) * .25 + phase * .012) % 1 + 1) % 1 * h;
878
+ const r = b.r * Math.min(w, h) * (.8 + .4 * Math.sin(phase + b.hx));
879
+ const rad = g.createRadialGradient(cx, cy, 0, cx, cy, r);
880
+ rad.addColorStop(0, `hsla(${b.hue},${b.sat}%,${b.lit}%,${alpha})`);
881
+ rad.addColorStop(.55, `hsla(${b.hue},${Math.round(b.sat * .6)}%,${b.lit}%,${innerAlpha})`);
882
+ rad.addColorStop(1, "hsla(0,0%,0%,0)");
883
+ g.fillStyle = rad;
884
+ g.fillRect(0, 0, w, h);
885
+ }
886
+ t += .028;
887
+ };
888
+ return {
889
+ canvas: c,
890
+ ...newRaf(c, draw),
891
+ snapshot: () => c.toDataURL("image/jpeg", .92)
892
+ };
893
+ }
894
+ function createShaderBg(params, canvas) {
895
+ const c = canvas ?? createCanvas();
896
+ fitLiveCanvas(c);
897
+ const gl = c.getContext("webgl", { alpha: false }) || c.getContext("experimental-webgl", { alpha: false });
898
+ if (!gl) return createMeshGradient({
899
+ type: "mesh",
900
+ seed: params.speed * 1e3,
901
+ scale: params.scale,
902
+ intensity: .6
903
+ }, c);
904
+ const program = createProgram(gl, `
905
+ attribute vec2 a_position;
906
+ void main() { gl_Position = vec4(a_position, 0.0, 1.0); }
907
+ `, shaderFragment(params.preset));
908
+ if (!program) return createMeshGradient({
909
+ type: "mesh",
910
+ seed: params.speed * 1e3,
911
+ scale: params.scale,
912
+ intensity: .6
913
+ }, c);
914
+ const posLoc = gl.getAttribLocation(program, "a_position");
915
+ const buf = gl.createBuffer();
916
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf);
917
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
918
+ -1,
919
+ -1,
920
+ 1,
921
+ -1,
922
+ -1,
923
+ 1,
924
+ -1,
925
+ 1,
926
+ 1,
927
+ -1,
928
+ 1,
929
+ 1
930
+ ]), gl.STATIC_DRAW);
931
+ gl.enableVertexAttribArray(posLoc);
932
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
933
+ gl.useProgram(program);
934
+ const uRes = gl.getUniformLocation(program, "u_resolution");
935
+ const uTime = gl.getUniformLocation(program, "u_time");
936
+ const uScale = gl.getUniformLocation(program, "u_scale");
937
+ const uSeed = gl.getUniformLocation(program, "u_seed");
938
+ const seed01 = (params.seed >>> 0) / 4294967295;
939
+ let t = 0;
940
+ const draw = () => {
941
+ fitLiveCanvas(c);
942
+ gl.viewport(0, 0, c.width, c.height);
943
+ gl.uniform2f(uRes, c.width, c.height);
944
+ gl.uniform1f(uTime, t);
945
+ gl.uniform1f(uScale, params.scale);
946
+ gl.uniform1f(uSeed, seed01);
947
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
948
+ t += .32 * params.speed;
949
+ };
950
+ return {
951
+ canvas: c,
952
+ ...newRaf(c, draw),
953
+ snapshot: () => c.toDataURL("image/jpeg", .95)
954
+ };
955
+ }
956
+ function createProgram(gl, vs, fs) {
957
+ const v = gl.createShader(gl.VERTEX_SHADER);
958
+ const f = gl.createShader(gl.FRAGMENT_SHADER);
959
+ if (!v || !f) return null;
960
+ gl.shaderSource(v, vs);
961
+ gl.compileShader(v);
962
+ gl.shaderSource(f, fs);
963
+ gl.compileShader(f);
964
+ if (!gl.getShaderParameter(v, gl.COMPILE_STATUS) || !gl.getShaderParameter(f, gl.COMPILE_STATUS)) {
965
+ gl.deleteShader(v);
966
+ gl.deleteShader(f);
967
+ return null;
968
+ }
969
+ const p = gl.createProgram();
970
+ if (!p) return null;
971
+ gl.attachShader(p, v);
972
+ gl.attachShader(p, f);
973
+ gl.linkProgram(p);
974
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
975
+ gl.deleteProgram(p);
976
+ return null;
977
+ }
978
+ return p;
979
+ }
980
+ function shaderFragment(preset) {
981
+ const common = `
982
+ precision mediump float;
983
+ uniform vec2 u_resolution;
984
+ uniform float u_time;
985
+ uniform float u_scale;
986
+ uniform float u_seed;
987
+
988
+ vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
989
+ vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
990
+ vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
991
+ vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
992
+ vec3 hueRotate(vec3 rgb, float angle) {
993
+ float c = cos(angle), s = sin(angle);
994
+ mat3 m = mat3(
995
+ 0.299 + 0.701*c + 0.168*s, 0.587 - 0.587*c + 0.330*s, 0.114 - 0.114*c - 0.497*s,
996
+ 0.299 - 0.299*c - 0.328*s, 0.587 + 0.413*c + 0.035*s, 0.114 - 0.114*c + 0.292*s,
997
+ 0.299 - 0.300*c + 1.250*s, 0.587 - 0.588*c - 1.050*s, 0.114 + 0.886*c - 0.203*s
998
+ );
999
+ return rgb * m;
1000
+ }
1001
+ float snoise(vec3 v) {
1002
+ const vec2 C = vec2(1.0/6.0, 1.0/3.0);
1003
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
1004
+ vec3 i = floor(v + dot(v, C.yyy));
1005
+ vec3 x0 = v - i + dot(i, C.xxx);
1006
+ vec3 g = step(x0.yzx, x0.xyz);
1007
+ vec3 l = 1.0 - g;
1008
+ vec3 i1 = min(g.xyz, l.zxy);
1009
+ vec3 i2 = max(g.xyz, l.zxy);
1010
+ vec3 x1 = x0 - i1 + C.xxx;
1011
+ vec3 x2 = x0 - i2 + C.yyy;
1012
+ vec3 x3 = x0 - D.yyy;
1013
+ i = mod289(i);
1014
+ vec4 p = permute(permute(permute(
1015
+ i.z + vec4(0.0, i1.z, i2.z, 1.0))
1016
+ + i.y + vec4(0.0, i1.y, i2.y, 1.0))
1017
+ + i.x + vec4(0.0, i1.x, i2.x, 1.0));
1018
+ float n_ = 0.142857142857;
1019
+ vec3 ns = n_ * D.wyz - D.xzx;
1020
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
1021
+ vec4 x_ = floor(j * ns.z);
1022
+ vec4 y_ = floor(j - 7.0 * x_);
1023
+ vec4 x = x_ * ns.x + ns.yyyy;
1024
+ vec4 y = y_ * ns.x + ns.yyyy;
1025
+ vec4 h = 1.0 - abs(x) - abs(y);
1026
+ vec4 b0 = vec4(x.xy, y.xy);
1027
+ vec4 b1 = vec4(x.zw, y.zw);
1028
+ vec4 s0 = floor(b0)*2.0 + 1.0;
1029
+ vec4 s1 = floor(b1)*2.0 + 1.0;
1030
+ vec4 sh = -step(h, vec4(0.0));
1031
+ vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
1032
+ vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
1033
+ vec3 p0 = vec3(a0.xy, h.x);
1034
+ vec3 p1 = vec3(a0.zw, h.y);
1035
+ vec3 p2 = vec3(a1.xy, h.z);
1036
+ vec3 p3 = vec3(a1.zw, h.w);
1037
+ vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
1038
+ p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
1039
+ vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
1040
+ m = m * m;
1041
+ return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
1042
+ }
1043
+ float fbm(vec3 p) {
1044
+ float v = 0.0, a = 0.5;
1045
+ for (int i = 0; i < 5; i++) {
1046
+ v += a * snoise(p);
1047
+ p *= 2.0; a *= 0.5;
1048
+ }
1049
+ return v;
1050
+ }
1051
+ `;
1052
+ if (preset === "aurora") return common + `
1053
+ void main() {
1054
+ vec2 uv = gl_FragCoord.xy / u_resolution;
1055
+ float t = u_time;
1056
+ float n1 = fbm(vec3(uv * 2.5 * u_scale, t));
1057
+ float n2 = fbm(vec3(uv * 4.0 * u_scale + 7.0, t * 1.4));
1058
+ float n3 = fbm(vec3(uv * 1.2 * u_scale - 3.0, t * 0.7));
1059
+ float bands = smoothstep(0.15, 0.85, 0.5 + 0.5 * sin((uv.y + n3 * 0.08) * 8.0 + n1 * 1.5));
1060
+ vec3 c1 = vec3(0.03, 0.08, 0.14);
1061
+ vec3 c2 = vec3(0.05, 0.28, 0.32);
1062
+ vec3 c3 = vec3(0.18, 0.62, 0.42);
1063
+ vec3 c4 = vec3(0.55, 0.22, 0.52);
1064
+ vec3 c5 = vec3(0.85, 0.35, 0.25);
1065
+ vec3 col = mix(c1, c2, bands);
1066
+ col = mix(col, c3, smoothstep(0.25, 0.75, n1));
1067
+ col = mix(col, c4, smoothstep(0.45, 0.85, n2) * 0.75);
1068
+ col = mix(col, c5, smoothstep(0.7, 0.95, n2 + n1 * 0.3) * 0.45);
1069
+ col = hueRotate(col, u_seed * 6.28318530718);
1070
+ gl_FragColor = vec4(col, 1.0);
1071
+ }
1072
+ `;
1073
+ if (preset === "nebula") return common + `
1074
+ void main() {
1075
+ vec2 uv = gl_FragCoord.xy / u_resolution;
1076
+ float t = u_time * 0.8;
1077
+ float n = fbm(vec3(uv * 2.2 * u_scale, t));
1078
+ float n2 = fbm(vec3(uv * 5.0 * u_scale - 4.0, t * 0.65));
1079
+ float n3 = fbm(vec3(uv * 0.9 * u_scale + 2.0, t * 0.4));
1080
+ vec3 c1 = vec3(0.02, 0.02, 0.08);
1081
+ vec3 c2 = vec3(0.12, 0.04, 0.22);
1082
+ vec3 c3 = vec3(0.32, 0.10, 0.35);
1083
+ vec3 c4 = vec3(0.10, 0.18, 0.42);
1084
+ vec3 c5 = vec3(0.55, 0.30, 0.55);
1085
+ vec3 col = mix(c1, c2, smoothstep(-0.5, 0.6, n));
1086
+ col = mix(col, c3, smoothstep(0.15, 0.8, n2) * 0.75);
1087
+ col = mix(col, c4, smoothstep(0.35, 0.85, n + n3 * 0.3) * 0.55);
1088
+ col = mix(col, c5, smoothstep(0.6, 0.95, n2) * 0.35);
1089
+ col = hueRotate(col, u_seed * 6.28318530718);
1090
+ gl_FragColor = vec4(col, 1.0);
1091
+ }
1092
+ `;
1093
+ return common + `
1094
+ void main() {
1095
+ vec2 uv = gl_FragCoord.xy / u_resolution;
1096
+ float t = u_time;
1097
+ float n = fbm(vec3(uv * 3.5 * u_scale, t));
1098
+ float n2 = fbm(vec3(uv * 9.0 * u_scale + 15.0, t * 1.6));
1099
+ float n3 = fbm(vec3(uv * 1.5 * u_scale - 5.0, t * 0.5));
1100
+ vec3 c1 = vec3(0.06, 0.06, 0.08);
1101
+ vec3 c2 = vec3(0.16, 0.18, 0.22);
1102
+ vec3 c3 = vec3(0.30, 0.32, 0.36);
1103
+ vec3 c4 = vec3(0.46, 0.48, 0.52);
1104
+ vec3 col = mix(c1, c2, 0.5 + 0.5 * n + n3 * 0.15);
1105
+ col = mix(col, c3, smoothstep(0.3, 0.8, n2) * 0.45);
1106
+ col = mix(col, c4, smoothstep(0.6, 0.95, n2) * 0.25);
1107
+ col = hueRotate(col, u_seed * 6.28318530718);
1108
+ gl_FragColor = vec4(col, 1.0);
1109
+ }
1110
+ `;
1111
+ }
1112
+ function createPatternBg(params, canvas) {
1113
+ if (params.preset === "waves") return createWaves(params, canvas);
1114
+ if (params.preset === "poly") return createLowPoly(params, canvas);
1115
+ return createDots(params, canvas);
1116
+ }
1117
+ function createDots(params, canvas) {
1118
+ const c = canvas ?? createCanvas();
1119
+ fitLiveCanvas(c);
1120
+ const g = c.getContext("2d", { alpha: false });
1121
+ const rng = createRng(params.seed);
1122
+ const dark = rng() < .5;
1123
+ const baseHue = Math.round(rng() * 360);
1124
+ const spacing = Math.max(26, 150 * params.scale / (.25 + params.density));
1125
+ const dots = [];
1126
+ for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) dots.push({
1127
+ cx: x / 7,
1128
+ cy: y / 7,
1129
+ r: spacing * .16 * params.scale * (.6 + rng() * .7),
1130
+ hue: (baseHue + rng() * 80) % 360,
1131
+ sat: Math.round(35 + rng() * 55),
1132
+ lit: dark ? Math.round(30 + rng() * 35) : Math.round(55 + rng() * 30),
1133
+ phase: rng() * Math.PI * 2,
1134
+ speed: .5 + rng() * 1.2
1135
+ });
1136
+ let t = 0;
1137
+ const draw = () => {
1138
+ fitLiveCanvas(c);
1139
+ const w = c.width, h = c.height;
1140
+ g.fillStyle = dark ? "#0a0b0d" : "#f6f7f9";
1141
+ g.fillRect(0, 0, w, h);
1142
+ for (const d of dots) {
1143
+ const pulse = .75 + .35 * Math.sin(t * d.speed + d.phase);
1144
+ const r = Math.max(1, d.r * pulse);
1145
+ const x = d.cx * w + d.r * .3 * Math.sin(t * d.speed * .5 + d.phase);
1146
+ const y = d.cy * h + d.r * .3 * Math.cos(t * d.speed * .7 + d.phase);
1147
+ g.beginPath();
1148
+ g.arc(x, y, r, 0, Math.PI * 2);
1149
+ g.fillStyle = `hsla(${d.hue},${d.sat}%,${d.lit}%,${(.18 + .25 * pulse).toFixed(2)})`;
1150
+ g.fill();
1151
+ }
1152
+ t += .032;
1153
+ };
1154
+ return {
1155
+ canvas: c,
1156
+ ...newRaf(c, draw),
1157
+ snapshot: () => c.toDataURL("image/jpeg", .94)
1158
+ };
1159
+ }
1160
+ function createWaves(params, canvas) {
1161
+ const c = canvas ?? createCanvas();
1162
+ fitLiveCanvas(c);
1163
+ const g = c.getContext("2d", { alpha: false });
1164
+ const rng = createRng(params.seed);
1165
+ const dark = rng() < .5;
1166
+ const hue = Math.round(rng() * 360);
1167
+ const layers = Math.round(5 + params.density * 8);
1168
+ const waves = [];
1169
+ for (let i = 0; i < layers; i++) waves.push({
1170
+ yBase: .25 + i / layers * .55,
1171
+ amp: (25 + rng() * 45) * params.scale,
1172
+ freq: (.006 + rng() * .01) / params.scale,
1173
+ phase: rng() * Math.PI * 2,
1174
+ speed: (.3 + rng() * .7) * (rng() < .5 ? 1 : -1),
1175
+ hue: (hue + i * 12) % 360,
1176
+ sat: Math.round(40 + rng() * 45),
1177
+ lit: dark ? Math.round(14 + i / layers * 32) : Math.round(72 - i / layers * 28),
1178
+ alpha: .22 + rng() * .32
1179
+ });
1180
+ let t = 0;
1181
+ const draw = () => {
1182
+ fitLiveCanvas(c);
1183
+ const w = c.width, h = c.height;
1184
+ g.fillStyle = dark ? "#07080a" : "#f8f9fb";
1185
+ g.fillRect(0, 0, w, h);
1186
+ for (const wave of waves) {
1187
+ g.beginPath();
1188
+ g.moveTo(0, h);
1189
+ for (let x = 0; x <= w; x += Math.max(6, Math.floor(w / 180))) {
1190
+ const y = h * wave.yBase + Math.sin(x * wave.freq + wave.phase + t * wave.speed) * wave.amp + Math.sin(x * wave.freq * 2.1 + wave.phase * 1.3 - t * wave.speed * 1.5) * wave.amp * .5;
1191
+ g.lineTo(x, y);
1192
+ }
1193
+ g.lineTo(w, h);
1194
+ g.closePath();
1195
+ g.fillStyle = `hsla(${wave.hue},${wave.sat}%,${wave.lit}%,${wave.alpha.toFixed(2)})`;
1196
+ g.fill();
1197
+ }
1198
+ t += .032;
1199
+ };
1200
+ return {
1201
+ canvas: c,
1202
+ ...newRaf(c, draw),
1203
+ snapshot: () => c.toDataURL("image/jpeg", .94)
1204
+ };
1205
+ }
1206
+ function createLowPoly(params, canvas) {
1207
+ const c = canvas ?? createCanvas();
1208
+ fitLiveCanvas(c);
1209
+ const g = c.getContext("2d", { alpha: false });
1210
+ const rng = createRng(params.seed);
1211
+ const dark = rng() < .5;
1212
+ const hue = Math.round(rng() * 360);
1213
+ const cols = Math.round(10 + params.density * 20);
1214
+ const rows = Math.max(6, Math.round(cols * .65));
1215
+ const points = [];
1216
+ for (let y = 0; y <= rows; y++) {
1217
+ const row = [];
1218
+ for (let x = 0; x <= cols; x++) row.push({
1219
+ x: x / cols,
1220
+ y: y / rows,
1221
+ dx: (rng() - .5) * .02,
1222
+ dy: (rng() - .5) * .02,
1223
+ speed: .3 + rng() * .5
1224
+ });
1225
+ points.push(row);
1226
+ }
1227
+ const cells = [];
1228
+ for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) cells.push({
1229
+ y,
1230
+ x,
1231
+ hue: (hue + rng() * 60) % 360,
1232
+ sat: Math.round(35 + rng() * 40),
1233
+ lit: dark ? Math.round(12 + rng() * 35) : Math.round(65 - rng() * 10),
1234
+ phase: rng() * Math.PI * 2
1235
+ });
1236
+ let t = 0;
1237
+ const px = points.map((row) => row.map(() => ({
1238
+ x: 0,
1239
+ y: 0
1240
+ })));
1241
+ const draw = () => {
1242
+ fitLiveCanvas(c);
1243
+ const w = c.width, h = c.height;
1244
+ g.fillStyle = dark ? "#08090c" : "#f5f6f8";
1245
+ g.fillRect(0, 0, w, h);
1246
+ for (let y = 0; y <= rows; y++) {
1247
+ const row = points[y];
1248
+ const out = px[y];
1249
+ for (let x = 0; x <= cols; x++) {
1250
+ const p = row[x];
1251
+ out[x].x = (p.x + Math.sin(t * p.speed + p.dx * 100) * p.dx) * w;
1252
+ out[x].y = (p.y + Math.cos(t * p.speed + p.dy * 100) * p.dy) * h;
1253
+ }
1254
+ }
1255
+ for (const cell of cells) {
1256
+ const p1 = px[cell.y][cell.x], p2 = px[cell.y][cell.x + 1], p3 = px[cell.y + 1][cell.x], p4 = px[cell.y + 1][cell.x + 1];
1257
+ const cx = (p1.x + p2.x + p3.x) / 3 / w;
1258
+ const lit = Math.max(0, Math.min(100, cell.lit + Math.sin(t * .6 + cell.phase) * 6));
1259
+ g.beginPath();
1260
+ g.moveTo(p1.x, p1.y);
1261
+ g.lineTo(p2.x, p2.y);
1262
+ g.lineTo(p3.x, p3.y);
1263
+ g.closePath();
1264
+ g.fillStyle = `hsla(${cell.hue + cx * 40},${cell.sat}%,${lit}%,0.92)`;
1265
+ g.fill();
1266
+ g.beginPath();
1267
+ g.moveTo(p2.x, p2.y);
1268
+ g.lineTo(p4.x, p4.y);
1269
+ g.lineTo(p3.x, p3.y);
1270
+ g.closePath();
1271
+ g.fillStyle = `hsla(${(cell.hue + cx * 40 + 12) % 360},${cell.sat}%,${Math.max(0, lit - 4)}%,0.92)`;
1272
+ g.fill();
1273
+ }
1274
+ t += .03;
1275
+ };
1276
+ return {
1277
+ canvas: c,
1278
+ ...newRaf(c, draw),
1279
+ snapshot: () => c.toDataURL("image/jpeg", .94)
1280
+ };
1281
+ }
1282
+ function createDynamicBackground(params, canvas) {
1283
+ if (params.type === "mesh") return createMeshGradient(params, canvas);
1284
+ if (params.type === "shader") return createShaderBg(params, canvas);
1285
+ return createPatternBg(params, canvas);
1286
+ }
1287
+ function randomSeed$1() {
1288
+ return Math.floor(Math.random() * 2147483647);
1289
+ }
1290
+ /** Build default params for a newly selected background type. */
1291
+ function defaultParamsFor(type) {
1292
+ if (type === "mesh") return {
1293
+ type: "mesh",
1294
+ seed: randomSeed$1(),
1295
+ scale: 1.1,
1296
+ intensity: .65
1297
+ };
1298
+ if (type === "shader") return {
1299
+ type: "shader",
1300
+ preset: "aurora",
1301
+ speed: .35,
1302
+ scale: 1,
1303
+ seed: randomSeed$1()
1304
+ };
1305
+ return {
1306
+ type: "pattern",
1307
+ preset: "dots",
1308
+ density: .5,
1309
+ scale: 1,
1310
+ seed: randomSeed$1()
1311
+ };
1312
+ }
1313
+ //#endregion
526
1314
  //#region src/client/wallpaper.ts
527
1315
  let wpEl = null;
528
1316
  let appliedTokenNames = [];
1317
+ let wpController = null;
1318
+ let snapshotListener = null;
1319
+ let tokenStyleEl = null;
1320
+ function clearDynamicBg() {
1321
+ wpController?.stop();
1322
+ wpController?.canvas.remove();
1323
+ wpController = null;
1324
+ }
1325
+ /** Register a callback fired once a generated snapshot is ready (so the caller
1326
+ * can re-sync the settings preview / store). */
1327
+ function onGeneratedSnapshot(cb) {
1328
+ snapshotListener = cb;
1329
+ }
1330
+ function ensureTokenStyle() {
1331
+ if (tokenStyleEl?.isConnected) return tokenStyleEl;
1332
+ tokenStyleEl = document.createElement("style");
1333
+ tokenStyleEl.dataset.plugin = "dsh-any-background-tokens";
1334
+ document.head.appendChild(tokenStyleEl);
1335
+ return tokenStyleEl;
1336
+ }
529
1337
  function clearCustomTokens() {
1338
+ if (tokenStyleEl) tokenStyleEl.textContent = "";
530
1339
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
531
1340
  appliedTokenNames = [];
532
1341
  }
@@ -539,20 +1348,39 @@ window.__ModuleLoader__.load({
539
1348
  * brand) is written verbatim. No reads: nothing can observe a stale or reset
540
1349
  * theme value and leave the homepage on the system color.
541
1350
  */
1351
+ /** Label tokens flipped by the generated-background brightness verdict. */
1352
+ const LABEL_TOKENS = [
1353
+ "--dsw-alias-label-primary",
1354
+ "--dsw-alias-label-secondary",
1355
+ "--dsw-alias-label-tertiary",
1356
+ "--dsw-alias-label-caption",
1357
+ "--dsw-alias-label-dimmed",
1358
+ "--dsw-alias-label-quaternary"
1359
+ ];
542
1360
  function applyCustomTokens(ops) {
543
1361
  const [h, s, l] = rColor();
544
- const { tokens } = genTokens(h, s, l);
545
- clearCustomTokens();
546
- if (l < .55) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
547
- else document.body.removeAttribute("data-ds-dark-theme");
548
- for (const [name, value] of Object.entries(tokens)) {
549
- let v = value;
550
- if (name === "--dsw-alias-bg-base") v = toRgba(value, ops.bg);
551
- else if (name === "--dsw-specific-sidebar-fill") v = toRgba(value, ops.sidebar);
552
- else if (name === "--dsw-alias-bg-layer-1" || name === "--dsw-alias-bg-layer-2" || name === "--dsw-alias-bg-layer-3") v = toRgba(value, ops.card);
553
- document.body.style.setProperty(name, v);
554
- appliedTokenNames.push(name);
555
- }
1362
+ let { tokens } = genTokens(h, s, l);
1363
+ try {
1364
+ const dark = rBgDark();
1365
+ if (dark !== null) {
1366
+ tokens = { ...tokens };
1367
+ const font = dark ? "#fff" : "#000";
1368
+ for (const name of LABEL_TOKENS) tokens[name] = font;
1369
+ }
1370
+ if (dark ?? l < .55) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
1371
+ else document.body.removeAttribute("data-ds-dark-theme");
1372
+ const decls = [];
1373
+ for (const [name, value] of Object.entries(tokens)) {
1374
+ let v = value;
1375
+ if (name === "--dsw-alias-bg-base") v = toRgba(value, ops.bg);
1376
+ else if (name === "--dsw-specific-sidebar-fill") v = toRgba(value, ops.sidebar);
1377
+ else if (name === "--dsw-alias-bg-layer-1" || name === "--dsw-alias-bg-layer-2" || name === "--dsw-alias-bg-layer-3") v = toRgba(value, ops.card);
1378
+ decls.push(`${name}:${v}!important`);
1379
+ }
1380
+ ensureTokenStyle().textContent = `body{${decls.join(";")}}`;
1381
+ for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1382
+ appliedTokenNames = Object.keys(tokens);
1383
+ } catch (e) {}
556
1384
  }
557
1385
  const SETTINGS_STYLE_RULE = `[role="dialog"][aria-modal="true"][aria-labelledby]{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none)}`;
558
1386
  function applySettingsOverrides(op) {
@@ -560,14 +1388,109 @@ window.__ModuleLoader__.load({
560
1388
  document.documentElement.style.removeProperty("--dsh-any-bg-settings-surface");
561
1389
  return;
562
1390
  }
1391
+ if (!rHasColor()) return;
563
1392
  const [h, s, l] = rColor();
564
1393
  const layer2 = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-2"];
565
1394
  if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-surface", toRgba(layer2, op));
566
1395
  }
1396
+ /** Apply only the wallpaper layer (position, scale, opacity, blur) without
1397
+ * touching the theme color palette. Used when generated background parameters
1398
+ * change so the visual keeps updating without shifting the picked theme color. */
1399
+ function applyWallpaper() {
1400
+ applyWp();
1401
+ }
1402
+ /** Apply the theme color. If the user has an explicit saved pick we use it
1403
+ * directly; otherwise we fall back to extracting a dominant color from the
1404
+ * current wallpaper (one HSL sample, no palette), so new uploads still get a
1405
+ * matching theme automatically. */
1406
+ function applyThemeColor() {
1407
+ if (rHasColor()) {
1408
+ applyWp();
1409
+ return;
1410
+ }
1411
+ const url = rWp();
1412
+ if (url) extractWallpaperColor(url, rBgState()).then((hsl) => {
1413
+ if (hsl) cfg.color = hsl;
1414
+ applyWp();
1415
+ });
1416
+ else applyWp();
1417
+ }
1418
+ /** Switch the background source type. For generated types a new live canvas is
1419
+ * attached to the wallpaper layer and a snapshot is kept for the store/preview. */
1420
+ function setBackgroundType(type) {
1421
+ cfg.backgroundType = type;
1422
+ if (type === "image") {
1423
+ clearDynamicBg();
1424
+ setBgDark(null);
1425
+ setWpUrl(rWpImage());
1426
+ applyThemeColor();
1427
+ return;
1428
+ }
1429
+ if (!cfg.generatedBg || cfg.generatedBg.type !== type) cfg.generatedBg = defaultParamsFor(type);
1430
+ applyGeneratedBg(cfg.generatedBg);
1431
+ }
1432
+ function randomSeed() {
1433
+ return Math.floor(Math.random() * 2147483647);
1434
+ }
1435
+ /** Regenerate the current generated background with a new visual seed while
1436
+ * preserving the user's scale/intensity/speed/density/preset choices. */
1437
+ function regenerateGeneratedBg() {
1438
+ const params = cfg.generatedBg;
1439
+ if (!params || cfg.backgroundType === "image") return;
1440
+ const next = params.type === "mesh" ? {
1441
+ ...params,
1442
+ seed: randomSeed()
1443
+ } : {
1444
+ ...params,
1445
+ seed: randomSeed()
1446
+ };
1447
+ cfg.generatedBg = next;
1448
+ applyGeneratedBg(next);
1449
+ }
1450
+ /** Update a generated background's parameters and re-render. */
1451
+ function updateGeneratedBg(params) {
1452
+ cfg.backgroundType = params.type;
1453
+ cfg.generatedBg = params;
1454
+ applyGeneratedBg(params);
1455
+ }
1456
+ function applyGeneratedBg(params) {
1457
+ clearDynamicBg();
1458
+ ensureWpContainer();
1459
+ wpController = createDynamicBackground(params);
1460
+ if (wpEl) {
1461
+ wpEl.style.backgroundImage = "none";
1462
+ wpEl.appendChild(wpController.canvas);
1463
+ }
1464
+ requestAnimationFrame(() => {
1465
+ if (!wpController) return;
1466
+ const controller = wpController;
1467
+ const frame = controller.snapshot();
1468
+ setWpUrl(frame);
1469
+ applyWallpaper();
1470
+ snapshotListener?.();
1471
+ setBgDark(null);
1472
+ analyzeFrameDark(frame).then((dark) => {
1473
+ if (dark === null || wpController !== controller) return;
1474
+ setBgDark(dark);
1475
+ applyCustomTokens(rOps());
1476
+ });
1477
+ });
1478
+ }
567
1479
  let frameEl = null;
568
1480
  let sidebarEl = null;
569
1481
  let centerEl = null;
570
1482
  let detailsEl = null;
1483
+ const PART_BLUR_CLASS = "dab-part-blur";
1484
+ const PART_UNDERLAY_CLASS = "dab-part-underlay";
1485
+ const PART_BLUR_RULE = `${PART_BLUR_CLASS}{isolation:isolate}.${PART_UNDERLAY_CLASS}{position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;backdrop-filter:var(--dsh-any-part-blur,none);-webkit-backdrop-filter:var(--dsh-any-part-blur,none)}`;
1486
+ let partBlurStyleEl = null;
1487
+ function ensurePartBlurStyle() {
1488
+ if (partBlurStyleEl?.isConnected) return;
1489
+ partBlurStyleEl = document.createElement("style");
1490
+ partBlurStyleEl.dataset.plugin = "dsh-any-background-parts";
1491
+ partBlurStyleEl.textContent = PART_BLUR_RULE;
1492
+ document.head.appendChild(partBlurStyleEl);
1493
+ }
571
1494
  function discoverParts() {
572
1495
  const overlay = document.querySelector("[data-shell-overlay]");
573
1496
  if (overlay === null) return;
@@ -581,8 +1504,29 @@ window.__ModuleLoader__.load({
581
1504
  }
582
1505
  function setBlur(el, px) {
583
1506
  if (el === null) return;
584
- if (px > 0) el.style.backdropFilter = `blur(${px}px)`;
585
- else el.style.removeProperty("backdrop-filter");
1507
+ const underlay = el.querySelector(`:scope > .${PART_UNDERLAY_CLASS}`);
1508
+ if (px > 0) {
1509
+ ensurePartBlurStyle();
1510
+ if (!el.classList.contains(PART_BLUR_CLASS) && getComputedStyle(el).position === "static") {
1511
+ el.style.position = "relative";
1512
+ el.setAttribute("data-dab-pos-patched", "1");
1513
+ }
1514
+ el.classList.add(PART_BLUR_CLASS);
1515
+ if (underlay === null) {
1516
+ const node = document.createElement("div");
1517
+ node.className = PART_UNDERLAY_CLASS;
1518
+ el.prepend(node);
1519
+ }
1520
+ el.style.setProperty("--dsh-any-part-blur", `blur(${px}px)`);
1521
+ } else {
1522
+ el.classList.remove(PART_BLUR_CLASS);
1523
+ el.style.removeProperty("--dsh-any-part-blur");
1524
+ underlay?.remove();
1525
+ if (el.getAttribute("data-dab-pos-patched") === "1") {
1526
+ el.style.removeProperty("position");
1527
+ el.removeAttribute("data-dab-pos-patched");
1528
+ }
1529
+ }
586
1530
  }
587
1531
  function applySettingsBlur(px) {
588
1532
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-settings", `blur(${px}px)`);
@@ -630,41 +1574,68 @@ window.__ModuleLoader__.load({
630
1574
  partsObserver?.disconnect();
631
1575
  partsObserver = null;
632
1576
  }
1577
+ function ensureWpContainer() {
1578
+ if (!wpEl || !document.body.contains(wpEl)) {
1579
+ wpEl = document.createElement("div");
1580
+ wpEl.style.cssText = "position:fixed;inset:0;z-index:-1;pointer-events:none;overflow:hidden;";
1581
+ document.body.prepend(wpEl);
1582
+ }
1583
+ }
1584
+ function applyImageWp(url) {
1585
+ clearDynamicBg();
1586
+ ensureWpContainer();
1587
+ const bg = rBgState();
1588
+ const next = `url("${url}")`;
1589
+ if (wpEl.style.backgroundImage !== next) {
1590
+ wpEl.style.backgroundImage = next;
1591
+ wpEl.style.backgroundRepeat = "no-repeat";
1592
+ }
1593
+ if (bg.iw > 0) {
1594
+ const fit = Math.min(window.innerWidth / bg.iw, window.innerHeight / bg.ih);
1595
+ const w = bg.iw * fit * bg.zoom;
1596
+ const h = bg.ih * fit * bg.zoom;
1597
+ wpEl.style.backgroundSize = `${w}px ${h}px`;
1598
+ wpEl.style.backgroundPosition = `${bg.x * window.innerWidth - w / 2}px ${bg.y * window.innerHeight - h / 2}px`;
1599
+ } else {
1600
+ wpEl.style.backgroundSize = "contain";
1601
+ wpEl.style.backgroundPosition = "center";
1602
+ }
1603
+ applyWpEffects();
1604
+ }
1605
+ function applyWpEffects() {
1606
+ if (!wpEl) return;
1607
+ const blur = rBl();
1608
+ wpEl.style.filter = blur > 0 ? `blur(${blur}px)` : "none";
1609
+ wpEl.style.opacity = String(rWop());
1610
+ }
633
1611
  function applyWp() {
634
1612
  const url = rWp();
635
- if (!url) {
1613
+ if (cfg.backgroundType !== "image" && cfg.generatedBg) {
1614
+ if (!wpController) {
1615
+ applyGeneratedBg(cfg.generatedBg);
1616
+ return;
1617
+ }
1618
+ ensureWpContainer();
1619
+ if (wpController.canvas.parentElement !== wpEl) wpEl.appendChild(wpController.canvas);
1620
+ applyWpEffects();
1621
+ } else if (url) applyImageWp(url);
1622
+ else {
1623
+ clearDynamicBg();
636
1624
  wpEl?.remove();
637
1625
  wpEl = null;
638
- } else {
639
- if (!wpEl || !document.body.contains(wpEl)) {
640
- wpEl = document.createElement("div");
641
- wpEl.style.cssText = "position:fixed;inset:0;z-index:-1;pointer-events:none;background-repeat:no-repeat;";
642
- document.body.prepend(wpEl);
643
- }
644
- const bg = rBgState();
645
- wpEl.style.backgroundImage = `url("${url}")`;
646
- if (bg.iw > 0) {
647
- const fit = Math.min(window.innerWidth / bg.iw, window.innerHeight / bg.ih);
648
- const w = bg.iw * fit * bg.zoom;
649
- const h = bg.ih * fit * bg.zoom;
650
- wpEl.style.backgroundSize = `${w}px ${h}px`;
651
- wpEl.style.backgroundPosition = `${bg.x * window.innerWidth - w / 2}px ${bg.y * window.innerHeight - h / 2}px`;
652
- } else {
653
- wpEl.style.backgroundSize = "contain";
654
- wpEl.style.backgroundPosition = "center";
655
- }
656
- const blur = rBl();
657
- wpEl.style.filter = blur > 0 ? `blur(${blur}px)` : "none";
658
- wpEl.style.opacity = String(rWop());
659
1626
  }
660
- applyCustomTokens(rOps());
661
- applySettingsOverrides(rSop());
1627
+ if (rHasColor() || rBgDark() !== null) applyCustomTokens(rOps());
1628
+ if (rHasColor()) applySettingsOverrides(rSop());
662
1629
  applyPartBlurs(rBlurs());
663
1630
  }
664
1631
  function teardownWp() {
1632
+ clearDynamicBg();
1633
+ setBgDark(null);
665
1634
  wpEl?.remove();
666
1635
  wpEl = null;
667
1636
  clearCustomTokens();
1637
+ tokenStyleEl?.remove();
1638
+ tokenStyleEl = null;
668
1639
  document.documentElement.style.removeProperty("--dsh-any-bg-settings-surface");
669
1640
  document.documentElement.style.removeProperty("--dsh-any-blur-settings");
670
1641
  setBlur(frameEl, 0);
@@ -681,378 +1652,554 @@ window.__ModuleLoader__.load({
681
1652
  function setWpBlur(v) {
682
1653
  if (wpEl) wpEl.style.filter = v > 0 ? `blur(${v}px)` : "none";
683
1654
  }
684
- //#endregion
685
- //#region src/client/styles.ts
686
- const ST = {
687
- root: {
688
- display: "flex",
689
- flexDirection: "column",
690
- gap: "20px",
691
- padding: "4px 0",
692
- maxWidth: "640px"
693
- },
694
- headerRow: {
695
- display: "flex",
696
- alignItems: "flex-start",
697
- justifyContent: "space-between",
698
- gap: "12px",
699
- flexWrap: "wrap"
700
- },
701
- h2: {
702
- color: "var(--dsw-alias-label-primary)",
703
- fontSize: "18px",
704
- fontWeight: 600,
705
- margin: 0
706
- },
707
- sub: {
708
- color: "var(--dsw-alias-label-tertiary)",
709
- fontSize: "13px"
710
- },
711
- hr: {
712
- height: "1px",
713
- background: "var(--dsw-alias-border-l2)",
714
- border: "none",
715
- margin: "4px 0"
716
- },
717
- label: {
718
- color: "var(--dsw-alias-label-primary)",
719
- fontSize: "16px",
720
- fontWeight: 600,
721
- marginBottom: "8px"
722
- },
723
- hint: {
724
- color: "var(--dsw-alias-label-tertiary)",
725
- fontSize: "12px",
726
- lineHeight: "18px",
727
- marginTop: "4px"
728
- },
729
- colorHint: {
730
- color: "var(--dsw-alias-label-tertiary)",
731
- fontSize: "12px",
732
- lineHeight: "18px",
733
- marginTop: "7px",
734
- textAlign: "center"
735
- },
736
- wheelCanvas: {
737
- cursor: "crosshair",
738
- borderRadius: "50%"
739
- },
740
- center: {
741
- display: "flex",
742
- justifyContent: "center"
743
- },
744
- wheelRow: {
745
- display: "flex",
746
- alignItems: "center",
747
- justifyContent: "center",
748
- gap: "24px",
749
- flexWrap: "wrap"
750
- },
751
- inputPanel: {
752
- display: "flex",
753
- flexDirection: "column",
754
- gap: "10px",
755
- minWidth: "168px"
756
- },
757
- inputTabs: {
758
- display: "flex",
759
- gap: "6px"
760
- },
761
- inputTab: {
762
- height: "26px",
763
- padding: "0 12px",
764
- borderRadius: "6px",
765
- border: "1px solid var(--dsw-alias-border-l2)",
766
- background: "var(--dsw-alias-button-elevated-fill)",
767
- color: "var(--dsw-alias-label-tertiary)",
768
- cursor: "pointer",
769
- fontSize: "12px",
770
- font: "inherit"
771
- },
772
- inputTabActive: {
773
- background: "var(--dsw-alias-brand-primary)",
774
- color: "var(--dsw-alias-brand-text)",
775
- borderColor: "transparent"
776
- },
777
- inputGrid: {
778
- display: "flex",
779
- flexDirection: "column",
780
- gap: "6px"
781
- },
782
- inputField: {
783
- display: "flex",
784
- alignItems: "center",
785
- gap: "8px"
786
- },
787
- inputLabel: {
788
- color: "var(--dsw-alias-label-secondary)",
789
- fontSize: "12px",
790
- width: "14px",
791
- textAlign: "center"
792
- },
793
- input: {
794
- flex: 1,
795
- height: "28px",
796
- padding: "0 8px",
797
- borderRadius: "6px",
798
- border: "1px solid var(--dsw-alias-border-l2)",
799
- background: "var(--dsw-alias-button-elevated-fill)",
800
- color: "var(--dsw-alias-label-primary)",
801
- fontSize: "12px",
802
- font: "inherit",
803
- boxSizing: "border-box",
804
- minWidth: "0"
805
- },
806
- swatch: {
807
- height: "22px",
808
- borderRadius: "6px",
809
- border: "1px solid var(--dsw-alias-border-l2)"
810
- },
811
- crashBox: {
812
- display: "flex",
813
- flexDirection: "column",
814
- gap: "10px",
815
- alignItems: "center",
816
- padding: "24px 16px",
817
- border: "1px solid var(--dsw-alias-border-l2)",
818
- borderRadius: "10px"
819
- },
820
- crashTitle: {
821
- color: "var(--dsw-alias-label-primary)",
822
- fontSize: "15px",
823
- fontWeight: 600
824
- },
825
- crashDesc: {
826
- color: "var(--dsw-alias-label-tertiary)",
827
- fontSize: "12px",
828
- textAlign: "center"
829
- },
830
- btnGroup: {
831
- display: "flex",
832
- justifyContent: "center",
833
- marginTop: "10px"
834
- },
835
- row: {
836
- display: "flex",
837
- alignItems: "center",
838
- gap: "10px",
839
- flexWrap: "wrap"
840
- },
841
- sliderBlock: {
842
- display: "flex",
843
- flexDirection: "column",
844
- gap: "4px"
845
- },
846
- sliderRow: {
847
- display: "flex",
848
- alignItems: "center",
849
- gap: "10px"
850
- },
851
- sliderLabel: {
852
- color: "var(--dsw-alias-label-secondary)",
853
- fontSize: "13px"
854
- },
855
- slider: {
856
- flex: 1,
857
- accentColor: "var(--dsw-alias-brand-primary)",
858
- minWidth: "160px"
859
- },
860
- sliderVal: {
861
- color: "var(--dsw-alias-label-secondary)",
862
- fontSize: "12px",
863
- whiteSpace: "nowrap",
864
- width: "44px",
865
- textAlign: "right"
866
- },
867
- btn: {
868
- height: "32px",
869
- padding: "0 14px",
870
- borderRadius: "8px",
871
- border: "1px solid var(--dsw-alias-border-l2)",
872
- background: "var(--dsw-alias-button-elevated-fill)",
873
- color: "var(--dsw-alias-label-primary)",
874
- cursor: "pointer",
875
- fontSize: "13px",
876
- font: "inherit",
877
- boxSizing: "border-box"
878
- },
879
- btnDanger: { color: "var(--dsw-alias-state-error-primary)" },
880
- btnPrimary: {
881
- background: "var(--dsw-alias-brand-primary)",
882
- color: "var(--dsw-alias-brand-text)",
883
- border: "none"
884
- },
885
- preview: {
886
- width: "368px",
887
- height: "225px",
888
- objectFit: "cover",
889
- borderRadius: "6px",
890
- border: "1px solid var(--dsw-alias-border-l2)",
891
- cursor: "pointer"
892
- },
893
- sliders: {
894
- display: "flex",
895
- flexDirection: "column",
896
- gap: "12px",
897
- marginTop: "8px"
898
- },
899
- overlay: {
900
- position: "fixed",
901
- inset: 0,
902
- zIndex: 9999,
903
- background: "rgba(0,0,0,0.7)",
904
- display: "flex",
905
- flexDirection: "column",
906
- alignItems: "center",
907
- justifyContent: "center",
908
- gap: "12px"
909
- },
910
- modalTitle: {
911
- color: "#fff",
912
- fontSize: "16px",
913
- fontWeight: 500
914
- },
915
- modalHint: {
916
- color: "rgba(255,255,255,0.6)",
917
- fontSize: "12px"
918
- },
919
- previewRect: {
920
- position: "relative",
921
- overflow: "hidden",
922
- border: "2px solid rgba(255,255,255,0.3)",
923
- borderRadius: "8px",
924
- background: "#000",
925
- cursor: "grab"
926
- },
927
- previewImg: {
928
- position: "absolute",
929
- transformOrigin: "0 0",
930
- pointerEvents: "none"
931
- },
932
- modalBtns: {
933
- display: "flex",
934
- gap: "10px"
935
- },
936
- pickerBox: {
937
- maxWidth: "min(90vw, 720px)",
938
- maxHeight: "60vh",
939
- overflow: "hidden",
940
- borderRadius: "8px",
941
- border: "2px solid rgba(255,255,255,0.3)",
942
- background: "#000",
943
- cursor: "crosshair"
944
- },
945
- pickerCanvas: {
946
- display: "block",
947
- maxWidth: "100%",
948
- maxHeight: "60vh",
949
- objectFit: "contain"
950
- },
951
- pickerBar: {
952
- display: "flex",
953
- alignItems: "center",
954
- gap: "10px",
955
- minWidth: "260px"
956
- },
957
- pickerSwatch: {
958
- width: "28px",
959
- height: "28px",
960
- borderRadius: "6px",
961
- border: "1px solid rgba(255,255,255,0.4)",
962
- flexShrink: 0
963
- },
964
- pickerValue: {
965
- color: "#fff",
966
- fontSize: "13px",
967
- fontFamily: "monospace"
968
- },
969
- magnifier: {
970
- position: "fixed",
971
- width: "96px",
972
- height: "96px",
973
- borderRadius: "50%",
974
- border: "2px solid rgba(255,255,255,0.7)",
975
- boxShadow: "0 4px 16px rgba(0,0,0,0.5)",
976
- zIndex: 1e4,
977
- background: "#000",
978
- pointerEvents: "none",
979
- transition: "opacity 0.08s"
1655
+ const UI_CSS = `
1656
+ /* The section is rendered INLINE inside the host settings dialog's content
1657
+ * column: the host provides the modal chrome (backdrop, centering, closing).
1658
+ * These classes style only the embedded shell; transient fixed layers (toast,
1659
+ * color picker, background editor) escape through Portals on <html>. */
1660
+ .dab-root{position:relative;color:var(--dsw-alias-label-primary);animation:dab-fade-in .35s ease both;container-type:inline-size;display:flex;flex-direction:column;align-items:center;width:100%;min-width:0;--dab-mono:ui-monospace,"Cascadia Mono","SF Mono",Consolas,"Courier New",monospace}
1661
+ .dab-root *,.dab-root *::before,.dab-root *::after{box-sizing:border-box}
1662
+ .dab-root button{font-family:inherit}
1663
+
1664
+ /* ── shell: nav rail + page body ─────────────────────────────────────────── */
1665
+ .dab-shell{display:grid;grid-template-columns:158px minmax(0,1fr);gap:26px;align-items:start;padding-bottom:8px;width:100%;max-width:980px;margin:0 auto}
1666
+ .dab-nav{position:sticky;top:0;display:flex;flex-direction:column;gap:18px}
1667
+ .dab-brand{display:flex;align-items:center;gap:10px;padding:2px 6px}
1668
+ .dab-brand-tile{width:30px;height:30px;flex:none;border-radius:9px;display:grid;place-items:center;color:var(--dsw-alias-brand-text);background:var(--dsw-alias-brand-primary);box-shadow:0 4px 14px -4px var(--dsw-alias-brand-primary)}
1669
+ .dab-brand-name{font-size:13px;font-weight:650;letter-spacing:.01em;line-height:1.25}
1670
+ .dab-brand-tag{font-size:9px;letter-spacing:.16em;font-weight:600;color:var(--dsw-alias-label-quaternary,var(--dsw-alias-label-tertiary));text-transform:uppercase}
1671
+ .dab-nav-list{position:relative;display:flex;flex-direction:column;gap:4px}
1672
+ .dab-nav-ind{position:absolute;left:0;right:0;top:0;height:38px;border-radius:11px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,transparent);transition:transform .38s cubic-bezier(.22,1,.36,1)}
1673
+ .dab-nav-item{position:relative;z-index:1;display:flex;align-items:center;gap:10px;height:38px;padding:0 12px;border:0;background:none;border-radius:11px;color:var(--dsw-alias-label-tertiary);font-size:13px;cursor:pointer;text-align:left;transition:color .22s ease}
1674
+ .dab-nav-item:hover{color:var(--dsw-alias-label-primary)}
1675
+ .dab-nav-item.is-active{color:var(--dsw-alias-brand-primary);font-weight:600}
1676
+ .dab-nav-item svg{flex:none;transition:transform .3s cubic-bezier(.34,1.56,.64,1)}
1677
+ .dab-nav-item:hover svg{transform:scale(1.14) rotate(-5deg)}
1678
+
1679
+ /* ── page chrome ─────────────────────────────────────────────────────────── */
1680
+ .dab-page{animation:dab-page-in .4s cubic-bezier(.22,1,.36,1) both;min-width:0;display:flex;flex-direction:column;gap:13px}
1681
+ .dab-head{margin:2px 0 5px}
1682
+ .dab-overline{font-size:10.5px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--dsw-alias-brand-primary);opacity:.9}
1683
+ .dab-h1{margin:4px 0 0;font-size:21px;font-weight:700;letter-spacing:-.01em}
1684
+ .dab-desc{margin:6px 0 0;font-size:12.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);max-width:56ch}
1685
+ .dab-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;padding:18px}
1686
+ .dab-card-hover{transition:transform .28s ease,box-shadow .28s ease,border-color .28s ease}
1687
+ .dab-card-hover:hover{transform:translateY(-2px);box-shadow:0 10px 28px -12px rgba(0,0,0,.28)}
1688
+ .dab-rise{animation:dab-rise-in .55s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--d,0) * 62ms)}
1689
+
1690
+ /* ── accent hero (color orb) ─────────────────────────────────────────────── */
1691
+ .dab-hero-accent{display:flex;align-items:center;gap:24px;flex-wrap:wrap}
1692
+ .dab-orb-wrap{position:relative;width:118px;height:118px;flex:none}
1693
+ .dab-orb{position:absolute;inset:11px;border-radius:50%;background:radial-gradient(circle at 32% 28%,rgba(255,255,255,.5),rgba(255,255,255,0) 44%),var(--c,#888);box-shadow:0 16px 36px -10px var(--c-soft,transparent),inset 0 -10px 20px rgba(0,0,0,.16);animation:dab-orb-in .7s cubic-bezier(.22,1,.36,1) both}
1694
+ .dab-orb-ring{position:absolute;inset:0;border-radius:50%;background:conic-gradient(from 0deg,transparent 0 30%,var(--c,#888) 46%,transparent 62%,transparent 76%,var(--c,#888) 90%,transparent 100%);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 3.5px),#000 calc(100% - 2.5px));mask:radial-gradient(farthest-side,transparent calc(100% - 3.5px),#000 calc(100% - 2.5px));animation:dab-spin 7s linear infinite;opacity:.9}
1695
+ .dab-hex-caption{font-size:11px;color:var(--dsw-alias-label-tertiary);letter-spacing:.04em}
1696
+ .dab-hex{font-family:var(--dab-mono);font-size:24px;font-weight:600;letter-spacing:.02em;line-height:1.2;margin-top:2px}
1697
+ .dab-hsl-row{display:flex;gap:16px;margin-top:7px;font-family:var(--dab-mono);font-size:11px;color:var(--dsw-alias-label-tertiary)}
1698
+ .dab-hsl-row b{font-weight:600;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
1699
+
1700
+ /* ── swatches ────────────────────────────────────────────────────────────── */
1701
+ .dab-swatch-title{font-size:12px;font-weight:600;margin-bottom:10px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
1702
+ .dab-swatches{display:flex;flex-wrap:wrap;gap:9px}
1703
+ .dab-swatch{width:25px;height:25px;border-radius:50%;border:0;padding:0;cursor:pointer;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1);transition:transform .22s cubic-bezier(.34,1.56,.64,1),box-shadow .22s ease}
1704
+ .dab-swatch:hover{transform:scale(1.2)}
1705
+ .dab-swatch.is-on{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1),0 0 0 4px var(--dsw-alias-brand-primary)}
1706
+
1707
+ /* ── wheel card ──────────────────────────────────────────────────────────── */
1708
+ .dab-wheel-card{position:relative;display:flex;align-items:center;justify-content:center;gap:28px;flex-wrap:wrap;padding:24px 18px}
1709
+ .dab-wheel-glow{position:absolute;width:230px;height:230px;border-radius:50%;filter:blur(48px);opacity:.2;background:var(--c,#888);pointer-events:none;transition:background .4s ease}
1710
+ .dab-wheel{position:relative;cursor:crosshair;border-radius:50%;box-shadow:0 12px 32px -14px rgba(0,0,0,.4)}
1711
+ .dab-hint{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);padding:0 4px}
1712
+
1713
+ /* ── precise color inputs ────────────────────────────────────────────────── */
1714
+ .dab-inputs{display:flex;flex-direction:column;gap:10px;min-width:172px}
1715
+ .dab-field{display:flex;align-items:center;gap:8px}
1716
+ .dab-field-label{width:14px;text-align:center;font-family:var(--dab-mono);font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary)}
1717
+ .dab-num{flex:1;min-width:0;height:30px;padding:0 10px;border-radius:9px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-family:var(--dab-mono);font-size:12px;outline:none;transition:border-color .2s,box-shadow .2s}
1718
+ .dab-num:focus{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
1719
+ .dab-num::-webkit-outer-spin-button,.dab-num::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
1720
+ .dab-num{-moz-appearance:textfield;appearance:textfield}
1721
+ .dab-swatch-lg{height:38px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);box-shadow:inset 0 0 14px rgba(0,0,0,.1);transition:transform .3s ease}
1722
+ .dab-swatch-lg:hover{transform:scale(1.02)}
1723
+
1724
+ /* ── buttons & chips ─────────────────────────────────────────────────────── */
1725
+ .dab-btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:34px;padding:0 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:550;cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease,opacity .18s ease,background .18s ease}
1726
+ .dab-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 5px 14px -6px rgba(0,0,0,.32)}
1727
+ .dab-btn:active:not(:disabled){transform:translateY(0) scale(.97);box-shadow:none}
1728
+ .dab-btn:disabled{opacity:.5;cursor:not-allowed}
1729
+ .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);border-color:transparent}
1730
+ .dab-btn-danger{color:var(--dsw-alias-state-error-primary)}
1731
+ .dab-btn-ghost{background:transparent;border-color:transparent;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
1732
+ .dab-btn-ghost:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);box-shadow:none}
1733
+ .dab-btn:focus-visible,.dab-nav-item:focus-visible,.dab-seg-item:focus-visible,.dab-swatch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
1734
+ .dab-chip-row{display:flex;flex-wrap:wrap;gap:8px}
1735
+ .dab-chip{height:30px;padding:0 14px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .22s ease}
1736
+ .dab-chip:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
1737
+ .dab-chip.is-active{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);border-color:transparent}
1738
+
1739
+ /* ── sliders ─────────────────────────────────────────────────────────────── */
1740
+ .dab-slider-block{display:flex;flex-direction:column;gap:6px}
1741
+ .dab-slider-block + .dab-slider-block{margin-top:13px}
1742
+ .dab-slider-label{font-size:12px;font-weight:550;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
1743
+ .dab-slider-val{font-family:var(--dab-mono);font-size:11px;color:var(--dsw-alias-label-tertiary);min-width:44px;text-align:right}
1744
+ .dab-slider{-webkit-appearance:none;appearance:none;flex:1;min-width:0;height:4px;border-radius:99px;outline:none;cursor:pointer;margin:5px 0;background:linear-gradient(to right,var(--dsw-alias-brand-primary) calc(var(--pct,50) * 1%),var(--dsw-alias-border-l2) calc(var(--pct,50) * 1%));transition:height .15s ease}
1745
+ .dab-slider:hover{height:5px}
1746
+ .dab-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:15px;height:15px;border-radius:50%;background:var(--dsw-alias-button-elevated-fill,#fff);border:2px solid var(--dsw-alias-brand-primary);box-shadow:0 1px 5px rgba(0,0,0,.28);transition:transform .16s cubic-bezier(.34,1.56,.64,1)}
1747
+ .dab-slider:hover::-webkit-slider-thumb,.dab-slider:focus-visible::-webkit-slider-thumb{transform:scale(1.22)}
1748
+ .dab-slider:active::-webkit-slider-thumb{transform:scale(1.34)}
1749
+ .dab-slider::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:var(--dsw-alias-button-elevated-fill,#fff);border:2px solid var(--dsw-alias-brand-primary)}
1750
+
1751
+ /* ── interface part cards ────────────────────────────────────────────────── */
1752
+ .dab-grid-parts{display:grid;grid-template-columns:repeat(auto-fill,minmax(256px,1fr));gap:13px}
1753
+ .dab-part-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}
1754
+ .dab-part-ico{width:32px;height:32px;flex:none;border-radius:10px;display:grid;place-items:center;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
1755
+ .dab-part-name{font-size:13.5px;font-weight:600}
1756
+ .dab-part-badge{margin-left:auto;font-family:var(--dab-mono);font-size:11px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2);border-radius:99px;padding:3px 9px}
1757
+
1758
+ /* ── segmented control ───────────────────────────────────────────────────── */
1759
+ .dab-seg{position:relative;display:inline-flex;padding:3px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:11px}
1760
+ .dab-seg-thumb{position:absolute;top:3px;bottom:3px;left:3px;width:var(--w,96px);border-radius:8px;background:var(--dsw-alias-button-elevated-fill);box-shadow:0 2px 8px -2px rgba(0,0,0,.28);transition:transform .32s cubic-bezier(.22,1,.36,1)}
1761
+ .dab-seg-item{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;width:var(--w,96px);height:30px;border:0;background:none;border-radius:8px;color:var(--dsw-alias-label-tertiary);font-size:12.5px;cursor:pointer;transition:color .25s ease}
1762
+ .dab-seg-item.is-active{color:var(--dsw-alias-label-primary);font-weight:600}
1763
+
1764
+ /* ── toggle switch ───────────────────────────────────────────────────────── */
1765
+ .dab-toggle{position:relative;width:42px;height:24px;flex:none;border-radius:99px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;padding:0;transition:background .28s ease,border-color .28s ease}
1766
+ .dab-toggle-knob{position:absolute;top:2.5px;left:2.5px;width:17px;height:17px;border-radius:50%;background:var(--dsw-alias-label-secondary,#999);box-shadow:0 1px 3px rgba(0,0,0,.3);transition:transform .28s cubic-bezier(.22,1,.36,1),background .28s ease}
1767
+ .dab-toggle.is-on{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
1768
+ .dab-toggle.is-on .dab-toggle-knob{transform:translateX(18px);background:#fff}
1769
+
1770
+ /* ── background preview hero ─────────────────────────────────────────────── */
1771
+ .dab-hero{position:relative;border-radius:16px;overflow:hidden;aspect-ratio:16/9;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}
1772
+ .dab-hero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:transform .5s cubic-bezier(.22,1,.36,1)}
1773
+ .dab-hero:hover .dab-hero-img{transform:scale(1.03)}
1774
+ .dab-hero-empty{position:absolute;inset:6px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--dsw-alias-label-tertiary);font-size:12.5px;border:1.5px dashed var(--dsw-alias-border-l2);border-radius:12px;cursor:pointer;background:transparent;transition:border-color .25s,color .25s,background .25s;width:auto;height:auto}
1775
+ .dab-hero-empty:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
1776
+ .dab-hero-empty.is-over{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
1777
+ .dab-hero-badge{position:absolute;top:10px;left:10px;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 11px;border-radius:99px;background:rgba(0,0,0,.45);color:#fff;font-size:11px;backdrop-filter:blur(6px);pointer-events:none}
1778
+ .dab-hero-veil{position:absolute;left:0;right:0;bottom:0;padding:34px 12px 12px;display:flex;align-items:flex-end;justify-content:flex-end;gap:8px;background:linear-gradient(to top,rgba(0,0,0,.55),rgba(0,0,0,0));opacity:0;transform:translateY(6px);transition:opacity .3s ease,transform .3s ease}
1779
+ .dab-hero:hover .dab-hero-veil{opacity:1;transform:none}
1780
+ .dab-hero-veil .dab-btn{background:rgba(255,255,255,.94);color:#14161a;border-color:transparent;height:30px;font-size:12px}
1781
+ .dab-hero-veil .dab-btn-danger{color:#dc2626}
1782
+
1783
+ /* ── generated background type cards ─────────────────────────────────────── */
1784
+ .dab-types{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
1785
+ .dab-type{position:relative;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:14px;padding:10px;cursor:pointer;text-align:left;font:inherit;transition:border-color .25s,transform .25s,box-shadow .25s}
1786
+ .dab-type:hover{transform:translateY(-2px)}
1787
+ .dab-type.is-active{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
1788
+ .dab-type-thumb{height:62px;border-radius:9px;overflow:hidden;position:relative}
1789
+ .dab-type-name{margin-top:9px;font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary)}
1790
+ .dab-type-desc{margin-top:2px;font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.4}
1791
+ .dab-type-check{position:absolute;top:16px;right:16px;width:20px;height:20px;border-radius:50%;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);display:grid;place-items:center;opacity:0;transform:scale(.4);transition:opacity .25s,transform .25s cubic-bezier(.34,1.56,.64,1)}
1792
+ .dab-type.is-active .dab-type-check{opacity:1;transform:none}
1793
+ .dab-thumb-mesh{background:radial-gradient(circle at 22% 30%,#f472b6,transparent 42%),radial-gradient(circle at 80% 22%,#38bdf8,transparent 46%),radial-gradient(circle at 52% 84%,#fbbf24,transparent 52%),#1e293b}
1794
+ .dab-thumb-shader{background:linear-gradient(120deg,#0ea5e9,#8b5cf6,#d946ef,#0ea5e9);background-size:300% 300%;animation:dab-flow 6s linear infinite}
1795
+ .dab-thumb-pattern{background-image:radial-gradient(circle,rgba(255,255,255,.92) 1.2px,transparent 1.4px);background-size:10px 10px;background-color:#334155}
1796
+
1797
+ /* ── seed row ────────────────────────────────────────────────────────────── */
1798
+ .dab-seed{display:flex;align-items:center;gap:13px;padding:12px 14px;border-radius:12px;background:var(--dsw-alias-bg-layer-2);margin-top:14px}
1799
+ .dab-seed-ico{color:var(--dsw-alias-brand-primary);display:grid;place-items:center}
1800
+ .dab-seed-txt{flex:1;min-width:0}
1801
+ .dab-seed-title{font-size:12.5px;font-weight:600;display:flex;align-items:center;gap:6px}
1802
+ .dab-seed-desc{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-top:2px}
1803
+ .dab-spin{animation:dab-rotate .7s cubic-bezier(.3,.7,.3,1) 1}
1804
+
1805
+ /* ── profile page ────────────────────────────────────────────────────────── */
1806
+ .dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(238px,1fr));gap:13px}
1807
+ .dab-profile-ico{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;margin-bottom:13px;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
1808
+ .dab-profile-title{font-size:14px;font-weight:650}
1809
+ .dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
1810
+ .dab-footer{margin-top:6px;padding:14px 4px 0;border-top:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dsw-alias-label-tertiary)}
1811
+ .dab-footer-mono{font-family:var(--dab-mono);letter-spacing:.02em}
1812
+
1813
+ /* ── toast ───────────────────────────────────────────────────────────────── */
1814
+ .dab-toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%);display:flex;align-items:center;gap:8px;height:38px;padding:0 16px;border-radius:99px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2);box-shadow:0 10px 30px -8px rgba(0,0,0,.38);font-size:12.5px;z-index:10001;animation:dab-toast-in .32s cubic-bezier(.22,1,.36,1) both}
1815
+ .dab-toast-ok{color:var(--dsw-alias-brand-primary);display:grid;place-items:center}
1816
+ .dab-toast-err{color:var(--dsw-alias-state-error-primary);display:grid;place-items:center}
1817
+
1818
+ /* ── modals (editor / eyedropper / crash) ────────────────────────────────── */
1819
+ /* Pin to the viewport explicitly with vw/vh so ancestor padding/margins on
1820
+ * body cannot shift or clip the overlay; keep it above host sidebar chrome.
1821
+ * pointer-events:auto re-enables interaction: these overlays render inside a
1822
+ * Portal root that is pointer-events:none so it never blocks the page alone. */
1823
+ .dab-overlay{position:fixed;left:0;top:0;width:100vw;height:100vh;z-index:999999;background:rgba(8,10,14,.62);backdrop-filter:blur(8px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;animation:dab-fade-in .25s ease both;pointer-events:auto}
1824
+ .dab-overlay-title{color:#fff;font-size:15px;font-weight:600}
1825
+ .dab-overlay-hint{color:rgba(255,255,255,.62);font-size:12px}
1826
+ .dab-modal-card{animation:dab-zoom-in .3s cubic-bezier(.22,1,.36,1) both;max-width:calc(100vw - 40px);max-height:calc(100vh - 120px);overflow:auto}
1827
+ .dab-overlay .dab-btn{background:rgba(255,255,255,.94);color:#14161a;border-color:transparent}
1828
+ .dab-overlay .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text)}
1829
+ .dab-crash{display:flex;flex-direction:column;gap:10px;align-items:center;padding:28px 18px;border:1px solid var(--dsw-alias-border-l2);border-radius:16px}
1830
+ .dab-crash-title{font-size:15px;font-weight:650}
1831
+ .dab-crash-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.5}
1832
+
1833
+ /* ── keyframes ───────────────────────────────────────────────────────────── */
1834
+ @keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
1835
+ @keyframes dab-page-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
1836
+ @keyframes dab-rise-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
1837
+ @keyframes dab-orb-in{from{opacity:0;transform:scale(.82)}to{opacity:1;transform:none}}
1838
+ @keyframes dab-spin{to{transform:rotate(360deg)}}
1839
+ @keyframes dab-rotate{to{transform:rotate(360deg)}}
1840
+ @keyframes dab-toast-in{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}
1841
+ @keyframes dab-zoom-in{from{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:none}}
1842
+ @keyframes dab-flow{to{background-position:300% 50%}}
1843
+
1844
+ /* ── responsive & motion preferences ─────────────────────────────────────── */
1845
+ @container (max-width:620px){
1846
+ .dab-shell{grid-template-columns:1fr;gap:14px}
1847
+ .dab-nav{position:static;flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
1848
+ .dab-nav-list{flex-direction:row;overflow-x:auto;scrollbar-width:none}
1849
+ .dab-nav-list::-webkit-scrollbar{display:none}
1850
+ .dab-nav-ind{display:none}
1851
+ .dab-nav-item{flex:none;padding:0 10px}
1852
+ .dab-nav-item.is-active{background:var(--dsw-alias-bg-layer-2)}
1853
+ .dab-types{grid-template-columns:1fr}
1854
+ }
1855
+ @container (min-width:621px){
1856
+ .dab-shell{grid-template-columns:158px minmax(0,1fr);gap:26px}
1857
+ }
1858
+ @media (prefers-reduced-motion:reduce){
1859
+ .dab-root *,.dab-root *::before,.dab-root *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}
1860
+ }
1861
+ `;
1862
+ const CSS_ID = "dab-ui-css";
1863
+ /** Inject the design-system stylesheet once per document (HMR-safe). */
1864
+ function ensureUiCss() {
1865
+ if (typeof document === "undefined") return;
1866
+ let el = document.getElementById(CSS_ID);
1867
+ if (!el) {
1868
+ el = document.createElement("style");
1869
+ el.id = CSS_ID;
1870
+ document.head.appendChild(el);
980
1871
  }
981
- };
1872
+ if (el.textContent !== UI_CSS) el.textContent = UI_CSS;
1873
+ }
982
1874
  //#endregion
983
- //#region src/client/utils/image.ts
984
- /**
985
- * Read a chosen image file as a data URL WITHOUT re-encoding: the original
986
- * pixels are kept as-is (no canvas downscale / JPEG re-compression), so the
987
- * wallpaper is stored and displayed at full fidelity. The tradeoff is a larger
988
- * payload over the RPC channel and on disk for big images.
989
- */
990
- function readImg(file, cb) {
991
- const r = new FileReader();
992
- r.onerror = () => cb(null);
993
- r.onload = () => cb(r.result);
994
- r.readAsDataURL(file);
1875
+ //#region src/client/components/icons.tsx
1876
+ /** Sun glyph, migrated from @deepseek-ai/dsh-client-ui-primitives IconLightOutline16. */
1877
+ const SUN_PATHS = "<path d=\"M11.3496 8C11.3496 6.14985 9.85015 4.65039 8 4.65039C6.14985 4.65039 4.65039 6.14985 4.65039 8C4.65039 9.85015 6.14985 11.3496 8 11.3496C9.85015 11.3496 11.3496 9.85015 11.3496 8ZM12.6504 8C12.6504 10.5681 10.5681 12.6504 8 12.6504C5.43188 12.6504 3.34961 10.5681 3.34961 8C3.34961 5.43188 5.43188 3.34961 8 3.34961C10.5681 3.34961 12.6504 5.43188 12.6504 8Z\" fill=\"currentColor\"/><path d=\"M8.65039 0.5V2.5H7.34961V0.5H8.65039Z\" fill=\"currentColor\"/><path d=\"M8.65039 13.5V15.5H7.34961V13.5H8.65039Z\" fill=\"currentColor\"/><path d=\"M3.15808 2.24035L4.57229 3.65456L3.6525 4.57435L2.23829 3.16014L3.15808 2.24035Z\" fill=\"currentColor\"/><path d=\"M12.3505 11.4327L13.7647 12.8469L12.8449 13.7667L11.4307 12.3525L12.3505 11.4327Z\" fill=\"currentColor\"/><path d=\"M2.24537 12.8469L3.65958 11.4327L4.57937 12.3525L3.16516 13.7667L2.24537 12.8469Z\" fill=\"currentColor\"/><path d=\"M11.4377 3.65455L12.852 2.24033L13.7718 3.16012L12.3575 4.57434L11.4377 3.65455Z\" fill=\"currentColor\"/><path d=\"M0.5 7.35461H2.5V8.6554H0.5L0.5 7.35461Z\" fill=\"currentColor\"/><path d=\"M13.5 7.35461H15.5V8.6554H13.5V8.6554Z\" fill=\"currentColor\"/>";
1878
+ function SunIcon({ size = 16, className }) {
1879
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1880
+ width: size,
1881
+ height: size,
1882
+ className,
1883
+ viewBox: "0 0 16 16",
1884
+ fill: "none",
1885
+ xmlns: "http://www.w3.org/2000/svg",
1886
+ dangerouslySetInnerHTML: { __html: SUN_PATHS }
1887
+ });
1888
+ }
1889
+ function Glyph({ children, size = 16, className }) {
1890
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1891
+ width: size,
1892
+ height: size,
1893
+ className,
1894
+ viewBox: "0 0 16 16",
1895
+ fill: "none",
1896
+ stroke: "currentColor",
1897
+ strokeWidth: 1.5,
1898
+ strokeLinecap: "round",
1899
+ strokeLinejoin: "round",
1900
+ "aria-hidden": "true",
1901
+ children
1902
+ });
995
1903
  }
1904
+ const DropletIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
1905
+ size,
1906
+ className,
1907
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 1.9c2.4 2.8 4.3 5 4.3 7.1a4.3 4.3 0 1 1-8.6 0C3.7 6.9 5.6 4.7 8 1.9z" })
1908
+ });
1909
+ const LayersIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1910
+ size,
1911
+ className,
1912
+ children: [
1913
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 2.2 13.2 5 8 7.8 2.8 5 8 2.2z" }),
1914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.8 8.2 8 11l5.2-2.8" }),
1915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.8 11.2 8 14l5.2-2.8" })
1916
+ ]
1917
+ });
1918
+ const PhotoIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1919
+ size,
1920
+ className,
1921
+ children: [
1922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1923
+ x: "2",
1924
+ y: "3.2",
1925
+ width: "12",
1926
+ height: "9.6",
1927
+ rx: "2"
1928
+ }),
1929
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1930
+ cx: "5.7",
1931
+ cy: "6.3",
1932
+ r: "0.9"
1933
+ }),
1934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 10.4l-2.8-2.8-4.8 4.8" })
1935
+ ]
1936
+ });
1937
+ const SlidersIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1938
+ size,
1939
+ className,
1940
+ children: [
1941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 4.5h4.9M11.6 4.5h1.9M2.5 11.5h1.9M8.6 11.5h4.9" }),
1942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1943
+ cx: "9.5",
1944
+ cy: "4.5",
1945
+ r: "1.7"
1946
+ }),
1947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1948
+ cx: "5.5",
1949
+ cy: "11.5",
1950
+ r: "1.7"
1951
+ })
1952
+ ]
1953
+ });
1954
+ const SparkleIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1955
+ size,
1956
+ className,
1957
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7.2 1.8l1.2 3.1 3.1 1.2-3.1 1.2-1.2 3.1-1.2-3.1L2.9 6.1 6 4.9l1.2-3.1z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 10.5l.5 1.2 1.2.5-1.2.5-.5 1.2-.5-1.2-1.2-.5 1.2-.5.5-1.2z" })]
1958
+ });
1959
+ const RefreshIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1960
+ size,
1961
+ className,
1962
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.2 8A5.2 5.2 0 1 1 11.5 4.2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.4 1.8v2.9h-2.9" })]
1963
+ });
1964
+ const DownloadIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1965
+ size,
1966
+ className,
1967
+ children: [
1968
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 2.2v8" }),
1969
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.6 7l3.4 3.4L11.4 7" }),
1970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.8 13.8h10.4" })
1971
+ ]
1972
+ });
1973
+ const UploadIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1974
+ size,
1975
+ className,
1976
+ children: [
1977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 10.4V2.6" }),
1978
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.6 5.8L8 2.4l3.4 3.4" }),
1979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.8 13.8h10.4" })
1980
+ ]
1981
+ });
1982
+ const LockIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1983
+ size,
1984
+ className,
1985
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1986
+ x: "3.6",
1987
+ y: "7.2",
1988
+ width: "8.8",
1989
+ height: "6",
1990
+ rx: "1.4"
1991
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.6 7.2V5.4a2.4 2.4 0 0 1 4.8 0v1.8" })]
1992
+ });
1993
+ const TrashIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1994
+ size,
1995
+ className,
1996
+ children: [
1997
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.8 4.4h10.4" }),
1998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6.4 4.4V2.9h3.2v1.5" }),
1999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.4 4.4l.5 8.6h6.2l.5-8.6" })
2000
+ ]
2001
+ });
2002
+ const EditIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2003
+ size,
2004
+ className,
2005
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M9.5 4l2.5 2.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 13l.8-3L10 3.8 12.2 6 6 12.2 3 13z" })]
2006
+ });
2007
+ const PipetteIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2008
+ size,
2009
+ className,
2010
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2011
+ cx: "8",
2012
+ cy: "8",
2013
+ r: "4.2"
2014
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 1.6v2.6M8 11.8v2.6M1.6 8h2.6M11.8 8h2.6" })]
2015
+ });
2016
+ const CheckIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
2017
+ size,
2018
+ className,
2019
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3.2 8.6l3 3L12.8 5" })
2020
+ });
2021
+ const AlertIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2022
+ size,
2023
+ className,
2024
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 2.6l5.4 9.8H2.6L8 2.6z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.8v2.4M8 11.4v.01" })]
2025
+ });
2026
+ const CanvasIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
2027
+ size,
2028
+ className,
2029
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
2030
+ x: "2.2",
2031
+ y: "2.2",
2032
+ width: "11.6",
2033
+ height: "11.6",
2034
+ rx: "2"
2035
+ })
2036
+ });
2037
+ const SidebarIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2038
+ size,
2039
+ className,
2040
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
2041
+ x: "2.2",
2042
+ y: "2.2",
2043
+ width: "11.6",
2044
+ height: "11.6",
2045
+ rx: "2"
2046
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6.6 2.2v11.6" })]
2047
+ });
2048
+ const ChatIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
2049
+ size,
2050
+ className,
2051
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.4 4.4v4.4a2 2 0 0 1-2 2H6.2l-3.6 3V4.4a2 2 0 0 1 2-2h6.8a2 2 0 0 1 2 2z" })
2052
+ });
2053
+ const GearIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2054
+ size,
2055
+ className,
2056
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2057
+ cx: "8",
2058
+ cy: "8",
2059
+ r: "2.1"
2060
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 1.7v1.8M8 12.5v1.8M1.7 8h1.8M12.5 8h1.8M3.6 3.6l1.3 1.3M11.1 11.1l1.3 1.3M12.4 3.6l-1.3 1.3M4.9 11.1l-1.3 1.3" })]
2061
+ });
996
2062
  //#endregion
997
- //#region src/client/components/ColorWheel.tsx
998
- const WHEEL_SIZE = 220;
999
- const CX = WHEEL_SIZE / 2;
1000
- const RING_OUTER = 106;
1001
- const RING_INNER = 82;
1002
- const SQ_HALF = Math.round(RING_INNER / Math.SQRT2);
1003
- function drawWheel(cvs, hue, sat, lit) {
1004
- const c = cvs.getContext("2d");
1005
- c.clearRect(0, 0, WHEEL_SIZE, WHEEL_SIZE);
1006
- for (let a = 0; a < 360; a++) {
1007
- const r1 = (a - 90) * Math.PI / 180;
1008
- const r2 = (a + 1.5 - 90) * Math.PI / 180;
1009
- c.beginPath();
1010
- c.arc(CX, CX, RING_OUTER, r1, r2);
1011
- c.arc(CX, CX, RING_INNER, r2, r1, true);
1012
- c.closePath();
1013
- c.fillStyle = `hsl(${a},100%,50%)`;
1014
- c.fill();
1015
- }
1016
- const gx = CX - SQ_HALF, gy = CX - SQ_HALF, sz = SQ_HALF * 2;
1017
- c.fillStyle = "#fff";
1018
- c.fillRect(gx, gy, sz, sz);
1019
- const gh = c.createLinearGradient(gx, 0, gx + sz, 0);
1020
- gh.addColorStop(0, "rgba(255,255,255,1)");
1021
- gh.addColorStop(1, `hsl(${hue},100%,50%)`);
1022
- c.fillStyle = gh;
1023
- c.fillRect(gx, gy, sz, sz);
2063
+ //#region src/client/components/ErrorBoundary.tsx
2064
+ /**
2065
+ * Catches render errors from the theme section subtree (wheel, sliders, editor)
2066
+ * so a single bad state can never take down the whole settings panel. Shows a
2067
+ * compact fallback with a reset button; the reset re-renders the section with
2068
+ * the current saved config, which is enough to recover from most transient
2069
+ * failures (corrupt transient UI state, stale image decode, etc.).
2070
+ */
2071
+ var ErrorBoundary = class extends react.Component {
2072
+ state = { error: null };
2073
+ static getDerivedStateFromError(error) {
2074
+ return { error };
2075
+ }
2076
+ componentDidCatch(error, info) {
2077
+ console.error("dsh-any-background: section render crashed", error, info.componentStack);
2078
+ }
2079
+ reset = () => {
2080
+ this.setState({ error: null });
2081
+ this.props.onReset?.();
2082
+ };
2083
+ render() {
2084
+ if (this.state.error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2085
+ className: "dab-crash",
2086
+ children: [
2087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2088
+ className: "dab-crash-title",
2089
+ children: this.props.t("crashTitle")
2090
+ }),
2091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2092
+ className: "dab-crash-desc",
2093
+ children: this.props.t("crashDesc")
2094
+ }),
2095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2096
+ type: "button",
2097
+ className: "dab-btn",
2098
+ onClick: this.reset,
2099
+ children: this.props.t("crashReset")
2100
+ })
2101
+ ]
2102
+ });
2103
+ return this.props.children;
2104
+ }
2105
+ };
2106
+ //#endregion
2107
+ //#region src/client/components/Portal.tsx
2108
+ /**
2109
+ * Render children into a fixed root attached to document.documentElement.
2110
+ *
2111
+ * The host's sidebar is often implemented by translating the body or a wrapper
2112
+ * (margin-left / transform). A fixed element portaled to body would still be
2113
+ * captured by that transformed ancestor and shift with the sidebar. Attaching
2114
+ * the portal root directly to <html> escapes body-level transforms so the
2115
+ * overlay is always painted relative to the viewport and centered correctly.
2116
+ */
2117
+ function Portal({ children }) {
2118
+ const [target, setTarget] = (0, react.useState)(null);
2119
+ (0, react.useEffect)(() => {
2120
+ const el = document.createElement("div");
2121
+ el.dataset.dabPortal = "1";
2122
+ el.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:999999";
2123
+ document.documentElement.appendChild(el);
2124
+ setTarget(el);
2125
+ return () => {
2126
+ el.remove();
2127
+ };
2128
+ }, []);
2129
+ return target ? (0, react_dom.createPortal)(children, target) : null;
2130
+ }
2131
+ //#endregion
2132
+ //#region src/client/components/ColorWheel.tsx
2133
+ const WHEEL_SIZE = 220;
2134
+ const CX = WHEEL_SIZE / 2;
2135
+ const RING_OUTER = 106;
2136
+ const RING_INNER = 82;
2137
+ const SQ_HALF = RING_INNER / Math.SQRT2;
2138
+ /** Static hue ring cached once across all wheels. */
2139
+ let ringCache = null;
2140
+ function getRingCache() {
2141
+ if (ringCache) return ringCache;
2142
+ const cvs = document.createElement("canvas");
2143
+ cvs.width = WHEEL_SIZE;
2144
+ cvs.height = WHEEL_SIZE;
2145
+ const c = cvs.getContext("2d");
2146
+ const g = c.createConicGradient(0, CX, CX);
2147
+ for (let i = 0; i <= 360; i++) g.addColorStop(i / 360, `hsl(${i},100%,50%)`);
2148
+ c.beginPath();
2149
+ c.arc(CX, CX, RING_OUTER, 0, Math.PI * 2);
2150
+ c.arc(CX, CX, RING_INNER, 0, Math.PI * 2, true);
2151
+ c.fillStyle = g;
2152
+ c.fill();
2153
+ ringCache = cvs;
2154
+ return ringCache;
2155
+ }
2156
+ function drawMarkers(ctx, hue, sat, lit) {
2157
+ const hRad = hue * Math.PI / 180;
2158
+ const hR = 94;
2159
+ const hmx = CX + Math.cos(hRad) * hR;
2160
+ const hmy = CX + Math.sin(hRad) * hR;
2161
+ ctx.beginPath();
2162
+ ctx.arc(hmx, hmy, 8, 0, Math.PI * 2);
2163
+ ctx.fillStyle = "rgba(0,0,0,0.25)";
2164
+ ctx.fill();
2165
+ ctx.beginPath();
2166
+ ctx.arc(hmx, hmy, 6.5, 0, Math.PI * 2);
2167
+ ctx.strokeStyle = "#fff";
2168
+ ctx.lineWidth = 2;
2169
+ ctx.stroke();
2170
+ const gx = CX - SQ_HALF, gy = CX - SQ_HALF, sz = SQ_HALF * 2;
2171
+ const smx = gx + sat * sz;
2172
+ const smy = gy + (1 - lit) * sz;
2173
+ ctx.beginPath();
2174
+ ctx.arc(smx, smy, 7, 0, Math.PI * 2);
2175
+ ctx.fillStyle = "rgba(0,0,0,0.25)";
2176
+ ctx.fill();
2177
+ ctx.beginPath();
2178
+ ctx.arc(smx, smy, 5.5, 0, Math.PI * 2);
2179
+ ctx.strokeStyle = "#fff";
2180
+ ctx.lineWidth = 2;
2181
+ ctx.stroke();
2182
+ ctx.beginPath();
2183
+ ctx.arc(smx, smy, 3.5, 0, Math.PI * 2);
2184
+ ctx.strokeStyle = "#000";
2185
+ ctx.lineWidth = 1;
2186
+ ctx.stroke();
2187
+ }
2188
+ function drawSquare(c, hue) {
2189
+ const gx = CX - SQ_HALF, gy = CX - SQ_HALF, sz = SQ_HALF * 2;
2190
+ c.clearRect(gx - 1, gy - 1, sz + 2, sz + 2);
2191
+ c.fillStyle = "#fff";
2192
+ c.fillRect(gx, gy, sz, sz);
2193
+ const gh = c.createLinearGradient(gx, 0, gx + sz, 0);
2194
+ gh.addColorStop(0, "rgba(255,255,255,1)");
2195
+ gh.addColorStop(1, `hsl(${hue},100%,50%)`);
2196
+ c.fillStyle = gh;
2197
+ c.fillRect(gx, gy, sz, sz);
1024
2198
  const gv = c.createLinearGradient(0, gy, 0, gy + sz);
1025
2199
  gv.addColorStop(0, "rgba(0,0,0,0)");
1026
2200
  gv.addColorStop(1, "rgba(0,0,0,1)");
1027
2201
  c.fillStyle = gv;
1028
2202
  c.fillRect(gx, gy, sz, sz);
1029
- const hRad = (hue - 90) * Math.PI / 180;
1030
- const hR = 94;
1031
- const hmx = CX + Math.cos(hRad) * hR, hmy = CX + Math.sin(hRad) * hR;
1032
- c.beginPath();
1033
- c.arc(hmx, hmy, 8, 0, Math.PI * 2);
1034
- c.fillStyle = "rgba(0,0,0,0.25)";
1035
- c.fill();
1036
- c.beginPath();
1037
- c.arc(hmx, hmy, 6.5, 0, Math.PI * 2);
1038
- c.strokeStyle = "#fff";
1039
- c.lineWidth = 2;
1040
- c.stroke();
1041
- const smx = gx + sat * sz, smy = gy + (1 - lit) * sz;
1042
- c.beginPath();
1043
- c.arc(smx, smy, 7, 0, Math.PI * 2);
1044
- c.fillStyle = "rgba(0,0,0,0.25)";
1045
- c.fill();
1046
- c.beginPath();
1047
- c.arc(smx, smy, 5.5, 0, Math.PI * 2);
1048
- c.strokeStyle = "#fff";
1049
- c.lineWidth = 2;
1050
- c.stroke();
1051
- c.beginPath();
1052
- c.arc(smx, smy, 3.5, 0, Math.PI * 2);
1053
- c.strokeStyle = "#000";
1054
- c.lineWidth = 1;
1055
- c.stroke();
1056
2203
  }
1057
2204
  function hitTest(x, y) {
1058
2205
  if (Math.abs(x - CX) <= SQ_HALF && Math.abs(y - CX) <= SQ_HALF) return "square";
@@ -1062,7 +2209,7 @@ window.__ModuleLoader__.load({
1062
2209
  return null;
1063
2210
  }
1064
2211
  function pickHue(x, y) {
1065
- let angle = Math.atan2(y - CX, x - CX) * 180 / Math.PI + 90;
2212
+ let angle = Math.atan2(y - CX, x - CX) * 180 / Math.PI;
1066
2213
  if (angle < 0) angle += 360;
1067
2214
  return angle;
1068
2215
  }
@@ -1070,7 +2217,7 @@ window.__ModuleLoader__.load({
1070
2217
  const gx = CX - SQ_HALF, gy = CX - SQ_HALF, sz = SQ_HALF * 2;
1071
2218
  return [Math.max(0, Math.min(1, (x - gx) / sz)), Math.max(.02, Math.min(.98, 1 - (y - gy) / sz))];
1072
2219
  }
1073
- function ColorWheel({ hue, sat, lit, onChange }) {
2220
+ const ColorWheel = (0, react.memo)(function ColorWheel({ hue, sat, lit, onChange }) {
1074
2221
  const cvsRef = (0, react.useRef)(null);
1075
2222
  const [col, setCol] = (0, react.useState)({
1076
2223
  hue,
@@ -1091,35 +2238,55 @@ window.__ModuleLoader__.load({
1091
2238
  lit
1092
2239
  ]);
1093
2240
  (0, react.useEffect)(() => {
1094
- if (cvsRef.current) drawWheel(cvsRef.current, col.hue, col.sat, col.lit);
2241
+ const cvs = cvsRef.current;
2242
+ if (!cvs) return;
2243
+ const ctx = cvs.getContext("2d");
2244
+ ctx.clearRect(0, 0, WHEEL_SIZE, WHEEL_SIZE);
2245
+ drawSquare(ctx, col.hue);
2246
+ ctx.drawImage(getRingCache(), 0, 0);
2247
+ drawMarkers(ctx, col.hue, col.sat, col.lit);
1095
2248
  }, [col]);
1096
- const apply = (0, react.useCallback)((nh, ns, nl) => {
2249
+ const pendingRef = (0, react.useRef)(null);
2250
+ const rafRef = (0, react.useRef)(null);
2251
+ const flushPending = (0, react.useCallback)(() => {
2252
+ rafRef.current = null;
2253
+ const p = pendingRef.current;
2254
+ if (!p) return;
2255
+ pendingRef.current = null;
1097
2256
  setCol({
1098
- hue: nh,
1099
- sat: ns,
1100
- lit: nl
2257
+ hue: p.h,
2258
+ sat: p.s,
2259
+ lit: p.l
1101
2260
  });
1102
- onChange(nh, ns, nl);
2261
+ onChange(p.h, p.s, p.l);
1103
2262
  }, [onChange]);
2263
+ const schedule = (0, react.useCallback)((h, s, l) => {
2264
+ pendingRef.current = {
2265
+ h,
2266
+ s,
2267
+ l
2268
+ };
2269
+ if (rafRef.current === null) rafRef.current = requestAnimationFrame(flushPending);
2270
+ }, [flushPending]);
1104
2271
  const onDown = (0, react.useCallback)((e) => {
1105
2272
  const r = cvsRef.current.getBoundingClientRect();
1106
2273
  const x = e.clientX - r.left, y = e.clientY - r.top;
1107
2274
  const region = hitTest(x, y);
1108
2275
  if (!region) return;
1109
- if (region === "ring") apply(pickHue(x, y), colRef.current.sat, colRef.current.lit);
2276
+ if (region === "ring") schedule(pickHue(x, y), colRef.current.sat, colRef.current.lit);
1110
2277
  else {
1111
2278
  const [s, l] = pickSL(x, y);
1112
- apply(colRef.current.hue, s, l);
2279
+ schedule(colRef.current.hue, s, l);
1113
2280
  }
1114
2281
  const onMove = (ev) => {
1115
2282
  const rr = cvsRef.current.getBoundingClientRect();
1116
2283
  const mx = ev.clientX - rr.left, my = ev.clientY - rr.top;
1117
2284
  if (region === "ring") {
1118
2285
  const d = Math.sqrt((mx - CX) ** 2 + (my - CX) ** 2);
1119
- if (d >= 72 && d <= 116) apply(pickHue(mx, my), colRef.current.sat, colRef.current.lit);
2286
+ if (d >= 72 && d <= 116) schedule(pickHue(mx, my), colRef.current.sat, colRef.current.lit);
1120
2287
  } else {
1121
2288
  const [s, l] = pickSL(mx, my);
1122
- apply(colRef.current.hue, s, l);
2289
+ schedule(colRef.current.hue, s, l);
1123
2290
  }
1124
2291
  };
1125
2292
  const onUp = () => {
@@ -1128,17 +2295,18 @@ window.__ModuleLoader__.load({
1128
2295
  };
1129
2296
  document.addEventListener("mousemove", onMove);
1130
2297
  document.addEventListener("mouseup", onUp);
1131
- }, [apply]);
2298
+ }, [schedule]);
1132
2299
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
1133
2300
  ref: cvsRef,
1134
2301
  width: WHEEL_SIZE,
1135
2302
  height: WHEEL_SIZE,
1136
- style: ST.wheelCanvas,
2303
+ className: "dab-wheel",
1137
2304
  onMouseDown: onDown
1138
2305
  });
1139
- }
2306
+ });
1140
2307
  //#endregion
1141
2308
  //#region src/client/components/ColorInputs.tsx
2309
+ const SEG_W$1 = 66;
1142
2310
  function clamp(v, min, max) {
1143
2311
  return Math.min(max, Math.max(min, v));
1144
2312
  }
@@ -1156,9 +2324,9 @@ window.__ModuleLoader__.load({
1156
2324
  if (!focused.current) setText(String(value));
1157
2325
  }, [value]);
1158
2326
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1159
- style: ST.inputField,
2327
+ className: "dab-field",
1160
2328
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1161
- style: ST.inputLabel,
2329
+ className: "dab-field-label",
1162
2330
  children: label
1163
2331
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1164
2332
  type: "number",
@@ -1166,7 +2334,7 @@ window.__ModuleLoader__.load({
1166
2334
  max,
1167
2335
  step,
1168
2336
  value: text,
1169
- style: ST.input,
2337
+ className: "dab-num",
1170
2338
  onFocus: () => {
1171
2339
  focused.current = true;
1172
2340
  },
@@ -1183,7 +2351,7 @@ window.__ModuleLoader__.load({
1183
2351
  });
1184
2352
  }
1185
2353
  /**
1186
- * Precise color entry next to the wheel: a HSL/RGB mode toggle plus three
2354
+ * Precise color entry next to the wheel: a HSL/RGB segmented toggle plus three
1187
2355
  * numeric channel fields and a live swatch. The wheel is HSV end-to-end, so
1188
2356
  * this panel converts at the boundary — HSL fields map straight onto the
1189
2357
  * stored HSL, RGB fields round-trip through rgbToHsl — and both emit HSV via
@@ -1199,30 +2367,36 @@ window.__ModuleLoader__.load({
1199
2367
  onChange(...hslToHsv(nh, ns, nl));
1200
2368
  };
1201
2369
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1202
- style: ST.inputPanel,
2370
+ className: "dab-inputs",
1203
2371
  children: [
1204
2372
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1205
- style: ST.inputTabs,
1206
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1207
- type: "button",
1208
- style: {
1209
- ...ST.inputTab,
1210
- ...mode === "hsl" ? ST.inputTabActive : {}
1211
- },
1212
- onClick: () => setMode("hsl"),
1213
- children: "HSL"
1214
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1215
- type: "button",
1216
- style: {
1217
- ...ST.inputTab,
1218
- ...mode === "rgb" ? ST.inputTabActive : {}
1219
- },
1220
- onClick: () => setMode("rgb"),
1221
- children: "RGB"
1222
- })]
2373
+ className: "dab-seg",
2374
+ style: { "--w": `${SEG_W$1}px` },
2375
+ children: [
2376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2377
+ className: "dab-seg-thumb",
2378
+ style: { transform: `translateX(${mode === "hsl" ? 0 : SEG_W$1}px)` }
2379
+ }),
2380
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2381
+ type: "button",
2382
+ className: `dab-seg-item${mode === "hsl" ? " is-active" : ""}`,
2383
+ onClick: () => setMode("hsl"),
2384
+ children: "HSL"
2385
+ }),
2386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2387
+ type: "button",
2388
+ className: `dab-seg-item${mode === "rgb" ? " is-active" : ""}`,
2389
+ onClick: () => setMode("rgb"),
2390
+ children: "RGB"
2391
+ })
2392
+ ]
1223
2393
  }),
1224
2394
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1225
- style: ST.inputGrid,
2395
+ style: {
2396
+ display: "flex",
2397
+ flexDirection: "column",
2398
+ gap: 7
2399
+ },
1226
2400
  children: mode === "hsl" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1227
2401
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumField, {
1228
2402
  label: "H",
@@ -1275,10 +2449,10 @@ window.__ModuleLoader__.load({
1275
2449
  })
1276
2450
  ] })
1277
2451
  }),
1278
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1279
- ...ST.swatch,
1280
- background: `hsl(${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%)`
1281
- } })
2452
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2453
+ className: "dab-swatch-lg",
2454
+ style: { background: `hsl(${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%)` }
2455
+ })
1282
2456
  ]
1283
2457
  });
1284
2458
  }
@@ -1286,7 +2460,7 @@ window.__ModuleLoader__.load({
1286
2460
  //#region src/client/components/ColorPicker.tsx
1287
2461
  const MAG_SIZE = 96;
1288
2462
  const MAG_ZOOM = 8;
1289
- function toHex(rgb) {
2463
+ function toHex$1(rgb) {
1290
2464
  return "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join("");
1291
2465
  }
1292
2466
  /**
@@ -1383,22 +2557,36 @@ window.__ModuleLoader__.load({
1383
2557
  const [h, sl, l] = rgbToHsl(s.rgb[0], s.rgb[1], s.rgb[2]);
1384
2558
  onPick(hslToHsv(h, sl, l));
1385
2559
  };
1386
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1387
- style: ST.overlay,
2560
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2561
+ className: "dab-overlay",
1388
2562
  children: [
1389
2563
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1390
- style: ST.modalTitle,
2564
+ className: "dab-overlay-title",
1391
2565
  children: t("pickerTitle")
1392
2566
  }),
1393
2567
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1394
- style: ST.modalHint,
2568
+ className: "dab-overlay-hint",
1395
2569
  children: t("pickerHint")
1396
2570
  }),
1397
2571
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1398
- style: ST.pickerBox,
2572
+ className: "dab-modal-card",
2573
+ style: {
2574
+ maxWidth: "min(90vw, 720px)",
2575
+ maxHeight: "60vh",
2576
+ overflow: "hidden",
2577
+ borderRadius: 12,
2578
+ border: "2px solid rgba(255,255,255,0.3)",
2579
+ background: "#000",
2580
+ cursor: "crosshair"
2581
+ },
1399
2582
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
1400
2583
  ref: canvasRef,
1401
- style: ST.pickerCanvas,
2584
+ style: {
2585
+ display: "block",
2586
+ maxWidth: "100%",
2587
+ maxHeight: "60vh",
2588
+ objectFit: "contain"
2589
+ },
1402
2590
  onMouseMove: onMove,
1403
2591
  onMouseLeave: () => {
1404
2592
  setHover(null);
@@ -1408,20 +2596,35 @@ window.__ModuleLoader__.load({
1408
2596
  })
1409
2597
  }),
1410
2598
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1411
- style: ST.pickerBar,
2599
+ style: {
2600
+ display: "flex",
2601
+ alignItems: "center",
2602
+ gap: 10,
2603
+ minWidth: 260
2604
+ },
1412
2605
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1413
- ...ST.pickerSwatch,
1414
- background: hover ? toHex(hover.rgb) : "transparent"
2606
+ width: 28,
2607
+ height: 28,
2608
+ borderRadius: 8,
2609
+ border: "1px solid rgba(255,255,255,0.4)",
2610
+ background: hover ? toHex$1(hover.rgb) : "transparent"
1415
2611
  } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1416
- style: ST.pickerValue,
1417
- children: hover ? `${toHex(hover.rgb)} · rgb(${hover.rgb.join(", ")})` : "—"
2612
+ style: {
2613
+ color: "#fff",
2614
+ fontSize: 13,
2615
+ fontFamily: "var(--dab-mono, monospace)"
2616
+ },
2617
+ children: hover ? `${toHex$1(hover.rgb)} · rgb(${hover.rgb.join(", ")})` : "—"
1418
2618
  })]
1419
2619
  }),
1420
2620
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1421
- style: ST.modalBtns,
2621
+ style: {
2622
+ display: "flex",
2623
+ gap: 10
2624
+ },
1422
2625
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1423
2626
  type: "button",
1424
- style: ST.btn,
2627
+ className: "dab-btn",
1425
2628
  onClick: onClose,
1426
2629
  children: t("pickerClose")
1427
2630
  })
@@ -1431,16 +2634,447 @@ window.__ModuleLoader__.load({
1431
2634
  width: MAG_SIZE,
1432
2635
  height: MAG_SIZE,
1433
2636
  style: {
1434
- ...ST.magnifier,
2637
+ position: "fixed",
2638
+ width: MAG_SIZE,
2639
+ height: MAG_SIZE,
2640
+ borderRadius: "50%",
2641
+ border: "2px solid rgba(255,255,255,0.7)",
2642
+ boxShadow: "0 4px 16px rgba(0,0,0,0.5)",
2643
+ zIndex: 1e4,
2644
+ background: "#000",
2645
+ pointerEvents: "none",
2646
+ transition: "opacity 0.08s",
1435
2647
  left: mag?.x ?? 0,
1436
2648
  top: mag?.y ?? 0,
1437
2649
  opacity: mag ? 1 : 0
1438
2650
  }
1439
2651
  })
1440
2652
  ]
2653
+ }) });
2654
+ }
2655
+ //#endregion
2656
+ //#region src/client/components/pages/ColorPage.tsx
2657
+ /** Curated quick-pick hues (HSL, s .72 l .55). */
2658
+ const SWATCHES = [
2659
+ [
2660
+ 356,
2661
+ .72,
2662
+ .55
2663
+ ],
2664
+ [
2665
+ 24,
2666
+ .78,
2667
+ .55
2668
+ ],
2669
+ [
2670
+ 44,
2671
+ .8,
2672
+ .55
2673
+ ],
2674
+ [
2675
+ 152,
2676
+ .62,
2677
+ .5
2678
+ ],
2679
+ [
2680
+ 174,
2681
+ .68,
2682
+ .48
2683
+ ],
2684
+ [
2685
+ 208,
2686
+ .72,
2687
+ .55
2688
+ ],
2689
+ [
2690
+ 252,
2691
+ .68,
2692
+ .6
2693
+ ],
2694
+ [
2695
+ 300,
2696
+ .64,
2697
+ .58
2698
+ ]
2699
+ ];
2700
+ function toHex(rgb) {
2701
+ return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
2702
+ }
2703
+ function ColorPage({ p, notify }) {
2704
+ const { t, hue, sat, lit, setColor, extractColor, useStore } = p;
2705
+ const store = useStore((s) => s);
2706
+ const storeUrl = store.url;
2707
+ const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
2708
+ const [extracting, setExtracting] = (0, react.useState)(false);
2709
+ const wheel = store.color ?? [
2710
+ hue,
2711
+ sat,
2712
+ lit
2713
+ ];
2714
+ const [h, s, l] = hsvToHsl(wheel[0], wheel[1], wheel[2]);
2715
+ const [r, g, b] = hslToRgb(h, s, l);
2716
+ const hex = toHex([
2717
+ r,
2718
+ g,
2719
+ b
2720
+ ]);
2721
+ const orbVars = {
2722
+ "--c": hex,
2723
+ "--c-soft": `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},0.5)`
2724
+ };
2725
+ const onExtract = async () => {
2726
+ if (!storeUrl || extracting) return;
2727
+ setExtracting(true);
2728
+ try {
2729
+ const ok = await extractColor();
2730
+ notify(ok ? t("extractDone") : t("extractFail"), ok);
2731
+ } catch {
2732
+ notify(t("extractFail"), false);
2733
+ } finally {
2734
+ setExtracting(false);
2735
+ }
2736
+ };
2737
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2739
+ className: "dab-head dab-rise",
2740
+ style: { "--d": 0 },
2741
+ children: [
2742
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2743
+ className: "dab-overline",
2744
+ children: "Accent"
2745
+ }),
2746
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2747
+ className: "dab-h1",
2748
+ children: t("colorTitle")
2749
+ }),
2750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2751
+ className: "dab-desc",
2752
+ children: t("descColor")
2753
+ })
2754
+ ]
2755
+ }),
2756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2757
+ className: "dab-card dab-card-hover dab-hero-accent dab-rise",
2758
+ style: { "--d": 1 },
2759
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2760
+ className: "dab-orb-wrap",
2761
+ style: orbVars,
2762
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dab-orb-ring" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dab-orb" })]
2763
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2764
+ style: {
2765
+ flex: 1,
2766
+ minWidth: 200
2767
+ },
2768
+ children: [
2769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2770
+ className: "dab-hex-caption",
2771
+ children: t("hexCaption")
2772
+ }),
2773
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2774
+ className: "dab-hex",
2775
+ children: hex.toUpperCase()
2776
+ }),
2777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2778
+ className: "dab-hsl-row",
2779
+ children: [
2780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["H ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [Math.round(h), "°"] })] }),
2781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["S ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [Math.round(s * 100), "%"] })] }),
2782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["L ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [Math.round(l * 100), "%"] })] })
2783
+ ]
2784
+ }),
2785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2786
+ className: "dab-chip-row",
2787
+ style: { marginTop: 14 },
2788
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2789
+ type: "button",
2790
+ className: "dab-btn",
2791
+ disabled: !storeUrl || extracting,
2792
+ onClick: onExtract,
2793
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), extracting ? t("extracting") : t("extractColor")]
2794
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2795
+ type: "button",
2796
+ className: "dab-btn",
2797
+ disabled: !storeUrl,
2798
+ onClick: () => setPickerOpen(true),
2799
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PipetteIcon, { size: 14 }), t("eyedropper")]
2800
+ })]
2801
+ })
2802
+ ]
2803
+ })]
2804
+ }),
2805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2806
+ className: "dab-card dab-wheel-card dab-rise",
2807
+ style: { "--d": 2 },
2808
+ children: [
2809
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2810
+ className: "dab-wheel-glow",
2811
+ style: { background: hex }
2812
+ }),
2813
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorWheel, {
2814
+ hue: wheel[0],
2815
+ sat: wheel[1],
2816
+ lit: wheel[2],
2817
+ onChange: setColor
2818
+ }),
2819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorInputs, {
2820
+ hue: wheel[0],
2821
+ sat: wheel[1],
2822
+ lit: wheel[2],
2823
+ onChange: setColor
2824
+ })
2825
+ ]
2826
+ }),
2827
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2828
+ className: "dab-hint dab-rise",
2829
+ style: { "--d": 3 },
2830
+ children: t("colorHint")
2831
+ }),
2832
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2833
+ className: "dab-card dab-card-hover dab-rise",
2834
+ style: { "--d": 3 },
2835
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2836
+ className: "dab-swatch-title",
2837
+ children: t("swatchTitle")
2838
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2839
+ className: "dab-swatches",
2840
+ children: SWATCHES.map(([sh, ss, sl], i) => {
2841
+ const on = Math.abs((wheel[0] - sh + 540) % 360 - 180) < 3 && Math.abs(s - ss) < .05 && Math.abs(l - sl) < .05;
2842
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2843
+ type: "button",
2844
+ className: `dab-swatch${on ? " is-on" : ""}`,
2845
+ style: { background: `hsl(${sh} ${Math.round(ss * 100)}% ${Math.round(sl * 100)}%)` },
2846
+ title: toHex(hslToRgb(sh, ss, sl)).toUpperCase(),
2847
+ onClick: () => setColor(...hslToHsv(sh, ss, sl))
2848
+ }, i);
2849
+ })
2850
+ })]
2851
+ }),
2852
+ pickerOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorPicker, {
2853
+ url: storeUrl,
2854
+ t,
2855
+ onClose: () => setPickerOpen(false),
2856
+ onPick: (hsv) => {
2857
+ setColor(hsv[0], hsv[1], hsv[2]);
2858
+ setPickerOpen(false);
2859
+ }
2860
+ }) : null
2861
+ ] });
2862
+ }
2863
+ //#endregion
2864
+ //#region src/client/components/LiveSlider.tsx
2865
+ /**
2866
+ * Zero-lag slider: the thumb and value label update through DOM refs while
2867
+ * dragging (onInput) so the caller can mutate the live UI directly without a
2868
+ * React re-render; onChange commits the settled value. Double-click resets to
2869
+ * the canonical default. The track fill is a gradient driven by the --pct
2870
+ * custom property, updated imperatively alongside the thumb.
2871
+ */
2872
+ function LiveSlider({ min, max, step, def, fmt, label, onInput, onChange }) {
2873
+ const inputRef = (0, react.useRef)(null);
2874
+ const valRef = (0, react.useRef)(null);
2875
+ const paint = (el, v) => {
2876
+ el.style.setProperty("--pct", String((v - min) / (max - min) * 100));
2877
+ };
2878
+ (0, react.useEffect)(() => {
2879
+ if (inputRef.current) {
2880
+ inputRef.current.value = String(def);
2881
+ paint(inputRef.current, def);
2882
+ }
2883
+ if (valRef.current) valRef.current.textContent = fmt(def);
2884
+ }, [
2885
+ def,
2886
+ fmt,
2887
+ min,
2888
+ max
2889
+ ]);
2890
+ const apply = (v) => {
2891
+ if (inputRef.current) {
2892
+ inputRef.current.value = String(v);
2893
+ paint(inputRef.current, v);
2894
+ }
2895
+ if (valRef.current) valRef.current.textContent = fmt(v);
2896
+ onInput(v);
2897
+ onChange(v);
2898
+ };
2899
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2900
+ className: "dab-slider-block",
2901
+ children: [label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2902
+ className: "dab-slider-label",
2903
+ children: label
2904
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2905
+ style: {
2906
+ display: "flex",
2907
+ alignItems: "center",
2908
+ gap: 10
2909
+ },
2910
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2911
+ ref: inputRef,
2912
+ type: "range",
2913
+ className: "dab-slider",
2914
+ style: { "--pct": (def - min) / (max - min) * 100 },
2915
+ min,
2916
+ max,
2917
+ step,
2918
+ defaultValue: def,
2919
+ title: label ? `${label} · ${fmt(def)}` : fmt(def),
2920
+ onDoubleClick: () => apply(def),
2921
+ onInput: (e) => {
2922
+ const el = e.target;
2923
+ const v = Number(el.value);
2924
+ paint(el, v);
2925
+ onInput(v);
2926
+ if (valRef.current) valRef.current.textContent = fmt(v);
2927
+ },
2928
+ onChange: (e) => onChange(Number(e.target.value))
2929
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2930
+ ref: valRef,
2931
+ className: "dab-slider-val",
2932
+ children: fmt(def)
2933
+ })]
2934
+ })]
1441
2935
  });
1442
2936
  }
1443
2937
  //#endregion
2938
+ //#region src/client/components/pages/InterfacePage.tsx
2939
+ const PARTS = [
2940
+ {
2941
+ opKey: "bg",
2942
+ labelKey: "uiOpacityBg",
2943
+ Icon: CanvasIcon
2944
+ },
2945
+ {
2946
+ opKey: "sidebar",
2947
+ labelKey: "uiOpacitySide",
2948
+ Icon: SidebarIcon
2949
+ },
2950
+ {
2951
+ opKey: "card",
2952
+ labelKey: "uiOpacityCard",
2953
+ Icon: ChatIcon
2954
+ },
2955
+ {
2956
+ isSettings: true,
2957
+ labelKey: "uiSop",
2958
+ Icon: GearIcon
2959
+ }
2960
+ ];
2961
+ function InterfacePage({ p }) {
2962
+ const { t, setOps, setBlurs, setSop } = p;
2963
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2964
+ className: "dab-head dab-rise",
2965
+ style: { "--d": 0 },
2966
+ children: [
2967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2968
+ className: "dab-overline",
2969
+ children: "Surfaces"
2970
+ }),
2971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2972
+ className: "dab-h1",
2973
+ children: t("uiTitle")
2974
+ }),
2975
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2976
+ className: "dab-desc",
2977
+ children: t("descInterface")
2978
+ })
2979
+ ]
2980
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2981
+ className: "dab-grid-parts",
2982
+ children: PARTS.map((part, i) => {
2983
+ const { labelKey, Icon, isSettings } = part;
2984
+ const opKey = part.opKey;
2985
+ const blurKey = opKey ?? "settings";
2986
+ const opacity = isSettings ? rSop() : rOps()[opKey];
2987
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2988
+ className: "dab-card dab-card-hover dab-rise",
2989
+ style: { "--d": i + 1 },
2990
+ children: [
2991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2992
+ className: "dab-part-head",
2993
+ children: [
2994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2995
+ className: "dab-part-ico",
2996
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { size: 16 })
2997
+ }),
2998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2999
+ className: "dab-part-name",
3000
+ children: t(labelKey)
3001
+ }),
3002
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3003
+ className: "dab-part-badge",
3004
+ children: [Math.round(opacity * 100), "%"]
3005
+ })
3006
+ ]
3007
+ }),
3008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3009
+ label: t("uiOpacity"),
3010
+ min: 0,
3011
+ max: 100,
3012
+ step: 1,
3013
+ def: Math.round(opacity * 100),
3014
+ fmt: (v) => `${v}%`,
3015
+ onInput: (v) => {
3016
+ const op = v / 100;
3017
+ if (isSettings) {
3018
+ cfg.settingsOpacity = op;
3019
+ applySettingsOverrides(op);
3020
+ } else {
3021
+ const ops = { ...rOps() };
3022
+ ops[opKey] = op;
3023
+ cfg.opacities = ops;
3024
+ applyCustomTokens(ops);
3025
+ }
3026
+ saveConfig();
3027
+ },
3028
+ onChange: (v) => {
3029
+ const op = v / 100;
3030
+ if (isSettings) setSop(op);
3031
+ else {
3032
+ const ops = { ...rOps() };
3033
+ ops[opKey] = op;
3034
+ setOps(ops);
3035
+ }
3036
+ }
3037
+ }),
3038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3039
+ label: t("uiBlur"),
3040
+ min: 0,
3041
+ max: 60,
3042
+ step: 1,
3043
+ def: rBlurs()[blurKey],
3044
+ fmt: (v) => `${v}px`,
3045
+ onInput: (v) => {
3046
+ const blurs = { ...rBlurs() };
3047
+ blurs[blurKey] = v;
3048
+ cfg.blurs = blurs;
3049
+ setPartBlur(blurKey, v);
3050
+ saveConfig();
3051
+ },
3052
+ onChange: (v) => {
3053
+ const blurs = { ...rBlurs() };
3054
+ blurs[blurKey] = v;
3055
+ setBlurs(blurs);
3056
+ }
3057
+ })
3058
+ ]
3059
+ }, blurKey);
3060
+ })
3061
+ })] });
3062
+ }
3063
+ //#endregion
3064
+ //#region src/client/utils/image.ts
3065
+ /**
3066
+ * Read a chosen image file as a data URL WITHOUT re-encoding: the original
3067
+ * pixels are kept as-is (no canvas downscale / JPEG re-compression), so the
3068
+ * wallpaper is stored and displayed at full fidelity. The tradeoff is a larger
3069
+ * payload over the RPC channel and on disk for big images.
3070
+ */
3071
+ function readImg(file, cb) {
3072
+ const r = new FileReader();
3073
+ r.onerror = () => cb(null);
3074
+ r.onload = () => cb(r.result);
3075
+ r.readAsDataURL(file);
3076
+ }
3077
+ //#endregion
1444
3078
  //#region src/client/components/BgEditor.tsx
1445
3079
  function BgEditor({ url, t, onClose, onCommit }) {
1446
3080
  const pw = Math.min(window.innerWidth * .75, 860);
@@ -1527,7 +3161,7 @@ window.__ModuleLoader__.load({
1527
3161
  if (!el) return;
1528
3162
  const rect = el.getBoundingClientRect();
1529
3163
  const mx = rect.width / 2, my = rect.height / 2;
1530
- const factor = e.deltaY > 0 ? .92 : 1.08;
3164
+ const factor = e.deltaY > 0 ? .97 : 1.03;
1531
3165
  const nz = Math.max(.1, Math.min(10, zoom * factor));
1532
3166
  const nx = mx - (mx - pos.x) * (nz / zoom);
1533
3167
  const ny = my - (my - pos.y) * (nz / zoom);
@@ -1555,20 +3189,26 @@ window.__ModuleLoader__.load({
1555
3189
  ph,
1556
3190
  imgSize
1557
3191
  ]);
1558
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1559
- style: ST.overlay,
3192
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3193
+ className: "dab-overlay",
1560
3194
  onClick: (e) => {
1561
3195
  if (e.target === e.currentTarget) onClose();
1562
3196
  },
1563
3197
  children: [
1564
3198
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1565
- style: ST.modalTitle,
3199
+ className: "dab-overlay-title",
1566
3200
  children: t("editorTitle")
1567
3201
  }),
1568
3202
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1569
3203
  ref: containerRef,
3204
+ className: "dab-modal-card",
1570
3205
  style: {
1571
- ...ST.previewRect,
3206
+ position: "relative",
3207
+ overflow: "hidden",
3208
+ border: "2px solid rgba(255,255,255,0.3)",
3209
+ borderRadius: 12,
3210
+ background: "#000",
3211
+ cursor: "grab",
1572
3212
  width: pw,
1573
3213
  height: ph
1574
3214
  },
@@ -1579,7 +3219,9 @@ window.__ModuleLoader__.load({
1579
3219
  alt: "",
1580
3220
  draggable: false,
1581
3221
  style: {
1582
- ...ST.previewImg,
3222
+ position: "absolute",
3223
+ transformOrigin: "0 0",
3224
+ pointerEvents: "none",
1583
3225
  width: imgSize.w,
1584
3226
  height: imgSize.h,
1585
3227
  transform: `translate(${pos.x}px,${pos.y}px) scale(${zoom})`
@@ -1587,479 +3229,680 @@ window.__ModuleLoader__.load({
1587
3229
  })
1588
3230
  }),
1589
3231
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1590
- style: ST.modalHint,
3232
+ className: "dab-overlay-hint",
1591
3233
  children: t("editorHint")
1592
3234
  }),
1593
3235
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1594
- style: ST.modalBtns,
3236
+ style: {
3237
+ display: "flex",
3238
+ gap: 10
3239
+ },
1595
3240
  children: [
1596
3241
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1597
- style: ST.btn,
3242
+ type: "button",
3243
+ className: "dab-btn",
1598
3244
  onClick: resetView,
1599
3245
  children: t("editorReset")
1600
3246
  }),
1601
3247
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1602
- style: ST.btn,
3248
+ type: "button",
3249
+ className: "dab-btn",
1603
3250
  onClick: onClose,
1604
3251
  children: t("editorCancel")
1605
3252
  }),
1606
3253
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1607
- style: {
1608
- ...ST.btn,
1609
- ...ST.btnPrimary
1610
- },
3254
+ type: "button",
3255
+ className: "dab-btn dab-btn-primary",
1611
3256
  onClick: () => onCommit(zoom, (pos.x + imgSize.w * zoom / 2) / pw, (pos.y + imgSize.h * zoom / 2) / ph, imgRef.current?.naturalWidth ?? 0, imgRef.current?.naturalHeight ?? 0),
1612
3257
  children: t("editorCommit")
1613
3258
  })
1614
3259
  ]
1615
3260
  })
1616
3261
  ]
1617
- });
3262
+ }) });
1618
3263
  }
1619
3264
  //#endregion
1620
- //#region src/client/components/LiveSlider.tsx
1621
- function LiveSlider({ min, max, step, def, fmt, label, onInput, onChange }) {
1622
- const valRef = (0, react.useRef)(null);
1623
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1624
- style: ST.sliderRow,
1625
- children: [
1626
- label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1627
- style: ST.sliderLabel,
1628
- children: label
1629
- }) : null,
1630
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1631
- type: "range",
1632
- min,
1633
- max,
1634
- step,
1635
- defaultValue: def,
1636
- style: ST.slider,
1637
- onInput: (e) => {
1638
- const v = Number(e.target.value);
1639
- onInput(v);
1640
- if (valRef.current) valRef.current.textContent = fmt(v);
1641
- },
1642
- onChange: (e) => onChange(Number(e.target.value))
1643
- }),
1644
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1645
- ref: valRef,
1646
- style: ST.sliderVal,
1647
- children: fmt(def)
1648
- })
1649
- ]
1650
- });
1651
- }
1652
- //#endregion
1653
- //#region src/client/components/ErrorBoundary.tsx
1654
- /**
1655
- * Catches render errors from the theme section subtree (wheel, sliders, editor)
1656
- * so a single bad state can never take down the whole settings panel. Shows a
1657
- * compact fallback with a reset button; the reset re-renders the section with
1658
- * the current saved config, which is enough to recover from most transient
1659
- * failures (corrupt transient UI state, stale image decode, etc.).
1660
- */
1661
- var ErrorBoundary = class extends react.Component {
1662
- state = { error: null };
1663
- static getDerivedStateFromError(error) {
1664
- return { error };
1665
- }
1666
- componentDidCatch(error, info) {
1667
- console.error("dsh-any-background: section render crashed", error, info.componentStack);
1668
- }
1669
- reset = () => {
1670
- this.setState({ error: null });
1671
- this.props.onReset?.();
3265
+ //#region src/client/components/pages/BackgroundPage.tsx
3266
+ const SEG_W = 108;
3267
+ function BackgroundPage({ p }) {
3268
+ const { t, setWp, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, useStore } = p;
3269
+ const store = useStore((s) => s);
3270
+ const storeUrl = store.url;
3271
+ const backgroundType = store.backgroundType;
3272
+ const generatedBg = store.generatedBg;
3273
+ const regenerateOnReload = store.regenerateOnReload;
3274
+ const fileRef = (0, react.useRef)(null);
3275
+ const [editorOpen, setEditorOpen] = (0, react.useState)(false);
3276
+ const [dragOver, setDragOver] = (0, react.useState)(false);
3277
+ const [spinTick, setSpinTick] = (0, react.useState)(0);
3278
+ const isGenerated = backgroundType !== "image";
3279
+ const activeGenType = isGenerated && generatedBg ? generatedBg.type : "mesh";
3280
+ const onFileSelect = (f) => {
3281
+ readImg(f, (d) => {
3282
+ if (d) setWp(d);
3283
+ });
1672
3284
  };
1673
- render() {
1674
- if (this.state.error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1675
- style: ST.crashBox,
3285
+ const onDrop = (e) => {
3286
+ e.preventDefault();
3287
+ setDragOver(false);
3288
+ const f = e.dataTransfer.files?.[0];
3289
+ if (f && f.type.startsWith("image/")) onFileSelect(f);
3290
+ };
3291
+ const switchToImage = () => {
3292
+ if (backgroundType !== "image") setBgType("image");
3293
+ };
3294
+ const setGenType = (type) => {
3295
+ if (type === activeGenType) return;
3296
+ setBgType(type);
3297
+ };
3298
+ const ensureGenParams = () => generatedBg ?? defaultParamsFor(activeGenType);
3299
+ const updateGenerated = (patch) => {
3300
+ setGeneratedBg({
3301
+ ...ensureGenParams(),
3302
+ ...patch
3303
+ });
3304
+ };
3305
+ const typeMeta = [
3306
+ {
3307
+ type: "mesh",
3308
+ labelKey: "bgTypeMesh",
3309
+ descKey: "bgMeshDesc",
3310
+ thumb: "dab-thumb-mesh"
3311
+ },
3312
+ {
3313
+ type: "shader",
3314
+ labelKey: "bgTypeShader",
3315
+ descKey: "bgShaderDesc",
3316
+ thumb: "dab-thumb-shader"
3317
+ },
3318
+ {
3319
+ type: "pattern",
3320
+ labelKey: "bgTypePattern",
3321
+ descKey: "bgPatternDesc",
3322
+ thumb: "dab-thumb-pattern"
3323
+ }
3324
+ ];
3325
+ const presetLabel = (key) => {
3326
+ switch (key) {
3327
+ case "aurora": return t("presetAurora");
3328
+ case "nebula": return t("presetNebula");
3329
+ case "noise": return t("presetNoise");
3330
+ case "dots": return t("presetDots");
3331
+ case "waves": return t("presetWaves");
3332
+ case "poly": return t("presetPoly");
3333
+ default: return key;
3334
+ }
3335
+ };
3336
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
3338
+ className: "dab-head dab-rise",
3339
+ style: { "--d": 0 },
1676
3340
  children: [
1677
3341
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1678
- style: ST.crashTitle,
1679
- children: this.props.t("crashTitle")
3342
+ className: "dab-overline",
3343
+ children: "Canvas"
1680
3344
  }),
1681
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1682
- style: ST.crashDesc,
1683
- children: this.props.t("crashDesc")
3345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
3346
+ className: "dab-h1",
3347
+ children: t("bgTitle")
1684
3348
  }),
1685
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3350
+ className: "dab-desc",
3351
+ children: t("descBackground")
3352
+ })
3353
+ ]
3354
+ }),
3355
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
3356
+ className: "dab-rise",
3357
+ style: { "--d": 1 },
3358
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3359
+ className: "dab-hero",
3360
+ children: storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3362
+ className: "dab-hero-img",
3363
+ src: storeUrl,
3364
+ alt: "",
3365
+ draggable: false
3366
+ }),
3367
+ isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3368
+ className: "dab-hero-badge",
3369
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 11 }), t("liveBadge")]
3370
+ }) : null,
3371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3372
+ className: "dab-hero-veil",
3373
+ children: [backgroundType === "image" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3374
+ type: "button",
3375
+ className: "dab-btn",
3376
+ onClick: () => setEditorOpen(true),
3377
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 13 }), t("bgEdit")]
3378
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3379
+ type: "button",
3380
+ className: "dab-btn",
3381
+ onClick: () => {
3382
+ regenerateBg();
3383
+ setSpinTick((x) => x + 1);
3384
+ },
3385
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 13 }), t("bgRegenerate")]
3386
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3387
+ type: "button",
3388
+ className: "dab-btn dab-btn-danger",
3389
+ onClick: () => setWp(null),
3390
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 }), t("bgRemove")]
3391
+ })]
3392
+ })
3393
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1686
3394
  type: "button",
1687
- style: ST.btn,
1688
- onClick: this.reset,
1689
- children: this.props.t("crashReset")
3395
+ className: `dab-hero-empty${dragOver ? " is-over" : ""}`,
3396
+ onClick: () => fileRef.current?.click(),
3397
+ onDragOver: (e) => {
3398
+ e.preventDefault();
3399
+ setDragOver(true);
3400
+ },
3401
+ onDragLeave: () => setDragOver(false),
3402
+ onDrop,
3403
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 20 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dropHint") })]
3404
+ })
3405
+ })
3406
+ }),
3407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
3408
+ className: "dab-rise",
3409
+ style: { "--d": 2 },
3410
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3411
+ className: "dab-seg",
3412
+ style: { "--w": `${SEG_W}px` },
3413
+ children: [
3414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3415
+ className: "dab-seg-thumb",
3416
+ style: { transform: `translateX(${isGenerated ? SEG_W : 0}px)` }
3417
+ }),
3418
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3419
+ type: "button",
3420
+ className: `dab-seg-item${!isGenerated ? " is-active" : ""}`,
3421
+ onClick: switchToImage,
3422
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhotoIcon, { size: 14 }), t("bgSourceImage")]
3423
+ }),
3424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3425
+ type: "button",
3426
+ className: `dab-seg-item${isGenerated ? " is-active" : ""}`,
3427
+ onClick: () => {
3428
+ if (!isGenerated) setBgType(generatedBg?.type ?? "mesh");
3429
+ },
3430
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), t("bgSourceGenerated")]
3431
+ })
3432
+ ]
3433
+ })
3434
+ }),
3435
+ !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
3436
+ className: "dab-card dab-card-hover dab-rise",
3437
+ style: { "--d": 3 },
3438
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3439
+ className: "dab-chip-row",
3440
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3441
+ type: "button",
3442
+ className: "dab-btn dab-btn-primary",
3443
+ onClick: () => fileRef.current?.click(),
3444
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 14 }), t("bgChoose")]
3445
+ }), storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3446
+ type: "button",
3447
+ className: "dab-btn",
3448
+ onClick: () => setEditorOpen(true),
3449
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 14 }), t("bgEdit")]
3450
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3451
+ type: "button",
3452
+ className: "dab-btn dab-btn-ghost dab-btn-danger",
3453
+ onClick: () => setWp(null),
3454
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("bgRemove")]
3455
+ })] }) : null]
3456
+ })
3457
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3458
+ className: "dab-card dab-rise",
3459
+ style: { "--d": 3 },
3460
+ children: [
3461
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3462
+ className: "dab-types",
3463
+ children: typeMeta.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3464
+ type: "button",
3465
+ className: `dab-type${activeGenType === m.type ? " is-active" : ""}`,
3466
+ onClick: () => setGenType(m.type),
3467
+ children: [
3468
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `dab-type-thumb ${m.thumb}` }),
3469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3470
+ className: "dab-type-name",
3471
+ children: t(m.labelKey)
3472
+ }),
3473
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3474
+ className: "dab-type-desc",
3475
+ children: t(m.descKey)
3476
+ }),
3477
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3478
+ className: "dab-type-check",
3479
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 11 })
3480
+ })
3481
+ ]
3482
+ }, m.type))
3483
+ }),
3484
+ activeGenType === "shader" && generatedBg?.type === "shader" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3485
+ style: { marginTop: 14 },
3486
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3487
+ className: "dab-swatch-title",
3488
+ children: t("bgShaderPreset")
3489
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3490
+ className: "dab-chip-row",
3491
+ children: [
3492
+ "aurora",
3493
+ "nebula",
3494
+ "noise"
3495
+ ].map((pr) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3496
+ type: "button",
3497
+ className: `dab-chip${generatedBg.preset === pr ? " is-active" : ""}`,
3498
+ onClick: () => updateGenerated({ preset: pr }),
3499
+ children: presetLabel(pr)
3500
+ }, pr))
3501
+ })]
3502
+ }) : null,
3503
+ activeGenType === "pattern" && generatedBg?.type === "pattern" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3504
+ style: { marginTop: 14 },
3505
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3506
+ className: "dab-swatch-title",
3507
+ children: t("bgPatternPreset")
3508
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3509
+ className: "dab-chip-row",
3510
+ children: [
3511
+ "dots",
3512
+ "waves",
3513
+ "poly"
3514
+ ].map((pr) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3515
+ type: "button",
3516
+ className: `dab-chip${generatedBg.preset === pr ? " is-active" : ""}`,
3517
+ onClick: () => updateGenerated({ preset: pr }),
3518
+ children: presetLabel(pr)
3519
+ }, pr))
3520
+ })]
3521
+ }) : null,
3522
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3523
+ style: { marginTop: 16 },
3524
+ children: [
3525
+ activeGenType === "mesh" && generatedBg?.type === "mesh" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3526
+ label: t("bgMeshScale"),
3527
+ min: 30,
3528
+ max: 300,
3529
+ step: 1,
3530
+ def: Math.round(generatedBg.scale * 100),
3531
+ fmt: (v) => `${v}%`,
3532
+ onInput: () => {},
3533
+ onChange: (v) => updateGenerated({ scale: v / 100 })
3534
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3535
+ label: t("bgMeshIntensity"),
3536
+ min: 0,
3537
+ max: 100,
3538
+ step: 1,
3539
+ def: Math.round(generatedBg.intensity * 100),
3540
+ fmt: (v) => `${v}%`,
3541
+ onInput: () => {},
3542
+ onChange: (v) => updateGenerated({ intensity: v / 100 })
3543
+ })] }) : null,
3544
+ activeGenType === "shader" && generatedBg?.type === "shader" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3545
+ label: t("bgShaderSpeed"),
3546
+ min: 0,
3547
+ max: 200,
3548
+ step: 1,
3549
+ def: Math.round(generatedBg.speed * 100),
3550
+ fmt: (v) => `${v}%`,
3551
+ onInput: () => {},
3552
+ onChange: (v) => updateGenerated({ speed: v / 100 })
3553
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3554
+ label: t("bgShaderScale"),
3555
+ min: 30,
3556
+ max: 300,
3557
+ step: 1,
3558
+ def: Math.round(generatedBg.scale * 100),
3559
+ fmt: (v) => `${v}%`,
3560
+ onInput: () => {},
3561
+ onChange: (v) => updateGenerated({ scale: v / 100 })
3562
+ })] }) : null,
3563
+ activeGenType === "pattern" && generatedBg?.type === "pattern" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3564
+ label: t("bgPatternDensity"),
3565
+ min: 0,
3566
+ max: 100,
3567
+ step: 1,
3568
+ def: Math.round(generatedBg.density * 100),
3569
+ fmt: (v) => `${v}%`,
3570
+ onInput: () => {},
3571
+ onChange: (v) => updateGenerated({ density: v / 100 })
3572
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3573
+ label: t("bgPatternScale"),
3574
+ min: 30,
3575
+ max: 300,
3576
+ step: 1,
3577
+ def: Math.round(generatedBg.scale * 100),
3578
+ fmt: (v) => `${v}%`,
3579
+ onInput: () => {},
3580
+ onChange: (v) => updateGenerated({ scale: v / 100 })
3581
+ })] }) : null
3582
+ ]
3583
+ }),
3584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3585
+ className: "dab-seed",
3586
+ children: [
3587
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3588
+ className: "dab-seed-ico",
3589
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LockIcon, { size: 15 })
3590
+ }),
3591
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3592
+ className: "dab-seed-txt",
3593
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3594
+ className: "dab-seed-title",
3595
+ children: t("seedLock")
3596
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3597
+ className: "dab-seed-desc",
3598
+ children: !regenerateOnReload ? t("bgSeedLocked") : t("bgSeedUnlocked")
3599
+ })]
3600
+ }),
3601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3602
+ type: "button",
3603
+ className: `dab-toggle${!regenerateOnReload ? " is-on" : ""}`,
3604
+ role: "switch",
3605
+ "aria-checked": !regenerateOnReload,
3606
+ onClick: () => setRegenerateOnReload(!regenerateOnReload),
3607
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
3608
+ }),
3609
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3610
+ type: "button",
3611
+ className: "dab-btn dab-btn-primary",
3612
+ onClick: () => {
3613
+ regenerateBg();
3614
+ setSpinTick((x) => x + 1);
3615
+ },
3616
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3617
+ className: "dab-spin",
3618
+ style: { display: "grid" },
3619
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 13 })
3620
+ }, spinTick), t("bgRegenerate")]
3621
+ })
3622
+ ]
1690
3623
  })
1691
3624
  ]
1692
- });
1693
- return this.props.children;
1694
- }
1695
- };
3625
+ }),
3626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3627
+ className: "dab-card dab-card-hover dab-rise",
3628
+ style: { "--d": 4 },
3629
+ children: [
3630
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3631
+ label: t("wpOpacity"),
3632
+ min: 0,
3633
+ max: 100,
3634
+ step: 1,
3635
+ def: Math.round(rWop() * 100),
3636
+ fmt: (v) => `${v}%`,
3637
+ onInput: (v) => {
3638
+ const op = v / 100;
3639
+ cfg.wallpaperOpacity = op;
3640
+ setWpOpacity(op);
3641
+ saveConfig();
3642
+ },
3643
+ onChange: (v) => setWop(v / 100)
3644
+ }),
3645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3646
+ label: t("bgBlur"),
3647
+ min: 0,
3648
+ max: 60,
3649
+ step: 1,
3650
+ def: rBl(),
3651
+ fmt: (v) => `${v}px`,
3652
+ onInput: (v) => {
3653
+ cfg.blur = v;
3654
+ setWpBlur(v);
3655
+ saveConfig();
3656
+ },
3657
+ onChange: (v) => setBl(v)
3658
+ }),
3659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3660
+ className: "dab-hint",
3661
+ style: { marginTop: 12 },
3662
+ children: t("bgHint")
3663
+ })
3664
+ ]
3665
+ }),
3666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3667
+ ref: fileRef,
3668
+ type: "file",
3669
+ accept: "image/*",
3670
+ style: { display: "none" },
3671
+ onChange: (e) => {
3672
+ const f = e.target.files?.[0];
3673
+ if (!f) return;
3674
+ onFileSelect(f);
3675
+ e.target.value = "";
3676
+ }
3677
+ }),
3678
+ editorOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
3679
+ url: storeUrl,
3680
+ t,
3681
+ onClose: () => setEditorOpen(false),
3682
+ onCommit: (z, x, y, iw, ih) => {
3683
+ cfg.bgState = {
3684
+ zoom: z,
3685
+ x,
3686
+ y,
3687
+ iw,
3688
+ ih
3689
+ };
3690
+ applyWp();
3691
+ saveConfig();
3692
+ setEditorOpen(false);
3693
+ }
3694
+ }) : null
3695
+ ] });
3696
+ }
1696
3697
  //#endregion
1697
- //#region src/client/components/ThemeSection.tsx
1698
- function ThemeSection(props) {
1699
- const { t, hue, sat, lit, setColor, setWp, setOps, setBlurs, setWop, setBl, setSop, useStore, extractColor, exportTheme, importTheme } = props;
1700
- const storeUrl = useStore((s) => s.url);
1701
- const storeColor = useStore((s) => s.color);
1702
- const fileRef = (0, react.useRef)(null);
3698
+ //#region src/client/components/pages/ProfilePage.tsx
3699
+ function ProfilePage({ p, notify }) {
3700
+ const { t, exportTheme, importTheme } = p;
1703
3701
  const importRef = (0, react.useRef)(null);
1704
- const [editorOpen, setEditorOpen] = (0, react.useState)(false);
1705
- const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
1706
- const [extracting, setExtracting] = (0, react.useState)(false);
1707
- const [extractMsg, setExtractMsg] = (0, react.useState)(null);
1708
- const [importMsg, setImportMsg] = (0, react.useState)(null);
1709
- const msgTimer = (0, react.useRef)(void 0);
1710
- (0, react.useEffect)(() => () => window.clearTimeout(msgTimer.current), []);
1711
- const showMsg = (setter, key) => {
1712
- setter(t(key));
1713
- window.clearTimeout(msgTimer.current);
1714
- msgTimer.current = window.setTimeout(() => setter(null), 2500);
1715
- };
1716
- const onExtract = async () => {
1717
- if (!storeUrl || extracting) return;
1718
- setExtracting(true);
1719
- try {
1720
- showMsg(setExtractMsg, await extractColor() ? "extractDone" : "extractFail");
1721
- } catch {
1722
- showMsg(setExtractMsg, "extractFail");
1723
- } finally {
1724
- setExtracting(false);
1725
- }
1726
- };
1727
3702
  const onImport = async (file) => {
1728
3703
  try {
1729
- showMsg(setImportMsg, await importTheme(file) ? "importDone" : "importFail");
3704
+ const ok = await importTheme(file);
3705
+ notify(ok ? t("importDone") : t("importFail"), ok);
1730
3706
  } catch {
1731
- showMsg(setImportMsg, "importFail");
3707
+ notify(t("importFail"), false);
1732
3708
  }
1733
3709
  };
1734
- const wheel = storeColor ?? [
1735
- hue,
1736
- sat,
1737
- lit
1738
- ];
1739
- const blurSlider = (part) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
1740
- label: t("uiBlur"),
1741
- min: 0,
1742
- max: 60,
1743
- step: 1,
1744
- def: rBlurs()[part],
1745
- fmt: (v) => `${v}px`,
1746
- onInput: (v) => {
1747
- const blurs = { ...rBlurs() };
1748
- blurs[part] = v;
1749
- cfg.blurs = blurs;
1750
- setPartBlur(part, v);
1751
- saveConfig();
1752
- },
1753
- onChange: (v) => {
1754
- const blurs = { ...rBlurs() };
1755
- blurs[part] = v;
1756
- setBlurs(blurs);
1757
- }
1758
- });
1759
- const partSlider = (labelKey, part) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1760
- style: ST.sliderBlock,
1761
- children: [
1762
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1763
- style: ST.sliderLabel,
1764
- children: t(labelKey)
1765
- }),
1766
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
1767
- label: t("uiOpacity"),
1768
- min: 0,
1769
- max: 100,
1770
- step: 1,
1771
- def: Math.round(rOps()[part] * 100),
1772
- fmt: (v) => `${v}%`,
1773
- onInput: (v) => {
1774
- const ops = { ...rOps() };
1775
- ops[part] = v / 100;
1776
- cfg.opacities = ops;
1777
- applyCustomTokens(ops);
1778
- saveConfig();
1779
- },
1780
- onChange: (v) => {
1781
- const ops = { ...rOps() };
1782
- ops[part] = v / 100;
1783
- setOps(ops);
1784
- }
1785
- }),
1786
- blurSlider(part)
1787
- ]
1788
- });
1789
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary, {
1790
- t,
1791
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1792
- style: ST.root,
3710
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3711
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
3712
+ className: "dab-head dab-rise",
3713
+ style: { "--d": 0 },
1793
3714
  children: [
1794
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1795
- style: ST.headerRow,
1796
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1797
- style: ST.h2,
1798
- children: t("nav")
1799
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1800
- style: ST.sub,
1801
- children: t("subtitle")
1802
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1803
- style: ST.row,
1804
- children: [
1805
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1806
- type: "button",
1807
- style: ST.btn,
1808
- onClick: exportTheme,
1809
- children: t("exportTheme")
1810
- }),
1811
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1812
- type: "button",
1813
- style: ST.btn,
1814
- onClick: () => importRef.current?.click(),
1815
- children: t("importTheme")
1816
- }),
1817
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1818
- ref: importRef,
1819
- type: "file",
1820
- accept: "application/json,.json",
1821
- style: { display: "none" },
1822
- onChange: (e) => {
1823
- const f = e.target.files?.[0];
1824
- if (!f) return;
1825
- onImport(f);
1826
- e.target.value = "";
1827
- }
1828
- })
1829
- ]
1830
- })]
3715
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3716
+ className: "dab-overline",
3717
+ children: "Profile"
1831
3718
  }),
1832
- importMsg ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1833
- style: ST.colorHint,
1834
- children: importMsg
1835
- }) : null,
1836
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
3719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
3720
+ className: "dab-h1",
3721
+ children: t("pageProfile")
3722
+ }),
3723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3724
+ className: "dab-desc",
3725
+ children: t("descProfile")
3726
+ })
3727
+ ]
3728
+ }),
3729
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3730
+ className: "dab-profile-grid",
3731
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3732
+ className: "dab-card dab-card-hover dab-rise",
3733
+ style: { "--d": 1 },
3734
+ children: [
1837
3735
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1838
- style: ST.label,
1839
- children: t("colorTitle")
1840
- }),
1841
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1842
- style: ST.wheelRow,
1843
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorWheel, {
1844
- hue: wheel[0],
1845
- sat: wheel[1],
1846
- lit: wheel[2],
1847
- onChange: setColor
1848
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorInputs, {
1849
- hue: wheel[0],
1850
- sat: wheel[1],
1851
- lit: wheel[2],
1852
- onChange: setColor
1853
- })]
3736
+ className: "dab-profile-ico",
3737
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, { size: 17 })
1854
3738
  }),
1855
3739
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1856
- style: ST.colorHint,
1857
- children: t("colorHint")
3740
+ className: "dab-profile-title",
3741
+ children: t("exportCardTitle")
1858
3742
  }),
1859
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1860
- style: ST.btnGroup,
1861
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1862
- type: "button",
1863
- style: {
1864
- ...ST.btn,
1865
- opacity: !storeUrl || extracting ? .55 : 1
1866
- },
1867
- title: !storeUrl ? t("extractNoWp") : void 0,
1868
- disabled: !storeUrl || extracting,
1869
- onClick: onExtract,
1870
- children: extracting ? t("extracting") : t("extractColor")
1871
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1872
- type: "button",
1873
- style: {
1874
- ...ST.btn,
1875
- opacity: !storeUrl ? .55 : 1
1876
- },
1877
- title: !storeUrl ? t("extractNoWp") : void 0,
1878
- disabled: !storeUrl,
1879
- onClick: () => setPickerOpen(true),
1880
- children: t("eyedropper")
1881
- })]
3743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3744
+ className: "dab-profile-desc",
3745
+ children: t("exportCardDesc")
1882
3746
  }),
1883
- extractMsg ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1884
- style: ST.colorHint,
1885
- children: extractMsg
1886
- }) : null
1887
- ] }),
1888
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { style: ST.hr }),
1889
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1890
- style: ST.label,
1891
- children: t("uiTitle")
1892
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1893
- style: ST.sliders,
1894
- children: [
1895
- partSlider("uiOpacityBg", "bg"),
1896
- partSlider("uiOpacitySide", "sidebar"),
1897
- partSlider("uiOpacityCard", "card"),
1898
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1899
- style: ST.sliderBlock,
1900
- children: [
1901
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1902
- style: ST.sliderLabel,
1903
- children: t("uiSop")
1904
- }),
1905
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
1906
- label: t("uiOpacity"),
1907
- min: 0,
1908
- max: 100,
1909
- step: 1,
1910
- def: Math.round(rSop() * 100),
1911
- fmt: (v) => `${v}%`,
1912
- onInput: (v) => {
1913
- const op = v / 100;
1914
- cfg.settingsOpacity = op;
1915
- applySettingsOverrides(op);
1916
- saveConfig();
1917
- },
1918
- onChange: (v) => setSop(v / 100)
1919
- }),
1920
- blurSlider("settings")
1921
- ]
1922
- })
1923
- ]
1924
- })] }),
1925
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { style: ST.hr }),
1926
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
3747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3748
+ type: "button",
3749
+ className: "dab-btn dab-btn-primary",
3750
+ onClick: () => {
3751
+ exportTheme();
3752
+ notify(t("toastExportDone"));
3753
+ },
3754
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, { size: 14 }), t("exportTheme")]
3755
+ })
3756
+ ]
3757
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3758
+ className: "dab-card dab-card-hover dab-rise",
3759
+ style: { "--d": 2 },
3760
+ children: [
1927
3761
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1928
- style: ST.label,
1929
- children: t("bgTitle")
3762
+ className: "dab-profile-ico",
3763
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 17 })
1930
3764
  }),
1931
3765
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1932
- style: ST.center,
1933
- children: storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1934
- src: storeUrl,
1935
- alt: "",
1936
- style: ST.preview,
1937
- onClick: () => setEditorOpen(true)
1938
- }) : null
3766
+ className: "dab-profile-title",
3767
+ children: t("importCardTitle")
1939
3768
  }),
1940
3769
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1941
- style: ST.btnGroup,
1942
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1943
- style: ST.row,
1944
- children: [
1945
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1946
- type: "button",
1947
- style: ST.btn,
1948
- onClick: () => fileRef.current?.click(),
1949
- children: t("bgChoose")
1950
- }),
1951
- storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1952
- type: "button",
1953
- style: ST.btn,
1954
- onClick: () => setEditorOpen(true),
1955
- children: t("bgEdit")
1956
- }) : null,
1957
- storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1958
- type: "button",
1959
- style: {
1960
- ...ST.btn,
1961
- ...ST.btnDanger
1962
- },
1963
- onClick: () => setWp(null),
1964
- children: t("bgRemove")
1965
- }) : null,
1966
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1967
- ref: fileRef,
1968
- type: "file",
1969
- accept: "image/*",
1970
- style: { display: "none" },
1971
- onChange: (e) => {
1972
- const f = e.target.files?.[0];
1973
- if (!f) return;
1974
- readImg(f, (d) => {
1975
- if (d) setWp(d);
1976
- e.target.value = "";
1977
- });
1978
- }
1979
- })
1980
- ]
1981
- })
3770
+ className: "dab-profile-desc",
3771
+ children: t("importCardDesc")
1982
3772
  }),
1983
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1984
- style: ST.sliders,
1985
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1986
- style: ST.sliderBlock,
1987
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1988
- style: ST.sliderLabel,
1989
- children: t("wpOpacity")
1990
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
1991
- min: 0,
1992
- max: 100,
1993
- step: 1,
1994
- def: Math.round(rWop() * 100),
1995
- fmt: (v) => `${v}%`,
1996
- onInput: (v) => {
1997
- const op = v / 100;
1998
- cfg.wallpaperOpacity = op;
1999
- setWpOpacity(op);
2000
- saveConfig();
2001
- },
2002
- onChange: (v) => setWop(v / 100)
2003
- })]
2004
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2005
- style: ST.sliderBlock,
2006
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2007
- style: ST.sliderLabel,
2008
- children: t("bgBlur")
2009
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
2010
- min: 0,
2011
- max: 60,
2012
- step: 1,
2013
- def: rBl(),
2014
- fmt: (v) => `${v}px`,
2015
- onInput: (v) => {
2016
- cfg.blur = v;
2017
- setWpBlur(v);
2018
- saveConfig();
2019
- },
2020
- onChange: (v) => setBl(v)
2021
- })]
2022
- })]
3773
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3774
+ type: "button",
3775
+ className: "dab-btn",
3776
+ onClick: () => importRef.current?.click(),
3777
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 14 }), t("importTheme")]
2023
3778
  }),
2024
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2025
- style: ST.hint,
2026
- children: t("bgHint")
3779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3780
+ ref: importRef,
3781
+ type: "file",
3782
+ accept: "application/json,.json",
3783
+ style: { display: "none" },
3784
+ onChange: (e) => {
3785
+ const f = e.target.files?.[0];
3786
+ if (!f) return;
3787
+ onImport(f);
3788
+ e.target.value = "";
3789
+ }
2027
3790
  })
2028
- ] }),
2029
- pickerOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorPicker, {
2030
- url: storeUrl,
2031
- t,
2032
- onClose: () => setPickerOpen(false),
2033
- onPick: (hsv) => {
2034
- setColor(hsv[0], hsv[1], hsv[2]);
2035
- setPickerOpen(false);
2036
- }
2037
- }) : null,
2038
- editorOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
2039
- url: storeUrl,
2040
- t,
2041
- onClose: () => setEditorOpen(false),
2042
- onCommit: (z, x, y, iw, ih) => {
2043
- cfg.bgState = {
2044
- zoom: z,
2045
- x,
2046
- y,
2047
- iw,
2048
- ih
2049
- };
2050
- applyWp();
2051
- saveConfig();
2052
- setEditorOpen(false);
2053
- }
2054
- }) : null
2055
- ]
3791
+ ]
3792
+ })]
3793
+ }),
3794
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
3795
+ className: "dab-footer dab-rise",
3796
+ style: { "--d": 3 },
3797
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3798
+ className: "dab-footer-mono",
3799
+ children: "dsh-any-background"
3800
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("footerTag") })]
2056
3801
  })
2057
- });
3802
+ ] });
2058
3803
  }
2059
3804
  //#endregion
2060
- //#region src/client/components/icons.tsx
2061
- /** Sun glyph, migrated from @deepseek-ai/dsh-client-ui-primitives IconLightOutline16. */
2062
- const SUN_PATHS = "<path d=\"M11.3496 8C11.3496 6.14985 9.85015 4.65039 8 4.65039C6.14985 4.65039 4.65039 6.14985 4.65039 8C4.65039 9.85015 6.14985 11.3496 8 11.3496C9.85015 11.3496 11.3496 9.85015 11.3496 8ZM12.6504 8C12.6504 10.5681 10.5681 12.6504 8 12.6504C5.43188 12.6504 3.34961 10.5681 3.34961 8C3.34961 5.43188 5.43188 3.34961 8 3.34961C10.5681 3.34961 12.6504 5.43188 12.6504 8Z\" fill=\"currentColor\"/><path d=\"M8.65039 0.5V2.5H7.34961V0.5H8.65039Z\" fill=\"currentColor\"/><path d=\"M8.65039 13.5V15.5H7.34961V13.5H8.65039Z\" fill=\"currentColor\"/><path d=\"M3.15808 2.24035L4.57229 3.65456L3.6525 4.57435L2.23829 3.16014L3.15808 2.24035Z\" fill=\"currentColor\"/><path d=\"M12.3505 11.4327L13.7647 12.8469L12.8449 13.7667L11.4307 12.3525L12.3505 11.4327Z\" fill=\"currentColor\"/><path d=\"M2.24537 12.8469L3.65958 11.4327L4.57937 12.3525L3.16516 13.7667L2.24537 12.8469Z\" fill=\"currentColor\"/><path d=\"M11.4377 3.65455L12.852 2.24033L13.7718 3.16012L12.3575 4.57434L11.4377 3.65455Z\" fill=\"currentColor\"/><path d=\"M0.5 7.35461H2.5V8.6554H0.5L0.5 7.35461Z\" fill=\"currentColor\"/><path d=\"M13.5 7.35461H15.5V8.6554H13.5V7.35461Z\" fill=\"currentColor\"/>";
3805
+ //#region src/client/components/ThemeSection.tsx
3806
+ /**
3807
+ * dsh-any-background — settings section shell.
3808
+ *
3809
+ * The host renders this section only while the settings dialog is open and the
3810
+ * section is the active nav entry, so mounting IS being visible. The section is
3811
+ * rendered INLINE inside the host settings dialog's content column (the host
3812
+ * provides the modal chrome); the shell is a left nav rail + page body. Only
3813
+ * the transient toast escapes through a Portal (the shell's container-type
3814
+ * containment would otherwise capture its fixed positioning).
3815
+ */
3816
+ function ThemeSection(props) {
3817
+ ensureUiCss();
3818
+ const { t } = props;
3819
+ const [page, setPage] = (0, react.useState)(0);
3820
+ const [toast, setToast] = (0, react.useState)(null);
3821
+ const toastTimer = (0, react.useRef)(void 0);
3822
+ (0, react.useEffect)(() => () => window.clearTimeout(toastTimer.current), []);
3823
+ const notify = (msg, ok = true) => {
3824
+ setToast({
3825
+ msg,
3826
+ ok
3827
+ });
3828
+ window.clearTimeout(toastTimer.current);
3829
+ toastTimer.current = window.setTimeout(() => setToast(null), 2600);
3830
+ };
3831
+ const pages = [
3832
+ {
3833
+ label: t("pageColor"),
3834
+ Icon: DropletIcon,
3835
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorPage, {
3836
+ p: props,
3837
+ notify
3838
+ })
3839
+ },
3840
+ {
3841
+ label: t("pageInterface"),
3842
+ Icon: LayersIcon,
3843
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InterfacePage, { p: props })
3844
+ },
3845
+ {
3846
+ label: t("pageBackground"),
3847
+ Icon: PhotoIcon,
3848
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackgroundPage, { p: props })
3849
+ },
3850
+ {
3851
+ label: t("pageProfile"),
3852
+ Icon: SlidersIcon,
3853
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfilePage, {
3854
+ p: props,
3855
+ notify
3856
+ })
3857
+ }
3858
+ ];
3859
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ErrorBoundary, {
3860
+ t,
3861
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3862
+ className: "dab-root",
3863
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3864
+ className: "dab-shell",
3865
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
3866
+ className: "dab-nav",
3867
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3868
+ className: "dab-brand",
3869
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3870
+ className: "dab-brand-tile",
3871
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 15 })
3872
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3873
+ className: "dab-brand-name",
3874
+ children: t("nav")
3875
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3876
+ className: "dab-brand-tag",
3877
+ children: t("brandTag")
3878
+ })] })]
3879
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3880
+ className: "dab-nav-list",
3881
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3882
+ className: "dab-nav-ind",
3883
+ style: { transform: `translateY(${page * 42}px)` }
3884
+ }), pages.map((pg, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3885
+ type: "button",
3886
+ className: `dab-nav-item${i === page ? " is-active" : ""}`,
3887
+ onClick: () => setPage(i),
3888
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(pg.Icon, { size: 16 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: pg.label })]
3889
+ }, pg.label))]
3890
+ })]
3891
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3892
+ className: "dab-page",
3893
+ children: pages[page].node
3894
+ }, page)]
3895
+ })
3896
+ }), toast ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3897
+ className: "dab-toast",
3898
+ role: "status",
3899
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3900
+ className: toast.ok ? "dab-toast-ok" : "dab-toast-err",
3901
+ children: toast.ok ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlertIcon, { size: 14 })
3902
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: toast.msg })]
3903
+ }) }) : null]
3904
+ });
3905
+ }
2063
3906
  //#endregion
2064
3907
  //#region src/client/index.tsx
2065
3908
  /**
@@ -2081,9 +3924,9 @@ window.__ModuleLoader__.load({
2081
3924
  * rpc.ts file-backed persistence over the /dsh-any-background channel
2082
3925
  * wallpaper.ts wallpaper DOM layer + inline token writes
2083
3926
  * utils/ color math, token generation, image compression
2084
- * components/ ColorWheel, BgEditor, LiveSlider, ThemeSection
3927
+ * components/ ThemeSection shell + pages/, ColorWheel, BgEditor, LiveSlider
2085
3928
  * i18n.ts zh/en dictionaries
2086
- * styles.ts shared inline styles
3929
+ * ui.css.ts scoped design-system stylesheet (classes + keyframes)
2087
3930
  */
2088
3931
  const name = "dsh-any-background";
2089
3932
  const inject = [
@@ -2114,6 +3957,7 @@ window.__ModuleLoader__.load({
2114
3957
  if (rHasColor()) registerCustom(initH, initS, initL);
2115
3958
  ctx.effect(() => () => {
2116
3959
  customDispose?.();
3960
+ if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
2117
3961
  }, "dsh-any-background: skin dispose");
2118
3962
  let styleEl;
2119
3963
  if (typeof document !== "undefined") {
@@ -2127,31 +3971,47 @@ window.__ModuleLoader__.load({
2127
3971
  }, "dsh-any-background: gradient");
2128
3972
  let rev = 0;
2129
3973
  let colorRev = 0;
3974
+ let bgRev = 0;
3975
+ const colorTimerRef = { current: null };
2130
3976
  const store = (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
2131
3977
  init: () => ({
2132
3978
  url: null,
2133
3979
  rev: -1,
2134
3980
  colorRev: -1,
2135
- color: null
3981
+ color: null,
3982
+ backgroundType: cfg.backgroundType,
3983
+ generatedBg: cfg.generatedBg,
3984
+ bgRev: -1,
3985
+ regenerateOnReload: cfg.regenerateOnReload
2136
3986
  }),
2137
3987
  actions: {
2138
- syncBg: (d, url, r) => {
2139
- if (r <= d.rev) return;
2140
- d.url = url;
2141
- d.rev = r;
3988
+ syncBg: (d, url, r, bgType, genBg, bgr, reload) => {
3989
+ if (r > d.rev) {
3990
+ d.url = url;
3991
+ d.rev = r;
3992
+ }
3993
+ if (bgr !== void 0 && bgr > d.bgRev) {
3994
+ d.backgroundType = bgType;
3995
+ d.generatedBg = genBg ?? null;
3996
+ d.bgRev = bgr;
3997
+ }
3998
+ if (reload !== void 0) d.regenerateOnReload = reload;
2142
3999
  },
2143
4000
  syncColor: (d, hsv, r) => {
2144
- if (r <= d.colorRev) return;
2145
- d.color = hsv;
2146
- d.colorRev = r;
4001
+ if (r > d.colorRev) {
4002
+ d.color = hsv;
4003
+ d.colorRev = r;
4004
+ }
2147
4005
  }
2148
4006
  }
2149
4007
  });
2150
4008
  let bound = null;
2151
4009
  const syncBg = () => {
2152
4010
  rev++;
2153
- bound?.syncBg(rWp(), rev);
4011
+ bgRev++;
4012
+ bound?.syncBg(rWp(), rev, cfg.backgroundType, cfg.generatedBg, bgRev, cfg.regenerateOnReload);
2154
4013
  };
4014
+ onGeneratedSnapshot(syncBg);
2155
4015
  applyWp();
2156
4016
  syncBg();
2157
4017
  watchParts();
@@ -2160,7 +4020,11 @@ window.__ModuleLoader__.load({
2160
4020
  const [h, s, l] = rColor();
2161
4021
  registerCustom(h, s, l);
2162
4022
  }
2163
- applyWp();
4023
+ if (cfg.backgroundType !== "image") {
4024
+ if (cfg.regenerateOnReload) regenerateGeneratedBg();
4025
+ else if (cfg.generatedBg) updateGeneratedBg(cfg.generatedBg);
4026
+ persistConfig();
4027
+ } else applyThemeColor();
2164
4028
  syncBg();
2165
4029
  if (rHasColor()) {
2166
4030
  colorRev++;
@@ -2218,9 +4082,13 @@ window.__ModuleLoader__.load({
2218
4082
  ss,
2219
4083
  sl
2220
4084
  ];
2221
- registerCustom(sh, ss, sl);
2222
- applyWp();
2223
- saveConfig();
4085
+ if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
4086
+ colorTimerRef.current = window.setTimeout(() => {
4087
+ colorTimerRef.current = null;
4088
+ registerCustom(sh, ss, sl);
4089
+ applyWp();
4090
+ saveConfig();
4091
+ }, 80);
2224
4092
  colorRev++;
2225
4093
  bound?.syncColor([
2226
4094
  nh,
@@ -2229,11 +4097,34 @@ window.__ModuleLoader__.load({
2229
4097
  ], colorRev);
2230
4098
  },
2231
4099
  setWp: (u) => {
4100
+ cfg.backgroundType = "image";
4101
+ setBgDark(null);
4102
+ setWpImageUrl(u);
2232
4103
  setWpUrl(u);
2233
4104
  setBgState({ ...DEFAULT_CONFIG.bgState });
2234
- applyWp();
2235
- syncBg();
2236
4105
  persistWallpaper(u);
4106
+ applyThemeColor();
4107
+ syncBg();
4108
+ },
4109
+ setBgType: (type) => {
4110
+ setBackgroundType(type);
4111
+ saveConfig();
4112
+ syncBg();
4113
+ },
4114
+ setGeneratedBg: (params) => {
4115
+ updateGeneratedBg(params);
4116
+ saveConfig();
4117
+ syncBg();
4118
+ },
4119
+ regenerateBg: () => {
4120
+ regenerateGeneratedBg();
4121
+ persistConfig();
4122
+ syncBg();
4123
+ },
4124
+ setRegenerateOnReload: (v) => {
4125
+ cfg.regenerateOnReload = v;
4126
+ persistConfig();
4127
+ syncBg();
2237
4128
  },
2238
4129
  setOps: (ops) => {
2239
4130
  cfg.opacities = ops;
@@ -2280,10 +4171,10 @@ window.__ModuleLoader__.load({
2280
4171
  },
2281
4172
  exportTheme: () => {
2282
4173
  const payload = {
2283
- version: 1,
4174
+ version: 2,
2284
4175
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2285
4176
  config: cfg,
2286
- wallpaper: rWp()
4177
+ wallpaper: cfg.backgroundType === "image" ? rWp() : null
2287
4178
  };
2288
4179
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
2289
4180
  const url = URL.createObjectURL(blob);
@@ -2300,15 +4191,24 @@ window.__ModuleLoader__.load({
2300
4191
  const d = data;
2301
4192
  if (typeof d.config !== "object" || d.config === null) return false;
2302
4193
  adoptConfig(d.config);
2303
- const wallpaper = typeof d.wallpaper === "string" && /^data:image\//.test(d.wallpaper) ? d.wallpaper : null;
2304
- setWpUrl(wallpaper);
4194
+ if (cfg.backgroundType === "image") {
4195
+ const wallpaper = typeof d.wallpaper === "string" && /^data:image\//.test(d.wallpaper) ? d.wallpaper : null;
4196
+ setWpImageUrl(wallpaper);
4197
+ setWpUrl(wallpaper);
4198
+ persistWallpaper(wallpaper);
4199
+ applyThemeColor();
4200
+ } else {
4201
+ setWpImageUrl(null);
4202
+ setWpUrl(null);
4203
+ persistWallpaper(null);
4204
+ if (cfg.regenerateOnReload) regenerateGeneratedBg();
4205
+ else if (cfg.generatedBg) updateGeneratedBg(cfg.generatedBg);
4206
+ }
2305
4207
  persistConfig();
2306
- persistWallpaper(wallpaper);
2307
4208
  if (rHasColor()) {
2308
4209
  const [h, s, l] = rColor();
2309
4210
  registerCustom(h, s, l);
2310
4211
  }
2311
- applyWp();
2312
4212
  syncBg();
2313
4213
  if (rHasColor()) {
2314
4214
  colorRev++;
@@ -2380,8 +4280,11 @@ window.__ModuleLoader__.load({
2380
4280
  }, "dsh-any-background: nav icon watch");
2381
4281
  const restoreSaved = () => {
2382
4282
  if (rHasColor()) {
2383
- const [h, s, l] = rColor();
2384
- registerCustom(h, s, l);
4283
+ const snapshot = ctx.theme.getTheme();
4284
+ if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) {
4285
+ const [h, s, l] = rColor();
4286
+ registerCustom(h, s, l);
4287
+ } else if (snapshot.preference !== CUSTOM_ID) ctx.theme.setTheme(CUSTOM_ID);
2385
4288
  }
2386
4289
  applyWp();
2387
4290
  };