clay-server 2.6.0 → 2.7.1

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 (38) hide show
  1. package/bin/cli.js +53 -4
  2. package/lib/config.js +15 -6
  3. package/lib/daemon.js +47 -5
  4. package/lib/ipc.js +12 -0
  5. package/lib/notes.js +2 -2
  6. package/lib/project.js +883 -2
  7. package/lib/public/app.js +862 -14
  8. package/lib/public/css/diff.css +12 -0
  9. package/lib/public/css/filebrowser.css +1 -1
  10. package/lib/public/css/loop.css +841 -0
  11. package/lib/public/css/menus.css +5 -0
  12. package/lib/public/css/mobile-nav.css +15 -15
  13. package/lib/public/css/rewind.css +23 -0
  14. package/lib/public/css/scheduler-modal.css +546 -0
  15. package/lib/public/css/scheduler.css +944 -0
  16. package/lib/public/css/sidebar.css +1 -0
  17. package/lib/public/css/skills.css +59 -0
  18. package/lib/public/css/sticky-notes.css +486 -0
  19. package/lib/public/css/title-bar.css +83 -3
  20. package/lib/public/index.html +181 -3
  21. package/lib/public/modules/diff.js +3 -3
  22. package/lib/public/modules/filebrowser.js +169 -45
  23. package/lib/public/modules/input.js +17 -3
  24. package/lib/public/modules/markdown.js +10 -0
  25. package/lib/public/modules/qrcode.js +23 -26
  26. package/lib/public/modules/scheduler.js +1240 -0
  27. package/lib/public/modules/server-settings.js +40 -0
  28. package/lib/public/modules/sidebar.js +12 -0
  29. package/lib/public/modules/skills.js +84 -0
  30. package/lib/public/modules/sticky-notes.js +617 -52
  31. package/lib/public/modules/theme.js +9 -19
  32. package/lib/public/modules/tools.js +16 -2
  33. package/lib/public/style.css +3 -0
  34. package/lib/scheduler.js +362 -0
  35. package/lib/sdk-bridge.js +36 -0
  36. package/lib/sessions.js +9 -5
  37. package/lib/utils.js +49 -3
  38. package/package.json +1 -1
@@ -3,12 +3,15 @@ import { refreshIcons, iconHtml } from './icons.js';
3
3
  var ctx;
4
4
  var notes = new Map(); // id -> { data, el }
5
5
  var notesVisible = false;
6
+ var archiveOpen = false;
6
7
  var updateTimers = {};
7
8
  var textTimers = {};
8
9
  var colorPickerEl = null;
10
+ var formatToolbarEl = null;
9
11
 
10
12
  var NOTE_COLORS = ["yellow", "blue", "green", "pink", "orange", "purple"];
11
13
 
