clay-server 2.14.0-beta.9 → 2.14.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.
@@ -1,59 +1,89 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { hideNotes } from './sticky-notes.js';
3
+ import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
3
4
 
4
5
  var getMateWs = null;
5
6
  var containerEl = null;
6
- var gridEl = null;
7
- var closeBtn = null;
7
+ var filesEl = null;
8
8
  var sidebarBtn = null;
9
9
  var countBadge = null;
10
10
  var visible = false;
11
11
  var cachedFiles = [];
12
12
 
13
+ // Sidebar panels
14
+ var conversationsPanel = null;
15
+ var knowledgePanel = null;
16
+ var knowledgeBackBtn = null;
17
+ var knowledgeAddSidebarBtn = null;
18
+
13
19
  // Editor elements
14
- var editorEl = null;
20
+ var activeNameEl = null;
15
21
  var editorNameEl = null;
16
22
  var editorContentEl = null;
17
23
  var editorSaveBtn = null;
18
24
  var editorDeleteBtn = null;
19
- var editorCloseBtn = null;
20
- var editorBackdrop = null;
25
+ var editorPreviewEl = null;
26
+ var editorHighlightEl = null;
27
+ var editorHighlightPre = null;
28
+ var previewTimer = null;
29
+ var editorExtEl = null;
30
+ var nameGroupEl = null;
21
31
  var editingFile = null;
32
+ var dirty = false;
22
33
 
