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.
Files changed (54) hide show
  1. package/README.md +21 -5
  2. package/bin/cli.js +214 -9
  3. package/lib/cli-sessions.js +270 -0
  4. package/lib/config.js +3 -2
  5. package/lib/daemon.js +45 -1
  6. package/lib/pages.js +8 -1
  7. package/lib/project.js +121 -12
  8. package/lib/public/app.js +411 -87
  9. package/lib/public/css/base.css +41 -7
  10. package/lib/public/css/diff.css +6 -6
  11. package/lib/public/css/filebrowser.css +62 -52
  12. package/lib/public/css/highlight.css +144 -0
  13. package/lib/public/css/input.css +11 -9
  14. package/lib/public/css/menus.css +82 -23
  15. package/lib/public/css/messages.css +183 -35
  16. package/lib/public/css/overlays.css +166 -50
  17. package/lib/public/css/rewind.css +17 -17
  18. package/lib/public/css/sidebar.css +210 -137
  19. package/lib/public/index.html +75 -42
  20. package/lib/public/modules/filebrowser.js +2 -1
  21. package/lib/public/modules/markdown.js +10 -10
  22. package/lib/public/modules/notifications.js +38 -1
  23. package/lib/public/modules/sidebar.js +109 -31
  24. package/lib/public/modules/terminal.js +84 -23
  25. package/lib/public/modules/theme.js +622 -0
  26. package/lib/public/modules/tools.js +247 -4
  27. package/lib/public/modules/utils.js +21 -5
  28. package/lib/public/style.css +1 -0
  29. package/lib/sdk-bridge.js +95 -0
  30. package/lib/server.js +45 -3
  31. package/lib/sessions.js +16 -3
  32. package/lib/themes/ayu-light.json +9 -0
  33. package/lib/themes/catppuccin-latte.json +9 -0
  34. package/lib/themes/catppuccin-mocha.json +9 -0
  35. package/lib/themes/claude-light.json +9 -0
  36. package/lib/themes/claude.json +9 -0
  37. package/lib/themes/dracula.json +9 -0
  38. package/lib/themes/everforest-light.json +9 -0
  39. package/lib/themes/everforest.json +9 -0
  40. package/lib/themes/github-light.json +9 -0
  41. package/lib/themes/gruvbox-dark.json +9 -0
  42. package/lib/themes/gruvbox-light.json +9 -0
  43. package/lib/themes/monokai.json +9 -0
  44. package/lib/themes/nord-light.json +9 -0
  45. package/lib/themes/nord.json +9 -0
  46. package/lib/themes/one-dark.json +9 -0
  47. package/lib/themes/one-light.json +9 -0
  48. package/lib/themes/rose-pine-dawn.json +9 -0
  49. package/lib/themes/rose-pine.json +9 -0
  50. package/lib/themes/solarized-dark.json +9 -0
  51. package/lib/themes/solarized-light.json +9 -0
  52. package/lib/themes/tokyo-night-light.json +9 -0
  53. package/lib/themes/tokyo-night.json +9 -0
  54. 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
+ }