collabmd 0.1.19 → 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 +13 -11
- package/docker-compose.yml +1 -1
- 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 +102 -75
- package/public/assets/js/preview-render-worker.js +15 -15
- package/public/index.html +14 -4
- package/src/client/application/app-shell/git-feature.js +86 -19
- package/src/client/application/app-shell/ui-feature.js +4 -0
- package/src/client/application/app-shell-elements.js +1 -0
- package/src/client/bootstrap/collabmd-app-shell.js +16 -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 +245 -7
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/backlinks-panel.js +157 -101
- package/src/client/presentation/comment-ui-controller.js +233 -10
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/presentation/git-panel-controller.js +73 -0
- package/src/client/presentation/outline-controller.js +25 -0
- package/src/client/styles/style.css +184 -9
- 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/errors.js +4 -1
- package/src/server/infrastructure/git/git-service.js +215 -1
- package/src/server/infrastructure/git/responses.js +12 -19
- package/src/server/infrastructure/http/create-git-api-command-handler.js +58 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +16 -1
- 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/pull-backup-store.js +283 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +179 -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-AH6Z3MXW.js +0 -22
|
@@ -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
|
|
|
@@ -279,6 +301,7 @@ export class CommentUiController {
|
|
|
279
301
|
commentsDrawerList,
|
|
280
302
|
commentsToggleButton,
|
|
281
303
|
editorContainer,
|
|
304
|
+
onWillOpenDrawer,
|
|
282
305
|
onCreateThread,
|
|
283
306
|
onNavigateToLine,
|
|
284
307
|
onReplyToThread,
|
|
@@ -293,6 +316,7 @@ export class CommentUiController {
|
|
|
293
316
|
this.commentsDrawerList = commentsDrawerList;
|
|
294
317
|
this.commentsToggleButton = commentsToggleButton;
|
|
295
318
|
this.editorContainer = editorContainer;
|
|
319
|
+
this.onWillOpenDrawer = onWillOpenDrawer;
|
|
296
320
|
this.onCreateThread = onCreateThread;
|
|
297
321
|
this.onNavigateToLine = onNavigateToLine;
|
|
298
322
|
this.onReplyToThread = onReplyToThread;
|
|
@@ -313,6 +337,12 @@ export class CommentUiController {
|
|
|
313
337
|
this.pointerSelecting = false;
|
|
314
338
|
this.session = null;
|
|
315
339
|
this.activeCard = null;
|
|
340
|
+
this.hoveredEditorGroupKeys = [];
|
|
341
|
+
this.hoveredEditorGroupKeysSignature = '';
|
|
342
|
+
this.hoveredPreviewGroupKeys = [];
|
|
343
|
+
this.hoveredPreviewGroupKeysSignature = '';
|
|
344
|
+
this.previewHoverRegions = [];
|
|
345
|
+
this.lastPreviewPointerPosition = null;
|
|
316
346
|
this.editorLayer = null;
|
|
317
347
|
this.previewLayer = null;
|
|
318
348
|
this.previewHighlightLayer = null;
|
|
@@ -329,6 +359,20 @@ export class CommentUiController {
|
|
|
329
359
|
this.handleEditorScroll = () => this.scheduleLayoutRefresh();
|
|
330
360
|
this.handlePreviewScroll = () => this.scheduleLayoutRefresh();
|
|
331
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
|
+
};
|
|
332
376
|
this.handleCommentSelectionButtonPointerDown = (event) => {
|
|
333
377
|
event.preventDefault();
|
|
334
378
|
};
|
|
@@ -384,13 +428,30 @@ export class CommentUiController {
|
|
|
384
428
|
this.scheduleLayoutRefresh();
|
|
385
429
|
};
|
|
386
430
|
this.handleDocumentPointerDown = (event) => {
|
|
387
|
-
|
|
431
|
+
const target = event.target;
|
|
432
|
+
|
|
433
|
+
if (this.activeCard && this.cardRoot) {
|
|
434
|
+
if (this.cardRoot.contains(target)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
this.closeCard();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!this.drawerOpen) {
|
|
388
441
|
return;
|
|
389
442
|
}
|
|
390
|
-
|
|
443
|
+
|
|
444
|
+
if (
|
|
445
|
+
target instanceof Node
|
|
446
|
+
&& (
|
|
447
|
+
this.commentsDrawer?.contains(target)
|
|
448
|
+
|| this.commentsToggleButton?.contains(target)
|
|
449
|
+
)
|
|
450
|
+
) {
|
|
391
451
|
return;
|
|
392
452
|
}
|
|
393
|
-
|
|
453
|
+
|
|
454
|
+
this.closeDrawer();
|
|
394
455
|
};
|
|
395
456
|
this.handleDocumentKeyDown = (event) => {
|
|
396
457
|
if (event.key === 'Escape' && this.reactionPicker) {
|
|
@@ -414,10 +475,13 @@ export class CommentUiController {
|
|
|
414
475
|
this.openComposerForSelection('toolbar');
|
|
415
476
|
});
|
|
416
477
|
this.commentsToggleButton?.addEventListener('click', () => {
|
|
417
|
-
this.
|
|
418
|
-
this.render();
|
|
478
|
+
this.setDrawerOpen(!this.drawerOpen);
|
|
419
479
|
});
|
|
420
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);
|
|
421
485
|
this.editorContainer?.addEventListener('pointerdown', this.handleEditorPointerDown);
|
|
422
486
|
this.editorContainer?.addEventListener('focusout', this.handleEditorFocusOut);
|
|
423
487
|
window.addEventListener('resize', this.handleWindowResize);
|
|
@@ -434,6 +498,10 @@ export class CommentUiController {
|
|
|
434
498
|
}
|
|
435
499
|
this.attachSession(null);
|
|
436
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);
|
|
437
505
|
this.commentSelectionButton?.removeEventListener('pointerdown', this.handleCommentSelectionButtonPointerDown);
|
|
438
506
|
this.editorContainer?.removeEventListener('pointerdown', this.handleEditorPointerDown);
|
|
439
507
|
this.editorContainer?.removeEventListener('focusout', this.handleEditorFocusOut);
|
|
@@ -442,6 +510,7 @@ export class CommentUiController {
|
|
|
442
510
|
document.removeEventListener('pointercancel', this.handleDocumentPointerUp);
|
|
443
511
|
document.removeEventListener('pointerdown', this.handleDocumentPointerDown);
|
|
444
512
|
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
|
513
|
+
this.previewHoverRegions = [];
|
|
445
514
|
this.cardRoot?.remove();
|
|
446
515
|
this.editorLayer?.remove();
|
|
447
516
|
this.previewLayer?.remove();
|
|
@@ -475,6 +544,10 @@ export class CommentUiController {
|
|
|
475
544
|
this.clearSelectionRevealTimer();
|
|
476
545
|
this.pointerSelecting = false;
|
|
477
546
|
this.activeCard = null;
|
|
547
|
+
this.updateHoveredEditorGroups([]);
|
|
548
|
+
this.updateHoveredPreviewGroups([]);
|
|
549
|
+
this.previewHoverRegions = [];
|
|
550
|
+
this.lastPreviewPointerPosition = null;
|
|
478
551
|
this.reactionPicker = null;
|
|
479
552
|
}
|
|
480
553
|
if (!this.supported) {
|
|
@@ -486,6 +559,10 @@ export class CommentUiController {
|
|
|
486
559
|
this.committedSelectionAnchor = null;
|
|
487
560
|
this.clearSelectionRevealTimer();
|
|
488
561
|
this.pointerSelecting = false;
|
|
562
|
+
this.updateHoveredEditorGroups([]);
|
|
563
|
+
this.updateHoveredPreviewGroups([]);
|
|
564
|
+
this.previewHoverRegions = [];
|
|
565
|
+
this.lastPreviewPointerPosition = null;
|
|
489
566
|
this.reactionPicker = null;
|
|
490
567
|
}
|
|
491
568
|
this.render();
|
|
@@ -569,6 +646,24 @@ export class CommentUiController {
|
|
|
569
646
|
this.render();
|
|
570
647
|
}
|
|
571
648
|
|
|
649
|
+
setDrawerOpen(nextState) {
|
|
650
|
+
const normalizedState = Boolean(nextState);
|
|
651
|
+
if (normalizedState === this.drawerOpen) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (normalizedState) {
|
|
656
|
+
this.onWillOpenDrawer?.();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
this.drawerOpen = normalizedState;
|
|
660
|
+
this.render();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
closeDrawer() {
|
|
664
|
+
this.setDrawerOpen(false);
|
|
665
|
+
}
|
|
666
|
+
|
|
572
667
|
refreshLayout() {
|
|
573
668
|
this.renderEditorLayer();
|
|
574
669
|
this.renderPreviewLayer();
|
|
@@ -721,11 +816,19 @@ export class CommentUiController {
|
|
|
721
816
|
}
|
|
722
817
|
|
|
723
818
|
const relativeRect = toRelativeRect(rect, containerRect);
|
|
819
|
+
if (relativeRect.bottom < 0 || relativeRect.top > containerRect.height) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
724
823
|
const button = document.createElement('button');
|
|
725
824
|
button.type = 'button';
|
|
726
825
|
button.className = 'comment-editor-badge';
|
|
727
826
|
button.dataset.count = String(group.threads.length);
|
|
728
|
-
|
|
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);
|
|
729
832
|
button.setAttribute('aria-label', `${group.threads.length} comment thread${group.threads.length === 1 ? '' : 's'}`);
|
|
730
833
|
button.appendChild(createCommentMarkerContent(group.threads.length));
|
|
731
834
|
const top = Math.max(relativeRect.top, 8);
|
|
@@ -735,6 +838,18 @@ export class CommentUiController {
|
|
|
735
838
|
button.addEventListener('pointerdown', (event) => {
|
|
736
839
|
event.preventDefault();
|
|
737
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
|
+
});
|
|
738
853
|
button.addEventListener('click', () => {
|
|
739
854
|
this.openThreadGroup(group, {
|
|
740
855
|
anchor: group.anchor,
|
|
@@ -804,20 +919,36 @@ export class CommentUiController {
|
|
|
804
919
|
this.previewHighlightLayer?.replaceChildren();
|
|
805
920
|
|
|
806
921
|
if (!this.supported || !this.previewElement) {
|
|
922
|
+
this.previewHoverRegions = [];
|
|
807
923
|
return;
|
|
808
924
|
}
|
|
809
925
|
|
|
810
926
|
const previewRect = this.previewElement.getBoundingClientRect();
|
|
811
927
|
const groups = this.getThreadGroups();
|
|
928
|
+
const occupiedTops = [];
|
|
929
|
+
const hoverRegions = [];
|
|
930
|
+
const showPassiveMarkers = this.shouldRenderPassivePreviewMarkers();
|
|
812
931
|
groups.forEach((group) => {
|
|
813
932
|
const target = this.resolvePreviewTarget(group.anchor);
|
|
814
933
|
if (!target?.bubbleRect) {
|
|
815
934
|
return;
|
|
816
935
|
}
|
|
817
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
|
+
|
|
818
946
|
target.highlightRects?.forEach((rect) => {
|
|
819
947
|
const highlight = document.createElement('div');
|
|
820
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);
|
|
821
952
|
highlight.style.left = `${rect.left - previewRect.left}px`;
|
|
822
953
|
highlight.style.top = `${rect.top - previewRect.top}px`;
|
|
823
954
|
highlight.style.width = `${rect.width}px`;
|
|
@@ -825,14 +956,32 @@ export class CommentUiController {
|
|
|
825
956
|
this.previewHighlightLayer?.appendChild(highlight);
|
|
826
957
|
});
|
|
827
958
|
|
|
959
|
+
if (!showPassiveMarkers && !isEmphasized) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
828
963
|
const bubble = document.createElement('button');
|
|
829
964
|
bubble.type = 'button';
|
|
830
965
|
bubble.className = 'comment-preview-badge';
|
|
831
|
-
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);
|
|
832
970
|
bubble.setAttribute('aria-label', `${group.threads.length} comment thread${group.threads.length === 1 ? '' : 's'}`);
|
|
833
971
|
bubble.appendChild(createCommentMarkerContent(group.threads.length));
|
|
834
|
-
|
|
835
|
-
|
|
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`;
|
|
836
985
|
bubble.title = `${group.threads.length} comment${group.threads.length === 1 ? '' : 's'}`;
|
|
837
986
|
bubble.addEventListener('pointerdown', (event) => {
|
|
838
987
|
event.preventDefault();
|
|
@@ -841,11 +990,19 @@ export class CommentUiController {
|
|
|
841
990
|
this.openThreadGroup(group, {
|
|
842
991
|
anchor: group.anchor,
|
|
843
992
|
origin: 'preview',
|
|
844
|
-
sourceRect:
|
|
993
|
+
sourceRect: bubble.getBoundingClientRect(),
|
|
845
994
|
});
|
|
846
995
|
});
|
|
847
996
|
this.previewLayer?.appendChild(bubble);
|
|
997
|
+
occupiedTops.push(bubbleTop);
|
|
848
998
|
});
|
|
999
|
+
|
|
1000
|
+
this.previewHoverRegions = hoverRegions;
|
|
1001
|
+
if (this.lastPreviewPointerPosition) {
|
|
1002
|
+
this.updateHoveredPreviewGroups(
|
|
1003
|
+
this.getPreviewGroupKeysAtPoint(this.lastPreviewPointerPosition.x, this.lastPreviewPointerPosition.y),
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
849
1006
|
}
|
|
850
1007
|
|
|
851
1008
|
resolvePreviewTarget(anchor) {
|
|
@@ -859,6 +1016,7 @@ export class CommentUiController {
|
|
|
859
1016
|
return {
|
|
860
1017
|
bubbleRect: diagramShell.getBoundingClientRect(),
|
|
861
1018
|
highlightRects: [],
|
|
1019
|
+
hoverRects: [diagramShell.getBoundingClientRect()],
|
|
862
1020
|
};
|
|
863
1021
|
}
|
|
864
1022
|
|
|
@@ -875,6 +1033,7 @@ export class CommentUiController {
|
|
|
875
1033
|
return {
|
|
876
1034
|
bubbleRect,
|
|
877
1035
|
highlightRects: rects,
|
|
1036
|
+
hoverRects: rects,
|
|
878
1037
|
};
|
|
879
1038
|
}
|
|
880
1039
|
}
|
|
@@ -887,6 +1046,7 @@ export class CommentUiController {
|
|
|
887
1046
|
return {
|
|
888
1047
|
bubbleRect: fallback.getBoundingClientRect(),
|
|
889
1048
|
highlightRects: [],
|
|
1049
|
+
hoverRects: [fallback.getBoundingClientRect()],
|
|
890
1050
|
};
|
|
891
1051
|
}
|
|
892
1052
|
|
|
@@ -1079,6 +1239,69 @@ export class CommentUiController {
|
|
|
1079
1239
|
this.scheduleLayoutRefresh();
|
|
1080
1240
|
}
|
|
1081
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
|
+
|
|
1082
1305
|
updateReactionPickerPosition(card) {
|
|
1083
1306
|
const bounds = getReactionPickerBounds(card);
|
|
1084
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,
|
|
@@ -12,6 +12,29 @@ function getPathLeaf(path) {
|
|
|
12
12
|
.pop() || '';
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function getParentPath(pathValue) {
|
|
16
|
+
const normalized = String(pathValue ?? '').replace(/\/+$/u, '');
|
|
17
|
+
const separatorIndex = normalized.lastIndexOf('/');
|
|
18
|
+
return separatorIndex >= 0 ? normalized.slice(0, separatorIndex) : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findNodeByPath(nodes = [], pathValue = '') {
|
|
22
|
+
for (const node of nodes) {
|
|
23
|
+
if (node.path === pathValue) {
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (node.type === 'directory' && Array.isArray(node.children)) {
|
|
28
|
+
const nested = findNodeByPath(node.children, pathValue);
|
|
29
|
+
if (nested) {
|
|
30
|
+
return nested;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
export class FileExplorerView {
|
|
16
39
|
constructor({
|
|
17
40
|
onDirectoryToggle,
|
|
@@ -27,6 +50,9 @@ export class FileExplorerView {
|
|
|
27
50
|
this.onTreeContextMenu = onTreeContextMenu;
|
|
28
51
|
this.treeContainer = document.getElementById('fileTree');
|
|
29
52
|
this.searchInput = document.getElementById('fileSearchInput');
|
|
53
|
+
this.renderedDirectoryWrappers = new Map();
|
|
54
|
+
this.renderedChildContainers = new Map();
|
|
55
|
+
this.lastRenderMode = 'tree';
|
|
30
56
|
}
|
|
31
57
|
|
|
32
58
|
initialize() {
|
|
@@ -44,30 +70,62 @@ export class FileExplorerView {
|
|
|
44
70
|
});
|
|
45
71
|
}
|
|
46
72
|
|
|
47
|
-
render({ activeFilePath, expandedDirs, searchMatches, searchQuery, tree }) {
|
|
73
|
+
render({ activeFilePath, changedPaths = null, expandedDirs, reset = false, searchMatches, searchQuery, tree }) {
|
|
48
74
|
if (!this.treeContainer) {
|
|
49
75
|
return;
|
|
50
76
|
}
|
|
51
77
|
|
|
52
78
|
if (searchQuery) {
|
|
79
|
+
this.lastRenderMode = 'search';
|
|
53
80
|
this.renderSearchResults(searchMatches, activeFilePath);
|
|
54
81
|
return;
|
|
55
82
|
}
|
|
56
83
|
|
|
57
|
-
|
|
84
|
+
if (
|
|
85
|
+
reset
|
|
86
|
+
|| this.lastRenderMode !== 'tree'
|
|
87
|
+
|| !Array.isArray(changedPaths)
|
|
88
|
+
|| changedPaths.length === 0
|
|
89
|
+
) {
|
|
90
|
+
this.renderFullTree(tree, {
|
|
91
|
+
activeFilePath,
|
|
92
|
+
expandedDirs,
|
|
93
|
+
});
|
|
94
|
+
this.lastRenderMode = 'tree';
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
58
97
|
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
const affectedParentPaths = Array.from(new Set(
|
|
99
|
+
changedPaths.map((pathValue) => getParentPath(pathValue)),
|
|
100
|
+
))
|
|
101
|
+
.sort((left, right) => left.split('/').length - right.split('/').length)
|
|
102
|
+
.filter((pathValue, index, values) => (
|
|
103
|
+
!values.slice(0, index).some((ancestorPath) => ancestorPath && pathValue.startsWith(`${ancestorPath}/`))
|
|
104
|
+
));
|
|
105
|
+
if (affectedParentPaths.includes('')) {
|
|
106
|
+
this.renderFullTree(tree, {
|
|
107
|
+
activeFilePath,
|
|
108
|
+
expandedDirs,
|
|
109
|
+
});
|
|
110
|
+
this.lastRenderMode = 'tree';
|
|
61
111
|
return;
|
|
62
112
|
}
|
|
63
113
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
114
|
+
for (const parentPath of affectedParentPaths) {
|
|
115
|
+
if (!this.rerenderDirectoryBranch(parentPath, tree, {
|
|
116
|
+
activeFilePath,
|
|
117
|
+
expandedDirs,
|
|
118
|
+
})) {
|
|
119
|
+
this.renderFullTree(tree, {
|
|
120
|
+
activeFilePath,
|
|
121
|
+
expandedDirs,
|
|
122
|
+
});
|
|
123
|
+
this.lastRenderMode = 'tree';
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.lastRenderMode = 'tree';
|
|
71
129
|
}
|
|
72
130
|
|
|
73
131
|
renderSearchResults(matches, activeFilePath) {
|
|
@@ -75,6 +133,7 @@ export class FileExplorerView {
|
|
|
75
133
|
return;
|
|
76
134
|
}
|
|
77
135
|
|
|
136
|
+
this.resetTreeIndexes();
|
|
78
137
|
this.treeContainer.innerHTML = '';
|
|
79
138
|
|
|
80
139
|
if (matches.length === 0) {
|
|
@@ -95,6 +154,29 @@ export class FileExplorerView {
|
|
|
95
154
|
this.treeContainer.appendChild(fragment);
|
|
96
155
|
}
|
|
97
156
|
|
|
157
|
+
renderFullTree(tree, { activeFilePath, expandedDirs }) {
|
|
158
|
+
this.resetTreeIndexes();
|
|
159
|
+
this.treeContainer.innerHTML = '';
|
|
160
|
+
|
|
161
|
+
if (tree.length === 0) {
|
|
162
|
+
this.treeContainer.innerHTML = '<div class="file-tree-empty">No vault files found</div>';
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fragment = document.createDocumentFragment();
|
|
167
|
+
this.renderNodes(tree, fragment, {
|
|
168
|
+
activeFilePath,
|
|
169
|
+
depth: 0,
|
|
170
|
+
expandedDirs,
|
|
171
|
+
});
|
|
172
|
+
this.treeContainer.appendChild(fragment);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
resetTreeIndexes() {
|
|
176
|
+
this.renderedDirectoryWrappers.clear();
|
|
177
|
+
this.renderedChildContainers.clear();
|
|
178
|
+
}
|
|
179
|
+
|
|
98
180
|
renderNodes(nodes, container, { activeFilePath, depth, expandedDirs }) {
|
|
99
181
|
for (const node of nodes) {
|
|
100
182
|
if (node.type === 'directory') {
|
|
@@ -142,21 +224,80 @@ export class FileExplorerView {
|
|
|
142
224
|
});
|
|
143
225
|
|
|
144
226
|
wrapper.appendChild(button);
|
|
227
|
+
this.renderedDirectoryWrappers.set(node.path, wrapper);
|
|
145
228
|
|
|
146
229
|
if (isExpanded && Array.isArray(node.children)) {
|
|
147
230
|
const childContainer = document.createElement('div');
|
|
148
231
|
childContainer.className = 'file-tree-children';
|
|
232
|
+
this.renderedChildContainers.set(node.path, childContainer);
|
|
149
233
|
this.renderNodes(node.children, childContainer, {
|
|
150
234
|
activeFilePath,
|
|
151
235
|
depth: depth + 1,
|
|
152
236
|
expandedDirs,
|
|
153
237
|
});
|
|
154
238
|
wrapper.appendChild(childContainer);
|
|
239
|
+
} else {
|
|
240
|
+
this.renderedChildContainers.delete(node.path);
|
|
155
241
|
}
|
|
156
242
|
|
|
157
243
|
return wrapper;
|
|
158
244
|
}
|
|
159
245
|
|
|
246
|
+
rerenderDirectoryBranch(parentPath, tree, { activeFilePath, expandedDirs }) {
|
|
247
|
+
const wrapper = this.renderedDirectoryWrappers.get(parentPath);
|
|
248
|
+
const parentNode = findNodeByPath(tree, parentPath);
|
|
249
|
+
if (!wrapper || parentNode?.type !== 'directory') {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const button = wrapper.querySelector('.file-tree-dir');
|
|
254
|
+
const isExpanded = expandedDirs.has(parentPath);
|
|
255
|
+
button?.setAttribute('aria-expanded', String(isExpanded));
|
|
256
|
+
button?.querySelector('.file-tree-chevron')?.classList.toggle('expanded', isExpanded);
|
|
257
|
+
|
|
258
|
+
const depth = Number(button?.dataset.depth ?? 0);
|
|
259
|
+
let childContainer = this.renderedChildContainers.get(parentPath) ?? wrapper.querySelector('.file-tree-children');
|
|
260
|
+
|
|
261
|
+
this.clearRenderedDescendants(parentPath);
|
|
262
|
+
|
|
263
|
+
if (!isExpanded) {
|
|
264
|
+
childContainer?.remove();
|
|
265
|
+
this.renderedChildContainers.delete(parentPath);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!childContainer) {
|
|
270
|
+
childContainer = document.createElement('div');
|
|
271
|
+
childContainer.className = 'file-tree-children';
|
|
272
|
+
wrapper.appendChild(childContainer);
|
|
273
|
+
} else {
|
|
274
|
+
childContainer.innerHTML = '';
|
|
275
|
+
}
|
|
276
|
+
this.renderedChildContainers.set(parentPath, childContainer);
|
|
277
|
+
|
|
278
|
+
this.renderNodes(parentNode.children ?? [], childContainer, {
|
|
279
|
+
activeFilePath,
|
|
280
|
+
depth: depth + 1,
|
|
281
|
+
expandedDirs,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
clearRenderedDescendants(parentPath) {
|
|
288
|
+
const prefix = `${parentPath}/`;
|
|
289
|
+
Array.from(this.renderedDirectoryWrappers.keys()).forEach((pathValue) => {
|
|
290
|
+
if (pathValue.startsWith(prefix)) {
|
|
291
|
+
this.renderedDirectoryWrappers.delete(pathValue);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
Array.from(this.renderedChildContainers.keys()).forEach((pathValue) => {
|
|
295
|
+
if (pathValue.startsWith(prefix)) {
|
|
296
|
+
this.renderedChildContainers.delete(pathValue);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
160
301
|
createFileItem({ activeFilePath, depth, filePath, fileType = 'file', name }) {
|
|
161
302
|
const button = document.createElement('button');
|
|
162
303
|
button.className = 'file-tree-item file-tree-file';
|