claude-relay 2.4.2 → 2.5.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 (75) hide show
  1. package/bin/cli.js +1 -2350
  2. package/package.json +7 -42
  3. package/LICENSE +0 -21
  4. package/README.md +0 -281
  5. package/lib/cli-sessions.js +0 -270
  6. package/lib/config.js +0 -222
  7. package/lib/daemon.js +0 -423
  8. package/lib/ipc.js +0 -112
  9. package/lib/pages.js +0 -714
  10. package/lib/project.js +0 -1224
  11. package/lib/public/app.js +0 -2157
  12. package/lib/public/apple-touch-icon.png +0 -0
  13. package/lib/public/css/base.css +0 -145
  14. package/lib/public/css/diff.css +0 -128
  15. package/lib/public/css/filebrowser.css +0 -1076
  16. package/lib/public/css/highlight.css +0 -144
  17. package/lib/public/css/input.css +0 -512
  18. package/lib/public/css/menus.css +0 -683
  19. package/lib/public/css/messages.css +0 -1159
  20. package/lib/public/css/overlays.css +0 -731
  21. package/lib/public/css/rewind.css +0 -529
  22. package/lib/public/css/sidebar.css +0 -794
  23. package/lib/public/favicon.svg +0 -26
  24. package/lib/public/icon-192.png +0 -0
  25. package/lib/public/icon-512.png +0 -0
  26. package/lib/public/icon-mono.svg +0 -19
  27. package/lib/public/index.html +0 -460
  28. package/lib/public/manifest.json +0 -27
  29. package/lib/public/modules/diff.js +0 -398
  30. package/lib/public/modules/events.js +0 -21
  31. package/lib/public/modules/filebrowser.js +0 -1375
  32. package/lib/public/modules/fileicons.js +0 -172
  33. package/lib/public/modules/icons.js +0 -54
  34. package/lib/public/modules/input.js +0 -578
  35. package/lib/public/modules/markdown.js +0 -149
  36. package/lib/public/modules/notifications.js +0 -643
  37. package/lib/public/modules/qrcode.js +0 -70
  38. package/lib/public/modules/rewind.js +0 -334
  39. package/lib/public/modules/sidebar.js +0 -628
  40. package/lib/public/modules/state.js +0 -3
  41. package/lib/public/modules/terminal.js +0 -658
  42. package/lib/public/modules/theme.js +0 -622
  43. package/lib/public/modules/tools.js +0 -1410
  44. package/lib/public/modules/utils.js +0 -56
  45. package/lib/public/style.css +0 -10
  46. package/lib/public/sw.js +0 -75
  47. package/lib/push.js +0 -125
  48. package/lib/sdk-bridge.js +0 -771
  49. package/lib/server.js +0 -577
  50. package/lib/sessions.js +0 -402
  51. package/lib/terminal-manager.js +0 -187
  52. package/lib/terminal.js +0 -24
  53. package/lib/themes/ayu-light.json +0 -9
  54. package/lib/themes/catppuccin-latte.json +0 -9
  55. package/lib/themes/catppuccin-mocha.json +0 -9
  56. package/lib/themes/claude-light.json +0 -9
  57. package/lib/themes/claude.json +0 -9
  58. package/lib/themes/dracula.json +0 -9
  59. package/lib/themes/everforest-light.json +0 -9
  60. package/lib/themes/everforest.json +0 -9
  61. package/lib/themes/github-light.json +0 -9
  62. package/lib/themes/gruvbox-dark.json +0 -9
  63. package/lib/themes/gruvbox-light.json +0 -9
  64. package/lib/themes/monokai.json +0 -9
  65. package/lib/themes/nord-light.json +0 -9
  66. package/lib/themes/nord.json +0 -9
  67. package/lib/themes/one-dark.json +0 -9
  68. package/lib/themes/one-light.json +0 -9
  69. package/lib/themes/rose-pine-dawn.json +0 -9
  70. package/lib/themes/rose-pine.json +0 -9
  71. package/lib/themes/solarized-dark.json +0 -9
  72. package/lib/themes/solarized-light.json +0 -9
  73. package/lib/themes/tokyo-night-light.json +0 -9
  74. package/lib/themes/tokyo-night.json +0 -9
  75. package/lib/updater.js +0 -96
@@ -1,622 +0,0 @@
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": isLight ? darken(b.base00, 0.06) : mixColors(b.base00, b.base01, 0.5),
155
- "--sidebar-active": isLight ? darken(b.base01, 0.05) : 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
- }