collabmd 0.1.33 → 0.1.34

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.
@@ -15,7 +15,7 @@
15
15
  document.documentElement.setAttribute('data-initial-file-requested', 'true');
16
16
  }
17
17
  </script>
18
- <script type="module" crossorigin src="./assets/index-Czg_KL-i.js"></script>
18
+ <script type="module" crossorigin src="./assets/index-BLrtj9YM.js"></script>
19
19
  <link rel="modulepreload" crossorigin href="./assets/runtime-config-loader-_D2KZKn-.js">
20
20
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-Bo9GmnuQ.js">
21
21
  <link rel="modulepreload" crossorigin href="./assets/base-h1uscjTi.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "collabmd",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "Collaborative markdown vault — like Obsidian, but online. Serve any directory of markdown files for realtime collaborative editing.",
6
6
  "repository": {
@@ -84,6 +84,7 @@
84
84
  "openid-client": "^6.8.2",
85
85
  "react": "^19.2.4",
86
86
  "react-dom": "^19.2.4",
87
+ "sharp": "^0.34.5",
87
88
  "ws": "^8.20.0",
88
89
  "y-codemirror.next": "^0.3.5",
89
90
  "y-protocols": "^1.0.7",
@@ -232,6 +232,48 @@ export const gitFeature = {
232
232
  await this.fileHistoryView.openFileHistory({ filePath });
233
233
  },
234
234
 
235
+ async _loadGitFileSnapshot({ hash, filePath, resolvedCurrentFilePath }) {
236
+ const data = await this.gitApiClient.readFileSnapshot({
237
+ hash,
238
+ path: filePath,
239
+ });
240
+ this.setStaticPreviewDocument?.({
241
+ ...data,
242
+ currentFilePath: resolvedCurrentFilePath,
243
+ });
244
+ this.resetPreviewMode();
245
+ this.elements.markdownToolbar?.classList.add('hidden');
246
+ this.elements.outlineToggle?.classList.toggle('hidden', data.fileKind !== 'markdown');
247
+
248
+ if (data.fileKind === 'markdown' || data.fileKind === 'mermaid' || data.fileKind === 'plantuml') {
249
+ if (data.fileKind === 'mermaid') {
250
+ this.elements.previewContent?.classList.add('is-mermaid-file-preview');
251
+ } else if (data.fileKind === 'plantuml') {
252
+ this.elements.previewContent?.classList.add('is-plantuml-file-preview');
253
+ }
254
+ this.previewRenderer.beginDocumentLoad();
255
+ this.previewRenderer.queueRender();
256
+ return;
257
+ }
258
+
259
+ this.elements.outlineToggle?.classList.add('hidden');
260
+ this.renderTextFilePreview?.({
261
+ content: data.content,
262
+ filePath: resolvedCurrentFilePath,
263
+ });
264
+ },
265
+
266
+ _handleGitFilePreviewError(error, resolvedCurrentFilePath) {
267
+ console.error('[git-preview] Failed to load file snapshot:', error);
268
+ this.clearStaticPreviewDocument?.();
269
+ this.toastController.show('Failed to load historical file preview');
270
+ this.renderTextFilePreview?.({
271
+ content: 'Failed to load historical file preview.',
272
+ filePath: resolvedCurrentFilePath,
273
+ });
274
+ this.elements.outlineToggle?.classList.add('hidden');
275
+ },
276
+
235
277
  async showGitFilePreview({ hash, filePath = null, currentFilePath = null } = {}) {
236
278
  const resolvedCurrentFilePath = String(currentFilePath ?? '').trim() || filePath;
237
279
  if (!hash || !filePath || !resolvedCurrentFilePath) {
@@ -256,43 +298,9 @@ export const gitFeature = {
256
298
  this.syncFileHistoryButton({ filePath: resolvedCurrentFilePath, mode: 'history-preview' });
257
299
 
258
300
  try {
259
- const data = await this.gitApiClient.readFileSnapshot({
260
- hash,
261
- path: filePath,
262
- });
263
- this.setStaticPreviewDocument?.({
264
- ...data,
265
- currentFilePath: resolvedCurrentFilePath,
266
- });
267
- this.resetPreviewMode();
268
- this.elements.markdownToolbar?.classList.add('hidden');
269
- this.elements.outlineToggle?.classList.toggle('hidden', data.fileKind !== 'markdown');
270
-
271
- if (data.fileKind === 'markdown' || data.fileKind === 'mermaid' || data.fileKind === 'plantuml') {
272
- if (data.fileKind === 'mermaid') {
273
- this.elements.previewContent?.classList.add('is-mermaid-file-preview');
274
- } else if (data.fileKind === 'plantuml') {
275
- this.elements.previewContent?.classList.add('is-plantuml-file-preview');
276
- }
277
- this.previewRenderer.beginDocumentLoad();
278
- this.previewRenderer.queueRender();
279
- return;
280
- }
281
-
282
- this.elements.outlineToggle?.classList.add('hidden');
283
- this.renderTextFilePreview?.({
284
- content: data.content,
285
- filePath: resolvedCurrentFilePath,
286
- });
301
+ await this._loadGitFileSnapshot({ hash, filePath, resolvedCurrentFilePath });
287
302
  } catch (error) {
288
- console.error('[git-preview] Failed to load file snapshot:', error);
289
- this.clearStaticPreviewDocument?.();
290
- this.toastController.show('Failed to load historical file preview');
291
- this.renderTextFilePreview?.({
292
- content: 'Failed to load historical file preview.',
293
- filePath: resolvedCurrentFilePath,
294
- });
295
- this.elements.outlineToggle?.classList.add('hidden');
303
+ this._handleGitFilePreviewError(error, resolvedCurrentFilePath);
296
304
  }
297
305
  },
298
306
 
@@ -136,31 +136,7 @@ function promptForVersionReload(payload = null) {
136
136
  }
137
137
 
138
138
  /** @this {UiShellContext} */
139
- function bindEvents() {
140
- this.elements.emptyStateNewFileBtn?.addEventListener('click', () => {
141
- this.fileExplorer.actionController.openRootCreateMenu({
142
- anchor: this.elements.emptyStateNewFileBtn,
143
- });
144
- });
145
-
146
- this.elements.emptyStateSearchBtn?.addEventListener('click', () => {
147
- void this.toggleQuickSwitcher();
148
- });
149
-
150
- this.elements.chatToggleButton?.addEventListener('click', () => {
151
- this.toggleChatPanel();
152
- this.closeToolbarOverflowMenu?.();
153
- });
154
-
155
- this.elements.chatForm?.addEventListener('submit', (event) => {
156
- event.preventDefault();
157
- this.handleChatSubmit();
158
- });
159
-
160
- this.elements.chatNotificationButton?.addEventListener('click', () => {
161
- void this.handleChatNotificationToggle();
162
- });
163
-
139
+ function bindToolbarEvents() {
164
140
  this.elements.shareButton?.addEventListener('click', () => {
165
141
  void this.copyCurrentLink();
166
142
  this.closeToolbarOverflowMenu?.();
@@ -197,7 +173,10 @@ function bindEvents() {
197
173
  this.openDisplayNameDialog();
198
174
  this.closeToolbarOverflowMenu?.();
199
175
  });
176
+ }
200
177
 
178
+ /** @this {UiShellContext} */
179
+ function bindDialogEvents() {
201
180
  this.elements.displayNameCancel?.addEventListener('click', () => {
202
181
  this.elements.displayNameDialog?.close();
203
182
  });
@@ -233,14 +212,6 @@ function bindEvents() {
233
212
  }
234
213
  });
235
214
 
236
- this.elements.markdownToolbar?.addEventListener('click', (event) => {
237
- this.handleMarkdownToolbarClick?.(event);
238
- });
239
-
240
- this.elements.markdownToolbar?.addEventListener('keydown', (event) => {
241
- this.handleMarkdownToolbarKeydown?.(event);
242
- });
243
-
244
215
  this.elements.displayNameForm?.addEventListener('submit', (event) => {
245
216
  event.preventDefault();
246
217
  this.handleDisplayNameSubmit();
@@ -250,6 +221,44 @@ function bindEvents() {
250
221
  event.preventDefault();
251
222
  void this.handleGitCommitSubmit();
252
223
  });
224
+ }
225
+
226
+ /** @this {UiShellContext} */
227
+ function bindEvents() {
228
+ this.elements.emptyStateNewFileBtn?.addEventListener('click', () => {
229
+ this.fileExplorer.actionController.openRootCreateMenu({
230
+ anchor: this.elements.emptyStateNewFileBtn,
231
+ });
232
+ });
233
+
234
+ this.elements.emptyStateSearchBtn?.addEventListener('click', () => {
235
+ void this.toggleQuickSwitcher();
236
+ });
237
+
238
+ this.elements.chatToggleButton?.addEventListener('click', () => {
239
+ this.toggleChatPanel();
240
+ this.closeToolbarOverflowMenu?.();
241
+ });
242
+
243
+ this.elements.chatForm?.addEventListener('submit', (event) => {
244
+ event.preventDefault();
245
+ this.handleChatSubmit();
246
+ });
247
+
248
+ this.elements.chatNotificationButton?.addEventListener('click', () => {
249
+ void this.handleChatNotificationToggle();
250
+ });
251
+
252
+ bindToolbarEvents.call(this);
253
+ bindDialogEvents.call(this);
254
+
255
+ this.elements.markdownToolbar?.addEventListener('click', (event) => {
256
+ this.handleMarkdownToolbarClick?.(event);
257
+ });
258
+
259
+ this.elements.markdownToolbar?.addEventListener('keydown', (event) => {
260
+ this.handleMarkdownToolbarKeydown?.(event);
261
+ });
253
262
 
254
263
  this.elements.tabLockTakeoverButton?.addEventListener('click', () => {
255
264
  this.handleTabTakeover();
@@ -299,43 +308,53 @@ function bindEvents() {
299
308
  });
300
309
 
301
310
  document.addEventListener('pointerdown', (event) => {
302
- this.handleMarkdownToolbarDocumentPointerDown?.(event);
311
+ this.handleDocumentPointerDown(event);
312
+ });
303
313
 
304
- if (
305
- this.toolbarOverflowOpen
306
- && !this.elements.toolbarOverflowMenu?.contains(event.target)
307
- && !this.elements.toolbarOverflowToggle?.contains(event.target)
308
- ) {
309
- this.closeToolbarOverflowMenu();
310
- }
314
+ document.addEventListener('keydown', (event) => {
315
+ this.handleDocumentKeydown(event);
316
+ });
317
+ }
311
318
 
312
- if (!this.chatIsOpen) {
313
- return;
314
- }
319
+ /** @this {UiShellContext} */
320
+ function handleDocumentPointerDown(event) {
321
+ this.handleMarkdownToolbarDocumentPointerDown?.(event);
322
+
323
+ if (
324
+ this.toolbarOverflowOpen
325
+ && !this.elements.toolbarOverflowMenu?.contains(event.target)
326
+ && !this.elements.toolbarOverflowToggle?.contains(event.target)
327
+ ) {
328
+ this.closeToolbarOverflowMenu();
329
+ }
315
330
 
316
- if (this.elements.chatContainer?.contains(event.target)) {
317
- return;
318
- }
331
+ if (!this.chatIsOpen) {
332
+ return;
333
+ }
319
334
 
320
- this.closeChatPanel();
321
- });
335
+ if (this.elements.chatContainer?.contains(event.target)) {
336
+ return;
337
+ }
322
338
 
323
- document.addEventListener('keydown', (event) => {
324
- if (event.key === 'Escape' && this.toolbarOverflowOpen) {
325
- this.closeToolbarOverflowMenu();
326
- return;
327
- }
339
+ this.closeChatPanel();
340
+ }
328
341
 
329
- if (event.key === 'Escape' && this.chatIsOpen) {
330
- this.closeChatPanel();
331
- return;
332
- }
342
+ /** @this {UiShellContext} */
343
+ function handleDocumentKeydown(event) {
344
+ if (event.key === 'Escape' && this.toolbarOverflowOpen) {
345
+ this.closeToolbarOverflowMenu();
346
+ return;
347
+ }
333
348
 
334
- if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
335
- event.preventDefault();
336
- void this.toggleQuickSwitcher();
337
- }
338
- });
349
+ if (event.key === 'Escape' && this.chatIsOpen) {
350
+ this.closeChatPanel();
351
+ return;
352
+ }
353
+
354
+ if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
355
+ event.preventDefault();
356
+ void this.toggleQuickSwitcher();
357
+ }
339
358
  }
340
359
 
341
360
  function isNestedTaskListClickTarget(target, taskItem) {
@@ -521,6 +540,8 @@ export const uiFeatureShellMethods = {
521
540
  closeToolbarOverflowMenu,
522
541
  getStoredLineWrapping,
523
542
  handleConnectionChange,
543
+ handleDocumentKeydown,
544
+ handleDocumentPointerDown,
524
545
  handlePreviewContentClick,
525
546
  handleThemeChange,
526
547
  hideEditorLoading,
@@ -153,6 +153,7 @@ export class ExcalidrawEmbedController {
153
153
  this.hydrationIdleId = null;
154
154
  this.hydrationQueue = [];
155
155
  this.hydrationInProgress = false;
156
+ this._exitMaximizedEmbed();
156
157
  if (this.overlayRoot) {
157
158
  this.overlayRoot.hidden = true;
158
159
  }
@@ -762,7 +763,9 @@ export class ExcalidrawEmbedController {
762
763
  enterMaximize();
763
764
  }
764
765
  });
765
- openBtn.addEventListener('click', () => {
766
+ openBtn.addEventListener('click', (event) => {
767
+ event.preventDefault();
768
+ this._exitMaximizedEmbed();
766
769
  this.onOpenFile?.(entry.filePath);
767
770
  });
768
771
 
@@ -516,9 +516,15 @@ export class FileHistoryViewController {
516
516
  additions: Number(selectedEntry.additions || 0),
517
517
  deletions: Number(selectedEntry.deletions || 0),
518
518
  } : null);
519
+ const hasSelection = Boolean(selectedEntry);
520
+
521
+ this.syncToolbarIndicators(selectedEntry, selectedSummary, hasSelection);
522
+ this.syncToolbarButtonStates(hasSelection);
523
+ }
524
+
525
+ syncToolbarIndicators(selectedEntry, selectedSummary, hasSelection) {
519
526
  const additions = Number(selectedSummary?.additions || 0);
520
527
  const deletions = Number(selectedSummary?.deletions || 0);
521
- const hasSelection = Boolean(selectedEntry);
522
528
 
523
529
  if (this.fileIndicator) {
524
530
  this.fileIndicator.textContent = selectedEntry?.type === 'commit'
@@ -535,7 +541,9 @@ export class FileHistoryViewController {
535
541
  `;
536
542
  this.stats.classList.toggle('hidden', !hasSelection);
537
543
  }
544
+ }
538
545
 
546
+ syncToolbarButtonStates(hasSelection) {
539
547
  this.backToHistoryButton?.classList.add('hidden');
540
548
  this.gitActionsGroup?.classList.add('hidden');
541
549
  this.layoutToggle?.classList.add('hidden');
@@ -149,115 +149,125 @@ export class GitPanelController {
149
149
  };
150
150
  }
151
151
 
152
- initialize() {
153
- this.panel?.addEventListener('click', (event) => {
154
- const modeButton = event.target instanceof Element
155
- ? event.target.closest('[data-git-panel-mode]')
156
- : null;
157
- if (modeButton) {
158
- const nextMode = modeButton.getAttribute('data-git-panel-mode');
159
- if (nextMode === 'changes' || nextMode === 'history') {
160
- void this.setMode(nextMode);
161
- }
162
- return;
152
+ _handlePanelModeClick(event) {
153
+ const modeButton = event.target instanceof Element
154
+ ? event.target.closest('[data-git-panel-mode]')
155
+ : null;
156
+ if (modeButton) {
157
+ const nextMode = modeButton.getAttribute('data-git-panel-mode');
158
+ if (nextMode === 'changes' || nextMode === 'history') {
159
+ void this.setMode(nextMode);
163
160
  }
161
+ return true;
162
+ }
163
+ return false;
164
+ }
164
165
 
165
- const toggleButton = event.target instanceof Element
166
- ? event.target.closest('[data-git-section-toggle]')
167
- : null;
168
- if (toggleButton) {
169
- const sectionKey = toggleButton.getAttribute('data-git-section-toggle');
170
- this.toggleSection(sectionKey);
171
- return;
172
- }
166
+ _handleSectionToggleClick(event) {
167
+ const toggleButton = event.target instanceof Element
168
+ ? event.target.closest('[data-git-section-toggle]')
169
+ : null;
170
+ if (toggleButton) {
171
+ this.toggleSection(toggleButton.getAttribute('data-git-section-toggle'));
172
+ return true;
173
+ }
174
+ return false;
175
+ }
173
176
 
174
- const historyButton = event.target instanceof Element
175
- ? event.target.closest('[data-git-commit-hash]')
176
- : null;
177
- if (historyButton && !event.target.closest('[data-git-history-load-more]')) {
178
- const hash = historyButton.getAttribute('data-git-commit-hash');
179
- if (!hash) {
180
- return;
181
- }
177
+ _handleCommitHistoryClick(event) {
178
+ const historyButton = event.target instanceof Element
179
+ ? event.target.closest('[data-git-commit-hash]')
180
+ : null;
181
+ if (historyButton && !event.target.closest('[data-git-history-load-more]')) {
182
+ const hash = historyButton.getAttribute('data-git-commit-hash');
183
+ if (hash) {
182
184
  this.onSelectCommit(hash, { path: this.selection.commitHash === hash ? this.selection.path : null });
183
- return;
184
185
  }
186
+ return true;
187
+ }
188
+ return false;
189
+ }
185
190
 
186
- const loadMoreButton = event.target instanceof Element
187
- ? event.target.closest('[data-git-history-load-more]')
188
- : null;
189
- if (loadMoreButton) {
190
- void this.loadMoreHistory();
191
- return;
191
+ _handleFileSelectionClick(event) {
192
+ const fileButton = event.target instanceof Element
193
+ ? event.target.closest('[data-git-path]')
194
+ : null;
195
+ if (fileButton && !event.target.closest('[data-git-file-action]')) {
196
+ const filePath = fileButton.getAttribute('data-git-path');
197
+ if (filePath) {
198
+ this.onSelectDiff(filePath, { scope: fileButton.getAttribute('data-git-scope') || 'working-tree' });
192
199
  }
200
+ return true;
201
+ }
202
+ return false;
203
+ }
193
204
 
194
- const fileButton = event.target instanceof Element
195
- ? event.target.closest('[data-git-path]')
196
- : null;
197
- if (fileButton && !event.target.closest('[data-git-file-action]')) {
198
- const filePath = fileButton.getAttribute('data-git-path');
199
- const scope = fileButton.getAttribute('data-git-scope') || 'working-tree';
200
- if (!filePath) {
201
- return;
202
- }
203
- this.onSelectDiff(filePath, { scope });
204
- return;
205
+ _handleFileActionClick(event) {
206
+ const actionButton = event.target instanceof Element
207
+ ? event.target.closest('[data-git-file-action]')
208
+ : null;
209
+ if (actionButton) {
210
+ const action = actionButton.getAttribute('data-git-file-action');
211
+ const filePath = actionButton.getAttribute('data-git-path');
212
+ if (action && filePath) {
213
+ event.preventDefault();
214
+ event.stopPropagation();
215
+ void this.handleFileAction(action, filePath, actionButton.getAttribute('data-git-scope') || 'working-tree');
205
216
  }
217
+ return true;
218
+ }
219
+ return false;
220
+ }
206
221
 
207
- const pullBackupButton = event.target instanceof Element
208
- ? event.target.closest('[data-git-pull-backup-path]')
209
- : null;
210
- if (pullBackupButton) {
211
- const summaryPath = pullBackupButton.getAttribute('data-git-pull-backup-path');
212
- if (!summaryPath) {
213
- return;
214
- }
222
+ _handlePanelClick(event) {
223
+ if (this._handlePanelModeClick(event)) return;
224
+ if (this._handleSectionToggleClick(event)) return;
225
+ if (this._handleCommitHistoryClick(event)) return;
226
+
227
+ if (event.target instanceof Element && event.target.closest('[data-git-history-load-more]')) {
228
+ void this.loadMoreHistory();
229
+ return;
230
+ }
231
+
232
+ if (this._handleFileSelectionClick(event)) return;
233
+
234
+ const pullBackupButton = event.target instanceof Element
235
+ ? event.target.closest('[data-git-pull-backup-path]')
236
+ : null;
237
+ if (pullBackupButton) {
238
+ const summaryPath = pullBackupButton.getAttribute('data-git-pull-backup-path');
239
+ if (summaryPath) {
215
240
  this.onOpenPullBackup(summaryPath);
216
- return;
217
241
  }
242
+ return;
243
+ }
218
244
 
219
- const actionButton = event.target instanceof Element
220
- ? event.target.closest('[data-git-file-action]')
221
- : null;
222
- if (actionButton) {
223
- const action = actionButton.getAttribute('data-git-file-action');
224
- const filePath = actionButton.getAttribute('data-git-path');
225
- const scope = actionButton.getAttribute('data-git-scope') || 'working-tree';
226
- if (!action || !filePath) {
227
- return;
228
- }
245
+ if (this._handleFileActionClick(event)) return;
229
246
 
230
- event.preventDefault();
231
- event.stopPropagation();
232
- void this.handleFileAction(action, filePath, scope);
233
- return;
234
- }
247
+ if (event.target instanceof Element && event.target.closest('[data-git-view-all]')) {
248
+ this.onViewAllDiff();
249
+ return;
250
+ }
235
251
 
236
- const viewAllButton = event.target instanceof Element
237
- ? event.target.closest('[data-git-view-all]')
238
- : null;
239
- if (viewAllButton) {
240
- this.onViewAllDiff();
241
- return;
242
- }
252
+ if (event.target instanceof Element && event.target.closest('[data-git-commit-staged]')) {
253
+ void this.handleCommitStaged();
254
+ return;
255
+ }
243
256
 
244
- const commitStagedButton = event.target instanceof Element
245
- ? event.target.closest('[data-git-commit-staged]')
246
- : null;
247
- if (commitStagedButton) {
248
- void this.handleCommitStaged();
249
- return;
257
+ const syncButton = event.target instanceof Element
258
+ ? event.target.closest('[data-git-sync-action]')
259
+ : null;
260
+ if (syncButton) {
261
+ const action = syncButton.getAttribute('data-git-sync-action');
262
+ if (action === 'pull' || action === 'push') {
263
+ void this.handleSyncAction(action);
250
264
  }
265
+ }
266
+ }
251
267
 
252
- const syncButton = event.target instanceof Element
253
- ? event.target.closest('[data-git-sync-action]')
254
- : null;
255
- if (syncButton) {
256
- const action = syncButton.getAttribute('data-git-sync-action');
257
- if (action === 'pull' || action === 'push') {
258
- void this.handleSyncAction(action);
259
- }
260
- }
268
+ initialize() {
269
+ this.panel?.addEventListener('click', (event) => {
270
+ this._handlePanelClick(event);
261
271
  });
262
272
 
263
273
  this.searchInput?.addEventListener('input', (event) => {