@mooncompany/uplink-chat 0.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.

Potentially problematic release.


This version of @mooncompany/uplink-chat might be problematic. Click here for more details.

Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/bin/uplink.js +279 -0
  4. package/middleware/error-handler.js +69 -0
  5. package/package.json +93 -0
  6. package/public/css/agents.36b98c0f.css +1469 -0
  7. package/public/css/agents.css +1469 -0
  8. package/public/css/app.a6a7f8f5.css +2731 -0
  9. package/public/css/app.css +2731 -0
  10. package/public/css/artifacts.css +444 -0
  11. package/public/css/commands.css +55 -0
  12. package/public/css/connection.css +131 -0
  13. package/public/css/dashboard.css +233 -0
  14. package/public/css/developer.css +328 -0
  15. package/public/css/files.css +123 -0
  16. package/public/css/markdown.css +156 -0
  17. package/public/css/message-actions.css +278 -0
  18. package/public/css/mobile.css +614 -0
  19. package/public/css/panels-unified.css +483 -0
  20. package/public/css/premium.css +415 -0
  21. package/public/css/realtime.css +189 -0
  22. package/public/css/satellites.css +401 -0
  23. package/public/css/shortcuts.css +185 -0
  24. package/public/css/split-view.4def0262.css +673 -0
  25. package/public/css/split-view.css +673 -0
  26. package/public/css/theme-generator.css +391 -0
  27. package/public/css/themes.css +387 -0
  28. package/public/css/timestamps.css +54 -0
  29. package/public/css/variables.css +78 -0
  30. package/public/dist/bundle.b55050c4.js +15757 -0
  31. package/public/favicon.svg +24 -0
  32. package/public/img/agents/ada.png +0 -0
  33. package/public/img/agents/clarice.png +0 -0
  34. package/public/img/agents/dennis-nedry.png +0 -0
  35. package/public/img/agents/elliot-alderson.png +0 -0
  36. package/public/img/agents/main.png +0 -0
  37. package/public/img/agents/scotty.png +0 -0
  38. package/public/img/agents/top-flight-security.png +0 -0
  39. package/public/index.html +1083 -0
  40. package/public/js/agents-data.js +234 -0
  41. package/public/js/agents-ui.js +72 -0
  42. package/public/js/agents.js +1525 -0
  43. package/public/js/app.js +79 -0
  44. package/public/js/appearance-settings.js +111 -0
  45. package/public/js/artifacts.js +432 -0
  46. package/public/js/audio-queue.js +168 -0
  47. package/public/js/bootstrap.js +54 -0
  48. package/public/js/chat.js +1211 -0
  49. package/public/js/commands.js +581 -0
  50. package/public/js/connection-api.js +121 -0
  51. package/public/js/connection.js +1231 -0
  52. package/public/js/context-tracker.js +271 -0
  53. package/public/js/core.js +172 -0
  54. package/public/js/dashboard.js +452 -0
  55. package/public/js/developer.js +432 -0
  56. package/public/js/encryption.js +124 -0
  57. package/public/js/errors.js +122 -0
  58. package/public/js/event-bus.js +77 -0
  59. package/public/js/fetch-utils.js +171 -0
  60. package/public/js/file-handler.js +229 -0
  61. package/public/js/files.js +352 -0
  62. package/public/js/gateway-chat.js +538 -0
  63. package/public/js/logger.js +112 -0
  64. package/public/js/markdown.js +190 -0
  65. package/public/js/message-actions.js +431 -0
  66. package/public/js/message-renderer.js +288 -0
  67. package/public/js/missed-messages.js +235 -0
  68. package/public/js/mobile-debug.js +95 -0
  69. package/public/js/notifications.js +367 -0
  70. package/public/js/offline-queue.js +178 -0
  71. package/public/js/onboarding.js +543 -0
  72. package/public/js/panels.js +156 -0
  73. package/public/js/premium.js +412 -0
  74. package/public/js/realtime-voice.js +844 -0
  75. package/public/js/satellite-sync.js +256 -0
  76. package/public/js/satellite-ui.js +175 -0
  77. package/public/js/satellites.js +1516 -0
  78. package/public/js/settings.js +1087 -0
  79. package/public/js/shortcuts.js +381 -0
  80. package/public/js/split-chat.js +1234 -0
  81. package/public/js/split-resize.js +211 -0
  82. package/public/js/splitview.js +340 -0
  83. package/public/js/storage.js +408 -0
  84. package/public/js/streaming-handler.js +324 -0
  85. package/public/js/stt-settings.js +316 -0
  86. package/public/js/theme-generator.js +661 -0
  87. package/public/js/themes.js +164 -0
  88. package/public/js/timestamps.js +198 -0
  89. package/public/js/tts-settings.js +575 -0
  90. package/public/js/ui.js +267 -0
  91. package/public/js/update-notifier.js +143 -0
  92. package/public/js/utils/constants.js +165 -0
  93. package/public/js/utils/sanitize.js +93 -0
  94. package/public/js/utils/sse-parser.js +195 -0
  95. package/public/js/voice.js +883 -0
  96. package/public/manifest.json +58 -0
  97. package/public/moon_texture.jpg +0 -0
  98. package/public/sw.js +221 -0
  99. package/public/three.min.js +6 -0
  100. package/server/channel.js +529 -0
  101. package/server/chat.js +270 -0
  102. package/server/config-store.js +362 -0
  103. package/server/config.js +159 -0
  104. package/server/context.js +131 -0
  105. package/server/gateway-commands.js +211 -0
  106. package/server/gateway-proxy.js +318 -0
  107. package/server/index.js +22 -0
  108. package/server/logger.js +89 -0
  109. package/server/middleware/auth.js +188 -0
  110. package/server/middleware.js +218 -0
  111. package/server/openclaw-discover.js +308 -0
  112. package/server/premium/index.js +156 -0
  113. package/server/premium/license.js +140 -0
  114. package/server/realtime/bridge.js +837 -0
  115. package/server/realtime/index.js +349 -0
  116. package/server/realtime/tts-stream.js +446 -0
  117. package/server/routes/agents.js +564 -0
  118. package/server/routes/artifacts.js +174 -0
  119. package/server/routes/chat.js +311 -0
  120. package/server/routes/config-settings.js +345 -0
  121. package/server/routes/config.js +603 -0
  122. package/server/routes/files.js +307 -0
  123. package/server/routes/index.js +18 -0
  124. package/server/routes/media.js +451 -0
  125. package/server/routes/missed-messages.js +107 -0
  126. package/server/routes/premium.js +75 -0
  127. package/server/routes/push.js +156 -0
  128. package/server/routes/satellite.js +406 -0
  129. package/server/routes/status.js +251 -0
  130. package/server/routes/stt.js +35 -0
  131. package/server/routes/voice.js +260 -0
  132. package/server/routes/webhooks.js +203 -0
  133. package/server/routes.js +206 -0
  134. package/server/runtime-config.js +336 -0
  135. package/server/share.js +305 -0
  136. package/server/stt/faster-whisper.js +72 -0
  137. package/server/stt/groq.js +51 -0
  138. package/server/stt/index.js +196 -0
  139. package/server/stt/openai.js +49 -0
  140. package/server/sync.js +244 -0
  141. package/server/tailscale-https.js +175 -0
  142. package/server/tts.js +646 -0
  143. package/server/update-checker.js +172 -0
  144. package/server/utils/filename.js +129 -0
  145. package/server/utils.js +147 -0
  146. package/server/watchdog.js +318 -0
  147. package/server/websocket/broadcast.js +359 -0
  148. package/server/websocket/connections.js +339 -0
  149. package/server/websocket/index.js +215 -0
  150. package/server/websocket/routing.js +277 -0
  151. package/server/websocket/sync.js +102 -0
  152. package/server.js +404 -0
  153. package/utils/detect-tool-usage.js +93 -0
  154. package/utils/errors.js +158 -0
  155. package/utils/html-escape.js +84 -0
  156. package/utils/id-sanitize.js +94 -0
  157. package/utils/response.js +130 -0
  158. package/utils/with-retry.js +105 -0
