collabmd 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +99 -17
  2. package/package.json +1 -1
  3. package/public/assets/css/style.css +1 -1
  4. package/public/assets/js/chunks/{preview-render-compiler-WD3A76I6.js → chunk-5QRWYPYT.js} +18 -18
  5. package/public/assets/js/chunks/chunk-HEWVH67U.js +9 -0
  6. package/public/assets/js/chunks/chunk-R3DDMJHH.js +1 -0
  7. package/public/assets/js/chunks/editor-session-AH6Z3MXW.js +22 -0
  8. package/public/assets/js/chunks/preview-render-compiler-UZ4SZDQQ.js +1 -0
  9. package/public/assets/js/chunks/quick-switcher-controller-J7I3CUET.js +5 -0
  10. package/public/assets/js/{excalidraw-editor-LKW2AZBU.js → excalidraw-editor-ZDYKMZOL.js} +33 -33
  11. package/public/assets/js/excalidraw-editor.js +1 -1
  12. package/public/assets/js/main.js +59 -59
  13. package/public/assets/js/preview-render-worker.js +19 -19
  14. package/src/client/application/app-shell/presence-feature.js +2 -1
  15. package/src/client/application/app-shell/ui-feature.js +113 -1
  16. package/src/client/application/app-shell/workspace-feature.js +9 -0
  17. package/src/client/application/preview-render-compiler.js +55 -5
  18. package/src/client/application/preview-render-executor.js +8 -0
  19. package/src/client/application/preview-render-worker.js +13 -2
  20. package/src/client/application/preview-renderer.js +5 -0
  21. package/src/client/application/workspace-chrome-controller.js +12 -1
  22. package/src/client/application/workspace-coordinator.js +16 -7
  23. package/src/client/application/workspace-preview-controller.js +45 -5
  24. package/src/client/application/workspace-route-controller.js +4 -0
  25. package/src/client/bootstrap/collabmd-app-shell.js +15 -3
  26. package/src/client/domain/room.js +37 -0
  27. package/src/client/domain/vault-utils.js +46 -0
  28. package/src/client/infrastructure/comment-thread-store.js +98 -0
  29. package/src/client/infrastructure/editor-paste-utils.js +45 -0
  30. package/src/client/infrastructure/editor-session.js +10 -0
  31. package/src/client/infrastructure/editor-view-adapter.js +34 -1
  32. package/src/client/infrastructure/vault-api-client.js +17 -0
  33. package/src/client/presentation/comment-markdown-renderer.js +68 -0
  34. package/src/client/presentation/comment-ui-controller.js +311 -15
  35. package/src/client/presentation/file-explorer-controller.js +5 -0
  36. package/src/client/presentation/file-explorer-view.js +10 -2
  37. package/src/client/presentation/file-tree-state.js +7 -1
  38. package/src/client/presentation/image-lightbox-controller.js +394 -0
  39. package/src/client/styles/style.css +539 -14
  40. package/src/domain/comment-threads.js +95 -13
  41. package/src/domain/file-kind.js +16 -1
  42. package/src/server/infrastructure/http/create-vault-api-command-handler.js +48 -2
  43. package/src/server/infrastructure/http/create-vault-api-query-handler.js +67 -1
  44. package/src/server/infrastructure/http/request-body.js +11 -2
  45. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  46. package/src/server/infrastructure/persistence/vault-file-store.js +187 -2
  47. package/public/assets/js/chunks/chunk-BBHPYU2R.js +0 -1
  48. package/public/assets/js/chunks/chunk-OG2TNZEU.js +0 -9
  49. package/public/assets/js/chunks/chunk-QSTBGTWJ.js +0 -1
  50. package/public/assets/js/chunks/chunk-SR3U53EQ.js +0 -1
  51. package/public/assets/js/chunks/editor-session-IHFTYG5D.js +0 -22
  52. package/public/assets/js/chunks/quick-switcher-controller-JYDIJVAJ.js +0 -5
