collabmd 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-BMTwP5mD.js} +1 -1
  2. package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
  3. package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
  4. package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
  5. package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
  6. package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
  7. package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
  8. package/dist/client/assets/index-BIt5EZXz.css +1 -0
  9. package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
  10. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  18. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  19. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  20. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  21. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  22. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  23. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  24. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  25. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  26. package/dist/client/assets/main-DeowJOSp.js +1368 -0
  27. package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
  28. package/dist/client/assets/vault-api-client-p7h7cXKM.js +1 -0
  29. package/dist/client/drawio-editor.html +1 -1
  30. package/dist/client/excalidraw-editor.html +1 -1
  31. package/dist/client/export-document.html +2 -2
  32. package/dist/client/index.html +2 -2
  33. package/package.json +8 -5
  34. package/src/client/app/main-entry.js +2 -0
  35. package/src/client/application/app-shell/git-feature.js +28 -36
  36. package/src/client/application/app-shell/presence-feature.js +3 -1
  37. package/src/client/application/diagram-preview-hydrator.js +24 -15
  38. package/src/client/application/mermaid-preview-hydrator.js +121 -4
  39. package/src/client/application/plantuml-preview-hydrator.js +6 -16
  40. package/src/client/application/preview-renderer.js +11 -2
  41. package/src/client/bootstrap/collabmd-app-shell.js +182 -50
  42. package/src/client/domain/excalidraw-document-switch.js +3 -0
  43. package/src/client/excalidraw-editor.js +187 -84
  44. package/src/client/infrastructure/auth-client.js +4 -4
  45. package/src/client/infrastructure/backlinks-api-client.js +18 -0
  46. package/src/client/infrastructure/comment-thread-store.js +4 -0
  47. package/src/client/infrastructure/editor-session.js +6 -0
  48. package/src/client/infrastructure/editor-view-adapter.js +23 -0
  49. package/src/client/infrastructure/git-api-client.js +134 -0
  50. package/src/client/infrastructure/plantuml-api-client.js +21 -0
  51. package/src/client/infrastructure/vault-api-client.js +115 -0
  52. package/src/client/presentation/backlinks-panel.js +6 -9
  53. package/src/client/presentation/bases-preview-controller.js +1413 -34
  54. package/src/client/presentation/comments-panel.js +50 -5
  55. package/src/client/presentation/excalidraw-embed-controller.js +251 -15
  56. package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
  57. package/src/client/presentation/file-action-controller.js +97 -0
  58. package/src/client/presentation/file-explorer-controller.js +2 -0
  59. package/src/client/presentation/file-explorer-view.js +368 -0
  60. package/src/client/presentation/file-history-view-controller.js +41 -40
  61. package/src/client/presentation/git-diff-view-controller.js +70 -53
  62. package/src/client/presentation/git-panel-controller.js +12 -30
  63. package/src/client/styles/components/scrollbars.css +5 -0
  64. package/src/client/styles/export-document.css +45 -2
  65. package/src/client/styles/features/auth-gate.css +13 -24
  66. package/src/client/styles/features/comment-card.css +6 -0
  67. package/src/client/styles/features/comments-drawer.css +38 -18
  68. package/src/client/styles/features/diagram-preview.css +33 -0
  69. package/src/client/styles/features/embedded-preview.css +37 -11
  70. package/src/client/styles/features/preview-markdown.css +559 -39
  71. package/src/client/styles/foundation/themes.css +4 -0
  72. package/src/client/styles/foundation/tokens.css +4 -3
  73. package/src/client/styles/layout/sidebar.css +72 -0
  74. package/src/server/auth/auth-common.js +224 -0
  75. package/src/server/auth/auth-constants.js +30 -0
  76. package/src/server/auth/auth-strategies.js +320 -0
  77. package/src/server/auth/create-auth-service.js +24 -545
  78. package/src/server/domain/backlink-index.js +177 -5
  79. package/src/server/domain/bases/base-definition.js +382 -0
  80. package/src/server/domain/bases/base-expression-runtime.js +1196 -0
  81. package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
  82. package/src/server/domain/bases/base-query-metadata.js +242 -0
  83. package/src/server/domain/bases/base-query-results.js +215 -0
  84. package/src/server/domain/bases/base-query-service.js +406 -1992
  85. package/src/server/domain/bases/base-transform.js +116 -0
  86. package/src/server/domain/collaboration/collaboration-room.js +36 -1
  87. package/src/server/domain/collaboration/room-client-state-store.js +1 -0
  88. package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
  89. package/src/server/infrastructure/http/create-vault-api-query-handler.js +161 -1
  90. package/src/server/infrastructure/http/http-response.js +56 -0
  91. package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
  92. package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
  93. package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
  94. package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
  95. package/dist/client/assets/index-D69HOZHr.css +0 -1
  96. package/dist/client/assets/main-DSxNTtD-.js +0 -1243
  97. package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
  98. package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
  99. /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Zfwq1XVx.js} +0 -0
