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
|
@@ -87,6 +87,7 @@ export class GitPanelController {
|
|
|
87
87
|
constructor({
|
|
88
88
|
enabled = true,
|
|
89
89
|
onCommitStaged = () => {},
|
|
90
|
+
onOpenPullBackup = () => {},
|
|
90
91
|
onPullBranch = () => {},
|
|
91
92
|
onPushBranch = () => {},
|
|
92
93
|
onRepoChange = () => {},
|
|
@@ -100,6 +101,7 @@ export class GitPanelController {
|
|
|
100
101
|
} = {}) {
|
|
101
102
|
this.enabled = enabled;
|
|
102
103
|
this.onCommitStaged = onCommitStaged;
|
|
104
|
+
this.onOpenPullBackup = onOpenPullBackup;
|
|
103
105
|
this.onPullBranch = onPullBranch;
|
|
104
106
|
this.onPushBranch = onPushBranch;
|
|
105
107
|
this.onRepoChange = onRepoChange;
|
|
@@ -111,6 +113,7 @@ export class GitPanelController {
|
|
|
111
113
|
this.searchInput = searchInput;
|
|
112
114
|
this.toastController = toastController;
|
|
113
115
|
this.panel = document.getElementById('gitPanel');
|
|
116
|
+
this.pullBackups = [];
|
|
114
117
|
this.status = null;
|
|
115
118
|
this.active = false;
|
|
116
119
|
this.refreshTimer = null;
|
|
@@ -147,6 +150,18 @@ export class GitPanelController {
|
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
const pullBackupButton = event.target instanceof Element
|
|
154
|
+
? event.target.closest('[data-git-pull-backup-path]')
|
|
155
|
+
: null;
|
|
156
|
+
if (pullBackupButton) {
|
|
157
|
+
const summaryPath = pullBackupButton.getAttribute('data-git-pull-backup-path');
|
|
158
|
+
if (!summaryPath) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.onOpenPullBackup(summaryPath);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
const actionButton = event.target instanceof Element
|
|
151
166
|
? event.target.closest('[data-git-file-action]')
|
|
152
167
|
: null;
|
|
@@ -243,6 +258,7 @@ export class GitPanelController {
|
|
|
243
258
|
sections: [],
|
|
244
259
|
summary: { changedFiles: 0 },
|
|
245
260
|
};
|
|
261
|
+
this.pullBackups = [];
|
|
246
262
|
this.onRepoChange(false, this.status);
|
|
247
263
|
this.render();
|
|
248
264
|
return this.status;
|
|
@@ -256,6 +272,19 @@ export class GitPanelController {
|
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
this.status = data;
|
|
275
|
+
this.pullBackups = [];
|
|
276
|
+
if (data.isGitRepo) {
|
|
277
|
+
try {
|
|
278
|
+
const backupResponse = await fetch(resolveApiUrl('/git/pull-backups'));
|
|
279
|
+
const backupData = await backupResponse.json();
|
|
280
|
+
if (!backupResponse.ok) {
|
|
281
|
+
throw new Error(backupData.error || 'Failed to load pull backups');
|
|
282
|
+
}
|
|
283
|
+
this.pullBackups = Array.isArray(backupData.backups) ? backupData.backups : [];
|
|
284
|
+
} catch (backupError) {
|
|
285
|
+
console.error('[git-panel] Failed to load pull backups:', backupError);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
259
288
|
this.onRepoChange(Boolean(data.isGitRepo), data);
|
|
260
289
|
this.render();
|
|
261
290
|
return data;
|
|
@@ -267,6 +296,7 @@ export class GitPanelController {
|
|
|
267
296
|
sections: [],
|
|
268
297
|
summary: { changedFiles: 0 },
|
|
269
298
|
};
|
|
299
|
+
this.pullBackups = [];
|
|
270
300
|
this.onRepoChange(false, this.status);
|
|
271
301
|
this.render();
|
|
272
302
|
return this.status;
|
|
@@ -367,6 +397,47 @@ export class GitPanelController {
|
|
|
367
397
|
`;
|
|
368
398
|
}
|
|
369
399
|
|
|
400
|
+
renderPullBackupsSection() {
|
|
401
|
+
if (!Array.isArray(this.pullBackups) || this.pullBackups.length === 0) {
|
|
402
|
+
return '';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return `
|
|
406
|
+
<section class="git-section">
|
|
407
|
+
<button class="git-section-header" type="button" data-git-section-toggle="pull-backups">
|
|
408
|
+
${chevronSvg(this.collapsedSections.has('pull-backups'))}
|
|
409
|
+
Pull Backups
|
|
410
|
+
<span class="git-section-count">${this.pullBackups.length}</span>
|
|
411
|
+
</button>
|
|
412
|
+
<div class="git-file-list${this.collapsedSections.has('pull-backups') ? ' hidden' : ''}">
|
|
413
|
+
${this.pullBackups.map((backup) => this.renderPullBackup(backup)).join('')}
|
|
414
|
+
</div>
|
|
415
|
+
</section>
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
renderPullBackup(backup) {
|
|
420
|
+
const createdAt = String(backup?.createdAt || '').replace('T', ' ').replace(/\.\d+Z?$/u, 'Z');
|
|
421
|
+
const fileCount = Number(backup?.fileCount || 0);
|
|
422
|
+
|
|
423
|
+
return `
|
|
424
|
+
<div class="git-file-row">
|
|
425
|
+
<button
|
|
426
|
+
class="git-file-item"
|
|
427
|
+
type="button"
|
|
428
|
+
data-git-pull-backup-path="${escapeHtml(backup.summaryPath || '')}"
|
|
429
|
+
>
|
|
430
|
+
${fileIconSvg()}
|
|
431
|
+
<span class="git-file-copy">
|
|
432
|
+
<span class="git-file-name">Pull backup ${escapeHtml(backup.id || '')}</span>
|
|
433
|
+
<span class="git-file-path">${escapeHtml(`${createdAt} · ${backup.branch || 'HEAD'} · ${fileCount} file${fileCount === 1 ? '' : 's'}`)}</span>
|
|
434
|
+
</span>
|
|
435
|
+
<span class="git-status-badge modified">BK</span>
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
|
|
370
441
|
renderFile(file) {
|
|
371
442
|
const isActive = this.selection.path === file.path && this.selection.scope === file.scope;
|
|
372
443
|
const dirPath = getPathDir(file.path);
|
|
@@ -449,6 +520,7 @@ export class GitPanelController {
|
|
|
449
520
|
}
|
|
450
521
|
|
|
451
522
|
const branch = this.status.branch ?? {};
|
|
523
|
+
const pullBackupsMarkup = this.renderPullBackupsSection();
|
|
452
524
|
const sectionMarkup = (this.status.sections ?? [])
|
|
453
525
|
.map((section) => this.renderSection(section))
|
|
454
526
|
.filter(Boolean)
|
|
@@ -492,6 +564,7 @@ export class GitPanelController {
|
|
|
492
564
|
</button>
|
|
493
565
|
</div>
|
|
494
566
|
</div>
|
|
567
|
+
${pullBackupsMarkup}
|
|
495
568
|
${sectionMarkup || '<div class="git-panel-empty">No local changes</div>'}
|
|
496
569
|
${hasChanges ? `
|
|
497
570
|
<div class="git-panel-footer">
|
|
@@ -2,6 +2,7 @@ export class OutlineController {
|
|
|
2
2
|
constructor({
|
|
3
3
|
mobileBreakpointQuery = window.matchMedia('(max-width: 768px)'),
|
|
4
4
|
onNavigateToHeading,
|
|
5
|
+
onWillOpen,
|
|
5
6
|
} = {}) {
|
|
6
7
|
this.outlineOpen = false;
|
|
7
8
|
this.activeHeadingFrame = null;
|
|
@@ -9,16 +10,36 @@ export class OutlineController {
|
|
|
9
10
|
this.headings = [];
|
|
10
11
|
this.pinnedHeadingId = null;
|
|
11
12
|
this.onNavigateToHeading = onNavigateToHeading;
|
|
13
|
+
this.onWillOpen = onWillOpen;
|
|
12
14
|
this.panel = document.getElementById('outlinePanel');
|
|
13
15
|
this.navigation = document.getElementById('outlineNav');
|
|
14
16
|
this.previewContainer = document.getElementById('previewContainer');
|
|
15
17
|
this.toggleButton = document.getElementById('outlineToggle');
|
|
16
18
|
this.mobileBreakpointQuery = mobileBreakpointQuery;
|
|
17
19
|
this.handlePreviewScroll = () => this.scheduleActiveHeadingUpdate();
|
|
20
|
+
this.handleDocumentPointerDown = (event) => {
|
|
21
|
+
if (!this.outlineOpen) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const target = event.target;
|
|
26
|
+
if (
|
|
27
|
+
target instanceof Node
|
|
28
|
+
&& (
|
|
29
|
+
this.panel?.contains(target)
|
|
30
|
+
|| this.toggleButton?.contains(target)
|
|
31
|
+
)
|
|
32
|
+
) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.close();
|
|
37
|
+
};
|
|
18
38
|
}
|
|
19
39
|
|
|
20
40
|
initialize() {
|
|
21
41
|
this.toggleButton?.addEventListener('click', () => this.toggle());
|
|
42
|
+
document.addEventListener('pointerdown', this.handleDocumentPointerDown);
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
toggle() {
|
|
@@ -30,6 +51,10 @@ export class OutlineController {
|
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
setOpenState(nextState) {
|
|
54
|
+
if (nextState && !this.outlineOpen) {
|
|
55
|
+
this.onWillOpen?.();
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
this.outlineOpen = nextState;
|
|
34
59
|
this.panel?.classList.toggle('hidden', !this.outlineOpen);
|
|
35
60
|
this.toggleButton?.classList.toggle('active', this.outlineOpen);
|
|
@@ -2265,6 +2265,12 @@ html[data-initial-file-requested="true"] .editor-page.hidden {
|
|
|
2265
2265
|
|
|
2266
2266
|
/* Preview body (contains preview + outline side by side) */
|
|
2267
2267
|
.preview-body {
|
|
2268
|
+
--preview-floating-sidebar-gap: var(--space-3);
|
|
2269
|
+
--preview-floating-comments-width: min(300px, 34vw);
|
|
2270
|
+
--preview-floating-outline-width: 220px;
|
|
2271
|
+
--preview-content-max-width: 720px;
|
|
2272
|
+
--preview-comment-rail-width: 0px;
|
|
2273
|
+
--preview-comment-rail-inset: 10px;
|
|
2268
2274
|
display: flex;
|
|
2269
2275
|
flex: 1;
|
|
2270
2276
|
overflow: hidden;
|
|
@@ -2283,7 +2289,7 @@ html[data-initial-file-requested="true"] .editor-page.hidden {
|
|
|
2283
2289
|
}
|
|
2284
2290
|
|
|
2285
2291
|
.preview-content {
|
|
2286
|
-
padding: var(--space-6) var(--space-6) var(--space-12);
|
|
2292
|
+
padding: var(--space-6) calc(var(--space-6) + var(--preview-comment-rail-width)) var(--space-12) var(--space-6);
|
|
2287
2293
|
max-width: 720px;
|
|
2288
2294
|
margin: 0 auto;
|
|
2289
2295
|
min-width: 0;
|
|
@@ -2298,6 +2304,16 @@ html[data-initial-file-requested="true"] .editor-page.hidden {
|
|
|
2298
2304
|
color: var(--color-text-muted);
|
|
2299
2305
|
}
|
|
2300
2306
|
|
|
2307
|
+
@media (min-width: 769px) {
|
|
2308
|
+
.preview-body {
|
|
2309
|
+
--preview-comment-rail-width: 52px;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
.preview-content {
|
|
2313
|
+
padding-bottom: calc(var(--space-12) + 96px);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2301
2317
|
.preview-content.is-excalidraw-file-preview {
|
|
2302
2318
|
display: flex;
|
|
2303
2319
|
width: 100%;
|
|
@@ -3180,7 +3196,7 @@ body.plantuml-maximized-open::before {
|
|
|
3180
3196
|
font-size: 10px;
|
|
3181
3197
|
font-weight: 700;
|
|
3182
3198
|
pointer-events: auto;
|
|
3183
|
-
transition: transform 140ms ease, box-shadow 140ms ease, background-color 140ms ease, border-color 140ms ease;
|
|
3199
|
+
transition: transform 140ms ease, box-shadow 140ms ease, background-color 140ms ease, border-color 140ms ease, color 140ms ease, opacity 140ms ease;
|
|
3184
3200
|
}
|
|
3185
3201
|
|
|
3186
3202
|
.comment-editor-badge.is-active,
|
|
@@ -3190,6 +3206,26 @@ body.plantuml-maximized-open::before {
|
|
|
3190
3206
|
color: var(--color-text-inverse);
|
|
3191
3207
|
}
|
|
3192
3208
|
|
|
3209
|
+
.comment-preview-badge {
|
|
3210
|
+
right: var(--preview-comment-rail-inset);
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
.comment-editor-badge.is-passive,
|
|
3214
|
+
.comment-preview-badge.is-passive {
|
|
3215
|
+
opacity: 0.48;
|
|
3216
|
+
background: color-mix(in srgb, var(--color-surface) 76%, var(--color-comment) 24%);
|
|
3217
|
+
border-color: oklch(from var(--color-comment) l c h / 0.16);
|
|
3218
|
+
box-shadow: none;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
.comment-editor-badge.is-hovered,
|
|
3222
|
+
.comment-preview-badge.is-hovered {
|
|
3223
|
+
opacity: 1;
|
|
3224
|
+
background: color-mix(in srgb, var(--color-surface) 56%, var(--color-comment) 44%);
|
|
3225
|
+
border-color: oklch(from var(--color-comment) l c h / 0.28);
|
|
3226
|
+
color: var(--color-text-inverse);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3193
3229
|
.comment-marker-icon {
|
|
3194
3230
|
display: inline-flex;
|
|
3195
3231
|
width: 12px;
|
|
@@ -3236,6 +3272,24 @@ body.plantuml-maximized-open::before {
|
|
|
3236
3272
|
border-radius: 6px;
|
|
3237
3273
|
background: var(--color-comment-soft);
|
|
3238
3274
|
border: 1px solid oklch(from var(--color-comment) l c h / 0.16);
|
|
3275
|
+
opacity: 0.56;
|
|
3276
|
+
transition: opacity 140ms ease, background-color 140ms ease, border-color 140ms ease;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
.comment-preview-highlight.is-passive {
|
|
3280
|
+
opacity: 0.32;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
.comment-preview-highlight.is-hovered {
|
|
3284
|
+
opacity: 0.72;
|
|
3285
|
+
background: color-mix(in srgb, var(--color-comment-soft) 74%, var(--color-comment) 26%);
|
|
3286
|
+
border-color: oklch(from var(--color-comment) l c h / 0.24);
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
.comment-preview-highlight.is-active {
|
|
3290
|
+
opacity: 0.9;
|
|
3291
|
+
background: color-mix(in srgb, var(--color-comment-soft) 62%, var(--color-comment) 38%);
|
|
3292
|
+
border-color: oklch(from var(--color-comment) l c h / 0.3);
|
|
3239
3293
|
}
|
|
3240
3294
|
|
|
3241
3295
|
.comment-card-root {
|
|
@@ -3670,14 +3724,45 @@ body.plantuml-maximized-open::before {
|
|
|
3670
3724
|
|
|
3671
3725
|
/* ===== BACKLINKS PANEL ===== */
|
|
3672
3726
|
|
|
3727
|
+
.backlinks-panel.hidden,
|
|
3728
|
+
.backlinks-panel-layer.hidden {
|
|
3729
|
+
display: none;
|
|
3730
|
+
}
|
|
3673
3731
|
.backlinks-panel {
|
|
3732
|
+
min-width: 0;
|
|
3733
|
+
}
|
|
3734
|
+
.backlinks-panel-layer {
|
|
3735
|
+
position: absolute;
|
|
3736
|
+
left: max(
|
|
3737
|
+
var(--space-4),
|
|
3738
|
+
calc((100% - min(var(--preview-content-max-width), 100%)) / 2)
|
|
3739
|
+
);
|
|
3740
|
+
bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
|
|
3741
|
+
width: min(320px, calc(min(var(--preview-content-max-width), 100%) - var(--space-8)));
|
|
3742
|
+
z-index: 14;
|
|
3743
|
+
pointer-events: none;
|
|
3744
|
+
}
|
|
3745
|
+
.backlinks-panel-dock {
|
|
3746
|
+
display: flex;
|
|
3747
|
+
flex-direction: column-reverse;
|
|
3748
|
+
border: 1px solid var(--color-divider);
|
|
3749
|
+
border-radius: 18px;
|
|
3750
|
+
background: color-mix(in srgb, var(--color-surface) 84%, var(--color-surface-offset) 16%);
|
|
3751
|
+
box-shadow: 0 14px 34px oklch(0 0 0 / 0.18);
|
|
3752
|
+
backdrop-filter: blur(16px);
|
|
3753
|
+
overflow: hidden;
|
|
3754
|
+
pointer-events: auto;
|
|
3755
|
+
transition: box-shadow 160ms ease, transform 160ms ease, background-color 160ms ease, border-color 160ms ease;
|
|
3756
|
+
}
|
|
3757
|
+
.backlinks-panel-dock:hover {
|
|
3758
|
+
transform: translateY(-1px);
|
|
3759
|
+
box-shadow: 0 18px 40px oklch(0 0 0 / 0.2);
|
|
3760
|
+
}
|
|
3761
|
+
.backlinks-panel-inline {
|
|
3674
3762
|
border-top: 1px solid var(--color-divider);
|
|
3675
3763
|
margin-top: var(--space-6);
|
|
3676
3764
|
padding: 0 var(--space-4);
|
|
3677
3765
|
}
|
|
3678
|
-
.backlinks-panel.hidden {
|
|
3679
|
-
display: none;
|
|
3680
|
-
}
|
|
3681
3766
|
.backlinks-header {
|
|
3682
3767
|
display: flex;
|
|
3683
3768
|
align-items: center;
|
|
@@ -3693,6 +3778,10 @@ body.plantuml-maximized-open::before {
|
|
|
3693
3778
|
.backlinks-header:hover {
|
|
3694
3779
|
color: var(--color-text);
|
|
3695
3780
|
}
|
|
3781
|
+
.backlinks-panel-dock .backlinks-header {
|
|
3782
|
+
padding: 11px 14px;
|
|
3783
|
+
background: color-mix(in srgb, var(--color-surface-offset) 76%, var(--color-bg) 24%);
|
|
3784
|
+
}
|
|
3696
3785
|
.backlinks-header-empty {
|
|
3697
3786
|
cursor: default;
|
|
3698
3787
|
opacity: 0.5;
|
|
@@ -3709,15 +3798,20 @@ body.plantuml-maximized-open::before {
|
|
|
3709
3798
|
}
|
|
3710
3799
|
.backlinks-toggle {
|
|
3711
3800
|
flex: 1;
|
|
3801
|
+
min-width: 0;
|
|
3802
|
+
overflow: hidden;
|
|
3803
|
+
text-overflow: ellipsis;
|
|
3804
|
+
white-space: nowrap;
|
|
3712
3805
|
}
|
|
3713
3806
|
.backlinks-count {
|
|
3714
3807
|
font-size: var(--text-xs);
|
|
3715
3808
|
font-weight: 600;
|
|
3716
3809
|
color: var(--color-primary);
|
|
3717
|
-
background: oklch(from var(--color-primary) l c h / 0.
|
|
3810
|
+
background: oklch(from var(--color-primary) l c h / 0.14);
|
|
3718
3811
|
border-radius: var(--radius-full);
|
|
3719
|
-
padding: 1px
|
|
3720
|
-
min-width:
|
|
3812
|
+
padding: 1px 7px;
|
|
3813
|
+
min-width: 22px;
|
|
3814
|
+
text-align: center;
|
|
3721
3815
|
}
|
|
3722
3816
|
.backlinks-count:empty {
|
|
3723
3817
|
display: none;
|
|
@@ -3727,16 +3821,31 @@ body.plantuml-maximized-open::before {
|
|
|
3727
3821
|
max-height: 0;
|
|
3728
3822
|
transition: max-height 200ms ease;
|
|
3729
3823
|
}
|
|
3730
|
-
.backlinks-panel.expanded .backlinks-body {
|
|
3824
|
+
.backlinks-panel-inline.expanded .backlinks-body {
|
|
3731
3825
|
max-height: 600px;
|
|
3732
3826
|
overflow-y: auto;
|
|
3733
3827
|
}
|
|
3828
|
+
.backlinks-panel-dock .backlinks-body {
|
|
3829
|
+
background: color-mix(in srgb, var(--color-surface) 94%, var(--color-surface-offset) 6%);
|
|
3830
|
+
}
|
|
3831
|
+
.backlinks-panel-dock.expanded .backlinks-body {
|
|
3832
|
+
max-height: min(44vh, 400px);
|
|
3833
|
+
overflow-y: auto;
|
|
3834
|
+
border-bottom: 1px solid color-mix(in srgb, var(--color-divider) 78%, var(--color-primary) 22%);
|
|
3835
|
+
}
|
|
3836
|
+
.backlinks-panel-dock.expanded {
|
|
3837
|
+
border-color: color-mix(in srgb, var(--color-divider) 72%, var(--color-primary) 28%);
|
|
3838
|
+
background: color-mix(in srgb, var(--color-surface) 90%, var(--color-surface-offset) 10%);
|
|
3839
|
+
}
|
|
3734
3840
|
.backlinks-list {
|
|
3735
3841
|
padding-bottom: var(--space-4);
|
|
3736
3842
|
display: flex;
|
|
3737
3843
|
flex-direction: column;
|
|
3738
3844
|
gap: var(--space-2);
|
|
3739
3845
|
}
|
|
3846
|
+
.backlinks-panel-dock .backlinks-list {
|
|
3847
|
+
padding: 10px;
|
|
3848
|
+
}
|
|
3740
3849
|
.backlink-item {
|
|
3741
3850
|
display: block;
|
|
3742
3851
|
width: 100%;
|
|
@@ -3748,6 +3857,10 @@ body.plantuml-maximized-open::before {
|
|
|
3748
3857
|
cursor: pointer;
|
|
3749
3858
|
transition: border-color var(--transition-interactive), background var(--transition-interactive);
|
|
3750
3859
|
}
|
|
3860
|
+
.backlinks-panel-dock .backlink-item {
|
|
3861
|
+
border-radius: 12px;
|
|
3862
|
+
background: color-mix(in srgb, var(--color-surface) 88%, var(--color-surface-offset) 12%);
|
|
3863
|
+
}
|
|
3751
3864
|
.backlink-item:hover {
|
|
3752
3865
|
border-color: var(--color-primary);
|
|
3753
3866
|
background: oklch(from var(--color-primary) l c h / 0.04);
|
|
@@ -3789,6 +3902,12 @@ body.plantuml-maximized-open::before {
|
|
|
3789
3902
|
color: var(--color-text-faint);
|
|
3790
3903
|
}
|
|
3791
3904
|
|
|
3905
|
+
@media (min-width: 769px) {
|
|
3906
|
+
.backlinks-panel-inline {
|
|
3907
|
+
display: none !important;
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3792
3911
|
.outline-panel {
|
|
3793
3912
|
width: 220px;
|
|
3794
3913
|
flex-shrink: 0;
|
|
@@ -3889,6 +4008,9 @@ body.plantuml-maximized-open::before {
|
|
|
3889
4008
|
.editor-layout[data-view="preview"] .preview-pane {
|
|
3890
4009
|
flex: 1;
|
|
3891
4010
|
}
|
|
4011
|
+
.editor-layout[data-view="preview"] .preview-body {
|
|
4012
|
+
--preview-content-max-width: 1080px;
|
|
4013
|
+
}
|
|
3892
4014
|
.editor-layout[data-view="preview"] .preview-content {
|
|
3893
4015
|
max-width: 1080px;
|
|
3894
4016
|
}
|
|
@@ -3923,6 +4045,55 @@ body.plantuml-maximized-open::before {
|
|
|
3923
4045
|
.editor-layout[data-view="preview"] .preview-content p:has(> .plantuml-shell:only-child) {
|
|
3924
4046
|
max-width: none;
|
|
3925
4047
|
}
|
|
4048
|
+
@media (min-width: 769px) {
|
|
4049
|
+
.preview-body > .comments-drawer,
|
|
4050
|
+
.preview-body > .outline-panel {
|
|
4051
|
+
position: absolute;
|
|
4052
|
+
top: var(--preview-floating-sidebar-gap);
|
|
4053
|
+
right: var(--preview-floating-sidebar-gap);
|
|
4054
|
+
bottom: var(--preview-floating-sidebar-gap);
|
|
4055
|
+
z-index: 20;
|
|
4056
|
+
overflow: hidden;
|
|
4057
|
+
border: 1px solid var(--color-divider);
|
|
4058
|
+
border-radius: calc(var(--radius-lg) + 2px);
|
|
4059
|
+
box-shadow: var(--shadow-lg);
|
|
4060
|
+
backdrop-filter: blur(14px);
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
.preview-body > .comments-drawer {
|
|
4064
|
+
width: var(--preview-floating-comments-width);
|
|
4065
|
+
min-width: 240px;
|
|
4066
|
+
border-left: 1px solid var(--color-divider);
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
.preview-body > .outline-panel {
|
|
4070
|
+
width: var(--preview-floating-outline-width);
|
|
4071
|
+
border-left: 1px solid var(--color-divider);
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
.preview-body > .comments-drawer.hidden,
|
|
4075
|
+
.preview-body > .outline-panel.hidden {
|
|
4076
|
+
display: flex !important;
|
|
4077
|
+
opacity: 0;
|
|
4078
|
+
pointer-events: none;
|
|
4079
|
+
transform: translateX(12px);
|
|
4080
|
+
visibility: hidden;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
.preview-body > .comments-drawer:not(.hidden),
|
|
4084
|
+
.preview-body > .outline-panel:not(.hidden) {
|
|
4085
|
+
opacity: 1;
|
|
4086
|
+
transform: translateX(0);
|
|
4087
|
+
visibility: visible;
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
.preview-body:has(> .outline-panel:not(.hidden)) > .comments-drawer:not(.hidden) {
|
|
4091
|
+
right: calc(
|
|
4092
|
+
var(--preview-floating-sidebar-gap) * 2
|
|
4093
|
+
+ var(--preview-floating-outline-width)
|
|
4094
|
+
);
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
3926
4097
|
.editor-layout[data-view="editor"] .editor-pane {
|
|
3927
4098
|
flex: 1;
|
|
3928
4099
|
}
|
|
@@ -4022,6 +4193,10 @@ body.plantuml-maximized-open::before {
|
|
|
4022
4193
|
padding: var(--space-4);
|
|
4023
4194
|
}
|
|
4024
4195
|
|
|
4196
|
+
.backlinks-panel-layer {
|
|
4197
|
+
display: none !important;
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4025
4200
|
.diff-toolbar {
|
|
4026
4201
|
flex-wrap: wrap;
|
|
4027
4202
|
padding: var(--space-2) var(--space-3);
|
|
@@ -66,10 +66,21 @@ export function resolveWikiTargetPath(target, files) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const rawTarget = String(target ?? '').trim();
|
|
69
|
+
let fallbackSuffixMatch = null;
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
filePath === normalizedTarget
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
for (const filePath of files) {
|
|
72
|
+
if (filePath === normalizedTarget) {
|
|
73
|
+
return filePath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (filePath.replace(/\.md$/i, '') === rawTarget) {
|
|
77
|
+
return filePath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!fallbackSuffixMatch && filePath.endsWith(`/${normalizedTarget}`)) {
|
|
81
|
+
fallbackSuffixMatch = filePath;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return fallbackSuffixMatch;
|
|
75
86
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function uniquePaths(values = []) {
|
|
2
|
+
return Array.from(new Set((values ?? []).filter(Boolean)));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function uniqueRenames(values = []) {
|
|
6
|
+
return Array.from(new Map(
|
|
7
|
+
(values ?? [])
|
|
8
|
+
.filter((entry) => entry?.oldPath && entry?.newPath && entry.oldPath !== entry.newPath)
|
|
9
|
+
.map((entry) => [`${entry.oldPath}:${entry.newPath}`, {
|
|
10
|
+
newPath: entry.newPath,
|
|
11
|
+
oldPath: entry.oldPath,
|
|
12
|
+
}]),
|
|
13
|
+
).values());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeHighlightRanges(values = []) {
|
|
17
|
+
return (values ?? [])
|
|
18
|
+
.filter((entry) => entry?.path && Number.isFinite(entry?.from) && Number.isFinite(entry?.to))
|
|
19
|
+
.map((entry) => ({
|
|
20
|
+
from: Math.max(0, Math.round(entry.from)),
|
|
21
|
+
path: entry.path,
|
|
22
|
+
to: Math.max(0, Math.round(entry.to)),
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createWorkspaceChange({
|
|
27
|
+
changedPaths = [],
|
|
28
|
+
deletedPaths = [],
|
|
29
|
+
refreshExplorer = true,
|
|
30
|
+
renamedPaths = [],
|
|
31
|
+
} = {}) {
|
|
32
|
+
return {
|
|
33
|
+
changedPaths: uniquePaths(changedPaths),
|
|
34
|
+
deletedPaths: uniquePaths(deletedPaths),
|
|
35
|
+
refreshExplorer: refreshExplorer !== false,
|
|
36
|
+
renamedPaths: uniqueRenames(renamedPaths),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createEmptyWorkspaceChange() {
|
|
41
|
+
return createWorkspaceChange();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hasWorkspaceMutation(workspaceChange = {}) {
|
|
45
|
+
return Boolean(
|
|
46
|
+
(workspaceChange.changedPaths?.length ?? 0) > 0
|
|
47
|
+
|| (workspaceChange.deletedPaths?.length ?? 0) > 0
|
|
48
|
+
|| (workspaceChange.renamedPaths?.length ?? 0) > 0,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeWorkspaceEvent(event) {
|
|
53
|
+
if (!event || typeof event !== 'object' || typeof event.id !== 'string') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
action: typeof event.action === 'string' ? event.action : 'workspace',
|
|
59
|
+
createdAt: Number.isFinite(event.createdAt) ? event.createdAt : Date.now(),
|
|
60
|
+
highlightRanges: normalizeHighlightRanges(event.highlightRanges),
|
|
61
|
+
id: event.id,
|
|
62
|
+
origin: typeof event.origin === 'string' ? event.origin : 'api',
|
|
63
|
+
reloadRequiredPaths: uniquePaths(event.reloadRequiredPaths),
|
|
64
|
+
requestId: typeof event.requestId === 'string' ? event.requestId : null,
|
|
65
|
+
sourceRef: typeof event.sourceRef === 'string' ? event.sourceRef : null,
|
|
66
|
+
workspaceChange: createWorkspaceChange(event.workspaceChange),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -11,6 +11,9 @@ import { RoomRegistry } from './domain/collaboration/room-registry.js';
|
|
|
11
11
|
import { createRequestHandler } from './infrastructure/http/create-request-handler.js';
|
|
12
12
|
import { VaultFileStore } from './infrastructure/persistence/vault-file-store.js';
|
|
13
13
|
import { attachCollaborationGateway } from './infrastructure/websocket/attach-collaboration-gateway.js';
|
|
14
|
+
import { WORKSPACE_ROOM_NAME } from '../domain/workspace-room.js';
|
|
15
|
+
import { FileSystemSyncService } from './infrastructure/workspace/file-system-sync-service.js';
|
|
16
|
+
import { WorkspaceMutationCoordinator } from './infrastructure/workspace/workspace-mutation-coordinator.js';
|
|
14
17
|
|
|
15
18
|
function getDisplayHost(host) {
|
|
16
19
|
return host === '127.0.0.1' ? 'localhost' : host;
|
|
@@ -45,18 +48,39 @@ export function createAppServer(config = loadConfig()) {
|
|
|
45
48
|
enabled: config.gitEnabled,
|
|
46
49
|
vaultDir: config.vaultDir,
|
|
47
50
|
});
|
|
51
|
+
let workspaceMutationCoordinator = null;
|
|
48
52
|
const roomRegistry = new RoomRegistry({
|
|
49
|
-
createRoom: ({ name, onEmpty }) =>
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
createRoom: ({ name, onEmpty }) => {
|
|
54
|
+
const room = new CollaborationRoom({
|
|
55
|
+
documentStore: new CollaborationDocumentStore({
|
|
56
|
+
backlinkIndex: name === '__lobby__' || name === WORKSPACE_ROOM_NAME ? null : backlinkIndex,
|
|
57
|
+
name,
|
|
58
|
+
vaultFileStore: name === '__lobby__' || name === WORKSPACE_ROOM_NAME ? null : vaultFileStore,
|
|
59
|
+
}),
|
|
60
|
+
idleGraceMs: config.wsRoomIdleGraceMs,
|
|
61
|
+
maxBufferedAmountBytes: config.wsMaxBufferedAmountBytes,
|
|
52
62
|
name,
|
|
53
|
-
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
onEmpty,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (name === WORKSPACE_ROOM_NAME && workspaceMutationCoordinator?.workspaceState) {
|
|
67
|
+
room.replaceWorkspaceEntries(workspaceMutationCoordinator.workspaceState.entries, {
|
|
68
|
+
generatedAt: workspaceMutationCoordinator.workspaceState.scannedAt,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return room;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
workspaceMutationCoordinator = new WorkspaceMutationCoordinator({
|
|
76
|
+
backlinkIndex,
|
|
77
|
+
roomRegistry,
|
|
78
|
+
vaultFileStore,
|
|
79
|
+
});
|
|
80
|
+
vaultFileStore.setManagedWriteTracker(workspaceMutationCoordinator);
|
|
81
|
+
const fileSystemSyncService = new FileSystemSyncService({
|
|
82
|
+
mutationCoordinator: workspaceMutationCoordinator,
|
|
83
|
+
vaultFileStore,
|
|
60
84
|
});
|
|
61
85
|
const requestHandler = createRequestHandler(
|
|
62
86
|
config,
|
|
@@ -66,6 +90,8 @@ export function createAppServer(config = loadConfig()) {
|
|
|
66
90
|
roomRegistry,
|
|
67
91
|
plantUmlRenderer,
|
|
68
92
|
gitService,
|
|
93
|
+
workspaceMutationCoordinator,
|
|
94
|
+
fileSystemSyncService,
|
|
69
95
|
);
|
|
70
96
|
const httpServer = createServer((req, res) => {
|
|
71
97
|
requestHandler(req, res).catch((error) => {
|
|
@@ -95,6 +121,8 @@ export function createAppServer(config = loadConfig()) {
|
|
|
95
121
|
async function listen() {
|
|
96
122
|
vaultFileCount = await vaultFileStore.countVaultFiles();
|
|
97
123
|
await backlinkIndex.build();
|
|
124
|
+
await workspaceMutationCoordinator.initialize();
|
|
125
|
+
await fileSystemSyncService.start();
|
|
98
126
|
|
|
99
127
|
return new Promise((resolve, reject) => {
|
|
100
128
|
httpServer.once('error', reject);
|
|
@@ -118,6 +146,7 @@ export function createAppServer(config = loadConfig()) {
|
|
|
118
146
|
|
|
119
147
|
shutdownPromise = (async () => {
|
|
120
148
|
await collaborationGateway.close();
|
|
149
|
+
await fileSystemSyncService.close();
|
|
121
150
|
await roomRegistry.reset();
|
|
122
151
|
await Promise.all([
|
|
123
152
|
closeHttpServer(httpServer),
|
|
@@ -135,7 +164,9 @@ export function createAppServer(config = loadConfig()) {
|
|
|
135
164
|
httpServer,
|
|
136
165
|
listen,
|
|
137
166
|
roomRegistry,
|
|
167
|
+
workspaceMutationCoordinator,
|
|
138
168
|
authService,
|
|
169
|
+
fileSystemSyncService,
|
|
139
170
|
gitService,
|
|
140
171
|
vaultFileStore,
|
|
141
172
|
get vaultFileCount() { return vaultFileCount; },
|