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,3 @@
1
+ // Centralized state — will be populated in Phase 2
2
+ // For now, modules that need state receive it via init()
3
+ export const state = {};
@@ -0,0 +1,688 @@
1
+ import { refreshIcons, iconHtml } from './icons.js';
2
+
3
+ var ctx;
4
+ var notes = new Map(); // id -> { data, el }
5
+ var notesVisible = false;
6
+ var updateTimers = {};
7
+ var textTimers = {};
8
+ var colorPickerEl = null;
9
+
10
+ var NOTE_COLORS = ["yellow", "blue", "green", "pink", "orange", "purple"];
11
+
12
+ function getContainerBounds() {
13
+ var c = document.getElementById("sticky-notes-container");
14
+ if (!c || c.clientWidth === 0 || c.clientHeight === 0) return null;
15
+ return { w: c.clientWidth, h: c.clientHeight };
16
+ }
17
+
18
+ function clampPos(x, y, noteW, noteH) {
19
+ var b = getContainerBounds();
20
+ if (!b) return { x: x, y: y };
21
+ return {
22
+ x: Math.max(0, Math.min(x, b.w - noteW)),
23
+ y: Math.max(0, Math.min(y, b.h - noteH)),
24
+ };
25
+ }
26
+
27
+ function clampSize(x, y, w, h) {
28
+ var b = getContainerBounds();
29
+ if (!b) return { w: w, h: h };
30
+ return {
31
+ w: Math.min(w, b.w - x),
32
+ h: Math.min(h, b.h - y),
33
+ };
34
+ }
35
+
36
+ function reclampAllNotes() {
37
+ notes.forEach(function (entry) {
38
+ var el = entry.el;
39
+ var noteW = el.offsetWidth;
40
+ var noteH = el.offsetHeight;
41
+ var curX = parseInt(el.style.left) || 0;
42
+ var curY = parseInt(el.style.top) || 0;
43
+ var c = clampPos(curX, curY, noteW, noteH);
44
+ el.style.left = c.x + "px";
45
+ el.style.top = c.y + "px";
46
+ });
47
+ }
48
+
49
+ export function initStickyNotes(_ctx) {
50
+ ctx = _ctx;
51
+
52
+ var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
53
+ if (toggleBtn) {
54
+ toggleBtn.addEventListener("click", function () {
55
+ if (!notesVisible && notes.size > 0) {
56
+ // Hidden with existing notes → just show them
57
+ showNotes();
58
+ } else {
59
+ // Visible or no notes → create a new one
60
+ showNotes();
61
+ createNote();
62
+ }
63
+ });
64
+
65
+ // Long-press or right-click to toggle hide
66
+ toggleBtn.addEventListener("contextmenu", function (e) {
67
+ e.preventDefault();
68
+ if (notesVisible) hideNotes();
69
+ });
70
+ }
71
+
72
+ // Re-clamp note positions on window resize so notes stay visible
73
+ var resizeTimer;
74
+ window.addEventListener("resize", function () {
75
+ clearTimeout(resizeTimer);
76
+ resizeTimer = setTimeout(function () {
77
+ if (notesVisible && notes.size > 0) {
78
+ reclampAllNotes();
79
+ }
80
+ }, 100);
81
+ });
82
+ }
83
+
84
+ // --- Visibility ---
85
+
86
+ function showNotes() {
87
+ notesVisible = true;
88
+ var container = document.getElementById("sticky-notes-container");
89
+ var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
90
+ if (container) container.classList.remove("hidden");
91
+ if (toggleBtn) toggleBtn.classList.add("active");
92
+ }
93
+
94
+ function hideNotes() {
95
+ notesVisible = false;
96
+ var container = document.getElementById("sticky-notes-container");
97
+ var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
98
+ if (container) container.classList.add("hidden");
99
+ if (toggleBtn) toggleBtn.classList.remove("active");
100
+ closeColorPicker();
101
+ }
102
+
103
+ function createNote() {
104
+ var container = document.getElementById("sticky-notes-container");
105
+ if (!container) return;
106
+ // Scatter position so notes don't stack exactly
107
+ var offset = (notes.size % 5) * 30;
108
+ wsSend({
109
+ type: "note_create",
110
+ x: 60 + offset,
111
+ y: 60 + offset,
112
+ color: "yellow",
113
+ });
114
+ }
115
+
116
+ function updateBadge() {
117
+ var badge = document.querySelector(".sticky-notes-count");
118
+ if (!badge) return;
119
+ if (notes.size > 0) {
120
+ badge.textContent = notes.size;
121
+ badge.classList.remove("hidden");
122
+ } else {
123
+ badge.classList.add("hidden");
124
+ }
125
+ }
126
+
127
+ // --- WS send helpers ---
128
+
129
+ function wsSend(obj) {
130
+ if (ctx && ctx.ws && ctx.connected) {
131
+ ctx.ws.send(JSON.stringify(obj));
132
+ }
133
+ }
134
+
135
+ function debouncedUpdate(id, changes, delay) {
136
+ clearTimeout(updateTimers[id]);
137
+ updateTimers[id] = setTimeout(function () {
138
+ changes.type = "note_update";
139
+ changes.id = id;
140
+ wsSend(changes);
141
+ }, delay || 300);
142
+ }
143
+
144
+ function debouncedTextUpdate(id, text) {
145
+ clearTimeout(textTimers[id]);
146
+ textTimers[id] = setTimeout(function () {
147
+ wsSend({ type: "note_update", id: id, text: text });
148
+ }, 500);
149
+ }
150
+
151
+ // --- Simple markdown ---
152
+
153
+ function getTitle(text) {
154
+ if (!text) return "";
155
+ var idx = text.indexOf("\n");
156
+ return idx === -1 ? text : text.substring(0, idx);
157
+ }
158
+
159
+ function renderMiniMarkdown(text) {
160
+ if (!text) return "";
161
+ var lines = text.split("\n");
162
+ var title = lines[0];
163
+ var body = lines.slice(1).join("\n");
164
+
165
+ function fmt(s) {
166
+ var escaped = s
167
+ .replace(/&/g, "&")
168
+ .replace(/</g, "&lt;")
169
+ .replace(/>/g, "&gt;");
170
+ return escaped
171
+ .replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
172
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
173
+ .replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<em>$1</em>")
174
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
175
+ .replace(/~~(.+?)~~/g, "<del>$1</del>")
176
+ .replace(/^- \[x\]/gm, '<span class="sn-check checked">✓</span>')
177
+ .replace(/^- \[ \]/gm, '<span class="sn-check">☐</span>')
178
+ .replace(/\n/g, "<br>");
179
+ }
180
+
181
+ var html = '<div class="sn-title">' + fmt(title) + '</div>';
182
+ if (body.trim()) {
183
+ html += fmt(body);
184
+ }
185
+ return html;
186
+ }
187
+
188
+ function syncTitle(noteEl, text) {
189
+ var spacer = noteEl.querySelector(".sticky-note-spacer");
190
+ if (spacer) spacer.textContent = getTitle(text);
191
+ }
192
+
193
+ // --- Note rendering ---
194
+
195
+ function renderNote(data) {
196
+ var el = document.createElement("div");
197
+ el.className = "sticky-note";
198
+ el.dataset.noteId = data.id;
199
+ var clamped = clampPos(data.x, data.y, data.w, data.h);
200
+ el.style.left = clamped.x + "px";
201
+ el.style.top = clamped.y + "px";
202
+ el.style.width = data.w + "px";
203
+ el.style.height = data.h + "px";
204
+ el.style.zIndex = 100 + (data.zIndex || 0);
205
+ el.dataset.color = data.color || "yellow";
206
+
207
+ if (data.minimized) el.classList.add("minimized");
208
+
209
+ // Header
210
+ var header = document.createElement("div");
211
+ header.className = "sticky-note-header";
212
+
213
+ var deleteBtn = document.createElement("button");
214
+ deleteBtn.className = "sticky-note-btn sticky-note-delete";
215
+ deleteBtn.title = "Delete";
216
+ deleteBtn.innerHTML = iconHtml("x");
217
+ header.appendChild(deleteBtn);
218
+
219
+ var minBtn = document.createElement("button");
220
+ minBtn.className = "sticky-note-btn sticky-note-min-btn";
221
+ minBtn.title = data.minimized ? "Expand" : "Minimize";
222
+ minBtn.innerHTML = data.minimized ? iconHtml("maximize-2") : iconHtml("minus");
223
+ header.appendChild(minBtn);
224
+
225
+ var spacer = document.createElement("div");
226
+ spacer.className = "sticky-note-spacer";
227
+ spacer.textContent = getTitle(data.text);
228
+ header.appendChild(spacer);
229
+
230
+ var addBtn = document.createElement("button");
231
+ addBtn.className = "sticky-note-btn";
232
+ addBtn.title = "New note";
233
+ addBtn.innerHTML = iconHtml("plus");
234
+ addBtn.addEventListener("click", function (e) {
235
+ e.stopPropagation();
236
+ createNote();
237
+ });
238
+ header.appendChild(addBtn);
239
+
240
+ var colorBtn = document.createElement("button");
241
+ colorBtn.className = "sticky-note-color-btn";
242
+ colorBtn.title = "Change color";
243
+ colorBtn.innerHTML = iconHtml("palette");
244
+ header.appendChild(colorBtn);
245
+
246
+ el.appendChild(header);
247
+
248
+ // Body
249
+ var body = document.createElement("div");
250
+ body.className = "sticky-note-body";
251
+
252
+ var textarea = document.createElement("textarea");
253
+ textarea.className = "sticky-note-text";
254
+ textarea.value = data.text || "";
255
+ textarea.placeholder = "Type a note...";
256
+ body.appendChild(textarea);
257
+
258
+ var rendered = document.createElement("div");
259
+ rendered.className = "sticky-note-rendered";
260
+ body.appendChild(rendered);
261
+
262
+ // Show rendered view if there's content
263
+ if (data.text) {
264
+ rendered.innerHTML = renderMiniMarkdown(data.text);
265
+ textarea.style.display = "none";
266
+ rendered.style.display = "";
267
+ } else {
268
+ textarea.style.display = "";
269
+ rendered.style.display = "none";
270
+ }
271
+
272
+ el.appendChild(body);
273
+
274
+ // Resize handle
275
+ var resizeHandle = document.createElement("div");
276
+ resizeHandle.className = "sticky-note-resize";
277
+ el.appendChild(resizeHandle);
278
+
279
+ // --- Event handlers ---
280
+ setupDrag(el, spacer, data.id);
281
+ setupResize(el, resizeHandle, data.id);
282
+ setupTextEdit(textarea, rendered, data.id);
283
+ setupColorPicker(colorBtn, el, data.id);
284
+ setupMinimize(minBtn, el, data.id);
285
+ setupDelete(deleteBtn, data.id);
286
+ setupBringToFront(el, data.id);
287
+
288
+ refreshIcons();
289
+ return el;
290
+ }
291
+
292
+ // --- Drag ---
293
+
294
+ function setupDrag(noteEl, spacerEl, noteId) {
295
+ var dragging = false;
296
+ var startX, startY, origX, origY;
297
+
298
+ spacerEl.addEventListener("mousedown", function (e) {
299
+ if (e.button !== 0) return;
300
+ e.preventDefault();
301
+ dragging = true;
302
+ startX = e.clientX;
303
+ startY = e.clientY;
304
+ origX = parseInt(noteEl.style.left) || 0;
305
+ origY = parseInt(noteEl.style.top) || 0;
306
+ noteEl.classList.add("dragging");
307
+ document.addEventListener("mousemove", onMove);
308
+ document.addEventListener("mouseup", onUp);
309
+ });
310
+
311
+ function onMove(e) {
312
+ if (!dragging) return;
313
+ var dx = e.clientX - startX;
314
+ var dy = e.clientY - startY;
315
+ var c = clampPos(origX + dx, origY + dy, noteEl.offsetWidth, noteEl.offsetHeight);
316
+ noteEl.style.left = c.x + "px";
317
+ noteEl.style.top = c.y + "px";
318
+ }
319
+
320
+ function onUp() {
321
+ if (!dragging) return;
322
+ dragging = false;
323
+ noteEl.classList.remove("dragging");
324
+ document.removeEventListener("mousemove", onMove);
325
+ document.removeEventListener("mouseup", onUp);
326
+ debouncedUpdate(noteId, {
327
+ x: parseInt(noteEl.style.left),
328
+ y: parseInt(noteEl.style.top),
329
+ }, 200);
330
+ }
331
+
332
+ // Touch support
333
+ spacerEl.addEventListener("touchstart", function (e) {
334
+ if (e.touches.length !== 1) return;
335
+ var touch = e.touches[0];
336
+ dragging = true;
337
+ startX = touch.clientX;
338
+ startY = touch.clientY;
339
+ origX = parseInt(noteEl.style.left) || 0;
340
+ origY = parseInt(noteEl.style.top) || 0;
341
+ noteEl.classList.add("dragging");
342
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
343
+ document.addEventListener("touchend", onTouchEnd);
344
+ }, { passive: true });
345
+
346
+ function onTouchMove(e) {
347
+ if (!dragging) return;
348
+ e.preventDefault();
349
+ var touch = e.touches[0];
350
+ var dx = touch.clientX - startX;
351
+ var dy = touch.clientY - startY;
352
+ var c = clampPos(origX + dx, origY + dy, noteEl.offsetWidth, noteEl.offsetHeight);
353
+ noteEl.style.left = c.x + "px";
354
+ noteEl.style.top = c.y + "px";
355
+ }
356
+
357
+ function onTouchEnd() {
358
+ if (!dragging) return;
359
+ dragging = false;
360
+ noteEl.classList.remove("dragging");
361
+ document.removeEventListener("touchmove", onTouchMove);
362
+ document.removeEventListener("touchend", onTouchEnd);
363
+ debouncedUpdate(noteId, {
364
+ x: parseInt(noteEl.style.left),
365
+ y: parseInt(noteEl.style.top),
366
+ }, 200);
367
+ }
368
+ }
369
+
370
+ // --- Resize ---
371
+
372
+ function setupResize(noteEl, handle, noteId) {
373
+ var resizing = false;
374
+ var startX, startY, origW, origH;
375
+ var MIN_W = 160;
376
+ var MIN_H = 80;
377
+
378
+ handle.addEventListener("mousedown", function (e) {
379
+ if (e.button !== 0) return;
380
+ e.preventDefault();
381
+ e.stopPropagation();
382
+ resizing = true;
383
+ startX = e.clientX;
384
+ startY = e.clientY;
385
+ origW = noteEl.offsetWidth;
386
+ origH = noteEl.offsetHeight;
387
+ noteEl.classList.add("resizing");
388
+ document.addEventListener("mousemove", onMove);
389
+ document.addEventListener("mouseup", onUp);
390
+ });
391
+
392
+ function onMove(e) {
393
+ if (!resizing) return;
394
+ var rawW = Math.max(MIN_W, origW + (e.clientX - startX));
395
+ var rawH = Math.max(MIN_H, origH + (e.clientY - startY));
396
+ var cs = clampSize(parseInt(noteEl.style.left) || 0, parseInt(noteEl.style.top) || 0, rawW, rawH);
397
+ noteEl.style.width = Math.max(MIN_W, cs.w) + "px";
398
+ noteEl.style.height = Math.max(MIN_H, cs.h) + "px";
399
+ }
400
+
401
+ function onUp() {
402
+ if (!resizing) return;
403
+ resizing = false;
404
+ noteEl.classList.remove("resizing");
405
+ document.removeEventListener("mousemove", onMove);
406
+ document.removeEventListener("mouseup", onUp);
407
+ debouncedUpdate(noteId, {
408
+ w: noteEl.offsetWidth,
409
+ h: noteEl.offsetHeight,
410
+ }, 200);
411
+ }
412
+
413
+ // Touch resize
414
+ handle.addEventListener("touchstart", function (e) {
415
+ if (e.touches.length !== 1) return;
416
+ e.stopPropagation();
417
+ var touch = e.touches[0];
418
+ resizing = true;
419
+ startX = touch.clientX;
420
+ startY = touch.clientY;
421
+ origW = noteEl.offsetWidth;
422
+ origH = noteEl.offsetHeight;
423
+ noteEl.classList.add("resizing");
424
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
425
+ document.addEventListener("touchend", onTouchEnd);
426
+ }, { passive: true });
427
+
428
+ function onTouchMove(e) {
429
+ if (!resizing) return;
430
+ e.preventDefault();
431
+ var touch = e.touches[0];
432
+ var rawW = Math.max(MIN_W, origW + (touch.clientX - startX));
433
+ var rawH = Math.max(MIN_H, origH + (touch.clientY - startY));
434
+ var cs = clampSize(parseInt(noteEl.style.left) || 0, parseInt(noteEl.style.top) || 0, rawW, rawH);
435
+ noteEl.style.width = Math.max(MIN_W, cs.w) + "px";
436
+ noteEl.style.height = Math.max(MIN_H, cs.h) + "px";
437
+ }
438
+
439
+ function onTouchEnd() {
440
+ if (!resizing) return;
441
+ resizing = false;
442
+ noteEl.classList.remove("resizing");
443
+ document.removeEventListener("touchmove", onTouchMove);
444
+ document.removeEventListener("touchend", onTouchEnd);
445
+ debouncedUpdate(noteId, {
446
+ w: noteEl.offsetWidth,
447
+ h: noteEl.offsetHeight,
448
+ }, 200);
449
+ }
450
+ }
451
+
452
+ // --- Text edit ---
453
+
454
+ function switchToEdit(textarea, rendered) {
455
+ rendered.style.display = "none";
456
+ textarea.style.display = "";
457
+ textarea.focus();
458
+ }
459
+
460
+ function setupTextEdit(textarea, rendered, noteId) {
461
+ var noteEl = textarea.closest(".sticky-note");
462
+ var spacer = noteEl.querySelector(".sticky-note-spacer");
463
+
464
+ textarea.addEventListener("input", function () {
465
+ debouncedTextUpdate(noteId, textarea.value);
466
+ syncTitle(noteEl, textarea.value);
467
+ });
468
+
469
+ // Click spacer → switch to edit
470
+ if (spacer) {
471
+ spacer.addEventListener("click", function () {
472
+ if (noteEl.classList.contains("minimized")) return;
473
+ switchToEdit(textarea, rendered);
474
+ });
475
+ }
476
+
477
+ // Click rendered → switch to edit
478
+ rendered.addEventListener("mousedown", function (e) {
479
+ e.stopPropagation();
480
+ });
481
+ rendered.addEventListener("click", function (e) {
482
+ if (e.target.tagName === "A") return;
483
+ switchToEdit(textarea, rendered);
484
+ });
485
+
486
+ // Blur textarea → switch to rendered (if has content)
487
+ textarea.addEventListener("blur", function () {
488
+ if (textarea.value.trim()) {
489
+ rendered.innerHTML = renderMiniMarkdown(textarea.value);
490
+ textarea.style.display = "none";
491
+ rendered.style.display = "";
492
+ }
493
+ });
494
+
495
+ // Prevent drag when clicking textarea
496
+ textarea.addEventListener("mousedown", function (e) {
497
+ e.stopPropagation();
498
+ });
499
+ }
500
+
501
+ // --- Color picker ---
502
+
503
+ function closeColorPicker() {
504
+ if (colorPickerEl) {
505
+ colorPickerEl.remove();
506
+ colorPickerEl = null;
507
+ }
508
+ }
509
+
510
+ function setupColorPicker(btn, noteEl, noteId) {
511
+ btn.addEventListener("click", function (e) {
512
+ e.stopPropagation();
513
+ showColorPicker(btn, noteEl, noteId);
514
+ });
515
+ }
516
+
517
+ function showColorPicker(anchor, noteEl, noteId) {
518
+ closeColorPicker();
519
+
520
+ var picker = document.createElement("div");
521
+ picker.className = "sticky-note-color-picker";
522
+
523
+ for (var i = 0; i < NOTE_COLORS.length; i++) {
524
+ (function (color) {
525
+ var dot = document.createElement("button");
526
+ dot.className = "sticky-note-color-dot";
527
+ dot.dataset.color = color;
528
+ if (noteEl.dataset.color === color) dot.classList.add("active");
529
+ dot.addEventListener("click", function (e) {
530
+ e.stopPropagation();
531
+ noteEl.dataset.color = color;
532
+ wsSend({ type: "note_update", id: noteId, color: color });
533
+ closeColorPicker();
534
+ });
535
+ picker.appendChild(dot);
536
+ })(NOTE_COLORS[i]);
537
+ }
538
+
539
+ document.body.appendChild(picker);
540
+ colorPickerEl = picker;
541
+
542
+ // Position relative to anchor
543
+ var rect = anchor.getBoundingClientRect();
544
+ picker.style.left = rect.left + "px";
545
+ picker.style.top = (rect.bottom + 4) + "px";
546
+
547
+ // Close on outside click
548
+ setTimeout(function () {
549
+ document.addEventListener("click", function closeHandler() {
550
+ closeColorPicker();
551
+ document.removeEventListener("click", closeHandler);
552
+ });
553
+ }, 0);
554
+ }
555
+
556
+ // --- Minimize ---
557
+
558
+ function setupMinimize(btn, noteEl, noteId) {
559
+ btn.addEventListener("click", function (e) {
560
+ e.stopPropagation();
561
+ var isMinimized = noteEl.classList.toggle("minimized");
562
+ btn.innerHTML = isMinimized ? iconHtml("maximize-2") : iconHtml("minus");
563
+ btn.title = isMinimized ? "Expand" : "Minimize";
564
+ refreshIcons();
565
+ wsSend({ type: "note_update", id: noteId, minimized: isMinimized });
566
+ });
567
+ }
568
+
569
+ // --- Delete ---
570
+
571
+ function setupDelete(btn, noteId) {
572
+ btn.addEventListener("click", function (e) {
573
+ e.stopPropagation();
574
+ wsSend({ type: "note_delete", id: noteId });
575
+ });
576
+ }
577
+
578
+ // --- Bring to front ---
579
+
580
+ function setupBringToFront(noteEl, noteId) {
581
+ noteEl.addEventListener("mousedown", function (e) {
582
+ // Skip bring-to-front when clicking header buttons to avoid
583
+ // race condition where server response replaces innerHTML
584
+ // between mousedown and click, causing the click event to be lost
585
+ if (e.target.closest("button")) return;
586
+ wsSend({ type: "note_bring_front", id: noteId });
587
+ });
588
+ }
589
+
590
+ // --- WS message handlers ---
591
+
592
+ export function handleNotesList(msg) {
593
+ var container = document.getElementById("sticky-notes-container");
594
+ if (!container) return;
595
+
596
+ // Clear existing
597
+ container.innerHTML = "";
598
+ notes.clear();
599
+
600
+ var list = msg.notes || [];
601
+ for (var i = 0; i < list.length; i++) {
602
+ var el = renderNote(list[i]);
603
+ notes.set(list[i].id, { data: list[i], el: el });
604
+ container.appendChild(el);
605
+ }
606
+
607
+ updateBadge();
608
+
609
+ // Auto-show if there are notes
610
+ if (list.length > 0 && !notesVisible) {
611
+ notesVisible = true;
612
+ container.classList.remove("hidden");
613
+ var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
614
+ if (toggleBtn) toggleBtn.classList.add("active");
615
+ }
616
+ }
617
+
618
+ export function handleNoteCreated(msg) {
619
+ var container = document.getElementById("sticky-notes-container");
620
+ if (!container || !msg.note) return;
621
+
622
+ // Don't duplicate
623
+ if (notes.has(msg.note.id)) return;
624
+
625
+ var el = renderNote(msg.note);
626
+ notes.set(msg.note.id, { data: msg.note, el: el });
627
+ container.appendChild(el);
628
+ updateBadge();
629
+
630
+ // Show container if hidden
631
+ if (!notesVisible) {
632
+ notesVisible = true;
633
+ container.classList.remove("hidden");
634
+ var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
635
+ if (toggleBtn) toggleBtn.classList.add("active");
636
+ }
637
+ }
638
+
639
+ export function handleNoteUpdated(msg) {
640
+ if (!msg.note) return;
641
+ var entry = notes.get(msg.note.id);
642
+ if (!entry) return;
643
+
644
+ entry.data = msg.note;
645
+
646
+ // Update DOM
647
+ entry.el.style.left = msg.note.x + "px";
648
+ entry.el.style.top = msg.note.y + "px";
649
+ entry.el.style.width = msg.note.w + "px";
650
+ entry.el.style.height = msg.note.h + "px";
651
+ entry.el.style.zIndex = 100 + (msg.note.zIndex || 0);
652
+ entry.el.dataset.color = msg.note.color || "yellow";
653
+
654
+ // Update text only if not actively editing
655
+ var textarea = entry.el.querySelector(".sticky-note-text");
656
+ var rendered = entry.el.querySelector(".sticky-note-rendered");
657
+ if (textarea && textarea !== document.activeElement) {
658
+ textarea.value = msg.note.text || "";
659
+ if (rendered && msg.note.text) {
660
+ rendered.innerHTML = renderMiniMarkdown(msg.note.text);
661
+ }
662
+ syncTitle(entry.el, msg.note.text);
663
+ }
664
+
665
+ var minBtn = entry.el.querySelector(".sticky-note-min-btn");
666
+ if (msg.note.minimized) {
667
+ entry.el.classList.add("minimized");
668
+ if (minBtn) { minBtn.innerHTML = iconHtml("maximize-2"); minBtn.title = "Expand"; }
669
+ } else {
670
+ entry.el.classList.remove("minimized");
671
+ if (minBtn) { minBtn.innerHTML = iconHtml("minus"); minBtn.title = "Minimize"; }
672
+ }
673
+ refreshIcons();
674
+ }
675
+
676
+ export function handleNoteDeleted(msg) {
677
+ var entry = notes.get(msg.id);
678
+ if (!entry) return;
679
+ entry.el.remove();
680
+ notes.delete(msg.id);
681
+ updateBadge();
682
+
683
+ // Clear debounce timers
684
+ clearTimeout(updateTimers[msg.id]);
685
+ clearTimeout(textTimers[msg.id]);
686
+ delete updateTimers[msg.id];
687
+ delete textTimers[msg.id];
688
+ }