twskin 0.5.3

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/CONTRIBUTING.md +37 -0
  3. package/LICENSE +21 -0
  4. package/README.md +104 -0
  5. package/SECURITY.md +28 -0
  6. package/dist/bin/twskin.d.ts +3 -0
  7. package/dist/bin/twskin.d.ts.map +1 -0
  8. package/dist/bin/twskin.js +4 -0
  9. package/dist/bin/twskin.js.map +1 -0
  10. package/dist/commands.d.ts +41 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/commands.js +421 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/context.d.ts +14 -0
  15. package/dist/context.d.ts.map +1 -0
  16. package/dist/context.js +51 -0
  17. package/dist/context.js.map +1 -0
  18. package/dist/errors.d.ts +9 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +20 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/lock.d.ts +3 -0
  23. package/dist/lock.d.ts.map +1 -0
  24. package/dist/lock.js +52 -0
  25. package/dist/lock.js.map +1 -0
  26. package/dist/main.d.ts +9 -0
  27. package/dist/main.d.ts.map +1 -0
  28. package/dist/main.js +92 -0
  29. package/dist/main.js.map +1 -0
  30. package/dist/output.d.ts +4 -0
  31. package/dist/output.d.ts.map +1 -0
  32. package/dist/output.js +27 -0
  33. package/dist/output.js.map +1 -0
  34. package/dist/persistent-cdp.d.ts +14 -0
  35. package/dist/persistent-cdp.d.ts.map +1 -0
  36. package/dist/persistent-cdp.js +140 -0
  37. package/dist/persistent-cdp.js.map +1 -0
  38. package/dist/system.d.ts +64 -0
  39. package/dist/system.d.ts.map +1 -0
  40. package/dist/system.js +234 -0
  41. package/dist/system.js.map +1 -0
  42. package/dist/theme-download.d.ts +9 -0
  43. package/dist/theme-download.d.ts.map +1 -0
  44. package/dist/theme-download.js +250 -0
  45. package/dist/theme-download.js.map +1 -0
  46. package/dist/themes.d.ts +9 -0
  47. package/dist/themes.d.ts.map +1 -0
  48. package/dist/themes.js +185 -0
  49. package/dist/themes.js.map +1 -0
  50. package/dist/types.d.ts +58 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/docs/publishing.md +45 -0
  55. package/docs/theme-distribution.md +62 -0
  56. package/package.json +74 -0
  57. package/runtime/component-map.mjs +200 -0
  58. package/runtime/injector.mjs +705 -0
  59. package/runtime/manifest.json +47 -0
  60. package/runtime/restore.sh +33 -0
  61. package/runtime/skin.js +1133 -0
  62. package/runtime/start.sh +86 -0
  63. package/runtime/styles/base.css +687 -0
  64. package/runtime/styles/manager.css +346 -0
  65. package/runtime/token-map.mjs +432 -0
