clay-server 4.0.0-beta.2 → 4.0.0-beta.21

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 (177) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/capsule-display-floor.js +51 -0
  3. package/lib/capsule-frame-server.js +255 -0
  4. package/lib/capsule-mate-turn.js +105 -0
  5. package/lib/capsule-pig-logic.js +300 -0
  6. package/lib/capsule-server-runtimes.js +39 -0
  7. package/lib/capsules/pig/display.js +198 -0
  8. package/lib/capsules/pig/manifest.json +9 -0
  9. package/lib/capsules/pig/ui.json +84 -0
  10. package/lib/config.js +25 -1
  11. package/lib/daemon-projects.js +13 -8
  12. package/lib/daemon.js +127 -14
  13. package/lib/git-cli.js +95 -1
  14. package/lib/knowledge-import.js +436 -0
  15. package/lib/knowledge-record-store.js +188 -0
  16. package/lib/knowledge-search.js +240 -0
  17. package/lib/mate-knowledge-mcp-server.js +151 -0
  18. package/lib/mate-knowledge-migration.js +283 -0
  19. package/lib/mate-knowledge-service.js +488 -0
  20. package/lib/mate-knowledge-sync.js +482 -0
  21. package/lib/migrate-single-user.js +2 -0
  22. package/lib/notes-lifecycle.js +149 -0
  23. package/lib/notes.js +60 -3
  24. package/lib/os-users.js +3 -1
  25. package/lib/project-capsule-catalog.js +9 -1
  26. package/lib/project-capsule-turn.js +96 -0
  27. package/lib/project-connection.js +14 -1
  28. package/lib/project-http.js +15 -1
  29. package/lib/project-knowledge.js +22 -0
  30. package/lib/project-log-context.js +172 -0
  31. package/lib/project-log-feedback-delivery.js +118 -0
  32. package/lib/project-logs-comments.js +137 -0
  33. package/lib/project-logs-context-state.js +42 -0
  34. package/lib/project-logs-mcp-server.js +328 -0
  35. package/lib/project-logs-query.js +185 -0
  36. package/lib/project-logs-root.js +75 -0
  37. package/lib/project-logs-schema.js +259 -0
  38. package/lib/project-logs-service.js +463 -0
  39. package/lib/project-logs-snapshot.js +205 -0
  40. package/lib/project-logs-store.js +495 -0
  41. package/lib/project-logs-versioning.js +132 -0
  42. package/lib/project-logs.js +400 -0
  43. package/lib/project-mate-interaction.js +14 -0
  44. package/lib/project-mate-knowledge.js +110 -0
  45. package/lib/project-memory.js +22 -0
  46. package/lib/project-message-delivery.js +70 -0
  47. package/lib/project-pair-lifecycle.js +460 -0
  48. package/lib/project-session-handoff.js +1 -1
  49. package/lib/project-session-notes.js +64 -13
  50. package/lib/project-session-pair.js +159 -146
  51. package/lib/project-session-spawn.js +1 -1
  52. package/lib/project-sessions.js +58 -13
  53. package/lib/project-user-message.js +53 -4
  54. package/lib/project-vendor-login.js +20 -3
  55. package/lib/project-worker-permission.js +430 -0
  56. package/lib/project-worker-proposal.js +208 -62
  57. package/lib/project.js +178 -6
  58. package/lib/public/app.js +58 -35
  59. package/lib/public/css/admin.css +2 -2
  60. package/lib/public/css/capsule-ui.css +38 -0
  61. package/lib/public/css/command-palette.css +19 -9
  62. package/lib/public/css/git-placard.css +154 -0
  63. package/lib/public/css/home-hub.css +4 -0
  64. package/lib/public/css/home-session-actions.css +66 -0
  65. package/lib/public/css/home-sidebar.css +66 -0
  66. package/lib/public/css/input.css +163 -28
  67. package/lib/public/css/mobile-nav.css +80 -0
  68. package/lib/public/css/notifications-center.css +67 -0
  69. package/lib/public/css/pane.css +52 -0
  70. package/lib/public/css/project-logs.css +198 -0
  71. package/lib/public/css/scheduler.css +1 -1
  72. package/lib/public/css/session-actions.css +16 -101
  73. package/lib/public/css/sidebar.css +103 -0
  74. package/lib/public/css/sticky-notes.css +237 -229
  75. package/lib/public/css/worker-proposal.css +34 -1
  76. package/lib/public/index.html +45 -11
  77. package/lib/public/modules/admin.js +8 -8
  78. package/lib/public/modules/app-connection.js +109 -4
  79. package/lib/public/modules/app-home-hub.js +19 -7
  80. package/lib/public/modules/app-message-cards.js +2 -1
  81. package/lib/public/modules/app-messages.js +84 -7
  82. package/lib/public/modules/app-misc.js +2 -14
  83. package/lib/public/modules/app-notifications.js +11 -7
  84. package/lib/public/modules/app-panels.js +46 -4
  85. package/lib/public/modules/app-projects.js +13 -8
  86. package/lib/public/modules/app-rendering.js +1 -0
  87. package/lib/public/modules/background-tasks-ui.js +139 -16
  88. package/lib/public/modules/capsule-preference.js +44 -0
  89. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  90. package/lib/public/modules/clay-log-links.js +70 -0
  91. package/lib/public/modules/command-palette.js +52 -15
  92. package/lib/public/modules/context-sources.js +0 -3
  93. package/lib/public/modules/git-agent-sessions.js +85 -0
  94. package/lib/public/modules/git-panel.js +111 -84
  95. package/lib/public/modules/git-placard.js +161 -0
  96. package/lib/public/modules/home-chat-empty-state.js +39 -0
  97. package/lib/public/modules/home-chat-identity.js +9 -0
  98. package/lib/public/modules/home-conversations-sheet.js +102 -48
  99. package/lib/public/modules/home-mate-chat.js +15 -35
  100. package/lib/public/modules/home-session-actions.js +6 -0
  101. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  102. package/lib/public/modules/home-sidebar.js +7 -0
  103. package/lib/public/modules/home-tool-frame.js +182 -0
  104. package/lib/public/modules/home-tools.js +138 -1
  105. package/lib/public/modules/input.js +38 -7
  106. package/lib/public/modules/markdown.js +2 -0
  107. package/lib/public/modules/mate-sidebar.js +0 -8
  108. package/lib/public/modules/message-delivery.js +97 -0
  109. package/lib/public/modules/paste-modal.js +84 -0
  110. package/lib/public/modules/project-logs-render.js +405 -0
  111. package/lib/public/modules/project-logs.js +461 -0
  112. package/lib/public/modules/project-removal-target.js +26 -0
  113. package/lib/public/modules/scheduler.js +17 -3
  114. package/lib/public/modules/session-actions.js +29 -105
  115. package/lib/public/modules/session-hierarchy.js +54 -0
  116. package/lib/public/modules/sidebar-mobile.js +40 -15
  117. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  118. package/lib/public/modules/sidebar-sessions.js +77 -11
  119. package/lib/public/modules/sidebar.js +17 -5
  120. package/lib/public/modules/split-group-helpers.js +8 -0
  121. package/lib/public/modules/split-view.js +5 -2
  122. package/lib/public/modules/sticky-note-markdown.js +5 -0
  123. package/lib/public/modules/sticky-notes-browser.js +326 -0
  124. package/lib/public/modules/sticky-notes-card.js +360 -0
  125. package/lib/public/modules/sticky-notes-editor.js +291 -0
  126. package/lib/public/modules/sticky-notes-shared.js +90 -0
  127. package/lib/public/modules/sticky-notes.js +170 -946
  128. package/lib/public/modules/tool-palette-order.js +143 -0
  129. package/lib/public/modules/tool-palette-overlays.js +159 -0
  130. package/lib/public/modules/tool-palette.js +21 -188
  131. package/lib/public/modules/tools.js +21 -1
  132. package/lib/public/modules/update-snooze.js +169 -0
  133. package/lib/public/modules/user-settings.js +4 -0
  134. package/lib/public/modules/worker-pane-lock.js +176 -0
  135. package/lib/public/modules/worker-proposal-state.js +16 -0
  136. package/lib/public/modules/worker-proposal.js +49 -12
  137. package/lib/public/style.css +3 -1
  138. package/lib/sdk-bridge.js +132 -46
  139. package/lib/sdk-message-processor.js +60 -14
  140. package/lib/server-experimental-settings.js +49 -0
  141. package/lib/server-global-ws.js +11 -0
  142. package/lib/server-home-chat-events.js +31 -1
  143. package/lib/server-home-chat.js +27 -4
  144. package/lib/server-home-clay-entry.js +1 -1
  145. package/lib/server-home-clay-session-links.js +34 -5
  146. package/lib/server-settings.js +5 -0
  147. package/lib/server-tools.js +127 -9
  148. package/lib/server.js +124 -18
  149. package/lib/session-driver-eligibility.js +38 -0
  150. package/lib/session-driver-orchestration.js +31 -0
  151. package/lib/session-hygiene.js +3 -0
  152. package/lib/session-notes-mcp-server.js +25 -5
  153. package/lib/session-pair-factory.js +247 -0
  154. package/lib/session-pair-mcp-server.js +46 -8
  155. package/lib/session-pair-prompts.js +79 -0
  156. package/lib/session-pair-turn-control.js +153 -0
  157. package/lib/session-provenance.js +119 -0
  158. package/lib/session-spawn-mcp-server.js +1 -1
  159. package/lib/session-split-groups.js +4 -1
  160. package/lib/session-title-policy.js +60 -0
  161. package/lib/session-visibility.js +39 -0
  162. package/lib/sessions.js +61 -11
  163. package/lib/tools-registry.js +89 -10
  164. package/lib/update-snooze.js +437 -0
  165. package/lib/users-experimental-preferences.js +30 -0
  166. package/lib/users-permissions.js +2 -2
  167. package/lib/users.js +22 -16
  168. package/lib/workspace-query-access.js +102 -0
  169. package/lib/workspace-query-service.js +24 -17
  170. package/lib/worktree.js +25 -38
  171. package/lib/ws-schema.js +38 -7
  172. package/lib/yoke/adapters/claude-worker.js +16 -6
  173. package/lib/yoke/adapters/claude.js +81 -4
  174. package/lib/yoke/adapters/codex.js +63 -15
  175. package/lib/yoke/codex-background-tasks.js +8 -2
  176. package/lib/yoke/vendor-registry.js +1 -1
  177. package/package.json +3 -3
