clay-server 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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. package/package.json +47 -0
@@ -0,0 +1,738 @@
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
+ // --- Clay default: exact CSS values for initial render (before API loads) ---
59
+ var clayExactVars = {
60
+ "--bg": "#1F1B1B",
61
+ "--bg-alt": "#2A2525",
62
+ "--text": "#E5DED8",
63
+ "--text-secondary": "#C2BAB4",
64
+ "--text-muted": "#A09590",
65
+ "--text-dimmer": "#7D7370",
66
+ "--accent": "#FE7150",
67
+ "--accent-hover": "#FE8265",
68
+ "--accent-bg": "rgba(254, 113, 80, 0.12)",
69
+ "--code-bg": "#1A1717",
70
+ "--border": "#352F2F",
71
+ "--border-subtle": "#282323",
72
+ "--input-bg": "#302A2A",
73
+ "--user-bubble": "#322C2C",
74
+ "--error": "#F74728",
75
+ "--success": "#09E5A3",
76
+ "--warning": "#E5A040",
77
+ "--sidebar-bg": "#1C1818",
78
+ "--sidebar-hover": "#252020",
79
+ "--sidebar-active": "#302A2A",
80
+ "--accent-8": "rgba(254, 113, 80, 0.08)",
81
+ "--accent-12": "rgba(254, 113, 80, 0.12)",
82
+ "--accent-15": "rgba(254, 113, 80, 0.15)",
83
+ "--accent-20": "rgba(254, 113, 80, 0.20)",
84
+ "--accent-25": "rgba(254, 113, 80, 0.25)",
85
+ "--accent-30": "rgba(254, 113, 80, 0.30)",
86
+ "--accent2": "#5857FC",
87
+ "--accent2-hover": "#6C6BFC",
88
+ "--accent2-bg": "rgba(88, 87, 252, 0.12)",
89
+ "--accent2-8": "rgba(88, 87, 252, 0.08)",
90
+ "--accent2-12": "rgba(88, 87, 252, 0.12)",
91
+ "--accent2-15": "rgba(88, 87, 252, 0.15)",
92
+ "--accent2-20": "rgba(88, 87, 252, 0.20)",
93
+ "--accent2-25": "rgba(88, 87, 252, 0.25)",
94
+ "--accent2-30": "rgba(88, 87, 252, 0.30)",
95
+ "--error-8": "rgba(247, 71, 40, 0.08)",
96
+ "--error-12": "rgba(247, 71, 40, 0.12)",
97
+ "--error-15": "rgba(247, 71, 40, 0.15)",
98
+ "--error-25": "rgba(247, 71, 40, 0.25)",
99
+ "--success-8": "rgba(9, 229, 163, 0.08)",
100
+ "--success-12": "rgba(9, 229, 163, 0.12)",
101
+ "--success-15": "rgba(9, 229, 163, 0.15)",
102
+ "--success-25": "rgba(9, 229, 163, 0.25)",
103
+ "--warning-bg": "rgba(229, 160, 64, 0.12)",
104
+ "--overlay-rgb": "255,255,255",
105
+ "--shadow-rgb": "0,0,0",
106
+ "--hl-comment": "#7D7370",
107
+ "--hl-keyword": "#D085CC",
108
+ "--hl-string": "#09E5A3",
109
+ "--hl-number": "#FE7150",
110
+ "--hl-function": "#6BA0E5",
111
+ "--hl-variable": "#F74728",
112
+ "--hl-type": "#E5A040",
113
+ "--hl-constant": "#FE7150",
114
+ "--hl-tag": "#F74728",
115
+ "--hl-attr": "#6BA0E5",
116
+ "--hl-regexp": "#4EC9B0",
117
+ "--hl-meta": "#D09558",
118
+ "--hl-builtin": "#FE7150",
119
+ "--hl-symbol": "#D09558",
120
+ "--hl-addition": "#09E5A3",
121
+ "--hl-deletion": "#F74728"
122
+ };
123
+
124
+ // Minimal clay dark palette for getThemeColor before API loads
125
+ var clayFallback = {
126
+ name: "Clay Dark", variant: "dark",
127
+ base00: "1F1B1B", base01: "2A2525", base02: "352F2F", base03: "7D7370",
128
+ base04: "A09590", base05: "C2BAB4", base06: "E5DED8", base07: "FFFFFF",
129
+ base08: "F74728", base09: "FE7150", base0A: "E5A040", base0B: "09E5A3",
130
+ base0C: "4EC9B0", base0D: "6BA0E5", base0E: "D085CC", base0F: "D09558",
131
+ accent2: "5857FC"
132
+ };
133
+
134
+ // --- Compute CSS variables from a base16 palette ---
135
+ function computeVars(theme) {
136
+ var b = {};
137
+ var keys = ["base00","base01","base02","base03","base04","base05","base06","base07",
138
+ "base08","base09","base0A","base0B","base0C","base0D","base0E","base0F"];
139
+ for (var i = 0; i < keys.length; i++) {
140
+ b[keys[i]] = "#" + theme[keys[i]];
141
+ }
142
+
143
+ var isLight = theme.variant === "light";
144
+ var accent2 = theme.accent2 ? "#" + theme.accent2 : b.base0D;
145
+
146
+ return {
147
+ "--bg": b.base00,
148
+ "--bg-alt": b.base01,
149
+ "--text": b.base06,
150
+ "--text-secondary": b.base05,
151
+ "--text-muted": b.base04,
152
+ "--text-dimmer": b.base03,
153
+ "--accent": b.base09,
154
+ "--accent-hover": isLight ? darken(b.base09, 0.12) : lighten(b.base09, 0.12),
155
+ "--accent-bg": hexToRgba(b.base09, 0.12),
156
+ "--code-bg": isLight ? darken(b.base00, 0.03) : darken(b.base00, 0.15),
157
+ "--border": b.base02,
158
+ "--border-subtle": mixColors(b.base00, b.base02, 0.6),
159
+ "--input-bg": mixColors(b.base01, b.base02, 0.5),
160
+ "--user-bubble": isLight ? darken(b.base01, 0.03) : mixColors(b.base01, b.base02, 0.3),
161
+ "--error": b.base08,
162
+ "--success": b.base0B,
163
+ "--warning": b.base0A,
164
+ "--sidebar-bg": isLight ? darken(b.base00, 0.02) : darken(b.base00, 0.10),
165
+ "--sidebar-hover": isLight ? darken(b.base00, 0.06) : mixColors(b.base00, b.base01, 0.5),
166
+ "--sidebar-active": isLight ? darken(b.base01, 0.05) : mixColors(b.base01, b.base02, 0.5),
167
+ "--accent-8": hexToRgba(b.base09, 0.08),
168
+ "--accent-12": hexToRgba(b.base09, 0.12),
169
+ "--accent-15": hexToRgba(b.base09, 0.15),
170
+ "--accent-20": hexToRgba(b.base09, 0.20),
171
+ "--accent-25": hexToRgba(b.base09, 0.25),
172
+ "--accent-30": hexToRgba(b.base09, 0.30),
173
+ "--accent2": accent2,
174
+ "--accent2-hover": isLight ? darken(accent2, 0.12) : lighten(accent2, 0.12),
175
+ "--accent2-bg": hexToRgba(accent2, 0.12),
176
+ "--accent2-8": hexToRgba(accent2, 0.08),
177
+ "--accent2-12": hexToRgba(accent2, 0.12),
178
+ "--accent2-15": hexToRgba(accent2, 0.15),
179
+ "--accent2-20": hexToRgba(accent2, 0.20),
180
+ "--accent2-25": hexToRgba(accent2, 0.25),
181
+ "--accent2-30": hexToRgba(accent2, 0.30),
182
+ "--error-8": hexToRgba(b.base08, 0.08),
183
+ "--error-12": hexToRgba(b.base08, 0.12),
184
+ "--error-15": hexToRgba(b.base08, 0.15),
185
+ "--error-25": hexToRgba(b.base08, 0.25),
186
+ "--success-8": hexToRgba(b.base0B, 0.08),
187
+ "--success-12": hexToRgba(b.base0B, 0.12),
188
+ "--success-15": hexToRgba(b.base0B, 0.15),
189
+ "--success-25": hexToRgba(b.base0B, 0.25),
190
+ "--warning-bg": hexToRgba(b.base0A, 0.12),
191
+ "--overlay-rgb": isLight ? "0,0,0" : "255,255,255",
192
+ "--shadow-rgb": "0,0,0",
193
+ "--hl-comment": b.base03,
194
+ "--hl-keyword": b.base0E,
195
+ "--hl-string": b.base0B,
196
+ "--hl-number": b.base09,
197
+ "--hl-function": b.base0D,
198
+ "--hl-variable": b.base08,
199
+ "--hl-type": b.base0A,
200
+ "--hl-constant": b.base09,
201
+ "--hl-tag": b.base08,
202
+ "--hl-attr": b.base0D,
203
+ "--hl-regexp": b.base0C,
204
+ "--hl-meta": b.base0F,
205
+ "--hl-builtin": b.base09,
206
+ "--hl-symbol": b.base0F,
207
+ "--hl-addition": b.base0B,
208
+ "--hl-deletion": b.base08
209
+ };
210
+ }
211
+
212
+ function computeTerminalTheme(theme) {
213
+ var b = {};
214
+ var keys = ["base00","base01","base02","base03","base04","base05","base06","base07",
215
+ "base08","base09","base0A","base0B","base0C","base0D","base0E","base0F"];
216
+ for (var i = 0; i < keys.length; i++) {
217
+ b[keys[i]] = "#" + theme[keys[i]];
218
+ }
219
+
220
+ var isLight = theme.variant === "light";
221
+ return {
222
+ background: isLight ? darken(b.base00, 0.03) : darken(b.base00, 0.15),
223
+ foreground: b.base05,
224
+ cursor: b.base06,
225
+ selectionBackground: hexToRgba(b.base02, 0.5),
226
+ black: isLight ? b.base07 : b.base00,
227
+ red: b.base08,
228
+ green: b.base0B,
229
+ yellow: b.base0A,
230
+ blue: b.base0D,
231
+ magenta: b.base0E,
232
+ cyan: b.base0C,
233
+ white: isLight ? b.base00 : b.base05,
234
+ brightBlack: b.base03,
235
+ brightRed: isLight ? darken(b.base08, 0.1) : lighten(b.base08, 0.1),
236
+ brightGreen: isLight ? darken(b.base0B, 0.1) : lighten(b.base0B, 0.1),
237
+ brightYellow: isLight ? darken(b.base0A, 0.1) : lighten(b.base0A, 0.1),
238
+ brightBlue: isLight ? darken(b.base0D, 0.1) : lighten(b.base0D, 0.1),
239
+ brightMagenta: isLight ? darken(b.base0E, 0.1) : lighten(b.base0E, 0.1),
240
+ brightCyan: isLight ? darken(b.base0C, 0.1) : lighten(b.base0C, 0.1),
241
+ brightWhite: b.base07
242
+ };
243
+ }
244
+
245
+ function computeMermaidVars(theme) {
246
+ var vars = currentThemeId === "clay" ? clayExactVars : computeVars(theme);
247
+ var isLight = theme.variant === "light";
248
+ return {
249
+ darkMode: !isLight,
250
+ background: vars["--code-bg"],
251
+ primaryColor: vars["--accent"],
252
+ primaryTextColor: vars["--text"],
253
+ primaryBorderColor: vars["--border"],
254
+ lineColor: vars["--text-muted"],
255
+ secondaryColor: vars["--bg-alt"],
256
+ tertiaryColor: vars["--bg"]
257
+ };
258
+ }
259
+
260
+ // --- State ---
261
+ // All themes loaded from server: bundled + custom, keyed by id
262
+ var themes = {};
263
+ var customSet = {}; // ids that came from ~/.clay/themes/
264
+ var themesLoaded = false;
265
+ var currentThemeId = "clay";
266
+ var changeCallbacks = [];
267
+ var STORAGE_KEY = "clay-theme";
268
+ var MODE_KEY = "clay-mode"; // "light" | "dark" | null (system)
269
+ var SKIN_KEY = "clay-skin"; // theme id within current variant pair
270
+
271
+ // --- Helpers ---
272
+
273
+ function getTheme(id) {
274
+ return themes[id] || (id === "clay" ? clayFallback : null);
275
+ }
276
+
277
+ function isCustom(id) {
278
+ return !!customSet[id];
279
+ }
280
+
281
+ // --- Public API ---
282
+
283
+ export function getCurrentTheme() {
284
+ return getTheme(currentThemeId) || clayFallback;
285
+ }
286
+
287
+ export function getThemeId() {
288
+ return currentThemeId;
289
+ }
290
+
291
+ export function getThemeColor(baseKey) {
292
+ var theme = getCurrentTheme();
293
+ return "#" + (theme[baseKey] || "000000");
294
+ }
295
+
296
+ export function getComputedVar(varName) {
297
+ if (currentThemeId === "clay" && !themesLoaded) return clayExactVars[varName] || "";
298
+ var theme = getCurrentTheme();
299
+ var vars = computeVars(theme);
300
+ return vars[varName] || "";
301
+ }
302
+
303
+ export function getTerminalTheme() {
304
+ return computeTerminalTheme(getCurrentTheme());
305
+ }
306
+
307
+ export function getMermaidThemeVars() {
308
+ return computeMermaidVars(getCurrentTheme());
309
+ }
310
+
311
+ export function onThemeChange(fn) {
312
+ changeCallbacks.push(fn);
313
+ }
314
+
315
+ export function getThemes() {
316
+ // Return a copy
317
+ var all = {};
318
+ var k;
319
+ for (k in themes) all[k] = themes[k];
320
+ return all;
321
+ }
322
+
323
+ export function applyTheme(themeId, fromPicker) {
324
+ var theme = getTheme(themeId);
325
+ if (!theme) themeId = "clay";
326
+ theme = getTheme(themeId);
327
+ currentThemeId = themeId;
328
+
329
+ var vars = (themeId === "clay" && !themesLoaded) ? clayExactVars : computeVars(theme);
330
+ var root = document.documentElement;
331
+ var varNames = Object.keys(vars);
332
+ for (var i = 0; i < varNames.length; i++) {
333
+ root.style.setProperty(varNames[i], vars[varNames[i]]);
334
+ }
335
+
336
+ var isLight = theme.variant === "light";
337
+ root.classList.toggle("light-theme", isLight);
338
+ root.classList.toggle("dark-theme", !isLight);
339
+
340
+ var meta = document.querySelector('meta[name="theme-color"]');
341
+ if (meta) meta.setAttribute("content", vars["--bg"]);
342
+
343
+ updatePickerActive(themeId);
344
+
345
+ try { updateMascotSvgs(vars, isLight); } catch (e) {}
346
+
347
+ var termTheme = computeTerminalTheme(theme);
348
+ try { setTerminalTheme(termTheme); } catch (e) {}
349
+
350
+ var mermaidVars = computeMermaidVars(theme);
351
+ try { updateMermaidTheme(mermaidVars); } catch (e) {}
352
+
353
+ try {
354
+ localStorage.setItem(STORAGE_KEY, themeId);
355
+ localStorage.setItem(STORAGE_KEY + "-vars", JSON.stringify(vars));
356
+ localStorage.setItem(STORAGE_KEY + "-variant", theme.variant || "dark");
357
+ } catch (e) {}
358
+
359
+ // When picked from skin selector, save as skin preference and sync mode
360
+ if (fromPicker) {
361
+ try {
362
+ localStorage.setItem(SKIN_KEY, themeId);
363
+ localStorage.setItem(MODE_KEY, isLight ? "light" : "dark");
364
+ } catch (e) {}
365
+ }
366
+
367
+ updateToggleIcon();
368
+
369
+ for (var j = 0; j < changeCallbacks.length; j++) {
370
+ try { changeCallbacks[j](themeId, vars); } catch (e) {}
371
+ }
372
+ }
373
+
374
+ // --- Mascot icon update (swap light/dark src) ---
375
+ function updateMascotSvgs(vars, isLight) {
376
+ var lightSrc = "favicon.svg";
377
+ var darkSrc = "favicon-dark.svg";
378
+ var src = isLight ? lightSrc : darkSrc;
379
+ var mascots = document.querySelectorAll("img.footer-mascot");
380
+ for (var i = 0; i < mascots.length; i++) {
381
+ mascots[i].setAttribute("src", src);
382
+ }
383
+ var faviconEl = document.querySelector('link[rel="icon"][type="image/svg+xml"]');
384
+ if (faviconEl) faviconEl.setAttribute("href", src);
385
+ }
386
+
387
+ // --- Theme loading from server ---
388
+ function loadThemes() {
389
+ return fetch("/api/themes").then(function (res) {
390
+ if (!res.ok) throw new Error("fetch failed");
391
+ return res.json();
392
+ }).then(function (data) {
393
+ if (!data) return;
394
+ var bundled = data.bundled || {};
395
+ var custom = data.custom || {};
396
+ var id;
397
+
398
+ // Bundled themes first
399
+ for (id in bundled) {
400
+ if (validateTheme(bundled[id])) {
401
+ themes[id] = bundled[id];
402
+ }
403
+ }
404
+ // Custom themes override bundled
405
+ for (id in custom) {
406
+ if (validateTheme(custom[id])) {
407
+ themes[id] = custom[id];
408
+ customSet[id] = true;
409
+ }
410
+ }
411
+
412
+ // Ensure clay always exists
413
+ if (!themes.clay) themes.clay = clayFallback;
414
+
415
+ themesLoaded = true;
416
+
417
+ // Rebuild picker if already created
418
+ if (pickerEl) rebuildPicker();
419
+
420
+ // Always apply the current theme now that real data is loaded
421
+ // (before this, only clayExactVars was used as fallback)
422
+ applyTheme(currentThemeId);
423
+ }).catch(function () {
424
+ // API unavailable — keep clay fallback
425
+ themes.clay = clayFallback;
426
+ themesLoaded = true;
427
+ });
428
+ }
429
+
430
+ function validateTheme(t) {
431
+ if (!t || typeof t !== "object") return false;
432
+ if (!t.name || typeof t.name !== "string") return false;
433
+ var keys = ["base00","base01","base02","base03","base04","base05","base06","base07",
434
+ "base08","base09","base0A","base0B","base0C","base0D","base0E","base0F"];
435
+ for (var i = 0; i < keys.length; i++) {
436
+ if (!t[keys[i]] || !/^[0-9a-fA-F]{6}$/.test(t[keys[i]])) return false;
437
+ }
438
+ if (t.variant && t.variant !== "dark" && t.variant !== "light") return false;
439
+ if (!t.variant) {
440
+ t.variant = luminance("#" + t.base00) > 0.5 ? "light" : "dark";
441
+ }
442
+ return true;
443
+ }
444
+
445
+ // --- Light / Dark mode toggle ---
446
+
447
+ // Returns the system preferred mode
448
+ function getSystemMode() {
449
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches) {
450
+ return "light";
451
+ }
452
+ return "dark";
453
+ }
454
+
455
+ // Returns the effective mode: user override or system
456
+ function getEffectiveMode() {
457
+ var saved = null;
458
+ try { saved = localStorage.getItem(MODE_KEY); } catch (e) {}
459
+ if (saved === "light" || saved === "dark") return saved;
460
+ return getSystemMode();
461
+ }
462
+
463
+ // Map a mode to the appropriate theme id
464
+ // If user has a custom skin selected, find its dark/light counterpart
465
+ function themeIdForMode(mode) {
466
+ var skin = null;
467
+ try { skin = localStorage.getItem(SKIN_KEY); } catch (e) {}
468
+
469
+ // Default skin pair: clay (dark) / clay-light (light)
470
+ if (!skin) {
471
+ return mode === "light" ? "clay-light" : "clay";
472
+ }
473
+
474
+ // Custom skin — try to find the counterpart
475
+ var current = getTheme(skin);
476
+ if (!current) return mode === "light" ? "clay-light" : "clay";
477
+
478
+ // Already the right variant?
479
+ if (current.variant === mode) return skin;
480
+
481
+ // Find the counterpart by looking for a theme with matching colors but opposite variant
482
+ // Convention: id / id-light or id-dark / id
483
+ var base = skin.replace(/-light$/, "").replace(/-dark$/, "");
484
+ var darkId = themes[base] && themes[base].variant === "dark" ? base : base + "-dark";
485
+ var lightId = themes[base + "-light"] ? base + "-light" : (themes[base] && themes[base].variant === "light" ? base : null);
486
+
487
+ if (mode === "light") {
488
+ if (lightId && themes[lightId]) return lightId;
489
+ return "clay-light";
490
+ } else {
491
+ if (darkId && themes[darkId]) return darkId;
492
+ return "clay";
493
+ }
494
+ }
495
+
496
+ // Toggle between light and dark
497
+ export function toggleDarkMode() {
498
+ var current = getEffectiveMode();
499
+ var next = current === "dark" ? "light" : "dark";
500
+ try { localStorage.setItem(MODE_KEY, next); } catch (e) {}
501
+ var tid = themeIdForMode(next);
502
+ applyTheme(tid);
503
+ updateToggleIcon();
504
+ }
505
+
506
+ // Update the toggle button icon
507
+ function updateToggleIcon() {
508
+ var btn = document.getElementById("theme-toggle-btn");
509
+ if (!btn) return;
510
+ var mode = getEffectiveMode();
511
+ var iconName = mode === "dark" ? "moon" : "sun";
512
+ // Replace the icon element entirely
513
+ var existing = btn.querySelector(".lucide, [data-lucide]");
514
+ if (existing) {
515
+ var i = document.createElement("i");
516
+ i.setAttribute("data-lucide", iconName);
517
+ btn.replaceChild(i, existing);
518
+ if (window.lucide && window.lucide.createIcons) {
519
+ window.lucide.createIcons();
520
+ }
521
+ }
522
+ }
523
+
524
+ // --- Theme picker UI ---
525
+ var pickerEl = null;
526
+
527
+ function updatePickerActive(themeId) {
528
+ if (!pickerEl) return;
529
+ var items = pickerEl.querySelectorAll(".theme-picker-item");
530
+ for (var i = 0; i < items.length; i++) {
531
+ var item = items[i];
532
+ if (item.dataset.theme === themeId) {
533
+ item.classList.add("active");
534
+ } else {
535
+ item.classList.remove("active");
536
+ }
537
+ }
538
+ }
539
+
540
+ function createThemeItem(id, theme) {
541
+ var item = document.createElement("button");
542
+ item.className = "theme-picker-item";
543
+ if (id === currentThemeId) item.className += " active";
544
+ item.dataset.theme = id;
545
+
546
+ var swatches = document.createElement("span");
547
+ swatches.className = "theme-swatches";
548
+ var previewKeys = ["base00", "base01", "base09", "base0B", "base0D"];
549
+ for (var j = 0; j < previewKeys.length; j++) {
550
+ var dot = document.createElement("span");
551
+ dot.className = "theme-swatch";
552
+ dot.style.background = "#" + theme[previewKeys[j]];
553
+ swatches.appendChild(dot);
554
+ }
555
+ item.appendChild(swatches);
556
+
557
+ var label = document.createElement("span");
558
+ label.className = "theme-picker-label";
559
+ label.textContent = theme.name;
560
+ item.appendChild(label);
561
+
562
+ var check = document.createElement("span");
563
+ check.className = "theme-picker-check";
564
+ check.textContent = "\u2713";
565
+ item.appendChild(check);
566
+
567
+ item.addEventListener("click", function (e) {
568
+ e.stopPropagation();
569
+ applyTheme(id, true);
570
+ });
571
+
572
+ return item;
573
+ }
574
+
575
+ function buildPickerContent() {
576
+ pickerEl.innerHTML = "";
577
+
578
+ var darkIds = [];
579
+ var lightIds = [];
580
+ var customIds = [];
581
+ var themeIds = Object.keys(themes);
582
+ for (var i = 0; i < themeIds.length; i++) {
583
+ var id = themeIds[i];
584
+ if (isCustom(id)) {
585
+ customIds.push(id);
586
+ } else if (themes[id].variant === "light") {
587
+ lightIds.push(id);
588
+ } else {
589
+ darkIds.push(id);
590
+ }
591
+ }
592
+
593
+ // Clay default themes always first in their section
594
+ function pinFirst(arr, pinId) {
595
+ var idx = arr.indexOf(pinId);
596
+ if (idx > 0) { arr.splice(idx, 1); arr.unshift(pinId); }
597
+ }
598
+ pinFirst(darkIds, "clay");
599
+ pinFirst(lightIds, "clay-light");
600
+
601
+ // Dark section
602
+ if (darkIds.length > 0) {
603
+ var darkHeader = document.createElement("div");
604
+ darkHeader.className = "theme-picker-header";
605
+ darkHeader.textContent = "Dark";
606
+ pickerEl.appendChild(darkHeader);
607
+
608
+ var darkList = document.createElement("div");
609
+ darkList.className = "theme-picker-section";
610
+ for (var d = 0; d < darkIds.length; d++) {
611
+ darkList.appendChild(createThemeItem(darkIds[d], themes[darkIds[d]]));
612
+ }
613
+ pickerEl.appendChild(darkList);
614
+ }
615
+
616
+ // Light section
617
+ if (lightIds.length > 0) {
618
+ var lightHeader = document.createElement("div");
619
+ lightHeader.className = "theme-picker-header";
620
+ lightHeader.textContent = "Light";
621
+ pickerEl.appendChild(lightHeader);
622
+
623
+ var lightList = document.createElement("div");
624
+ lightList.className = "theme-picker-section";
625
+ for (var l = 0; l < lightIds.length; l++) {
626
+ lightList.appendChild(createThemeItem(lightIds[l], themes[lightIds[l]]));
627
+ }
628
+ pickerEl.appendChild(lightList);
629
+ }
630
+
631
+ // Custom section
632
+ if (customIds.length > 0) {
633
+ var customHeader = document.createElement("div");
634
+ customHeader.className = "theme-picker-header";
635
+ customHeader.textContent = "Custom";
636
+ pickerEl.appendChild(customHeader);
637
+
638
+ var customList = document.createElement("div");
639
+ customList.className = "theme-picker-section";
640
+ for (var c = 0; c < customIds.length; c++) {
641
+ customList.appendChild(createThemeItem(customIds[c], themes[customIds[c]]));
642
+ }
643
+ pickerEl.appendChild(customList);
644
+ }
645
+ }
646
+
647
+ function createThemePicker() {
648
+ if (pickerEl) return pickerEl;
649
+
650
+ pickerEl = document.createElement("div");
651
+ pickerEl.className = "theme-picker";
652
+ pickerEl.id = "theme-picker";
653
+
654
+ buildPickerContent();
655
+ return pickerEl;
656
+ }
657
+
658
+ function rebuildPicker() {
659
+ if (!pickerEl) return;
660
+ buildPickerContent();
661
+ }
662
+
663
+ var pickerVisible = false;
664
+
665
+ function togglePicker() {
666
+ // Legacy — no longer used as floating popover
667
+ // Picker is now embedded in server settings
668
+ }
669
+
670
+ function closePicker() {
671
+ // Legacy — no longer needed
672
+ }
673
+
674
+ // --- Init ---
675
+ export function initTheme() {
676
+ // Determine initial theme from saved mode + skin, or system preference
677
+ var saved = null;
678
+ try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {}
679
+
680
+ if (saved) {
681
+ currentThemeId = saved;
682
+ } else {
683
+ // No saved theme — use system mode
684
+ var mode = getSystemMode();
685
+ currentThemeId = mode === "light" ? "clay-light" : "clay";
686
+ }
687
+
688
+ // Load all themes from server, then apply properly
689
+ loadThemes();
690
+
691
+ // Wire up title bar toggle button
692
+ var toggleBtn = document.getElementById("theme-toggle-btn");
693
+ if (toggleBtn) {
694
+ toggleBtn.addEventListener("click", function (e) {
695
+ e.stopPropagation();
696
+ toggleDarkMode();
697
+ });
698
+ }
699
+
700
+ // Listen for system preference changes (only applies if user has no manual override)
701
+ if (window.matchMedia) {
702
+ var mq = window.matchMedia("(prefers-color-scheme: dark)");
703
+ var handler = function () {
704
+ var userMode = null;
705
+ try { userMode = localStorage.getItem(MODE_KEY); } catch (e) {}
706
+ if (!userMode) {
707
+ // No manual override — follow system
708
+ var sysMode = getSystemMode();
709
+ var tid = themeIdForMode(sysMode);
710
+ applyTheme(tid);
711
+ }
712
+ };
713
+ if (mq.addEventListener) {
714
+ mq.addEventListener("change", handler);
715
+ } else if (mq.addListener) {
716
+ mq.addListener(handler);
717
+ }
718
+ }
719
+
720
+ // Set initial toggle icon
721
+ updateToggleIcon();
722
+ }
723
+
724
+ // --- Settings picker (for appearance section in server settings) ---
725
+ export function openSettingsThemePicker(containerEl) {
726
+ if (!containerEl) return;
727
+
728
+ if (!pickerEl) {
729
+ createThemePicker();
730
+ }
731
+
732
+ // Move picker into settings container if not already there
733
+ if (pickerEl.parentNode !== containerEl) {
734
+ containerEl.innerHTML = "";
735
+ containerEl.appendChild(pickerEl);
736
+ }
737
+ pickerEl.classList.add("visible");
738
+ }