collabmd 0.1.44 → 0.1.45

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 (170) hide show
  1. package/dist/client/assets/{chunk-AXR3VHVW-0GTk_evH.js → chunk-ERSWUTIN-ME9RVNel.js} +3 -3
  2. package/dist/client/assets/{chunk-CZQZEIFA-DT3N9fo7.js → chunk-YGJVY4SA-CjSe7Bx7.js} +2 -2
  3. package/dist/client/assets/{comment-threads-D-iKphIp.js → comment-threads-DDegmaFO.js} +1 -1
  4. package/dist/client/assets/drawio-editor-ClE8jpoG.js +9 -0
  5. package/dist/client/assets/drawioEditor-CXaTM9Ds.js +2 -0
  6. package/dist/client/assets/editor-session-GTwZS6Df.js +16 -0
  7. package/dist/client/assets/embedded-editor-base-CiNEc8vy.css +1 -0
  8. package/dist/client/assets/{en-GADW4C6J-DQ6zyCEM.js → en-7NFCYGKX-BrgYZAEe.js} +1 -1
  9. package/dist/client/assets/{excalidraw-editor-BHu6k8d4.js → excalidraw-editor-BZVM81BO.js} +2 -2
  10. package/dist/client/assets/excalidraw-editor-CyGwOhLz.css +1 -0
  11. package/dist/client/assets/excalidraw-scene-C8n9CHsK.js +176 -0
  12. package/dist/client/assets/excalidrawEditor-DxJanCze.js +2 -0
  13. package/dist/client/assets/export-page-CUfws2T-.js +39 -0
  14. package/dist/client/assets/exportDocument-BCuxqXDY.js +2 -0
  15. package/dist/client/assets/exportDocument-DV-OX74E.css +1 -0
  16. package/dist/client/assets/image-QXRYHUNP-lfTHE3bb.js +1 -0
  17. package/dist/client/assets/index-C3YnUgMD.css +1 -0
  18. package/dist/client/assets/index-D2wXraJ1.js +2 -0
  19. package/dist/client/assets/main-DtiGz7uI.js +795 -0
  20. package/dist/client/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  21. package/dist/client/assets/preview-diagram-utils-txBCoW8d.js +1 -0
  22. package/dist/client/assets/quick-switcher-controller-shW_6vPY.js +12 -0
  23. package/dist/client/assets/room-CQLoqtMo.js +1 -0
  24. package/dist/client/assets/runtime-config-DSGmGE9V.js +1 -0
  25. package/dist/client/assets/vault-api-client-C6bcGwy-.js +5 -0
  26. package/dist/client/assets/{yjs-provider-reset-guard-Cox4nxwu.js → yjs-provider-reset-guard-DE0vINDP.js} +1 -1
  27. package/dist/client/drawio-editor.html +3 -2
  28. package/dist/client/excalidraw-editor.html +3 -2
  29. package/dist/client/export-document.html +4 -3
  30. package/dist/client/index.html +97 -27
  31. package/package.json +10 -12
  32. package/src/client/app/drawio-editor-entry.js +0 -3
  33. package/src/client/app/drawio-editor.html +1 -0
  34. package/src/client/app/excalidraw-editor-entry.js +0 -3
  35. package/src/client/app/excalidraw-editor.html +1 -0
  36. package/src/client/app/export-document-entry.js +0 -3
  37. package/src/client/app/export-document.html +1 -0
  38. package/src/client/app/index.html +94 -24
  39. package/src/client/app/main-entry.js +0 -5
  40. package/src/client/application/app-shell/chat-feature.js +180 -124
  41. package/src/client/application/app-shell/comments-feature.js +1 -1
  42. package/src/client/application/app-shell/git-feature.js +4 -9
  43. package/src/client/application/app-shell/presence-feature.js +16 -10
  44. package/src/client/application/app-shell/ui-feature-shell.js +33 -15
  45. package/src/client/application/app-shell/ui-feature-sidebar.js +74 -1
  46. package/src/client/application/app-shell/ui-feature-tab-activity.js +2 -2
  47. package/src/client/application/app-shell/workspace-feature.js +22 -83
  48. package/src/client/application/app-shell-elements.js +7 -1
  49. package/src/client/application/diagram-chrome.js +161 -166
  50. package/src/client/application/diagram-preview-export.js +7 -34
  51. package/src/client/application/diagram-preview-hydrator.js +1 -19
  52. package/src/client/application/markdown-frontmatter.js +0 -2
  53. package/src/client/application/mermaid-preview-hydrator.js +1 -14
  54. package/src/client/application/plantuml-preview-hydrator.js +0 -10
  55. package/src/client/application/preview-diagram-utils.js +31 -2
  56. package/src/client/application/preview-render-compiler.js +2 -1
  57. package/src/client/application/preview-renderer.js +2 -2
  58. package/src/client/application/quick-switcher-loader.js +3 -1
  59. package/src/client/application/workspace-preview-controller.js +0 -3
  60. package/src/client/bootstrap/collabmd-app-shell.js +125 -39
  61. package/src/client/domain/excalidraw-collaboration.js +0 -2
  62. package/src/client/domain/room.js +38 -0
  63. package/src/client/export/export-pipeline.js +6 -34
  64. package/src/client/infrastructure/auth-client.js +4 -10
  65. package/src/client/infrastructure/browser-notification-port.js +33 -20
  66. package/src/client/infrastructure/browser-preferences-port.js +36 -9
  67. package/src/client/infrastructure/editor-view-adapter.js +87 -3
  68. package/src/client/infrastructure/lobby-presence.js +1 -0
  69. package/src/client/infrastructure/vault-api-client.js +25 -0
  70. package/src/client/infrastructure/workspace-sync-client.js +13 -1
  71. package/src/client/presentation/comment-overview-controller.js +18 -53
  72. package/src/client/presentation/comment-ui/comment-ui-card.js +1 -12
  73. package/src/client/presentation/comment-ui/comment-ui-layout.js +242 -219
  74. package/src/client/presentation/comment-ui/comment-ui-render.js +32 -45
  75. package/src/client/presentation/comment-ui/comment-ui-shared.js +65 -14
  76. package/src/client/presentation/comment-ui-controller.js +5 -1
  77. package/src/client/presentation/file-action-controller.js +32 -0
  78. package/src/client/presentation/file-explorer-controller.js +33 -3
  79. package/src/client/presentation/file-explorer-view.js +49 -5
  80. package/src/client/presentation/file-tree-state.js +10 -4
  81. package/src/client/presentation/quick-switcher-controller.js +243 -72
  82. package/src/client/presentation/quick-switcher-text-search.js +2 -2
  83. package/src/client/presentation/toast-controller.js +3 -0
  84. package/src/client/styles/components/feedback.css +6 -0
  85. package/src/client/styles/features/collaboration-chat.css +49 -53
  86. package/src/client/styles/features/collaboration-presence.css +9 -7
  87. package/src/client/styles/features/comment-card.css +25 -11
  88. package/src/client/styles/features/comment-overlays.css +0 -4
  89. package/src/client/styles/features/comment-overview.css +44 -21
  90. package/src/client/styles/features/comments-drawer.css +2 -100
  91. package/src/client/styles/features/diagram-preview.css +5 -18
  92. package/src/client/styles/features/embedded-preview.css +8 -33
  93. package/src/client/styles/features/quick-switcher.css +8 -0
  94. package/src/client/styles/foundation/tokens.css +1 -0
  95. package/src/client/styles/layout/editor-page.css +40 -0
  96. package/src/client/styles/layout/responsive.css +31 -1
  97. package/src/client/styles/layout/shell.css +25 -6
  98. package/src/client/styles/layout/sidebar.css +47 -15
  99. package/src/client/styles/overrides/codemirror.css +217 -15
  100. package/src/server/application/workspace-reconciliation.js +1 -1
  101. package/src/server/create-app-server.js +2 -3
  102. package/src/server/domain/bases/base-query-service.js +1 -77
  103. package/src/server/domain/comment-overview.js +10 -3
  104. package/src/server/domain/docx-exporter.js +24 -33
  105. package/src/server/domain/ripgrep-search-service.js +3 -1
  106. package/src/server/domain/workspace-state.js +32 -11
  107. package/src/server/infrastructure/git/diff-service.js +24 -13
  108. package/src/server/infrastructure/git/git-service.js +40 -10
  109. package/src/server/infrastructure/git/history-service.js +42 -29
  110. package/src/server/infrastructure/git/path-utils.js +1 -1
  111. package/src/server/infrastructure/git/responses.js +0 -95
  112. package/src/server/infrastructure/github/github-app-client.js +22 -15
  113. package/src/server/infrastructure/http/create-request-handler.js +2 -8
  114. package/src/server/infrastructure/http/create-vault-api-command-handler.js +5 -5
  115. package/src/server/infrastructure/http/http-errors.js +7 -1
  116. package/dist/client/assets/bases-preview-controller-2pLHk0uk.js +0 -143
  117. package/dist/client/assets/button-BCEiw4Rw.js +0 -1
  118. package/dist/client/assets/diagram-action-icons-aa_KOv9h.js +0 -36
  119. package/dist/client/assets/drawio-editor-DDvFoarP.js +0 -9
  120. package/dist/client/assets/drawio-embed-controller-BD1GTRwn.js +0 -1
  121. package/dist/client/assets/drawioEditor-Bu5grlH5.js +0 -2
  122. package/dist/client/assets/editor-session-DGlpiAsK.js +0 -16
  123. package/dist/client/assets/embedded-editor-base-C9roKYpW.css +0 -1
  124. package/dist/client/assets/excalidraw-editor-Cr_184EF.css +0 -1
  125. package/dist/client/assets/excalidraw-embed-controller-y2fenN_g.js +0 -1
  126. package/dist/client/assets/excalidraw-embed-reconciler-DGiagExg.js +0 -1
  127. package/dist/client/assets/excalidraw-scene-BvsQ6mLP.js +0 -176
  128. package/dist/client/assets/excalidrawEditor-4vFE--Uw.js +0 -2
  129. package/dist/client/assets/export-page-Cby8JzPO.js +0 -39
  130. package/dist/client/assets/exportDocument-C9v-bc7I.css +0 -1
  131. package/dist/client/assets/exportDocument-CRHCfsdo.js +0 -2
  132. package/dist/client/assets/file-history-view-controller-BDjEV9xJ.js +0 -112
  133. package/dist/client/assets/git-diff-view-controller-DgdM1aqb.js +0 -150
  134. package/dist/client/assets/git-panel-controller-BDTR2HVp.js +0 -207
  135. package/dist/client/assets/image-XGD7JQR6-XDUWHRpO.js +0 -1
  136. package/dist/client/assets/index-7xOyXDhP.css +0 -1
  137. package/dist/client/assets/index-Bz1KuQ6y.js +0 -2
  138. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  139. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  140. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  141. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  142. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  143. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  144. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  145. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  146. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  147. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  148. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  149. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  150. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  151. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  152. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  153. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  154. package/dist/client/assets/main-Ci07m9oP.js +0 -152
  155. package/dist/client/assets/preview-diagram-utils-n_bbJA9V.js +0 -1
  156. package/dist/client/assets/quick-switcher-controller-CoiYvllm.js +0 -12
  157. package/dist/client/assets/room-CDrmIdDt.js +0 -1
  158. package/dist/client/assets/runtime-config-CDgeuPWN.js +0 -1
  159. package/dist/client/assets/runtime-config-loader-BHGniHP5.js +0 -1
  160. package/dist/client/assets/runtime-paths-DvpihCEq.js +0 -1
  161. package/dist/client/assets/segmented-control-DJrHSz8M.js +0 -1
  162. package/dist/client/assets/vault-api-client-sLB2bo56.js +0 -1
  163. package/dist/client/assets/vault-paths-CVkuh0j-.js +0 -5
  164. package/src/client/bootstrap/lazy-controller-feature.js +0 -433
  165. package/src/client/infrastructure/backlinks-api-client.js +0 -16
  166. package/src/client/infrastructure/plantuml-api-client.js +0 -19
  167. package/src/client/infrastructure/runtime-config-loader.js +0 -56
  168. package/src/server/infrastructure/git/command-runner.js +0 -49
  169. package/src/server/infrastructure/git/errors.js +0 -8
  170. package/src/server/infrastructure/http/create-esm-proxy-handler.js +0 -130
