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.
- package/bin/cli.js +53 -4
- package/lib/config.js +15 -6
- package/lib/daemon.js +47 -5
- package/lib/ipc.js +12 -0
- package/lib/notes.js +2 -2
- package/lib/project.js +883 -2
- package/lib/public/app.js +862 -14
- package/lib/public/css/diff.css +12 -0
- package/lib/public/css/filebrowser.css +1 -1
- package/lib/public/css/loop.css +841 -0
- package/lib/public/css/menus.css +5 -0
- package/lib/public/css/mobile-nav.css +15 -15
- package/lib/public/css/rewind.css +23 -0
- package/lib/public/css/scheduler-modal.css +546 -0
- package/lib/public/css/scheduler.css +944 -0
- package/lib/public/css/sidebar.css +1 -0
- package/lib/public/css/skills.css +59 -0
- package/lib/public/css/sticky-notes.css +486 -0
- package/lib/public/css/title-bar.css +83 -3
- package/lib/public/index.html +181 -3
- package/lib/public/modules/diff.js +3 -3
- package/lib/public/modules/filebrowser.js +169 -45
- package/lib/public/modules/input.js +17 -3
- package/lib/public/modules/markdown.js +10 -0
- package/lib/public/modules/qrcode.js +23 -26
- package/lib/public/modules/scheduler.js +1240 -0
- package/lib/public/modules/server-settings.js +40 -0
- package/lib/public/modules/sidebar.js +12 -0
- package/lib/public/modules/skills.js +84 -0
- package/lib/public/modules/sticky-notes.js +617 -52
- package/lib/public/modules/theme.js +9 -19
- package/lib/public/modules/tools.js +16 -2
- package/lib/public/style.css +3 -0
- package/lib/scheduler.js +362 -0
- package/lib/sdk-bridge.js +36 -0
- package/lib/sessions.js +9 -5
- package/lib/utils.js +49 -3
- 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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
header.appendChild(
|
|
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.
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
syncTitle(noteEl, textarea.value);
|
|
467
|
-
});
|
|
639
|
+
function showFormatToolbar(rendered) {
|
|
640
|
+
closeFormatToolbar();
|
|
468
641
|
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
//
|
|
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
|
-
// ---
|
|
884
|
+
// --- Close (hide) note ---
|
|
570
885
|
|
|
571
|
-
function
|
|
886
|
+
function setupClose(btn, noteEl, noteId) {
|
|
572
887
|
btn.addEventListener("click", function (e) {
|
|
573
888
|
e.stopPropagation();
|
|
574
|
-
|
|
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 (
|
|
658
|
-
textarea.value = msg.note.text || "";
|
|
659
|
-
if (
|
|
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
|
}
|