collabmd 0.1.19 → 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 +10 -10
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/{editor-session-AH6Z3MXW.js → editor-session-TAV2VXTA.js} +11 -11
- package/public/assets/js/main.js +95 -68
- package/public/index.html +14 -4
- package/src/client/application/app-shell/git-feature.js +29 -1
- package/src/client/application/app-shell-elements.js +1 -0
- package/src/client/bootstrap/collabmd-app-shell.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +64 -6
- package/src/client/presentation/backlinks-panel.js +157 -101
- package/src/client/presentation/comment-ui-controller.js +41 -5
- 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 +138 -7
- 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/persistence/pull-backup-store.js +283 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +16 -0
package/public/index.html
CHANGED
|
@@ -364,16 +364,26 @@
|
|
|
364
364
|
<div class="preview-body">
|
|
365
365
|
<div id="previewContainer" class="preview-container">
|
|
366
366
|
<div id="previewContent" class="preview-content"></div>
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
<div id="backlinksInlinePanel" class="backlinks-panel backlinks-panel-inline hidden">
|
|
368
|
+
<div class="backlinks-header" role="button" tabindex="0" aria-expanded="false">
|
|
369
|
+
<svg class="backlinks-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
370
|
+
<span class="backlinks-toggle">0 Linked Mentions</span>
|
|
371
|
+
<span class="backlinks-count"></span>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="backlinks-body" aria-hidden="true" inert>
|
|
374
|
+
<div class="backlinks-list"></div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div id="backlinksPanel" class="backlinks-panel-layer hidden">
|
|
379
|
+
<div class="backlinks-panel backlinks-panel-dock" data-backlinks-variant="dock">
|
|
370
380
|
<div class="backlinks-header" role="button" tabindex="0" aria-expanded="false">
|
|
371
381
|
<svg class="backlinks-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
372
382
|
<span class="backlinks-toggle">0 Linked Mentions</span>
|
|
373
383
|
<span class="backlinks-count"></span>
|
|
374
384
|
</div>
|
|
375
385
|
<div class="backlinks-body" aria-hidden="true" inert>
|
|
376
|
-
<div class="backlinks-list"
|
|
386
|
+
<div class="backlinks-list"></div>
|
|
377
387
|
</div>
|
|
378
388
|
</div>
|
|
379
389
|
</div>
|
|
@@ -12,6 +12,17 @@ function normalizeWorkspaceChange(workspaceChange = {}) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const gitFeature = {
|
|
15
|
+
formatPullBackupToast(pullBackup = null) {
|
|
16
|
+
const fileCount = Number(pullBackup?.fileCount || 0);
|
|
17
|
+
if (fileCount === 1) {
|
|
18
|
+
return 'Pulled latest changes. 1 overlapping local file was backed up.';
|
|
19
|
+
}
|
|
20
|
+
if (fileCount > 1) {
|
|
21
|
+
return `Pulled latest changes. ${fileCount} overlapping local files were backed up.`;
|
|
22
|
+
}
|
|
23
|
+
return 'Pulled latest changes. Overlapping local changes were recorded in a pull backup.';
|
|
24
|
+
},
|
|
25
|
+
|
|
15
26
|
setGitOperationStatus(message = '') {
|
|
16
27
|
const badge = this.elements.gitOperationStatus;
|
|
17
28
|
if (!badge) {
|
|
@@ -92,7 +103,11 @@ export const gitFeature = {
|
|
|
92
103
|
});
|
|
93
104
|
const data = await response.json();
|
|
94
105
|
if (!response.ok) {
|
|
95
|
-
|
|
106
|
+
const error = new Error(data.error || 'Git action failed');
|
|
107
|
+
if (typeof data?.code === 'string') {
|
|
108
|
+
error.code = data.code;
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
96
111
|
}
|
|
97
112
|
return data;
|
|
98
113
|
},
|
|
@@ -242,7 +257,20 @@ export const gitFeature = {
|
|
|
242
257
|
result,
|
|
243
258
|
showLocalFileToast: true,
|
|
244
259
|
});
|
|
260
|
+
if (result.pullBackup) {
|
|
261
|
+
this.toastController.show(this.formatPullBackupToast(result.pullBackup), 5000);
|
|
262
|
+
}
|
|
245
263
|
} catch (error) {
|
|
264
|
+
if (error?.code === 'pull_diverged_ff_only') {
|
|
265
|
+
this.toastController.show('Cannot pull because local and remote commits have diverged. Fast-forward only pull is not possible.', 5000);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (error?.code === 'pull_conflicted_after_autostash') {
|
|
270
|
+
this.toastController.show('Pull updated the branch, but reapplying local changes caused conflicts. Review the conflicted files and the pull backup summary.', 6000);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
246
274
|
this.toastController.show(error.message || 'Failed to pull branch');
|
|
247
275
|
}
|
|
248
276
|
});
|
|
@@ -61,6 +61,7 @@ export function bindAppShellElements(doc = document) {
|
|
|
61
61
|
emptyStateNewFileBtn: doc.getElementById('emptyStateNewFileBtn'),
|
|
62
62
|
emptyStateSearchBtn: doc.getElementById('emptyStateSearchBtn'),
|
|
63
63
|
backlinksPanel: doc.getElementById('backlinksPanel'),
|
|
64
|
+
backlinksInlinePanel: doc.getElementById('backlinksInlinePanel'),
|
|
64
65
|
commentSelectionButton: doc.getElementById('commentSelectionBtn'),
|
|
65
66
|
commentsDrawer: doc.getElementById('commentsDrawer'),
|
|
66
67
|
commentsDrawerEmpty: doc.getElementById('commentsDrawerEmpty'),
|
|
@@ -131,6 +131,7 @@ export class CollabMdAppShell {
|
|
|
131
131
|
this.gitPanel = new GitPanelController({
|
|
132
132
|
enabled: this.runtimeConfig.gitEnabled !== false,
|
|
133
133
|
onCommitStaged: () => this.openGitCommitDialog(),
|
|
134
|
+
onOpenPullBackup: (filePath) => filePath && this.navigation.navigateToFile(filePath),
|
|
134
135
|
onPullBranch: () => this.pullGitBranch(),
|
|
135
136
|
onPushBranch: () => this.pushGitBranch(),
|
|
136
137
|
onRepoChange: (isGitRepo, status) => this.handleGitRepoChange(isGitRepo, status),
|
|
@@ -149,6 +150,7 @@ export class CollabMdAppShell {
|
|
|
149
150
|
this.scrollSyncController.suspendSync(250);
|
|
150
151
|
this.session?.scrollToLine(sourceLine, 0);
|
|
151
152
|
},
|
|
153
|
+
onWillOpen: () => this.commentUi?.closeDrawer(),
|
|
152
154
|
});
|
|
153
155
|
this.videoEmbed = new VideoEmbedController({
|
|
154
156
|
previewElement: this.elements.previewContent,
|
|
@@ -196,6 +198,7 @@ export class CollabMdAppShell {
|
|
|
196
198
|
scrollEditorToLine: (lineNumber, viewportRatio) => this.session?.scrollToLine(lineNumber, viewportRatio),
|
|
197
199
|
});
|
|
198
200
|
this.backlinksPanel = new BacklinksPanel({
|
|
201
|
+
inlinePanelElement: this.elements.backlinksInlinePanel,
|
|
199
202
|
onFileSelect: (filePath) => this.handleFileSelection(filePath, { closeSidebarOnMobile: true }),
|
|
200
203
|
panelElement: this.elements.backlinksPanel,
|
|
201
204
|
});
|
|
@@ -214,6 +217,7 @@ export class CollabMdAppShell {
|
|
|
214
217
|
commentsDrawerList: this.elements.commentsDrawerList,
|
|
215
218
|
commentsToggleButton: this.elements.commentsToggleButton,
|
|
216
219
|
editorContainer: this.elements.editorContainer,
|
|
220
|
+
onWillOpenDrawer: () => this.outlineController.close(),
|
|
217
221
|
previewContainer: this.elements.previewContainer,
|
|
218
222
|
previewElement: this.elements.previewContent,
|
|
219
223
|
onCreateThread: ({ anchor, body }) => this.session?.createCommentThread({ anchor, body }),
|
|
@@ -14,6 +14,7 @@ import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
|
|
14
14
|
import { Compartment, EditorSelection, EditorState, Prec } from '@codemirror/state';
|
|
15
15
|
import { oneDark } from '@codemirror/theme-one-dark';
|
|
16
16
|
import {
|
|
17
|
+
Decoration,
|
|
17
18
|
EditorView,
|
|
18
19
|
crosshairCursor,
|
|
19
20
|
drawSelection,
|
|
@@ -33,6 +34,38 @@ import { plantUmlLanguage, plantUmlLanguageDescription } from '../domain/plantum
|
|
|
33
34
|
import { handleImagePasteEvent } from './editor-paste-utils.js';
|
|
34
35
|
|
|
35
36
|
const markdownCodeLanguages = [...languages, plantUmlLanguageDescription];
|
|
37
|
+
const pairedMatchingBracketMark = Decoration.mark({ class: 'cm-matchingBracket cm-matchingBracket-paired' });
|
|
38
|
+
const nonmatchingBracketMark = Decoration.mark({ class: 'cm-nonmatchingBracket' });
|
|
39
|
+
|
|
40
|
+
function isBracketBeforeCaret(range, state) {
|
|
41
|
+
return state.selection.ranges.some((selectionRange) =>
|
|
42
|
+
selectionRange.empty && range.to === selectionRange.head
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderBracketMatch(match, state) {
|
|
47
|
+
const decorations = [];
|
|
48
|
+
|
|
49
|
+
if (!match.matched) {
|
|
50
|
+
decorations.push(nonmatchingBracketMark.range(match.start.from, match.start.to));
|
|
51
|
+
if (match.end) {
|
|
52
|
+
decorations.push(nonmatchingBracketMark.range(match.end.from, match.end.to));
|
|
53
|
+
}
|
|
54
|
+
return decorations;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const shouldHideStartBracket = isBracketBeforeCaret(match.start, state);
|
|
58
|
+
const shouldHideEndBracket = match.end ? isBracketBeforeCaret(match.end, state) : false;
|
|
59
|
+
|
|
60
|
+
if (!shouldHideStartBracket) {
|
|
61
|
+
decorations.push(pairedMatchingBracketMark.range(match.start.from, match.start.to));
|
|
62
|
+
}
|
|
63
|
+
if (match.end && !shouldHideEndBracket) {
|
|
64
|
+
decorations.push(pairedMatchingBracketMark.range(match.end.from, match.end.to));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return decorations;
|
|
68
|
+
}
|
|
36
69
|
|
|
37
70
|
function createEditorTheme(theme) {
|
|
38
71
|
const activeLineBackground = theme === 'dark'
|
|
@@ -47,6 +80,21 @@ function createEditorTheme(theme) {
|
|
|
47
80
|
const selectionBorder = theme === 'dark'
|
|
48
81
|
? 'oklch(from var(--color-primary) l c h / 0.65)'
|
|
49
82
|
: 'oklch(from var(--color-primary) l c h / 0.5)';
|
|
83
|
+
const caretColor = theme === 'dark'
|
|
84
|
+
? 'color-mix(in oklab, var(--color-primary) 78%, white)'
|
|
85
|
+
: 'color-mix(in oklab, var(--color-primary) 84%, black)';
|
|
86
|
+
const bracketPairBackground = theme === 'dark'
|
|
87
|
+
? 'oklch(from var(--color-primary) l c h / 0.2)'
|
|
88
|
+
: 'oklch(from var(--color-primary) l c h / 0.12)';
|
|
89
|
+
const bracketPairOutline = theme === 'dark'
|
|
90
|
+
? 'oklch(from var(--color-primary) calc(l + 0.08) c h / 0.85)'
|
|
91
|
+
: 'oklch(from var(--color-primary) calc(l - 0.04) c h / 0.7)';
|
|
92
|
+
const nonmatchingBracketBackground = theme === 'dark'
|
|
93
|
+
? 'oklch(from var(--color-error) l c h / 0.12)'
|
|
94
|
+
: 'oklch(from var(--color-error) l c h / 0.1)';
|
|
95
|
+
const nonmatchingBracketOutline = theme === 'dark'
|
|
96
|
+
? 'oklch(from var(--color-error) calc(l + 0.08) c h / 0.8)'
|
|
97
|
+
: 'oklch(from var(--color-error) calc(l - 0.04) c h / 0.7)';
|
|
50
98
|
|
|
51
99
|
return EditorView.theme({
|
|
52
100
|
'&': {
|
|
@@ -54,13 +102,14 @@ function createEditorTheme(theme) {
|
|
|
54
102
|
color: 'var(--color-text)',
|
|
55
103
|
},
|
|
56
104
|
'.cm-content': {
|
|
57
|
-
caretColor
|
|
105
|
+
caretColor,
|
|
58
106
|
fontFamily: 'var(--font-mono)',
|
|
59
107
|
padding: '16px 0',
|
|
60
108
|
},
|
|
61
109
|
'.cm-cursor, .cm-dropCursor': {
|
|
62
|
-
borderLeftColor:
|
|
63
|
-
borderLeftWidth: '
|
|
110
|
+
borderLeftColor: caretColor,
|
|
111
|
+
borderLeftWidth: '3px',
|
|
112
|
+
marginLeft: '-1px',
|
|
64
113
|
},
|
|
65
114
|
'.cm-foldPlaceholder': {
|
|
66
115
|
backgroundColor: 'var(--color-surface-dynamic)',
|
|
@@ -86,8 +135,17 @@ function createEditorTheme(theme) {
|
|
|
86
135
|
color: 'var(--color-text-muted)',
|
|
87
136
|
},
|
|
88
137
|
'.cm-matchingBracket': {
|
|
89
|
-
|
|
90
|
-
|
|
138
|
+
borderRadius: '2px',
|
|
139
|
+
color: 'inherit',
|
|
140
|
+
},
|
|
141
|
+
'.cm-matchingBracket.cm-matchingBracket-paired': {
|
|
142
|
+
backgroundColor: bracketPairBackground,
|
|
143
|
+
outline: `1px solid ${bracketPairOutline}`,
|
|
144
|
+
},
|
|
145
|
+
'.cm-nonmatchingBracket': {
|
|
146
|
+
backgroundColor: nonmatchingBracketBackground,
|
|
147
|
+
outline: `1px solid ${nonmatchingBracketOutline}`,
|
|
148
|
+
borderRadius: '2px',
|
|
91
149
|
},
|
|
92
150
|
'.cm-selectionMatch': {
|
|
93
151
|
backgroundColor: 'var(--color-primary-highlight)',
|
|
@@ -191,7 +249,7 @@ export class EditorViewAdapter {
|
|
|
191
249
|
EditorState.allowMultipleSelections.of(true),
|
|
192
250
|
indentOnInput(),
|
|
193
251
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
194
|
-
bracketMatching(),
|
|
252
|
+
bracketMatching({ renderMatch: renderBracketMatch }),
|
|
195
253
|
closeBrackets(),
|
|
196
254
|
autocompletion({
|
|
197
255
|
override: [wikiLinkCompletions(this.getFileList)],
|
|
@@ -2,62 +2,95 @@ import { escapeHtml } from '../domain/vault-utils.js';
|
|
|
2
2
|
import { resolveApiUrl } from '../domain/runtime-paths.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* BacklinksPanel —
|
|
6
|
-
* which files link to the currently open file ("Linked Mentions").
|
|
5
|
+
* BacklinksPanel — renders "Linked Mentions" for the current file.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Desktop uses a docked bottom-left panel outside the preview scroll flow.
|
|
8
|
+
* Mobile keeps an inline collapsible panel at the end of the preview content.
|
|
10
9
|
*/
|
|
11
10
|
export class BacklinksPanel {
|
|
12
|
-
constructor({
|
|
13
|
-
|
|
11
|
+
constructor({
|
|
12
|
+
documentRef = document,
|
|
13
|
+
inlinePanelElement = null,
|
|
14
|
+
onFileSelect,
|
|
15
|
+
panelElement,
|
|
16
|
+
}) {
|
|
17
|
+
this.documentRef = documentRef;
|
|
18
|
+
this.panelRoot = panelElement;
|
|
19
|
+
this.inlinePanel = inlinePanelElement;
|
|
14
20
|
this.onFileSelect = onFileSelect;
|
|
15
21
|
|
|
16
|
-
this.header = this.panel?.querySelector('.backlinks-header');
|
|
17
|
-
this.toggle = this.panel?.querySelector('.backlinks-toggle');
|
|
18
|
-
this.countBadge = this.panel?.querySelector('.backlinks-count');
|
|
19
|
-
this.body = this.panel?.querySelector('.backlinks-body');
|
|
20
|
-
this.list = this.panel?.querySelector('.backlinks-list');
|
|
21
|
-
|
|
22
22
|
this._expanded = false;
|
|
23
23
|
this._currentFile = null;
|
|
24
24
|
this._fetchController = null;
|
|
25
25
|
this._backlinks = [];
|
|
26
26
|
|
|
27
|
+
const desktopPanel = this.panelRoot?.querySelector('[data-backlinks-variant="dock"]') ?? this.panelRoot;
|
|
28
|
+
this.panels = [
|
|
29
|
+
this._createPanelRef(desktopPanel),
|
|
30
|
+
this._createPanelRef(this.inlinePanel),
|
|
31
|
+
].filter(Boolean);
|
|
32
|
+
|
|
33
|
+
this.handleDocumentPointerDown = (event) => {
|
|
34
|
+
if (!this._expanded) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const target = event?.target;
|
|
39
|
+
if (target && this.panels.some(({ panel }) => panel?.contains?.(target))) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.close();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.handleDocumentKeyDown = (event) => {
|
|
47
|
+
if (!this._expanded || event?.key !== 'Escape') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
event.preventDefault?.();
|
|
52
|
+
this.close();
|
|
53
|
+
};
|
|
54
|
+
|
|
27
55
|
this.bindEvents();
|
|
28
56
|
}
|
|
29
57
|
|
|
30
58
|
bindEvents() {
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
this.panels.forEach((refs) => {
|
|
60
|
+
refs.header?.addEventListener('click', () => {
|
|
61
|
+
if (this._backlinks.length === 0) return;
|
|
62
|
+
this._expanded = !this._expanded;
|
|
63
|
+
this._applyExpandState();
|
|
64
|
+
});
|
|
36
65
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
refs.header?.addEventListener('keydown', (event) => {
|
|
67
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
41
70
|
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
refs.header.click();
|
|
73
|
+
});
|
|
44
74
|
});
|
|
75
|
+
|
|
76
|
+
this.documentRef?.addEventListener?.('pointerdown', this.handleDocumentPointerDown);
|
|
77
|
+
this.documentRef?.addEventListener?.('keydown', this.handleDocumentKeyDown);
|
|
45
78
|
}
|
|
46
79
|
|
|
47
|
-
/**
|
|
48
|
-
* Load backlinks for a file. Call on every file open and after
|
|
49
|
-
* content changes (debounced).
|
|
50
|
-
*/
|
|
51
80
|
async load(filePath) {
|
|
81
|
+
const fileChanged = filePath !== this._currentFile;
|
|
52
82
|
this._currentFile = filePath;
|
|
53
83
|
|
|
84
|
+
if (fileChanged) {
|
|
85
|
+
this._expanded = false;
|
|
86
|
+
}
|
|
87
|
+
|
|
54
88
|
if (!filePath) {
|
|
55
89
|
this._backlinks = [];
|
|
56
90
|
this._render();
|
|
57
91
|
return;
|
|
58
92
|
}
|
|
59
93
|
|
|
60
|
-
// Abort any in-flight fetch
|
|
61
94
|
this._fetchController?.abort();
|
|
62
95
|
this._fetchController = new AbortController();
|
|
63
96
|
|
|
@@ -68,7 +101,6 @@ export class BacklinksPanel {
|
|
|
68
101
|
);
|
|
69
102
|
const data = await response.json();
|
|
70
103
|
|
|
71
|
-
// Guard against stale responses (user switched files)
|
|
72
104
|
if (this._currentFile !== filePath) return;
|
|
73
105
|
|
|
74
106
|
this._backlinks = data.backlinks ?? [];
|
|
@@ -81,7 +113,6 @@ export class BacklinksPanel {
|
|
|
81
113
|
this._render();
|
|
82
114
|
}
|
|
83
115
|
|
|
84
|
-
/** Clear the panel (e.g. when navigating to empty state). */
|
|
85
116
|
clear() {
|
|
86
117
|
this._currentFile = null;
|
|
87
118
|
this._backlinks = [];
|
|
@@ -89,100 +120,125 @@ export class BacklinksPanel {
|
|
|
89
120
|
this._render();
|
|
90
121
|
}
|
|
91
122
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
close() {
|
|
124
|
+
if (!this._expanded) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
96
127
|
|
|
97
|
-
|
|
98
|
-
|
|
128
|
+
this._expanded = false;
|
|
129
|
+
this._applyExpandState();
|
|
130
|
+
}
|
|
99
131
|
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
_createPanelRef(panel) {
|
|
133
|
+
if (!panel) {
|
|
134
|
+
return null;
|
|
102
135
|
}
|
|
103
136
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
return {
|
|
138
|
+
panel,
|
|
139
|
+
header: panel.querySelector('.backlinks-header'),
|
|
140
|
+
toggle: panel.querySelector('.backlinks-toggle'),
|
|
141
|
+
countBadge: panel.querySelector('.backlinks-count'),
|
|
142
|
+
body: panel.querySelector('.backlinks-body'),
|
|
143
|
+
list: panel.querySelector('.backlinks-list'),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
107
146
|
|
|
108
|
-
|
|
109
|
-
this.
|
|
147
|
+
_render() {
|
|
148
|
+
const count = this._backlinks.length;
|
|
149
|
+
const label = count === 1 ? 'Linked Mention' : 'Linked Mentions';
|
|
150
|
+
const shouldShow = Boolean(this._currentFile) && count > 0;
|
|
110
151
|
|
|
111
|
-
|
|
112
|
-
this.
|
|
152
|
+
this.panelRoot?.classList?.toggle('hidden', !shouldShow);
|
|
153
|
+
this.inlinePanel?.classList?.toggle('hidden', !shouldShow);
|
|
113
154
|
|
|
114
|
-
if (
|
|
155
|
+
if (!shouldShow) {
|
|
115
156
|
this._expanded = false;
|
|
116
|
-
this._applyExpandState();
|
|
117
|
-
if (this.list) this.list.innerHTML = '';
|
|
118
|
-
return;
|
|
119
157
|
}
|
|
120
158
|
|
|
121
|
-
this.
|
|
159
|
+
this.panels.forEach((refs) => {
|
|
160
|
+
refs.countBadge.textContent = shouldShow ? String(count) : '';
|
|
161
|
+
refs.toggle.textContent = label;
|
|
162
|
+
refs.header?.classList.toggle('backlinks-header-empty', !shouldShow);
|
|
163
|
+
|
|
164
|
+
if (!shouldShow) {
|
|
165
|
+
refs.list.innerHTML = '';
|
|
166
|
+
} else {
|
|
167
|
+
this._renderList(refs.list);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
122
171
|
this._applyExpandState();
|
|
123
172
|
}
|
|
124
173
|
|
|
125
|
-
_renderList() {
|
|
126
|
-
if (!
|
|
127
|
-
|
|
174
|
+
_renderList(listElement) {
|
|
175
|
+
if (!listElement) return;
|
|
176
|
+
listElement.innerHTML = '';
|
|
128
177
|
|
|
129
|
-
const fragment =
|
|
178
|
+
const fragment = this.documentRef?.createDocumentFragment?.();
|
|
179
|
+
const target = fragment ?? listElement;
|
|
130
180
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
? backlink.file.substring(0, backlink.file.lastIndexOf('/'))
|
|
135
|
-
: '';
|
|
181
|
+
this._backlinks.forEach((backlink) => {
|
|
182
|
+
target.appendChild(this._createBacklinkItem(backlink));
|
|
183
|
+
});
|
|
136
184
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.onFileSelect?.(backlink.file);
|
|
142
|
-
});
|
|
185
|
+
if (fragment) {
|
|
186
|
+
listElement.appendChild(fragment);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
143
189
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// Context snippets
|
|
158
|
-
for (const ctx of backlink.contexts.slice(0, 3)) {
|
|
159
|
-
const contextEl = document.createElement('div');
|
|
160
|
-
contextEl.className = 'backlink-context';
|
|
161
|
-
contextEl.textContent = ctx;
|
|
162
|
-
item.appendChild(contextEl);
|
|
163
|
-
}
|
|
190
|
+
_createBacklinkItem(backlink) {
|
|
191
|
+
const fileName = backlink.file.replace(/\.md$/i, '').split('/').pop();
|
|
192
|
+
const dirPath = backlink.file.includes('/')
|
|
193
|
+
? backlink.file.substring(0, backlink.file.lastIndexOf('/'))
|
|
194
|
+
: '';
|
|
195
|
+
|
|
196
|
+
const item = this.documentRef.createElement('button');
|
|
197
|
+
item.type = 'button';
|
|
198
|
+
item.className = 'backlink-item';
|
|
199
|
+
item.addEventListener('click', () => {
|
|
200
|
+
this.close();
|
|
201
|
+
this.onFileSelect?.(backlink.file);
|
|
202
|
+
});
|
|
164
203
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
204
|
+
const nameRow = this.documentRef.createElement('div');
|
|
205
|
+
nameRow.className = 'backlink-file-name';
|
|
206
|
+
nameRow.innerHTML = `
|
|
207
|
+
<svg class="backlink-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
208
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
209
|
+
<polyline points="14 2 14 8 20 8"/>
|
|
210
|
+
</svg>
|
|
211
|
+
<span>${escapeHtml(fileName)}</span>
|
|
212
|
+
${dirPath ? `<span class="backlink-dir">${escapeHtml(dirPath)}</span>` : ''}
|
|
213
|
+
`;
|
|
214
|
+
item.appendChild(nameRow);
|
|
215
|
+
|
|
216
|
+
backlink.contexts.slice(0, 3).forEach((ctx) => {
|
|
217
|
+
const contextElement = this.documentRef.createElement('div');
|
|
218
|
+
contextElement.className = 'backlink-context';
|
|
219
|
+
contextElement.textContent = ctx;
|
|
220
|
+
item.appendChild(contextElement);
|
|
221
|
+
});
|
|
171
222
|
|
|
172
|
-
|
|
223
|
+
if (backlink.contexts.length > 3) {
|
|
224
|
+
const moreElement = this.documentRef.createElement('div');
|
|
225
|
+
moreElement.className = 'backlink-context backlink-more';
|
|
226
|
+
moreElement.textContent = `+${backlink.contexts.length - 3} more`;
|
|
227
|
+
item.appendChild(moreElement);
|
|
173
228
|
}
|
|
174
229
|
|
|
175
|
-
|
|
230
|
+
return item;
|
|
176
231
|
}
|
|
177
232
|
|
|
178
233
|
_applyExpandState() {
|
|
179
|
-
this.
|
|
180
|
-
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
234
|
+
const expanded = this._expanded && this._backlinks.length > 0;
|
|
235
|
+
|
|
236
|
+
this.panels.forEach((refs) => {
|
|
237
|
+
refs.panel.classList.toggle('expanded', expanded);
|
|
238
|
+
refs.header?.setAttribute('aria-expanded', String(expanded));
|
|
239
|
+
refs.header?.setAttribute('aria-disabled', String(this._backlinks.length === 0));
|
|
240
|
+
refs.body?.setAttribute('aria-hidden', String(!expanded));
|
|
241
|
+
refs.body?.toggleAttribute('inert', !expanded);
|
|
242
|
+
});
|
|
187
243
|
}
|
|
188
244
|
}
|
|
@@ -279,6 +279,7 @@ export class CommentUiController {
|
|
|
279
279
|
commentsDrawerList,
|
|
280
280
|
commentsToggleButton,
|
|
281
281
|
editorContainer,
|
|
282
|
+
onWillOpenDrawer,
|
|
282
283
|
onCreateThread,
|
|
283
284
|
onNavigateToLine,
|
|
284
285
|
onReplyToThread,
|
|
@@ -293,6 +294,7 @@ export class CommentUiController {
|
|
|
293
294
|
this.commentsDrawerList = commentsDrawerList;
|
|
294
295
|
this.commentsToggleButton = commentsToggleButton;
|
|
295
296
|
this.editorContainer = editorContainer;
|
|
297
|
+
this.onWillOpenDrawer = onWillOpenDrawer;
|
|
296
298
|
this.onCreateThread = onCreateThread;
|
|
297
299
|
this.onNavigateToLine = onNavigateToLine;
|
|
298
300
|
this.onReplyToThread = onReplyToThread;
|
|
@@ -384,13 +386,30 @@ export class CommentUiController {
|
|
|
384
386
|
this.scheduleLayoutRefresh();
|
|
385
387
|
};
|
|
386
388
|
this.handleDocumentPointerDown = (event) => {
|
|
387
|
-
|
|
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) {
|
|
388
399
|
return;
|
|
389
400
|
}
|
|
390
|
-
|
|
401
|
+
|
|
402
|
+
if (
|
|
403
|
+
target instanceof Node
|
|
404
|
+
&& (
|
|
405
|
+
this.commentsDrawer?.contains(target)
|
|
406
|
+
|| this.commentsToggleButton?.contains(target)
|
|
407
|
+
)
|
|
408
|
+
) {
|
|
391
409
|
return;
|
|
392
410
|
}
|
|
393
|
-
|
|
411
|
+
|
|
412
|
+
this.closeDrawer();
|
|
394
413
|
};
|
|
395
414
|
this.handleDocumentKeyDown = (event) => {
|
|
396
415
|
if (event.key === 'Escape' && this.reactionPicker) {
|
|
@@ -414,8 +433,7 @@ export class CommentUiController {
|
|
|
414
433
|
this.openComposerForSelection('toolbar');
|
|
415
434
|
});
|
|
416
435
|
this.commentsToggleButton?.addEventListener('click', () => {
|
|
417
|
-
this.
|
|
418
|
-
this.render();
|
|
436
|
+
this.setDrawerOpen(!this.drawerOpen);
|
|
419
437
|
});
|
|
420
438
|
this.previewContainer?.addEventListener('scroll', this.handlePreviewScroll, { passive: true });
|
|
421
439
|
this.editorContainer?.addEventListener('pointerdown', this.handleEditorPointerDown);
|
|
@@ -569,6 +587,24 @@ export class CommentUiController {
|
|
|
569
587
|
this.render();
|
|
570
588
|
}
|
|
571
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;
|
|
601
|
+
this.render();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
closeDrawer() {
|
|
605
|
+
this.setDrawerOpen(false);
|
|
606
|
+
}
|
|
607
|
+
|
|
572
608
|
refreshLayout() {
|
|
573
609
|
this.renderEditorLayer();
|
|
574
610
|
this.renderPreviewLayer();
|