collabmd 0.1.18 → 0.1.20
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 +107 -25
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/{preview-render-compiler-WD3A76I6.js → chunk-5QRWYPYT.js} +18 -18
- package/public/assets/js/chunks/chunk-HEWVH67U.js +9 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +1 -0
- package/public/assets/js/chunks/editor-session-TAV2VXTA.js +22 -0
- package/public/assets/js/chunks/preview-render-compiler-UZ4SZDQQ.js +1 -0
- package/public/assets/js/chunks/quick-switcher-controller-J7I3CUET.js +5 -0
- package/public/assets/js/{excalidraw-editor-LKW2AZBU.js → excalidraw-editor-ZDYKMZOL.js} +33 -33
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +100 -73
- package/public/assets/js/preview-render-worker.js +19 -19
- package/public/index.html +14 -4
- package/src/client/application/app-shell/git-feature.js +29 -1
- package/src/client/application/app-shell/presence-feature.js +2 -1
- package/src/client/application/app-shell/ui-feature.js +113 -1
- package/src/client/application/app-shell/workspace-feature.js +9 -0
- package/src/client/application/app-shell-elements.js +1 -0
- package/src/client/application/preview-render-compiler.js +55 -5
- package/src/client/application/preview-render-executor.js +8 -0
- package/src/client/application/preview-render-worker.js +13 -2
- package/src/client/application/preview-renderer.js +5 -0
- package/src/client/application/workspace-chrome-controller.js +12 -1
- package/src/client/application/workspace-coordinator.js +16 -7
- package/src/client/application/workspace-preview-controller.js +45 -5
- package/src/client/application/workspace-route-controller.js +4 -0
- package/src/client/bootstrap/collabmd-app-shell.js +19 -3
- package/src/client/domain/room.js +37 -0
- package/src/client/domain/vault-utils.js +46 -0
- package/src/client/infrastructure/comment-thread-store.js +98 -0
- package/src/client/infrastructure/editor-paste-utils.js +45 -0
- package/src/client/infrastructure/editor-session.js +10 -0
- package/src/client/infrastructure/editor-view-adapter.js +98 -7
- package/src/client/infrastructure/vault-api-client.js +17 -0
- package/src/client/presentation/backlinks-panel.js +157 -101
- package/src/client/presentation/comment-markdown-renderer.js +68 -0
- package/src/client/presentation/comment-ui-controller.js +352 -20
- package/src/client/presentation/file-explorer-controller.js +5 -0
- package/src/client/presentation/file-explorer-view.js +10 -2
- package/src/client/presentation/file-tree-state.js +7 -1
- package/src/client/presentation/git-panel-controller.js +73 -0
- package/src/client/presentation/image-lightbox-controller.js +394 -0
- package/src/client/presentation/outline-controller.js +25 -0
- package/src/client/styles/style.css +677 -21
- package/src/domain/comment-threads.js +95 -13
- package/src/domain/file-kind.js +16 -1
- package/src/server/infrastructure/git/errors.js +4 -1
- package/src/server/infrastructure/git/git-service.js +215 -1
- package/src/server/infrastructure/http/create-git-api-command-handler.js +6 -1
- package/src/server/infrastructure/http/create-git-api-query-handler.js +16 -1
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +48 -2
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +67 -1
- package/src/server/infrastructure/http/request-body.js +11 -2
- package/src/server/infrastructure/persistence/path-utils.js +1 -1
- package/src/server/infrastructure/persistence/pull-backup-store.js +283 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +203 -2
- package/public/assets/js/chunks/chunk-BBHPYU2R.js +0 -1
- package/public/assets/js/chunks/chunk-OG2TNZEU.js +0 -9
- package/public/assets/js/chunks/chunk-QSTBGTWJ.js +0 -1
- package/public/assets/js/chunks/chunk-SR3U53EQ.js +0 -1
- package/public/assets/js/chunks/editor-session-IHFTYG5D.js +0 -22
- package/public/assets/js/chunks/quick-switcher-controller-JYDIJVAJ.js +0 -5
|
@@ -2,12 +2,15 @@ import {
|
|
|
2
2
|
COMMENT_BODY_MAX_LENGTH,
|
|
3
3
|
normalizeCommentQuoteForComparison,
|
|
4
4
|
} from '../../domain/comment-threads.js';
|
|
5
|
+
import { renderCommentMarkdownToHtml } from './comment-markdown-renderer.js';
|
|
5
6
|
|
|
6
7
|
const COMMENT_CARD_OFFSET = 14;
|
|
7
|
-
const COMMENT_CARD_WIDTH =
|
|
8
|
+
const COMMENT_CARD_WIDTH = 520;
|
|
8
9
|
const COMMENT_SELECTION_REVEAL_DELAY_MS = 150;
|
|
9
10
|
const COMMENT_SELECTION_CHIP_GAP = 12;
|
|
10
11
|
const COMMENT_CONTROL_SLOT_HEIGHT = 36;
|
|
12
|
+
const COMMENT_REACTION_PRESET_EMOJIS = Object.freeze(['👍', '❤️', '🎉', '👀', '🚀']);
|
|
13
|
+
const COMMENT_REACTION_MORE_EMOJIS = Object.freeze(['😂', '🔥', '✅', '🙏', '💡', '🤔', '👏', '😄', '🎯', '🙌']);
|
|
11
14
|
|
|
12
15
|
function clamp(value, min, max) {
|
|
13
16
|
return Math.min(Math.max(value, min), max);
|
|
@@ -81,6 +84,61 @@ function parseLineNumber(value) {
|
|
|
81
84
|
return Number.isFinite(parsed) ? parsed : null;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
function getLatestMessage(messages = []) {
|
|
88
|
+
return messages.reduce((latest, message) => {
|
|
89
|
+
if (!latest) {
|
|
90
|
+
return message;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (message?.createdAt ?? 0) >= (latest?.createdAt ?? 0)
|
|
94
|
+
? message
|
|
95
|
+
: latest;
|
|
96
|
+
}, null);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getLatestGroupMessage(group) {
|
|
100
|
+
return group?.threads?.reduce((latest, thread) => {
|
|
101
|
+
const next = getLatestMessage(thread?.messages ?? []);
|
|
102
|
+
if (!next) {
|
|
103
|
+
return latest;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (next.createdAt ?? 0) >= (latest?.createdAt ?? 0)
|
|
107
|
+
? next
|
|
108
|
+
: latest;
|
|
109
|
+
}, null);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createRenderedCommentBody(body, className = 'comment-markdown') {
|
|
113
|
+
const container = document.createElement('div');
|
|
114
|
+
container.className = className;
|
|
115
|
+
container.innerHTML = renderCommentMarkdownToHtml(body);
|
|
116
|
+
return container;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasLocalReaction(reaction, localUserId) {
|
|
120
|
+
return Boolean(localUserId && reaction?.users?.some((user) => user?.userId === localUserId));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatReactionCount(reaction) {
|
|
124
|
+
return String(Array.isArray(reaction?.users) ? reaction.users.length : 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isReactionPickerOpen(reactionPicker, threadId, messageId) {
|
|
128
|
+
return reactionPicker?.threadId === threadId && reactionPicker?.messageId === messageId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getReactionPickerBounds(card) {
|
|
132
|
+
const picker = card?.querySelector?.('.comment-reaction-picker');
|
|
133
|
+
const wrap = picker?.closest?.('.comment-reaction-picker-wrap');
|
|
134
|
+
const scroll = card?.querySelector?.('.comment-card-scroll');
|
|
135
|
+
if (!(picker instanceof HTMLElement) || !(wrap instanceof HTMLElement) || !(scroll instanceof HTMLElement)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { picker, scroll, wrap };
|
|
140
|
+
}
|
|
141
|
+
|
|
84
142
|
function overlapsAnchorRange(element, anchor) {
|
|
85
143
|
const startLine = parseLineNumber(element?.getAttribute?.('data-source-line'));
|
|
86
144
|
const endLine = parseLineNumber(element?.getAttribute?.('data-source-line-end')) ?? startLine;
|
|
@@ -221,9 +279,11 @@ export class CommentUiController {
|
|
|
221
279
|
commentsDrawerList,
|
|
222
280
|
commentsToggleButton,
|
|
223
281
|
editorContainer,
|
|
282
|
+
onWillOpenDrawer,
|
|
224
283
|
onCreateThread,
|
|
225
284
|
onNavigateToLine,
|
|
226
285
|
onReplyToThread,
|
|
286
|
+
onToggleReaction,
|
|
227
287
|
onResolveThread,
|
|
228
288
|
previewContainer,
|
|
229
289
|
previewElement,
|
|
@@ -234,9 +294,11 @@ export class CommentUiController {
|
|
|
234
294
|
this.commentsDrawerList = commentsDrawerList;
|
|
235
295
|
this.commentsToggleButton = commentsToggleButton;
|
|
236
296
|
this.editorContainer = editorContainer;
|
|
297
|
+
this.onWillOpenDrawer = onWillOpenDrawer;
|
|
237
298
|
this.onCreateThread = onCreateThread;
|
|
238
299
|
this.onNavigateToLine = onNavigateToLine;
|
|
239
300
|
this.onReplyToThread = onReplyToThread;
|
|
301
|
+
this.onToggleReaction = onToggleReaction;
|
|
240
302
|
this.onResolveThread = onResolveThread;
|
|
241
303
|
this.previewContainer = previewContainer;
|
|
242
304
|
this.previewElement = previewElement;
|
|
@@ -258,6 +320,7 @@ export class CommentUiController {
|
|
|
258
320
|
this.previewHighlightLayer = null;
|
|
259
321
|
this.cardRoot = null;
|
|
260
322
|
this.pendingCardFocusElement = null;
|
|
323
|
+
this.reactionPicker = null;
|
|
261
324
|
this.layoutFrame = 0;
|
|
262
325
|
this.timeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
263
326
|
day: 'numeric',
|
|
@@ -268,6 +331,9 @@ export class CommentUiController {
|
|
|
268
331
|
this.handleEditorScroll = () => this.scheduleLayoutRefresh();
|
|
269
332
|
this.handlePreviewScroll = () => this.scheduleLayoutRefresh();
|
|
270
333
|
this.handleWindowResize = () => this.scheduleLayoutRefresh();
|
|
334
|
+
this.handleCommentSelectionButtonPointerDown = (event) => {
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
};
|
|
271
337
|
this.handleEditorPointerDown = (event) => {
|
|
272
338
|
if (event.button !== 0 || !this.supported || !this.session) {
|
|
273
339
|
return;
|
|
@@ -320,15 +386,37 @@ export class CommentUiController {
|
|
|
320
386
|
this.scheduleLayoutRefresh();
|
|
321
387
|
};
|
|
322
388
|
this.handleDocumentPointerDown = (event) => {
|
|
323
|
-
|
|
389
|
+
const target = event.target;
|
|
390
|
+
|
|
391
|
+
if (this.activeCard && this.cardRoot) {
|
|
392
|
+
if (this.cardRoot.contains(target)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.closeCard();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!this.drawerOpen) {
|
|
324
399
|
return;
|
|
325
400
|
}
|
|
326
|
-
|
|
401
|
+
|
|
402
|
+
if (
|
|
403
|
+
target instanceof Node
|
|
404
|
+
&& (
|
|
405
|
+
this.commentsDrawer?.contains(target)
|
|
406
|
+
|| this.commentsToggleButton?.contains(target)
|
|
407
|
+
)
|
|
408
|
+
) {
|
|
327
409
|
return;
|
|
328
410
|
}
|
|
329
|
-
|
|
411
|
+
|
|
412
|
+
this.closeDrawer();
|
|
330
413
|
};
|
|
331
414
|
this.handleDocumentKeyDown = (event) => {
|
|
415
|
+
if (event.key === 'Escape' && this.reactionPicker) {
|
|
416
|
+
this.reactionPicker = null;
|
|
417
|
+
this.renderCard();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
332
420
|
if (event.key === 'Escape' && this.activeCard) {
|
|
333
421
|
this.closeCard();
|
|
334
422
|
}
|
|
@@ -340,12 +428,12 @@ export class CommentUiController {
|
|
|
340
428
|
}
|
|
341
429
|
};
|
|
342
430
|
|
|
431
|
+
this.commentSelectionButton?.addEventListener('pointerdown', this.handleCommentSelectionButtonPointerDown);
|
|
343
432
|
this.commentSelectionButton?.addEventListener('click', () => {
|
|
344
433
|
this.openComposerForSelection('toolbar');
|
|
345
434
|
});
|
|
346
435
|
this.commentsToggleButton?.addEventListener('click', () => {
|
|
347
|
-
this.
|
|
348
|
-
this.render();
|
|
436
|
+
this.setDrawerOpen(!this.drawerOpen);
|
|
349
437
|
});
|
|
350
438
|
this.previewContainer?.addEventListener('scroll', this.handlePreviewScroll, { passive: true });
|
|
351
439
|
this.editorContainer?.addEventListener('pointerdown', this.handleEditorPointerDown);
|
|
@@ -364,6 +452,7 @@ export class CommentUiController {
|
|
|
364
452
|
}
|
|
365
453
|
this.attachSession(null);
|
|
366
454
|
this.previewContainer?.removeEventListener('scroll', this.handlePreviewScroll);
|
|
455
|
+
this.commentSelectionButton?.removeEventListener('pointerdown', this.handleCommentSelectionButtonPointerDown);
|
|
367
456
|
this.editorContainer?.removeEventListener('pointerdown', this.handleEditorPointerDown);
|
|
368
457
|
this.editorContainer?.removeEventListener('focusout', this.handleEditorFocusOut);
|
|
369
458
|
window.removeEventListener('resize', this.handleWindowResize);
|
|
@@ -383,6 +472,7 @@ export class CommentUiController {
|
|
|
383
472
|
this.selectionAnchor = session?.getCurrentSelectionCommentAnchor?.() ?? null;
|
|
384
473
|
this.pendingSelectionAnchor = null;
|
|
385
474
|
this.committedSelectionAnchor = null;
|
|
475
|
+
this.reactionPicker = null;
|
|
386
476
|
this.clearSelectionRevealTimer();
|
|
387
477
|
this.pointerSelecting = false;
|
|
388
478
|
session?.getScrollContainer?.()?.addEventListener('scroll', this.handleEditorScroll, { passive: true });
|
|
@@ -403,6 +493,7 @@ export class CommentUiController {
|
|
|
403
493
|
this.clearSelectionRevealTimer();
|
|
404
494
|
this.pointerSelecting = false;
|
|
405
495
|
this.activeCard = null;
|
|
496
|
+
this.reactionPicker = null;
|
|
406
497
|
}
|
|
407
498
|
if (!this.supported) {
|
|
408
499
|
this.drawerOpen = false;
|
|
@@ -413,6 +504,7 @@ export class CommentUiController {
|
|
|
413
504
|
this.committedSelectionAnchor = null;
|
|
414
505
|
this.clearSelectionRevealTimer();
|
|
415
506
|
this.pointerSelecting = false;
|
|
507
|
+
this.reactionPicker = null;
|
|
416
508
|
}
|
|
417
509
|
this.render();
|
|
418
510
|
}
|
|
@@ -483,9 +575,36 @@ export class CommentUiController {
|
|
|
483
575
|
) {
|
|
484
576
|
this.activeCard = null;
|
|
485
577
|
}
|
|
578
|
+
if (
|
|
579
|
+
this.reactionPicker
|
|
580
|
+
&& !this.threads.some((thread) => (
|
|
581
|
+
thread.id === this.reactionPicker.threadId
|
|
582
|
+
&& thread.messages?.some((message) => message.id === this.reactionPicker.messageId)
|
|
583
|
+
))
|
|
584
|
+
) {
|
|
585
|
+
this.reactionPicker = null;
|
|
586
|
+
}
|
|
587
|
+
this.render();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
setDrawerOpen(nextState) {
|
|
591
|
+
const normalizedState = Boolean(nextState);
|
|
592
|
+
if (normalizedState === this.drawerOpen) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (normalizedState) {
|
|
597
|
+
this.onWillOpenDrawer?.();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
this.drawerOpen = normalizedState;
|
|
486
601
|
this.render();
|
|
487
602
|
}
|
|
488
603
|
|
|
604
|
+
closeDrawer() {
|
|
605
|
+
this.setDrawerOpen(false);
|
|
606
|
+
}
|
|
607
|
+
|
|
489
608
|
refreshLayout() {
|
|
490
609
|
this.renderEditorLayer();
|
|
491
610
|
this.renderPreviewLayer();
|
|
@@ -578,11 +697,26 @@ export class CommentUiController {
|
|
|
578
697
|
quote.className = 'comments-drawer-item-quote';
|
|
579
698
|
quote.textContent = group.anchor.quote || group.anchor.excerpt || 'Source anchored comment';
|
|
580
699
|
|
|
700
|
+
const latestMessage = getLatestGroupMessage(group);
|
|
701
|
+
const preview = createRenderedCommentBody(
|
|
702
|
+
latestMessage?.body || '',
|
|
703
|
+
'comment-markdown comments-drawer-item-preview',
|
|
704
|
+
);
|
|
705
|
+
|
|
581
706
|
const footer = document.createElement('div');
|
|
582
707
|
footer.className = 'comments-drawer-item-footer';
|
|
583
|
-
|
|
708
|
+
const countLabel = document.createElement('span');
|
|
709
|
+
countLabel.textContent = `${group.threads.length} thread${group.threads.length === 1 ? '' : 's'}`;
|
|
584
710
|
|
|
585
|
-
|
|
711
|
+
const updatedLabel = document.createElement('span');
|
|
712
|
+
updatedLabel.className = 'comments-drawer-item-updated';
|
|
713
|
+
updatedLabel.textContent = latestMessage
|
|
714
|
+
? `${latestMessage.userName} • ${this.formatTimestamp(latestMessage.createdAt)}`
|
|
715
|
+
: '';
|
|
716
|
+
|
|
717
|
+
footer.append(countLabel, updatedLabel);
|
|
718
|
+
|
|
719
|
+
button.append(header, quote, preview, footer);
|
|
586
720
|
fragment.appendChild(button);
|
|
587
721
|
});
|
|
588
722
|
|
|
@@ -811,6 +945,7 @@ export class CommentUiController {
|
|
|
811
945
|
}
|
|
812
946
|
|
|
813
947
|
this.selectionAnchor = anchor;
|
|
948
|
+
this.reactionPicker = null;
|
|
814
949
|
const nextOrigin = origin === 'editor' && sourceRect ? 'editor-chip' : origin;
|
|
815
950
|
const nextSourceRect = sourceRect ?? (origin === 'toolbar'
|
|
816
951
|
? this.commentSelectionButton?.getBoundingClientRect?.()
|
|
@@ -826,6 +961,7 @@ export class CommentUiController {
|
|
|
826
961
|
}
|
|
827
962
|
|
|
828
963
|
openThreadGroup(group, { anchor, origin, sourceRect }) {
|
|
964
|
+
this.reactionPicker = null;
|
|
829
965
|
this.activeCard = {
|
|
830
966
|
anchor,
|
|
831
967
|
groupKey: group.key,
|
|
@@ -841,6 +977,7 @@ export class CommentUiController {
|
|
|
841
977
|
closeCard() {
|
|
842
978
|
this.activeCard = null;
|
|
843
979
|
this.pendingCardFocusElement = null;
|
|
980
|
+
this.reactionPicker = null;
|
|
844
981
|
this.renderCard();
|
|
845
982
|
this.scheduleLayoutRefresh();
|
|
846
983
|
}
|
|
@@ -902,6 +1039,21 @@ export class CommentUiController {
|
|
|
902
1039
|
|
|
903
1040
|
const card = document.createElement('section');
|
|
904
1041
|
card.className = 'comment-card';
|
|
1042
|
+
card.addEventListener('click', (event) => {
|
|
1043
|
+
if (
|
|
1044
|
+
!this.reactionPicker
|
|
1045
|
+
|| event.target?.closest?.('.comment-reaction-picker-wrap')
|
|
1046
|
+
) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
this.reactionPicker = null;
|
|
1051
|
+
requestAnimationFrame(() => {
|
|
1052
|
+
if (this.activeCard) {
|
|
1053
|
+
this.renderCard();
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
905
1057
|
|
|
906
1058
|
const header = document.createElement('div');
|
|
907
1059
|
header.className = 'comment-card-header';
|
|
@@ -929,15 +1081,18 @@ export class CommentUiController {
|
|
|
929
1081
|
header.append(titleWrap, closeButton);
|
|
930
1082
|
card.appendChild(header);
|
|
931
1083
|
|
|
1084
|
+
const content = document.createElement('div');
|
|
1085
|
+
content.className = 'comment-card-scroll';
|
|
1086
|
+
|
|
932
1087
|
if (this.activeCard.anchor?.quote) {
|
|
933
1088
|
const quote = document.createElement('p');
|
|
934
1089
|
quote.className = 'comment-card-quote';
|
|
935
1090
|
quote.textContent = this.activeCard.anchor.quote;
|
|
936
|
-
|
|
1091
|
+
content.appendChild(quote);
|
|
937
1092
|
}
|
|
938
1093
|
|
|
939
1094
|
if (this.activeCard.mode === 'create') {
|
|
940
|
-
|
|
1095
|
+
content.appendChild(this.createComposer());
|
|
941
1096
|
} else {
|
|
942
1097
|
const group = this.getThreadGroups().find((entry) => entry.key === this.activeCard.groupKey);
|
|
943
1098
|
if (!group) {
|
|
@@ -946,16 +1101,46 @@ export class CommentUiController {
|
|
|
946
1101
|
}
|
|
947
1102
|
|
|
948
1103
|
group.threads.forEach((thread) => {
|
|
949
|
-
|
|
1104
|
+
content.appendChild(this.createThreadElement(thread));
|
|
950
1105
|
});
|
|
951
1106
|
}
|
|
952
1107
|
|
|
1108
|
+
root.style.visibility = 'hidden';
|
|
1109
|
+
card.appendChild(content);
|
|
953
1110
|
root.appendChild(card);
|
|
1111
|
+
this.updateReactionPickerPosition(card);
|
|
1112
|
+
this.positionCard(card);
|
|
1113
|
+
root.style.visibility = '';
|
|
954
1114
|
this.flushPendingCardFocus();
|
|
955
|
-
root.style.visibility = 'hidden';
|
|
956
1115
|
this.scheduleLayoutRefresh();
|
|
957
1116
|
}
|
|
958
1117
|
|
|
1118
|
+
updateReactionPickerPosition(card) {
|
|
1119
|
+
const bounds = getReactionPickerBounds(card);
|
|
1120
|
+
if (!bounds) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const { picker, scroll, wrap } = bounds;
|
|
1125
|
+
picker.classList.remove('is-upward');
|
|
1126
|
+
picker.style.maxHeight = '';
|
|
1127
|
+
|
|
1128
|
+
const wrapRect = wrap.getBoundingClientRect();
|
|
1129
|
+
const scrollRect = scroll.getBoundingClientRect();
|
|
1130
|
+
const pickerRect = picker.getBoundingClientRect();
|
|
1131
|
+
const safeViewportTop = 12;
|
|
1132
|
+
const safeViewportBottom = window.innerHeight - 12;
|
|
1133
|
+
const lowerBoundary = Math.min(scrollRect.bottom, safeViewportBottom);
|
|
1134
|
+
const upperBoundary = Math.max(scrollRect.top, safeViewportTop);
|
|
1135
|
+
const availableBelow = Math.max(lowerBoundary - wrapRect.bottom - 8, 0);
|
|
1136
|
+
const availableAbove = Math.max(wrapRect.top - upperBoundary - 8, 0);
|
|
1137
|
+
const shouldOpenUpward = pickerRect.height > availableBelow && availableAbove > availableBelow;
|
|
1138
|
+
const maxHeight = Math.max((shouldOpenUpward ? availableAbove : availableBelow), 120);
|
|
1139
|
+
|
|
1140
|
+
picker.classList.toggle('is-upward', shouldOpenUpward);
|
|
1141
|
+
picker.style.maxHeight = `${maxHeight}px`;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
959
1144
|
repositionActiveCard() {
|
|
960
1145
|
const card = this.cardRoot?.firstElementChild;
|
|
961
1146
|
if (!card || !this.activeCard) {
|
|
@@ -1022,6 +1207,9 @@ export class CommentUiController {
|
|
|
1022
1207
|
const header = document.createElement('div');
|
|
1023
1208
|
header.className = 'comment-thread-card-header';
|
|
1024
1209
|
|
|
1210
|
+
const heading = document.createElement('div');
|
|
1211
|
+
heading.className = 'comment-thread-card-heading';
|
|
1212
|
+
|
|
1025
1213
|
const author = document.createElement('span');
|
|
1026
1214
|
author.className = 'comment-thread-card-author';
|
|
1027
1215
|
author.textContent = thread.createdByName;
|
|
@@ -1065,12 +1253,13 @@ export class CommentUiController {
|
|
|
1065
1253
|
});
|
|
1066
1254
|
|
|
1067
1255
|
actions.append(jump, reply, resolve);
|
|
1068
|
-
|
|
1256
|
+
heading.append(author, time);
|
|
1257
|
+
header.append(heading, actions);
|
|
1069
1258
|
|
|
1070
1259
|
article.append(header);
|
|
1071
1260
|
|
|
1072
1261
|
thread.messages.forEach((message) => {
|
|
1073
|
-
article.appendChild(this.createMessageElement(message));
|
|
1262
|
+
article.appendChild(this.createMessageElement(thread, message));
|
|
1074
1263
|
});
|
|
1075
1264
|
|
|
1076
1265
|
if (this.activeCard?.replyThreadId === thread.id) {
|
|
@@ -1080,7 +1269,7 @@ export class CommentUiController {
|
|
|
1080
1269
|
return article;
|
|
1081
1270
|
}
|
|
1082
1271
|
|
|
1083
|
-
createMessageElement(message) {
|
|
1272
|
+
createMessageElement(thread, message) {
|
|
1084
1273
|
const container = document.createElement('div');
|
|
1085
1274
|
container.className = 'comment-message-card';
|
|
1086
1275
|
|
|
@@ -1095,15 +1284,133 @@ export class CommentUiController {
|
|
|
1095
1284
|
time.className = 'comment-message-card-time';
|
|
1096
1285
|
time.textContent = this.formatTimestamp(message.createdAt);
|
|
1097
1286
|
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1287
|
+
const renderedBody = createRenderedCommentBody(
|
|
1288
|
+
message.body,
|
|
1289
|
+
'comment-message-card-body comment-markdown',
|
|
1290
|
+
);
|
|
1101
1291
|
|
|
1102
1292
|
meta.append(author, time);
|
|
1103
|
-
container.append(meta,
|
|
1293
|
+
container.append(meta, renderedBody);
|
|
1294
|
+
container.appendChild(this.createReactionBar(thread, message));
|
|
1104
1295
|
return container;
|
|
1105
1296
|
}
|
|
1106
1297
|
|
|
1298
|
+
createReactionBar(thread, message) {
|
|
1299
|
+
const localUserId = this.session?.getLocalUser?.()?.userId ?? '';
|
|
1300
|
+
const wrap = document.createElement('div');
|
|
1301
|
+
wrap.className = 'comment-reaction-bar';
|
|
1302
|
+
|
|
1303
|
+
const existingReactionEmojis = new Set((message.reactions ?? []).map((reaction) => reaction.emoji));
|
|
1304
|
+
|
|
1305
|
+
const chips = document.createElement('div');
|
|
1306
|
+
chips.className = 'comment-reaction-chips';
|
|
1307
|
+
|
|
1308
|
+
(message.reactions ?? []).forEach((reaction) => {
|
|
1309
|
+
const chip = document.createElement('button');
|
|
1310
|
+
chip.type = 'button';
|
|
1311
|
+
chip.className = 'comment-reaction-chip';
|
|
1312
|
+
chip.classList.toggle('is-active', hasLocalReaction(reaction, localUserId));
|
|
1313
|
+
chip.setAttribute('aria-pressed', String(hasLocalReaction(reaction, localUserId)));
|
|
1314
|
+
chip.title = reaction.users?.map((user) => user.userName).join(', ') || reaction.emoji;
|
|
1315
|
+
|
|
1316
|
+
const emoji = document.createElement('span');
|
|
1317
|
+
emoji.className = 'comment-reaction-chip-emoji';
|
|
1318
|
+
emoji.textContent = reaction.emoji;
|
|
1319
|
+
|
|
1320
|
+
const count = document.createElement('span');
|
|
1321
|
+
count.className = 'comment-reaction-chip-count';
|
|
1322
|
+
count.textContent = formatReactionCount(reaction);
|
|
1323
|
+
|
|
1324
|
+
chip.append(emoji, count);
|
|
1325
|
+
chip.addEventListener('click', async () => {
|
|
1326
|
+
await this.onToggleReaction?.(thread.id, message.id, reaction.emoji);
|
|
1327
|
+
});
|
|
1328
|
+
chips.appendChild(chip);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
const actions = document.createElement('div');
|
|
1332
|
+
actions.className = 'comment-reaction-actions';
|
|
1333
|
+
|
|
1334
|
+
COMMENT_REACTION_PRESET_EMOJIS
|
|
1335
|
+
.filter((emoji) => !existingReactionEmojis.has(emoji))
|
|
1336
|
+
.forEach((emoji) => {
|
|
1337
|
+
actions.appendChild(this.createQuickReactionButton(thread, message, emoji));
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
const pickerWrap = document.createElement('div');
|
|
1341
|
+
pickerWrap.className = 'comment-reaction-picker-wrap';
|
|
1342
|
+
|
|
1343
|
+
const moreButton = document.createElement('button');
|
|
1344
|
+
moreButton.type = 'button';
|
|
1345
|
+
moreButton.className = 'comment-reaction-more-trigger';
|
|
1346
|
+
moreButton.dataset.reactionPickerToggle = 'true';
|
|
1347
|
+
moreButton.setAttribute('aria-expanded', String(
|
|
1348
|
+
isReactionPickerOpen(this.reactionPicker, thread.id, message.id),
|
|
1349
|
+
));
|
|
1350
|
+
moreButton.textContent = 'More';
|
|
1351
|
+
moreButton.addEventListener('click', () => {
|
|
1352
|
+
const isOpen = isReactionPickerOpen(this.reactionPicker, thread.id, message.id);
|
|
1353
|
+
this.reactionPicker = isOpen
|
|
1354
|
+
? null
|
|
1355
|
+
: {
|
|
1356
|
+
messageId: message.id,
|
|
1357
|
+
threadId: thread.id,
|
|
1358
|
+
};
|
|
1359
|
+
this.renderCard();
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
pickerWrap.appendChild(moreButton);
|
|
1363
|
+
|
|
1364
|
+
if (isReactionPickerOpen(this.reactionPicker, thread.id, message.id)) {
|
|
1365
|
+
pickerWrap.appendChild(this.createReactionPicker(thread, message));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
actions.appendChild(pickerWrap);
|
|
1369
|
+
wrap.append(chips, actions);
|
|
1370
|
+
return wrap;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
createQuickReactionButton(thread, message, emoji) {
|
|
1374
|
+
const button = document.createElement('button');
|
|
1375
|
+
button.type = 'button';
|
|
1376
|
+
button.className = 'comment-reaction-quick-add';
|
|
1377
|
+
button.textContent = emoji;
|
|
1378
|
+
button.title = `React with ${emoji}`;
|
|
1379
|
+
button.addEventListener('click', async () => {
|
|
1380
|
+
await this.onToggleReaction?.(thread.id, message.id, emoji);
|
|
1381
|
+
});
|
|
1382
|
+
return button;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
createReactionPicker(thread, message) {
|
|
1386
|
+
const picker = document.createElement('div');
|
|
1387
|
+
picker.className = 'comment-reaction-picker';
|
|
1388
|
+
const moreGrid = document.createElement('div');
|
|
1389
|
+
moreGrid.className = 'comment-reaction-picker-grid';
|
|
1390
|
+
COMMENT_REACTION_MORE_EMOJIS.forEach((emoji) => {
|
|
1391
|
+
moreGrid.appendChild(this.createReactionPickerButton(thread, message, emoji));
|
|
1392
|
+
});
|
|
1393
|
+
picker.appendChild(moreGrid);
|
|
1394
|
+
|
|
1395
|
+
return picker;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
createReactionPickerButton(thread, message, emoji) {
|
|
1399
|
+
const button = document.createElement('button');
|
|
1400
|
+
button.type = 'button';
|
|
1401
|
+
button.className = 'comment-reaction-picker-btn';
|
|
1402
|
+
button.textContent = emoji;
|
|
1403
|
+
button.title = `React with ${emoji}`;
|
|
1404
|
+
button.addEventListener('click', async () => {
|
|
1405
|
+
const didToggle = await this.onToggleReaction?.(thread.id, message.id, emoji);
|
|
1406
|
+
if (didToggle) {
|
|
1407
|
+
this.reactionPicker = null;
|
|
1408
|
+
this.renderCard();
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
return button;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1107
1414
|
createReplyComposer(thread) {
|
|
1108
1415
|
const form = document.createElement('form');
|
|
1109
1416
|
form.className = 'comment-reply-form';
|
|
@@ -1204,7 +1511,32 @@ export class CommentUiController {
|
|
|
1204
1511
|
}
|
|
1205
1512
|
|
|
1206
1513
|
this.pendingCardFocusElement = null;
|
|
1207
|
-
|
|
1514
|
+
const focusElement = () => {
|
|
1515
|
+
if (!element.isConnected) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const activeElement = document.activeElement;
|
|
1520
|
+
if (activeElement instanceof HTMLElement && this.editorContainer?.contains(activeElement)) {
|
|
1521
|
+
activeElement.blur();
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
element.focus({ preventScroll: true });
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
focusElement();
|
|
1528
|
+
if (document.activeElement === element) {
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
requestAnimationFrame(() => {
|
|
1533
|
+
focusElement();
|
|
1534
|
+
if (document.activeElement !== element) {
|
|
1535
|
+
setTimeout(() => {
|
|
1536
|
+
focusElement();
|
|
1537
|
+
}, 50);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1208
1540
|
}
|
|
1209
1541
|
|
|
1210
1542
|
clearSelectionRevealTimer() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isImageAttachmentFilePath } from '../../domain/file-kind.js';
|
|
1
2
|
import { vaultApiClient } from '../domain/vault-api-client.js';
|
|
2
3
|
import { FileActionController } from './file-action-controller.js';
|
|
3
4
|
import { FileTreeState } from './file-tree-state.js';
|
|
@@ -69,6 +70,10 @@ export class FileExplorerController {
|
|
|
69
70
|
return this.state.flatFiles;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
get flatDocumentFiles() {
|
|
74
|
+
return this.state.flatFiles.filter((path) => !isImageAttachmentFilePath(path));
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
renderTree() {
|
|
73
78
|
this.view.render({
|
|
74
79
|
activeFilePath: this.state.activeFilePath,
|
|
@@ -161,12 +161,16 @@ export class FileExplorerView {
|
|
|
161
161
|
const button = document.createElement('button');
|
|
162
162
|
button.className = 'file-tree-item file-tree-file';
|
|
163
163
|
const isExcalidraw = fileType === 'excalidraw';
|
|
164
|
+
const isImage = fileType === 'image';
|
|
164
165
|
const isMermaid = fileType === 'mermaid';
|
|
165
166
|
const isPlantUml = fileType === 'plantuml';
|
|
166
167
|
|
|
167
168
|
if (isExcalidraw) {
|
|
168
169
|
button.classList.add('is-excalidraw');
|
|
169
170
|
}
|
|
171
|
+
if (isImage) {
|
|
172
|
+
button.classList.add('is-image');
|
|
173
|
+
}
|
|
170
174
|
if (isMermaid) {
|
|
171
175
|
button.classList.add('is-mermaid');
|
|
172
176
|
}
|
|
@@ -181,7 +185,7 @@ export class FileExplorerView {
|
|
|
181
185
|
button.dataset.depth = depth;
|
|
182
186
|
button.dataset.path = filePath;
|
|
183
187
|
button.innerHTML = `
|
|
184
|
-
${this.getFileIconSvg({ isExcalidraw, isMermaid, isPlantUml })}
|
|
188
|
+
${this.getFileIconSvg({ isExcalidraw, isImage, isMermaid, isPlantUml })}
|
|
185
189
|
<span class="file-tree-name">${escapeHtml(stripVaultFileExtension(name))}</span>
|
|
186
190
|
`;
|
|
187
191
|
|
|
@@ -196,11 +200,15 @@ export class FileExplorerView {
|
|
|
196
200
|
return button;
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
getFileIconSvg({ isExcalidraw, isMermaid, isPlantUml }) {
|
|
203
|
+
getFileIconSvg({ isExcalidraw, isImage, isMermaid, isPlantUml }) {
|
|
200
204
|
if (isExcalidraw) {
|
|
201
205
|
return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>';
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
if (isImage) {
|
|
209
|
+
return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><path d="m21 16-5-5L7 20"/><path d="m14 14 2 2"/></svg>';
|
|
210
|
+
}
|
|
211
|
+
|
|
204
212
|
if (isMermaid) {
|
|
205
213
|
return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7.5c0-1.38 1.12-2.5 2.5-2.5 1.04 0 1.93.64 2.3 1.56A2.5 2.5 0 0 1 14 8.5v1"/><path d="M19 16.5c0 1.38-1.12 2.5-2.5 2.5-1.04 0-1.93-.64-2.3-1.56A2.5 2.5 0 0 1 10 15.5v-1"/><path d="M8 10.5h8"/><path d="M8 13.5h8"/><path d="M10 8.5v7"/><path d="M14 8.5v7"/></svg>';
|
|
206
214
|
}
|
|
@@ -2,7 +2,13 @@ import { normalizeVaultPathInput } from '../domain/vault-paths.js';
|
|
|
2
2
|
|
|
3
3
|
function flattenTree(nodes, files = []) {
|
|
4
4
|
for (const node of nodes) {
|
|
5
|
-
if (
|
|
5
|
+
if (
|
|
6
|
+
node.type === 'file'
|
|
7
|
+
|| node.type === 'excalidraw'
|
|
8
|
+
|| node.type === 'mermaid'
|
|
9
|
+
|| node.type === 'plantuml'
|
|
10
|
+
|| node.type === 'image'
|
|
11
|
+
) {
|
|
6
12
|
files.push(node.path);
|
|
7
13
|
continue;
|
|
8
14
|
}
|