collabmd 0.1.20 → 0.1.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/README.md +4 -2
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.js +22 -0
- package/public/assets/js/chunks/{preview-render-compiler-UZ4SZDQQ.js → preview-render-compiler-6Y7TKXFJ.js} +1 -1
- package/public/assets/js/chunks/{quick-switcher-controller-J7I3CUET.js → quick-switcher-controller-A6SKH5HI.js} +1 -1
- package/public/assets/js/{excalidraw-editor-ZDYKMZOL.js → excalidraw-editor-TCIH6GJL.js} +1 -1
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +68 -68
- package/public/assets/js/preview-render-worker.js +15 -15
- package/src/client/application/app-shell/git-feature.js +57 -18
- package/src/client/application/app-shell/ui-feature.js +4 -0
- package/src/client/bootstrap/collabmd-app-shell.js +12 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +2 -1
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +181 -1
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/comment-ui-controller.js +192 -5
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/styles/style.css +46 -2
- package/src/domain/wiki-link-resolver.js +16 -5
- package/src/domain/workspace-change.js +68 -0
- package/src/domain/workspace-room.js +3 -0
- package/src/server/create-app-server.js +41 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +191 -22
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/infrastructure/git/responses.js +12 -19
- package/src/server/infrastructure/http/create-git-api-command-handler.js +53 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-request-handler.js +7 -0
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +58 -14
- package/src/server/infrastructure/http/create-vault-api-handler.js +3 -0
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +3 -1
- package/src/server/infrastructure/http/http-response.js +65 -28
- package/src/server/infrastructure/persistence/vault-file-store.js +163 -52
- package/src/server/infrastructure/workspace/file-system-sync-service.js +488 -0
- package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +551 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +0 -1
- package/public/assets/js/chunks/editor-session-TAV2VXTA.js +0 -22
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { WebsocketProvider } from 'y-websocket';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
|
|
4
|
+
import { normalizeWorkspaceEvent } from '../../domain/workspace-change.js';
|
|
5
|
+
import { WORKSPACE_ROOM_NAME } from '../../domain/workspace-room.js';
|
|
6
|
+
import { resolveWsBaseUrl } from '../domain/runtime-paths.js';
|
|
7
|
+
import { stopReconnectOnControlledClose } from './yjs-provider-reset-guard.js';
|
|
8
|
+
|
|
9
|
+
function createNode(entry) {
|
|
10
|
+
if (!entry?.path || !entry?.type) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (entry.nodeType === 'directory' || entry.type === 'directory') {
|
|
15
|
+
return {
|
|
16
|
+
children: [],
|
|
17
|
+
name: entry.name,
|
|
18
|
+
path: entry.path,
|
|
19
|
+
type: 'directory',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
name: entry.name,
|
|
25
|
+
path: entry.path,
|
|
26
|
+
type: entry.type,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sortNodes(nodes = []) {
|
|
31
|
+
nodes.sort((left, right) => {
|
|
32
|
+
if (left.type === 'directory' && right.type !== 'directory') return -1;
|
|
33
|
+
if (left.type !== 'directory' && right.type === 'directory') return 1;
|
|
34
|
+
return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
nodes.forEach((node) => {
|
|
38
|
+
if (Array.isArray(node.children)) {
|
|
39
|
+
sortNodes(node.children);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return nodes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toEntryMap(value) {
|
|
47
|
+
if (value instanceof Map) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return new Map(Object.entries(value ?? {}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sortPathsByDepth(values = [], direction = 'asc') {
|
|
55
|
+
const factor = direction === 'desc' ? -1 : 1;
|
|
56
|
+
return [...values].sort((left, right) => {
|
|
57
|
+
const depthDelta = left.split('/').length - right.split('/').length;
|
|
58
|
+
if (depthDelta !== 0) {
|
|
59
|
+
return depthDelta * factor;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return left.localeCompare(right, undefined, { sensitivity: 'base' }) * factor;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class WorkspaceTreeModel {
|
|
67
|
+
constructor() {
|
|
68
|
+
this.entriesByPath = new Map();
|
|
69
|
+
this.nodesByPath = new Map();
|
|
70
|
+
this.nodeParentPathByPath = new Map();
|
|
71
|
+
this.roots = [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
reset(rawEntries) {
|
|
75
|
+
this.entriesByPath = toEntryMap(rawEntries);
|
|
76
|
+
this.nodesByPath = new Map();
|
|
77
|
+
this.nodeParentPathByPath = new Map();
|
|
78
|
+
this.roots = [];
|
|
79
|
+
|
|
80
|
+
this.entriesByPath.forEach((entry) => {
|
|
81
|
+
const node = createNode(entry);
|
|
82
|
+
if (node) {
|
|
83
|
+
this.nodesByPath.set(entry.path, node);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
sortPathsByDepth(Array.from(this.nodesByPath.keys())).forEach((pathValue) => {
|
|
88
|
+
const node = this.nodesByPath.get(pathValue);
|
|
89
|
+
if (node) {
|
|
90
|
+
this.attachNode(pathValue, node, this.entriesByPath.get(pathValue)?.parentPath || '');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
sortNodes(this.roots);
|
|
95
|
+
return this.roots;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getTree() {
|
|
99
|
+
return this.roots;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
applyMapChanges(changes, entriesMap) {
|
|
103
|
+
const deletePaths = [];
|
|
104
|
+
const upsertPaths = [];
|
|
105
|
+
|
|
106
|
+
changes.forEach((change, pathValue) => {
|
|
107
|
+
if (change.action === 'delete') {
|
|
108
|
+
deletePaths.push(pathValue);
|
|
109
|
+
} else {
|
|
110
|
+
upsertPaths.push(pathValue);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
sortPathsByDepth(deletePaths, 'desc').forEach((pathValue) => {
|
|
115
|
+
this.removeEntry(pathValue);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
sortPathsByDepth(upsertPaths).forEach((pathValue) => {
|
|
119
|
+
this.upsertEntry(pathValue, entriesMap.get(pathValue));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
sortNodes(this.roots);
|
|
123
|
+
return this.roots;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
removeEntry(pathValue) {
|
|
127
|
+
const node = this.nodesByPath.get(pathValue);
|
|
128
|
+
if (!node) {
|
|
129
|
+
this.entriesByPath.delete(pathValue);
|
|
130
|
+
this.nodeParentPathByPath.delete(pathValue);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.detachNode(pathValue, node);
|
|
135
|
+
this.nodesByPath.delete(pathValue);
|
|
136
|
+
this.entriesByPath.delete(pathValue);
|
|
137
|
+
this.nodeParentPathByPath.delete(pathValue);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
upsertEntry(pathValue, entry) {
|
|
141
|
+
if (!entry?.path || !entry?.type) {
|
|
142
|
+
this.removeEntry(pathValue);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const nextNode = createNode(entry);
|
|
147
|
+
if (!nextNode) {
|
|
148
|
+
this.removeEntry(pathValue);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const existingNode = this.nodesByPath.get(pathValue);
|
|
153
|
+
let node = existingNode;
|
|
154
|
+
if (!node || (node.type === 'directory') !== (nextNode.type === 'directory')) {
|
|
155
|
+
if (node) {
|
|
156
|
+
this.detachNode(pathValue, node);
|
|
157
|
+
}
|
|
158
|
+
node = nextNode;
|
|
159
|
+
this.nodesByPath.set(pathValue, node);
|
|
160
|
+
} else {
|
|
161
|
+
node.name = nextNode.name;
|
|
162
|
+
node.path = nextNode.path;
|
|
163
|
+
node.type = nextNode.type;
|
|
164
|
+
if (node.type === 'directory' && !Array.isArray(node.children)) {
|
|
165
|
+
node.children = [];
|
|
166
|
+
}
|
|
167
|
+
if (node.type !== 'directory' && Array.isArray(node.children)) {
|
|
168
|
+
delete node.children;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.entriesByPath.set(pathValue, entry);
|
|
173
|
+
this.attachNode(pathValue, node, entry.parentPath || '');
|
|
174
|
+
|
|
175
|
+
if (node.type === 'directory') {
|
|
176
|
+
this.rehomeChildren(pathValue);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
detachNode(pathValue, node) {
|
|
181
|
+
const currentParentPath = this.nodeParentPathByPath.get(pathValue) || '';
|
|
182
|
+
const siblings = currentParentPath
|
|
183
|
+
? this.nodesByPath.get(currentParentPath)?.children
|
|
184
|
+
: this.roots;
|
|
185
|
+
const index = siblings?.indexOf?.(node) ?? -1;
|
|
186
|
+
if (index >= 0) {
|
|
187
|
+
siblings.splice(index, 1);
|
|
188
|
+
}
|
|
189
|
+
this.nodeParentPathByPath.delete(pathValue);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
attachNode(pathValue, node, requestedParentPath = '') {
|
|
193
|
+
const parentPath = this.nodesByPath.get(requestedParentPath)?.type === 'directory'
|
|
194
|
+
? requestedParentPath
|
|
195
|
+
: '';
|
|
196
|
+
const currentParentPath = this.nodeParentPathByPath.get(pathValue);
|
|
197
|
+
|
|
198
|
+
if (currentParentPath === parentPath) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (currentParentPath !== undefined) {
|
|
203
|
+
this.detachNode(pathValue, node);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const siblings = parentPath
|
|
207
|
+
? this.nodesByPath.get(parentPath)?.children
|
|
208
|
+
: this.roots;
|
|
209
|
+
if (!siblings.includes(node)) {
|
|
210
|
+
siblings.push(node);
|
|
211
|
+
}
|
|
212
|
+
this.nodeParentPathByPath.set(pathValue, parentPath);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
rehomeChildren(parentPath) {
|
|
216
|
+
this.entriesByPath.forEach((entry, pathValue) => {
|
|
217
|
+
if (entry?.parentPath !== parentPath) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const node = this.nodesByPath.get(pathValue);
|
|
222
|
+
if (node) {
|
|
223
|
+
this.attachNode(pathValue, node, parentPath);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class WorkspaceSyncClient {
|
|
230
|
+
constructor({
|
|
231
|
+
onTreeChange = () => {},
|
|
232
|
+
onWorkspaceEvent = () => {},
|
|
233
|
+
} = {}) {
|
|
234
|
+
this.onTreeChange = onTreeChange;
|
|
235
|
+
this.onWorkspaceEvent = onWorkspaceEvent;
|
|
236
|
+
this.ydoc = new Y.Doc();
|
|
237
|
+
this.entries = this.ydoc.getMap('entries');
|
|
238
|
+
this.events = this.ydoc.getArray('events');
|
|
239
|
+
this.provider = null;
|
|
240
|
+
this._didInitialSync = false;
|
|
241
|
+
this.seenEventIds = new Set();
|
|
242
|
+
this.treeModel = new WorkspaceTreeModel();
|
|
243
|
+
|
|
244
|
+
this.handleEntriesChange = (event) => {
|
|
245
|
+
if (!this._didInitialSync || !event) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.onTreeChange(this.treeModel.applyMapChanges(event.changes.keys, this.entries), {
|
|
250
|
+
changedPaths: Array.from(event.changes.keys.keys()),
|
|
251
|
+
reset: false,
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
this.handleEventsChange = () => {
|
|
255
|
+
if (!this._didInitialSync) {
|
|
256
|
+
this.primeEventCache();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.events.toArray().forEach((event) => {
|
|
261
|
+
const normalized = normalizeWorkspaceEvent(event);
|
|
262
|
+
if (!normalized || this.seenEventIds.has(normalized.id)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.seenEventIds.add(normalized.id);
|
|
267
|
+
this.onWorkspaceEvent(normalized);
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
connect() {
|
|
273
|
+
if (this.provider) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this._didInitialSync = false;
|
|
278
|
+
this.provider = new WebsocketProvider(resolveWsBaseUrl(), WORKSPACE_ROOM_NAME, this.ydoc, {
|
|
279
|
+
disableBc: true,
|
|
280
|
+
maxBackoffTime: 5000,
|
|
281
|
+
});
|
|
282
|
+
stopReconnectOnControlledClose(this.provider);
|
|
283
|
+
|
|
284
|
+
this.entries.observe(this.handleEntriesChange);
|
|
285
|
+
this.events.observe(this.handleEventsChange);
|
|
286
|
+
this.provider.on('sync', (isSynced) => {
|
|
287
|
+
if (!isSynced || this._didInitialSync) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this._didInitialSync = true;
|
|
292
|
+
this.primeEventCache();
|
|
293
|
+
this.onTreeChange(this.treeModel.reset(this.entries.toJSON()), {
|
|
294
|
+
changedPaths: [],
|
|
295
|
+
reset: true,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
primeEventCache() {
|
|
301
|
+
this.events.toArray().forEach((event) => {
|
|
302
|
+
const normalized = normalizeWorkspaceEvent(event);
|
|
303
|
+
if (normalized) {
|
|
304
|
+
this.seenEventIds.add(normalized.id);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
disconnect() {
|
|
310
|
+
this.entries.unobserve(this.handleEntriesChange);
|
|
311
|
+
this.events.unobserve(this.handleEventsChange);
|
|
312
|
+
this.provider?.disconnect();
|
|
313
|
+
this.provider?.destroy();
|
|
314
|
+
this.provider = null;
|
|
315
|
+
this._didInitialSync = false;
|
|
316
|
+
this.seenEventIds.clear();
|
|
317
|
+
this.treeModel.reset(new Map());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
destroy() {
|
|
321
|
+
this.disconnect();
|
|
322
|
+
this.ydoc.destroy();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -9,6 +9,9 @@ const COMMENT_CARD_WIDTH = 520;
|
|
|
9
9
|
const COMMENT_SELECTION_REVEAL_DELAY_MS = 150;
|
|
10
10
|
const COMMENT_SELECTION_CHIP_GAP = 12;
|
|
11
11
|
const COMMENT_CONTROL_SLOT_HEIGHT = 36;
|
|
12
|
+
const COMMENT_PREVIEW_RAIL_SLOT_HEIGHT = 30;
|
|
13
|
+
const COMMENT_PREVIEW_RAIL_MIN_WIDTH = 400;
|
|
14
|
+
const COMMENT_PREVIEW_RAIL_BREAKPOINT = 769;
|
|
12
15
|
const COMMENT_REACTION_PRESET_EMOJIS = Object.freeze(['👍', '❤️', '🎉', '👀', '🚀']);
|
|
13
16
|
const COMMENT_REACTION_MORE_EMOJIS = Object.freeze(['😂', '🔥', '✅', '🙏', '💡', '🤔', '👏', '😄', '🎯', '🙌']);
|
|
14
17
|
|
|
@@ -248,6 +251,25 @@ function createRectFromRects(rects = []) {
|
|
|
248
251
|
};
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
function pointIntersectsRect(x, y, rect) {
|
|
255
|
+
if (!rect) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return x >= rect.left
|
|
260
|
+
&& x <= rect.right
|
|
261
|
+
&& y >= rect.top
|
|
262
|
+
&& y <= rect.bottom;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeGroupKeys(keys = []) {
|
|
266
|
+
return [...new Set(keys.filter(Boolean))].sort();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function serializeGroupKeys(keys = []) {
|
|
270
|
+
return normalizeGroupKeys(keys).join(' ');
|
|
271
|
+
}
|
|
272
|
+
|
|
251
273
|
function createCommentMarkerContent(count) {
|
|
252
274
|
const fragment = document.createDocumentFragment();
|
|
253
275
|
|
|
@@ -315,6 +337,12 @@ export class CommentUiController {
|
|
|
315
337
|
this.pointerSelecting = false;
|
|
316
338
|
this.session = null;
|
|
317
339
|
this.activeCard = null;
|
|
340
|
+
this.hoveredEditorGroupKeys = [];
|
|
341
|
+
this.hoveredEditorGroupKeysSignature = '';
|
|
342
|
+
this.hoveredPreviewGroupKeys = [];
|
|
343
|
+
this.hoveredPreviewGroupKeysSignature = '';
|
|
344
|
+
this.previewHoverRegions = [];
|
|
345
|
+
this.lastPreviewPointerPosition = null;
|
|
318
346
|
this.editorLayer = null;
|
|
319
347
|
this.previewLayer = null;
|
|
320
348
|
this.previewHighlightLayer = null;
|
|
@@ -331,6 +359,20 @@ export class CommentUiController {
|
|
|
331
359
|
this.handleEditorScroll = () => this.scheduleLayoutRefresh();
|
|
332
360
|
this.handlePreviewScroll = () => this.scheduleLayoutRefresh();
|
|
333
361
|
this.handleWindowResize = () => this.scheduleLayoutRefresh();
|
|
362
|
+
this.handlePreviewPointerMove = (event) => {
|
|
363
|
+
this.lastPreviewPointerPosition = { x: event.clientX, y: event.clientY };
|
|
364
|
+
this.updateHoveredPreviewGroups(this.getPreviewGroupKeysAtPoint(event.clientX, event.clientY));
|
|
365
|
+
};
|
|
366
|
+
this.handlePreviewPointerLeave = () => {
|
|
367
|
+
this.lastPreviewPointerPosition = null;
|
|
368
|
+
this.updateHoveredPreviewGroups([]);
|
|
369
|
+
};
|
|
370
|
+
this.handlePreviewFocusIn = (event) => {
|
|
371
|
+
this.updateHoveredPreviewGroups(this.getPreviewGroupKeysForTarget(event.target));
|
|
372
|
+
};
|
|
373
|
+
this.handlePreviewFocusOut = (event) => {
|
|
374
|
+
this.updateHoveredPreviewGroups(this.getPreviewGroupKeysForTarget(event.relatedTarget));
|
|
375
|
+
};
|
|
334
376
|
this.handleCommentSelectionButtonPointerDown = (event) => {
|
|
335
377
|
event.preventDefault();
|
|
336
378
|
};
|
|
@@ -436,6 +478,10 @@ export class CommentUiController {
|
|
|
436
478
|
this.setDrawerOpen(!this.drawerOpen);
|
|
437
479
|
});
|
|
438
480
|
this.previewContainer?.addEventListener('scroll', this.handlePreviewScroll, { passive: true });
|
|
481
|
+
this.previewElement?.addEventListener('pointermove', this.handlePreviewPointerMove, { passive: true });
|
|
482
|
+
this.previewElement?.addEventListener('pointerleave', this.handlePreviewPointerLeave);
|
|
483
|
+
this.previewElement?.addEventListener('focusin', this.handlePreviewFocusIn);
|
|
484
|
+
this.previewElement?.addEventListener('focusout', this.handlePreviewFocusOut);
|
|
439
485
|
this.editorContainer?.addEventListener('pointerdown', this.handleEditorPointerDown);
|
|
440
486
|
this.editorContainer?.addEventListener('focusout', this.handleEditorFocusOut);
|
|
441
487
|
window.addEventListener('resize', this.handleWindowResize);
|
|
@@ -452,6 +498,10 @@ export class CommentUiController {
|
|
|
452
498
|
}
|
|
453
499
|
this.attachSession(null);
|
|
454
500
|
this.previewContainer?.removeEventListener('scroll', this.handlePreviewScroll);
|
|
501
|
+
this.previewElement?.removeEventListener('pointermove', this.handlePreviewPointerMove);
|
|
502
|
+
this.previewElement?.removeEventListener('pointerleave', this.handlePreviewPointerLeave);
|
|
503
|
+
this.previewElement?.removeEventListener('focusin', this.handlePreviewFocusIn);
|
|
504
|
+
this.previewElement?.removeEventListener('focusout', this.handlePreviewFocusOut);
|
|
455
505
|
this.commentSelectionButton?.removeEventListener('pointerdown', this.handleCommentSelectionButtonPointerDown);
|
|
456
506
|
this.editorContainer?.removeEventListener('pointerdown', this.handleEditorPointerDown);
|
|
457
507
|
this.editorContainer?.removeEventListener('focusout', this.handleEditorFocusOut);
|
|
@@ -460,6 +510,7 @@ export class CommentUiController {
|
|
|
460
510
|
document.removeEventListener('pointercancel', this.handleDocumentPointerUp);
|
|
461
511
|
document.removeEventListener('pointerdown', this.handleDocumentPointerDown);
|
|
462
512
|
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
|
513
|
+
this.previewHoverRegions = [];
|
|
463
514
|
this.cardRoot?.remove();
|
|
464
515
|
this.editorLayer?.remove();
|
|
465
516
|
this.previewLayer?.remove();
|
|
@@ -493,6 +544,10 @@ export class CommentUiController {
|
|
|
493
544
|
this.clearSelectionRevealTimer();
|
|
494
545
|
this.pointerSelecting = false;
|
|
495
546
|
this.activeCard = null;
|
|
547
|
+
this.updateHoveredEditorGroups([]);
|
|
548
|
+
this.updateHoveredPreviewGroups([]);
|
|
549
|
+
this.previewHoverRegions = [];
|
|
550
|
+
this.lastPreviewPointerPosition = null;
|
|
496
551
|
this.reactionPicker = null;
|
|
497
552
|
}
|
|
498
553
|
if (!this.supported) {
|
|
@@ -504,6 +559,10 @@ export class CommentUiController {
|
|
|
504
559
|
this.committedSelectionAnchor = null;
|
|
505
560
|
this.clearSelectionRevealTimer();
|
|
506
561
|
this.pointerSelecting = false;
|
|
562
|
+
this.updateHoveredEditorGroups([]);
|
|
563
|
+
this.updateHoveredPreviewGroups([]);
|
|
564
|
+
this.previewHoverRegions = [];
|
|
565
|
+
this.lastPreviewPointerPosition = null;
|
|
507
566
|
this.reactionPicker = null;
|
|
508
567
|
}
|
|
509
568
|
this.render();
|
|
@@ -757,11 +816,19 @@ export class CommentUiController {
|
|
|
757
816
|
}
|
|
758
817
|
|
|
759
818
|
const relativeRect = toRelativeRect(rect, containerRect);
|
|
819
|
+
if (relativeRect.bottom < 0 || relativeRect.top > containerRect.height) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
760
823
|
const button = document.createElement('button');
|
|
761
824
|
button.type = 'button';
|
|
762
825
|
button.className = 'comment-editor-badge';
|
|
763
826
|
button.dataset.count = String(group.threads.length);
|
|
764
|
-
|
|
827
|
+
const isActive = this.activeCard?.groupKey === group.key;
|
|
828
|
+
const isHovered = this.hoveredEditorGroupKeys.includes(group.key);
|
|
829
|
+
button.classList.toggle('is-active', isActive);
|
|
830
|
+
button.classList.toggle('is-hovered', isHovered);
|
|
831
|
+
button.classList.toggle('is-passive', !isActive && !isHovered);
|
|
765
832
|
button.setAttribute('aria-label', `${group.threads.length} comment thread${group.threads.length === 1 ? '' : 's'}`);
|
|
766
833
|
button.appendChild(createCommentMarkerContent(group.threads.length));
|
|
767
834
|
const top = Math.max(relativeRect.top, 8);
|
|
@@ -771,6 +838,18 @@ export class CommentUiController {
|
|
|
771
838
|
button.addEventListener('pointerdown', (event) => {
|
|
772
839
|
event.preventDefault();
|
|
773
840
|
});
|
|
841
|
+
button.addEventListener('pointerenter', () => {
|
|
842
|
+
this.updateHoveredEditorGroups([group.key]);
|
|
843
|
+
});
|
|
844
|
+
button.addEventListener('pointerleave', () => {
|
|
845
|
+
this.updateHoveredEditorGroups([]);
|
|
846
|
+
});
|
|
847
|
+
button.addEventListener('focusin', () => {
|
|
848
|
+
this.updateHoveredEditorGroups([group.key]);
|
|
849
|
+
});
|
|
850
|
+
button.addEventListener('focusout', () => {
|
|
851
|
+
this.updateHoveredEditorGroups([]);
|
|
852
|
+
});
|
|
774
853
|
button.addEventListener('click', () => {
|
|
775
854
|
this.openThreadGroup(group, {
|
|
776
855
|
anchor: group.anchor,
|
|
@@ -840,20 +919,36 @@ export class CommentUiController {
|
|
|
840
919
|
this.previewHighlightLayer?.replaceChildren();
|
|
841
920
|
|
|
842
921
|
if (!this.supported || !this.previewElement) {
|
|
922
|
+
this.previewHoverRegions = [];
|
|
843
923
|
return;
|
|
844
924
|
}
|
|
845
925
|
|
|
846
926
|
const previewRect = this.previewElement.getBoundingClientRect();
|
|
847
927
|
const groups = this.getThreadGroups();
|
|
928
|
+
const occupiedTops = [];
|
|
929
|
+
const hoverRegions = [];
|
|
930
|
+
const showPassiveMarkers = this.shouldRenderPassivePreviewMarkers();
|
|
848
931
|
groups.forEach((group) => {
|
|
849
932
|
const target = this.resolvePreviewTarget(group.anchor);
|
|
850
933
|
if (!target?.bubbleRect) {
|
|
851
934
|
return;
|
|
852
935
|
}
|
|
853
936
|
|
|
937
|
+
hoverRegions.push({
|
|
938
|
+
key: group.key,
|
|
939
|
+
rects: target.hoverRects?.length > 0 ? target.hoverRects : [target.bubbleRect],
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
const isActive = this.activeCard?.groupKey === group.key;
|
|
943
|
+
const isHovered = this.hoveredPreviewGroupKeys.includes(group.key);
|
|
944
|
+
const isEmphasized = isActive || isHovered;
|
|
945
|
+
|
|
854
946
|
target.highlightRects?.forEach((rect) => {
|
|
855
947
|
const highlight = document.createElement('div');
|
|
856
948
|
highlight.className = 'comment-preview-highlight';
|
|
949
|
+
highlight.classList.toggle('is-active', isActive);
|
|
950
|
+
highlight.classList.toggle('is-hovered', isHovered);
|
|
951
|
+
highlight.classList.toggle('is-passive', !isEmphasized);
|
|
857
952
|
highlight.style.left = `${rect.left - previewRect.left}px`;
|
|
858
953
|
highlight.style.top = `${rect.top - previewRect.top}px`;
|
|
859
954
|
highlight.style.width = `${rect.width}px`;
|
|
@@ -861,14 +956,32 @@ export class CommentUiController {
|
|
|
861
956
|
this.previewHighlightLayer?.appendChild(highlight);
|
|
862
957
|
});
|
|
863
958
|
|
|
959
|
+
if (!showPassiveMarkers && !isEmphasized) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
864
963
|
const bubble = document.createElement('button');
|
|
865
964
|
bubble.type = 'button';
|
|
866
965
|
bubble.className = 'comment-preview-badge';
|
|
867
|
-
bubble.
|
|
966
|
+
bubble.dataset.commentPreviewGroupKeys = group.key;
|
|
967
|
+
bubble.classList.toggle('is-active', isActive);
|
|
968
|
+
bubble.classList.toggle('is-hovered', isHovered);
|
|
969
|
+
bubble.classList.toggle('is-passive', !isEmphasized);
|
|
868
970
|
bubble.setAttribute('aria-label', `${group.threads.length} comment thread${group.threads.length === 1 ? '' : 's'}`);
|
|
869
971
|
bubble.appendChild(createCommentMarkerContent(group.threads.length));
|
|
870
|
-
|
|
871
|
-
|
|
972
|
+
let bubbleTop = clamp(
|
|
973
|
+
target.bubbleRect.top - previewRect.top,
|
|
974
|
+
6,
|
|
975
|
+
Math.max(this.previewElement.clientHeight - COMMENT_PREVIEW_RAIL_SLOT_HEIGHT, 6),
|
|
976
|
+
);
|
|
977
|
+
while (occupiedTops.some((top) => Math.abs(top - bubbleTop) < (COMMENT_PREVIEW_RAIL_SLOT_HEIGHT - 4))) {
|
|
978
|
+
bubbleTop = clamp(
|
|
979
|
+
bubbleTop + COMMENT_PREVIEW_RAIL_SLOT_HEIGHT,
|
|
980
|
+
6,
|
|
981
|
+
Math.max(this.previewElement.clientHeight - COMMENT_PREVIEW_RAIL_SLOT_HEIGHT, 6),
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
bubble.style.top = `${bubbleTop}px`;
|
|
872
985
|
bubble.title = `${group.threads.length} comment${group.threads.length === 1 ? '' : 's'}`;
|
|
873
986
|
bubble.addEventListener('pointerdown', (event) => {
|
|
874
987
|
event.preventDefault();
|
|
@@ -877,11 +990,19 @@ export class CommentUiController {
|
|
|
877
990
|
this.openThreadGroup(group, {
|
|
878
991
|
anchor: group.anchor,
|
|
879
992
|
origin: 'preview',
|
|
880
|
-
sourceRect:
|
|
993
|
+
sourceRect: bubble.getBoundingClientRect(),
|
|
881
994
|
});
|
|
882
995
|
});
|
|
883
996
|
this.previewLayer?.appendChild(bubble);
|
|
997
|
+
occupiedTops.push(bubbleTop);
|
|
884
998
|
});
|
|
999
|
+
|
|
1000
|
+
this.previewHoverRegions = hoverRegions;
|
|
1001
|
+
if (this.lastPreviewPointerPosition) {
|
|
1002
|
+
this.updateHoveredPreviewGroups(
|
|
1003
|
+
this.getPreviewGroupKeysAtPoint(this.lastPreviewPointerPosition.x, this.lastPreviewPointerPosition.y),
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
885
1006
|
}
|
|
886
1007
|
|
|
887
1008
|
resolvePreviewTarget(anchor) {
|
|
@@ -895,6 +1016,7 @@ export class CommentUiController {
|
|
|
895
1016
|
return {
|
|
896
1017
|
bubbleRect: diagramShell.getBoundingClientRect(),
|
|
897
1018
|
highlightRects: [],
|
|
1019
|
+
hoverRects: [diagramShell.getBoundingClientRect()],
|
|
898
1020
|
};
|
|
899
1021
|
}
|
|
900
1022
|
|
|
@@ -911,6 +1033,7 @@ export class CommentUiController {
|
|
|
911
1033
|
return {
|
|
912
1034
|
bubbleRect,
|
|
913
1035
|
highlightRects: rects,
|
|
1036
|
+
hoverRects: rects,
|
|
914
1037
|
};
|
|
915
1038
|
}
|
|
916
1039
|
}
|
|
@@ -923,6 +1046,7 @@ export class CommentUiController {
|
|
|
923
1046
|
return {
|
|
924
1047
|
bubbleRect: fallback.getBoundingClientRect(),
|
|
925
1048
|
highlightRects: [],
|
|
1049
|
+
hoverRects: [fallback.getBoundingClientRect()],
|
|
926
1050
|
};
|
|
927
1051
|
}
|
|
928
1052
|
|
|
@@ -1115,6 +1239,69 @@ export class CommentUiController {
|
|
|
1115
1239
|
this.scheduleLayoutRefresh();
|
|
1116
1240
|
}
|
|
1117
1241
|
|
|
1242
|
+
getPreviewGroupKeysForTarget(target) {
|
|
1243
|
+
if (!(target instanceof Node)) {
|
|
1244
|
+
return [];
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const keyCarrier = target.closest?.('[data-comment-preview-group-keys]');
|
|
1248
|
+
return serializeGroupKeys(
|
|
1249
|
+
String(keyCarrier?.dataset?.commentPreviewGroupKeys ?? '')
|
|
1250
|
+
.split(/\s+/)
|
|
1251
|
+
.filter(Boolean),
|
|
1252
|
+
).split(' ').filter(Boolean);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
getPreviewGroupKeysAtPoint(x, y) {
|
|
1256
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1257
|
+
return [];
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const targetAtPoint = document.elementFromPoint(x, y);
|
|
1261
|
+
const targetKeys = this.getPreviewGroupKeysForTarget(targetAtPoint);
|
|
1262
|
+
if (targetKeys.length > 0) {
|
|
1263
|
+
return targetKeys;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const matchingKeys = this.previewHoverRegions
|
|
1267
|
+
.filter((region) => region.rects.some((rect) => pointIntersectsRect(x, y, rect)))
|
|
1268
|
+
.map((region) => region.key);
|
|
1269
|
+
return normalizeGroupKeys(matchingKeys);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
updateHoveredPreviewGroups(nextKeys = []) {
|
|
1273
|
+
const normalizedKeys = normalizeGroupKeys(nextKeys);
|
|
1274
|
+
const signature = normalizedKeys.join(' ');
|
|
1275
|
+
if (signature === this.hoveredPreviewGroupKeysSignature) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
this.hoveredPreviewGroupKeys = normalizedKeys;
|
|
1280
|
+
this.hoveredPreviewGroupKeysSignature = signature;
|
|
1281
|
+
this.scheduleLayoutRefresh();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
updateHoveredEditorGroups(nextKeys = []) {
|
|
1285
|
+
const normalizedKeys = normalizeGroupKeys(nextKeys);
|
|
1286
|
+
const signature = normalizedKeys.join(' ');
|
|
1287
|
+
if (signature === this.hoveredEditorGroupKeysSignature) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
this.hoveredEditorGroupKeys = normalizedKeys;
|
|
1292
|
+
this.hoveredEditorGroupKeysSignature = signature;
|
|
1293
|
+
this.scheduleLayoutRefresh();
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
syncHoveredPreviewGroupsFromTarget(target) {
|
|
1297
|
+
this.updateHoveredPreviewGroups(this.getPreviewGroupKeysForTarget(target));
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
shouldRenderPassivePreviewMarkers() {
|
|
1301
|
+
const previewWidth = this.previewContainer?.clientWidth ?? this.previewElement?.clientWidth ?? 0;
|
|
1302
|
+
return window.innerWidth >= COMMENT_PREVIEW_RAIL_BREAKPOINT && previewWidth >= COMMENT_PREVIEW_RAIL_MIN_WIDTH;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1118
1305
|
updateReactionPickerPosition(card) {
|
|
1119
1306
|
const bounds = getReactionPickerBounds(card);
|
|
1120
1307
|
if (!bounds) {
|
|
@@ -54,13 +54,20 @@ export class FileExplorerController {
|
|
|
54
54
|
async refresh() {
|
|
55
55
|
try {
|
|
56
56
|
const data = await this.vaultClient.readTree();
|
|
57
|
-
this.
|
|
58
|
-
this.renderTree();
|
|
57
|
+
this.setTree(data.tree || []);
|
|
59
58
|
} catch (error) {
|
|
60
59
|
console.error('[explorer] Failed to load file tree:', error.message);
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
setTree(tree, {
|
|
64
|
+
changedPaths = null,
|
|
65
|
+
reset = false,
|
|
66
|
+
} = {}) {
|
|
67
|
+
this.state.setTree(tree);
|
|
68
|
+
this.renderTree({ changedPaths, reset });
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
setActiveFile(filePath) {
|
|
65
72
|
this.state.setActiveFile(filePath);
|
|
66
73
|
this.renderTree();
|
|
@@ -74,10 +81,15 @@ export class FileExplorerController {
|
|
|
74
81
|
return this.state.flatFiles.filter((path) => !isImageAttachmentFilePath(path));
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
renderTree(
|
|
84
|
+
renderTree({
|
|
85
|
+
changedPaths = null,
|
|
86
|
+
reset = false,
|
|
87
|
+
} = {}) {
|
|
78
88
|
this.view.render({
|
|
79
89
|
activeFilePath: this.state.activeFilePath,
|
|
90
|
+
changedPaths,
|
|
80
91
|
expandedDirs: this.state.expandedDirs,
|
|
92
|
+
reset,
|
|
81
93
|
searchMatches: this.state.getSearchMatches(),
|
|
82
94
|
searchQuery: this.state.searchQuery,
|
|
83
95
|
tree: this.state.tree,
|