@@ -0,0 +1,661 @@
1
+ // ============================================
2
+ // THEME GENERATOR MODULE
3
+ // Custom theme creator — Premium feature
4
+ // Multi-channel color control + named themes
5
+ // ============================================
6
+
7
+ // ── Color utilities ──
8
+
9
+ function hexToHSL(hex) {
10
+ hex = hex.replace('#', '');
11
+ var r = parseInt(hex.substring(0, 2), 16) / 255;
12
+ var g = parseInt(hex.substring(2, 4), 16) / 255;
13
+ var b = parseInt(hex.substring(4, 6), 16) / 255;
14
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
15
+ var h, s, l = (max + min) / 2;
16
+ if (max === min) { h = s = 0; }
17
+ else {
18
+ var d = max - min;
19
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
20
+ switch (max) {
21
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
22
+ case g: h = ((b - r) / d + 2) / 6; break;
23
+ case b: h = ((r - g) / d + 4) / 6; break;
24
+ }
25
+ }
26
+ return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
27
+ }
28
+
29
+ function hslToHex(h, s, l) {
30
+ s /= 100; l /= 100;
31
+ var a = s * Math.min(l, 1 - l);
32
+ var f = function(n) {
33
+ var k = (n + h / 30) % 12;
34
+ var color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
35
+ return Math.round(255 * color).toString(16).padStart(2, '0');
36
+ };
37
+ return '#' + f(0) + f(8) + f(4);
38
+ }
39
+
40
+ function hexToRGB(hex) {
41
+ hex = hex.replace('#', '');
42
+ return {
43
+ r: parseInt(hex.substring(0, 2), 16),
44
+ g: parseInt(hex.substring(2, 4), 16),
45
+ b: parseInt(hex.substring(4, 6), 16)
46
+ };
47
+ }
48
+
49
+ function luminance(hex) {
50
+ var rgb = hexToRGB(hex);
51
+ var vals = [rgb.r, rgb.g, rgb.b].map(function(v) {
52
+ v /= 255;
53
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
54
+ });
55
+ return 0.2126 * vals[0] + 0.7152 * vals[1] + 0.0722 * vals[2];
56
+ }
57
+
58
+ function bgIsDark(hex) { return luminance(hex) < 0.2; }
59
+
60
+ function escapeHtml(str) {
61
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
62
+ }
63
+ function escapeAttr(str) { return escapeHtml(str); }
64
+
65
+ // ── Theme generation from 6 color channels ──
66
+
67
+ function generateTheme(colors) {
68
+ var accent = colors.accent;
69
+ var bg = colors.background;
70
+ var userColor = colors.userBubble;
71
+ var aiColor = colors.aiBubble;
72
+ var textColor = colors.text;
73
+ var starColor = colors.stars;
74
+
75
+ var aHSL = hexToHSL(accent);
76
+ var aRGB = hexToRGB(accent);
77
+ var bHSL = hexToHSL(bg);
78
+ var bRGB = hexToRGB(bg);
79
+ var uRGB = hexToRGB(userColor);
80
+ var aiRGB = hexToRGB(aiColor);
81
+ var tHSL = hexToHSL(textColor);
82
+ var dark = bgIsDark(bg);
83
+
84
+ var accentHover = hslToHex(aHSL.h, Math.min(aHSL.s, 80), Math.min(aHSL.l + 10, 80));
85
+ var accentSecondary = hslToHex((aHSL.h + 40) % 360, Math.min(aHSL.s, 70), 50);
86
+
87
+ var bgSecL = dark ? Math.min(bHSL.l + 8, 25) : Math.max(bHSL.l - 3, 90);
88
+ var bgSecHex = hslToHex(bHSL.h, Math.min(bHSL.s, 20), bgSecL);
89
+ var bgSecRGB = hexToRGB(bgSecHex);
90
+
91
+ var bgInputL = dark ? Math.min(bHSL.l + 5, 20) : Math.max(bHSL.l - 2, 94);
92
+ var bgInputHex = hslToHex(bHSL.h, Math.min(bHSL.s, 15), bgInputL);
93
+ var bgInputRGB = hexToRGB(bgInputHex);
94
+
95
+ var textMuted = hslToHex(tHSL.h, Math.min(tHSL.s, 30), dark ? 55 : 50);
96
+ var textDim = hslToHex(tHSL.h, Math.min(tHSL.s, 25), dark ? 42 : 60);
97
+ var textInverse = (luminance(accent) < 0.3) ? '#ffffff' : '#000000';
98
+
99
+ var uHSL = hexToHSL(userColor);
100
+ var userAccent = hslToHex(uHSL.h, Math.min(uHSL.s, 60), dark ? 25 : 70);
101
+ var userAccentHover = hslToHex(uHSL.h, Math.min(uHSL.s, 55), dark ? 20 : 65);
102
+
103
+ var h = aHSL.h;
104
+ var vars = {
105
+ '--bg': bg,
106
+ '--bg-secondary': 'rgba(' + bgSecRGB.r + ',' + bgSecRGB.g + ',' + bgSecRGB.b + ',0.95)',
107
+ '--bg-input': 'rgba(' + bgInputRGB.r + ',' + bgInputRGB.g + ',' + bgInputRGB.b + ',0.8)',
108
+ '--accent': accent,
109
+ '--accent-hover': accentHover,
110
+ '--accent-secondary': accentSecondary,
111
+ '--text': textColor,
112
+ '--text-muted': textMuted,
113
+ '--text-dim': textDim,
114
+ '--text-inverse': textInverse,
115
+ '--border': 'rgba(' + aRGB.r + ',' + aRGB.g + ',' + aRGB.b + ',' + (dark ? '0.15' : '0.1') + ')',
116
+ '--accent-05': 'rgba(' + aRGB.r + ',' + aRGB.g + ',' + aRGB.b + ',0.05)',
117
+ '--accent-10': 'rgba(' + aRGB.r + ',' + aRGB.g + ',' + aRGB.b + ',0.1)',
118
+ '--accent-15': 'rgba(' + aRGB.r + ',' + aRGB.g + ',' + aRGB.b + ',0.15)',
119
+ '--accent-20': 'rgba(' + aRGB.r + ',' + aRGB.g + ',' + aRGB.b + ',0.2)',
120
+ '--accent-30': 'rgba(' + aRGB.r + ',' + aRGB.g + ',' + aRGB.b + ',0.3)',
121
+ '--accent-40': 'rgba(' + aRGB.r + ',' + aRGB.g + ',' + aRGB.b + ',0.4)',
122
+ '--accent-50': 'rgba(' + aRGB.r + ',' + aRGB.g + ',' + aRGB.b + ',0.5)',
123
+ '--user-bubble': 'rgba(' + uRGB.r + ',' + uRGB.g + ',' + uRGB.b + ',' + (dark ? '0.35' : '0.15') + ')',
124
+ '--user-accent': userAccent,
125
+ '--user-accent-hover': userAccentHover,
126
+ '--assistant-bubble': 'rgba(' + aiRGB.r + ',' + aiRGB.g + ',' + aiRGB.b + ',' + (dark ? '0.9' : '0.12') + ')',
127
+ '--star-color': starColor,
128
+ '--success': dark ? '#4ade80' : '#16a34a',
129
+ '--warning': dark ? '#fbbf24' : '#d97706',
130
+ '--info': '#60a5fa',
131
+ '--recording': dark ? '#ff3366' : '#dc2626',
132
+ '--error': dark ? '#ef4444' : '#dc2626',
133
+ '--neutral': '#6b7280',
134
+ '--syntax-keyword': dark ? hslToHex((h+280)%360,60,75) : hslToHex((h+280)%360,55,40),
135
+ '--syntax-string': dark ? hslToHex((h+120)%360,50,72) : hslToHex((h+120)%360,50,35),
136
+ '--syntax-comment': dark ? hslToHex(h,20,38) : hslToHex(h,8,64),
137
+ '--syntax-number': dark ? hslToHex((h+30)%360,60,65) : hslToHex(h,60,40),
138
+ };
139
+ if (!dark) {
140
+ vars['--error-10'] = 'rgba(220,38,38,0.08)';
141
+ vars['--error-20'] = 'rgba(220,38,38,0.12)';
142
+ vars['--error-30'] = 'rgba(220,38,38,0.2)';
143
+ vars['--success-20'] = 'rgba(22,163,74,0.1)';
144
+ vars['--black-20'] = 'rgba(0,0,0,0.06)';
145
+ vars['--black-40'] = 'rgba(0,0,0,0.1)';
146
+ } else {
147
+ vars['--error-10'] = 'rgba(255,68,102,0.1)';
148
+ vars['--error-20'] = 'rgba(255,68,102,0.2)';
149
+ vars['--error-30'] = 'rgba(255,68,102,0.3)';
150
+ vars['--success-20'] = 'rgba(0,255,136,0.2)';
151
+ }
152
+ return vars;
153
+ }
154
+
155
+ // ── Apply / Clear ──
156
+
157
+ function applyCustomVars(vars) {
158
+ var root = document.documentElement;
159
+ for (var key in vars) root.style.setProperty(key, vars[key]);
160
+ }
161
+
162
+ var ALL_VARS = [
163
+ '--bg','--bg-secondary','--bg-input',
164
+ '--accent','--accent-hover','--accent-secondary',
165
+ '--text','--text-muted','--text-dim','--text-inverse','--border',
166
+ '--accent-05','--accent-10','--accent-15','--accent-20',
167
+ '--accent-30','--accent-40','--accent-50',
168
+ '--error-10','--error-20','--error-30','--success-20',
169
+ '--user-bubble','--user-accent','--user-accent-hover',
170
+ '--assistant-bubble','--success','--warning','--info',
171
+ '--recording','--error','--neutral','--star-color',
172
+ '--syntax-keyword','--syntax-string','--syntax-comment','--syntax-number',
173
+ '--black-20','--black-40',
174
+ ];
175
+
176
+ function clearCustomVars() {
177
+ var root = document.documentElement;
178
+ ALL_VARS.forEach(function(v) { root.style.removeProperty(v); });
179
+ }
180
+
181
+ // ── Persistence ──
182
+
183
+ var STORAGE_KEY = 'uplink-custom-themes';
184
+ var ACTIVE_KEY = 'uplink-custom-active';
185
+
186
+ function loadAllThemes() {
187
+ try { var r = localStorage.getItem(STORAGE_KEY); return r ? JSON.parse(r) : []; }
188
+ catch(e) { return []; }
189
+ }
190
+ function saveAllThemes(themes) {
191
+ try { localStorage.setItem(STORAGE_KEY, JSON.stringify(themes)); }
192
+ catch(e) { console.warn('ThemeGen: save failed', e); }
193
+ }
194
+ function loadActiveId() { return localStorage.getItem(ACTIVE_KEY) || null; }
195
+ function saveActiveId(id) {
196
+ if (id) localStorage.setItem(ACTIVE_KEY, id);
197
+ else localStorage.removeItem(ACTIVE_KEY);
198
+ }
199
+ function generateId() {
200
+ return 'ct-' + Date.now().toString(36) + Math.random().toString(36).slice(2,6);
201
+ }
202
+
203
+ // Migrate v1 format
204
+ function migrateOldFormat() {
205
+ try {
206
+ var old = localStorage.getItem('uplink-custom-theme');
207
+ if (!old) return;
208
+ var data = JSON.parse(old);
209
+ if (data && data.accent) {
210
+ var m = {
211
+ id: generateId(), name: 'My Theme',
212
+ colors: {
213
+ accent: data.accent, background: '#000000',
214
+ userBubble: data.accent, aiBubble: '#002840',
215
+ text: '#f1f5f9', stars: data.accent,
216
+ },
217
+ createdAt: Date.now(),
218
+ };
219
+ var themes = loadAllThemes();
220
+ themes.push(m);
221
+ saveAllThemes(themes);
222
+ if (data.active) saveActiveId(m.id);
223
+ localStorage.removeItem('uplink-custom-theme');
224
+ }
225
+ } catch(e) {}
226
+ }
227
+
228
+ // ── Defaults & presets ──
229
+
230
+ var defaultColors = {
231
+ accent: '#8b5cf6', background: '#000000', userBubble: '#4c1d95',
232
+ aiBubble: '#002840', text: '#f1f5f9', stars: '#ffffff',
233
+ };
234
+
235
+ var presets = [
236
+ { name: 'Violet', accent:'#8b5cf6', bg:'#000000', user:'#4c1d95', ai:'#1a1030', text:'#f1f5f9', stars:'#c4b5fd' },
237
+ { name: 'Cyan', accent:'#00f0ff', bg:'#000000', user:'#0e4d65', ai:'#002840', text:'#f1f5f9', stars:'#00f0ff' },
238
+ { name: 'Rose', accent:'#f43f5e', bg:'#0a0000', user:'#6b1020', ai:'#2a0a10', text:'#fce4ec', stars:'#ff6b8a' },
239
+ { name: 'Amber', accent:'#f59e0b', bg:'#050200', user:'#6b3a00', ai:'#1a1000', text:'#fef3c7', stars:'#fbbf24' },
240
+ { name: 'Emerald', accent:'#10b981', bg:'#000a05', user:'#064e3b', ai:'#0a1f18', text:'#d1fae5', stars:'#6ee7b7' },
241
+ { name: 'Ocean', accent:'#3b82f6', bg:'#000008', user:'#1e3a6e', ai:'#0a1628', text:'#dbeafe', stars:'#60a5fa' },
242
+ { name: 'Sakura', accent:'#ec4899', bg:'#080004', user:'#6b1048', ai:'#1f0a18', text:'#fce7f3', stars:'#f9a8d4' },
243
+ { name: 'Snow', accent:'#6366f1', bg:'#f8f9fa', user:'#c7d2fe', ai:'#e9ecef', text:'#1a1a2e', stars:'#ffffff' },
244
+ ];
245
+
246
+ var channels = [
247
+ { key:'accent', label:'Accent', desc:'Buttons, links, highlights' },
248
+ { key:'background', label:'Background', desc:'Main background' },
249
+ { key:'userBubble', label:'Your Bubbles', desc:'Your message color' },
250
+ { key:'aiBubble', label:'AI Bubbles', desc:'Assistant message color' },
251
+ { key:'text', label:'Text', desc:'Primary text color' },
252
+ { key:'stars', label:'Stars', desc:'Background particles' },
253
+ ];
254
+
255
+ // ── State ──
256
+
257
+ var generatorEl = null;
258
+ var currentColors = assign({}, defaultColors);
259
+ var currentName = '';
260
+ var editingId = null;
261
+ var isActive = false;
262
+ var activeThemeId = null;
263
+
264
+ function assign(target, src) {
265
+ for (var k in src) target[k] = src[k];
266
+ return target;
267
+ }
268
+
269
+ // ── Apply a saved theme by ID ──
270
+
271
+ function applyThemeById(id) {
272
+ var themes = loadAllThemes();
273
+ var theme = themes.find(function(t) { return t.id === id; });
274
+ if (!theme) return;
275
+
276
+ var vars = generateTheme(theme.colors);
277
+ applyCustomVars(vars);
278
+ document.documentElement.setAttribute('data-theme', 'custom');
279
+ isActive = true;
280
+ activeThemeId = id;
281
+ saveActiveId(id);
282
+ ensureCustomOption(theme.name);
283
+
284
+ var themeSelect = document.getElementById('themeSelect');
285
+ if (themeSelect) themeSelect.value = 'custom';
286
+ var storage = window.UplinkStorage;
287
+ if (storage) storage.saveSettings({ theme: 'custom' });
288
+ }
289
+
290
+ // ── Theme select integration ──
291
+
292
+ function ensureCustomOption(name) {
293
+ var select = document.getElementById('themeSelect');
294
+ if (!select) return;
295
+ var opt = select.querySelector('option[value="custom"]');
296
+ if (!opt) {
297
+ opt = document.createElement('option');
298
+ opt.value = 'custom';
299
+ select.appendChild(opt);
300
+ }
301
+ opt.textContent = '✦ ' + (name || 'Custom');
302
+ }
303
+
304
+ function removeCustomOption() {
305
+ var select = document.getElementById('themeSelect');
306
+ if (!select) return;
307
+ var opt = select.querySelector('option[value="custom"]');
308
+ if (opt) opt.remove();
309
+ }
310
+
311
+ // ── Sync all pickers to currentColors ──
312
+
313
+ function syncPickersToColors() {
314
+ if (!generatorEl) return;
315
+ channels.forEach(function(ch) {
316
+ var val = currentColors[ch.key];
317
+ var picker = generatorEl.querySelector('.tg-color-picker[data-channel="' + ch.key + '"]');
318
+ var hex = generatorEl.querySelector('.tg-hex-input[data-channel="' + ch.key + '"]');
319
+ var display = picker ? picker.parentElement.querySelector('.tg-picker-display') : null;
320
+ if (picker) picker.value = val;
321
+ if (hex) hex.value = val;
322
+ if (display) display.style.background = val;
323
+ });
324
+ }
325
+
326
+ // ── Live update (when theme is already applied) ──
327
+
328
+ function liveUpdate() {
329
+ if (isActive && activeThemeId === editingId) {
330
+ var vars = generateTheme(currentColors);
331
+ applyCustomVars(vars);
332
+ }
333
+ }
334
+
335
+ // ── Preview ──
336
+
337
+ function updatePreview() {
338
+ var vars = generateTheme(currentColors);
339
+ var bar = document.getElementById('tgPreviewBar');
340
+ var dot = document.getElementById('tgPreviewAccent');
341
+ var label = document.getElementById('tgPreviewText');
342
+ var chat = document.getElementById('tgPreviewChat');
343
+ var userMsg = document.getElementById('tgPreviewUser');
344
+ var aiMsg = document.getElementById('tgPreviewAssistant');
345
+
346
+ if (bar) bar.style.background = vars['--bg-secondary'];
347
+ if (dot) dot.style.background = vars['--accent'];
348
+ if (label) label.style.color = vars['--text-muted'];
349
+ if (chat) chat.style.background = vars['--bg'];
350
+ if (userMsg) { userMsg.style.background = vars['--user-bubble']; userMsg.style.color = vars['--text']; }
351
+ if (aiMsg) { aiMsg.style.background = vars['--assistant-bubble']; aiMsg.style.color = vars['--text']; }
352
+ }
353
+
354
+ // ── Build UI ──
355
+
356
+ function buildUI() {
357
+ var container = document.getElementById('themeGeneratorContainer');
358
+ if (!container) return;
359
+
360
+ if (window.UplinkPremium && !window.UplinkPremium.isActive()) {
361
+ container.innerHTML = '';
362
+ return;
363
+ }
364
+
365
+ migrateOldFormat();
366
+ var savedThemes = loadAllThemes();
367
+ activeThemeId = loadActiveId();
368
+ isActive = !!activeThemeId;
369
+
370
+ if (editingId) {
371
+ var ed = savedThemes.find(function(t) { return t.id === editingId; });
372
+ if (ed) { currentColors = assign({}, ed.colors); currentName = ed.name; }
373
+ }
374
+
375
+ // Saved themes list
376
+ var savedHTML = '';
377
+ if (savedThemes.length > 0) {
378
+ var items = savedThemes.map(function(t) {
379
+ var act = activeThemeId === t.id;
380
+ var edt = editingId === t.id;
381
+ return '<div class="tg-saved-item' + (act ? ' active' : '') + (edt ? ' editing' : '') + '" data-theme-id="' + t.id + '">' +
382
+ '<div class="tg-saved-swatch" style="background:' + t.colors.accent + '"></div>' +
383
+ '<span class="tg-saved-name">' + escapeHtml(t.name || 'Untitled') + '</span>' +
384
+ (act ? '<span class="tg-saved-badge">Active</span>' : '') +
385
+ '<div class="tg-saved-actions">' +
386
+ '<button class="tg-saved-btn" title="Apply" data-action="apply">✓</button>' +
387
+ '<button class="tg-saved-btn" title="Edit" data-action="edit">✎</button>' +
388
+ '<button class="tg-saved-btn" title="Delete" data-action="delete">✕</button>' +
389
+ '</div>' +
390
+ '</div>';
391
+ }).join('');
392
+ savedHTML = '<div class="tg-saved"><div class="tg-label">Saved Themes</div><div class="tg-saved-list">' + items + '</div></div>';
393
+ }
394
+
395
+ // Channel pickers
396
+ var channelHTML = channels.map(function(ch) {
397
+ var val = currentColors[ch.key] || defaultColors[ch.key];
398
+ return '<div class="tg-channel">' +
399
+ '<div class="tg-channel-info">' +
400
+ '<span class="tg-channel-label">' + ch.label + '</span>' +
401
+ '<span class="tg-channel-desc">' + ch.desc + '</span>' +
402
+ '</div>' +
403
+ '<div class="tg-channel-controls">' +
404
+ '<div class="tg-picker-wrap">' +
405
+ '<input type="color" class="tg-color-picker" data-channel="' + ch.key + '" value="' + val + '">' +
406
+ '<div class="tg-picker-display" style="background:' + val + '"></div>' +
407
+ '</div>' +
408
+ '<input type="text" class="tg-hex-input" data-channel="' + ch.key + '" value="' + val + '" maxlength="7" spellcheck="false" autocomplete="off">' +
409
+ '</div>' +
410
+ '</div>';
411
+ }).join('');
412
+
413
+ // Presets
414
+ var presetsHTML = presets.map(function(p) {
415
+ return '<button class="tg-preset" data-preset="' + p.name + '" title="' + p.name + '">' +
416
+ '<span class="tg-preset-swatch" style="background:linear-gradient(135deg,' + p.bg + ' 40%,' + p.accent + ' 100%)"></span>' +
417
+ '<span class="tg-preset-name">' + p.name + '</span>' +
418
+ '</button>';
419
+ }).join('');
420
+
421
+ container.innerHTML =
422
+ '<div class="tg-section">' +
423
+ '<div class="tg-header">' +
424
+ '<span class="tg-title">Theme Studio</span>' +
425
+ '<span class="tg-badge">Premium</span>' +
426
+ '</div>' +
427
+ savedHTML +
428
+ '<div class="tg-editor">' +
429
+ '<div class="tg-name-row">' +
430
+ '<input type="text" class="tg-name-input" id="tgNameInput" placeholder="Theme name..." value="' + escapeAttr(currentName) + '" maxlength="24" spellcheck="false">' +
431
+ '</div>' +
432
+ '<div class="tg-presets-row">' +
433
+ '<div class="tg-label">Presets</div>' +
434
+ '<div class="tg-presets">' + presetsHTML + '</div>' +
435
+ '</div>' +
436
+ '<div class="tg-channels">' + channelHTML + '</div>' +
437
+ '<div class="tg-preview">' +
438
+ '<div class="tg-preview-bar" id="tgPreviewBar">' +
439
+ '<div class="tg-preview-dot" id="tgPreviewAccent"></div>' +
440
+ '<span class="tg-preview-label" id="tgPreviewText">Preview</span>' +
441
+ '</div>' +
442
+ '<div class="tg-preview-chat" id="tgPreviewChat">' +
443
+ '<div class="tg-preview-msg tg-preview-assistant" id="tgPreviewAssistant">Hey, how can I help?</div>' +
444
+ '<div class="tg-preview-msg tg-preview-user" id="tgPreviewUser">Looks great!</div>' +
445
+ '</div>' +
446
+ '</div>' +
447
+ '<div class="tg-actions">' +
448
+ '<button class="tg-btn tg-btn-primary" id="tgSaveBtn">' + (editingId ? 'Save Changes' : 'Save & Apply') + '</button>' +
449
+ '<button class="tg-btn tg-btn-secondary" id="tgCancelBtn">' + (editingId ? 'Cancel' : 'Reset') + '</button>' +
450
+ '</div>' +
451
+ '</div>' +
452
+ '</div>';
453
+
454
+ generatorEl = container;
455
+ setupEvents();
456
+ updatePreview();
457
+ }
458
+
459
+ // ── Events ──
460
+
461
+ function setupEvents() {
462
+ if (!generatorEl) return;
463
+
464
+ // Color pickers
465
+ generatorEl.querySelectorAll('.tg-color-picker').forEach(function(picker) {
466
+ picker.addEventListener('input', function() {
467
+ var ch = picker.dataset.channel;
468
+ currentColors[ch] = picker.value;
469
+ var hex = generatorEl.querySelector('.tg-hex-input[data-channel="' + ch + '"]');
470
+ if (hex) hex.value = picker.value;
471
+ var disp = picker.parentElement.querySelector('.tg-picker-display');
472
+ if (disp) disp.style.background = picker.value;
473
+ updatePreview();
474
+ liveUpdate();
475
+ });
476
+ });
477
+
478
+ // Hex inputs
479
+ generatorEl.querySelectorAll('.tg-hex-input').forEach(function(input) {
480
+ input.addEventListener('input', function() {
481
+ var val = input.value.trim();
482
+ if (/^#[0-9a-fA-F]{6}$/.test(val)) {
483
+ var ch = input.dataset.channel;
484
+ currentColors[ch] = val;
485
+ var picker = generatorEl.querySelector('.tg-color-picker[data-channel="' + ch + '"]');
486
+ if (picker) picker.value = val;
487
+ var disp = input.closest('.tg-channel-controls').querySelector('.tg-picker-display');
488
+ if (disp) disp.style.background = val;
489
+ updatePreview();
490
+ liveUpdate();
491
+ }
492
+ });
493
+ });
494
+
495
+ // Presets
496
+ generatorEl.querySelectorAll('.tg-preset').forEach(function(btn) {
497
+ btn.addEventListener('click', function() {
498
+ var p = presets.find(function(x) { return x.name === btn.dataset.preset; });
499
+ if (!p) return;
500
+ currentColors = { accent:p.accent, background:p.bg, userBubble:p.user, aiBubble:p.ai, text:p.text, stars:p.stars };
501
+ var nameInput = document.getElementById('tgNameInput');
502
+ if (nameInput && !nameInput.value.trim()) nameInput.value = p.name;
503
+ syncPickersToColors();
504
+ updatePreview();
505
+ liveUpdate();
506
+ });
507
+ });
508
+
509
+ // Name
510
+ var nameInput = document.getElementById('tgNameInput');
511
+ if (nameInput) nameInput.addEventListener('input', function() { currentName = nameInput.value; });
512
+
513
+ // Save & Apply
514
+ var saveBtn = document.getElementById('tgSaveBtn');
515
+ if (saveBtn) {
516
+ saveBtn.addEventListener('click', function() {
517
+ var themes = loadAllThemes();
518
+ var name = (currentName || '').trim() || 'Custom ' + (themes.length + 1);
519
+
520
+ if (editingId) {
521
+ var idx = -1;
522
+ for (var i = 0; i < themes.length; i++) { if (themes[i].id === editingId) { idx = i; break; } }
523
+ if (idx >= 0) { themes[idx].name = name; themes[idx].colors = assign({}, currentColors); }
524
+ saveAllThemes(themes);
525
+ applyThemeById(editingId);
526
+ } else {
527
+ var newT = { id: generateId(), name: name, colors: assign({}, currentColors), createdAt: Date.now() };
528
+ themes.push(newT);
529
+ saveAllThemes(themes);
530
+ applyThemeById(newT.id);
531
+ }
532
+ editingId = null;
533
+ currentName = '';
534
+ currentColors = assign({}, defaultColors);
535
+ buildUI();
536
+ });
537
+ }
538
+
539
+ // Cancel / Reset
540
+ var cancelBtn = document.getElementById('tgCancelBtn');
541
+ if (cancelBtn) {
542
+ cancelBtn.addEventListener('click', function() {
543
+ if (editingId) {
544
+ editingId = null;
545
+ currentName = '';
546
+ currentColors = assign({}, defaultColors);
547
+ buildUI();
548
+ } else {
549
+ clearCustomVars();
550
+ isActive = false;
551
+ activeThemeId = null;
552
+ saveActiveId(null);
553
+ document.documentElement.setAttribute('data-theme', 'midnight');
554
+ var sel = document.getElementById('themeSelect');
555
+ if (sel) { removeCustomOption(); sel.value = 'midnight'; }
556
+ var storage = window.UplinkStorage;
557
+ if (storage) storage.saveSettings({ theme: 'midnight' });
558
+ currentColors = assign({}, defaultColors);
559
+ currentName = '';
560
+ editingId = null;
561
+ buildUI();
562
+ }
563
+ });
564
+ }
565
+
566
+ // Saved theme actions (delegation)
567
+ var savedList = generatorEl.querySelector('.tg-saved-list');
568
+ if (savedList) {
569
+ savedList.addEventListener('click', function(e) {
570
+ var btn = e.target.closest('[data-action]');
571
+ if (!btn) return;
572
+ var item = btn.closest('.tg-saved-item');
573
+ if (!item) return;
574
+ var id = item.dataset.themeId;
575
+ var action = btn.dataset.action;
576
+
577
+ if (action === 'apply') {
578
+ applyThemeById(id);
579
+ buildUI();
580
+ } else if (action === 'edit') {
581
+ editingId = id;
582
+ buildUI();
583
+ } else if (action === 'delete') {
584
+ var themes = loadAllThemes();
585
+ themes = themes.filter(function(t) { return t.id !== id; });
586
+ saveAllThemes(themes);
587
+ if (activeThemeId === id) {
588
+ clearCustomVars();
589
+ isActive = false;
590
+ activeThemeId = null;
591
+ saveActiveId(null);
592
+ document.documentElement.setAttribute('data-theme', 'midnight');
593
+ var sel = document.getElementById('themeSelect');
594
+ if (sel) { removeCustomOption(); sel.value = 'midnight'; }
595
+ var storage = window.UplinkStorage;
596
+ if (storage) storage.saveSettings({ theme: 'midnight' });
597
+ }
598
+ if (editingId === id) {
599
+ editingId = null;
600
+ currentColors = assign({}, defaultColors);
601
+ currentName = '';
602
+ }
603
+ buildUI();
604
+ }
605
+ });
606
+ }
607
+ }
608
+
609
+ // ── Init ──
610
+
611
+ function init() {
612
+ if (window.UplinkPremium && !window.UplinkPremium.isActive()) {
613
+ window.addEventListener('uplink:premiumChange', function(e) {
614
+ if (e.detail && e.detail.active) buildUI();
615
+ });
616
+ return;
617
+ }
618
+ buildUI();
619
+
620
+ // Restore active custom theme on load
621
+ var id = loadActiveId();
622
+ if (id) {
623
+ var themes = loadAllThemes();
624
+ var theme = themes.find(function(t) { return t.id === id; });
625
+ if (theme) {
626
+ var vars = generateTheme(theme.colors);
627
+ applyCustomVars(vars);
628
+ document.documentElement.setAttribute('data-theme', 'custom');
629
+ isActive = true;
630
+ activeThemeId = id;
631
+ ensureCustomOption(theme.name);
632
+ var sel = document.getElementById('themeSelect');
633
+ if (sel) sel.value = 'custom';
634
+ }
635
+ }
636
+ }
637
+
638
+ // Listen for built-in theme changes — deactivate custom
639
+ window.addEventListener('uplink:themeChange', function(e) {
640
+ if (e.detail && e.detail.theme !== 'custom' && isActive) {
641
+ clearCustomVars();
642
+ isActive = false;
643
+ activeThemeId = null;
644
+ saveActiveId(null);
645
+ var applyBtn = document.getElementById('tgSaveBtn');
646
+ if (applyBtn) { applyBtn.textContent = 'Save & Apply'; }
647
+ }
648
+ });
649
+
650
+ // Expose API
651
+ export const UplinkThemeGenerator = {
652
+ build: buildUI,
653
+ isActive: function() { return isActive; },
654
+ };
655
+
656
+ import { UplinkCore } from './core.js';
657
+
658
+ // Backward compat: assign to window
659
+ window.UplinkThemeGenerator = UplinkThemeGenerator;
660
+
661
+ UplinkCore.registerModule('themeGenerator', init);