@@ -8,6 +8,37 @@ export {
8
8
  requestIdleRender,
9
9
  } from '../browser-utils.js';
10
10
 
11
+ export function encodeSvgDataUrl(svgMarkup) {
12
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup)}`;
13
+ }
14
+
15
+ export function loadImage(src) {
16
+ return new Promise((resolve, reject) => {
17
+ const image = new Image();
18
+ image.decoding = 'async';
19
+ image.addEventListener('load', () => resolve(image), { once: true });
20
+ image.addEventListener('error', () => reject(new Error(`Failed to load image: ${src}`)), { once: true });
21
+ image.src = src;
22
+ });
23
+ }
24
+
25
+ export function resolveSvgDimensions(svgElement) {
26
+ const widthAttr = Number.parseFloat(svgElement.getAttribute('width') || '');
27
+ const heightAttr = Number.parseFloat(svgElement.getAttribute('height') || '');
28
+ const viewBoxParts = (svgElement.getAttribute('viewBox') || '')
29
+ .split(/\s+/u)
30
+ .map((value) => Number.parseFloat(value));
31
+
32
+ return {
33
+ height: Number.isFinite(heightAttr) && heightAttr > 0
34
+ ? heightAttr
35
+ : (Number.isFinite(viewBoxParts[3]) && viewBoxParts[3] > 0 ? viewBoxParts[3] : 800),
36
+ width: Number.isFinite(widthAttr) && widthAttr > 0
37
+ ? widthAttr
38
+ : (Number.isFinite(viewBoxParts[2]) && viewBoxParts[2] > 0 ? viewBoxParts[2] : 1200),
39
+ };
40
+ }
41
+
11
42
  export function syncAttribute(target, source, name) {
12
43
  const nextValue = source.getAttribute(name);
13
44
  if (nextValue === null) {
@@ -62,10 +93,8 @@ export function normalizeMermaidSvg(svg) {
62
93
  export function getFrameViewportSize(frame) {
63
94
  const styles = window.getComputedStyle(frame);
64
95
  const paddingX = Number.parseFloat(styles.paddingLeft || '0') + Number.parseFloat(styles.paddingRight || '0');
65
- const paddingY = Number.parseFloat(styles.paddingTop || '0') + Number.parseFloat(styles.paddingBottom || '0');
66
96
 
67
97
  return {
68
- height: Math.max(frame.clientHeight - paddingY, 0),
69
98
  width: Math.max(frame.clientWidth - paddingX, 0),
70
99
  };
71
100
  }
@@ -5,7 +5,8 @@ import { isImageAttachmentFilePath } from '../../domain/file-kind.js';
5
5
  import { resolveVaultRelativePath } from '../../domain/vault-paths.js';
6
6
  import { resolveWikiTargetPath } from '../../domain/wiki-link-resolver.js';
7
7
  import { escapeHtml } from '../domain/vault-utils.js';
8
- import { extractYamlFrontmatter, renderFrontmatterBlock } from './markdown-frontmatter.js';
8
+ import { extractYamlFrontmatter } from '../../domain/yaml-frontmatter.js';
9
+ import { renderFrontmatterBlock } from './markdown-frontmatter.js';
9
10
  import { analyzeMarkdownComplexity } from './preview-render-profile.js';
10
11
 
11
12
  const DIRECT_VIDEO_MIME_TYPES = Object.freeze({
@@ -91,8 +91,8 @@ export class PreviewRenderer {
91
91
  };
92
92
 
93
93
  this.handleWindowResize = () => {
94
- this.mermaidHydrator.scheduleActiveRefit();
95
- this.plantUmlHydrator.scheduleActiveRefit();
94
+ this.scheduleActiveMermaidRefit();
95
+ this.scheduleActivePlantUmlRefit();
96
96
  };
97
97
 
98
98
  this.diagramChrome = new DiagramChrome({
@@ -22,8 +22,10 @@ export async function ensureQuickSwitcherInstance(host) {
22
22
  if (!host.quickSwitcher) {
23
23
  host.quickSwitcher = new QuickSwitcherController({
24
24
  getFileList: () => host.fileExplorer.flatFiles,
25
+ getFileMetadata: () => host.fileExplorer.fileEntries,
26
+ getRecentFiles: () => host.preferences?.getRecentFiles?.() ?? [],
25
27
  getSearchConfig: () => host.runtimeConfig.search ?? {},
26
- onFileSelect: (filePath) => host.handleFileSelection(filePath, {
28
+ onFileSelect: (filePath) => host.workspaceRouteController.handleFileSelection(filePath, {
27
29
  closeSidebarOnMobile: true,
28
30
  revealInTree: true,
29
31
  }),
@@ -253,7 +253,6 @@ export class WorkspacePreviewController {
253
253
  this.previewRenderer.normalizePreviewChildren(renderHost);
254
254
 
255
255
  if (renderHost) {
256
- renderHost.replaceChildren();
257
256
  renderHost.style.minHeight = '';
258
257
  }
259
258
 
@@ -364,8 +363,6 @@ export class WorkspacePreviewController {
364
363
  return;
365
364
  }
366
365
 
367
- this.previewRenderer.scheduleActiveMermaidRefit();
368
- this.previewRenderer.scheduleActivePlantUmlRefit();
369
366
  this.videoEmbed?.syncLayout();
370
367
  this.drawioEmbed.syncLayout();
371
368
  this.excalidrawEmbed.syncLayout();
@@ -17,10 +17,9 @@ import { uiFeatureTabActivityMethods } from '../application/app-shell/ui-feature
17
17
  import { uiFeatureToolbarMethods } from '../application/app-shell/ui-feature-toolbar.js';
18
18
  import { workspaceFeature } from '../application/app-shell/workspace-feature.js';
19
19
  import { LOBBY_CHAT_MESSAGE_MAX_LENGTH, LobbyPresence } from '../infrastructure/lobby-presence.js';
20
- import { BrowserNotificationPort } from '../infrastructure/browser-notification-port.js';
21
20
  import { BrowserPreferencesPort } from '../infrastructure/browser-preferences-port.js';
21
+ import { BrowserNotificationPort } from '../infrastructure/browser-notification-port.js';
22
22
  import { AppVersionMonitor } from '../infrastructure/app-version-monitor.js';
23
- import { backlinksApiClient } from '../infrastructure/backlinks-api-client.js';
24
23
  import { gitApiClient } from '../infrastructure/git-api-client.js';
25
24
  import {
26
25
  getHashRoute,
@@ -32,7 +31,6 @@ import {
32
31
  navigateToGitFilePreview,
33
32
  navigateToGitHistory,
34
33
  } from '../infrastructure/runtime-config.js';
35
- import { plantUmlApiClient } from '../infrastructure/plantuml-api-client.js';
36
34
  import { TabActivityLock } from '../infrastructure/tab-activity-lock.js';
37
35
  import { vaultApiClient } from '../infrastructure/vault-api-client.js';
38
36
  import { WorkspaceSyncClient } from '../infrastructure/workspace-sync-client.js';
@@ -40,6 +38,12 @@ import { BacklinksPanel } from '../presentation/backlinks-panel.js';
40
38
  import { CommentOverviewController } from '../presentation/comment-overview-controller.js';
41
39
  import { CommentUiController } from '../presentation/comment-ui-controller.js';
42
40
  import { FileExplorerController } from '../presentation/file-explorer-controller.js';
41
+ import { FileHistoryViewController } from '../presentation/file-history-view-controller.js';
42
+ import { BasesPreviewController } from '../presentation/bases-preview-controller.js';
43
+ import { DrawioEmbedController } from '../presentation/drawio-embed-controller.js';
44
+ import { ExcalidrawEmbedController } from '../presentation/excalidraw-embed-controller.js';
45
+ import { GitDiffViewController } from '../presentation/git-diff-view-controller.js';
46
+ import { GitPanelController } from '../presentation/git-panel-controller.js';
43
47
  import { LayoutController } from '../presentation/layout-controller.js';
44
48
  import { OutlineController } from '../presentation/outline-controller.js';
45
49
  import { ScrollSyncController } from '../presentation/scroll-sync-controller.js';
@@ -47,14 +51,12 @@ import { ThemeController } from '../presentation/theme-controller.js';
47
51
  import { ToastController } from '../presentation/toast-controller.js';
48
52
  import { VideoEmbedController } from '../presentation/video-embed-controller.js';
49
53
  import { ImageLightboxController } from '../presentation/image-lightbox-controller.js';
50
- import { lazyControllerFeature } from './lazy-controller-feature.js';
51
54
 
52
55
  const APP_SHELL_FEATURES = [
53
56
  chatFeature,
54
57
  commentsFeature,
55
58
  exportFeature,
56
59
  gitFeature,
57
- lazyControllerFeature,
58
60
  presenceFeature,
59
61
  {
60
62
  ...uiFeatureShellMethods,
@@ -103,18 +105,18 @@ export class CollabMdAppShell {
103
105
  navigateToGitHistory,
104
106
  };
105
107
  this.preferences = new BrowserPreferencesPort({
106
- chatNotificationsKey: 'collabmd-chat-notifications-enabled',
108
+ fileTreeShowExtensionsKey: 'collabmd-file-tree-show-extensions',
107
109
  lineWrappingKey: 'collabmd-editor-line-wrap',
110
+ recentFilesKey: 'collabmd-recent-files',
108
111
  sidebarVisibleKey: 'collabmd-sidebar-visible',
109
112
  userNameKey: 'collabmd-user-name',
110
113
  });
111
114
  this.notifications = new BrowserNotificationPort();
112
- this.backlinksApiClient = backlinksApiClient;
113
115
  this.gitApiClient = gitApiClient;
114
- this.plantUmlApiClient = plantUmlApiClient;
115
116
  this.vaultApiClient = vaultApiClient;
116
117
  this._session = null;
117
118
  this._hasPromptedForDisplayName = false;
119
+ this._basePreviewRenderTimer = null;
118
120
  this._backlinkRefreshTimer = null;
119
121
  this._pendingPreviewLayoutSync = false;
120
122
  this._previewHydrationPaused = false;
@@ -124,13 +126,10 @@ export class CollabMdAppShell {
124
126
  this._staticPreviewDocument = null;
125
127
  this.pendingGitResetPath = null;
126
128
  this.chatTimeFormatter = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' });
127
- this.chatNotificationsEnabled = this.preferences.getChatNotificationsEnabled();
128
- this.chatNotificationPermission = this.notifications.getPermission();
129
129
  this.lobbyChatMessageMaxLength = LOBBY_CHAT_MESSAGE_MAX_LENGTH;
130
130
  this.quickSwitcher = null;
131
131
  this.quickSwitcherModulePromise = null;
132
132
  this.fileExplorerReadyPromise = Promise.resolve();
133
- this._gitControllerPrewarmHandle = null;
134
133
  this.mobileBreakpointQuery = window.matchMedia('(max-width: 768px)');
135
134
  this.pendingWorkspaceRequestIds = new Set();
136
135
  this._fileOpenPerf = null;
@@ -152,7 +151,7 @@ export class CollabMdAppShell {
152
151
  this.handleCommentOverviewWorkspaceTreeChange?.();
153
152
  this.fileExplorerReady = true;
154
153
  if (!wasReady && this.isTabActive) {
155
- void this.handleHashChange();
154
+ void this.workspaceRouteController.handleHashChange();
156
155
  }
157
156
  },
158
157
  onWorkspaceEvent: (event) => {
@@ -165,8 +164,14 @@ export class CollabMdAppShell {
165
164
  this.fileExplorer = new FileExplorerController({
166
165
  mobileBreakpointQuery: this.mobileBreakpointQuery,
167
166
  onFileDelete: () => this.navigation.navigateToFile(null),
168
- onFileSelect: (filePath) => this.handleFileSelection(filePath, { closeSidebarOnMobile: true }),
167
+ onFileSelect: (filePath) => this.workspaceRouteController.handleFileSelection(filePath, {
168
+ closeSidebarOnMobile: true,
169
+ }),
170
+ onShowFileExtensionsChange: (showFileExtensions) => {
171
+ this.preferences.setFileTreeShowExtensions(showFileExtensions);
172
+ },
169
173
  pendingWorkspaceRequestIds: this.pendingWorkspaceRequestIds,
174
+ showFileExtensions: this.preferences.getFileTreeShowExtensions(),
170
175
  toastController: this.toastController,
171
176
  vaultClient: this.vaultApiClient,
172
177
  });
@@ -179,7 +184,23 @@ export class CollabMdAppShell {
179
184
  toastController: this.toastController,
180
185
  vaultApiClient: this.vaultApiClient,
181
186
  });
182
- this.gitPanel = this.createLazyGitPanelController();
187
+ this.gitPanel = new GitPanelController({
188
+ enabled: this.runtimeConfig.gitEnabled !== false,
189
+ gitApiClient: this.gitApiClient,
190
+ onCommitStaged: () => this.openGitCommitDialog(),
191
+ onOpenPullBackup: (filePath) => filePath && this.navigation.navigateToFile(filePath),
192
+ onPullBranch: () => this.pullGitBranch(),
193
+ onPushBranch: () => this.pushGitBranch(),
194
+ onRepoChange: (isGitRepo, status) => this.handleGitRepoChange(isGitRepo, status),
195
+ onResetFile: (filePath, { scope }) => this.openGitResetDialog(filePath, { scope }),
196
+ onSelectCommit: (hash, { path }) => this.handleGitCommitSelection(hash, { closeSidebarOnMobile: true, path }),
197
+ onSelectDiff: (filePath, { scope }) => this.handleGitDiffSelection(filePath, { closeSidebarOnMobile: true, scope }),
198
+ onStageFile: (filePath, { scope }) => this.stageGitFile(filePath, { scope }),
199
+ onUnstageFile: (filePath, { scope }) => this.unstageGitFile(filePath, { scope }),
200
+ onViewAllDiff: () => this.handleGitDiffSelection(null, { closeSidebarOnMobile: true, scope: 'all' }),
201
+ searchInput: this.elements.gitSearchInput,
202
+ toastController: this.toastController,
203
+ });
183
204
  this.outlineController = new OutlineController({
184
205
  mobileBreakpointQuery: this.mobileBreakpointQuery,
185
206
  onNavigateToHeading: ({ sourceLine }) => {
@@ -192,7 +213,19 @@ export class CollabMdAppShell {
192
213
  this.videoEmbed = new VideoEmbedController({
193
214
  previewElement: this.elements.previewContent,
194
215
  });
195
- this.basesPreview = this.createLazyBasesPreviewController();
216
+ this.basesPreview = new BasesPreviewController({
217
+ getActiveFilePath: () => this.currentFilePath,
218
+ getSession: () => this.session,
219
+ onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
220
+ previewElement: this.elements.previewContent,
221
+ replaceBaseSource: ({ path, source }) => {
222
+ if (path && path === this.currentFilePath) {
223
+ this.session?.replaceText?.(source);
224
+ }
225
+ },
226
+ toastController: this.toastController,
227
+ vaultApiClient: this.vaultApiClient,
228
+ });
196
229
  this.imageLightbox = new ImageLightboxController({
197
230
  previewElement: this.elements.previewContent,
198
231
  });
@@ -211,12 +244,9 @@ export class CollabMdAppShell {
211
244
  renderVersion: stats?.renderVersion ?? 0,
212
245
  });
213
246
  this.videoEmbed.reconcileEmbeds(this.elements.previewContent);
214
- this.videoEmbed.syncLayout();
215
247
  this.basesPreview.reconcileEmbeds(this.elements.previewContent);
216
248
  this.drawioEmbed.reconcileEmbeds(this.elements.previewContent);
217
- this.drawioEmbed.syncLayout();
218
249
  this.excalidrawEmbed.reconcileEmbeds(this.elements.previewContent, { isLargeDocument: stats.isLargeDocument });
219
- this.excalidrawEmbed.syncLayout();
220
250
  this.scrollSyncController.setLargeDocumentMode(stats.isLargeDocument);
221
251
  this.syncPreviewHeadingLinkButtons();
222
252
  this.applyPendingPreviewRouteAnchor({ behavior: 'auto', clearMissing: true });
@@ -235,15 +265,12 @@ export class CollabMdAppShell {
235
265
  this.refreshCommentUiLayout();
236
266
  },
237
267
  onRenderComplete: () => {
238
- this.videoEmbed.syncLayout();
239
- this.drawioEmbed.syncLayout();
240
- this.excalidrawEmbed.syncLayout();
241
268
  this.applyPendingPreviewRouteAnchor({ allowExpired: true, behavior: 'auto', clearMissing: true });
242
269
  this.schedulePreviewLayoutSync({ delayMs: 0 });
243
270
  this.refreshCommentUiLayout();
244
271
  },
245
272
  outlineController: this.outlineController,
246
- plantUmlRenderClient: this.plantUmlApiClient,
273
+ plantUmlRenderClient: this.vaultApiClient,
247
274
  previewContainer: this.elements.previewContainer,
248
275
  previewElement: this.elements.previewContent,
249
276
  toastController: this.toastController,
@@ -264,12 +291,36 @@ export class CollabMdAppShell {
264
291
  this.backlinksPanel = new BacklinksPanel({
265
292
  headerPanelElement: this.elements.backlinksHeaderPanel,
266
293
  inlinePanelElement: this.elements.backlinksInlinePanel,
267
- loadBacklinks: (filePath, options = {}) => this.backlinksApiClient.readBacklinks(filePath, options),
268
- onFileSelect: (filePath) => this.handleFileSelection(filePath, { closeSidebarOnMobile: true }),
294
+ loadBacklinks: (filePath, options = {}) => this.vaultApiClient.readBacklinks(filePath, options),
295
+ onFileSelect: (filePath) => this.workspaceRouteController.handleFileSelection(filePath, {
296
+ closeSidebarOnMobile: true,
297
+ }),
269
298
  panelElement: this.elements.backlinksPanel,
270
299
  });
271
- this.excalidrawEmbed = this.createLazyExcalidrawEmbedController();
272
- this.drawioEmbed = this.createLazyDrawioEmbedController();
300
+ this.excalidrawEmbed = new ExcalidrawEmbedController({
301
+ getLocalUser: () => this.lobby.getLocalUser(),
302
+ getTheme: () => this.themeController.getTheme(),
303
+ onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
304
+ onToggleQuickSwitcher: () => {
305
+ void this.toggleQuickSwitcher();
306
+ },
307
+ previewContainer: this.elements.previewContainer,
308
+ previewElement: this.elements.previewContent,
309
+ toastController: this.toastController,
310
+ });
311
+ this.drawioEmbed = new DrawioEmbedController({
312
+ getLocalUser: () => this.lobby.getLocalUser(),
313
+ getTheme: () => this.themeController.getTheme(),
314
+ onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
315
+ onOpenTextFile: (filePath) => filePath && this.navigation.navigateToFile(filePath, { drawioMode: 'text' }),
316
+ onToggleQuickSwitcher: () => {
317
+ void this.toggleQuickSwitcher();
318
+ },
319
+ previewContainer: this.elements.previewContainer,
320
+ previewElement: this.elements.previewContent,
321
+ toastController: this.toastController,
322
+ vaultApiClient: this.vaultApiClient,
323
+ });
273
324
  this.commentUi = new CommentUiController({
274
325
  commentSelectionButton: this.elements.commentSelectionButton,
275
326
  commentsDrawer: this.elements.commentsDrawer,
@@ -329,8 +380,41 @@ export class CollabMdAppShell {
329
380
  vaultApiClient: this.vaultApiClient,
330
381
  wikiLinkAutoCreate: this.runtimeConfig.wikiLinkAutoCreate !== false,
331
382
  });
332
- this.gitDiffView = this.createLazyGitDiffViewController();
333
- this.fileHistoryView = this.createLazyFileHistoryViewController();
383
+ this.gitDiffView = new GitDiffViewController({
384
+ gitApiClient: this.gitApiClient,
385
+ onBackToHistory: ({ historyFilePath } = {}) => {
386
+ if (historyFilePath) {
387
+ this.navigation.navigateToGitFileHistory({ filePath: historyFilePath });
388
+ return;
389
+ }
390
+ this.navigation.navigateToGitHistory();
391
+ },
392
+ onCommitStaged: () => this.openGitCommitDialog(),
393
+ onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
394
+ onStageFile: (filePath, { scope }) => this.stageGitFile(filePath, { scope }),
395
+ onUnstageFile: (filePath, { scope }) => this.unstageGitFile(filePath, { scope }),
396
+ toastController: this.toastController,
397
+ });
398
+ this.fileHistoryView = new FileHistoryViewController({
399
+ diffRenderer: this.gitDiffView,
400
+ gitApiClient: this.gitApiClient,
401
+ onOpenCommitDiff: (hash, { historyFilePath, path }) => this.handleGitCommitSelection(hash, {
402
+ closeSidebarOnMobile: false,
403
+ historyFilePath,
404
+ path,
405
+ }),
406
+ onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
407
+ onOpenPreview: ({ hash, path, currentFilePath }) => this.handleGitFilePreviewSelection({
408
+ hash,
409
+ path,
410
+ currentFilePath,
411
+ }),
412
+ onOpenWorkspaceDiff: (filePath) => this.handleGitDiffSelection(filePath, {
413
+ closeSidebarOnMobile: false,
414
+ scope: 'all',
415
+ }),
416
+ toastController: this.toastController,
417
+ });
334
418
  this.tabActivityLock = new TabActivityLock({
335
419
  onActivated: ({ takeover }) => this.handleTabActivated({ takeover }),
336
420
  onBlocked: () => this.handleTabBlocked({ reason: 'active-elsewhere' }),
@@ -381,10 +465,12 @@ export class CollabMdAppShell {
381
465
  loadEditorSessionClass: () => this.loadEditorSessionClass(),
382
466
  loadBacklinks: (filePath) => this.backlinksPanel.load(filePath),
383
467
  onBeforeFileOpen: () => {
468
+ clearTimeout(this._basePreviewRenderTimer);
469
+ this._basePreviewRenderTimer = null;
384
470
  this.session = null;
385
471
  this.commentUi.attachSession(null);
386
472
  this.layoutController.reset();
387
- this.resetPreviewMode();
473
+ this.workspacePreviewController.resetPreviewMode();
388
474
  this.elements.emptyState?.classList.add('hidden');
389
475
  this.elements.editorPage?.classList.remove('hidden');
390
476
  this.elements.diffPage?.classList.add('hidden');
@@ -394,7 +480,7 @@ export class CollabMdAppShell {
394
480
  onContentChange: ({ isBase, isMermaid, isPlantUml }) => {
395
481
  this.handleCommentEditorContentChange();
396
482
  if (isBase) {
397
- void this.renderBaseFilePreview(this.currentFilePath, {
483
+ this.scheduleBaseFilePreview(this.currentFilePath, {
398
484
  source: this.session?.getText?.() ?? '',
399
485
  });
400
486
  return;
@@ -414,6 +500,9 @@ export class CollabMdAppShell {
414
500
  },
415
501
  onFileOpenReady: () => {
416
502
  this.hideEditorLoading();
503
+ if (this.currentFilePath) {
504
+ this.preferences.recordRecentFile(this.currentFilePath);
505
+ }
417
506
  },
418
507
  onSelectionChange: (anchor) => this.handleCommentSelectionChange(anchor),
419
508
  onImagePaste: (file) => this.handleEditorImageInsert(file),
@@ -422,10 +511,10 @@ export class CollabMdAppShell {
422
511
  this.session = session;
423
512
  this.commentUi.attachSession(session);
424
513
  },
425
- onRenderDrawioPreview: (filePath) => this.renderDrawioFilePreview(filePath),
514
+ onRenderDrawioPreview: (filePath) => this.workspacePreviewController.renderDrawioFilePreview(filePath),
426
515
  onRenderBasePreview: (filePath) => this.renderBaseFilePreview(filePath),
427
- onRenderExcalidrawPreview: (filePath) => this.renderExcalidrawFilePreview(filePath),
428
- onRenderImagePreview: (filePath) => this.renderImageFilePreview(filePath),
516
+ onRenderExcalidrawPreview: (filePath) => this.workspacePreviewController.renderExcalidrawFilePreview(filePath),
517
+ onRenderImagePreview: (filePath) => this.workspacePreviewController.renderImageFilePreview(filePath),
429
518
  onSyncWrapToggle: () => this.syncWrapToggle(),
430
519
  onUpdateActiveFile: (filePath) => this.fileExplorer.setActiveFile(filePath),
431
520
  onUpdateCurrentFile: (filePath) => {
@@ -433,7 +522,7 @@ export class CollabMdAppShell {
433
522
  },
434
523
  onUpdateLobbyCurrentFile: (filePath) => this.lobby.setCurrentFile(filePath),
435
524
  onUpdateVisibleChrome: (filePath, { displayName }) => {
436
- this.syncFileChrome(filePath, {
525
+ this.workspacePreviewController.syncFileChrome(filePath, {
437
526
  drawioMode: this.currentDrawioMode,
438
527
  preferPreviewForBase: this.isBaseFile(filePath),
439
528
  });
@@ -443,7 +532,7 @@ export class CollabMdAppShell {
443
532
  this.elements.activeFileName.textContent = displayName;
444
533
  }
445
534
  },
446
- onViewModeReset: () => this.resetPreviewMode(),
535
+ onViewModeReset: () => this.workspacePreviewController.resetPreviewMode(),
447
536
  renderPresence: () => this.renderPresence(),
448
537
  scrollContainerForSession: (session) => session.getScrollContainer(),
449
538
  shouldUseDrawioPreview: () => Boolean(this.runtimeConfig.drawioBaseUrl),
@@ -472,7 +561,7 @@ export class CollabMdAppShell {
472
561
  requestPreviewRouteAnchor: (anchorId, filePath) => this.requestPreviewRouteAnchor(anchorId, filePath),
473
562
  renderAvatars: () => this.renderAvatars(),
474
563
  renderPresence: () => this.renderPresence(),
475
- resetPreviewMode: () => this.resetPreviewMode(),
564
+ resetPreviewMode: () => this.workspacePreviewController.resetPreviewMode(),
476
565
  scrollSyncController: this.scrollSyncController,
477
566
  setCurrentFilePath: (value) => {
478
567
  this.currentFilePath = value;
@@ -501,9 +590,6 @@ export class CollabMdAppShell {
501
590
  workspaceCoordinator: this.workspaceCoordinator,
502
591
  });
503
592
 
504
- if (this.chatNotificationPermission !== 'granted') {
505
- this.chatNotificationsEnabled = false;
506
- }
507
593
  }
508
594
 
509
595
  get session() { return this._session; }
@@ -1,7 +1,5 @@
1
1
  import { generatePeerId, normalizeUserName } from './room.js';
2
2
 
3
- export { generatePeerId };
4
-
5
3
  export const USER_COLOR_PALETTE = [
6
4
  '#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4',
7
5
  '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6', '#f59e0b',
@@ -9,6 +9,32 @@ const USER_COLORS = [
9
9
  ];
10
10
  export const USER_NAME_MAX_LENGTH = 24;
11
11
  const LOCAL_USER_ID_STORAGE_KEY = 'collabmd-user-id';
12
+ const HEX_COLOR_RE = /^#(?:[\da-f]{3}|[\da-f]{6})$/iu;
13
+
14
+ function colorChannelToLinear(value) {
15
+ const channel = value / 255;
16
+ return channel <= 0.04045
17
+ ? channel / 12.92
18
+ : ((channel + 0.055) / 1.055) ** 2.4;
19
+ }
20
+
21
+ function getColorLuminance(color) {
22
+ const hex = color.length === 4
23
+ ? color.slice(1).split('').map((channel) => channel.repeat(2)).join('')
24
+ : color.slice(1);
25
+ const [red, green, blue] = hex.match(/../gu).map((channel) => parseInt(channel, 16));
26
+ return (
27
+ 0.2126 * colorChannelToLinear(red)
28
+ + 0.7152 * colorChannelToLinear(green)
29
+ + 0.0722 * colorChannelToLinear(blue)
30
+ );
31
+ }
32
+
33
+ function getContrastRatio(firstLuminance, secondLuminance) {
34
+ const lighter = Math.max(firstLuminance, secondLuminance);
35
+ const darker = Math.min(firstLuminance, secondLuminance);
36
+ return (lighter + 0.05) / (darker + 0.05);
37
+ }
12
38
 
13
39
  function pickRandom(items) {
14
40
  return items[Math.floor(Math.random() * items.length)];
@@ -73,6 +99,18 @@ export function normalizeUserName(value) {
73
99
  return normalized || null;
74
100
  }
75
101
 
102
+ export function getUserAvatarTextColor(value) {
103
+ const color = String(value ?? '').trim();
104
+ if (!HEX_COLOR_RE.test(color)) {
105
+ return '#fff';
106
+ }
107
+
108
+ const luminance = getColorLuminance(color.toLowerCase());
109
+ const whiteContrast = getContrastRatio(1, luminance);
110
+ const inkContrast = getContrastRatio(0, luminance);
111
+ return inkContrast >= whiteContrast ? '#000' : '#fff';
112
+ }
113
+
76
114
  export function createRandomUser(preferredName = null) {
77
115
  const color = pickRandom(USER_COLORS);
78
116
 
@@ -1,6 +1,11 @@
1
1
  import { exportToSvg as exportExcalidrawToSvg } from '@excalidraw/excalidraw';
2
2
 
3
- import { sanitizeSvgMarkup } from '../application/preview-diagram-utils.js';
3
+ import {
4
+ encodeSvgDataUrl,
5
+ loadImage,
6
+ resolveSvgDimensions,
7
+ sanitizeSvgMarkup,
8
+ } from '../application/preview-diagram-utils.js';
4
9
  import { compilePreviewDocument } from '../application/preview-render-compiler.js';
5
10
  import { stripVaultFileExtension } from '../../domain/file-kind.js';
6
11
  import { parseSceneJson, sceneToInitialData } from '../domain/excalidraw-scene.js';
@@ -60,10 +65,6 @@ function createDocumentTitle(filePath, title = '') {
60
65
  return stripVaultFileExtension(leaf) || 'document';
61
66
  }
62
67
 
63
- function encodeSvgDataUrl(svgMarkup) {
64
- return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup)}`;
65
- }
66
-
67
68
  function encodeSvgBase64DataUrl(svgMarkup) {
68
69
  const bytes = new TextEncoder().encode(String(svgMarkup ?? ''));
69
70
  const chunkSize = 0x8000;
@@ -87,35 +88,6 @@ function blobToDataUrl(blob) {
87
88
  });
