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.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/bin/cli.js +2385 -0
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +237 -0
- package/lib/daemon.js +489 -0
- package/lib/ipc.js +112 -0
- package/lib/notes.js +120 -0
- package/lib/pages.js +664 -0
- package/lib/project.js +1433 -0
- package/lib/public/app.js +2795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +264 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +1114 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/icon-strip.css +296 -0
- package/lib/public/css/input.css +573 -0
- package/lib/public/css/menus.css +856 -0
- package/lib/public/css/messages.css +1445 -0
- package/lib/public/css/mobile-nav.css +354 -0
- package/lib/public/css/overlays.css +697 -0
- package/lib/public/css/rewind.css +505 -0
- package/lib/public/css/server-settings.css +761 -0
- package/lib/public/css/sidebar.css +936 -0
- package/lib/public/css/sticky-notes.css +358 -0
- package/lib/public/css/title-bar.css +314 -0
- package/lib/public/favicon-dark.svg +1 -0
- package/lib/public/favicon.svg +1 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +762 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1411 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +584 -0
- package/lib/public/modules/markdown.js +356 -0
- package/lib/public/modules/notifications.js +649 -0
- package/lib/public/modules/qrcode.js +70 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/server-settings.js +510 -0
- package/lib/public/modules/sidebar.js +1083 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +688 -0
- package/lib/public/modules/terminal.js +697 -0
- package/lib/public/modules/theme.js +738 -0
- package/lib/public/modules/tools.js +1608 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +15 -0
- package/lib/public/sw.js +75 -0
- package/lib/push.js +124 -0
- package/lib/sdk-bridge.js +989 -0
- package/lib/server.js +582 -0
- package/lib/sessions.js +424 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +24 -0
- 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/clay-light.json +10 -0
- package/lib/themes/clay.json +10 -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/lib/updater.js +97 -0
- 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
|
+
}
|