@@ -0,0 +1,360 @@
1
+ // One floating sticky-note card: its markup, its drag and resize behaviour, and
2
+ // its header controls (colour, minimize, close, bring to front).
3
+ //
4
+ // The card knows how to build and wire itself. It does not own the board, so it
5
+ // never touches the notes map or the badges; the canvas module holds those and
6
+ // calls renderNote for each note.
7
+
8
+ import { refreshIcons, iconHtml } from './icons.js';
9
+ import { VENDOR_AVATARS } from './app-rendering.js';
10
+ import { getTitle, renderMiniMarkdown } from './sticky-note-markdown.js';
11
+ import { enhanceClayLogLinks } from './clay-log-links.js';
12
+ import {
13
+ NOTE_COLORS,
14
+ isClosedNote,
15
+ send,
16
+ clampPos,
17
+ clampSize,
18
+ debouncedUpdate,
19
+ } from './sticky-notes-shared.js';
20
+ import { setupTextEdit } from './sticky-notes-editor.js';
21
+
22
+ var colorPickerEl = null;
23
+
24
+ // --- Note rendering ---
25
+
26
+ export function renderNote(data) {
27
+ var el = document.createElement("div");
28
+ el.className = "sticky-note";
29
+ el.dataset.noteId = data.id;
30
+ var clamped = clampPos(data.x, data.y, data.w, data.h);
31
+ el.style.left = clamped.x + "px";
32
+ el.style.top = clamped.y + "px";
33
+ el.style.width = data.w + "px";
34
+ el.style.height = data.h + "px";
35
+ el.style.zIndex = 100 + (data.zIndex || 0);
36
+ el.dataset.color = data.color || "purple";
37
+
38
+ if (data.minimized) el.classList.add("minimized");
39
+ if (isClosedNote(data)) el.classList.add("hidden");
40
+
41
+ // Header
42
+ var header = document.createElement("div");
43
+ header.className = "sticky-note-header";
44
+
45
+ if (data.origin && VENDOR_AVATARS[data.origin.vendor]) {
46
+ var originBadge = document.createElement("img");
47
+ originBadge.className = "sticky-note-agent-badge";
48
+ originBadge.src = VENDOR_AVATARS[data.origin.vendor];
49
+ originBadge.alt = "";
50
+ originBadge.title = "Created by " + data.origin.vendor;
51
+ header.appendChild(originBadge);
52
+ }
53
+
54
+ var closeBtn = document.createElement("button");
55
+ closeBtn.className = "sticky-note-btn sticky-note-close";
56
+ closeBtn.title = "Close";
57
+ closeBtn.innerHTML = iconHtml("x");
58
+ header.appendChild(closeBtn);
59
+
60
+ var minBtn = document.createElement("button");
61
+ minBtn.className = "sticky-note-btn sticky-note-min-btn";
62
+ minBtn.title = data.minimized ? "Expand" : "Minimize";
63
+ minBtn.innerHTML = data.minimized ? iconHtml("maximize-2") : iconHtml("minus");
64
+ header.appendChild(minBtn);
65
+
66
+ var spacer = document.createElement("div");
67
+ spacer.className = "sticky-note-spacer";
68
+ spacer.textContent = getTitle(data.text) || "Untitled";
69
+ header.appendChild(spacer);
70
+
71
+ var addBtn = document.createElement("button");
72
+ addBtn.className = "sticky-note-btn";
73
+ addBtn.title = "New note";
74
+ addBtn.innerHTML = iconHtml("plus");
75
+ addBtn.addEventListener("click", function (e) {
76
+ e.stopPropagation();
77
+ createNote();
78
+ });
79
+ header.appendChild(addBtn);
80
+
81
+ var colorBtn = document.createElement("button");
82
+ colorBtn.className = "sticky-note-color-btn";
83
+ colorBtn.title = "Change color";
84
+ colorBtn.innerHTML = iconHtml("palette");
85
+ header.appendChild(colorBtn);
86
+
87
+ var mdBtn = document.createElement("button");
88
+ mdBtn.className = "sticky-note-btn sticky-note-md-btn";
89
+ mdBtn.title = "Edit markdown";
90
+ mdBtn.innerHTML = "<span class='sn-md-label'>MD</span>";
91
+ header.appendChild(mdBtn);
92
+
93
+ var opacityWrap = document.createElement("div");
94
+ opacityWrap.className = "sticky-note-opacity";
95
+ var opacitySlider = document.createElement("input");
96
+ opacitySlider.type = "range";
97
+ opacitySlider.min = "20";
98
+ opacitySlider.max = "100";
99
+ opacitySlider.value = String(Math.round((typeof data.opacity === "number" ? data.opacity : 0.64) * 100));
100
+ opacitySlider.className = "sticky-note-opacity-slider";
101
+ opacitySlider.title = "Opacity";
102
+ opacityWrap.appendChild(opacitySlider);
103
+ header.appendChild(opacityWrap);
104
+
105
+ // Apply saved opacity via CSS variable (not element opacity, so header stays visible on hover)
106
+ if (typeof data.opacity === "number") {
107
+ el.style.setProperty("--note-opacity", data.opacity);
108
+ }
109
+
110
+ opacitySlider.addEventListener("input", function (e) {
111
+ e.stopPropagation();
112
+ var val = parseInt(opacitySlider.value, 10) / 100;
113
+ el.style.setProperty("--note-opacity", val);
114
+ });
115
+ opacitySlider.addEventListener("change", function (e) {
116
+ e.stopPropagation();
117
+ var val = parseInt(opacitySlider.value, 10) / 100;
118
+ el.style.setProperty("--note-opacity", val);
119
+ debouncedUpdate(data.id, { opacity: val }, 300);
120
+ });
121
+ opacitySlider.addEventListener("mousedown", function (e) { e.stopPropagation(); });
122
+
123
+ el.appendChild(header);
124
+
125
+ // Body
126
+ var body = document.createElement("div");
127
+ body.className = "sticky-note-body";
128
+
129
+ // Hidden textarea as markdown data store
130
+ var textarea = document.createElement("textarea");
131
+ textarea.className = "sticky-note-text";
132
+ textarea.value = data.text || "";
133
+ textarea.style.display = "none";
134
+ body.appendChild(textarea);
135
+
136
+ // Contenteditable rendered view (primary editing surface)
137
+ var rendered = document.createElement("div");
138
+ rendered.className = "sticky-note-rendered";
139
+ rendered.contentEditable = "true";
140
+ rendered.spellcheck = true;
141
+ if (data.text && data.text.trim()) {
142
+ rendered.innerHTML = renderMiniMarkdown(data.text);
143
+ enhanceClayLogLinks(rendered);
144
+ } else {
145
+ rendered.classList.add("is-empty");
146
+ }
147
+ body.appendChild(rendered);
148
+
149
+ el.appendChild(body);
150
+
151
+ // Resize handle
152
+ var resizeHandle = document.createElement("div");
153
+ resizeHandle.className = "sticky-note-resize";
154
+ el.appendChild(resizeHandle);
155
+
156
+ // --- Event handlers ---
157
+ setupDrag(el, spacer, data.id);
158
+ setupResize(el, resizeHandle, data.id);
159
+ setupTextEdit(textarea, rendered, data.id, mdBtn);
160
+ setupColorPicker(colorBtn, el, data.id);
161
+ setupMinimize(minBtn, el, data.id);
162
+ setupClose(closeBtn, el, data.id);
163
+ setupBringToFront(el, data.id);
164
+
165
+ refreshIcons();
166
+ return el;
167
+ }
168
+ // --- Drag ---
169
+
170
+ function setupDrag(noteEl, spacerEl, noteId) {
171
+ var dragging = false;
172
+ var pointerId = null;
173
+ var startX, startY, origX, origY;
174
+
175
+ spacerEl.addEventListener("pointerdown", function (e) {
176
+ if (e.pointerType === "mouse" && e.button !== 0) return;
177
+ e.preventDefault();
178
+ dragging = true;
179
+ pointerId = e.pointerId;
180
+ startX = e.clientX;
181
+ startY = e.clientY;
182
+ origX = parseInt(noteEl.style.left) || 0;
183
+ origY = parseInt(noteEl.style.top) || 0;
184
+ noteEl.classList.add("dragging");
185
+ document.body.classList.add("sticky-note-interacting");
186
+ spacerEl.setPointerCapture(pointerId);
187
+ });
188
+
189
+ function onMove(e) {
190
+ if (!dragging || e.pointerId !== pointerId) return;
191
+ var dx = e.clientX - startX;
192
+ var dy = e.clientY - startY;
193
+ var c = clampPos(origX + dx, origY + dy, noteEl.offsetWidth, noteEl.offsetHeight);
194
+ noteEl.style.left = c.x + "px";
195
+ noteEl.style.top = c.y + "px";
196
+ }
197
+
198
+ function onUp(e) {
199
+ if (!dragging || (e && e.pointerId !== pointerId)) return;
200
+ dragging = false;
201
+ noteEl.classList.remove("dragging");
202
+ document.body.classList.remove("sticky-note-interacting");
203
+ if (pointerId !== null && spacerEl.hasPointerCapture(pointerId)) spacerEl.releasePointerCapture(pointerId);
204
+ pointerId = null;
205
+ debouncedUpdate(noteId, {
206
+ x: parseInt(noteEl.style.left),
207
+ y: parseInt(noteEl.style.top),
208
+ }, 200);
209
+ }
210
+
211
+ spacerEl.addEventListener("pointermove", onMove);
212
+ spacerEl.addEventListener("pointerup", onUp);
213
+ spacerEl.addEventListener("pointercancel", onUp);
214
+ spacerEl.addEventListener("lostpointercapture", onUp);
215
+ }
216
+
217
+ // --- Resize ---
218
+
219
+ function setupResize(noteEl, handle, noteId) {
220
+ var resizing = false;
221
+ var pointerId = null;
222
+ var startX, startY, origW, origH;
223
+ var MIN_W = 160;
224
+ var MIN_H = 80;
225
+
226
+ handle.addEventListener("pointerdown", function (e) {
227
+ if (e.pointerType === "mouse" && e.button !== 0) return;
228
+ e.preventDefault();
229
+ e.stopPropagation();
230
+ resizing = true;
231
+ pointerId = e.pointerId;
232
+ startX = e.clientX;
233
+ startY = e.clientY;
234
+ origW = noteEl.offsetWidth;
235
+ origH = noteEl.offsetHeight;
236
+ noteEl.classList.add("resizing");
237
+ document.body.classList.add("sticky-note-interacting");
238
+ handle.setPointerCapture(pointerId);
239
+ });
240
+
241
+ function onMove(e) {
242
+ if (!resizing || e.pointerId !== pointerId) return;
243
+ var rawW = Math.max(MIN_W, origW + (e.clientX - startX));
244
+ var rawH = Math.max(MIN_H, origH + (e.clientY - startY));
245
+ var cs = clampSize(parseInt(noteEl.style.left) || 0, parseInt(noteEl.style.top) || 0, rawW, rawH);
246
+ noteEl.style.width = Math.max(MIN_W, cs.w) + "px";
247
+ noteEl.style.height = Math.max(MIN_H, cs.h) + "px";
248
+ }
249
+
250
+ function onUp(e) {
251
+ if (!resizing || (e && e.pointerId !== pointerId)) return;
252
+ resizing = false;
253
+ noteEl.classList.remove("resizing");
254
+ document.body.classList.remove("sticky-note-interacting");
255
+ if (pointerId !== null && handle.hasPointerCapture(pointerId)) handle.releasePointerCapture(pointerId);
256
+ pointerId = null;
257
+ debouncedUpdate(noteId, {
258
+ w: noteEl.offsetWidth,
259
+ h: noteEl.offsetHeight,
260
+ }, 200);
261
+ }
262
+
263
+ handle.addEventListener("pointermove", onMove);
264
+ handle.addEventListener("pointerup", onUp);
265
+ handle.addEventListener("pointercancel", onUp);
266
+ handle.addEventListener("lostpointercapture", onUp);
267
+ }
268
+
269
+ // --- Color picker ---
270
+
271
+ export function closeColorPicker() {
272
+ if (colorPickerEl) {
273
+ colorPickerEl.remove();
274
+ colorPickerEl = null;
275
+ }
276
+ }
277
+
278
+ function setupColorPicker(btn, noteEl, noteId) {
279
+ btn.addEventListener("click", function (e) {
280
+ e.stopPropagation();
281
+ showColorPicker(btn, noteEl, noteId);
282
+ });
283
+ }
284
+
285
+ function showColorPicker(anchor, noteEl, noteId) {
286
+ closeColorPicker();
287
+
288
+ var picker = document.createElement("div");
289
+ picker.className = "sticky-note-color-picker";
290
+
291
+ for (var i = 0; i < NOTE_COLORS.length; i++) {
292
+ (function (color) {
293
+ var dot = document.createElement("button");
294
+ dot.className = "sticky-note-color-dot";
295
+ dot.dataset.color = color;
296
+ if (noteEl.dataset.color === color) dot.classList.add("active");
297
+ dot.addEventListener("click", function (e) {
298
+ e.stopPropagation();
299
+ noteEl.dataset.color = color;
300
+ send({ type: "note_update", id: noteId, color: color });
301
+ closeColorPicker();
302
+ });
303
+ picker.appendChild(dot);
304
+ })(NOTE_COLORS[i]);
305
+ }
306
+
307
+ document.body.appendChild(picker);
308
+ colorPickerEl = picker;
309
+
310
+ // Position relative to anchor
311
+ var rect = anchor.getBoundingClientRect();
312
+ picker.style.left = rect.left + "px";
313
+ picker.style.top = (rect.bottom + 4) + "px";
314
+
315
+ // Close on outside click
316
+ setTimeout(function () {
317
+ document.addEventListener("click", function closeHandler() {
318
+ closeColorPicker();
319
+ document.removeEventListener("click", closeHandler);
320
+ });
321
+ }, 0);
322
+ }
323
+
324
+ // --- Minimize ---
325
+
326
+ function setupMinimize(btn, noteEl, noteId) {
327
+ btn.addEventListener("click", function (e) {
328
+ e.stopPropagation();
329
+ var isMinimized = noteEl.classList.toggle("minimized");
330
+ btn.innerHTML = isMinimized ? iconHtml("maximize-2") : iconHtml("minus");
331
+ btn.title = isMinimized ? "Expand" : "Minimize";
332
+ refreshIcons();
333
+ send({ type: "note_update", id: noteId, minimized: isMinimized });
334
+ });
335
+ }
336
+
337
+ // --- Close note ---
338
+ //
339
+ // Completing a note closes it. The record is kept and stays reachable in the
340
+ // browser's Closed tab; nothing is ever deleted from this control.
341
+
342
+ function setupClose(btn, noteEl, noteId) {
343
+ btn.addEventListener("click", function (e) {
344
+ e.stopPropagation();
345
+ noteEl.classList.add("hidden");
346
+ send({ type: "note_close", id: noteId });
347
+ });
348
+ }
349
+
350
+ // --- Bring to front ---
351
+
352
+ function setupBringToFront(noteEl, noteId) {
353
+ noteEl.addEventListener("mousedown", function (e) {
354
+ // Skip bring-to-front when clicking header buttons to avoid
355
+ // race condition where server response replaces innerHTML
356
+ // between mousedown and click, causing the click event to be lost
357
+ if (e.target.closest("button")) return;
358
+ send({ type: "note_bring_front", id: noteId });
359
+ });
360
+ }
@@ -0,0 +1,291 @@
1
+ // Sticky-note text editing: the WYSIWYG format toolbar and the contenteditable
2
+ // surface behind it.
3
+ //
4
+ // Split out of sticky-notes.js to keep every module under the size limit. It
5
+ // owns the toolbar element and nothing else, and depends only on the leaf
6
+ // helpers, so it never has to reach back into the canvas.
7
+
8
+ import { refreshIcons, iconHtml } from './icons.js';
9
+ import { extractMarkdown, renderMiniMarkdown } from './sticky-note-markdown.js';
10
+ import { enhanceClayLogLinks } from './clay-log-links.js';
11
+ import { debouncedTextUpdate, syncTitle } from './sticky-notes-shared.js';
12
+
13
+ var formatToolbarEl = null;
14
+
15
+ // --- Text edit (contenteditable) ---
16
+
17
+ // --- Format toolbar (WYSIWYG) ---
18
+
19
+ var FORMAT_BUTTONS = [
20
+ { label: "B", title: "Bold", cls: "sn-fmt-bold", command: "bold" },
21
+ { label: "I", title: "Italic", cls: "sn-fmt-italic", command: "italic" },
22
+ { label: "S", title: "Strikethrough", cls: "sn-fmt-strike", command: "strikethrough" },
23
+ { label: "code-2", title: "Code", cls: "sn-fmt-code", command: "code", isIcon: true },
24
+ ];
25
+
26
+ // The canvas asks this before deciding an outside click should dismiss the
27
+ // toolbar, so the element itself stays private to this module.
28
+ export function isFormatToolbarOpen() {
29
+ return !!formatToolbarEl;
30
+ }
31
+ export function closeFormatToolbar() {
32
+ if (formatToolbarEl) {
33
+ formatToolbarEl.remove();
34
+ formatToolbarEl = null;
35
+ }
36
+ }
37
+
38
+ function applyFormat(command, rendered) {
39
+ if (command === "code") {
40
+ var sel = window.getSelection();
41
+ if (!sel.rangeCount || sel.isCollapsed) return;
42
+ var range = sel.getRangeAt(0);
43
+ var ancestor = range.commonAncestorContainer;
44
+ var codeParent = (ancestor.nodeType === 3 ? ancestor.parentElement : ancestor);
45
+ if (codeParent && codeParent.closest && codeParent.closest("code")) {
46
+ // Unwrap: replace <code> with its text content
47
+ var codeEl = codeParent.closest("code");
48
+ var textNode = document.createTextNode(codeEl.textContent);
49
+ codeEl.parentNode.replaceChild(textNode, codeEl);
50
+ var newRange = document.createRange();
51
+ newRange.selectNodeContents(textNode);
52
+ sel.removeAllRanges();
53
+ sel.addRange(newRange);
54
+ } else {
55
+ // Wrap selection in <code>
56
+ var code = document.createElement("code");
57
+ try { range.surroundContents(code); } catch (e) {
58
+ var frag = range.extractContents();
59
+ code.appendChild(frag);
60
+ range.insertNode(code);
61
+ }
62
+ }
63
+ } else {
64
+ document.execCommand(command, false, null);
65
+ }
66
+ // Trigger sync
67
+ rendered.dispatchEvent(new Event("input", { bubbles: true }));
68
+ }
69
+
70
+ function showFormatToolbar(rendered) {
71
+ closeFormatToolbar();
72
+
73
+ var sel = window.getSelection();
74
+ if (!sel || !sel.rangeCount || sel.isCollapsed) return;
75
+ if (!sel.toString().trim()) return;
76
+
77
+ var range = sel.getRangeAt(0);
78
+ // When dragging outside the note, commonAncestorContainer may be a parent
79
+ // of rendered. Clamp the range to the rendered element so the toolbar shows.
80
+ if (!rendered.contains(range.commonAncestorContainer)) {
81
+ try {
82
+ var clampedRange = range.cloneRange();
83
+ if (range.startContainer === rendered || rendered.contains(range.startContainer)) {
84
+ clampedRange.selectNodeContents(rendered);
85
+ clampedRange.setStart(range.startContainer, range.startOffset);
86
+ } else if (range.endContainer === rendered || rendered.contains(range.endContainer)) {
87
+ clampedRange.selectNodeContents(rendered);
88
+ clampedRange.setEnd(range.endContainer, range.endOffset);
89
+ } else {
90
+ return;
91
+ }
92
+ range = clampedRange;
93
+ } catch (e) {
94
+ return;
95
+ }
96
+ }
97
+
98
+ var toolbar = document.createElement("div");
99
+ toolbar.className = "sn-format-toolbar";
100
+
101
+ for (var i = 0; i < FORMAT_BUTTONS.length; i++) {
102
+ (function (cfg) {
103
+ var btn = document.createElement("button");
104
+ btn.className = "sn-fmt-btn " + cfg.cls;
105
+ btn.title = cfg.title;
106
+ btn.innerHTML = cfg.isIcon ? iconHtml(cfg.label) : cfg.label;
107
+ btn.addEventListener("mousedown", function (e) {
108
+ e.preventDefault();
109
+ e.stopPropagation();
110
+ applyFormat(cfg.command, rendered);
111
+ setTimeout(function () {
112
+ var s = window.getSelection();
113
+ if (s && !s.isCollapsed) {
114
+ showFormatToolbar(rendered);
115
+ } else {
116
+ closeFormatToolbar();
117
+ }
118
+ }, 0);
119
+ });
120
+ toolbar.appendChild(btn);
121
+ })(FORMAT_BUTTONS[i]);
122
+ }
123
+
124
+ refreshIcons();
125
+ document.body.appendChild(toolbar);
126
+ formatToolbarEl = toolbar;
127
+ positionToolbarAtRange(toolbar, range);
128
+ }
129
+
130
+ function positionToolbarAtRange(toolbar, range) {
131
+ var rect = range.getBoundingClientRect();
132
+ var toolbarX = rect.left + rect.width / 2;
133
+ var toolbarY = rect.top - 4;
134
+
135
+ toolbar.style.left = toolbarX + "px";
136
+ toolbar.style.top = toolbarY + "px";
137
+
138
+ requestAnimationFrame(function () {
139
+ var tw = toolbar.offsetWidth;
140
+ var th = toolbar.offsetHeight;
141
+ var x = Math.max(8, Math.min(toolbarX - tw / 2, window.innerWidth - tw - 8));
142
+ var y = Math.max(8, toolbarY - th);
143
+ toolbar.style.left = x + "px";
144
+ toolbar.style.top = y + "px";
145
+ });
146
+ }
147
+
148
+ export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
149
+ var noteEl = textarea.closest(".sticky-note");
150
+ var mdMode = false;
151
+
152
+ function saveRenderedMarkdown() {
153
+ var markdown = extractMarkdown(rendered);
154
+ textarea.value = markdown;
155
+ debouncedTextUpdate(noteId, markdown);
156
+ syncTitle(noteEl, markdown);
157
+ rendered.classList.toggle("is-empty", !markdown.trim());
158
+ }
159
+
160
+ function toggleChecklistItem(check) {
161
+ var checked = !check.classList.contains("checked");
162
+ check.classList.toggle("checked", checked);
163
+ check.textContent = checked ? "✓" : "☐";
164
+ check.setAttribute("aria-checked", checked ? "true" : "false");
165
+ saveRenderedMarkdown();
166
+ }
167
+
168
+ // MD button: toggle between contenteditable rendered view and raw textarea
169
+ mdBtn.addEventListener("click", function (e) {
170
+ e.stopPropagation();
171
+ mdMode = !mdMode;
172
+ if (mdMode) {
173
+ // Switch to raw markdown editing
174
+ var md = extractMarkdown(rendered);
175
+ textarea.value = md;
176
+ textarea.style.display = "";
177
+ rendered.style.display = "none";
178
+ mdBtn.classList.add("active");
179
+ textarea.focus();
180
+ } else {
181
+ // Switch back to contenteditable rendered view
182
+ var md = textarea.value;
183
+ debouncedTextUpdate(noteId, md);
184
+ syncTitle(noteEl, md);
185
+ if (md.trim()) {
186
+ rendered.innerHTML = renderMiniMarkdown(md);
187
+ enhanceClayLogLinks(rendered);
188
+ rendered.classList.remove("is-empty");
189
+ } else {
190
+ rendered.innerHTML = "";
191
+ rendered.classList.add("is-empty");
192
+ }
193
+ textarea.style.display = "none";
194
+ rendered.style.display = "";
195
+ mdBtn.classList.remove("active");
196
+ rendered.focus();
197
+ }
198
+ });
199
+
200
+ // Sync contenteditable changes to textarea (data store) and save
201
+ rendered.addEventListener("input", function () {
202
+ saveRenderedMarkdown();
203
+ });
204
+
205
+ rendered.addEventListener("click", function (e) {
206
+ var check = e.target.closest && e.target.closest(".sn-check");
207
+ if (!check || !rendered.contains(check)) return;
208
+ e.preventDefault();
209
+ e.stopPropagation();
210
+ toggleChecklistItem(check);
211
+ });
212
+
213
+ // On blur, re-render to normalize HTML structure
214
+ rendered.addEventListener("blur", function (e) {
215
+ // Don't re-render if clicking format toolbar (it prevents default, but just in case)
216
+ if (e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(".sn-format-toolbar")) return;
217
+ closeFormatToolbar();
218
+ var md = extractMarkdown(rendered);
219
+ textarea.value = md;
220
+ if (md.trim()) {
221
+ rendered.innerHTML = renderMiniMarkdown(md);
222
+ enhanceClayLogLinks(rendered);
223
+ rendered.classList.remove("is-empty");
224
+ } else {
225
+ rendered.innerHTML = "";
226
+ rendered.classList.add("is-empty");
227
+ }
228
+ });
229
+
230
+ // Show format toolbar when selecting text
231
+ rendered.addEventListener("mouseup", function (e) {
232
+ if (e.target.tagName === "A") return;
233
+ setTimeout(function () {
234
+ var sel = window.getSelection();
235
+ if (sel && !sel.isCollapsed && sel.toString().trim()) {
236
+ showFormatToolbar(rendered);
237
+ } else {
238
+ closeFormatToolbar();
239
+ }
240
+ }, 10);
241
+ });
242
+
243
+ // Keyboard selection
244
+ rendered.addEventListener("keyup", function (e) {
245
+ if (e.shiftKey || e.key === "Shift") {
246
+ var sel = window.getSelection();
247
+ if (sel && !sel.isCollapsed) {
248
+ showFormatToolbar(rendered);
249
+ } else {
250
+ closeFormatToolbar();
251
+ }
252
+ }
253
+ });
254
+
255
+ // Insert <br> on Enter instead of <div>
256
+ rendered.addEventListener("keydown", function (e) {
257
+ if (e.target.classList && e.target.classList.contains("sn-check") &&
258
+ (e.key === "Enter" || e.key === " ")) {
259
+ e.preventDefault();
260
+ toggleChecklistItem(e.target);
261
+ return;
262
+ }
263
+ if (e.key === "Enter" && !e.shiftKey) {
264
+ e.preventDefault();
265
+ document.execCommand("insertLineBreak");
266
+ }
267
+ });
268
+
269
+ // Paste as plain text to avoid importing HTML formatting
270
+ rendered.addEventListener("paste", function (e) {
271
+ e.preventDefault();
272
+ var text = (e.clipboardData || window.clipboardData).getData("text/plain");
273
+ document.execCommand("insertText", false, text);
274
+ });
275
+
276
+ // Prevent drag when clicking body
277
+ rendered.addEventListener("mousedown", function (e) {
278
+ e.stopPropagation();
279
+ });
280
+
281
+ // Sync textarea edits when in MD mode
282
+ textarea.addEventListener("input", function () {
283
+ if (!mdMode) return;
284
+ debouncedTextUpdate(noteId, textarea.value);
285
+ syncTitle(noteEl, textarea.value);
286
+ });
287
+
288
+ textarea.addEventListener("mousedown", function (e) {
289
+ e.stopPropagation();
290
+ });
291
+ }