claude-relay 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/bin/cli.js +214 -9
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +3 -2
- package/lib/daemon.js +45 -1
- package/lib/pages.js +8 -1
- package/lib/project.js +121 -12
- package/lib/public/app.js +411 -87
- package/lib/public/css/base.css +41 -7
- package/lib/public/css/diff.css +6 -6
- package/lib/public/css/filebrowser.css +62 -52
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/input.css +11 -9
- package/lib/public/css/menus.css +82 -23
- package/lib/public/css/messages.css +183 -35
- package/lib/public/css/overlays.css +166 -50
- package/lib/public/css/rewind.css +17 -17
- package/lib/public/css/sidebar.css +210 -137
- package/lib/public/index.html +75 -42
- package/lib/public/modules/filebrowser.js +2 -1
- package/lib/public/modules/markdown.js +10 -10
- package/lib/public/modules/notifications.js +38 -1
- package/lib/public/modules/sidebar.js +109 -31
- package/lib/public/modules/terminal.js +84 -23
- package/lib/public/modules/theme.js +622 -0
- package/lib/public/modules/tools.js +247 -4
- package/lib/public/modules/utils.js +21 -5
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +95 -0
- package/lib/server.js +45 -3
- package/lib/sessions.js +16 -3
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/claude-light.json +9 -0
- package/lib/themes/claude.json +9 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/package.json +2 -1
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { setTerminalTheme } from './terminal.js';
|
|
2
|
+
import { updateMermaidTheme } from './markdown.js';
|
|
3
|
+
|
|
4
|
+
// --- Color utilities ---
|
|
5
|
+
|
|
6
|
+
function hexToRgb(hex) {
|
|
7
|
+
var h = hex.replace("#", "");
|
|
8
|
+
return {
|
|
9
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
10
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
11
|
+
b: parseInt(h.substring(4, 6), 16)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rgbToHex(r, g, b) {
|
|
16
|
+
return "#" + [r, g, b].map(function (v) {
|
|
17
|
+
var c = Math.max(0, Math.min(255, Math.round(v)));
|
|
18
|
+
return c.toString(16).padStart(2, "0");
|
|
19
|
+
}).join("");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function darken(hex, amount) {
|
|
23
|
+
var c = hexToRgb(hex);
|
|
24
|
+
var f = 1 - amount;
|
|
25
|
+
return rgbToHex(c.r * f, c.g * f, c.b * f);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function lighten(hex, amount) {
|
|
29
|
+
var c = hexToRgb(hex);
|
|
30
|
+
return rgbToHex(
|
|
31
|
+
c.r + (255 - c.r) * amount,
|
|
32
|
+
c.g + (255 - c.g) * amount,
|
|
33
|
+
c.b + (255 - c.b) * amount
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mixColors(hex1, hex2, weight) {
|
|
38
|
+
var c1 = hexToRgb(hex1);
|
|
39
|
+
var c2 = hexToRgb(hex2);
|
|
40
|
+
var w = weight;
|
|
41
|
+
return rgbToHex(
|
|
42
|
+
c1.r * w + c2.r * (1 - w),
|
|
43
|
+
c1.g * w + c2.g * (1 - w),
|
|
44
|
+
c1.b * w + c2.b * (1 - w)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hexToRgba(hex, alpha) {
|
|
49
|
+
var c = hexToRgb(hex);
|
|
50
|
+
return "rgba(" + c.r + ", " + c.g + ", " + c.b + ", " + alpha + ")";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function luminance(hex) {
|
|
54
|
+
var c = hexToRgb(hex);
|
|
55
|
+
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) / 255;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Claude default: exact CSS values for initial render (before API loads) ---
|
|
59
|
+
var claudeExactVars = {
|
|
60
|
+
"--bg": "#2F2E2B",
|
|
61
|
+
"--bg-alt": "#35332F",
|
|
62
|
+
"--text": "#E8E5DE",
|
|
63
|
+
"--text-secondary": "#B5B0A6",
|
|
64
|
+
"--text-muted": "#908B81",
|
|
65
|
+
"--text-dimmer": "#6D6860",
|
|
66
|
+
"--accent": "#DA7756",
|
|
67
|
+
"--accent-hover": "#E5886A",
|
|
68
|
+
"--accent-bg": "rgba(218, 119, 86, 0.12)",
|
|
69
|
+
"--code-bg": "#1E1D1A",
|
|
70
|
+
"--border": "#3E3C37",
|
|
71
|
+
"--border-subtle": "#36342F",
|
|
72
|
+
"--input-bg": "#393733",
|
|
73
|
+
"--user-bubble": "#46423A",
|
|
74
|
+
"--error": "#E5534B",
|
|
75
|
+
"--success": "#57AB5A",
|
|
76
|
+
"--warning": "#E5A84B",
|
|
77
|
+
"--sidebar-bg": "#262522",
|
|
78
|
+
"--sidebar-hover": "#302E2A",
|
|
79
|
+
"--sidebar-active": "#3A3834",
|
|
80
|
+
"--accent-8": "rgba(218, 119, 86, 0.08)",
|
|
81
|
+
"--accent-12": "rgba(218, 119, 86, 0.12)",
|
|
82
|
+
"--accent-15": "rgba(218, 119, 86, 0.15)",
|
|
83
|
+
"--accent-20": "rgba(218, 119, 86, 0.20)",
|
|
84
|
+
"--accent-25": "rgba(218, 119, 86, 0.25)",
|
|
85
|
+
"--accent-30": "rgba(218, 119, 86, 0.30)",
|
|
86
|
+
"--error-8": "rgba(229, 83, 75, 0.08)",
|
|
87
|
+
"--error-12": "rgba(229, 83, 75, 0.12)",
|
|
88
|
+
"--error-15": "rgba(229, 83, 75, 0.15)",
|
|
89
|
+
"--error-25": "rgba(229, 83, 75, 0.25)",
|
|
90
|
+
"--success-8": "rgba(87, 171, 90, 0.08)",
|
|
91
|
+
"--success-12": "rgba(87, 171, 90, 0.12)",
|
|
92
|
+
"--success-15": "rgba(87, 171, 90, 0.15)",
|
|
93
|
+
"--success-25": "rgba(87, 171, 90, 0.25)",
|
|
94
|
+
"--warning-bg": "rgba(229, 168, 75, 0.12)",
|
|
95
|
+
"--overlay-rgb": "255,255,255",
|
|
96
|
+
"--shadow-rgb": "0,0,0",
|
|
97
|
+
"--hl-comment": "#6D6860",
|
|
98
|
+
"--hl-keyword": "#C586C0",
|
|
99
|
+
"--hl-string": "#57AB5A",
|
|
100
|
+
"--hl-number": "#DA7756",
|
|
101
|
+
"--hl-function": "#569CD6",
|
|
102
|
+
"--hl-variable": "#E5534B",
|
|
103
|
+
"--hl-type": "#E5A84B",
|
|
104
|
+
"--hl-constant": "#DA7756",
|
|
105
|
+
"--hl-tag": "#E5534B",
|
|
106
|
+
"--hl-attr": "#569CD6",
|
|
107
|
+
"--hl-regexp": "#4EC9B0",
|
|
108
|
+
"--hl-meta": "#D7BA7D",
|
|
109
|
+
"--hl-builtin": "#DA7756",
|
|
110
|
+
"--hl-symbol": "#D7BA7D",
|
|
111
|
+
"--hl-addition": "#57AB5A",
|
|
112
|
+
"--hl-deletion": "#E5534B"
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Minimal claude palette for getThemeColor before API loads
|
|
116
|
+
var claudeFallback = {
|
|
117
|
+
name: "Claude Dark", variant: "dark",
|
|
118
|
+
base00: "2F2E2B", base01: "35332F", base02: "3E3C37", base03: "6D6860",
|
|
119
|
+
base04: "908B81", base05: "B5B0A6", base06: "E8E5DE", base07: "FFFFFF",
|
|
120
|
+
base08: "E5534B", base09: "DA7756", base0A: "E5A84B", base0B: "57AB5A",
|
|
121
|
+
base0C: "4EC9B0", base0D: "569CD6", base0E: "C586C0", base0F: "D7BA7D"
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// --- Compute CSS variables from a base16 palette ---
|
|
125
|
+
function computeVars(theme) {
|
|
126
|
+
var b = {};
|
|
127
|
+
var keys = ["base00","base01","base02","base03","base04","base05","base06","base07",
|
|
128
|
+
"base08","base09","base0A","base0B","base0C","base0D","base0E","base0F"];
|
|
129
|
+
for (var i = 0; i < keys.length; i++) {
|
|
130
|
+
b[keys[i]] = "#" + theme[keys[i]];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
var isLight = theme.variant === "light";
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"--bg": b.base00,
|
|
137
|
+
"--bg-alt": b.base01,
|
|
138
|
+
"--text": b.base06,
|
|
139
|
+
"--text-secondary": b.base05,
|
|
140
|
+
"--text-muted": b.base04,
|
|
141
|
+
"--text-dimmer": b.base03,
|
|
142
|
+
"--accent": b.base09,
|
|
143
|
+
"--accent-hover": isLight ? darken(b.base09, 0.12) : lighten(b.base09, 0.12),
|
|
144
|
+
"--accent-bg": hexToRgba(b.base09, 0.12),
|
|
145
|
+
"--code-bg": isLight ? darken(b.base00, 0.03) : darken(b.base00, 0.15),
|
|
146
|
+
"--border": b.base02,
|
|
147
|
+
"--border-subtle": mixColors(b.base00, b.base02, 0.6),
|
|
148
|
+
"--input-bg": mixColors(b.base01, b.base02, 0.5),
|
|
149
|
+
"--user-bubble": isLight ? darken(b.base01, 0.03) : mixColors(b.base01, b.base02, 0.3),
|
|
150
|
+
"--error": b.base08,
|
|
151
|
+
"--success": b.base0B,
|
|
152
|
+
"--warning": b.base0A,
|
|
153
|
+
"--sidebar-bg": isLight ? darken(b.base00, 0.02) : darken(b.base00, 0.10),
|
|
154
|
+
"--sidebar-hover": mixColors(b.base00, b.base01, 0.5),
|
|
155
|
+
"--sidebar-active": mixColors(b.base01, b.base02, 0.5),
|
|
156
|
+
"--accent-8": hexToRgba(b.base09, 0.08),
|
|
157
|
+
"--accent-12": hexToRgba(b.base09, 0.12),
|
|
158
|
+
"--accent-15": hexToRgba(b.base09, 0.15),
|
|
159
|
+
"--accent-20": hexToRgba(b.base09, 0.20),
|
|
160
|
+
"--accent-25": hexToRgba(b.base09, 0.25),
|
|
161
|
+
"--accent-30": hexToRgba(b.base09, 0.30),
|
|
162
|
+
"--error-8": hexToRgba(b.base08, 0.08),
|
|
163
|
+
"--error-12": hexToRgba(b.base08, 0.12),
|
|
164
|
+
"--error-15": hexToRgba(b.base08, 0.15),
|
|
165
|
+
"--error-25": hexToRgba(b.base08, 0.25),
|
|
166
|
+
"--success-8": hexToRgba(b.base0B, 0.08),
|
|
167
|
+
"--success-12": hexToRgba(b.base0B, 0.12),
|
|
168
|
+
"--success-15": hexToRgba(b.base0B, 0.15),
|
|
169
|
+
"--success-25": hexToRgba(b.base0B, 0.25),
|
|
170
|
+
"--warning-bg": hexToRgba(b.base0A, 0.12),
|
|
171
|
+
"--overlay-rgb": isLight ? "0,0,0" : "255,255,255",
|
|
172
|
+
"--shadow-rgb": "0,0,0",
|
|
173
|
+
"--hl-comment": b.base03,
|
|
174
|
+
"--hl-keyword": b.base0E,
|
|
175
|
+
"--hl-string": b.base0B,
|
|
176
|
+
"--hl-number": b.base09,
|
|
177
|
+
"--hl-function": b.base0D,
|
|
178
|
+
"--hl-variable": b.base08,
|
|
179
|
+
"--hl-type": b.base0A,
|
|
180
|
+
"--hl-constant": b.base09,
|
|
181
|
+
"--hl-tag": b.base08,
|
|
182
|
+
"--hl-attr": b.base0D,
|
|
183
|
+
"--hl-regexp": b.base0C,
|
|
184
|
+
"--hl-meta": b.base0F,
|
|
185
|
+
"--hl-builtin": b.base09,
|
|
186
|
+
"--hl-symbol": b.base0F,
|
|
187
|
+
"--hl-addition": b.base0B,
|
|
188
|
+
"--hl-deletion": b.base08
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function computeTerminalTheme(theme) {
|
|
193
|
+
var b = {};
|
|
194
|
+
var keys = ["base00","base01","base02","base03","base04","base05","base06","base07",
|
|
195
|
+
"base08","base09","base0A","base0B","base0C","base0D","base0E","base0F"];
|
|
196
|
+
for (var i = 0; i < keys.length; i++) {
|
|
197
|
+
b[keys[i]] = "#" + theme[keys[i]];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var isLight = theme.variant === "light";
|
|
201
|
+
return {
|
|
202
|
+
background: isLight ? darken(b.base00, 0.03) : darken(b.base00, 0.15),
|
|
203
|
+
foreground: b.base05,
|
|
204
|
+
cursor: b.base06,
|
|
205
|
+
selectionBackground: hexToRgba(b.base02, 0.5),
|
|
206
|
+
black: isLight ? b.base07 : b.base00,
|
|
207
|
+
red: b.base08,
|
|
208
|
+
green: b.base0B,
|
|
209
|
+
yellow: b.base0A,
|
|
210
|
+
blue: b.base0D,
|
|
211
|
+
magenta: b.base0E,
|
|
212
|
+
cyan: b.base0C,
|
|
213
|
+
white: isLight ? b.base00 : b.base05,
|
|
214
|
+
brightBlack: b.base03,
|
|
215
|
+
brightRed: isLight ? darken(b.base08, 0.1) : lighten(b.base08, 0.1),
|
|
216
|
+
brightGreen: isLight ? darken(b.base0B, 0.1) : lighten(b.base0B, 0.1),
|
|
217
|
+
brightYellow: isLight ? darken(b.base0A, 0.1) : lighten(b.base0A, 0.1),
|
|
218
|
+
brightBlue: isLight ? darken(b.base0D, 0.1) : lighten(b.base0D, 0.1),
|
|
219
|
+
brightMagenta: isLight ? darken(b.base0E, 0.1) : lighten(b.base0E, 0.1),
|
|
220
|
+
brightCyan: isLight ? darken(b.base0C, 0.1) : lighten(b.base0C, 0.1),
|
|
221
|
+
brightWhite: b.base07
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function computeMermaidVars(theme) {
|
|
226
|
+
var vars = currentThemeId === "claude" ? claudeExactVars : computeVars(theme);
|
|
227
|
+
var isLight = theme.variant === "light";
|
|
228
|
+
return {
|
|
229
|
+
darkMode: !isLight,
|
|
230
|
+
background: vars["--code-bg"],
|
|
231
|
+
primaryColor: vars["--accent"],
|
|
232
|
+
primaryTextColor: vars["--text"],
|
|
233
|
+
primaryBorderColor: vars["--border"],
|
|
234
|
+
lineColor: vars["--text-muted"],
|
|
235
|
+
secondaryColor: vars["--bg-alt"],
|
|
236
|
+
tertiaryColor: vars["--bg"]
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- State ---
|
|
241
|
+
// All themes loaded from server: bundled + custom, keyed by id
|
|
242
|
+
var themes = {};
|
|
243
|
+
var customSet = {}; // ids that came from ~/.claude-relay/themes/
|
|
244
|
+
var themesLoaded = false;
|
|
245
|
+
var currentThemeId = "claude";
|
|
246
|
+
var changeCallbacks = [];
|
|
247
|
+
var STORAGE_KEY = "claude-relay-theme";
|
|
248
|
+
|
|
249
|
+
// --- Helpers ---
|
|
250
|
+
|
|
251
|
+
function getTheme(id) {
|
|
252
|
+
return themes[id] || (id === "claude" ? claudeFallback : null);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isCustom(id) {
|
|
256
|
+
return !!customSet[id];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Public API ---
|
|
260
|
+
|
|
261
|
+
export function getCurrentTheme() {
|
|
262
|
+
return getTheme(currentThemeId) || claudeFallback;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function getThemeId() {
|
|
266
|
+
return currentThemeId;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function getThemeColor(baseKey) {
|
|
270
|
+
var theme = getCurrentTheme();
|
|
271
|
+
return "#" + (theme[baseKey] || "000000");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function getComputedVar(varName) {
|
|
275
|
+
if (currentThemeId === "claude" && !themesLoaded) return claudeExactVars[varName] || "";
|
|
276
|
+
var theme = getCurrentTheme();
|
|
277
|
+
var vars = computeVars(theme);
|
|
278
|
+
return vars[varName] || "";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function getTerminalTheme() {
|
|
282
|
+
return computeTerminalTheme(getCurrentTheme());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function getMermaidThemeVars() {
|
|
286
|
+
return computeMermaidVars(getCurrentTheme());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function onThemeChange(fn) {
|
|
290
|
+
changeCallbacks.push(fn);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function getThemes() {
|
|
294
|
+
// Return a copy
|
|
295
|
+
var all = {};
|
|
296
|
+
var k;
|
|
297
|
+
for (k in themes) all[k] = themes[k];
|
|
298
|
+
return all;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function applyTheme(themeId) {
|
|
302
|
+
var theme = getTheme(themeId);
|
|
303
|
+
if (!theme) themeId = "claude";
|
|
304
|
+
theme = getTheme(themeId);
|
|
305
|
+
currentThemeId = themeId;
|
|
306
|
+
|
|
307
|
+
var vars = (themeId === "claude" && !themesLoaded) ? claudeExactVars : computeVars(theme);
|
|
308
|
+
var root = document.documentElement;
|
|
309
|
+
var varNames = Object.keys(vars);
|
|
310
|
+
for (var i = 0; i < varNames.length; i++) {
|
|
311
|
+
root.style.setProperty(varNames[i], vars[varNames[i]]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
var isLight = theme.variant === "light";
|
|
315
|
+
root.classList.toggle("light-theme", isLight);
|
|
316
|
+
root.classList.toggle("dark-theme", !isLight);
|
|
317
|
+
|
|
318
|
+
var meta = document.querySelector('meta[name="theme-color"]');
|
|
319
|
+
if (meta) meta.setAttribute("content", vars["--bg"]);
|
|
320
|
+
|
|
321
|
+
updatePickerActive(themeId);
|
|
322
|
+
|
|
323
|
+
try { updateMascotSvgs(vars); } catch (e) {}
|
|
324
|
+
|
|
325
|
+
var termTheme = computeTerminalTheme(theme);
|
|
326
|
+
try { setTerminalTheme(termTheme); } catch (e) {}
|
|
327
|
+
|
|
328
|
+
var mermaidVars = computeMermaidVars(theme);
|
|
329
|
+
try { updateMermaidTheme(mermaidVars); } catch (e) {}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
localStorage.setItem(STORAGE_KEY, themeId);
|
|
333
|
+
localStorage.setItem(STORAGE_KEY + "-vars", JSON.stringify(vars));
|
|
334
|
+
localStorage.setItem(STORAGE_KEY + "-variant", theme.variant || "dark");
|
|
335
|
+
} catch (e) {}
|
|
336
|
+
|
|
337
|
+
for (var j = 0; j < changeCallbacks.length; j++) {
|
|
338
|
+
try { changeCallbacks[j](themeId, vars); } catch (e) {}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// --- Mascot SVG update ---
|
|
343
|
+
var prevMascotColors = {
|
|
344
|
+
border: "#3E3C37",
|
|
345
|
+
dimmer: "#6D6860",
|
|
346
|
+
muted: "#908B81",
|
|
347
|
+
sidebar: "#262522"
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
function updateMascotSvgs(vars) {
|
|
351
|
+
var mascots = document.querySelectorAll(".footer-mascot");
|
|
352
|
+
for (var i = 0; i < mascots.length; i++) {
|
|
353
|
+
var svg = mascots[i];
|
|
354
|
+
var rects = svg.querySelectorAll("rect");
|
|
355
|
+
for (var j = 0; j < rects.length; j++) {
|
|
356
|
+
var fill = rects[j].getAttribute("fill");
|
|
357
|
+
if (fill === prevMascotColors.border) rects[j].setAttribute("fill", vars["--border"]);
|
|
358
|
+
else if (fill === prevMascotColors.dimmer) rects[j].setAttribute("fill", vars["--text-dimmer"]);
|
|
359
|
+
else if (fill === prevMascotColors.muted) rects[j].setAttribute("fill", vars["--text-muted"]);
|
|
360
|
+
else if (fill === prevMascotColors.sidebar) rects[j].setAttribute("fill", vars["--sidebar-bg"]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
prevMascotColors.border = vars["--border"];
|
|
364
|
+
prevMascotColors.dimmer = vars["--text-dimmer"];
|
|
365
|
+
prevMascotColors.muted = vars["--text-muted"];
|
|
366
|
+
prevMascotColors.sidebar = vars["--sidebar-bg"];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- Theme loading from server ---
|
|
370
|
+
function loadThemes() {
|
|
371
|
+
return fetch("/api/themes").then(function (res) {
|
|
372
|
+
if (!res.ok) throw new Error("fetch failed");
|
|
373
|
+
return res.json();
|
|
374
|
+
}).then(function (data) {
|
|
375
|
+
if (!data) return;
|
|
376
|
+
var bundled = data.bundled || {};
|
|
377
|
+
var custom = data.custom || {};
|
|
378
|
+
var id;
|
|
379
|
+
|
|
380
|
+
// Bundled themes first
|
|
381
|
+
for (id in bundled) {
|
|
382
|
+
if (validateTheme(bundled[id])) {
|
|
383
|
+
themes[id] = bundled[id];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Custom themes override bundled
|
|
387
|
+
for (id in custom) {
|
|
388
|
+
if (validateTheme(custom[id])) {
|
|
389
|
+
themes[id] = custom[id];
|
|
390
|
+
customSet[id] = true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Ensure claude always exists
|
|
395
|
+
if (!themes.claude) themes.claude = claudeFallback;
|
|
396
|
+
|
|
397
|
+
themesLoaded = true;
|
|
398
|
+
|
|
399
|
+
// Rebuild picker if already created
|
|
400
|
+
if (pickerEl) rebuildPicker();
|
|
401
|
+
|
|
402
|
+
// Always apply the current theme now that real data is loaded
|
|
403
|
+
// (before this, only claudeExactVars was used as fallback)
|
|
404
|
+
applyTheme(currentThemeId);
|
|
405
|
+
}).catch(function () {
|
|
406
|
+
// API unavailable — keep claude fallback
|
|
407
|
+
themes.claude = claudeFallback;
|
|
408
|
+
themesLoaded = true;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function validateTheme(t) {
|
|
413
|
+
if (!t || typeof t !== "object") return false;
|
|
414
|
+
if (!t.name || typeof t.name !== "string") return false;
|
|
415
|
+
var keys = ["base00","base01","base02","base03","base04","base05","base06","base07",
|
|
416
|
+
"base08","base09","base0A","base0B","base0C","base0D","base0E","base0F"];
|
|
417
|
+
for (var i = 0; i < keys.length; i++) {
|
|
418
|
+
if (!t[keys[i]] || !/^[0-9a-fA-F]{6}$/.test(t[keys[i]])) return false;
|
|
419
|
+
}
|
|
420
|
+
if (t.variant && t.variant !== "dark" && t.variant !== "light") return false;
|
|
421
|
+
if (!t.variant) {
|
|
422
|
+
t.variant = luminance("#" + t.base00) > 0.5 ? "light" : "dark";
|
|
423
|
+
}
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// --- Theme picker UI ---
|
|
428
|
+
var pickerEl = null;
|
|
429
|
+
|
|
430
|
+
function updatePickerActive(themeId) {
|
|
431
|
+
if (!pickerEl) return;
|
|
432
|
+
var items = pickerEl.querySelectorAll(".theme-picker-item");
|
|
433
|
+
for (var i = 0; i < items.length; i++) {
|
|
434
|
+
var item = items[i];
|
|
435
|
+
if (item.dataset.theme === themeId) {
|
|
436
|
+
item.classList.add("active");
|
|
437
|
+
} else {
|
|
438
|
+
item.classList.remove("active");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function createThemeItem(id, theme) {
|
|
444
|
+
var item = document.createElement("button");
|
|
445
|
+
item.className = "theme-picker-item";
|
|
446
|
+
if (id === currentThemeId) item.className += " active";
|
|
447
|
+
item.dataset.theme = id;
|
|
448
|
+
|
|
449
|
+
var swatches = document.createElement("span");
|
|
450
|
+
swatches.className = "theme-swatches";
|
|
451
|
+
var previewKeys = ["base00", "base01", "base09", "base0B", "base0D"];
|
|
452
|
+
for (var j = 0; j < previewKeys.length; j++) {
|
|
453
|
+
var dot = document.createElement("span");
|
|
454
|
+
dot.className = "theme-swatch";
|
|
455
|
+
dot.style.background = "#" + theme[previewKeys[j]];
|
|
456
|
+
swatches.appendChild(dot);
|
|
457
|
+
}
|
|
458
|
+
item.appendChild(swatches);
|
|
459
|
+
|
|
460
|
+
var label = document.createElement("span");
|
|
461
|
+
label.className = "theme-picker-label";
|
|
462
|
+
label.textContent = theme.name;
|
|
463
|
+
item.appendChild(label);
|
|
464
|
+
|
|
465
|
+
var check = document.createElement("span");
|
|
466
|
+
check.className = "theme-picker-check";
|
|
467
|
+
check.textContent = "\u2713";
|
|
468
|
+
item.appendChild(check);
|
|
469
|
+
|
|
470
|
+
item.addEventListener("click", function (e) {
|
|
471
|
+
e.stopPropagation();
|
|
472
|
+
applyTheme(id);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return item;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function buildPickerContent() {
|
|
479
|
+
pickerEl.innerHTML = "";
|
|
480
|
+
|
|
481
|
+
var darkIds = [];
|
|
482
|
+
var lightIds = [];
|
|
483
|
+
var customIds = [];
|
|
484
|
+
var themeIds = Object.keys(themes);
|
|
485
|
+
for (var i = 0; i < themeIds.length; i++) {
|
|
486
|
+
var id = themeIds[i];
|
|
487
|
+
if (isCustom(id)) {
|
|
488
|
+
customIds.push(id);
|
|
489
|
+
} else if (themes[id].variant === "light") {
|
|
490
|
+
lightIds.push(id);
|
|
491
|
+
} else {
|
|
492
|
+
darkIds.push(id);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Claude themes always first in their section
|
|
497
|
+
function pinFirst(arr, pinId) {
|
|
498
|
+
var idx = arr.indexOf(pinId);
|
|
499
|
+
if (idx > 0) { arr.splice(idx, 1); arr.unshift(pinId); }
|
|
500
|
+
}
|
|
501
|
+
pinFirst(darkIds, "claude");
|
|
502
|
+
pinFirst(lightIds, "claude-light");
|
|
503
|
+
|
|
504
|
+
// Dark section
|
|
505
|
+
if (darkIds.length > 0) {
|
|
506
|
+
var darkHeader = document.createElement("div");
|
|
507
|
+
darkHeader.className = "theme-picker-header";
|
|
508
|
+
darkHeader.textContent = "Dark";
|
|
509
|
+
pickerEl.appendChild(darkHeader);
|
|
510
|
+
|
|
511
|
+
var darkList = document.createElement("div");
|
|
512
|
+
darkList.className = "theme-picker-section";
|
|
513
|
+
for (var d = 0; d < darkIds.length; d++) {
|
|
514
|
+
darkList.appendChild(createThemeItem(darkIds[d], themes[darkIds[d]]));
|
|
515
|
+
}
|
|
516
|
+
pickerEl.appendChild(darkList);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Light section
|
|
520
|
+
if (lightIds.length > 0) {
|
|
521
|
+
var lightHeader = document.createElement("div");
|
|
522
|
+
lightHeader.className = "theme-picker-header";
|
|
523
|
+
lightHeader.textContent = "Light";
|
|
524
|
+
pickerEl.appendChild(lightHeader);
|
|
525
|
+
|
|
526
|
+
var lightList = document.createElement("div");
|
|
527
|
+
lightList.className = "theme-picker-section";
|
|
528
|
+
for (var l = 0; l < lightIds.length; l++) {
|
|
529
|
+
lightList.appendChild(createThemeItem(lightIds[l], themes[lightIds[l]]));
|
|
530
|
+
}
|
|
531
|
+
pickerEl.appendChild(lightList);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Custom section
|
|
535
|
+
if (customIds.length > 0) {
|
|
536
|
+
var customHeader = document.createElement("div");
|
|
537
|
+
customHeader.className = "theme-picker-header";
|
|
538
|
+
customHeader.textContent = "Custom";
|
|
539
|
+
pickerEl.appendChild(customHeader);
|
|
540
|
+
|
|
541
|
+
var customList = document.createElement("div");
|
|
542
|
+
customList.className = "theme-picker-section";
|
|
543
|
+
for (var c = 0; c < customIds.length; c++) {
|
|
544
|
+
customList.appendChild(createThemeItem(customIds[c], themes[customIds[c]]));
|
|
545
|
+
}
|
|
546
|
+
pickerEl.appendChild(customList);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function createThemePicker() {
|
|
551
|
+
if (pickerEl) return pickerEl;
|
|
552
|
+
|
|
553
|
+
pickerEl = document.createElement("div");
|
|
554
|
+
pickerEl.className = "theme-picker";
|
|
555
|
+
pickerEl.id = "theme-picker";
|
|
556
|
+
|
|
557
|
+
buildPickerContent();
|
|
558
|
+
return pickerEl;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function rebuildPicker() {
|
|
562
|
+
if (!pickerEl) return;
|
|
563
|
+
buildPickerContent();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
var pickerVisible = false;
|
|
567
|
+
|
|
568
|
+
function togglePicker() {
|
|
569
|
+
if (!pickerEl) {
|
|
570
|
+
createThemePicker();
|
|
571
|
+
document.body.appendChild(pickerEl);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
pickerVisible = !pickerVisible;
|
|
575
|
+
if (pickerVisible) {
|
|
576
|
+
var footer = document.getElementById("sidebar-footer");
|
|
577
|
+
if (footer) {
|
|
578
|
+
var rect = footer.getBoundingClientRect();
|
|
579
|
+
pickerEl.style.bottom = (window.innerHeight - rect.top + 4) + "px";
|
|
580
|
+
pickerEl.style.left = rect.left + "px";
|
|
581
|
+
}
|
|
582
|
+
pickerEl.classList.add("visible");
|
|
583
|
+
|
|
584
|
+
setTimeout(function () {
|
|
585
|
+
document.addEventListener("click", closePicker);
|
|
586
|
+
}, 0);
|
|
587
|
+
} else {
|
|
588
|
+
pickerEl.classList.remove("visible");
|
|
589
|
+
document.removeEventListener("click", closePicker);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function closePicker(e) {
|
|
594
|
+
if (pickerVisible) {
|
|
595
|
+
if (e && pickerEl && pickerEl.contains(e.target)) return;
|
|
596
|
+
pickerVisible = false;
|
|
597
|
+
if (pickerEl) pickerEl.classList.remove("visible");
|
|
598
|
+
document.removeEventListener("click", closePicker);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// --- Init ---
|
|
603
|
+
export function initTheme() {
|
|
604
|
+
// Apply saved theme immediately if not claude (use claudeExactVars fallback)
|
|
605
|
+
var saved = "claude";
|
|
606
|
+
try { saved = localStorage.getItem(STORAGE_KEY) || "claude"; } catch (e) {}
|
|
607
|
+
currentThemeId = saved;
|
|
608
|
+
|
|
609
|
+
// Load all themes from server, then apply properly
|
|
610
|
+
loadThemes();
|
|
611
|
+
|
|
612
|
+
// Wire up footer theme button
|
|
613
|
+
var btn = document.getElementById("footer-theme");
|
|
614
|
+
if (btn) {
|
|
615
|
+
btn.addEventListener("click", function (e) {
|
|
616
|
+
e.stopPropagation();
|
|
617
|
+
var footerMenu = document.getElementById("sidebar-footer-menu");
|
|
618
|
+
if (footerMenu) footerMenu.classList.add("hidden");
|
|
619
|
+
togglePicker();
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|