23
34
  export function initMateKnowledge(mateWsGetter) {
24
35
  getMateWs = mateWsGetter;
25
36
  containerEl = document.getElementById("mate-knowledge-container");
26
- gridEl = document.getElementById("mate-knowledge-grid");
27
- closeBtn = document.getElementById("mate-knowledge-close-btn");
37
+ filesEl = document.getElementById("mate-knowledge-files");
28
38
  sidebarBtn = document.getElementById("mate-knowledge-btn");
29
39
  countBadge = document.getElementById("mate-knowledge-count");
30
40
 
31
- editorEl = document.getElementById("mate-knowledge-editor");
41
+ // Sidebar panels
42
+ conversationsPanel = document.getElementById("mate-sidebar-conversations");
43
+ knowledgePanel = document.getElementById("mate-sidebar-knowledge");
44
+ knowledgeBackBtn = document.getElementById("mate-knowledge-back-btn");
45
+ knowledgeAddSidebarBtn = document.getElementById("mate-knowledge-add-sidebar-btn");
46
+
47
+ // Editor
48
+ activeNameEl = document.getElementById("mate-knowledge-active-name");
32
49
  editorNameEl = document.getElementById("mate-knowledge-editor-name");
33
50
  editorContentEl = document.getElementById("mate-knowledge-editor-content");
34
51
  editorSaveBtn = document.getElementById("mate-knowledge-editor-save");
35
52
  editorDeleteBtn = document.getElementById("mate-knowledge-editor-delete");
36
- editorCloseBtn = document.getElementById("mate-knowledge-editor-close");
37
- editorBackdrop = editorEl ? editorEl.querySelector(".mate-knowledge-editor-backdrop") : null;
53
+ editorPreviewEl = document.getElementById("mate-knowledge-editor-preview");
54
+ editorHighlightEl = document.getElementById("mate-knowledge-editor-highlight");
55
+ editorHighlightPre = editorHighlightEl ? editorHighlightEl.parentElement : null;
56
+ editorExtEl = document.getElementById("mate-knowledge-editor-ext");
57
+ nameGroupEl = document.getElementById("mate-knowledge-name-group");
38
58
 
39
59
  if (sidebarBtn) {
40
60
  sidebarBtn.addEventListener("click", function () {
41
- if (visible) {
42
- hideKnowledge();
43
- } else {
44
- showKnowledge();
45
- }
61
+ if (visible) { hideKnowledge(); } else { showKnowledge(); }
46
62
  });
47
63
  }
48
64
 
65
+ if (knowledgeBackBtn) {
66
+ knowledgeBackBtn.addEventListener("click", hideKnowledge);
67
+ }
68
+
69
+ var closeBtn = document.getElementById("mate-knowledge-close-btn");
49
70
  if (closeBtn) {
50
- closeBtn.addEventListener("click", hideKnowledge);
71
+ closeBtn.addEventListener("click", function () {
72
+ // Close editor, keep sidebar file list open
73
+ if (containerEl) containerEl.classList.add("hidden");
74
+ editingFile = null;
75
+ renderFileList();
76
+ });
51
77
  }
52
78
 
53
- if (editorSaveBtn) {
54
- editorSaveBtn.addEventListener("click", saveKnowledge);
79
+ if (knowledgeAddSidebarBtn) {
80
+ knowledgeAddSidebarBtn.addEventListener("click", function () {
81
+ selectFile(null, "");
82
+ });
55
83
  }
56
84
 
85
+ if (editorSaveBtn) editorSaveBtn.addEventListener("click", saveKnowledge);
86
+
57
87
  if (editorDeleteBtn) {
58
88
  editorDeleteBtn.addEventListener("click", function () {
59
89
  if (editingFile) {
@@ -61,45 +91,74 @@ export function initMateKnowledge(mateWsGetter) {
61
91
  if (ws && ws.readyState === 1) {
62
92
  ws.send(JSON.stringify({ type: "knowledge_delete", name: editingFile }));
63
93
  }
64
- closeEditor();
94
+ selectFile(null, "");
65
95
  }
66
96
  });
67
97
  }
68
98
 
69
- if (editorCloseBtn) {
70
- editorCloseBtn.addEventListener("click", closeEditor);
71
- }
72
-
73
- if (editorBackdrop) {
74
- editorBackdrop.addEventListener("click", closeEditor);
75
- }
76
-
77
99
  // Stop keyboard events from leaking
78
- var stopProp = function (e) { e.stopPropagation(); };
100
+ var stopProp = function (e) {
101
+ e.stopPropagation();
102
+ };
103
+ var editorKeydown = function (e) {
104
+ e.stopPropagation();
105
+ // Keep Cmd+Z / Cmd+Shift+Z inside the textarea only
106
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
107
+ e.stopImmediatePropagation();
108
+ }
109
+ };
79
110
  if (editorNameEl) {
80
111
  editorNameEl.addEventListener("keydown", stopProp);
81
112
  editorNameEl.addEventListener("keyup", stopProp);
82
113
  editorNameEl.addEventListener("keypress", stopProp);
83
114
  }
84
115
  if (editorContentEl) {
85
- editorContentEl.addEventListener("keydown", stopProp);
116
+ editorContentEl.addEventListener("keydown", editorKeydown);
86
117
  editorContentEl.addEventListener("keyup", stopProp);
87
118
  editorContentEl.addEventListener("keypress", stopProp);
119
+ editorContentEl.addEventListener("input", function () {
120
+ dirty = true;
121
+ if (editorSaveBtn) editorSaveBtn.disabled = false;
122
+ updateHighlight();
123
+ if (previewTimer) clearTimeout(previewTimer);
124
+ previewTimer = setTimeout(updatePreview, 150);
125
+ });
126
+ editorContentEl.addEventListener("scroll", syncHighlightScroll);
127
+ initFormatPopover(editorContentEl);
88
128
  }
89
129
  }
90
130
 
91
131
  export function showKnowledge() {
92
132
  visible = true;
93
133
  hideNotes();
94
- if (containerEl) containerEl.classList.remove("hidden");
134
+
135
+ // Toggle sidebar panels: hide conversations, show knowledge file list
136
+ if (conversationsPanel) conversationsPanel.classList.add("hidden");
137
+ if (knowledgePanel) knowledgePanel.classList.remove("hidden");
95
138
  if (sidebarBtn) sidebarBtn.classList.add("active");
139
+
140
+ // Don't show editor yet, only when a file is selected
96
141
  requestKnowledgeList();
97
142
  }
98
143
 
99
144
  export function hideKnowledge() {
100
145
  visible = false;
101
- if (containerEl) containerEl.classList.add("hidden");
146
+
147
+ // Toggle sidebar panels: show conversations, hide knowledge file list
148
+ if (conversationsPanel) conversationsPanel.classList.remove("hidden");
149
+ if (knowledgePanel) knowledgePanel.classList.add("hidden");
102
150
  if (sidebarBtn) sidebarBtn.classList.remove("active");
151
+
152
+ // Hide editor/preview and reset state
153
+ if (containerEl) containerEl.classList.add("hidden");
154
+ editingFile = null;
155
+ cachedFiles = [];
156
+
157
+ // Reset badge
158
+ if (countBadge) {
159
+ countBadge.textContent = "";
160
+ countBadge.classList.add("hidden");
161
+ }
103
162
  }
104
163
 
105
164
  export function isKnowledgeVisible() {
@@ -116,7 +175,7 @@ export function requestKnowledgeList() {
116
175
  export function renderKnowledgeList(files) {
117
176
  cachedFiles = files || [];
118
177
 
119
- // Update badges
178
+ // Update badge
120
179
  if (countBadge) {
121
180
  if (cachedFiles.length > 0) {
122
181
  countBadge.textContent = String(cachedFiles.length);
@@ -125,73 +184,169 @@ export function renderKnowledgeList(files) {
125
184
  countBadge.classList.add("hidden");
126
185
  }
127
186
  }
128
- var headerCount = document.getElementById("mate-knowledge-header-count");
129
- if (headerCount) {
130
- headerCount.textContent = cachedFiles.length > 0 ? cachedFiles.length + " files" : "";
131
- }
132
187
 
133
- if (!gridEl) return;
134
- gridEl.innerHTML = "";
188
+ renderFileList();
189
+ }
190
+
191
+ function renderFileList() {
192
+ if (!filesEl) return;
193
+ filesEl.innerHTML = "";
135
194
 
136
- // Add tile (always first)
137
- var addTile = document.createElement("div");
138
- addTile.className = "mate-knowledge-tile mate-knowledge-tile-add";
139
- addTile.innerHTML = '<span class="mate-knowledge-add-icon">' + iconHtml("plus") + '</span><span class="mate-knowledge-add-label">Add Knowledge</span>';
140
- addTile.addEventListener("click", function () { openEditor(null); });
141
- gridEl.appendChild(addTile);
195
+ if (cachedFiles.length === 0) {
196
+ var empty = document.createElement("div");
197
+ empty.className = "mate-knowledge-empty";
198
+ empty.textContent = "No knowledge files yet";
199
+ filesEl.appendChild(empty);
200
+ }
142
201
 
143
202
  for (var i = 0; i < cachedFiles.length; i++) {
144
- gridEl.appendChild(renderTile(cachedFiles[i]));
203
+ filesEl.appendChild(renderFileItem(cachedFiles[i]));
145
204
  }
146
205
  refreshIcons();
147
206
  }
148
207
 
149
- function renderTile(file) {
150
- var tile = document.createElement("div");
151
- tile.className = "mate-knowledge-tile";
208
+ function renderFileItem(file) {
209
+ var item = document.createElement("div");
210
+ item.className = "mate-knowledge-file-item";
211
+ if (editingFile === file.name) item.classList.add("active");
152
212
 
153
- var title = document.createElement("div");
154
- title.className = "mate-knowledge-tile-title";
155
- title.textContent = file.name.replace(/\.md$/, "");
156
- tile.appendChild(title);
213
+ var icon = document.createElement("span");
214
+ icon.className = "mate-knowledge-file-icon";
215
+ icon.innerHTML = iconHtml(file.name.endsWith(".jsonl") ? "database" : "file-text");
216
+ item.appendChild(icon);
157
217
 
158
- var preview = document.createElement("div");
159
- preview.className = "mate-knowledge-tile-preview";
160
- // Preview will be populated if we have content cached; for now show size
161
- var sizeKb = file.size > 1024 ? (file.size / 1024).toFixed(1) + " KB" : file.size + " bytes";
162
- preview.textContent = sizeKb;
163
- tile.appendChild(preview);
218
+ var name = document.createElement("span");
219
+ name.className = "mate-knowledge-file-name";
220
+ var isJsonl = file.name.endsWith(".jsonl");
221
+ name.textContent = file.name.replace(/\.(md|jsonl)$/, "");
222
+ item.appendChild(name);
164
223
 
165
- tile.addEventListener("click", (function (name) {
224
+ if (isJsonl) {
225
+ var badge = document.createElement("span");
226
+ badge.className = "mate-knowledge-file-badge";
227
+ badge.textContent = "data";
228
+ item.appendChild(badge);
229
+ }
230
+
231
+ item.addEventListener("click", (function (fname) {
166
232
  return function () {
167
233
  var ws = getMateWs ? getMateWs() : null;
168
234
  if (ws && ws.readyState === 1) {
169
- ws.send(JSON.stringify({ type: "knowledge_read", name: name }));
235
+ ws.send(JSON.stringify({ type: "knowledge_read", name: fname }));
170
236
  }
171
237
  };
172
238
  })(file.name));
173
239
 
174
- return tile;
240
+ return item;
175
241
  }
176
242
 
177
243
  export function handleKnowledgeContent(msg) {
178
- openEditor(msg.name, msg.content || "");
244
+ if (msg.name && msg.name.endsWith(".jsonl")) {
245
+ selectJsonlFile(msg.name, msg.content || "");
246
+ } else {
247
+ selectFile(msg.name, msg.content || "");
248
+ }
179
249
  }
180
250
 
181
- function openEditor(fileName, content) {
182
- if (!editorEl) return;
251
+ function selectJsonlFile(fileName, content) {
252
+ editingFile = fileName;
253
+ dirty = false;
254
+
255
+ if (containerEl) containerEl.classList.remove("hidden");
256
+
257
+ // Show name, hide name input group
258
+ if (activeNameEl) { activeNameEl.textContent = fileName.replace(/\.jsonl$/, ""); activeNameEl.classList.remove("hidden"); }
259
+ if (nameGroupEl) nameGroupEl.classList.add("hidden");
260
+
261
+ // Hide editor pane and controls
262
+ if (editorContentEl) editorContentEl.value = "";
263
+ if (editorSaveBtn) editorSaveBtn.style.display = "none";
264
+ if (editorDeleteBtn) editorDeleteBtn.style.display = "";
265
+ if (editorHighlightPre) editorHighlightPre.style.display = "none";
266
+ if (editorContentEl) editorContentEl.style.display = "none";
267
+
268
+ // Build table from JSONL
269
+ if (editorPreviewEl) {
270
+ var lines = content.trim().split("\n").filter(function (l) { return l.trim(); });
271
+ if (lines.length === 0) {
272
+ editorPreviewEl.innerHTML = "<p style=\"opacity:0.5\">No data entries yet</p>";
273
+ } else {
274
+ var rows = [];
275
+ var allKeys = [];
276
+ var keySet = {};
277
+ for (var i = 0; i < lines.length; i++) {
278
+ try {
279
+ var obj = JSON.parse(lines[i]);
280
+ rows.push(obj);
281
+ var keys = Object.keys(obj);
282
+ for (var k = 0; k < keys.length; k++) {
283
+ if (!keySet[keys[k]]) { keySet[keys[k]] = true; allKeys.push(keys[k]); }
284
+ }
285
+ } catch (e) { /* skip malformed lines */ }
286
+ }
287
+ var html = "<table class=\"mate-knowledge-jsonl-table\"><thead><tr>";
288
+ for (var c = 0; c < allKeys.length; c++) {
289
+ html += "<th>" + escapeHtml(allKeys[c]) + "</th>";
290
+ }
291
+ html += "</tr></thead><tbody>";
292
+ for (var r = 0; r < rows.length; r++) {
293
+ html += "<tr>";
294
+ for (var c = 0; c < allKeys.length; c++) {
295
+ var val = rows[r][allKeys[c]];
296
+ var cell = val === undefined ? "" : typeof val === "object" ? JSON.stringify(val) : String(val);
297
+ html += "<td>" + escapeHtml(cell) + "</td>";
298
+ }
299
+ html += "</tr>";
300
+ }
301
+ html += "</tbody></table>";
302
+ editorPreviewEl.innerHTML = html;
303
+ }
304
+ }
305
+
306
+ renderFileList();
307
+ }
308
+
309
+ function escapeHtml(str) {
310
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
311
+ }
312
+
313
+ function selectFile(fileName, content) {
183
314
  editingFile = fileName || null;
184
- if (editorNameEl) {
185
- editorNameEl.value = fileName ? fileName.replace(/\.md$/, "") : "";
186
- editorNameEl.readOnly = !!fileName;
315
+ dirty = false;
316
+
317
+ // Restore editor pane (may have been hidden by JSONL viewer)
318
+ if (editorContentEl) editorContentEl.style.display = "";
319
+ if (editorHighlightPre) editorHighlightPre.style.display = "";
320
+ if (editorSaveBtn) editorSaveBtn.style.display = "";
321
+
322
+ // Show editor when a file is selected or new file created
323
+ if (containerEl) containerEl.classList.remove("hidden");
324
+
325
+ // Update active name / name input group
326
+ if (fileName) {
327
+ if (activeNameEl) { activeNameEl.textContent = fileName.replace(/\.md$/, ""); activeNameEl.classList.remove("hidden"); }
328
+ if (nameGroupEl) nameGroupEl.classList.add("hidden");
329
+ } else {
330
+ if (activeNameEl) activeNameEl.classList.add("hidden");
331
+ if (nameGroupEl) nameGroupEl.classList.remove("hidden");
332
+ if (editorNameEl) editorNameEl.value = "";
187
333
  }
334
+
188
335
  if (editorContentEl) {
189
336
  editorContentEl.value = content || "";
337
+ editorContentEl.placeholder = fileName ? "" : "Start writing...";
190
338
  }
191
339
  if (editorDeleteBtn) {
192
340
  editorDeleteBtn.style.display = fileName ? "" : "none";
193
341
  }
194
- editorEl.classList.remove("hidden");
342
+ if (editorSaveBtn) {
343
+ editorSaveBtn.disabled = true;
344
+ }
345
+
346
+ updateHighlight();
347
+ updatePreview();
348
+ renderFileList();
349
+
195
350
  if (!fileName && editorNameEl) {
196
351
  editorNameEl.focus();
197
352
  } else if (editorContentEl) {
@@ -199,24 +354,184 @@ function openEditor(fileName, content) {
199
354
  }
200
355
  }
201
356
 
202
- function closeEditor() {
203
- if (editorEl) editorEl.classList.add("hidden");
204
- editingFile = null;
357
+ function updateHighlight() {
358
+ if (!editorHighlightEl || !editorContentEl) return;
359
+ var text = editorContentEl.value + "\n";
360
+ // Reset completely: hljs skips elements it already processed
361
+ editorHighlightEl.className = "language-markdown";
362
+ editorHighlightEl.removeAttribute("data-highlighted");
363
+ editorHighlightEl.textContent = text;
364
+ if (window.hljs) {
365
+ window.hljs.highlightElement(editorHighlightEl);
366
+ }
367
+ }
368
+
369
+ function syncHighlightScroll() {
370
+ if (!editorHighlightPre || !editorContentEl) return;
371
+ editorHighlightPre.scrollTop = editorContentEl.scrollTop;
372
+ editorHighlightPre.scrollLeft = editorContentEl.scrollLeft;
373
+ }
374
+
375
+ function updatePreview() {
376
+ if (!editorPreviewEl || !editorContentEl) return;
377
+ var text = editorContentEl.value;
378
+ if (!text.trim()) {
379
+ editorPreviewEl.innerHTML = "";
380
+ return;
381
+ }
382
+ editorPreviewEl.innerHTML = renderMarkdown(text);
383
+ highlightCodeBlocks(editorPreviewEl);
205
384
  }
206
385
 
207
386
  function saveKnowledge() {
208
387
  if (!editorNameEl || !editorContentEl) return;
209
- var name = editorNameEl.value.trim();
388
+ var name = (editingFile || editorNameEl.value.trim().replace(/\.md$/i, "") + ".md");
210
389
  var content = editorContentEl.value;
211
- if (!name) {
390
+ if (!name || name === ".md") {
212
391
  editorNameEl.style.outline = "2px solid var(--error, #ff5555)";
213
392
  setTimeout(function () { editorNameEl.style.outline = ""; }, 1500);
214
393
  return;
215
394
  }
216
- if (!name.endsWith(".md")) name += ".md";
217
395
  var ws = getMateWs ? getMateWs() : null;
218
396
  if (ws && ws.readyState === 1) {
219
397
  ws.send(JSON.stringify({ type: "knowledge_save", name: name, content: content }));
220
398
  }
221
- closeEditor();
399
+ editingFile = name;
400
+ dirty = false;
401
+ if (editorSaveBtn) editorSaveBtn.disabled = true;
402
+ if (activeNameEl) { activeNameEl.textContent = name.replace(/\.md$/, ""); activeNameEl.classList.remove("hidden"); }
403
+ if (nameGroupEl) nameGroupEl.classList.add("hidden");
404
+ if (editorDeleteBtn) editorDeleteBtn.style.display = "";
405
+ }
406
+
407
+ // --- Format Popover ---
408
+
409
+ var formatPopover = null;
410
+ var popoverHideTimer = null;
411
+
412
+ var FORMAT_ACTIONS = [
413
+ { icon: "bold", label: "Bold", wrap: ["**", "**"] },
414
+ { icon: "italic", label: "Italic", wrap: ["*", "*"] },
415
+ { icon: "strikethrough", label: "Strikethrough", wrap: ["~~", "~~"] },
416
+ { icon: "code", label: "Code", wrap: ["`", "`"] },
417
+ { icon: "link", label: "Link", wrap: ["[", "](url)"] },
418
+ { icon: "heading-1", label: "Heading", prefix: "# " },
419
+ { icon: "list", label: "List", prefix: "- " },
420
+ { icon: "quote", label: "Quote", prefix: "> " },
421
+ ];
422
+
423
+ function initFormatPopover(textarea) {
424
+ // Create popover element
425
+ formatPopover = document.createElement("div");
426
+ formatPopover.className = "mate-format-popover";
427
+ formatPopover.style.display = "none";
428
+
429
+ for (var i = 0; i < FORMAT_ACTIONS.length; i++) {
430
+ var action = FORMAT_ACTIONS[i];
431
+ var btn = document.createElement("button");
432
+ btn.className = "mate-format-btn";
433
+ btn.title = action.label;
434
+ btn.innerHTML = iconHtml(action.icon);
435
+ btn.dataset.index = String(i);
436
+ btn.addEventListener("mousedown", function (e) {
437
+ e.preventDefault(); // prevent textarea blur
438
+ var idx = parseInt(this.dataset.index);
439
+ applyFormat(textarea, FORMAT_ACTIONS[idx]);
440
+ });
441
+ formatPopover.appendChild(btn);
442
+ }
443
+
444
+ document.body.appendChild(formatPopover);
445
+ refreshIcons(formatPopover);
446
+
447
+ textarea.addEventListener("mouseup", function () {
448
+ setTimeout(function () { checkSelection(textarea); }, 10);
449
+ });
450
+ textarea.addEventListener("keyup", function (e) {
451
+ if (e.shiftKey || e.key === "Shift") {
452
+ checkSelection(textarea);
453
+ }
454
+ });
455
+
456
+ textarea.addEventListener("blur", function () {
457
+ popoverHideTimer = setTimeout(hidePopover, 150);
458
+ });
459
+ textarea.addEventListener("focus", function () {
460
+ if (popoverHideTimer) { clearTimeout(popoverHideTimer); popoverHideTimer = null; }
461
+ });
462
+
463
+ document.addEventListener("scroll", hidePopover, true);
464
+ }
465
+
466
+ function checkSelection(textarea) {
467
+ var start = textarea.selectionStart;
468
+ var end = textarea.selectionEnd;
469
+ if (start === end || !formatPopover) {
470
+ hidePopover();
471
+ return;
472
+ }
473
+ showPopover(textarea);
474
+ }
475
+
476
+ function showPopover(textarea) {
477
+ if (!formatPopover) return;
478
+
479
+ // Position above the textarea selection
480
+ // We approximate position using a mirror div technique
481
+ var rect = textarea.getBoundingClientRect();
482
+ var lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 20;
483
+ var paddingTop = parseInt(getComputedStyle(textarea).paddingTop) || 0;
484
+ var paddingLeft = parseInt(getComputedStyle(textarea).paddingLeft) || 0;
485
+ var fontSize = parseInt(getComputedStyle(textarea).fontSize) || 13;
486
+
487
+ // Get text before selection to calculate approximate position
488
+ var text = textarea.value;
489
+ var start = textarea.selectionStart;
490
+ var textBefore = text.substring(0, start);
491
+ var lines = textBefore.split("\n");
492
+ var currentLine = lines.length - 1;
493
+ var charInLine = lines[lines.length - 1].length;
494
+
495
+ // Approximate character width (monospace)
496
+ var charWidth = fontSize * 0.6;
497
+
498
+ var scrollTop = textarea.scrollTop;
499
+ var x = rect.left + paddingLeft + (charWidth * Math.min(charInLine, 40));
500
+ var y = rect.top + paddingTop + (currentLine * lineHeight) - scrollTop - 8;
501
+
502
+ // Clamp to viewport
503
+ var popoverWidth = 280;
504
+ if (x + popoverWidth / 2 > window.innerWidth) x = window.innerWidth - popoverWidth / 2 - 8;
505
+ if (x - popoverWidth / 2 < 8) x = popoverWidth / 2 + 8;
506
+ if (y < 40) y = rect.top + paddingTop + ((currentLine + 1) * lineHeight) - scrollTop + 28;
507
+
508
+ formatPopover.style.display = "flex";
509
+ formatPopover.style.left = x + "px";
510
+ formatPopover.style.top = y + "px";
511
+ }
512
+
513
+ function hidePopover() {
514
+ if (formatPopover) formatPopover.style.display = "none";
515
+ }
516
+
517
+ function applyFormat(textarea, action) {
518
+ var start = textarea.selectionStart;
519
+ var end = textarea.selectionEnd;
520
+ var selected = textarea.value.substring(start, end);
521
+
522
+ var replacement;
523
+
524
+ if (action.wrap) {
525
+ replacement = action.wrap[0] + selected + action.wrap[1];
526
+ } else if (action.prefix) {
527
+ var lines = selected.split("\n");
528
+ replacement = lines.map(function (line) { return action.prefix + line; }).join("\n");
529
+ }
530
+
531
+ // Use execCommand to preserve native undo/redo stack
532
+ textarea.focus();
533
+ textarea.setSelectionRange(start, end);
534
+ document.execCommand("insertText", false, replacement);
535
+
536
+ hidePopover();
222
537
  }