14
+
12
15
  function getContainerBounds() {
13
16
  var c = document.getElementById("sticky-notes-container");
14
17
  if (!c || c.clientWidth === 0 || c.clientHeight === 0) return null;
@@ -52,6 +55,7 @@ export function initStickyNotes(_ctx) {
52
55
  var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
53
56
  if (toggleBtn) {
54
57
  toggleBtn.addEventListener("click", function () {
58
+ dismissOnboarding();
55
59
  if (!notesVisible && notes.size > 0) {
56
60
  // Hidden with existing notes → just show them
57
61
  showNotes();
@@ -69,6 +73,13 @@ export function initStickyNotes(_ctx) {
69
73
  });
70
74
  }
71
75
 
76
+ // Close format toolbar on outside click
77
+ document.addEventListener("mousedown", function (e) {
78
+ if (formatToolbarEl && !e.target.closest(".sn-format-toolbar") && !e.target.closest(".sticky-note-text") && !e.target.closest(".sticky-note-rendered")) {
79
+ closeFormatToolbar();
80
+ }
81
+ });
82
+
72
83
  // Re-clamp note positions on window resize so notes stay visible
73
84
  var resizeTimer;
74
85
  window.addEventListener("resize", function () {
@@ -79,6 +90,68 @@ export function initStickyNotes(_ctx) {
79
90
  }
80
91
  }, 100);
81
92
  });
93
+
94
+ // First-time onboarding beacon
95
+ maybeShowOnboarding();
96
+ }
97
+
98
+ // --- Onboarding beacon (one-time discovery hint) ---
99
+
100
+ var onboardingEl = null;
101
+ var ONBOARDING_KEY = "clay-sticky-notes-discovered";
102
+
103
+ function maybeShowOnboarding() {
104
+ try {
105
+ if (localStorage.getItem(ONBOARDING_KEY)) return;
106
+ } catch (e) { return; }
107
+
108
+ var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
109
+ if (!toggleBtn) return;
110
+
111
+ // Show beacon after a short delay so UI settles
112
+ setTimeout(function () {
113
+ // Don't show if user already has notes (they know the feature)
114
+ if (notes.size > 0) {
115
+ dismissOnboarding();
116
+ return;
117
+ }
118
+
119
+ toggleBtn.classList.add("sn-onboarding-pulse");
120
+
121
+ var tooltip = document.createElement("div");
122
+ tooltip.className = "sn-onboarding-tooltip";
123
+ tooltip.innerHTML = '<span>Click here to create a sticky note</span>';
124
+ document.body.appendChild(tooltip);
125
+ onboardingEl = tooltip;
126
+
127
+ // Position tooltip below the button
128
+ var rect = toggleBtn.getBoundingClientRect();
129
+ tooltip.style.left = (rect.left + rect.width / 2) + "px";
130
+ tooltip.style.top = (rect.bottom + 8) + "px";
131
+
132
+ // Auto-dismiss after 8 seconds
133
+ setTimeout(function () {
134
+ dismissOnboarding();
135
+ }, 8000);
136
+
137
+ // Dismiss on click anywhere
138
+ document.addEventListener("click", function onClickDismiss() {
139
+ dismissOnboarding();
140
+ document.removeEventListener("click", onClickDismiss);
141
+ }, { once: true });
142
+ }, 2000);
143
+ }
144
+
145
+ function dismissOnboarding() {
146
+ var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
147
+ if (toggleBtn) toggleBtn.classList.remove("sn-onboarding-pulse");
148
+ if (onboardingEl) {
149
+ onboardingEl.classList.add("sn-onboarding-fade-out");
150
+ var el = onboardingEl;
151
+ setTimeout(function () { el.remove(); }, 300);
152
+ onboardingEl = null;
153
+ }
154
+ try { localStorage.setItem(ONBOARDING_KEY, "1"); } catch (e) {}
82
155
  }
83
156
 
84
157
  // --- Visibility ---
@@ -190,6 +263,64 @@ function syncTitle(noteEl, text) {
190
263
  if (spacer) spacer.textContent = getTitle(text);
191
264
  }
192
265
 
266
+ // --- HTML-to-Markdown reverse conversion (for contenteditable) ---
267
+
268
+ function nodeToMd(node) {
269
+ if (node.nodeType === 3) return node.textContent;
270
+ if (node.nodeType !== 1) return "";
271
+
272
+ var tag = node.tagName;
273
+ var inner = childrenToMd(node);
274
+
275
+ switch (tag) {
276
+ case "STRONG": case "B": return "**" + inner + "**";
277
+ case "EM": case "I": return "*" + inner + "*";
278
+ case "DEL": case "S": case "STRIKE": return "~~" + inner + "~~";
279
+ case "CODE": return "`" + inner + "`";
280
+ case "BR": return "\n";
281
+ case "DIV":
282
+ if (node.classList.contains("sn-title")) return inner;
283
+ if (node.classList.contains("sn-placeholder")) return "";
284
+ // Browser-generated div from Enter key = new line
285
+ return "\n" + inner;
286
+ case "P": return "\n" + inner;
287
+ case "A": return node.getAttribute("href") || inner;
288
+ case "SPAN":
289
+ if (node.classList.contains("sn-check")) {
290
+ return node.classList.contains("checked") ? "- [x]" : "- [ ]";
291
+ }
292
+ if (node.classList.contains("sn-placeholder")) return "";
293
+ return inner;
294
+ default: return inner;
295
+ }
296
+ }
297
+
298
+ function childrenToMd(el) {
299
+ var result = "";
300
+ for (var i = 0; i < el.childNodes.length; i++) {
301
+ result += nodeToMd(el.childNodes[i]);
302
+ }
303
+ return result;
304
+ }
305
+
306
+ function extractMdFromRendered(rendered) {
307
+ var titleEl = rendered.querySelector(".sn-title");
308
+ if (titleEl) {
309
+ var titleMd = childrenToMd(titleEl);
310
+ var rest = "";
311
+ var afterTitle = false;
312
+ for (var i = 0; i < rendered.childNodes.length; i++) {
313
+ var child = rendered.childNodes[i];
314
+ if (child === titleEl) { afterTitle = true; continue; }
315
+ if (afterTitle) rest += nodeToMd(child);
316
+ }
317
+ if (rest && rest.charAt(0) === "\n") rest = rest.substring(1);
318
+ return titleMd + (rest ? "\n" + rest : "");
319
+ }
320
+ var md = childrenToMd(rendered);
321
+ return md.replace(/^\n+/, "");
322
+ }
323
+
193
324
  // --- Note rendering ---
194
325
 
195
326
  function renderNote(data) {
@@ -205,16 +336,17 @@ function renderNote(data) {
205
336
  el.dataset.color = data.color || "yellow";
206
337
 
207
338
  if (data.minimized) el.classList.add("minimized");
339
+ if (data.hidden) el.classList.add("hidden");
208
340
 
209
341
  // Header
210
342
  var header = document.createElement("div");
211
343
  header.className = "sticky-note-header";
212
344
 
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);
345
+ var closeBtn = document.createElement("button");
346
+ closeBtn.className = "sticky-note-btn sticky-note-close";
347
+ closeBtn.title = "Close";
348
+ closeBtn.innerHTML = iconHtml("x");
349
+ header.appendChild(closeBtn);
218
350
 
219
351
  var minBtn = document.createElement("button");
220
352
  minBtn.className = "sticky-note-btn sticky-note-min-btn";
@@ -243,31 +375,36 @@ function renderNote(data) {
243
375
  colorBtn.innerHTML = iconHtml("palette");
244
376
  header.appendChild(colorBtn);
245
377
 
378
+ var mdBtn = document.createElement("button");
379
+ mdBtn.className = "sticky-note-btn sticky-note-md-btn";
380
+ mdBtn.title = "Edit markdown";
381
+ mdBtn.innerHTML = "<span class='sn-md-label'>MD</span>";
382
+ header.appendChild(mdBtn);
383
+
246
384
  el.appendChild(header);
247
385
 
248
386
  // Body
249
387
  var body = document.createElement("div");
250
388
  body.className = "sticky-note-body";
251
389
 
390
+ // Hidden textarea as markdown data store
252
391
  var textarea = document.createElement("textarea");
253
392
  textarea.className = "sticky-note-text";
254
393
  textarea.value = data.text || "";
255
- textarea.placeholder = "Type a note...";
394
+ textarea.style.display = "none";
256
395
  body.appendChild(textarea);
257
396
 
397
+ // Contenteditable rendered view (primary editing surface)
258
398
  var rendered = document.createElement("div");
259
399
  rendered.className = "sticky-note-rendered";
260
- body.appendChild(rendered);
261
-
262
- // Show rendered view if there's content
263
- if (data.text) {
400
+ rendered.contentEditable = "true";
401
+ rendered.spellcheck = true;
402
+ if (data.text && data.text.trim()) {
264
403
  rendered.innerHTML = renderMiniMarkdown(data.text);
265
- textarea.style.display = "none";
266
- rendered.style.display = "";
267
404
  } else {
268
- textarea.style.display = "";
269
- rendered.style.display = "none";
405
+ rendered.classList.add("is-empty");
270
406
  }
407
+ body.appendChild(rendered);
271
408
 
272
409
  el.appendChild(body);
273
410
 
@@ -279,10 +416,10 @@ function renderNote(data) {
279
416
  // --- Event handlers ---
280
417
  setupDrag(el, spacer, data.id);
281
418
  setupResize(el, resizeHandle, data.id);
282
- setupTextEdit(textarea, rendered, data.id);
419
+ setupTextEdit(textarea, rendered, data.id, mdBtn);
283
420
  setupColorPicker(colorBtn, el, data.id);
284
421
  setupMinimize(minBtn, el, data.id);
285
- setupDelete(deleteBtn, data.id);
422
+ setupClose(closeBtn, el, data.id);
286
423
  setupBringToFront(el, data.id);
287
424
 
288
425
  refreshIcons();
@@ -449,50 +586,228 @@ function setupResize(noteEl, handle, noteId) {
449
586
  }
450
587
  }
451
588
 
452
- // --- Text edit ---
589
+ // --- Text edit (contenteditable) ---
590
+
591
+ // --- Format toolbar (WYSIWYG) ---
453
592
 
454
- function switchToEdit(textarea, rendered) {
455
- rendered.style.display = "none";
456
- textarea.style.display = "";
457
- textarea.focus();
593
+ var FORMAT_BUTTONS = [
594
+ { label: "B", title: "Bold", cls: "sn-fmt-bold", command: "bold" },
595
+ { label: "I", title: "Italic", cls: "sn-fmt-italic", command: "italic" },
596
+ { label: "S", title: "Strikethrough", cls: "sn-fmt-strike", command: "strikethrough" },
597
+ { label: "code-2", title: "Code", cls: "sn-fmt-code", command: "code", isIcon: true },
598
+ ];
599
+
600
+ function closeFormatToolbar() {
601
+ if (formatToolbarEl) {
602
+ formatToolbarEl.remove();
603
+ formatToolbarEl = null;
604
+ }
458
605
  }
459
606
 
460
- function setupTextEdit(textarea, rendered, noteId) {
461
- var noteEl = textarea.closest(".sticky-note");
462
- var spacer = noteEl.querySelector(".sticky-note-spacer");
607
+ function applyFormat(command, rendered) {
608
+ if (command === "code") {
609
+ var sel = window.getSelection();
610
+ if (!sel.rangeCount || sel.isCollapsed) return;
611
+ var range = sel.getRangeAt(0);
612
+ var ancestor = range.commonAncestorContainer;
613
+ var codeParent = (ancestor.nodeType === 3 ? ancestor.parentElement : ancestor);
614
+ if (codeParent && codeParent.closest && codeParent.closest("code")) {
615
+ // Unwrap: replace <code> with its text content
616
+ var codeEl = codeParent.closest("code");
617
+ var textNode = document.createTextNode(codeEl.textContent);
618
+ codeEl.parentNode.replaceChild(textNode, codeEl);
619
+ var newRange = document.createRange();
620
+ newRange.selectNodeContents(textNode);
621
+ sel.removeAllRanges();
622
+ sel.addRange(newRange);
623
+ } else {
624
+ // Wrap selection in <code>
625
+ var code = document.createElement("code");
626
+ try { range.surroundContents(code); } catch (e) {
627
+ var frag = range.extractContents();
628
+ code.appendChild(frag);
629
+ range.insertNode(code);
630
+ }
631
+ }
632
+ } else {
633
+ document.execCommand(command, false, null);
634
+ }
635
+ // Trigger sync
636
+ rendered.dispatchEvent(new Event("input", { bubbles: true }));
637
+ }
463
638
 
464
- textarea.addEventListener("input", function () {
465
- debouncedTextUpdate(noteId, textarea.value);
466
- syncTitle(noteEl, textarea.value);
467
- });
639
+ function showFormatToolbar(rendered) {
640
+ closeFormatToolbar();
468
641
 
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
- });
642
+ var sel = window.getSelection();
643
+ if (!sel || !sel.rangeCount || sel.isCollapsed) return;
644
+ if (!sel.toString().trim()) return;
645
+
646
+ var range = sel.getRangeAt(0);
647
+ if (!rendered.contains(range.commonAncestorContainer)) return;
648
+
649
+ var toolbar = document.createElement("div");
650
+ toolbar.className = "sn-format-toolbar";
651
+
652
+ for (var i = 0; i < FORMAT_BUTTONS.length; i++) {
653
+ (function (cfg) {
654
+ var btn = document.createElement("button");
655
+ btn.className = "sn-fmt-btn " + cfg.cls;
656
+ btn.title = cfg.title;
657
+ btn.innerHTML = cfg.isIcon ? iconHtml(cfg.label) : cfg.label;
658
+ btn.addEventListener("mousedown", function (e) {
659
+ e.preventDefault();
660
+ e.stopPropagation();
661
+ applyFormat(cfg.command, rendered);
662
+ setTimeout(function () {
663
+ var s = window.getSelection();
664
+ if (s && !s.isCollapsed) {
665
+ showFormatToolbar(rendered);
666
+ } else {
667
+ closeFormatToolbar();
668
+ }
669
+ }, 0);
670
+ });
671
+ toolbar.appendChild(btn);
672
+ })(FORMAT_BUTTONS[i]);
475
673
  }
476
674
 
477
- // Click rendered → switch to edit
478
- rendered.addEventListener("mousedown", function (e) {
675
+ refreshIcons();
676
+ document.body.appendChild(toolbar);
677
+ formatToolbarEl = toolbar;
678
+ positionToolbarAtRange(toolbar, range);
679
+ }
680
+
681
+ function positionToolbarAtRange(toolbar, range) {
682
+ var rect = range.getBoundingClientRect();
683
+ var toolbarX = rect.left + rect.width / 2;
684
+ var toolbarY = rect.top - 4;
685
+
686
+ toolbar.style.left = toolbarX + "px";
687
+ toolbar.style.top = toolbarY + "px";
688
+
689
+ requestAnimationFrame(function () {
690
+ var tw = toolbar.offsetWidth;
691
+ var th = toolbar.offsetHeight;
692
+ var x = Math.max(8, Math.min(toolbarX - tw / 2, window.innerWidth - tw - 8));
693
+ var y = Math.max(8, toolbarY - th);
694
+ toolbar.style.left = x + "px";
695
+ toolbar.style.top = y + "px";
696
+ });
697
+ }
698
+
699
+ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
700
+ var noteEl = textarea.closest(".sticky-note");
701
+ var mdMode = false;
702
+
703
+ // MD button: toggle between contenteditable rendered view and raw textarea
704
+ mdBtn.addEventListener("click", function (e) {
479
705
  e.stopPropagation();
706
+ mdMode = !mdMode;
707
+ if (mdMode) {
708
+ // Switch to raw markdown editing
709
+ var md = extractMdFromRendered(rendered);
710
+ textarea.value = md;
711
+ textarea.style.display = "";
712
+ rendered.style.display = "none";
713
+ mdBtn.classList.add("active");
714
+ textarea.focus();
715
+ } else {
716
+ // Switch back to contenteditable rendered view
717
+ var md = textarea.value;
718
+ debouncedTextUpdate(noteId, md);
719
+ syncTitle(noteEl, md);
720
+ if (md.trim()) {
721
+ rendered.innerHTML = renderMiniMarkdown(md);
722
+ rendered.classList.remove("is-empty");
723
+ } else {
724
+ rendered.innerHTML = "";
725
+ rendered.classList.add("is-empty");
726
+ }
727
+ textarea.style.display = "none";
728
+ rendered.style.display = "";
729
+ mdBtn.classList.remove("active");
730
+ rendered.focus();
731
+ }
732
+ });
733
+
734
+ // Sync contenteditable changes to textarea (data store) and save
735
+ rendered.addEventListener("input", function () {
736
+ var md = extractMdFromRendered(rendered);
737
+ textarea.value = md;
738
+ debouncedTextUpdate(noteId, md);
739
+ syncTitle(noteEl, md);
740
+ rendered.classList.toggle("is-empty", !md.trim());
480
741
  });
481
- rendered.addEventListener("click", function (e) {
742
+
743
+ // On blur, re-render to normalize HTML structure
744
+ rendered.addEventListener("blur", function (e) {
745
+ // Don't re-render if clicking format toolbar (it prevents default, but just in case)
746
+ if (e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(".sn-format-toolbar")) return;
747
+ closeFormatToolbar();
748
+ var md = extractMdFromRendered(rendered);
749
+ textarea.value = md;
750
+ if (md.trim()) {
751
+ rendered.innerHTML = renderMiniMarkdown(md);
752
+ rendered.classList.remove("is-empty");
753
+ } else {
754
+ rendered.innerHTML = "";
755
+ rendered.classList.add("is-empty");
756
+ }
757
+ });
758
+
759
+ // Show format toolbar when selecting text
760
+ rendered.addEventListener("mouseup", function (e) {
482
761
  if (e.target.tagName === "A") return;
483
- switchToEdit(textarea, rendered);
762
+ setTimeout(function () {
763
+ var sel = window.getSelection();
764
+ if (sel && !sel.isCollapsed && sel.toString().trim()) {
765
+ showFormatToolbar(rendered);
766
+ } else {
767
+ closeFormatToolbar();
768
+ }
769
+ }, 10);
484
770
  });
485
771
 
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 = "";
772
+ // Keyboard selection
773
+ rendered.addEventListener("keyup", function (e) {
774
+ if (e.shiftKey || e.key === "Shift") {
775
+ var sel = window.getSelection();
776
+ if (sel && !sel.isCollapsed) {
777
+ showFormatToolbar(rendered);
778
+ } else {
779
+ closeFormatToolbar();
780
+ }
781
+ }
782
+ });
783
+
784
+ // Insert <br> on Enter instead of <div>
785
+ rendered.addEventListener("keydown", function (e) {
786
+ if (e.key === "Enter" && !e.shiftKey) {
787
+ e.preventDefault();
788
+ document.execCommand("insertLineBreak");
492
789
  }
493
790
  });
494
791
 
495
- // Prevent drag when clicking textarea
792
+ // Paste as plain text to avoid importing HTML formatting
793
+ rendered.addEventListener("paste", function (e) {
794
+ e.preventDefault();
795
+ var text = (e.clipboardData || window.clipboardData).getData("text/plain");
796
+ document.execCommand("insertText", false, text);
797
+ });
798
+
799
+ // Prevent drag when clicking body
800
+ rendered.addEventListener("mousedown", function (e) {
801
+ e.stopPropagation();
802
+ });
803
+
804
+ // Sync textarea edits when in MD mode
805
+ textarea.addEventListener("input", function () {
806
+ if (!mdMode) return;
807
+ debouncedTextUpdate(noteId, textarea.value);
808
+ syncTitle(noteEl, textarea.value);
809
+ });
810
+
496
811
  textarea.addEventListener("mousedown", function (e) {
497
812
  e.stopPropagation();
498
813
  });
@@ -566,12 +881,13 @@ function setupMinimize(btn, noteEl, noteId) {
566
881
  });
567
882
  }
568
883
 
569
- // --- Delete ---
884
+ // --- Close (hide) note ---
570
885
 
571
- function setupDelete(btn, noteId) {
886
+ function setupClose(btn, noteEl, noteId) {
572
887
  btn.addEventListener("click", function (e) {
573
888
  e.stopPropagation();
574
- wsSend({ type: "note_delete", id: noteId });
889
+ noteEl.classList.add("hidden");
890
+ wsSend({ type: "note_update", id: noteId, hidden: true });
575
891
  });
576
892
  }
577
893
 
@@ -605,6 +921,10 @@ export function handleNotesList(msg) {
605
921
  }
606
922
 
607
923
  updateBadge();
924
+ updateSidebarBadge();
925
+
926
+ // If user already has notes, they know the feature — dismiss onboarding
927
+ if (list.length > 0) dismissOnboarding();
608
928
 
609
929
  // Auto-show if there are notes
610
930
  if (list.length > 0 && !notesVisible) {
@@ -626,6 +946,10 @@ export function handleNoteCreated(msg) {
626
946
  notes.set(msg.note.id, { data: msg.note, el: el });
627
947
  container.appendChild(el);
628
948
  updateBadge();
949
+ updateSidebarBadge();
950
+
951
+ // Re-render archive if open
952
+ if (archiveOpen) renderArchiveCards();
629
953
 
630
954
  // Show container if hidden
631
955
  if (!notesVisible) {
@@ -651,17 +975,28 @@ export function handleNoteUpdated(msg) {
651
975
  entry.el.style.zIndex = 100 + (msg.note.zIndex || 0);
652
976
  entry.el.dataset.color = msg.note.color || "yellow";
653
977
 
654
- // Update text only if not actively editing
978
+ // Update text only if not actively editing (check rendered or textarea focus)
655
979
  var textarea = entry.el.querySelector(".sticky-note-text");
656
980
  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) {
981
+ if (rendered && rendered !== document.activeElement && textarea !== document.activeElement) {
982
+ if (textarea) textarea.value = msg.note.text || "";
983
+ if (msg.note.text && msg.note.text.trim()) {
660
984
  rendered.innerHTML = renderMiniMarkdown(msg.note.text);
985
+ rendered.classList.remove("is-empty");
986
+ } else {
987
+ rendered.innerHTML = "";
988
+ rendered.classList.add("is-empty");
661
989
  }
662
990
  syncTitle(entry.el, msg.note.text);
663
991
  }
664
992
 
993
+ // Handle hidden state
994
+ if (msg.note.hidden) {
995
+ entry.el.classList.add("hidden");
996
+ } else {
997
+ entry.el.classList.remove("hidden");
998
+ }
999
+
665
1000
  var minBtn = entry.el.querySelector(".sticky-note-min-btn");
666
1001
  if (msg.note.minimized) {
667
1002
  entry.el.classList.add("minimized");
@@ -671,6 +1006,9 @@ export function handleNoteUpdated(msg) {
671
1006
  if (minBtn) { minBtn.innerHTML = iconHtml("minus"); minBtn.title = "Minimize"; }
672
1007
  }
673
1008
  refreshIcons();
1009
+
1010
+ // Re-render archive if open (text or color may have changed)
1011
+ if (archiveOpen) renderArchiveCards();
674
1012
  }
675
1013
 
676
1014
  export function handleNoteDeleted(msg) {
@@ -679,10 +1017,237 @@ export function handleNoteDeleted(msg) {
679
1017
  entry.el.remove();
680
1018
  notes.delete(msg.id);
681
1019
  updateBadge();
1020
+ updateSidebarBadge();
682
1021
 
683
1022
  // Clear debounce timers
684
1023
  clearTimeout(updateTimers[msg.id]);
685
1024
  clearTimeout(textTimers[msg.id]);
686
1025
  delete updateTimers[msg.id];
687
1026
  delete textTimers[msg.id];
1027
+
1028
+ // Re-render archive if open
1029
+ if (archiveOpen) renderArchiveCards();
1030
+ }
1031
+
1032
+ // --- Sidebar badge ---
1033
+
1034
+ function updateSidebarBadge() {
1035
+ var badge = document.getElementById("sticky-notes-sidebar-count");
1036
+ if (!badge) return;
1037
+ if (notes.size > 0) {
1038
+ badge.textContent = notes.size;
1039
+ badge.classList.remove("hidden");
1040
+ } else {
1041
+ badge.classList.add("hidden");
1042
+ }
1043
+ }
1044
+
1045
+ // --- Notes Archive View ---
1046
+
1047
+ function renderArchiveCards() {
1048
+ var grid = document.getElementById("notes-archive-grid");
1049
+ if (!grid) return;
1050
+
1051
+ grid.innerHTML = "";
1052
+
1053
+ if (notes.size === 0) {
1054
+ var empty = document.createElement("div");
1055
+ empty.className = "notes-archive-empty";
1056
+ empty.innerHTML = iconHtml("sticky-note") + "<p>No sticky notes yet</p><p class=\"notes-archive-empty-sub\">Create one with the " + iconHtml("sticky-note") + " button in the title bar</p>";
1057
+ grid.appendChild(empty);
1058
+ refreshIcons();
1059
+ return;
1060
+ }
1061
+
1062
+ // Sort by creation time (id is timestamp-based) — newest first
1063
+ var sorted = Array.from(notes.values()).sort(function (a, b) {
1064
+ return (b.data.id || "").localeCompare(a.data.id || "");
1065
+ });
1066
+
1067
+ for (var i = 0; i < sorted.length; i++) {
1068
+ (function (noteData) {
1069
+ var card = document.createElement("div");
1070
+ card.className = "notes-archive-card" + (noteData.data.hidden ? " archived" : "");
1071
+ card.dataset.color = noteData.data.color || "yellow";
1072
+
1073
+ // Header with title + delete
1074
+ var header = document.createElement("div");
1075
+ header.className = "notes-archive-card-header";
1076
+
1077
+ var title = document.createElement("div");
1078
+ title.className = "notes-archive-card-title";
1079
+ title.textContent = getTitle(noteData.data.text) || "Untitled";
1080
+ header.appendChild(title);
1081
+
1082
+ var deleteBtn = document.createElement("button");
1083
+ deleteBtn.className = "notes-archive-card-delete";
1084
+ deleteBtn.title = "Delete permanently";
1085
+ deleteBtn.innerHTML = iconHtml("trash-2");
1086
+ deleteBtn.addEventListener("click", function (e) {
1087
+ e.stopPropagation();
1088
+ // Confirm before permanent delete
1089
+ if (card.classList.contains("confirm-delete")) {
1090
+ wsSend({ type: "note_delete", id: noteData.data.id });
1091
+ card.classList.add("deleting");
1092
+ return;
1093
+ }
1094
+ card.classList.add("confirm-delete");
1095
+ deleteBtn.title = "Click again to confirm";
1096
+ setTimeout(function () {
1097
+ card.classList.remove("confirm-delete");
1098
+ deleteBtn.title = "Delete permanently";
1099
+ }, 2000);
1100
+ });
1101
+ // Restore button (only for archived/hidden notes)
1102
+ if (noteData.data.hidden) {
1103
+ var restoreBtn = document.createElement("button");
1104
+ restoreBtn.className = "notes-archive-card-restore";
1105
+ restoreBtn.title = "Restore to canvas";
1106
+ restoreBtn.innerHTML = iconHtml("rotate-ccw") + "<span>Restore</span>";
1107
+ restoreBtn.addEventListener("click", function (e) {
1108
+ e.stopPropagation();
1109
+ wsSend({ type: "note_update", id: noteData.data.id, hidden: false });
1110
+ noteData.el.classList.remove("hidden");
1111
+ noteData.data.hidden = false;
1112
+ renderArchiveCards();
1113
+ });
1114
+ header.appendChild(restoreBtn);
1115
+ }
1116
+
1117
+ header.appendChild(deleteBtn);
1118
+ card.appendChild(header);
1119
+
1120
+ // Body (rendered markdown)
1121
+ var body = document.createElement("div");
1122
+ body.className = "notes-archive-card-body";
1123
+ var bodyLines = (noteData.data.text || "").split("\n").slice(1).join("\n").trim();
1124
+ if (bodyLines) {
1125
+ body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
1126
+ }
1127
+ card.appendChild(body);
1128
+
1129
+ // Color strip at bottom
1130
+ var colorStrip = document.createElement("div");
1131
+ colorStrip.className = "notes-archive-card-color";
1132
+ card.appendChild(colorStrip);
1133
+
1134
+ // Click card → close archive, jump to note on canvas
1135
+ card.addEventListener("click", function () {
1136
+ closeArchive();
1137
+ showNotes();
1138
+ // Un-hide if needed
1139
+ if (noteData.data.hidden) {
1140
+ wsSend({ type: "note_update", id: noteData.data.id, hidden: false });
1141
+ noteData.el.classList.remove("hidden");
1142
+ }
1143
+ // Bring note to front
1144
+ wsSend({ type: "note_bring_front", id: noteData.data.id });
1145
+ // Un-minimize if needed
1146
+ if (noteData.data.minimized) {
1147
+ wsSend({ type: "note_update", id: noteData.data.id, minimized: false });
1148
+ }
1149
+ // Flash the note
1150
+ var noteEl = noteData.el;
1151
+ if (noteEl) {
1152
+ noteEl.classList.add("note-flash");
1153
+ setTimeout(function () { noteEl.classList.remove("note-flash"); }, 600);
1154
+ }
1155
+ });
1156
+
1157
+ grid.appendChild(card);
1158
+ })(sorted[i]);
1159
+ }
1160
+
1161
+ refreshIcons();
1162
+ }
1163
+
1164
+ export function openArchive() {
1165
+ if (archiveOpen) return;
1166
+ archiveOpen = true;
1167
+
1168
+ var messagesEl = document.getElementById("messages");
1169
+ var appEl = document.getElementById("app");
1170
+ var inputArea = document.getElementById("input-area");
1171
+ var titleBar = document.querySelector("#main-column > .title-bar-content");
1172
+ var notesContainer = document.getElementById("sticky-notes-container");
1173
+
1174
+ // Hide messages, input area, session title bar, and floating notes
1175
+ if (messagesEl) messagesEl.classList.add("hidden");
1176
+ if (inputArea) inputArea.classList.add("hidden");
1177
+ if (titleBar) titleBar.classList.add("hidden");
1178
+ if (notesContainer) notesContainer.classList.add("hidden");
1179
+
1180
+ // Create or show archive container
1181
+ var archive = document.getElementById("notes-archive");
1182
+ if (!archive) {
1183
+ archive = document.createElement("div");
1184
+ archive.id = "notes-archive";
1185
+
1186
+ var header = document.createElement("div");
1187
+ header.className = "notes-archive-header";
1188
+
1189
+ var titleWrap = document.createElement("div");
1190
+ titleWrap.className = "notes-archive-title-wrap";
1191
+ titleWrap.innerHTML = iconHtml("sticky-note") + "<h2>Sticky Notes</h2><span class=\"notes-archive-count\"></span>";
1192
+ header.appendChild(titleWrap);
1193
+
1194
+ var closeBtn = document.createElement("button");
1195
+ closeBtn.className = "notes-archive-close";
1196
+ closeBtn.title = "Back to chat";
1197
+ closeBtn.innerHTML = iconHtml("x");
1198
+ closeBtn.addEventListener("click", function () {
1199
+ closeArchive();
1200
+ });
1201
+ header.appendChild(closeBtn);
1202
+
1203
+ archive.appendChild(header);
1204
+
1205
+ var grid = document.createElement("div");
1206
+ grid.id = "notes-archive-grid";
1207
+ grid.className = "notes-archive-grid";
1208
+ archive.appendChild(grid);
1209
+
1210
+ if (appEl) appEl.appendChild(archive);
1211
+ }
1212
+
1213
+ archive.classList.remove("hidden");
1214
+
1215
+ // Update count
1216
+ var countEl = archive.querySelector(".notes-archive-count");
1217
+ if (countEl) countEl.textContent = notes.size + " note" + (notes.size !== 1 ? "s" : "");
1218
+
1219
+ renderArchiveCards();
1220
+
1221
+ // Mark sidebar button active
1222
+ var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
1223
+ if (sidebarBtn) sidebarBtn.classList.add("active");
1224
+
1225
+ refreshIcons();
1226
+ }
1227
+
1228
+ export function closeArchive() {
1229
+ if (!archiveOpen) return;
1230
+ archiveOpen = false;
1231
+
1232
+ var archive = document.getElementById("notes-archive");
1233
+ var messagesEl = document.getElementById("messages");
1234
+ var inputArea = document.getElementById("input-area");
1235
+ var titleBar = document.querySelector("#main-column > .title-bar-content");
1236
+ var notesContainer = document.getElementById("sticky-notes-container");
1237
+
1238
+ if (archive) archive.classList.add("hidden");
1239
+ if (messagesEl) messagesEl.classList.remove("hidden");
1240
+ if (inputArea) inputArea.classList.remove("hidden");
1241
+ if (titleBar) titleBar.classList.remove("hidden");
1242
+
1243
+ // Restore floating notes if they were visible before
1244
+ if (notesContainer && notesVisible) notesContainer.classList.remove("hidden");
1245
+
1246
+ // Un-mark sidebar button
1247
+ var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
1248
+ if (sidebarBtn) sidebarBtn.classList.remove("active");
1249
+ }
1250
+
1251
+ export function isArchiveOpen() {
1252
+ return archiveOpen;
688
1253
  }