@@ -38,23 +38,28 @@ function findNodeByPath(nodes = [], pathValue = '') {
38
38
 
39
39
  const MOBILE_LONG_PRESS_DELAY_MS = 420;
40
40
  const MOBILE_LONG_PRESS_MOVE_TOLERANCE_PX = 10;
41
+ const DRAG_AUTO_EXPAND_DELAY_MS = 700;
41
42
 
42
43
  export class FileExplorerView {
43
44
  constructor({
44
45
  mobileBreakpointQuery = window.matchMedia('(max-width: 768px)'),
46
+ onEntryDrop,
45
47
  onDirectorySelect,
46
48
  onDirectoryToggle,
47
49
  onFileContextMenu,
48
50
  onFileSelect,
49
51
  onSearchChange,
50
52
  onTreeContextMenu,
53
+ onValidateDrop,
51
54
  }) {
55
+ this.onEntryDrop = onEntryDrop;
52
56
  this.onDirectorySelect = onDirectorySelect;
53
57
  this.onDirectoryToggle = onDirectoryToggle;
54
58
  this.onFileContextMenu = onFileContextMenu;
55
59
  this.onFileSelect = onFileSelect;
56
60
  this.onSearchChange = onSearchChange;
57
61
  this.onTreeContextMenu = onTreeContextMenu;
62
+ this.onValidateDrop = onValidateDrop;
58
63
  this.mobileBreakpointQuery = mobileBreakpointQuery;
59
64
  this.treeContainer = document.getElementById('fileTree');
60
65
  this.searchInput = document.getElementById('fileSearchInput');
@@ -66,6 +71,13 @@ export class FileExplorerView {
66
71
  this.suppressedActivationTarget = null;
67
72
  this.contextMenuCloseHandler = null;
68
73
  this.actionSheetCloseHandler = null;
74
+ this.dragSource = null;
75
+ this.currentSearchQuery = '';
76
+ this.activeDropTarget = null;
77
+ this.invalidDropAttempt = null;
78
+ this.autoExpandTimer = 0;
79
+ this.autoExpandTargetPath = '';
80
+ this.rootDropZone = null;
69
81
  }
70
82
 
71
83
  initialize() {
@@ -108,6 +120,15 @@ export class FileExplorerView {
108
120
  this.treeContainer?.addEventListener('scroll', () => {
109
121
  this.cancelLongPress();
110
122
  }, { passive: true });
123
+ this.treeContainer?.addEventListener('dragover', (event) => {
124
+ this.handleTreeDragOver(event);
125
+ });
126
+ this.treeContainer?.addEventListener('drop', (event) => {
127
+ this.handleTreeDrop(event);
128
+ });
129
+ this.treeContainer?.addEventListener('dragleave', (event) => {
130
+ this.handleTreeDragLeave(event);
131
+ });
111
132
  }
112
133
 
113
134
  render({ activeFilePath, changedPaths = null, expandedDirs, reset = false, searchMatches, searchQuery, tree }) {
@@ -115,6 +136,11 @@ export class FileExplorerView {
115
136
  return;
116
137
  }
117
138
 
139
+ this.currentSearchQuery = String(searchQuery ?? '');
140
+ if (this.currentSearchQuery) {
141
+ this.clearDragFeedback();
142
+ }
143
+
118
144
  if (this.searchInput && this.searchInput.value !== searchQuery) {
119
145
  this.searchInput.value = searchQuery;
120
146
  }
@@ -179,6 +205,7 @@ export class FileExplorerView {
179
205
 
180
206
  this.resetTreeIndexes();
181
207
  this.treeContainer.innerHTML = '';
208
+ this.rootDropZone = null;
182
209
 
183
210
  if (matches.length === 0) {
184
211
  this.treeContainer.innerHTML = '<div class="file-tree-empty">No matches</div>';
@@ -206,6 +233,7 @@ export class FileExplorerView {
206
233
  renderFullTree(tree, { activeFilePath, expandedDirs }) {
207
234
  this.resetTreeIndexes();
208
235
  this.treeContainer.innerHTML = '';
236
+ this.rootDropZone = null;
209
237
 
210
238
  if (tree.length === 0) {
211
239
  this.treeContainer.innerHTML = '<div class="file-tree-empty">No vault files found</div>';
@@ -213,6 +241,7 @@ export class FileExplorerView {
213
241
  }
214
242
 
215
243
  const fragment = document.createDocumentFragment();
244
+ fragment.appendChild(this.createRootDropZone());
216
245
  this.renderNodes(tree, fragment, {
217
246
  activeFilePath,
218
247
  depth: 0,
@@ -258,11 +287,15 @@ export class FileExplorerView {
258
287
 
259
288
  const isExpanded = expandedDirs.has(node.path);
260
289
  button.setAttribute('aria-expanded', String(isExpanded));
290
+ button.dataset.path = node.path;
291
+ button.dataset.entryType = 'directory';
261
292
  button.innerHTML = `
262
293
  <svg class="file-tree-chevron${isExpanded ? ' expanded' : ''}" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
263
294
  <svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
264
295
  <span class="file-tree-name">${escapeHtml(node.name)}</span>
265
296
  `;
297
+ this.configureDragSource(button, { path: node.path, type: 'directory' });
298
+ this.bindDirectoryDropTarget(button, node.path);
266
299
 
267
300
  button.addEventListener('click', (event) => {
268
301
  if (this.consumeSuppressedActivation(button, event)) {
@@ -303,6 +336,8 @@ export class FileExplorerView {
303
336
  button.className = 'file-tree-item file-tree-dir';
304
337
  button.style.setProperty('--depth', 0);
305
338
  button.dataset.depth = 0;
339
+ button.dataset.path = node.path;
340
+ button.dataset.entryType = 'directory';
306
341
  button.innerHTML = `
307
342
  <svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
308
343
  <span class="file-tree-name">${escapeHtml(node.name || getPathLeaf(node.path))}</span>
@@ -415,10 +450,12 @@ export class FileExplorerView {
415
450
  button.style.setProperty('--depth', depth);
416
451
  button.dataset.depth = depth;
417
452
  button.dataset.path = filePath;
453
+ button.dataset.entryType = 'file';
418
454
  button.innerHTML = `
419
455
  ${this.getFileIconSvg({ isBase, isDrawio, isExcalidraw, isImage, isMermaid, isPlantUml })}
420
456
  <span class="file-tree-name">${escapeHtml(stripVaultFileExtension(name))}</span>
421
457
  `;
458
+ this.configureDragSource(button, { path: filePath, type: 'file' });
422
459
 
423
460
  button.addEventListener('click', (event) => {
424
461
  if (this.consumeSuppressedActivation(button, event)) {
@@ -441,6 +478,336 @@ export class FileExplorerView {
441
478
  return Boolean(this.mobileBreakpointQuery?.matches);
442
479
  }
443
480
 
481
+ isDragAndDropEnabled() {
482
+ return !this.isMobileViewport() && !this.currentSearchQuery;
483
+ }
484
+
485
+ configureDragSource(element, payload) {
486
+ if (!(element instanceof HTMLElement)) {
487
+ return;
488
+ }
489
+
490
+ const isEnabled = this.isDragAndDropEnabled();
491
+ element.draggable = isEnabled;
492
+ if (!isEnabled) {
493
+ return;
494
+ }
495
+
496
+ element.addEventListener('dragstart', (event) => {
497
+ this.dragSource = {
498
+ path: payload.path,
499
+ type: payload.type,
500
+ };
501
+ document.body?.classList.add('is-file-tree-dragging');
502
+ if (this.treeContainer) {
503
+ this.treeContainer.dataset.dragActive = 'true';
504
+ }
505
+ element.classList.add('is-dragging');
506
+ if (event.dataTransfer) {
507
+ event.dataTransfer.effectAllowed = 'move';
508
+ event.dataTransfer.setData('text/plain', payload.path);
509
+ }
510
+ });
511
+ element.addEventListener('dragend', () => {
512
+ element.classList.remove('is-dragging');
513
+ const dragSource = this.dragSource;
514
+ if (this.invalidDropAttempt && dragSource) {
515
+ void this.onEntryDrop?.(this.invalidDropAttempt);
516
+ }
517
+ this.dragSource = null;
518
+ this.invalidDropAttempt = null;
519
+ this.clearDragFeedback();
520
+ });
521
+
522
+ if (payload.type === 'file') {
523
+ element.addEventListener('dragenter', (event) => {
524
+ this.handleNonDropTargetDragOver(event);
525
+ });
526
+ element.addEventListener('dragover', (event) => {
527
+ this.handleNonDropTargetDragOver(event);
528
+ });
529
+ }
530
+ }
531
+
532
+ bindDirectoryDropTarget(element, directoryPath) {
533
+ if (!(element instanceof HTMLElement) || this.isMobileViewport()) {
534
+ return;
535
+ }
536
+
537
+ element.addEventListener('dragenter', (event) => {
538
+ this.handleDirectoryDragEnter(event, element, directoryPath);
539
+ });
540
+ element.addEventListener('dragover', (event) => {
541
+ this.handleDirectoryDragOver(event, element, directoryPath);
542
+ });
543
+ element.addEventListener('drop', (event) => {
544
+ this.handleDirectoryDrop(event, directoryPath);
545
+ });
546
+ }
547
+
548
+ createRootDropZone() {
549
+ const zone = document.createElement('div');
550
+ zone.className = 'file-tree-root-drop-zone';
551
+ zone.textContent = 'Drop here to move to vault root';
552
+ zone.addEventListener('dragenter', (event) => {
553
+ this.handleRootZoneDragEnter(event);
554
+ });
555
+ zone.addEventListener('dragover', (event) => {
556
+ this.handleRootZoneDragOver(event);
557
+ });
558
+ zone.addEventListener('drop', (event) => {
559
+ this.handleRootZoneDrop(event);
560
+ });
561
+ this.rootDropZone = zone;
562
+ return zone;
563
+ }
564
+
565
+ validateDrop(destinationDirectory) {
566
+ if (!this.dragSource || this.currentSearchQuery) {
567
+ return false;
568
+ }
569
+
570
+ return this.onValidateDrop?.({
571
+ destinationDirectory,
572
+ sourcePath: this.dragSource.path,
573
+ sourceType: this.dragSource.type,
574
+ }) === true;
575
+ }
576
+
577
+ setDropTarget(target) {
578
+ if (this.activeDropTarget?.element && this.activeDropTarget.element !== target?.element) {
579
+ this.activeDropTarget.element.classList.remove('is-drop-target', 'is-drop-invalid');
580
+ }
581
+
582
+ if (this.activeDropTarget?.root && !target?.root) {
583
+ this.treeContainer?.classList.remove('is-drop-target-root', 'is-drop-invalid');
584
+ }
585
+ if (this.activeDropTarget?.rootZone && !target?.rootZone) {
586
+ this.rootDropZone?.classList.remove('is-drop-target', 'is-drop-invalid');
587
+ }
588
+
589
+ this.activeDropTarget = target;
590
+ if (!target || target.isValid) {
591
+ this.invalidDropAttempt = null;
592
+ } else if (this.dragSource) {
593
+ this.invalidDropAttempt = {
594
+ destinationDirectory: target.destinationDirectory || '',
595
+ sourcePath: this.dragSource.path,
596
+ sourceType: this.dragSource.type,
597
+ };
598
+ }
599
+
600
+ if (!target) {
601
+ return;
602
+ }
603
+
604
+ if (target.root) {
605
+ this.treeContainer?.classList.toggle('is-drop-target-root', target.isValid);
606
+ this.treeContainer?.classList.toggle('is-drop-invalid', !target.isValid);
607
+ return;
608
+ }
609
+
610
+ if (target.rootZone) {
611
+ this.rootDropZone?.classList.toggle('is-drop-target', target.isValid);
612
+ this.rootDropZone?.classList.toggle('is-drop-invalid', !target.isValid);
613
+ return;
614
+ }
615
+
616
+ target.element.classList.toggle('is-drop-target', target.isValid);
617
+ target.element.classList.toggle('is-drop-invalid', !target.isValid);
618
+ }
619
+
620
+ scheduleAutoExpand(element, directoryPath) {
621
+ const isExpanded = element.getAttribute('aria-expanded') === 'true';
622
+ if (isExpanded || this.autoExpandTargetPath === directoryPath) {
623
+ return;
624
+ }
625
+
626
+ this.cancelAutoExpand();
627
+ this.autoExpandTargetPath = directoryPath;
628
+ this.autoExpandTimer = window.setTimeout(() => {
629
+ this.autoExpandTimer = 0;
630
+ this.autoExpandTargetPath = '';
631
+ if (element.isConnected && element.getAttribute('aria-expanded') !== 'true') {
632
+ this.onDirectoryToggle?.(directoryPath);
633
+ }
634
+ }, DRAG_AUTO_EXPAND_DELAY_MS);
635
+ }
636
+
637
+ cancelAutoExpand() {
638
+ if (this.autoExpandTimer) {
639
+ window.clearTimeout(this.autoExpandTimer);
640
+ this.autoExpandTimer = 0;
641
+ }
642
+ this.autoExpandTargetPath = '';
643
+ }
644
+
645
+ clearDragFeedback() {
646
+ this.cancelAutoExpand();
647
+ this.setDropTarget(null);
648
+ document.body?.classList.remove('is-file-tree-dragging');
649
+ if (this.treeContainer) {
650
+ delete this.treeContainer.dataset.dragActive;
651
+ }
652
+ }
653
+
654
+ handleDirectoryDragEnter(event, element, directoryPath) {
655
+ if (!this.dragSource) {
656
+ return;
657
+ }
658
+
659
+ event.preventDefault();
660
+ event.stopPropagation();
661
+ const isValid = this.validateDrop(directoryPath);
662
+ this.setDropTarget({ destinationDirectory: directoryPath, element, isValid, root: false });
663
+ if (isValid) {
664
+ this.scheduleAutoExpand(element, directoryPath);
665
+ } else {
666
+ this.cancelAutoExpand();
667
+ }
668
+ }
669
+
670
+ handleDirectoryDragOver(event, element, directoryPath) {
671
+ if (!this.dragSource) {
672
+ return;
673
+ }
674
+
675
+ event.preventDefault();
676
+ event.stopPropagation();
677
+ const isValid = this.validateDrop(directoryPath);
678
+ if (event.dataTransfer) {
679
+ event.dataTransfer.dropEffect = isValid ? 'move' : 'none';
680
+ }
681
+ this.setDropTarget({ destinationDirectory: directoryPath, element, isValid, root: false });
682
+ if (isValid) {
683
+ this.scheduleAutoExpand(element, directoryPath);
684
+ } else {
685
+ this.cancelAutoExpand();
686
+ }
687
+ }
688
+
689
+ handleDirectoryDrop(event, directoryPath) {
690
+ if (!this.dragSource) {
691
+ return;
692
+ }
693
+
694
+ event.preventDefault();
695
+ event.stopPropagation();
696
+ const payload = {
697
+ destinationDirectory: directoryPath,
698
+ sourcePath: this.dragSource.path,
699
+ sourceType: this.dragSource.type,
700
+ };
701
+ this.clearDragFeedback();
702
+ void this.onEntryDrop?.(payload);
703
+ }
704
+
705
+ handleTreeDragOver(event) {
706
+ if (!this.dragSource || !this.treeContainer) {
707
+ return;
708
+ }
709
+
710
+ if (event.target.closest('.file-tree-dir') || event.target.closest('.file-tree-item')) {
711
+ return;
712
+ }
713
+
714
+ event.preventDefault();
715
+ this.cancelAutoExpand();
716
+ const isValid = this.validateDrop('');
717
+ if (event.dataTransfer) {
718
+ event.dataTransfer.dropEffect = isValid ? 'move' : 'none';
719
+ }
720
+ this.setDropTarget({ destinationDirectory: '', element: this.treeContainer, isValid, root: true });
721
+ }
722
+
723
+ handleTreeDrop(event) {
724
+ if (!this.dragSource || !this.treeContainer) {
725
+ return;
726
+ }
727
+
728
+ if (event.target.closest('.file-tree-dir') || event.target.closest('.file-tree-item')) {
729
+ return;
730
+ }
731
+
732
+ event.preventDefault();
733
+ const payload = {
734
+ destinationDirectory: '',
735
+ sourcePath: this.dragSource.path,
736
+ sourceType: this.dragSource.type,
737
+ };
738
+ this.clearDragFeedback();
739
+ void this.onEntryDrop?.(payload);
740
+ }
741
+
742
+ handleTreeDragLeave(event) {
743
+ if (!this.dragSource || !this.treeContainer) {
744
+ return;
745
+ }
746
+
747
+ const nextTarget = event.relatedTarget;
748
+ if (nextTarget && this.treeContainer.contains(nextTarget)) {
749
+ return;
750
+ }
751
+
752
+ this.clearDragFeedback();
753
+ }
754
+
755
+ handleRootZoneDragEnter(event) {
756
+ if (!this.dragSource) {
757
+ return;
758
+ }
759
+
760
+ event.preventDefault();
761
+ event.stopPropagation();
762
+ const isValid = this.validateDrop('');
763
+ this.cancelAutoExpand();
764
+ this.setDropTarget({ destinationDirectory: '', element: this.rootDropZone, isValid, rootZone: true });
765
+ }
766
+
767
+ handleRootZoneDragOver(event) {
768
+ if (!this.dragSource) {
769
+ return;
770
+ }
771
+
772
+ event.preventDefault();
773
+ event.stopPropagation();
774
+ const isValid = this.validateDrop('');
775
+ if (event.dataTransfer) {
776
+ event.dataTransfer.dropEffect = isValid ? 'move' : 'none';
777
+ }
778
+ this.cancelAutoExpand();
779
+ this.setDropTarget({ destinationDirectory: '', element: this.rootDropZone, isValid, rootZone: true });
780
+ }
781
+
782
+ handleRootZoneDrop(event) {
783
+ if (!this.dragSource) {
784
+ return;
785
+ }
786
+
787
+ event.preventDefault();
788
+ event.stopPropagation();
789
+ const payload = {
790
+ destinationDirectory: '',
791
+ sourcePath: this.dragSource.path,
792
+ sourceType: this.dragSource.type,
793
+ };
794
+ this.clearDragFeedback();
795
+ void this.onEntryDrop?.(payload);
796
+ }
797
+
798
+ handleNonDropTargetDragOver(event) {
799
+ if (!this.dragSource) {
800
+ return;
801
+ }
802
+
803
+ event.preventDefault();
804
+ event.stopPropagation();
805
+ if (event.dataTransfer) {
806
+ event.dataTransfer.dropEffect = 'none';
807
+ }
808
+ this.clearDragFeedback();
809
+ }
810
+
444
811
  bindLongPress(element, callback) {
445
812
  if (!(element instanceof HTMLElement) || typeof callback !== 'function') {
446
813
  return;
@@ -659,6 +1026,7 @@ export class FileExplorerView {
659
1026
 
660
1027
  removeContextMenu() {
661
1028
  this.cancelLongPress();
1029
+ this.clearDragFeedback();
662
1030
  document.querySelectorAll('.file-context-menu').forEach((menu) => menu.remove());
663
1031
  if (this.contextMenuCloseHandler) {
664
1032
  document.removeEventListener('click', this.contextMenuCloseHandler);
@@ -1,5 +1,4 @@
1
1
  import { escapeHtml } from '../domain/vault-utils.js';
2
- import { resolveApiUrl } from '../domain/runtime-paths.js';
3
2
  import { buttonClassNames } from './components/ui/button.js';
4
3
 
5
4
  const HISTORY_PAGE_SIZE = 30;
@@ -62,6 +61,7 @@ function createEmptyHistoryState() {
62
61
  export class FileHistoryViewController {
63
62
  constructor({
64
63
  diffRenderer = null,
64
+ gitApiClient = null,
65
65
  onOpenCommitDiff = () => {},
66
66
  onOpenFile = () => {},
67
67
  onOpenPreview = () => {},
@@ -69,6 +69,7 @@ export class FileHistoryViewController {
69
69
  toastController = null,
70
70
  } = {}) {
71
71
  this.diffRenderer = diffRenderer;
72
+ this.gitApiClient = gitApiClient;
72
73
  this.onOpenCommitDiff = onOpenCommitDiff;
73
74
  this.onOpenFile = onOpenFile;
74
75
  this.onOpenPreview = onOpenPreview;
@@ -89,6 +90,7 @@ export class FileHistoryViewController {
89
90
  this.nextButton = document.getElementById('diffNextBtn');
90
91
  this.layoutToggle = document.getElementById('diffLayoutToggle');
91
92
  this.modeButtons = Array.from(document.querySelectorAll('[data-diff-mode]'));
93
+ this.isActive = false;
92
94
  this.currentFilePath = null;
93
95
  this.localChanges = null;
94
96
  this.history = createEmptyHistoryState();
@@ -104,6 +106,9 @@ export class FileHistoryViewController {
104
106
 
105
107
  initialize() {
106
108
  this.openEditorButton?.addEventListener('click', () => {
109
+ if (!this.isActive) {
110
+ return;
111
+ }
107
112
  if (!this.currentFilePath) {
108
113
  return;
109
114
  }
@@ -113,6 +118,9 @@ export class FileHistoryViewController {
113
118
 
114
119
  this.modeButtons.forEach((button) => {
115
120
  button.addEventListener('click', () => {
121
+ if (!this.isActive) {
122
+ return;
123
+ }
116
124
  const nextMode = button.getAttribute('data-diff-mode');
117
125
  if (!nextMode || nextMode === this.diffMode) {
118
126
  return;
@@ -123,6 +131,9 @@ export class FileHistoryViewController {
123
131
  });
124
132
 
125
133
  this.content?.addEventListener('click', (event) => {
134
+ if (!this.isActive) {
135
+ return;
136
+ }
126
137
  const selectionButton = event.target instanceof Element
127
138
  ? event.target.closest('[data-file-history-select]')
128
139
  : null;
@@ -160,6 +171,7 @@ export class FileHistoryViewController {
160
171
  }
161
172
 
162
173
  hide() {
174
+ this.isActive = false;
163
175
  this.page?.classList.add('hidden');
164
176
  this.content?.classList.remove('diff-content--history');
165
177
  if (this.content) {
@@ -175,7 +187,6 @@ export class FileHistoryViewController {
175
187
  this.historyListElement = null;
176
188
  this.historyListScrollTop = 0;
177
189
  this.selectionRequestToken = 0;
178
- this.syncToolbar();
179
190
  }
180
191
 
181
192
  getEntries() {
@@ -229,6 +240,7 @@ export class FileHistoryViewController {
229
240
  }
230
241
 
231
242
  async openFileHistory({ filePath }) {
243
+ this.isActive = true;
232
244
  this.currentFilePath = String(filePath ?? '').trim() || null;
233
245
  this.localChanges = null;
234
246
  this.history = {
@@ -289,16 +301,11 @@ export class FileHistoryViewController {
289
301
  return null;
290
302
  }
291
303
 
292
- const query = new URLSearchParams();
293
- query.set('metaOnly', 'true');
294
- query.set('path', this.currentFilePath);
295
- query.set('scope', 'all');
296
- const response = await fetch(resolveApiUrl(`/git/diff?${query.toString()}`));
297
- const data = await response.json();
298
- if (!response.ok) {
299
- throw new Error(data.error || 'Failed to load local file changes');
300
- }
301
-
304
+ const data = await this.gitApiClient.readDiff({
305
+ metaOnly: true,
306
+ path: this.currentFilePath,
307
+ scope: 'all',
308
+ });
302
309
  const file = data.files?.[0];
303
310
  if (!file) {
304
311
  return null;
@@ -322,16 +329,11 @@ export class FileHistoryViewController {
322
329
  };
323
330
  }
324
331
 
325
- const query = new URLSearchParams();
326
- query.set('path', this.currentFilePath);
327
- query.set('limit', String(HISTORY_PAGE_SIZE));
328
- query.set('offset', String(offset));
329
- const response = await fetch(resolveApiUrl(`/git/file-history?${query.toString()}`));
330
- const data = await response.json();
331
- if (!response.ok) {
332
- throw new Error(data.error || 'Failed to load file history');
333
- }
334
-
332
+ const data = await this.gitApiClient.readFileHistory({
333
+ limit: HISTORY_PAGE_SIZE,
334
+ offset,
335
+ path: this.currentFilePath,
336
+ });
335
337
  const commits = Array.isArray(data.commits) ? data.commits : [];
336
338
  return {
337
339
  commits,
@@ -350,30 +352,20 @@ export class FileHistoryViewController {
350
352
  }
351
353
 
352
354
  if (entry.type === 'local') {
353
- const query = new URLSearchParams();
354
- query.set('path', this.currentFilePath || '');
355
- query.set('scope', 'all');
356
- const response = await fetch(resolveApiUrl(`/git/diff?${query.toString()}`));
357
- const data = await response.json();
358
- if (!response.ok) {
359
- throw new Error(data.error || 'Failed to load local diff');
360
- }
361
-
355
+ const data = await this.gitApiClient.readDiff({
356
+ path: this.currentFilePath || '',
357
+ scope: 'all',
358
+ });
362
359
  return {
363
360
  detail: data.files?.[0] ?? null,
364
361
  summary: data.summary ?? entry.summary ?? null,
365
362
  };
366
363
  }
367
364
 
368
- const query = new URLSearchParams();
369
- query.set('hash', entry.hash || '');
370
- query.set('path', entry.pathAtCommit || '');
371
- const response = await fetch(resolveApiUrl(`/git/commit?${query.toString()}`));
372
- const data = await response.json();
373
- if (!response.ok) {
374
- throw new Error(data.error || 'Failed to load commit diff');
375
- }
376
-
365
+ const data = await this.gitApiClient.readCommit({
366
+ hash: entry.hash || '',
367
+ path: entry.pathAtCommit || '',
368
+ });
377
369
  return {
378
370
  commit: data.commit ?? null,
379
371
  detail: data.files?.[0] ?? null,
@@ -564,6 +556,9 @@ export class FileHistoryViewController {
564
556
  }
565
557
 
566
558
  renderLoading() {
559
+ if (!this.isActive) {
560
+ return;
561
+ }
567
562
  this.page?.classList.remove('hidden');
568
563
  if (this.content) {
569
564
  this.content.classList.add('diff-content--history');
@@ -574,6 +569,9 @@ export class FileHistoryViewController {
574
569
  }
575
570
 
576
571
  renderEmpty(message) {
572
+ if (!this.isActive) {
573
+ return;
574
+ }
577
575
  this.page?.classList.remove('hidden');
578
576
  if (this.content) {
579
577
  this.content.classList.add('diff-content--history');
@@ -744,6 +742,9 @@ export class FileHistoryViewController {
744
742
  }
745
743
 
746
744
  render() {
745
+ if (!this.isActive) {
746
+ return;
747
+ }
747
748
  this.page?.classList.remove('hidden');
748
749
  this.captureListScroll();
749
750
  this.syncToolbar();