clay-server 2.27.0-beta.8 → 2.27.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.
- package/README.md +10 -0
- package/lib/daemon-projects.js +164 -0
- package/lib/daemon.js +13 -126
- package/lib/mates-identity.js +132 -0
- package/lib/mates-knowledge.js +113 -0
- package/lib/mates-prompts.js +398 -0
- package/lib/mates.js +40 -599
- package/lib/project-connection.js +2 -0
- package/lib/project-debate.js +19 -12
- package/lib/project-http.js +4 -2
- package/lib/project-loop.js +110 -48
- package/lib/project-mate-interaction.js +4 -0
- package/lib/project-notifications.js +210 -0
- package/lib/project-sessions.js +5 -2
- package/lib/project-user-message.js +2 -1
- package/lib/project.js +26 -2
- package/lib/public/app.js +1193 -8521
- package/lib/public/css/command-palette.css +14 -0
- package/lib/public/css/loop.css +301 -0
- package/lib/public/css/notifications-center.css +190 -0
- package/lib/public/css/rewind.css +6 -0
- package/lib/public/index.html +89 -35
- package/lib/public/modules/app-connection.js +160 -0
- package/lib/public/modules/app-cursors.js +473 -0
- package/lib/public/modules/app-debate-ui.js +389 -0
- package/lib/public/modules/app-dm.js +627 -0
- package/lib/public/modules/app-favicon.js +212 -0
- package/lib/public/modules/app-header.js +229 -0
- package/lib/public/modules/app-home-hub.js +600 -0
- package/lib/public/modules/app-loop-ui.js +589 -0
- package/lib/public/modules/app-loop-wizard.js +439 -0
- package/lib/public/modules/app-messages.js +1560 -0
- package/lib/public/modules/app-misc.js +299 -0
- package/lib/public/modules/app-notifications.js +372 -0
- package/lib/public/modules/app-panels.js +888 -0
- package/lib/public/modules/app-projects.js +798 -0
- package/lib/public/modules/app-rate-limit.js +451 -0
- package/lib/public/modules/app-rendering.js +597 -0
- package/lib/public/modules/app-skills-install.js +234 -0
- package/lib/public/modules/command-palette.js +27 -4
- package/lib/public/modules/input.js +31 -20
- package/lib/public/modules/scheduler-config.js +1532 -0
- package/lib/public/modules/scheduler-history.js +79 -0
- package/lib/public/modules/scheduler.js +33 -1554
- package/lib/public/modules/session-search.js +13 -1
- package/lib/public/modules/sidebar-mates.js +812 -0
- package/lib/public/modules/sidebar-mobile.js +1269 -0
- package/lib/public/modules/sidebar-projects.js +1449 -0
- package/lib/public/modules/sidebar-sessions.js +986 -0
- package/lib/public/modules/sidebar.js +232 -4591
- package/lib/public/modules/store.js +27 -0
- package/lib/public/modules/ws-ref.js +7 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +96 -717
- package/lib/sdk-message-processor.js +587 -0
- package/lib/sdk-message-queue.js +42 -0
- package/lib/sdk-skill-discovery.js +131 -0
- package/lib/server-admin.js +712 -0
- package/lib/server-auth.js +737 -0
- package/lib/server-dm.js +221 -0
- package/lib/server-mates.js +281 -0
- package/lib/server-palette.js +110 -0
- package/lib/server-settings.js +479 -0
- package/lib/server-skills.js +280 -0
- package/lib/server.js +246 -2755
- package/lib/sessions.js +11 -4
- package/lib/users-auth.js +146 -0
- package/lib/users-permissions.js +118 -0
- package/lib/users-preferences.js +210 -0
- package/lib/users.js +48 -398
- package/lib/ws-schema.js +498 -0
- package/package.json +1 -1
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// app-cursors.js - Remote cursor presence, text selection sharing
|
|
2
|
+
// Extracted from app.js (PR-27)
|
|
3
|
+
|
|
4
|
+
import { avatarUrl } from './avatar.js';
|
|
5
|
+
|
|
6
|
+
var _ctx = null;
|
|
7
|
+
|
|
8
|
+
// --- Module-owned state ---
|
|
9
|
+
var cursorSharingEnabled = localStorage.getItem("cursorSharing") !== "off";
|
|
10
|
+
var remoteCursors = {}; // userId -> { el, indicator, timer, lastY, active }
|
|
11
|
+
var cursorThrottleTimer = null;
|
|
12
|
+
var CURSOR_THROTTLE_MS = 30;
|
|
13
|
+
var CURSOR_HIDE_TIMEOUT = 5000;
|
|
14
|
+
|
|
15
|
+
var cursorColors = [
|
|
16
|
+
"#F24822", "#FF7262", "#A259FF", "#1ABCFE",
|
|
17
|
+
"#0ACF83", "#FF6D00", "#E84393", "#6C5CE7",
|
|
18
|
+
"#00B894", "#FDCB6E", "#E17055", "#74B9FF",
|
|
19
|
+
];
|
|
20
|
+
var userColorMap = {};
|
|
21
|
+
var nextColorIdx = 0;
|
|
22
|
+
|
|
23
|
+
var remoteSelections = {}; // userId -> { els: [], timer }
|
|
24
|
+
var selectionThrottleTimer = null;
|
|
25
|
+
var lastSelectionKey = "";
|
|
26
|
+
|
|
27
|
+
// --- Internal helpers ---
|
|
28
|
+
|
|
29
|
+
function getCursorColor(userId) {
|
|
30
|
+
if (!userColorMap[userId]) {
|
|
31
|
+
userColorMap[userId] = cursorColors[nextColorIdx % cursorColors.length];
|
|
32
|
+
nextColorIdx++;
|
|
33
|
+
}
|
|
34
|
+
return userColorMap[userId];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createCursorElement(userId, displayName, color, avatarStyle, avatarSeed, avatarCustom) {
|
|
38
|
+
var wrapper = document.createElement("div");
|
|
39
|
+
wrapper.className = "remote-cursor";
|
|
40
|
+
wrapper.dataset.userId = userId;
|
|
41
|
+
wrapper.style.position = "absolute";
|
|
42
|
+
wrapper.style.zIndex = "9999";
|
|
43
|
+
wrapper.style.pointerEvents = "none";
|
|
44
|
+
wrapper.style.display = "none";
|
|
45
|
+
wrapper.style.transition = "left 30ms linear, top 30ms linear";
|
|
46
|
+
wrapper.style.willChange = "left, top";
|
|
47
|
+
|
|
48
|
+
// SVG cursor arrow
|
|
49
|
+
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
50
|
+
svg.setAttribute("width", "16");
|
|
51
|
+
svg.setAttribute("height", "20");
|
|
52
|
+
svg.setAttribute("viewBox", "0 0 16 20");
|
|
53
|
+
svg.style.display = "block";
|
|
54
|
+
svg.style.filter = "drop-shadow(0 1px 2px rgba(0,0,0,0.3))";
|
|
55
|
+
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
56
|
+
path.setAttribute("d", "M0 0 L0 16 L4.5 12 L8 19 L10.5 18 L7 11 L13 11 Z");
|
|
57
|
+
path.setAttribute("fill", color);
|
|
58
|
+
path.setAttribute("stroke", "#fff");
|
|
59
|
+
path.setAttribute("stroke-width", "1");
|
|
60
|
+
svg.appendChild(path);
|
|
61
|
+
wrapper.appendChild(svg);
|
|
62
|
+
|
|
63
|
+
// Tag: avatar + name label together
|
|
64
|
+
var tag = document.createElement("div");
|
|
65
|
+
tag.className = "remote-cursor-tag";
|
|
66
|
+
tag.style.cssText = "position:absolute;left:14px;top:14px;display:flex;align-items:center;" +
|
|
67
|
+
"gap:3px;background:" + color + ";padding:1px 6px 1px 2px;border-radius:10px;" +
|
|
68
|
+
"pointer-events:none;white-space:nowrap;";
|
|
69
|
+
|
|
70
|
+
// Avatar
|
|
71
|
+
var avatarImg = document.createElement("img");
|
|
72
|
+
avatarImg.className = "remote-cursor-avatar";
|
|
73
|
+
avatarImg.src = avatarCustom ? avatarCustom : avatarUrl(avatarStyle || "thumbs", avatarSeed || userId, 16);
|
|
74
|
+
avatarImg.style.cssText = "width:14px;height:14px;border-radius:50%;background:#fff;flex-shrink:0;";
|
|
75
|
+
tag.appendChild(avatarImg);
|
|
76
|
+
|
|
77
|
+
// Name label
|
|
78
|
+
var label = document.createElement("span");
|
|
79
|
+
label.className = "remote-cursor-label";
|
|
80
|
+
label.textContent = displayName;
|
|
81
|
+
label.style.cssText = "color:#fff;font-size:11px;font-weight:500;line-height:16px;font-family:inherit;";
|
|
82
|
+
tag.appendChild(label);
|
|
83
|
+
|
|
84
|
+
wrapper.appendChild(tag);
|
|
85
|
+
|
|
86
|
+
return wrapper;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Compute cumulative character offset within a container element
|
|
90
|
+
function getCharOffset(container, targetNode, targetOffset) {
|
|
91
|
+
var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
|
|
92
|
+
var offset = 0;
|
|
93
|
+
var node;
|
|
94
|
+
while ((node = walker.nextNode())) {
|
|
95
|
+
if (node === targetNode) {
|
|
96
|
+
return offset + targetOffset;
|
|
97
|
+
}
|
|
98
|
+
offset += node.textContent.length;
|
|
99
|
+
}
|
|
100
|
+
return offset;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find text node + local offset for a given cumulative character offset
|
|
104
|
+
function getNodeAtCharOffset(container, charOffset) {
|
|
105
|
+
var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
|
|
106
|
+
var consumed = 0;
|
|
107
|
+
var node;
|
|
108
|
+
var lastNode = null;
|
|
109
|
+
while ((node = walker.nextNode())) {
|
|
110
|
+
lastNode = node;
|
|
111
|
+
var len = node.textContent.length;
|
|
112
|
+
if (consumed + len >= charOffset) {
|
|
113
|
+
return { node: node, offset: Math.min(charOffset - consumed, len) };
|
|
114
|
+
}
|
|
115
|
+
consumed += len;
|
|
116
|
+
}
|
|
117
|
+
if (lastNode) {
|
|
118
|
+
return { node: lastNode, offset: lastNode.textContent.length };
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Find parent [data-turn] element from a DOM node
|
|
124
|
+
function findParentTurn(node) {
|
|
125
|
+
var messagesEl = _ctx.messagesEl;
|
|
126
|
+
var el = node.nodeType === 3 ? node.parentElement : node;
|
|
127
|
+
while (el && el !== messagesEl) {
|
|
128
|
+
if (el.dataset && el.dataset.turn != null) return el;
|
|
129
|
+
el = el.parentElement;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function clearRemoteSelection(userId) {
|
|
135
|
+
var sel = remoteSelections[userId];
|
|
136
|
+
if (!sel) return;
|
|
137
|
+
for (var i = 0; i < sel.els.length; i++) {
|
|
138
|
+
if (sel.els[i].parentNode) sel.els[i].parentNode.removeChild(sel.els[i]);
|
|
139
|
+
}
|
|
140
|
+
sel.els = [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createOffscreenIndicator(userId, displayName, color) {
|
|
144
|
+
var btn = document.createElement("button");
|
|
145
|
+
btn.className = "remote-cursor-offscreen";
|
|
146
|
+
btn.dataset.userId = userId;
|
|
147
|
+
btn.style.cssText =
|
|
148
|
+
"position:absolute;left:50%;transform:translateX(-50%);" +
|
|
149
|
+
"z-index:10000;display:none;cursor:pointer;border:none;outline:none;" +
|
|
150
|
+
"background:" + color + ";color:#fff;font-size:11px;font-weight:500;" +
|
|
151
|
+
"padding:3px 10px 3px 8px;border-radius:12px;white-space:nowrap;" +
|
|
152
|
+
"font-family:inherit;line-height:16px;opacity:0.9;" +
|
|
153
|
+
"box-shadow:0 2px 8px rgba(0,0,0,0.2);pointer-events:auto;" +
|
|
154
|
+
"transition:opacity 0.15s;";
|
|
155
|
+
btn.addEventListener("mouseenter", function () { btn.style.opacity = "1"; });
|
|
156
|
+
btn.addEventListener("mouseleave", function () { btn.style.opacity = "0.9"; });
|
|
157
|
+
return btn;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function updateCursorVisibility(entry) {
|
|
161
|
+
var messagesEl = _ctx.messagesEl;
|
|
162
|
+
var visibleTop = messagesEl.scrollTop;
|
|
163
|
+
var visibleBottom = visibleTop + messagesEl.clientHeight;
|
|
164
|
+
var y = entry.lastY || 0;
|
|
165
|
+
|
|
166
|
+
if (y < visibleTop) {
|
|
167
|
+
entry.indicator.style.top = (visibleTop + 6) + "px";
|
|
168
|
+
entry.indicator.style.display = "";
|
|
169
|
+
} else if (y > visibleBottom) {
|
|
170
|
+
entry.indicator.style.top = (visibleBottom - 28) + "px";
|
|
171
|
+
entry.indicator.style.display = "";
|
|
172
|
+
} else {
|
|
173
|
+
entry.indicator.style.display = "none";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Find the closest [data-turn] element to a given clientY
|
|
178
|
+
function findClosestTurn(clientY) {
|
|
179
|
+
var messagesEl = _ctx.messagesEl;
|
|
180
|
+
var turns = messagesEl.querySelectorAll("[data-turn]");
|
|
181
|
+
if (!turns.length) return null;
|
|
182
|
+
// First: exact hit
|
|
183
|
+
for (var i = 0; i < turns.length; i++) {
|
|
184
|
+
var r = turns[i].getBoundingClientRect();
|
|
185
|
+
if (clientY >= r.top && clientY <= r.bottom) return turns[i];
|
|
186
|
+
}
|
|
187
|
+
// Second: closest by distance
|
|
188
|
+
var closest = null;
|
|
189
|
+
var closestDist = Infinity;
|
|
190
|
+
for (var j = 0; j < turns.length; j++) {
|
|
191
|
+
var rect = turns[j].getBoundingClientRect();
|
|
192
|
+
var mid = (rect.top + rect.bottom) / 2;
|
|
193
|
+
var dist = Math.abs(clientY - mid);
|
|
194
|
+
if (dist < closestDist) { closestDist = dist; closest = turns[j]; }
|
|
195
|
+
}
|
|
196
|
+
return closest;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Cursor sharing toggle button in user island (multi-user only)
|
|
200
|
+
export function initCursorToggle() {
|
|
201
|
+
if (!_ctx.isMultiUserMode) return;
|
|
202
|
+
var actionsEl = document.querySelector(".user-island-actions");
|
|
203
|
+
if (!actionsEl) return;
|
|
204
|
+
if (document.getElementById("cursor-share-toggle")) return;
|
|
205
|
+
|
|
206
|
+
var btn = document.createElement("button");
|
|
207
|
+
btn.id = "cursor-share-toggle";
|
|
208
|
+
btn.className = "cursor-share-btn";
|
|
209
|
+
btn.innerHTML = '<i data-lucide="mouse-pointer-2"></i>';
|
|
210
|
+
var settingsBtn = document.getElementById("user-settings-btn");
|
|
211
|
+
if (settingsBtn) {
|
|
212
|
+
actionsEl.insertBefore(btn, settingsBtn);
|
|
213
|
+
} else {
|
|
214
|
+
actionsEl.appendChild(btn);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function updateToggleStyle() {
|
|
218
|
+
if (cursorSharingEnabled) {
|
|
219
|
+
btn.classList.remove("off");
|
|
220
|
+
btn.classList.add("on");
|
|
221
|
+
_ctx.registerTooltip(btn, "Cursor sharing on");
|
|
222
|
+
} else {
|
|
223
|
+
btn.classList.remove("on");
|
|
224
|
+
btn.classList.add("off");
|
|
225
|
+
_ctx.registerTooltip(btn, "Cursor sharing off");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
updateToggleStyle();
|
|
230
|
+
lucide.createIcons({ nodes: [btn] });
|
|
231
|
+
|
|
232
|
+
btn.addEventListener("click", function () {
|
|
233
|
+
cursorSharingEnabled = !cursorSharingEnabled;
|
|
234
|
+
localStorage.setItem("cursorSharing", cursorSharingEnabled ? "on" : "off");
|
|
235
|
+
updateToggleStyle();
|
|
236
|
+
var ws = _ctx.ws;
|
|
237
|
+
if (!cursorSharingEnabled && ws && ws.readyState === 1) {
|
|
238
|
+
ws.send(JSON.stringify({ type: "cursor_leave" }));
|
|
239
|
+
ws.send(JSON.stringify({ type: "text_select", ranges: [] }));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// --- Exported functions ---
|
|
245
|
+
|
|
246
|
+
export function handleRemoteSelection(msg) {
|
|
247
|
+
var messagesEl = _ctx.messagesEl;
|
|
248
|
+
var userId = msg.userId;
|
|
249
|
+
var color = getCursorColor(userId);
|
|
250
|
+
|
|
251
|
+
if (!remoteSelections[userId]) {
|
|
252
|
+
remoteSelections[userId] = { els: [], timer: null };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Clear previous highlight
|
|
256
|
+
clearRemoteSelection(userId);
|
|
257
|
+
|
|
258
|
+
// If selection cleared, just remove
|
|
259
|
+
if (!msg.ranges || msg.ranges.length === 0) return;
|
|
260
|
+
|
|
261
|
+
var containerRect = messagesEl.getBoundingClientRect();
|
|
262
|
+
|
|
263
|
+
for (var r = 0; r < msg.ranges.length; r++) {
|
|
264
|
+
var sel = msg.ranges[r];
|
|
265
|
+
var startTurnEl = messagesEl.querySelector('[data-turn="' + sel.startTurn + '"]');
|
|
266
|
+
var endTurnEl = messagesEl.querySelector('[data-turn="' + sel.endTurn + '"]');
|
|
267
|
+
if (!startTurnEl || !endTurnEl) continue;
|
|
268
|
+
|
|
269
|
+
var startResult = getNodeAtCharOffset(startTurnEl, sel.startCh);
|
|
270
|
+
var endResult = getNodeAtCharOffset(endTurnEl, sel.endCh);
|
|
271
|
+
if (!startResult || !endResult) continue;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
var range = document.createRange();
|
|
275
|
+
range.setStart(startResult.node, startResult.offset);
|
|
276
|
+
range.setEnd(endResult.node, endResult.offset);
|
|
277
|
+
var rects = range.getClientRects();
|
|
278
|
+
|
|
279
|
+
for (var i = 0; i < rects.length; i++) {
|
|
280
|
+
var rect = rects[i];
|
|
281
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
282
|
+
var highlight = document.createElement("div");
|
|
283
|
+
highlight.className = "remote-selection";
|
|
284
|
+
highlight.dataset.userId = userId;
|
|
285
|
+
highlight.style.cssText =
|
|
286
|
+
"position:absolute;pointer-events:none;z-index:9998;" +
|
|
287
|
+
"background:" + color + ";" +
|
|
288
|
+
"opacity:0.2;" +
|
|
289
|
+
"border-radius:2px;" +
|
|
290
|
+
"left:" + (rect.left - containerRect.left + messagesEl.scrollLeft) + "px;" +
|
|
291
|
+
"top:" + (rect.top - containerRect.top + messagesEl.scrollTop) + "px;" +
|
|
292
|
+
"width:" + rect.width + "px;" +
|
|
293
|
+
"height:" + rect.height + "px;";
|
|
294
|
+
messagesEl.appendChild(highlight);
|
|
295
|
+
remoteSelections[userId].els.push(highlight);
|
|
296
|
+
}
|
|
297
|
+
} catch (e) {}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Auto-hide after timeout
|
|
301
|
+
if (remoteSelections[userId].timer) clearTimeout(remoteSelections[userId].timer);
|
|
302
|
+
remoteSelections[userId].timer = setTimeout(function () {
|
|
303
|
+
clearRemoteSelection(userId);
|
|
304
|
+
}, 10000);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function handleRemoteCursorMove(msg) {
|
|
308
|
+
var messagesEl = _ctx.messagesEl;
|
|
309
|
+
var userId = msg.userId;
|
|
310
|
+
|
|
311
|
+
var entry = remoteCursors[userId];
|
|
312
|
+
if (!entry) {
|
|
313
|
+
var color = getCursorColor(userId);
|
|
314
|
+
var el = createCursorElement(userId, msg.displayName, color, msg.avatarStyle, msg.avatarSeed, msg.avatarCustom);
|
|
315
|
+
messagesEl.appendChild(el);
|
|
316
|
+
var indicator = createOffscreenIndicator(userId, msg.displayName, color);
|
|
317
|
+
messagesEl.appendChild(indicator);
|
|
318
|
+
entry = { el: el, indicator: indicator, timer: null, lastY: 0, active: false };
|
|
319
|
+
remoteCursors[userId] = entry;
|
|
320
|
+
|
|
321
|
+
indicator.addEventListener("click", function () {
|
|
322
|
+
messagesEl.scrollTo({ top: entry.lastY - messagesEl.clientHeight / 2, behavior: "smooth" });
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Find the same turn element on this screen
|
|
327
|
+
var anchorEl = null;
|
|
328
|
+
if (msg.turn != null) {
|
|
329
|
+
anchorEl = messagesEl.querySelector('[data-turn="' + msg.turn + '"]');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (anchorEl && msg.rx != null && msg.ry != null) {
|
|
333
|
+
var x = anchorEl.offsetLeft + msg.rx * anchorEl.offsetWidth;
|
|
334
|
+
var y = anchorEl.offsetTop + msg.ry * anchorEl.offsetHeight;
|
|
335
|
+
entry.lastY = y;
|
|
336
|
+
entry.active = true;
|
|
337
|
+
|
|
338
|
+
// Update indicator label (direction set by updateCursorVisibility)
|
|
339
|
+
entry.indicator.textContent = (y < messagesEl.scrollTop ? "▲ " : "▼ ") + (msg.displayName || userId);
|
|
340
|
+
|
|
341
|
+
entry.el.style.left = x + "px";
|
|
342
|
+
entry.el.style.top = y + "px";
|
|
343
|
+
entry.el.style.display = "";
|
|
344
|
+
|
|
345
|
+
updateCursorVisibility(entry);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Reset hide timer
|
|
349
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
350
|
+
entry.timer = setTimeout(function () {
|
|
351
|
+
entry.el.style.display = "none";
|
|
352
|
+
entry.indicator.style.display = "none";
|
|
353
|
+
entry.active = false;
|
|
354
|
+
}, CURSOR_HIDE_TIMEOUT);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function handleRemoteCursorLeave(msg) {
|
|
358
|
+
var entry = remoteCursors[msg.userId];
|
|
359
|
+
if (entry) {
|
|
360
|
+
entry.el.style.display = "none";
|
|
361
|
+
entry.indicator.style.display = "none";
|
|
362
|
+
entry.active = false;
|
|
363
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function clearRemoteCursors() {
|
|
368
|
+
for (var uid in remoteCursors) {
|
|
369
|
+
var entry = remoteCursors[uid];
|
|
370
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
371
|
+
if (entry.el.parentNode) entry.el.parentNode.removeChild(entry.el);
|
|
372
|
+
if (entry.indicator && entry.indicator.parentNode) entry.indicator.parentNode.removeChild(entry.indicator);
|
|
373
|
+
}
|
|
374
|
+
remoteCursors = {};
|
|
375
|
+
for (var uid2 in remoteSelections) {
|
|
376
|
+
clearRemoteSelection(uid2);
|
|
377
|
+
if (remoteSelections[uid2].timer) clearTimeout(remoteSelections[uid2].timer);
|
|
378
|
+
}
|
|
379
|
+
remoteSelections = {};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function initCursors(ctx) {
|
|
383
|
+
_ctx = ctx;
|
|
384
|
+
var messagesEl = _ctx.messagesEl;
|
|
385
|
+
|
|
386
|
+
initCursorToggle();
|
|
387
|
+
|
|
388
|
+
// Track local cursor and send to server
|
|
389
|
+
messagesEl.addEventListener("mousemove", function (e) {
|
|
390
|
+
if (!cursorSharingEnabled) return;
|
|
391
|
+
var ws = _ctx.ws;
|
|
392
|
+
if (!ws || ws.readyState !== 1) return;
|
|
393
|
+
if (cursorThrottleTimer) return;
|
|
394
|
+
cursorThrottleTimer = setTimeout(function () { cursorThrottleTimer = null; }, CURSOR_THROTTLE_MS);
|
|
395
|
+
|
|
396
|
+
// Find which turn element the cursor is over
|
|
397
|
+
var turnEl = findClosestTurn(e.clientY);
|
|
398
|
+
if (!turnEl) return;
|
|
399
|
+
|
|
400
|
+
// Calculate ratio within the turn element
|
|
401
|
+
var turnRect = turnEl.getBoundingClientRect();
|
|
402
|
+
var rx = turnRect.width > 0 ? (e.clientX - turnRect.left) / turnRect.width : 0;
|
|
403
|
+
var ry = turnRect.height > 0 ? (e.clientY - turnRect.top) / turnRect.height : 0;
|
|
404
|
+
|
|
405
|
+
ws.send(JSON.stringify({
|
|
406
|
+
type: "cursor_move",
|
|
407
|
+
turn: parseInt(turnEl.dataset.turn, 10),
|
|
408
|
+
rx: Math.max(0, Math.min(1, rx)),
|
|
409
|
+
ry: Math.max(0, Math.min(1, ry))
|
|
410
|
+
}));
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
messagesEl.addEventListener("mouseleave", function () {
|
|
414
|
+
if (!cursorSharingEnabled) return;
|
|
415
|
+
var ws = _ctx.ws;
|
|
416
|
+
if (!ws || ws.readyState !== 1) return;
|
|
417
|
+
ws.send(JSON.stringify({ type: "cursor_leave" }));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Update offscreen indicators on scroll
|
|
421
|
+
messagesEl.addEventListener("scroll", function () {
|
|
422
|
+
for (var uid in remoteCursors) {
|
|
423
|
+
var entry = remoteCursors[uid];
|
|
424
|
+
if (!entry.active) continue;
|
|
425
|
+
updateCursorVisibility(entry);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Track local text selection and send to server
|
|
430
|
+
document.addEventListener("selectionchange", function () {
|
|
431
|
+
if (!cursorSharingEnabled) return;
|
|
432
|
+
var ws = _ctx.ws;
|
|
433
|
+
if (!ws || ws.readyState !== 1) return;
|
|
434
|
+
if (selectionThrottleTimer) return;
|
|
435
|
+
selectionThrottleTimer = setTimeout(function () { selectionThrottleTimer = null; }, 100);
|
|
436
|
+
|
|
437
|
+
var sel = window.getSelection();
|
|
438
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) {
|
|
439
|
+
// Selection cleared
|
|
440
|
+
if (lastSelectionKey !== "") {
|
|
441
|
+
lastSelectionKey = "";
|
|
442
|
+
ws.send(JSON.stringify({ type: "text_select", ranges: [] }));
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
var ranges = [];
|
|
448
|
+
for (var i = 0; i < sel.rangeCount; i++) {
|
|
449
|
+
var range = sel.getRangeAt(i);
|
|
450
|
+
var startTurn = findParentTurn(range.startContainer);
|
|
451
|
+
var endTurn = findParentTurn(range.endContainer);
|
|
452
|
+
if (!startTurn || !endTurn) continue;
|
|
453
|
+
// Both must be inside messagesEl
|
|
454
|
+
if (!messagesEl.contains(startTurn)) continue;
|
|
455
|
+
|
|
456
|
+
var startCh = getCharOffset(startTurn, range.startContainer, range.startOffset);
|
|
457
|
+
var endCh = getCharOffset(endTurn, range.endContainer, range.endOffset);
|
|
458
|
+
|
|
459
|
+
ranges.push({
|
|
460
|
+
startTurn: parseInt(startTurn.dataset.turn, 10),
|
|
461
|
+
startCh: startCh,
|
|
462
|
+
endTurn: parseInt(endTurn.dataset.turn, 10),
|
|
463
|
+
endCh: endCh
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
var key = JSON.stringify(ranges);
|
|
468
|
+
if (key === lastSelectionKey) return;
|
|
469
|
+
lastSelectionKey = key;
|
|
470
|
+
|
|
471
|
+
ws.send(JSON.stringify({ type: "text_select", ranges: ranges }));
|
|
472
|
+
});
|
|
473
|
+
}
|