@@ -0,0 +1,394 @@
1
+ const MIN_SCALE = 1;
2
+ const MAX_SCALE = 6;
3
+ const ZOOM_STEP = 0.25;
4
+ const CLICK_ZOOM_SCALE = 2;
5
+ const DRAG_THRESHOLD_PX = 6;
6
+
7
+ export function clampImageLightboxScale(value) {
8
+ const numericValue = Number(value);
9
+ if (!Number.isFinite(numericValue)) {
10
+ return MIN_SCALE;
11
+ }
12
+
13
+ return Math.min(MAX_SCALE, Math.max(MIN_SCALE, Math.round(numericValue * 100) / 100));
14
+ }
15
+
16
+ export function clampImageLightboxOffset(offset, {
17
+ contentSize = 0,
18
+ scale = 1,
19
+ viewportSize = 0,
20
+ } = {}) {
21
+ const numericOffset = Number(offset);
22
+ const numericContentSize = Number(contentSize);
23
+ const numericScale = Number(scale);
24
+ const numericViewportSize = Number(viewportSize);
25
+
26
+ if (
27
+ !Number.isFinite(numericOffset)
28
+ || !Number.isFinite(numericContentSize)
29
+ || !Number.isFinite(numericScale)
30
+ || !Number.isFinite(numericViewportSize)
31
+ ) {
32
+ return 0;
33
+ }
34
+
35
+ const overflow = ((numericContentSize * numericScale) - numericViewportSize) / 2;
36
+ if (overflow <= 0) {
37
+ return 0;
38
+ }
39
+
40
+ return Math.min(Math.max(numericOffset, -overflow), overflow);
41
+ }
42
+
43
+ export class ImageLightboxController {
44
+ constructor({
45
+ previewElement,
46
+ documentRef = document,
47
+ windowRef = window,
48
+ }) {
49
+ this.previewElement = previewElement;
50
+ this.document = documentRef;
51
+ this.window = windowRef;
52
+ this.overlayRoot = null;
53
+ this.viewport = null;
54
+ this.imageElement = null;
55
+ this.zoomLabel = null;
56
+ this.titleElement = null;
57
+ this.isOpen = false;
58
+ this.scale = MIN_SCALE;
59
+ this.offsetX = 0;
60
+ this.offsetY = 0;
61
+ this.activePointerId = null;
62
+ this.dragStartX = 0;
63
+ this.dragStartY = 0;
64
+ this.dragOriginX = 0;
65
+ this.dragOriginY = 0;
66
+ this.didDrag = false;
67
+ this.resetButton = null;
68
+
69
+ this.handlePreviewClick = (event) => {
70
+ const target = event.target;
71
+ if (!(target instanceof this.window.Element)) {
72
+ return;
73
+ }
74
+
75
+ const image = target.closest('img');
76
+ if (!image || !this.previewElement?.contains(image)) {
77
+ return;
78
+ }
79
+
80
+ if (image.closest('[data-video-overlay-root="true"], [data-excalidraw-overlay-root="true"]')) {
81
+ return;
82
+ }
83
+
84
+ event.preventDefault();
85
+ event.stopImmediatePropagation?.();
86
+ this.openFromImage(image);
87
+ };
88
+
89
+ this.handleKeyDown = (event) => {
90
+ if (!this.isOpen) {
91
+ return;
92
+ }
93
+
94
+ if (event.key === 'Escape') {
95
+ event.preventDefault();
96
+ this.close();
97
+ return;
98
+ }
99
+
100
+ if (event.key === '+' || event.key === '=') {
101
+ event.preventDefault();
102
+ this.zoomBy(ZOOM_STEP);
103
+ return;
104
+ }
105
+
106
+ if (event.key === '-') {
107
+ event.preventDefault();
108
+ this.zoomBy(-ZOOM_STEP);
109
+ return;
110
+ }
111
+
112
+ if (event.key === '0') {
113
+ event.preventDefault();
114
+ this.resetView();
115
+ }
116
+ };
117
+
118
+ this.handleWindowResize = () => {
119
+ if (!this.isOpen) {
120
+ return;
121
+ }
122
+
123
+ this.clampOffsets();
124
+ this.syncTransform();
125
+ };
126
+
127
+ this.previewElement?.addEventListener('click', this.handlePreviewClick);
128
+ this.document.addEventListener('keydown', this.handleKeyDown);
129
+ this.window.addEventListener('resize', this.handleWindowResize);
130
+ }
131
+
132
+ destroy() {
133
+ this.previewElement?.removeEventListener('click', this.handlePreviewClick);
134
+ this.document.removeEventListener('keydown', this.handleKeyDown);
135
+ this.window.removeEventListener('resize', this.handleWindowResize);
136
+ this.close();
137
+ this.overlayRoot?.remove();
138
+ this.overlayRoot = null;
139
+ this.viewport = null;
140
+ this.imageElement = null;
141
+ this.zoomLabel = null;
142
+ this.titleElement = null;
143
+ this.resetButton = null;
144
+ }
145
+
146
+ openFromImage(image) {
147
+ const src = image?.currentSrc || image?.getAttribute?.('src') || '';
148
+ if (!src) {
149
+ return false;
150
+ }
151
+
152
+ this.ensureOverlayRoot();
153
+ this.resetView();
154
+ this.titleElement.textContent = image.getAttribute('alt') || 'Image preview';
155
+ this.imageElement.src = src;
156
+ this.imageElement.alt = image.getAttribute('alt') || '';
157
+ this.overlayRoot.hidden = false;
158
+ this.isOpen = true;
159
+ this.document.body.classList.add('image-lightbox-open');
160
+ this.syncTransform();
161
+ this.overlayRoot.focus?.();
162
+ return true;
163
+ }
164
+
165
+ close() {
166
+ if (!this.isOpen && !this.overlayRoot) {
167
+ return;
168
+ }
169
+
170
+ this.activePointerId = null;
171
+ this.didDrag = false;
172
+ this.viewport?.classList?.remove('is-dragging');
173
+ if (this.overlayRoot) {
174
+ this.overlayRoot.hidden = true;
175
+ }
176
+ this.isOpen = false;
177
+ this.document.body.classList.remove('image-lightbox-open');
178
+ }
179
+
180
+ resetView() {
181
+ this.scale = MIN_SCALE;
182
+ this.offsetX = 0;
183
+ this.offsetY = 0;
184
+ this.syncTransform();
185
+ }
186
+
187
+ zoomBy(delta) {
188
+ this.setScale(this.scale + delta);
189
+ }
190
+
191
+ setScale(nextScale) {
192
+ this.scale = clampImageLightboxScale(nextScale);
193
+ this.clampOffsets();
194
+ this.syncTransform();
195
+ }
196
+
197
+ clampOffsets() {
198
+ if (!this.viewport || !this.imageElement) {
199
+ this.offsetX = 0;
200
+ this.offsetY = 0;
201
+ return;
202
+ }
203
+
204
+ this.offsetX = clampImageLightboxOffset(this.offsetX, {
205
+ contentSize: this.imageElement.offsetWidth || 0,
206
+ scale: this.scale,
207
+ viewportSize: this.viewport.clientWidth || 0,
208
+ });
209
+ this.offsetY = clampImageLightboxOffset(this.offsetY, {
210
+ contentSize: this.imageElement.offsetHeight || 0,
211
+ scale: this.scale,
212
+ viewportSize: this.viewport.clientHeight || 0,
213
+ });
214
+ }
215
+
216
+ syncTransform() {
217
+ if (!this.imageElement) {
218
+ return;
219
+ }
220
+
221
+ this.imageElement.style.transform = `translate3d(${this.offsetX}px, ${this.offsetY}px, 0) scale(${this.scale})`;
222
+ this.imageElement.style.cursor = this.scale > MIN_SCALE ? 'grab' : 'zoom-in';
223
+ if (this.zoomLabel) {
224
+ this.zoomLabel.textContent = `${Math.round(this.scale * 100)}%`;
225
+ }
226
+ if (this.resetButton) {
227
+ this.resetButton.disabled = this.scale <= MIN_SCALE && this.offsetX === 0 && this.offsetY === 0;
228
+ }
229
+ }
230
+
231
+ ensureOverlayRoot() {
232
+ if (this.overlayRoot?.isConnected && this.overlayRoot.parentElement === this.document.body) {
233
+ return this.overlayRoot;
234
+ }
235
+
236
+ const overlayRoot = this.document.createElement('div');
237
+ overlayRoot.className = 'image-lightbox-root';
238
+ overlayRoot.dataset.imageLightboxRoot = 'true';
239
+ overlayRoot.hidden = true;
240
+ overlayRoot.tabIndex = -1;
241
+
242
+ const backdrop = this.document.createElement('button');
243
+ backdrop.className = 'image-lightbox-backdrop';
244
+ backdrop.type = 'button';
245
+ backdrop.setAttribute('aria-label', 'Close image preview');
246
+ backdrop.addEventListener('click', (event) => {
247
+ event.preventDefault();
248
+ event.stopPropagation();
249
+ this.close();
250
+ });
251
+
252
+ const shell = this.document.createElement('div');
253
+ shell.className = 'image-lightbox-shell';
254
+ shell.setAttribute('role', 'dialog');
255
+ shell.setAttribute('aria-modal', 'true');
256
+ shell.setAttribute('aria-label', 'Image preview');
257
+ shell.addEventListener('click', (event) => event.stopPropagation());
258
+ shell.addEventListener('pointerdown', (event) => event.stopPropagation());
259
+
260
+ const toolbar = this.document.createElement('div');
261
+ toolbar.className = 'image-lightbox-toolbar';
262
+
263
+ const title = this.document.createElement('div');
264
+ title.className = 'image-lightbox-title';
265
+ title.textContent = 'Image preview';
266
+
267
+ const controls = this.document.createElement('div');
268
+ controls.className = 'image-lightbox-controls';
269
+
270
+ const zoomLabel = this.document.createElement('span');
271
+ zoomLabel.className = 'image-lightbox-zoom-label';
272
+ zoomLabel.textContent = '100%';
273
+ const resetButton = this.createControlButton('Reset', 'Reset zoom and position', () => this.resetView());
274
+ const closeButton = this.createControlButton('Close', 'Close image preview', () => this.close());
275
+
276
+ controls.append(zoomLabel, resetButton, closeButton);
277
+ toolbar.append(title, controls);
278
+
279
+ const viewport = this.document.createElement('div');
280
+ viewport.className = 'image-lightbox-viewport';
281
+
282
+ const image = this.document.createElement('img');
283
+ image.className = 'image-lightbox-image';
284
+ image.alt = '';
285
+ image.addEventListener('load', () => {
286
+ this.clampOffsets();
287
+ this.syncTransform();
288
+ });
289
+
290
+ viewport.addEventListener('wheel', (event) => {
291
+ if (!this.isOpen) {
292
+ return;
293
+ }
294
+
295
+ event.preventDefault();
296
+ this.zoomBy(event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP);
297
+ }, { passive: false });
298
+
299
+ viewport.addEventListener('pointerdown', (event) => {
300
+ if (!this.isOpen || this.scale <= MIN_SCALE || event.button !== 0) {
301
+ return;
302
+ }
303
+
304
+ this.activePointerId = event.pointerId;
305
+ this.dragStartX = event.clientX;
306
+ this.dragStartY = event.clientY;
307
+ this.dragOriginX = this.offsetX;
308
+ this.dragOriginY = this.offsetY;
309
+ this.didDrag = false;
310
+ viewport.classList.add('is-dragging');
311
+ viewport.setPointerCapture?.(event.pointerId);
312
+ event.preventDefault();
313
+ });
314
+
315
+ viewport.addEventListener('pointermove', (event) => {
316
+ if (!this.isOpen || this.activePointerId !== event.pointerId) {
317
+ return;
318
+ }
319
+
320
+ if (
321
+ !this.didDrag
322
+ && (Math.abs(event.clientX - this.dragStartX) >= DRAG_THRESHOLD_PX
323
+ || Math.abs(event.clientY - this.dragStartY) >= DRAG_THRESHOLD_PX)
324
+ ) {
325
+ this.didDrag = true;
326
+ }
327
+
328
+ this.offsetX = this.dragOriginX + (event.clientX - this.dragStartX);
329
+ this.offsetY = this.dragOriginY + (event.clientY - this.dragStartY);
330
+ this.clampOffsets();
331
+ this.syncTransform();
332
+ });
333
+
334
+ const finishDrag = (event) => {
335
+ if (this.activePointerId !== event.pointerId) {
336
+ return;
337
+ }
338
+
339
+ this.activePointerId = null;
340
+ viewport.classList.remove('is-dragging');
341
+ viewport.releasePointerCapture?.(event.pointerId);
342
+ this.imageElement.style.cursor = this.scale > MIN_SCALE ? 'grab' : 'zoom-in';
343
+ };
344
+
345
+ viewport.addEventListener('pointerup', finishDrag);
346
+ viewport.addEventListener('pointercancel', finishDrag);
347
+ image.addEventListener('click', (event) => {
348
+ if (!this.isOpen) {
349
+ return;
350
+ }
351
+
352
+ event.preventDefault();
353
+ event.stopPropagation();
354
+ if (this.didDrag) {
355
+ this.didDrag = false;
356
+ return;
357
+ }
358
+
359
+ if (this.scale <= MIN_SCALE) {
360
+ this.setScale(CLICK_ZOOM_SCALE);
361
+ }
362
+ });
363
+
364
+ viewport.appendChild(image);
365
+ shell.append(toolbar, viewport);
366
+ overlayRoot.append(backdrop, shell);
367
+ this.document.body.appendChild(overlayRoot);
368
+
369
+ this.overlayRoot = overlayRoot;
370
+ this.viewport = viewport;
371
+ this.imageElement = image;
372
+ this.zoomLabel = zoomLabel;
373
+ this.titleElement = title;
374
+ this.resetButton = resetButton;
375
+ this.syncTransform();
376
+ return overlayRoot;
377
+ }
378
+
379
+ createControlButton(label, ariaLabel, onClick, {
380
+ className = '',
381
+ } = {}) {
382
+ const button = this.document.createElement('button');
383
+ button.className = `image-lightbox-btn ${className}`.trim();
384
+ button.type = 'button';
385
+ button.setAttribute('aria-label', ariaLabel);
386
+ button.textContent = label;
387
+ button.addEventListener('click', (event) => {
388
+ event.preventDefault();
389
+ event.stopPropagation();
390
+ onClick(event);
391
+ });
392
+ return button;
393
+ }
394
+ }