88
89
  }
89
90
 
90
- function loadImage(src) {
91
- return new Promise((resolve, reject) => {
92
- const image = new Image();
93
- image.decoding = 'async';
94
- image.addEventListener('load', () => resolve(image), { once: true });
95
- image.addEventListener('error', () => reject(new Error(`Failed to load image: ${src}`)), { once: true });
96
- image.src = src;
97
- });
98
- }
99
-
100
- function resolveSvgDimensions(svgElement) {
101
- const widthAttr = Number.parseFloat(svgElement.getAttribute('width') || '');
102
- const heightAttr = Number.parseFloat(svgElement.getAttribute('height') || '');
103
- const viewBox = svgElement.getAttribute('viewBox') || '';
104
- const viewBoxParts = viewBox.split(/\s+/).map((value) => Number.parseFloat(value));
105
-
106
- const width = Number.isFinite(widthAttr) && widthAttr > 0
107
- ? widthAttr
108
- : (Number.isFinite(viewBoxParts[2]) && viewBoxParts[2] > 0 ? viewBoxParts[2] : 1200);
109
- const height = Number.isFinite(heightAttr) && heightAttr > 0
110
- ? heightAttr
111
- : (Number.isFinite(viewBoxParts[3]) && viewBoxParts[3] > 0 ? viewBoxParts[3] : 800);
112
-
113
- return {
114
- height,
115
- width,
116
- };
117
- }
118
-
119
91
  function trimSvgCanvas(svgMarkup, { padding = 16 } = {}) {
120
92
  const probe = document.createElement('div');
121
93
  probe.style.position = 'fixed';
@@ -1,5 +1,6 @@
1
1
  import '../styles/features/auth-gate.css';
2
2
 
3
+ import { getHashParamsFromRaw } from '../domain/hash-routes.js';
3
4
  import { getRuntimeConfig } from './runtime-config.js';
4
5
 
5
6
  const DEFAULT_AUTH_CONFIG = {
@@ -24,19 +25,12 @@ function getClientAuthConfig() {
24
25
  };
25
26
  }
26
27
 
27
- function getHashParams() {
28
- const rawHash = window.location.hash.startsWith('#')
29
- ? window.location.hash.slice(1)
30
- : window.location.hash;
31
- return new URLSearchParams(rawHash);
32
- }
33
-
34
28
  function getPasswordFromHash() {
35
- return getHashParams().get('auth_password') || '';
29
+ return getHashParamsFromRaw(window.location.hash).get('auth_password') || '';
36
30
  }
37
31
 
38
32
  function removePasswordFromHash() {
39
- const params = getHashParams();
33
+ const params = getHashParamsFromRaw(window.location.hash);
40
34
  if (!params.has('auth_password')) {
41
35
  return;
42
36
  }
@@ -48,7 +42,7 @@ function removePasswordFromHash() {
48
42
  }
49
43
 
50
44
  function consumeHashParam(name) {
51
- const params = getHashParams();
45
+ const params = getHashParamsFromRaw(window.location.hash);
52
46
  const value = params.get(name) || '';
53
47
  if (!value) {
54
48
  return '';