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.
- package/lib/background-task-timing.js +105 -0
- package/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +14 -1
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-vendor-login.js +20 -3
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +178 -6
- package/lib/public/app.js +58 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +163 -28
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/index.html +45 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +109 -4
- package/lib/public/modules/app-home-hub.js +19 -7
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +84 -7
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +46 -4
- package/lib/public/modules/app-projects.js +13 -8
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +21 -1
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +132 -46
- package/lib/sdk-message-processor.js +60 -14
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +124 -18
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +39 -0
- package/lib/sessions.js +61 -11
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +2 -2
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +81 -4
- package/lib/yoke/adapters/codex.js +63 -15
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/vendor-registry.js +1 -1
- 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
|
+
}
|