@@ -0,0 +1,1133 @@
1
+ // TRAE Work Skin — 页面内载荷(背景层 + 主题画廊面板)
2
+ // injector.mjs 注入映射、主题目录与共享样式后经 Runtime.evaluate 执行。
3
+ // 幂等:重复执行先移除旧节点再重新挂载;主题选择存 localStorage,重启/导航后保持。
4
+ (() => {
5
+ __TOKEN_MAP__
6
+ __COMPONENT_MAP__
7
+ const TOKEN_MAP = self.DREAM_SKIN_TOKEN_MAP;
8
+ const COMPONENT_MAP = self.DREAM_SKIN_COMPONENT_MAP;
9
+ const CATALOG = __CATALOG__;
10
+ const DEFAULT_THEME = __DEFAULT_THEME__;
11
+ const VERSION = __VERSION__;
12
+ const BASE_CSS = __BASE_CSS__;
13
+ const MANAGER_CSS = __MANAGER_CSS__;
14
+ const LS_PREFIX = "trae-dream-skin:";
15
+ const LS_KEY = `${LS_PREFIX}theme`;
16
+ const BLUR_LS_PREFIX = `${LS_PREFIX}panel-blur:`;
17
+ const BRIGHTNESS_LS_PREFIX = `${LS_PREFIX}background-brightness:`;
18
+ const MAIN_OVERLAY_LS_PREFIX = `${LS_PREFIX}main-overlay-opacity:`;
19
+ const DISABLED_KEY = `${LS_PREFIX}disabled`;
20
+ const STYLE_ID = "trae-dream-skin-style";
21
+ const UI_STYLE_ID = "trae-dream-skin-ui-style";
22
+ const THEME_STYLE_ID = "trae-dream-skin-theme-style";
23
+ const ICONS_STYLE_ID = "trae-dream-skin-icons-style";
24
+ const SCOPE_STYLE_ID = "trae-dream-skin-scope-style";
25
+ const PANEL_ID = "trae-dream-skin-panel";
26
+ const BUTTON_ID = "trae-dream-skin-button";
27
+
28
+ window.__TRAE_DREAM_SKIN_OBS__?.disconnect();
29
+ window.__TRAE_DREAM_SKIN_OBS__ = null;
30
+ document.getElementById(STYLE_ID)?.remove();
31
+ document.getElementById(UI_STYLE_ID)?.remove();
32
+ document.getElementById(THEME_STYLE_ID)?.remove();
33
+ document.getElementById(ICONS_STYLE_ID)?.remove();
34
+ document.getElementById(SCOPE_STYLE_ID)?.remove();
35
+ document.getElementById(PANEL_ID)?.remove();
36
+ document.getElementById(BUTTON_ID)?.remove();
37
+ // 清理上一次注入在 <html> 上写的令牌变量(v3)
38
+ if (Array.isArray(window.__TRAE_DREAM_SKIN_HTML_VARS__)) {
39
+ for (const name of window.__TRAE_DREAM_SKIN_HTML_VARS__) {
40
+ document.documentElement.style.removeProperty(name);
41
+ }
42
+ window.__TRAE_DREAM_SKIN_HTML_VARS__ = null;
43
+ }
44
+ // 清理旧版/调试会话遗留在 <html> 上的背景,避免加载失败时串用旧资产。
45
+ document.documentElement.style.removeProperty("--trae-skin-art");
46
+ window.__TRAE_DREAM_SKIN_APPEARANCE_OBS__?.disconnect?.();
47
+ window.__TRAE_DREAM_SKIN_APPEARANCE_OBS__ = null;
48
+ // 记录 App 原始外观(只在首次注入时记录一次,恢复默认时还原)
49
+ if (!window.__TRAE_DREAM_SKIN_ORIG_APPEARANCE__) {
50
+ window.__TRAE_DREAM_SKIN_ORIG_APPEARANCE__ = {
51
+ dataTheme: document.documentElement.getAttribute("data-theme"),
52
+ light: Boolean(document.body?.classList.contains("light")),
53
+ vsDark: Boolean(document.body?.classList.contains("vs-dark")),
54
+ };
55
+ }
56
+ if (document.body) {
57
+ for (const className of [...document.body.classList]) {
58
+ if (className.startsWith("trae-skin-theme-")) {
59
+ document.body.classList.remove(className);
60
+ }
61
+ }
62
+ for (const key of [
63
+ "traeSkinLandingBanner",
64
+ "traeSkinModeTabs",
65
+ "traeSkinModeTabIcons",
66
+ "traeSkinModeTabCodes",
67
+ ]) delete document.body.dataset[key];
68
+ }
69
+
70
+ let disabled = false;
71
+ try { disabled = localStorage.getItem(DISABLED_KEY) === "true"; } catch {}
72
+ if (disabled) {
73
+ if (document.body) {
74
+ const semanticPrefixes = [
75
+ "--bg-bg-",
76
+ "--text-text-",
77
+ "--icon-icon-",
78
+ "--border-border-",
79
+ ];
80
+ for (const name of [...document.body.style]) {
81
+ if (
82
+ name.startsWith("--trae-skin-")
83
+ || semanticPrefixes.some((prefix) => name.startsWith(prefix))
84
+ ) {
85
+ document.body.style.removeProperty(name);
86
+ }
87
+ }
88
+ document.body.classList.remove(
89
+ "trae-skin-v2",
90
+ "trae-skin-v3",
91
+ "trae-skin-appearance-dark",
92
+ "trae-skin-effects-max",
93
+ "trae-skin-blur-disabled",
94
+ );
95
+ }
96
+ }
97
+
98
+ const style = document.createElement("style");
99
+ style.id = STYLE_ID;
100
+ style.textContent = BASE_CSS;
101
+ // 管理器(画廊面板/按钮)样式独立于主题样式常驻:
102
+ // “恢复默认”只移除主题样式,管理器布局不受影响
103
+ const uiStyle = document.createElement("style");
104
+ uiStyle.id = UI_STYLE_ID;
105
+ uiStyle.textContent = MANAGER_CSS;
106
+ const themeStyle = document.createElement("style");
107
+ themeStyle.id = THEME_STYLE_ID;
108
+ (document.head || document.documentElement).appendChild(uiStyle);
109
+ if (!disabled) {
110
+ (document.head || document.documentElement).appendChild(style);
111
+ (document.head || document.documentElement).appendChild(themeStyle);
112
+ }
113
+
114
+ const findTheme = (id) => CATALOG.find((t) => t.id === id);
115
+ const setVar = (name, value) => {
116
+ if (value == null || value === "") return;
117
+ document.body.style.setProperty(name, String(value));
118
+ };
119
+ const asNumber = (value, fallback, min = -Infinity, max = Infinity) => {
120
+ const parsed = Number(value);
121
+ return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : fallback;
122
+ };
123
+ const asLength = (value, fallback) => {
124
+ if (value == null || value === "") return fallback;
125
+ return typeof value === "number" ? `${value}px` : String(value);
126
+ };
127
+ const asCssString = (value, fallback = "") => JSON.stringify(String(value ?? fallback));
128
+ const withAlpha = (color, opacity) => {
129
+ const alpha = asNumber(opacity, 1, 0, 1);
130
+ const match = /^#([0-9a-f]{6})$/i.exec(String(color || ""));
131
+ if (match) {
132
+ const value = Number.parseInt(match[1], 16);
133
+ return `rgba(${value >> 16}, ${(value >> 8) & 255}, ${value & 255}, ${alpha})`;
134
+ }
135
+ if (alpha === 1) return String(color);
136
+ return `color-mix(in srgb, ${color} ${alpha * 100}%, transparent)`;
137
+ };
138
+ const clearManagedVars = () => {
139
+ const semanticPrefixes = [
140
+ "--bg-bg-",
141
+ "--text-text-",
142
+ "--icon-icon-",
143
+ "--border-border-",
144
+ ];
145
+ for (const name of [...document.body.style]) {
146
+ if (
147
+ name.startsWith("--trae-skin-")
148
+ || semanticPrefixes.some((prefix) => name.startsWith(prefix))
149
+ ) {
150
+ document.body.style.removeProperty(name);
151
+ }
152
+ }
153
+ // v3 写在 <html> 上的令牌变量
154
+ if (Array.isArray(window.__TRAE_DREAM_SKIN_HTML_VARS__)) {
155
+ for (const name of window.__TRAE_DREAM_SKIN_HTML_VARS__) {
156
+ document.documentElement.style.removeProperty(name);
157
+ }
158
+ window.__TRAE_DREAM_SKIN_HTML_VARS__ = null;
159
+ }
160
+ document.documentElement.style.removeProperty("--trae-skin-art");
161
+ if (document.body) {
162
+ for (const key of [
163
+ "traeSkinLandingBanner",
164
+ "traeSkinModeTabs",
165
+ "traeSkinModeTabIcons",
166
+ "traeSkinModeTabCodes",
167
+ ]) delete document.body.dataset[key];
168
+ }
169
+ };
170
+ const setSurface = (name, config, fallbackColor, fallbackOpacity, fallbackBlur) => {
171
+ const surface = config || {};
172
+ setVar(
173
+ `--trae-skin-${name}-surface`,
174
+ withAlpha(surface.background || fallbackColor, surface.opacity ?? fallbackOpacity),
175
+ );
176
+ setVar(
177
+ `--trae-skin-${name}-blur`,
178
+ asLength(surface.backdropBlur, `${fallbackBlur}px`),
179
+ );
180
+ setVar(
181
+ `--trae-skin-${name}-saturation`,
182
+ asNumber(surface.backdropSaturation, 1.2, 0, 3),
183
+ );
184
+ };
185
+ const publicThemeConfig = (theme) => {
186
+ const config = {
187
+ id: theme.id,
188
+ name: theme.name,
189
+ desc: theme.desc,
190
+ category: theme.category,
191
+ };
192
+ if (theme.settings?.schemaVersion) {
193
+ Object.assign(config, theme.settings);
194
+ } else {
195
+ if (theme.surfaceLight != null) config.surfaceLight = theme.surfaceLight;
196
+ if (theme.surfaceDark != null) config.surfaceDark = theme.surfaceDark;
197
+ if (theme.blurPx != null) config.blurPx = theme.blurPx;
198
+ }
199
+ return config;
200
+ };
201
+ const panelBlurEnabled = (theme) => {
202
+ const defaultValue = theme.settings?.effects?.panelBlurEnabled ?? true;
203
+ try {
204
+ const saved = localStorage.getItem(`${BLUR_LS_PREFIX}${theme.id}`);
205
+ return saved == null ? Boolean(defaultValue) : saved === "true";
206
+ } catch {
207
+ return Boolean(defaultValue);
208
+ }
209
+ };
210
+ const savePanelBlur = (theme, enabled) => {
211
+ try {
212
+ localStorage.setItem(`${BLUR_LS_PREFIX}${theme.id}`, String(Boolean(enabled)));
213
+ } catch {}
214
+ };
215
+ const defaultBackgroundBrightness = (theme) => asNumber(
216
+ theme.settings?.background?.brightness,
217
+ 1,
218
+ 0,
219
+ 2,
220
+ );
221
+ const backgroundBrightness = (theme) => {
222
+ const fallback = defaultBackgroundBrightness(theme);
223
+ try {
224
+ const saved = localStorage.getItem(`${BRIGHTNESS_LS_PREFIX}${theme.id}`);
225
+ return saved == null ? fallback : asNumber(saved, fallback, 0, 2);
226
+ } catch {
227
+ return fallback;
228
+ }
229
+ };
230
+ const saveBackgroundBrightness = (theme, value) => {
231
+ const next = asNumber(value, defaultBackgroundBrightness(theme), 0, 2);
232
+ try { localStorage.setItem(`${BRIGHTNESS_LS_PREFIX}${theme.id}`, String(next)); } catch {}
233
+ return next;
234
+ };
235
+ const resetBackgroundBrightness = (theme) => {
236
+ try { localStorage.removeItem(`${BRIGHTNESS_LS_PREFIX}${theme.id}`); } catch {}
237
+ return defaultBackgroundBrightness(theme);
238
+ };
239
+ const defaultMainOverlayOpacity = (theme) => asNumber(
240
+ theme.settings?.surfaces?.opacity?.chat ?? theme.settings?.surfaces?.opacity?.main,
241
+ 0.68, 0, 1,
242
+ );
243
+ const mainOverlayOpacity = (theme) => {
244
+ try {
245
+ const saved = localStorage.getItem(`${MAIN_OVERLAY_LS_PREFIX}${theme.id}`);
246
+ return saved == null ? null : asNumber(saved, null, 0, 1);
247
+ } catch { return null; }
248
+ };
249
+ const saveMainOverlayOpacity = (theme, value) => {
250
+ const next = asNumber(value, defaultMainOverlayOpacity(theme), 0, 1);
251
+ try { localStorage.setItem(`${MAIN_OVERLAY_LS_PREFIX}${theme.id}`, String(next)); } catch {}
252
+ return next;
253
+ };
254
+ const resetMainOverlayOpacity = (theme) => {
255
+ try { localStorage.removeItem(`${MAIN_OVERLAY_LS_PREFIX}${theme.id}`); } catch {}
256
+ };
257
+ let ensureSelfHeal = () => {};
258
+ let activeThemeClass = null;
259
+
260
+ // v3 外观握手的逆操作:还原 App 原始外观(切换到 v1/v2 主题、恢复默认时调用)
261
+ const restoreNativeAppearance = () => {
262
+ window.__TRAE_DREAM_SKIN_APPEARANCE_OBS__?.disconnect?.();
263
+ window.__TRAE_DREAM_SKIN_APPEARANCE_OBS__ = null;
264
+ const orig = window.__TRAE_DREAM_SKIN_ORIG_APPEARANCE__;
265
+ if (!orig) return;
266
+ if (orig.dataTheme == null) {
267
+ document.documentElement.removeAttribute("data-theme");
268
+ } else {
269
+ document.documentElement.setAttribute("data-theme", orig.dataTheme);
270
+ }
271
+ if (document.body) {
272
+ document.body.classList.toggle("light", orig.light);
273
+ document.body.classList.toggle("vs-dark", orig.vsDark);
274
+ }
275
+ };
276
+ const restoreNativeTheme = () => {
277
+ try {
278
+ for (let index = localStorage.length - 1; index >= 0; index -= 1) {
279
+ const key = localStorage.key(index);
280
+ if (key?.startsWith(LS_PREFIX)) localStorage.removeItem(key);
281
+ }
282
+ localStorage.setItem(DISABLED_KEY, "true");
283
+ } catch {}
284
+ disabled = true;
285
+ clearManagedVars();
286
+ // v3 外观握手的逆操作:还原 App 原始外观
287
+ restoreNativeAppearance();
288
+ window.__TRAE_DREAM_SKIN_ORIG_APPEARANCE__ = null;
289
+ document.body.classList.remove(
290
+ "trae-skin-v2",
291
+ "trae-skin-v3",
292
+ "trae-skin-appearance-dark",
293
+ "trae-skin-effects-max",
294
+ "trae-skin-blur-disabled",
295
+ );
296
+ if (activeThemeClass) document.body.classList.remove(activeThemeClass);
297
+ activeThemeClass = null;
298
+ document.getElementById(STYLE_ID)?.remove();
299
+ document.getElementById(THEME_STYLE_ID)?.remove();
300
+ document.getElementById(ICONS_STYLE_ID)?.remove();
301
+ document.getElementById(SCOPE_STYLE_ID)?.remove();
302
+ document.getElementById(PANEL_ID)?.remove();
303
+ panel.style.display = "none";
304
+ panel.querySelectorAll(".ds-card").forEach((card) => card.classList.remove("ds-active"));
305
+ configToggle.style.display = "none";
306
+ showConfig(false);
307
+ if (!uiStyle.isConnected) (document.head || document.documentElement).appendChild(uiStyle);
308
+ ensureSelfHeal();
309
+ button.title = "打开 TRAE Work Skin 主题画廊";
310
+ window.__TRAE_DREAM_SKIN__ = VERSION;
311
+ return { ok: true, disabled: true, controlRetained: true };
312
+ };
313
+ let renderCurrentConfig = () => {};
314
+ let renderBlurControl = () => {};
315
+ let renderBrightnessControl = () => {};
316
+ let renderMainOverlayControl = () => {};
317
+
318
+ const setAppearance = (appearance) => {
319
+ const root = document.documentElement;
320
+ const wantDark = appearance === "dark";
321
+ if (root.getAttribute("data-theme") !== appearance) {
322
+ root.setAttribute("data-theme", appearance);
323
+ }
324
+ if (document.body) {
325
+ document.body.classList.toggle("light", !wantDark);
326
+ document.body.classList.toggle("vs-dark", wantDark);
327
+ }
328
+ };
329
+
330
+ const ensureAppearanceObserver = () => {
331
+ if (window.__TRAE_DREAM_SKIN_APPEARANCE_OBS__) return;
332
+ const obs = new MutationObserver(() => {
333
+ let id = null;
334
+ try { id = localStorage.getItem(LS_KEY); } catch {}
335
+ const current = id ? findTheme(id) : null;
336
+ if (!current || Number(current.settings?.schemaVersion) < 3) {
337
+ obs.disconnect();
338
+ window.__TRAE_DREAM_SKIN_APPEARANCE_OBS__ = null;
339
+ return;
340
+ }
341
+ const want = current.settings.appearance === "light" ? "light" : "dark";
342
+ if (document.documentElement.getAttribute("data-theme") !== want) {
343
+ setAppearance(want);
344
+ }
345
+ });
346
+ obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
347
+ window.__TRAE_DREAM_SKIN_APPEARANCE_OBS__ = obs;
348
+ };
349
+
350
+ const setTraeSkinVarsV3 = (roles, settings, theme) => {
351
+ const { accent, surface, text, icons, border, state } = roles;
352
+ const background = settings.background || {};
353
+ const surfaces = settings.surfaces || {};
354
+ const surfacesOpacity = surfaces.opacity || {};
355
+ const surfacesBlur = surfaces.blurPx || {};
356
+ const surfacesSaturation = surfaces.saturation ?? 1.2;
357
+ const typography = settings.typography || {};
358
+ const shape = settings.shape || {};
359
+ const radius = shape.radius || {};
360
+ const elevation = settings.elevation || {};
361
+ const scrollbar = settings.scrollbar || {};
362
+ const workbench = settings.workbench || {};
363
+ const workbenchOpacity = workbench.opacity || {};
364
+ const effects = settings.effects || {};
365
+ const surfacesColors = surfaces.colors || {};
366
+ const components = settings.components || {};
367
+ const chatC = components.chat || {};
368
+ const popoverC = components.popover || {};
369
+ const settingsC = components.settings || {};
370
+ const decorations = settings.decorations || {};
371
+ const componentRoles = COMPONENT_MAP.deriveComponents(components, { accent, surface, text, border, state });
372
+ const bannerC = componentRoles.banner;
373
+ const modeTabsC = componentRoles.modeTabs;
374
+ const mainOverlay = mainOverlayOpacity(theme);
375
+ const centerOpacity = (fallback) => mainOverlay ?? fallback;
376
+
377
+ document.body.classList.toggle("trae-skin-effects-max", effects.mode === "max");
378
+
379
+ setVar("--trae-skin-background-color", background.color || surface.base);
380
+ setVar("--trae-skin-background-position", background.position || "center");
381
+ setVar("--trae-skin-background-size", background.size || "cover");
382
+ setVar("--trae-skin-background-repeat", background.repeat || "no-repeat");
383
+ setVar("--trae-skin-background-blur", asLength(background.blur, "0px"));
384
+ setVar("--trae-skin-background-brightness", backgroundBrightness(theme));
385
+ setVar("--trae-skin-background-saturation", asNumber(background.saturation, 1, 0, 3));
386
+ setVar("--trae-skin-background-scale", background.blur ? 1.035 : 1);
387
+ setVar("--trae-skin-background-overlay", withAlpha(
388
+ background.overlay?.color || "transparent",
389
+ background.overlay?.opacity ?? 0,
390
+ ));
391
+
392
+ setSurface("left", { background: surfacesColors.left ?? surface.secondary, opacity: surfacesOpacity.left ?? 0.72, backdropBlur: surfacesBlur.left ?? 16, backdropSaturation: surfacesSaturation }, surface.secondary, 0.72, 16);
393
+ setSurface("chat", { background: surfacesColors.chat ?? surface.base, opacity: centerOpacity(surfacesOpacity.chat ?? 0.72), backdropBlur: surfacesBlur.chat ?? 20, backdropSaturation: surfacesSaturation }, surface.base, 0.72, 20);
394
+ setSurface("main", { background: surfacesColors.main ?? surface.base, opacity: centerOpacity(surfacesOpacity.main ?? 0.68), backdropBlur: surfacesBlur.main ?? 18, backdropSaturation: surfacesSaturation }, surface.base, 0.68, 18);
395
+ setSurface("landing", { background: surfacesColors.landing ?? surface.base, opacity: centerOpacity(surfacesOpacity.landing ?? 0.68), backdropBlur: surfacesBlur.landing ?? 18, backdropSaturation: surfacesSaturation }, surface.base, 0.68, 18);
396
+ setVar("--trae-skin-landing-overlay-opacity", centerOpacity(surfacesOpacity.landing ?? 0.68));
397
+ setVar("--trae-skin-layout-gap", surfaces.gap || withAlpha(surface.secondary, 0.35));
398
+ setVar("--trae-skin-divider", surfaces.divider || border.subtle);
399
+ setVar("--trae-skin-surface-light", centerOpacity(surfacesOpacity.chat ?? 0.72));
400
+ setVar("--trae-skin-surface-dark", centerOpacity(surfacesOpacity.chat ?? 0.72));
401
+ setVar("--trae-skin-blur", `${surfacesBlur.chat ?? 20}px`);
402
+
403
+ setVar("--trae-skin-text-primary", text.primary);
404
+ setVar("--trae-skin-text-secondary", text.secondary);
405
+ setVar("--trae-skin-text-tertiary", text.tertiary);
406
+ setVar("--trae-skin-text-disabled", text.disabled);
407
+ setVar("--trae-skin-accent", accent.base);
408
+ setVar("--trae-skin-accent-hover", accent.hover);
409
+ setVar("--trae-skin-accent-subtle", accent.subtle);
410
+ setVar("--trae-skin-accent-on", accent.onAccent);
411
+ setVar("--trae-skin-info", state.info);
412
+ setVar("--trae-skin-info-subtle", withAlpha(state.info, 0.12));
413
+ setVar("--trae-skin-error", state.error);
414
+ setVar("--trae-skin-warning", state.warning);
415
+ setVar("--trae-skin-success", state.success);
416
+ setVar("--trae-skin-input", surface.input);
417
+ setVar("--trae-skin-menu", surface.menu);
418
+ setVar("--trae-skin-border-subtle", border.subtle);
419
+ setVar("--trae-skin-border-default", border.default);
420
+ setVar("--trae-skin-border-strong", border.strong);
421
+ setVar("--trae-skin-scanline-color", effects.scanlineColor || "transparent");
422
+ setVar("--trae-skin-grid-color", effects.gridColor || "transparent");
423
+ setVar("--trae-skin-magenta", effects.magenta || state.error);
424
+ setVar("--trae-skin-neon-glow", effects.glow || "none");
425
+
426
+ setVar("--trae-skin-font-ui", typography.ui?.family || "inherit");
427
+ setVar("--trae-skin-font-heading", typography.heading?.family || typography.ui?.family || "inherit");
428
+ setVar("--trae-skin-font-code", typography.code?.family || "monospace");
429
+ setVar("--trae-skin-heading-weight", typography.heading?.weight || 600);
430
+ setVar("--trae-skin-heading-spacing", typography.heading?.letterSpacing || "normal");
431
+ setVar("--trae-skin-radius-small", asLength(radius.small, "6px"));
432
+ setVar("--trae-skin-radius-medium", asLength(radius.medium, "10px"));
433
+ setVar("--trae-skin-radius-large", asLength(radius.large, "14px"));
434
+ setVar("--trae-skin-radius-pill", asLength(radius.pill, "999px"));
435
+ setVar("--trae-skin-shadow-card", elevation.card || "0 8px 24px #00000026");
436
+ setVar("--trae-skin-shadow-floating", elevation.floating || "0 16px 48px #00000073");
437
+ setVar("--trae-skin-focus-ring", elevation.focusRing || `0 0 0 2px ${accent.base}`);
438
+
439
+ setVar("--trae-skin-scrollbar-track", scrollbar.track || "transparent");
440
+ setVar("--trae-skin-scrollbar-thumb", scrollbar.thumb || border.default);
441
+ setVar("--trae-skin-scrollbar-hover", scrollbar.thumbHover || accent.base);
442
+ setVar("--trae-skin-scrollbar-width", asLength(scrollbar.width, "8px"));
443
+
444
+ setVar("--trae-skin-chat-user-bubble", chatC.userBubble || surface.card);
445
+ setVar("--trae-skin-chat-user-border", chatC.userBubbleBorder || border.subtle);
446
+ setVar("--trae-skin-chat-code", chatC.code || surface.input);
447
+ setVar("--trae-skin-chat-card", chatC.card || surface.card);
448
+ setVar("--trae-skin-chat-assistant-message", chatC.assistantMessage || "transparent");
449
+ setVar("--trae-skin-chat-assistant-border", chatC.assistantMessageBorder || "transparent");
450
+ setVar("--trae-skin-chat-assistant-shadow", chatC.assistantMessageShadow || "none");
451
+ setVar("--trae-skin-popover", popoverC.background || surface.menu);
452
+ setVar("--trae-skin-popover-hover", popoverC.itemHover || withAlpha(state.info, 0.1));
453
+ setVar("--trae-skin-popover-selected", popoverC.itemSelected || accent.subtle);
454
+
455
+ setVar("--trae-skin-settings-overlay", settingsC.overlay || withAlpha(surface.base, 0.72));
456
+ setVar("--trae-skin-settings-panel", settingsC.panel || surface.menu);
457
+ setVar("--trae-skin-settings-sidebar", settingsC.sidebar || surface.secondary);
458
+ setVar("--trae-skin-settings-card", settingsC.card || surface.card);
459
+ setVar("--trae-skin-settings-control", settingsC.control || surface.input);
460
+ setVar("--trae-skin-settings-active", settingsC.active || accent.subtle);
461
+
462
+ document.body.dataset.traeSkinLandingBanner = bannerC.variant;
463
+ document.body.dataset.traeSkinModeTabs = modeTabsC.variant;
464
+ document.body.dataset.traeSkinModeTabIcons = modeTabsC.content.iconPolicy;
465
+ document.body.dataset.traeSkinModeTabCodes = String(modeTabsC.content.showModeCode);
466
+ setVar("--trae-skin-banner-surface", bannerC.colors.surface);
467
+ setVar("--trae-skin-banner-border", bannerC.colors.border);
468
+ setVar("--trae-skin-banner-accent", bannerC.colors.accent);
469
+ setVar("--trae-skin-banner-text", bannerC.colors.text);
470
+ setVar("--trae-skin-banner-muted", bannerC.colors.muted);
471
+ setVar("--trae-skin-banner-status-color", bannerC.colors.status);
472
+ setVar("--trae-skin-banner-shadow", bannerC.effects.shadow);
473
+ setVar("--trae-skin-banner-corner-cut", `${bannerC.metrics.cornerCutPx}px`);
474
+ setVar("--trae-skin-banner-max-width", `${bannerC.metrics.maxWidthPx}px`);
475
+ setVar("--trae-skin-banner-icon-size", `${bannerC.metrics.iconSizePx}px`);
476
+ setVar("--trae-skin-banner-code-work", asCssString(bannerC.content.modes.work.code));
477
+ setVar("--trae-skin-banner-code-code", asCssString(bannerC.content.modes.code.code));
478
+ setVar("--trae-skin-banner-code-design", asCssString(bannerC.content.modes.design.code));
479
+ setVar("--trae-skin-banner-label-work", asCssString(bannerC.content.modes.work.label));
480
+ setVar("--trae-skin-banner-label-code", asCssString(bannerC.content.modes.code.label));
481
+ setVar("--trae-skin-banner-label-design", asCssString(bannerC.content.modes.design.label));
482
+ setVar("--trae-skin-banner-status", asCssString(bannerC.content.statusText));
483
+ setVar("--trae-skin-mode-tabs-track", modeTabsC.colors.track);
484
+ setVar("--trae-skin-mode-tabs-border", modeTabsC.colors.border);
485
+ setVar("--trae-skin-mode-tabs-indicator", modeTabsC.colors.indicator);
486
+ setVar("--trae-skin-mode-tabs-accent", modeTabsC.colors.accent);
487
+ setVar("--trae-skin-mode-tabs-active-text", modeTabsC.colors.activeText);
488
+ setVar("--trae-skin-mode-tabs-inactive-text", modeTabsC.colors.inactiveText);
489
+ setVar("--trae-skin-mode-tabs-hover", modeTabsC.colors.hover);
490
+ setVar("--trae-skin-mode-tabs-shadow", modeTabsC.effects.shadow);
491
+ setVar("--trae-skin-mode-tabs-height", `${modeTabsC.metrics.heightPx}px`);
492
+ setVar("--trae-skin-mode-tabs-tab-width", `${modeTabsC.metrics.tabWidthPx}px`);
493
+ setVar("--trae-skin-mode-tabs-gap", `${modeTabsC.metrics.gapPx}px`);
494
+ setVar("--trae-skin-mode-tabs-corner-cut", `${modeTabsC.metrics.cornerCutPx}px`);
495
+ setVar("--trae-skin-mode-tabs-code-work", asCssString(modeTabsC.content.modes.work.code));
496
+ setVar("--trae-skin-mode-tabs-code-code", asCssString(modeTabsC.content.modes.code.code));
497
+ setVar("--trae-skin-mode-tabs-code-design", asCssString(modeTabsC.content.modes.design.code));
498
+
499
+ // 侧栏/右栏装饰图(与 v2 extensions.decorations 同语义,v3 移到顶层 decorations)
500
+ const setDecorationV3 = (name, asset, config, fallbackColor) => {
501
+ if (!asset || !config) return;
502
+ const overlay = withAlpha(config.overlay?.color || fallbackColor, config.overlay?.opacity ?? 0.42);
503
+ setVar(`--trae-skin-${name}-art-layer`, `linear-gradient(${overlay}, ${overlay}), url("${asset}")`);
504
+ setVar(`--trae-skin-${name}-art-position`, config.position || "center");
505
+ setVar(`--trae-skin-${name}-art-size`, config.size || "cover");
506
+ };
507
+ setDecorationV3("left", theme.assets?.leftSidebar, decorations.leftSidebar, surface.secondary);
508
+ setDecorationV3("right", theme.assets?.rightPanel, decorations.rightPanel, surface.base);
509
+
510
+ setVar("--trae-skin-workbench-sidebar", withAlpha(surface.secondary, workbenchOpacity.sidebar ?? 0.72));
511
+ setVar("--trae-skin-workbench-editor", withAlpha(surface.base, centerOpacity(workbenchOpacity.editor ?? 0.76)));
512
+ setVar("--trae-skin-workbench-panel", withAlpha(surface.secondary, workbenchOpacity.panel ?? 0.72));
513
+ return componentRoles;
514
+ };
515
+
516
+ // ---------- v3:图标 glyph 替换(CSS mask,颜色跟随 currentColor) ----------
517
+ const applyIconOverrides = (settings) => {
518
+ document.getElementById(ICONS_STYLE_ID)?.remove();
519
+ const overrides = settings.icons?.overrides || {};
520
+ const assets = settings.icons?.assets || {};
521
+ const rules = [];
522
+ for (const [key, spec] of Object.entries(overrides)) {
523
+ const art = assets[key];
524
+ if (!art) continue;
525
+ const selector = (typeof spec === "object" && spec?.selector)
526
+ ? spec.selector
527
+ : key.startsWith("codicon-") ? `.${key}` : `svg.trae-icon-${key}`;
528
+ const mask = `url("${art}") center / contain no-repeat`;
529
+ if (key.startsWith("codicon-") || (typeof spec === "object" && spec?.pseudo)) {
530
+ rules.push(`${selector}::before {
531
+ content: "" !important; display: inline-block !important;
532
+ width: 1em; height: 1em; background: currentColor !important;
533
+ -webkit-mask: ${mask} !important; mask: ${mask} !important;
534
+ -webkit-mask-mode: alpha !important; mask-mode: alpha !important;
535
+ }`);
536
+ } else {
537
+ rules.push(`${selector} * { display: none !important; }
538
+ ${selector} {
539
+ background: currentColor !important;
540
+ -webkit-mask: ${mask} !important; mask: ${mask} !important;
541
+ -webkit-mask-mode: alpha !important; mask-mode: alpha !important;
542
+ }`);
543
+ }
544
+ }
545
+ if (!rules.length) return 0;
546
+ const el = document.createElement("style");
547
+ el.id = ICONS_STYLE_ID;
548
+ el.textContent = rules.join("\n");
549
+ (document.head || document.documentElement).appendChild(el);
550
+ return rules.length;
551
+ };
552
+
553
+ // ---------- v3 主入口:握手 → 扇出 → 自有层 → 护栏 ----------
554
+ const applyV3Theme = (theme, settings) => {
555
+ const appearance = settings.appearance === "light" ? "light" : "dark";
556
+ setAppearance(appearance);
557
+ ensureAppearanceObserver();
558
+
559
+ // 角色 → 全命名空间扇出,写到 <html>(覆盖 :root 与 [data-theme] 定义)
560
+ const tokens = settings.tokens || {};
561
+ const varMap = TOKEN_MAP.buildVarMap(tokens, { appearance });
562
+ const htmlStyle = document.documentElement.style;
563
+ const applied = [];
564
+ for (const [name, value] of Object.entries(varMap)) {
565
+ htmlStyle.setProperty(name, value);
566
+ applied.push(name);
567
+ }
568
+ window.__TRAE_DREAM_SKIN_HTML_VARS__ = applied;
569
+
570
+ // App 还在 body / .solo-theme / body.icube-chat-next 上重定义令牌(如 --bg-bg-overlay-l1),
571
+ // 按最近祖先原则会遮蔽 <html> 内联值——用带 !important 的作用域规则压过它们
572
+ document.getElementById(SCOPE_STYLE_ID)?.remove();
573
+ const scopeStyle = document.createElement("style");
574
+ scopeStyle.id = SCOPE_STYLE_ID;
575
+ scopeStyle.textContent = `body, .solo-theme, body.icube-chat-next {\n${Object.entries(varMap)
576
+ .map(([name, value]) => ` ${name}: ${value} !important;`)
577
+ .join("\n")}\n}`;
578
+ (document.head || document.documentElement).appendChild(scopeStyle);
579
+
580
+ const roles = TOKEN_MAP.deriveRoles(tokens, { appearance });
581
+ const componentRoles = setTraeSkinVarsV3(roles, settings, theme);
582
+ const iconCount = applyIconOverrides(settings);
583
+
584
+ document.body.classList.add("trae-skin-v2", "trae-skin-v3");
585
+ document.body.classList.toggle("trae-skin-appearance-dark", appearance === "dark");
586
+
587
+ const componentAudit = TOKEN_MAP.auditPairs(COMPONENT_MAP.getContrastPairs(componentRoles));
588
+ const warnings = [
589
+ ...TOKEN_MAP.auditContrast(tokens, { appearance }),
590
+ ...componentAudit.failures,
591
+ ];
592
+ if (warnings.length) console.warn("[twskin] contrast warnings:", JSON.stringify(warnings));
593
+ if (componentAudit.unverifiable.length) {
594
+ console.warn("[twskin] contrast unverifiable:", JSON.stringify(componentAudit.unverifiable));
595
+ }
596
+ return {
597
+ contrastWarnings: warnings.length,
598
+ contrastUnverifiable: componentAudit.unverifiable.length,
599
+ iconOverrides: iconCount,
600
+ };
601
+ };
602
+
603
+ const apply = (id) => {
604
+ const theme = findTheme(id);
605
+ if (!theme) return { ok: false, error: `unknown theme: ${id}` };
606
+ clearManagedVars();
607
+ document.body.classList.remove(
608
+ "trae-skin-v2",
609
+ "trae-skin-v3",
610
+ "trae-skin-appearance-dark",
611
+ "trae-skin-effects-max",
612
+ "trae-skin-blur-disabled",
613
+ );
614
+ if (activeThemeClass) document.body.classList.remove(activeThemeClass);
615
+ activeThemeClass = `trae-skin-theme-${theme.id.replace(/[^a-z0-9_-]/gi, "-")}`;
616
+ themeStyle.textContent = theme.customCss || "";
617
+ if (!themeStyle.isConnected) (document.head || document.documentElement).appendChild(themeStyle);
618
+ document.body.classList.add(activeThemeClass);
619
+ setVar("--trae-skin-art", `url("${theme.art}")`);
620
+ const blurEnabled = panelBlurEnabled(theme);
621
+ const brightness = backgroundBrightness(theme);
622
+ const mainOverlay = mainOverlayOpacity(theme);
623
+ document.body.classList.toggle("trae-skin-blur-disabled", !blurEnabled);
624
+
625
+ const settings = theme.settings || {};
626
+ const v3Result = applyV3Theme(theme, settings);
627
+
628
+ try { localStorage.setItem(LS_KEY, id); } catch {}
629
+ panel?.querySelectorAll(".ds-card").forEach((card) => {
630
+ card.classList.toggle("ds-active", card.dataset.themeId === id);
631
+ });
632
+ renderCurrentConfig(theme);
633
+ renderBlurControl(theme, blurEnabled);
634
+ renderBrightnessControl(theme, brightness);
635
+ renderMainOverlayControl(theme, mainOverlay);
636
+ return {
637
+ ok: true,
638
+ id,
639
+ schemaVersion: theme.settings?.schemaVersion || 1,
640
+ panelBlurEnabled: blurEnabled,
641
+ backgroundBrightness: brightness,
642
+ mainOverlayOpacity: mainOverlay ?? defaultMainOverlayOpacity(theme),
643
+ customCss: Boolean(theme.customCss),
644
+ ...(v3Result || {}),
645
+ };
646
+ };
647
+
648
+ // 画廊面板
649
+ const panel = document.createElement("div");
650
+ panel.id = PANEL_ID;
651
+ panel.style.display = "none";
652
+ const header = document.createElement("div");
653
+ header.className = "ds-header";
654
+ const headerText = document.createElement("div");
655
+ headerText.className = "ds-header-text";
656
+ const title = document.createElement("div");
657
+ title.className = "ds-title";
658
+ title.textContent = "主题画廊";
659
+ const currentLabel = document.createElement("div");
660
+ currentLabel.className = "ds-current";
661
+ headerText.append(title, currentLabel);
662
+ const headerActions = document.createElement("div");
663
+ headerActions.className = "ds-header-actions";
664
+ const configToggle = document.createElement("button");
665
+ configToggle.className = "ds-config-toggle";
666
+ configToggle.type = "button";
667
+ configToggle.textContent = "配置";
668
+ configToggle.title = "查看当前主题配置";
669
+ const reloadButton = document.createElement("button");
670
+ reloadButton.className = "ds-reload";
671
+ reloadButton.type = "button";
672
+ reloadButton.innerHTML = '<span class="ds-reload-icon">↻</span>';
673
+ reloadButton.title = "重新加载主题目录(新主题热加载)";
674
+ const closeButton = document.createElement("button");
675
+ closeButton.className = "ds-close";
676
+ closeButton.type = "button";
677
+ closeButton.textContent = "×";
678
+ closeButton.title = "关闭 (Esc)";
679
+ headerActions.append(reloadButton, configToggle, closeButton);
680
+ header.append(headerText, headerActions);
681
+ const panelBody = document.createElement("div");
682
+ panelBody.className = "ds-body";
683
+
684
+ const galleryView = document.createElement("div");
685
+ galleryView.className = "ds-gallery-view";
686
+ const configView = document.createElement("div");
687
+ configView.className = "ds-config-view";
688
+ const configHead = document.createElement("div");
689
+ configHead.className = "ds-config-head";
690
+ const configName = document.createElement("div");
691
+ configName.className = "ds-config-name";
692
+ const copyConfig = document.createElement("button");
693
+ copyConfig.className = "ds-copy-config";
694
+ copyConfig.type = "button";
695
+ copyConfig.textContent = "复制 JSON";
696
+ const configCode = document.createElement("pre");
697
+ configCode.className = "ds-config-code";
698
+ const blurRow = document.createElement("div");
699
+ blurRow.className = "ds-setting-row";
700
+ const blurCopy = document.createElement("div");
701
+ blurCopy.className = "ds-setting-copy";
702
+ const blurLabel = document.createElement("div");
703
+ blurLabel.className = "ds-setting-label";
704
+ blurLabel.textContent = "启用毛玻璃效果";
705
+ blurCopy.append(blurLabel);
706
+ const blurSwitch = document.createElement("button");
707
+ blurSwitch.className = "ds-switch";
708
+ blurSwitch.type = "button";
709
+ blurSwitch.setAttribute("role", "switch");
710
+ blurSwitch.setAttribute("aria-label", "面板模糊");
711
+ blurRow.append(blurCopy, blurSwitch);
712
+ const brightnessRow = document.createElement("div");
713
+ brightnessRow.className = "ds-setting-row ds-setting-row-slider";
714
+ const brightnessHead = document.createElement("div");
715
+ brightnessHead.className = "ds-slider-head";
716
+ const brightnessCopy = document.createElement("div");
717
+ brightnessCopy.className = "ds-setting-copy";
718
+ const brightnessLabel = document.createElement("label");
719
+ brightnessLabel.className = "ds-setting-label";
720
+ brightnessLabel.htmlFor = "trae-dream-skin-brightness";
721
+ brightnessLabel.textContent = "背景亮度";
722
+ const brightnessDesc = document.createElement("div");
723
+ brightnessDesc.className = "ds-setting-desc";
724
+ brightnessDesc.textContent = "只调整壁纸,不改变面板与文字";
725
+ brightnessCopy.append(brightnessLabel, brightnessDesc);
726
+ const brightnessMeta = document.createElement("div");
727
+ brightnessMeta.className = "ds-slider-meta";
728
+ const brightnessValue = document.createElement("output");
729
+ brightnessValue.className = "ds-slider-value";
730
+ brightnessValue.setAttribute("for", "trae-dream-skin-brightness");
731
+ const brightnessReset = document.createElement("button");
732
+ brightnessReset.className = "ds-slider-reset";
733
+ brightnessReset.type = "button";
734
+ brightnessReset.textContent = "默认";
735
+ brightnessReset.title = "恢复当前主题的默认背景亮度";
736
+ brightnessMeta.append(brightnessValue, brightnessReset);
737
+ brightnessHead.append(brightnessCopy, brightnessMeta);
738
+ const brightnessRange = document.createElement("input");
739
+ brightnessRange.id = "trae-dream-skin-brightness";
740
+ brightnessRange.className = "ds-slider";
741
+ brightnessRange.type = "range";
742
+ brightnessRange.min = "0";
743
+ brightnessRange.max = "200";
744
+ brightnessRange.step = "1";
745
+ brightnessRange.setAttribute("aria-label", "背景亮度");
746
+ brightnessRow.append(brightnessHead, brightnessRange);
747
+ const mainOverlayRow = brightnessRow.cloneNode(true);
748
+ const mainOverlayRange = mainOverlayRow.querySelector("input");
749
+ const mainOverlayValue = mainOverlayRow.querySelector("output");
750
+ const mainOverlayReset = mainOverlayRow.querySelector("button");
751
+ const mainOverlayId = "trae-dream-skin-main-overlay";
752
+ mainOverlayRow.querySelector("label").htmlFor = mainOverlayId;
753
+ mainOverlayRow.querySelector("label").textContent = "主区域遮罩";
754
+ mainOverlayRow.querySelector(".ds-setting-desc").textContent = "调整中间工作区覆盖壁纸的透明度";
755
+ mainOverlayRange.id = mainOverlayId;
756
+ mainOverlayRange.max = "100";
757
+ mainOverlayRange.setAttribute("aria-label", "主区域遮罩透明度");
758
+ mainOverlayValue.setAttribute("for", mainOverlayId);
759
+ const resetConfig = document.createElement("button");
760
+ resetConfig.className = "ds-reset-config";
761
+ resetConfig.type = "button";
762
+ resetConfig.textContent = "恢复默认";
763
+ resetConfig.title = "移除 TRAE Work Skin,恢复 TRAE 原生主题";
764
+ const resetCancel = document.createElement("button");
765
+ resetCancel.className = "ds-reset-cancel";
766
+ resetCancel.type = "button";
767
+ resetCancel.textContent = "取消";
768
+ const resetActions = document.createElement("div");
769
+ resetActions.className = "ds-reset-actions";
770
+ resetActions.append(resetCancel, resetConfig);
771
+ configHead.append(configName, copyConfig);
772
+ configView.append(configHead, brightnessRow, mainOverlayRow, blurRow, configCode);
773
+ panelBody.append(galleryView, configView);
774
+
775
+ // 分类 Tab(固定在头部下,不随列表滚动;选择持久化)
776
+ const CATEGORY_LS = `${LS_PREFIX}category`;
777
+ const ALL_LABEL = "全部";
778
+ const categories = [ALL_LABEL, ...new Set(CATALOG.map((t) => t.category || "其他"))];
779
+ const tabsBar = document.createElement("div");
780
+ tabsBar.className = "ds-tabs";
781
+ let activeCategory = ALL_LABEL;
782
+ try {
783
+ const saved = localStorage.getItem(CATEGORY_LS);
784
+ if (saved && categories.includes(saved)) activeCategory = saved;
785
+ } catch {}
786
+ const applyCategoryFilter = () => {
787
+ galleryView.querySelectorAll(".ds-card").forEach((card) => {
788
+ card.style.display = (activeCategory === ALL_LABEL || card.dataset.category === activeCategory) ? "" : "none";
789
+ });
790
+ tabsBar.querySelectorAll(".ds-tab").forEach((tab) => {
791
+ tab.classList.toggle("ds-tab-active", tab.dataset.category === activeCategory);
792
+ });
793
+ try { localStorage.setItem(CATEGORY_LS, activeCategory); } catch {}
794
+ };
795
+ for (const cat of categories) {
796
+ const tab = document.createElement("button");
797
+ tab.className = "ds-tab";
798
+ tab.type = "button";
799
+ tab.dataset.category = cat;
800
+ tab.textContent = cat;
801
+ tab.addEventListener("click", (event) => {
802
+ event.stopPropagation();
803
+ activeCategory = cat;
804
+ applyCategoryFilter();
805
+ });
806
+ tabsBar.appendChild(tab);
807
+ }
808
+
809
+ const footer = document.createElement("div");
810
+ footer.className = "ds-footer";
811
+ const footerText = document.createElement("span");
812
+ footerText.className = "ds-footer-text";
813
+ footerText.textContent = `TRAE Work Skin v${VERSION}`;
814
+ footer.append(footerText, resetActions);
815
+ panel.append(header, tabsBar, panelBody, footer);
816
+
817
+ let configOpen = false;
818
+ const showConfig = (open) => {
819
+ configOpen = Boolean(open);
820
+ // 配置视图只展示当前主题设置;返回画廊时再恢复分类栏和主题列表。
821
+ tabsBar.style.display = configOpen ? "none" : "";
822
+ galleryView.style.display = configOpen ? "none" : "";
823
+ configView.style.display = configOpen ? "block" : "";
824
+ configToggle.textContent = configOpen ? "主题" : "配置";
825
+ configToggle.title = configOpen ? "返回主题列表" : "查看当前主题配置";
826
+ };
827
+ renderCurrentConfig = (theme = findTheme(
828
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
829
+ )) => {
830
+ if (!theme) return;
831
+ const config = publicThemeConfig(theme);
832
+ configName.textContent = `${theme.name} · ${theme.id}`;
833
+ currentLabel.textContent = `当前:${theme.name}`;
834
+ configCode.textContent = JSON.stringify(config, null, 2);
835
+ };
836
+ renderBlurControl = (theme = findTheme(
837
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
838
+ ), enabled = theme ? panelBlurEnabled(theme) : false) => {
839
+ if (!theme) return;
840
+ blurSwitch.setAttribute("aria-checked", String(Boolean(enabled)));
841
+ blurSwitch.title = enabled ? "关闭毛玻璃效果" : "启用毛玻璃效果";
842
+ };
843
+ renderBrightnessControl = (theme = findTheme(
844
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
845
+ ), value = theme ? backgroundBrightness(theme) : 1) => {
846
+ if (!theme) return;
847
+ const percent = Math.round(asNumber(value, defaultBackgroundBrightness(theme), 0, 2) * 100);
848
+ brightnessRange.value = String(percent);
849
+ brightnessRange.style.setProperty("--ds-slider-fill", `${percent / 2}%`);
850
+ brightnessRange.setAttribute("aria-valuetext", `${percent}%`);
851
+ brightnessValue.value = `${percent}%`;
852
+ brightnessValue.textContent = `${percent}%`;
853
+ brightnessReset.disabled = Math.abs(value - defaultBackgroundBrightness(theme)) < 0.001;
854
+ };
855
+ renderMainOverlayControl = (theme = findTheme(
856
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
857
+ ), saved = theme ? mainOverlayOpacity(theme) : null) => {
858
+ if (!theme) return;
859
+ const percent = Math.round((saved ?? defaultMainOverlayOpacity(theme)) * 100);
860
+ mainOverlayRange.value = String(percent);
861
+ mainOverlayRange.style.setProperty("--ds-slider-fill", `${percent}%`);
862
+ mainOverlayRange.setAttribute("aria-valuetext", `${percent}%`);
863
+ mainOverlayValue.value = `${percent}%`;
864
+ mainOverlayValue.textContent = `${percent}%`;
865
+ mainOverlayReset.disabled = saved == null;
866
+ };
867
+ configToggle.addEventListener("click", (event) => {
868
+ event.stopPropagation();
869
+ renderCurrentConfig();
870
+ showConfig(!configOpen);
871
+ });
872
+ copyConfig.addEventListener("click", async (event) => {
873
+ event.stopPropagation();
874
+ const original = copyConfig.textContent;
875
+ try {
876
+ await navigator.clipboard.writeText(configCode.textContent);
877
+ copyConfig.textContent = "已复制";
878
+ } catch {
879
+ const selection = getSelection();
880
+ const range = document.createRange();
881
+ range.selectNodeContents(configCode);
882
+ selection.removeAllRanges();
883
+ selection.addRange(range);
884
+ copyConfig.textContent = "已选中";
885
+ }
886
+ setTimeout(() => { copyConfig.textContent = original; }, 1200);
887
+ });
888
+ blurSwitch.addEventListener("click", (event) => {
889
+ event.stopPropagation();
890
+ const current = findTheme(
891
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
892
+ );
893
+ if (!current) return;
894
+ savePanelBlur(current, !panelBlurEnabled(current));
895
+ apply(current.id);
896
+ });
897
+ brightnessRange.addEventListener("input", (event) => {
898
+ event.stopPropagation();
899
+ const current = findTheme(
900
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
901
+ );
902
+ if (!current) return;
903
+ const value = saveBackgroundBrightness(current, Number(brightnessRange.value) / 100);
904
+ setVar("--trae-skin-background-brightness", value);
905
+ renderBrightnessControl(current, value);
906
+ });
907
+ brightnessReset.addEventListener("click", (event) => {
908
+ event.stopPropagation();
909
+ const current = findTheme(
910
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
911
+ );
912
+ if (!current) return;
913
+ const value = resetBackgroundBrightness(current);
914
+ setVar("--trae-skin-background-brightness", value);
915
+ renderBrightnessControl(current, value);
916
+ });
917
+ mainOverlayRange.addEventListener("input", (event) => {
918
+ event.stopPropagation();
919
+ const current = findTheme((() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })());
920
+ if (!current) return;
921
+ saveMainOverlayOpacity(current, Number(mainOverlayRange.value) / 100);
922
+ apply(current.id);
923
+ });
924
+ mainOverlayReset.addEventListener("click", (event) => {
925
+ event.stopPropagation();
926
+ const current = findTheme((() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })());
927
+ if (!current) return;
928
+ resetMainOverlayOpacity(current);
929
+ apply(current.id);
930
+ });
931
+ let resetArmed = false;
932
+ let resetTimer = null;
933
+ const disarmReset = () => {
934
+ resetArmed = false;
935
+ clearTimeout(resetTimer);
936
+ resetConfig.classList.remove("ds-reset-armed");
937
+ resetCancel.classList.remove("ds-reset-cancel-visible");
938
+ resetConfig.textContent = "恢复默认";
939
+ };
940
+ resetCancel.addEventListener("click", (event) => {
941
+ event.stopPropagation();
942
+ disarmReset();
943
+ });
944
+ resetConfig.addEventListener("click", (event) => {
945
+ event.stopPropagation();
946
+ if (!resetArmed) {
947
+ resetArmed = true;
948
+ resetConfig.classList.add("ds-reset-armed");
949
+ resetCancel.classList.add("ds-reset-cancel-visible");
950
+ resetConfig.textContent = "确认恢复";
951
+ resetTimer = setTimeout(disarmReset, 8000);
952
+ return;
953
+ }
954
+ disarmReset();
955
+ restoreNativeTheme();
956
+ });
957
+
958
+ const activateTheme = (id) => {
959
+ if (disabled) {
960
+ try { localStorage.removeItem(DISABLED_KEY); } catch {}
961
+ disabled = false;
962
+ if (!uiStyle.isConnected) (document.head || document.documentElement).appendChild(uiStyle);
963
+ if (!style.isConnected) (document.head || document.documentElement).appendChild(style);
964
+ if (!themeStyle.isConnected) (document.head || document.documentElement).appendChild(themeStyle);
965
+ configToggle.style.display = "";
966
+ button.title = "TRAE Work Skin 主题画廊";
967
+ ensureSelfHeal();
968
+ }
969
+ return apply(id);
970
+ };
971
+
972
+ for (const theme of CATALOG) {
973
+ const card = document.createElement("div");
974
+ card.className = "ds-card";
975
+ card.dataset.themeId = theme.id;
976
+ card.dataset.category = theme.category || "其他";
977
+ const badge = document.createElement("span");
978
+ badge.className = "ds-badge";
979
+ badge.textContent = "✓ 使用中";
980
+ const preview = document.createElement("div");
981
+ preview.className = "ds-preview";
982
+ preview.style.backgroundImage = `url("${theme.art}")`;
983
+ const meta = document.createElement("div");
984
+ meta.className = "ds-meta";
985
+ const name = document.createElement("span");
986
+ name.className = "ds-name";
987
+ name.textContent = theme.name;
988
+ meta.appendChild(name);
989
+ if (theme.desc) {
990
+ const desc = document.createElement("span");
991
+ desc.className = "ds-desc";
992
+ desc.textContent = theme.desc;
993
+ desc.title = theme.desc;
994
+ meta.appendChild(desc);
995
+ }
996
+ card.append(preview, meta, badge);
997
+ card.addEventListener("click", () => activateTheme(theme.id));
998
+ galleryView.appendChild(card);
999
+ }
1000
+ applyCategoryFilter();
1001
+
1002
+ const button = document.createElement("button");
1003
+ button.id = BUTTON_ID;
1004
+ button.title = "TRAE Work Skin 主题画廊";
1005
+ button.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 0 18c1.1 0 2-.9 2-2 0-.5-.2-1-.5-1.3-.3-.4-.5-.8-.5-1.3 0-1.1.9-2 2-2h2.4A4.6 4.6 0 0 0 22 9.9C21.6 6 17.2 3 12 3zm-5.5 9a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm3-4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm3 4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/></svg>';
1006
+ const setPanelOpen = (open) => {
1007
+ if (open && !panel.isConnected) document.body.appendChild(panel);
1008
+ panel.style.display = open ? "flex" : "none";
1009
+ };
1010
+ const isPanelOpen = () => panel.style.display !== "none";
1011
+ const toggle = () => setPanelOpen(!isPanelOpen());
1012
+ button.addEventListener("click", (e) => {
1013
+ e.stopPropagation();
1014
+ if (disabled) {
1015
+ if (!panel.isConnected) document.body.appendChild(panel);
1016
+ showConfig(false);
1017
+ setPanelOpen(!isPanelOpen());
1018
+ return;
1019
+ }
1020
+ toggle();
1021
+ });
1022
+ closeButton.addEventListener("click", (e) => {
1023
+ e.stopPropagation();
1024
+ setPanelOpen(false);
1025
+ });
1026
+ reloadButton.addEventListener("click", (e) => {
1027
+ e.stopPropagation();
1028
+ if (reloadButton.classList.contains("ds-reload-loading")) return;
1029
+ reloadButton.classList.add("ds-reload-loading");
1030
+ // 守护进程轮询到这个标记后会重建主题目录、重新注入并重新打开面板
1031
+ window.__TRAE_DREAM_SKIN_RELOAD_REQUEST__ = Date.now();
1032
+ setTimeout(() => reloadButton.classList.remove("ds-reload-loading"), 6000);
1033
+ });
1034
+ document.addEventListener("click", (e) => {
1035
+ if (isPanelOpen() && !panel.contains(e.target)) setPanelOpen(false);
1036
+ });
1037
+ document.addEventListener("keydown", (e) => {
1038
+ if (e.key === "Escape") setPanelOpen(false);
1039
+ });
1040
+
1041
+ if (disabled) {
1042
+ configToggle.style.display = "none";
1043
+ document.body.append(button);
1044
+ } else {
1045
+ document.body.append(panel, button);
1046
+ }
1047
+
1048
+ // 初始主题:localStorage 里的选择优先,其次 injector 给的默认
1049
+ if (!disabled) {
1050
+ let initial = null;
1051
+ try { initial = localStorage.getItem(LS_KEY); } catch {}
1052
+ apply(findTheme(initial) ? initial : DEFAULT_THEME);
1053
+ }
1054
+
1055
+ window.__TRAE_DREAM_SKIN__ = VERSION;
1056
+ window.__TRAE_DREAM_SKIN_GALLERY__ = {
1057
+ version: VERSION,
1058
+ apply: activateTheme,
1059
+ open: () => setPanelOpen(true),
1060
+ close: () => setPanelOpen(false),
1061
+ current: () => { try { return localStorage.getItem(LS_KEY); } catch { return null; } },
1062
+ config: () => {
1063
+ const current = findTheme(
1064
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
1065
+ );
1066
+ return current ? publicThemeConfig(current) : null;
1067
+ },
1068
+ panelBlur: (enabled) => {
1069
+ const current = findTheme(
1070
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
1071
+ );
1072
+ if (!current) return null;
1073
+ if (typeof enabled === "boolean") {
1074
+ savePanelBlur(current, enabled);
1075
+ apply(current.id);
1076
+ }
1077
+ return panelBlurEnabled(current);
1078
+ },
1079
+ backgroundBrightness: (value) => {
1080
+ const current = findTheme(
1081
+ (() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })(),
1082
+ );
1083
+ if (!current) return null;
1084
+ if (typeof value === "number") {
1085
+ const next = saveBackgroundBrightness(current, value);
1086
+ setVar("--trae-skin-background-brightness", next);
1087
+ renderBrightnessControl(current, next);
1088
+ }
1089
+ return backgroundBrightness(current);
1090
+ },
1091
+ mainOverlayOpacity: (value) => {
1092
+ const current = findTheme((() => { try { return localStorage.getItem(LS_KEY); } catch { return null; } })());
1093
+ if (!current) return null;
1094
+ if (typeof value === "number") {
1095
+ saveMainOverlayOpacity(current, value);
1096
+ apply(current.id);
1097
+ }
1098
+ return mainOverlayOpacity(current) ?? defaultMainOverlayOpacity(current);
1099
+ },
1100
+ restoreNative: restoreNativeTheme,
1101
+ showConfig: () => {
1102
+ renderCurrentConfig();
1103
+ showConfig(true);
1104
+ setPanelOpen(true);
1105
+ },
1106
+ themes: () => CATALOG.map((t) => t.id),
1107
+ };
1108
+
1109
+ // 自愈:节点被 SPA 清掉时清空标记,让 injector 重新注入
1110
+ // 恢复默认后主题样式不存在是正常的,此时改看常驻的管理器样式
1111
+ ensureSelfHeal = () => {
1112
+ if (window.__TRAE_DREAM_SKIN_OBS__) return;
1113
+ const obs = new MutationObserver(() => {
1114
+ const requiredIds = disabled ? [UI_STYLE_ID] : [STYLE_ID, THEME_STYLE_ID];
1115
+ if (requiredIds.some((id) => !document.getElementById(id))) {
1116
+ window.__TRAE_DREAM_SKIN__ = null;
1117
+ obs.disconnect();
1118
+ window.__TRAE_DREAM_SKIN_OBS__ = null;
1119
+ }
1120
+ });
1121
+ obs.observe(document.documentElement, { childList: true, subtree: true });
1122
+ window.__TRAE_DREAM_SKIN_OBS__ = obs;
1123
+ };
1124
+ ensureSelfHeal();
1125
+
1126
+ return {
1127
+ ok: true,
1128
+ version: VERSION,
1129
+ themes: CATALOG.length,
1130
+ disabled,
1131
+ url: String(location.href),
1132
+ };
1133
+ })();