collabmd 0.1.31 → 0.1.32

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 (93) hide show
  1. package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-Br4qyh3r.js} +1 -1
  2. package/dist/client/assets/drawioEditor-ClmnTx5J.js +2 -0
  3. package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Cf-iV5-3.js} +2 -2
  4. package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
  5. package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CXAxVhlw.js} +2 -2
  6. package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DoH_kMdu.js} +2 -2
  7. package/dist/client/assets/exportDocument-9wTrBZNS.css +1 -0
  8. package/dist/client/assets/{index-kPgEYvEX.js → index-C8QZYHvV.js} +2 -2
  9. package/dist/client/assets/index-CxSbUTs2.css +1 -0
  10. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  18. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  19. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  20. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  21. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  22. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  23. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  24. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  25. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  26. package/dist/client/assets/main-BjHHMlv0.js +1249 -0
  27. package/dist/client/assets/vault-api-client-Bf9tB_GW.js +1 -0
  28. package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
  29. package/dist/client/drawio-editor.html +1 -1
  30. package/dist/client/excalidraw-editor.html +1 -1
  31. package/dist/client/export-document.html +2 -2
  32. package/dist/client/index.html +2 -2
  33. package/package.json +4 -1
  34. package/src/client/app/main-entry.js +2 -0
  35. package/src/client/application/app-shell/git-feature.js +28 -36
  36. package/src/client/application/app-shell/presence-feature.js +3 -1
  37. package/src/client/application/diagram-preview-hydrator.js +24 -15
  38. package/src/client/application/mermaid-preview-hydrator.js +121 -4
  39. package/src/client/application/plantuml-preview-hydrator.js +6 -16
  40. package/src/client/application/preview-renderer.js +11 -2
  41. package/src/client/bootstrap/collabmd-app-shell.js +176 -50
  42. package/src/client/domain/excalidraw-document-switch.js +3 -0
  43. package/src/client/excalidraw-editor.js +187 -84
  44. package/src/client/infrastructure/auth-client.js +4 -4
  45. package/src/client/infrastructure/backlinks-api-client.js +18 -0
  46. package/src/client/infrastructure/comment-thread-store.js +4 -0
  47. package/src/client/infrastructure/editor-session.js +2 -0
  48. package/src/client/infrastructure/git-api-client.js +134 -0
  49. package/src/client/infrastructure/plantuml-api-client.js +21 -0
  50. package/src/client/infrastructure/vault-api-client.js +67 -0
  51. package/src/client/presentation/backlinks-panel.js +6 -9
  52. package/src/client/presentation/bases-preview-controller.js +26 -7
  53. package/src/client/presentation/excalidraw-embed-controller.js +251 -15
  54. package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
  55. package/src/client/presentation/file-action-controller.js +97 -0
  56. package/src/client/presentation/file-explorer-controller.js +2 -0
  57. package/src/client/presentation/file-explorer-view.js +368 -0
  58. package/src/client/presentation/file-history-view-controller.js +41 -40
  59. package/src/client/presentation/git-diff-view-controller.js +70 -53
  60. package/src/client/presentation/git-panel-controller.js +12 -30
  61. package/src/client/styles/export-document.css +45 -2
  62. package/src/client/styles/features/auth-gate.css +13 -24
  63. package/src/client/styles/features/comment-card.css +6 -0
  64. package/src/client/styles/features/comments-drawer.css +38 -18
  65. package/src/client/styles/features/diagram-preview.css +33 -0
  66. package/src/client/styles/features/embedded-preview.css +37 -11
  67. package/src/client/styles/features/preview-markdown.css +103 -28
  68. package/src/client/styles/foundation/themes.css +4 -0
  69. package/src/client/styles/foundation/tokens.css +4 -3
  70. package/src/client/styles/layout/sidebar.css +72 -0
  71. package/src/server/auth/auth-common.js +224 -0
  72. package/src/server/auth/auth-constants.js +30 -0
  73. package/src/server/auth/auth-strategies.js +320 -0
  74. package/src/server/auth/create-auth-service.js +24 -545
  75. package/src/server/domain/bases/base-definition.js +255 -0
  76. package/src/server/domain/bases/base-expression-runtime.js +1159 -0
  77. package/src/server/domain/bases/base-index-snapshot-store.js +418 -0
  78. package/src/server/domain/bases/base-query-results.js +205 -0
  79. package/src/server/domain/bases/base-query-service.js +68 -1965
  80. package/src/server/domain/collaboration/collaboration-room.js +36 -1
  81. package/src/server/domain/collaboration/room-client-state-store.js +1 -0
  82. package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
  83. package/src/server/infrastructure/http/create-vault-api-query-handler.js +109 -1
  84. package/src/server/infrastructure/http/http-response.js +56 -0
  85. package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
  86. package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
  87. package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
  88. package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
  89. package/dist/client/assets/index-D69HOZHr.css +0 -1
  90. package/dist/client/assets/main-DSxNTtD-.js +0 -1243
  91. package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
  92. package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
  93. /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Bia2hI25.js} +0 -0
@@ -0,0 +1 @@
1
+ import{l as e,n as t}from"./runtime-config-CqhchMC4.js";var n={enabled:!1,implemented:!0,loginEndpoint:`/api/auth/oidc/login`,passwordLabel:`Password`,provider:``,requiresLogin:!1,sessionEndpoint:`/api/auth/session`,statusEndpoint:`/api/auth/status`,strategy:`none`,submitLabel:`Continue`},r=`data:image/png;base64,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`;function i(){return{...n,...t().auth??{}}}function a(){let e=window.location.hash.startsWith(`#`)?window.location.hash.slice(1):window.location.hash;return new URLSearchParams(e)}function o(){return a().get(`auth_password`)||``}function s(){let e=a();if(!e.has(`auth_password`))return;e.delete(`auth_password`);let t=e.toString(),n=`${window.location.pathname}${window.location.search}${t?`#${t}`:``}`;window.history.replaceState(null,``,n)}function c(e){let t=a(),n=t.get(e)||``;if(!n)return``;t.delete(e);let r=t.toString(),i=`${window.location.pathname}${window.location.search}${r?`#${r}`:``}`;return window.history.replaceState(null,``,i),n}function l(){return`${window.location.pathname}${window.location.search}${window.location.hash}`}async function u(e){let t=await fetch(e.statusEndpoint,{headers:{Accept:`application/json`}}),n=await t.json().catch(()=>({}));if(!t.ok)throw Error(n.error||`Failed to read auth status`);return n}function d(e){let t=e?.auth??{};if(t.strategy!==`oidc`||t.provider!==`google`)return;let n=String(e?.user?.name??``).trim();if(n)try{window.localStorage.setItem(`collabmd-user-name`,n)}catch{}}async function f(e,t){let n=await fetch(e.sessionEndpoint,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({password:t})}),r=await n.json().catch(()=>({}));if(!n.ok)throw Error(r.error||`Authentication failed`);return r}function p(){let e=document.createElement(`div`);e.className=`auth-gate-overlay`;let t=document.createElement(`section`);return t.className=`auth-gate-card`,e.append(t),{card:t,overlay:e}}function m(){let e=document.createElement(`span`);e.className=`auth-gate-button__icon`,e.setAttribute(`aria-hidden`,`true`);let t=document.createElement(`img`);return t.alt=``,t.decoding=`async`,t.height=20,t.loading=`eager`,t.src=r,t.width=20,e.append(t),e}function h(e,t,{errorMessage:n=``}){e.replaceChildren();let r=document.createElement(`h1`);r.textContent=`Authentication required`;let i=document.createElement(`p`);i.textContent=`Sign in with Google to join this session.`;let a=document.createElement(`div`);a.className=`auth-gate-actions`;let o=document.createElement(`button`);o.className=`ui-button ui-button--secondary auth-gate-button auth-gate-button--google`,o.type=`button`,o.append(m(),document.createTextNode(t.submitLabel||`Continue with Google`));let s=document.createElement(`div`);s.className=`auth-gate-error`,s.textContent=n,a.append(o),e.append(r,i,s,a),o.addEventListener(`click`,()=>{let e=new URL(t.loginEndpoint,window.location.origin);e.searchParams.set(`returnTo`,l()),window.location.assign(e.toString())})}function g(e,{title:t,body:n,secondaryActionLabel:r=``,onSecondaryAction:i=null}){e.replaceChildren();let a=document.createElement(`h1`);a.textContent=t;let o=document.createElement(`p`);if(o.textContent=n,e.append(a,o),r&&typeof i==`function`){let t=document.createElement(`button`);t.className=`ui-button ui-button--secondary auth-gate-secondary-button`,t.type=`button`,t.textContent=r,t.addEventListener(`click`,()=>{i()}),e.append(t)}}function _(e,t,{onSubmit:n}){e.replaceChildren();let r=document.createElement(`h1`);r.textContent=`Authentication required`;let i=document.createElement(`p`);i.textContent=`Enter the host password to join this shared session.`;let a=document.createElement(`form`);a.className=`auth-gate-form`;let o=document.createElement(`label`);o.className=`auth-gate-label`,o.textContent=t.passwordLabel;let s=document.createElement(`input`);s.className=`ui-input auth-gate-input`,s.type=`password`,s.name=`password`,s.autocomplete=`current-password`,s.required=!0;let c=document.createElement(`div`);c.className=`auth-gate-error`;let l=document.createElement(`div`);l.className=`auth-gate-actions`;let u=document.createElement(`button`);u.className=`ui-button ui-button--primary auth-gate-button`,u.type=`submit`,u.textContent=t.submitLabel,l.append(u),o.append(s),a.append(r,i,o,c,l),e.append(a),a.addEventListener(`submit`,e=>{e.preventDefault();let t=s.value;s.disabled=!0,u.disabled=!0,c.textContent=``,n(t).catch(e=>{c.textContent=e instanceof Error?e.message:`Authentication failed`,s.disabled=!1,u.disabled=!1,s.select(),s.focus()})}),queueMicrotask(()=>{s.focus()})}async function v(){let e=i();if(!e.enabled||e.strategy===`none`)return{authenticated:!0,auth:e};let{card:t,overlay:n}=p();document.body.append(n);let r=c(`auth_error`);return new Promise(i=>{let a=(t=null)=>{s(),d(t),n.remove(),i({authenticated:!0,auth:t?.auth??e,user:t?.user??null})};(async()=>{g(t,{body:`Checking access to this session…`,title:`CollabMD`});try{let t=await u(e);if(t.authenticated){a(t);return}}catch(e){g(t,{body:e instanceof Error?`${e.message} Refresh the page and try again.`:`Failed to contact the auth service. Refresh the page and try again.`,title:`Cannot verify access`});return}if(e.strategy===`password`){let n=o();if(n)try{s(),await f(e,n),a();return}catch{}_(t,e,{onSubmit:async t=>{await f(e,t),a()}});return}if(e.strategy===`oidc`){h(t,e,{errorMessage:r}),r=``;return}g(t,{body:`This authentication strategy is not available.`,title:`Authentication unavailable`})})()})}function y(e){return encodeURIComponent(String(e??``))}function b(e){return String(e??``).replace(/\/+$/u,``).split(`/`).filter(Boolean).pop()||``}function x(e,t){let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,document.body.appendChild(r),r.click(),r.remove(),window.setTimeout(()=>URL.revokeObjectURL(n),0)}function S(e=``,t=`download`){let n=String(e).match(/filename\*=UTF-8''([^;]+)/iu);if(n)try{return decodeURIComponent(n[1])}catch{return t}return String(e).match(/filename="([^"]+)"/iu)?.[1]||t}function C(e,t={}){let n={...t};return e&&(n[`X-CollabMD-Request-Id`]=String(e)),n}async function w(e,t){let n=await e.json().catch(()=>({}));if(!e.ok||n.ok===!1){let r=Error(n.error||t);throw r.status=e.status,r.body=n,r}return n}async function T(e,{fallbackError:t,fallbackFileName:n}={}){let r=await fetch(e);if(!r.ok){let e=await r.json().catch(()=>({}));throw Error(e.error||t)}return x(await r.blob(),S(r.headers.get(`content-disposition`)||``,n)),r}var E=new class{async readTree(){return w(await fetch(e(`/files`)),`Failed to load file tree`)}async readFile(t){return w(await fetch(e(`/file?path=${encodeURIComponent(t)}`)),`Failed to read file`)}async queryBase({activeFilePath:t=``,path:n=``,search:r=``,source:i=null,sourcePath:a=``,view:o=``}={}){return w(await fetch(e(`/base/query`),{body:JSON.stringify({activeFilePath:t,path:n,search:r,source:i,sourcePath:a,view:o}),headers:{"Content-Type":`application/json`},method:`POST`}),`Failed to query base`)}async exportBaseCsv({activeFilePath:t=``,path:n=``,search:r=``,source:i=null,sourcePath:a=``,view:o=``}={}){let s=await fetch(e(`/base/export`),{body:JSON.stringify({activeFilePath:t,path:n,search:r,source:i,sourcePath:a,view:o}),headers:{"Content-Type":`application/json`},method:`POST`});if(!s.ok){let e=await s.json().catch(()=>({}));throw Error(e.error||`Failed to export base CSV`)}return{blob:await s.blob(),contentDisposition:s.headers.get(`content-disposition`)||``}}async createFile({content:t,path:n,requestId:r=null}){return w(await fetch(e(`/file`),{body:JSON.stringify({content:t,path:n}),headers:C(r,{"Content-Type":`application/json`}),method:`POST`}),`Failed to create file`)}async writeFile({content:t,path:n,requestId:r=null}){return w(await fetch(e(`/file`),{body:JSON.stringify({content:t,path:n}),headers:C(r,{"Content-Type":`application/json`}),method:`PUT`}),`Failed to write file`)}async renameFile({oldPath:t,newPath:n,requestId:r=null}){return w(await fetch(e(`/file`),{body:JSON.stringify({newPath:n,oldPath:t}),headers:C(r,{"Content-Type":`application/json`}),method:`PATCH`}),`Failed to rename file`)}async deleteFile(t,{requestId:n=null}={}){return w(await fetch(e(`/file?path=${encodeURIComponent(t)}`),{headers:n?C(n):void 0,method:`DELETE`}),`Failed to delete file`)}async createDirectory(t,{requestId:n=null}={}){return w(await fetch(e(`/directory`),{body:JSON.stringify({path:t}),headers:C(n,{"Content-Type":`application/json`}),method:`POST`}),`Failed to create folder`)}async renameDirectory({oldPath:t,newPath:n,requestId:r=null}){return w(await fetch(e(`/directory`),{body:JSON.stringify({newPath:n,oldPath:t}),headers:C(r,{"Content-Type":`application/json`}),method:`PATCH`}),`Failed to rename folder`)}async deleteDirectory(t,{recursive:n=!1,requestId:r=null}={}){let i=new URLSearchParams({path:String(t??``)});return n&&i.set(`recursive`,`1`),w(await fetch(e(`/directory?${i.toString()}`),{headers:r?C(r):void 0,method:`DELETE`}),`Failed to delete folder`)}async uploadImageAttachment({file:t,fileName:n=``,sourcePath:r}){return w(await fetch(e(`/attachments`),{body:t,headers:{"Content-Type":t?.type||`application/octet-stream`,"X-CollabMD-File-Name":y(n),"X-CollabMD-Source-Path":y(r)},method:`POST`}),`Failed to upload image`)}async downloadFile(t){let n=b(t)||`download`;return T(e(`/download/file?path=${encodeURIComponent(t)}`),{fallbackError:`Failed to download file`,fallbackFileName:n})}async downloadDirectory(t){let n=b(t)||`vault`;return T(e(`/download/directory?path=${encodeURIComponent(t)}`),{fallbackError:`Failed to download folder`,fallbackFileName:`${n}.zip`})}};export{v as n,E as t};
@@ -0,0 +1 @@
1
+ .auth-gate-overlay{z-index:5000;padding:var(--space-6);background:radial-gradient(circle at 50% 8%, oklch(from var(--color-primary) l c h / .18), transparent 28%), radial-gradient(circle at 60% 45%, oklch(from var(--color-primary) l c h / .08), transparent 18%), linear-gradient(180deg, oklch(from var(--color-bg) calc(l - .06) c h / .9), oklch(from var(--color-bg) calc(l - .08) c h / .96));-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.auth-gate-card{width:min(100%,440px);padding:var(--space-8);border:1px solid var(--border-accent-soft);border-radius:var(--radius-xl-plus);background:linear-gradient(180deg, var(--color-surface), var(--color-surface-2));color:var(--color-text);box-shadow:var(--shadow-card-strong), var(--shadow-sheet-inset);font-family:var(--font-body)}.auth-gate-card h1{font-family:var(--font-display);font-size:var(--text-xl);letter-spacing:-.04em;margin:0 0 10px;line-height:1.05}.auth-gate-card p{color:var(--color-text-muted);line-height:1.5;font-size:var(--text-sm);margin:0 0 22px}.auth-gate-form{gap:var(--space-3);display:grid}.auth-gate-label{gap:var(--space-2);color:var(--color-text-muted);font-size:var(--text-sm);font-weight:500;display:grid}.auth-gate-input{min-height:var(--control-h-lg);border-radius:var(--radius-xl);background:var(--surface-overlay);color:inherit}.auth-gate-input:focus{background:var(--surface-overlay-soft)}.auth-gate-actions{gap:var(--space-3);display:grid}.auth-gate-button,.auth-gate-secondary-button{width:100%;min-height:var(--control-h-lg);border-radius:var(--radius-xl);justify-content:center;transition:transform .16s,box-shadow .16s,border-color .16s,background-color .16s,color .16s}.auth-gate-button:hover,.auth-gate-secondary-button:hover{transform:translateY(-1px)}.auth-gate-button:focus-visible,.auth-gate-secondary-button:focus-visible{outline:3px solid oklch(from var(--color-primary) l c h / .3);outline-offset:2px}.auth-gate-button{box-shadow:var(--shadow-floating-soft);font-weight:600}.auth-gate-button:hover{box-shadow:var(--shadow-floating-hover)}.auth-gate-secondary-button{background:var(--surface-overlay)}.auth-gate-secondary-button:hover{background:var(--surface-overlay-soft)}.auth-gate-button[disabled],.auth-gate-secondary-button[disabled]{opacity:.7;cursor:wait;transform:none}.auth-gate-button--google{background:var(--color-surface-raised);color:var(--color-text);border-color:var(--color-border);box-shadow:var(--shadow-elevated-soft), var(--shadow-sheet-inset)}.auth-gate-button--google:hover{background:var(--surface-overlay-soft)}.auth-gate-button__icon{flex:none;justify-content:center;align-items:center;width:22px;height:22px;display:inline-flex}.auth-gate-button__icon img{object-fit:contain;width:100%;height:100%;display:block}.auth-gate-error{min-height:1.25rem;color:var(--color-error);font-size:var(--text-sm);margin-bottom:var(--space-1)}@media (width<=640px){.auth-gate-overlay{padding:var(--space-4)}.auth-gate-card{padding:var(--space-6);border-radius:var(--radius-xl)}}
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>draw.io — CollabMD</title>
7
- <script type="module" crossorigin src="./assets/drawioEditor-DGpo2hcx.js"></script>
7
+ <script type="module" crossorigin src="./assets/drawioEditor-ClmnTx5J.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/runtime-config-loader-_D2KZKn-.js">
9
9
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-Bo9GmnuQ.js">
10
10
  </head>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Excalidraw — CollabMD</title>
7
- <script type="module" crossorigin src="./assets/excalidrawEditor-BhaTX2Ko.js"></script>
7
+ <script type="module" crossorigin src="./assets/excalidrawEditor-DoH_kMdu.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/runtime-config-loader-_D2KZKn-.js">
9
9
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-Bo9GmnuQ.js">
10
10
  </head>
@@ -4,12 +4,12 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>CollabMD Export</title>
7
- <script type="module" crossorigin src="./assets/exportDocument-CtZKWE2c.js"></script>
7
+ <script type="module" crossorigin src="./assets/exportDocument-Bia2hI25.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/runtime-config-loader-_D2KZKn-.js">
9
9
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-Bo9GmnuQ.js">
10
10
  <link rel="modulepreload" crossorigin href="./assets/base-h1uscjTi.js">
11
11
  <link rel="stylesheet" crossorigin href="./assets/base-CaIEJUF9.css">
12
- <link rel="stylesheet" crossorigin href="./assets/exportDocument-DmV7Gngb.css">
12
+ <link rel="stylesheet" crossorigin href="./assets/exportDocument-9wTrBZNS.css">
13
13
  </head>
14
14
  <body data-theme="light" data-surface="export-document">
15
15
  <main class="export-page-shell">
@@ -15,12 +15,12 @@
15
15
  document.documentElement.setAttribute('data-initial-file-requested', 'true');
16
16
  }
17
17
  </script>
18
- <script type="module" crossorigin src="./assets/index-kPgEYvEX.js"></script>
18
+ <script type="module" crossorigin src="./assets/index-C8QZYHvV.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">
22
22
  <link rel="stylesheet" crossorigin href="./assets/base-CaIEJUF9.css">
23
- <link rel="stylesheet" crossorigin href="./assets/index-D69HOZHr.css">
23
+ <link rel="stylesheet" crossorigin href="./assets/index-CxSbUTs2.css">
24
24
  </head>
25
25
  <body>
26
26
  <div id="appRoot"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "collabmd",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
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": {
@@ -71,7 +71,10 @@
71
71
  "@codemirror/theme-one-dark": "^6.1.3",
72
72
  "@codemirror/view": "^6.40.0",
73
73
  "@excalidraw/excalidraw": "0.18.0-4a5c9e9",
74
+ "@fontsource-variable/inter": "^5.2.8",
75
+ "@fontsource/jetbrains-mono": "^5.2.8",
74
76
  "@turbodocx/html-to-docx": "^1.20.1",
77
+ "archiver": "^7.0.1",
75
78
  "highlight.js": "^11.11.1",
76
79
  "js-yaml": "^4.1.1",
77
80
  "lib0": "^0.2.117",
@@ -1,5 +1,7 @@
1
1
  import darkHighlightThemeUrl from '../assets/vendor/highlight/github-dark.min.css?url';
2
2
  import lightHighlightThemeUrl from '../assets/vendor/highlight/github.min.css?url';
3
+ import '@fontsource-variable/inter';
4
+ import '@fontsource/jetbrains-mono';
3
5
  import '../styles/base.css';
4
6
  import '../styles/style.css';
5
7
 
@@ -1,5 +1,4 @@
1
1
  import { createWorkspaceChange } from '../../../domain/workspace-change.js';
2
- import { resolveApiUrl } from '../../domain/runtime-paths.js';
3
2
  import { createWorkspaceRequestId } from '../../domain/workspace-request-id.js';
4
3
 
5
4
  function normalizeWorkspaceChange(workspaceChange = {}) {
@@ -257,15 +256,10 @@ export const gitFeature = {
257
256
  this.syncFileHistoryButton({ filePath: resolvedCurrentFilePath, mode: 'history-preview' });
258
257
 
259
258
  try {
260
- const query = new URLSearchParams();
261
- query.set('hash', hash);
262
- query.set('path', filePath);
263
- const response = await fetch(resolveApiUrl(`/git/file-snapshot?${query.toString()}`));
264
- const data = await response.json();
265
- if (!response.ok) {
266
- throw new Error(data.error || 'Failed to load historical file preview');
267
- }
268
-
259
+ const data = await this.gitApiClient.readFileSnapshot({
260
+ hash,
261
+ path: filePath,
262
+ });
269
263
  this.setStaticPreviewDocument?.({
270
264
  ...data,
271
265
  currentFilePath: resolvedCurrentFilePath,
@@ -286,7 +280,7 @@ export const gitFeature = {
286
280
  }
287
281
 
288
282
  this.elements.outlineToggle?.classList.add('hidden');
289
- this.renderTextFilePreview({
283
+ this.renderTextFilePreview?.({
290
284
  content: data.content,
291
285
  filePath: resolvedCurrentFilePath,
292
286
  });
@@ -294,7 +288,7 @@ export const gitFeature = {
294
288
  console.error('[git-preview] Failed to load file snapshot:', error);
295
289
  this.clearStaticPreviewDocument?.();
296
290
  this.toastController.show('Failed to load historical file preview');
297
- this.renderTextFilePreview({
291
+ this.renderTextFilePreview?.({
298
292
  content: 'Failed to load historical file preview.',
299
293
  filePath: resolvedCurrentFilePath,
300
294
  });
@@ -302,28 +296,26 @@ export const gitFeature = {
302
296
  }
303
297
  },
304
298
 
305
- async postGitAction(endpoint, payload) {
299
+ async postGitAction(actionName, payload) {
306
300
  const requestId = createWorkspaceRequestId();
307
301
  this.pendingWorkspaceRequestIds?.add(requestId);
308
302
  try {
309
- const response = await fetch(endpoint, {
310
- body: JSON.stringify(payload),
311
- headers: {
312
- 'Content-Type': 'application/json',
313
- 'X-CollabMD-Request-Id': requestId,
314
- },
315
- method: 'POST',
316
- });
317
- const data = await response.json();
318
- if (!response.ok) {
319
- this.pendingWorkspaceRequestIds?.delete(requestId);
320
- const error = new Error(data.error || 'Git action failed');
321
- if (typeof data?.code === 'string') {
322
- error.code = data.code;
323
- }
324
- throw error;
303
+ switch (actionName) {
304
+ case 'stage':
305
+ return await this.gitApiClient.stageFile({ path: payload.path, requestId });
306
+ case 'unstage':
307
+ return await this.gitApiClient.unstageFile({ path: payload.path, requestId });
308
+ case 'push':
309
+ return await this.gitApiClient.pushBranch({ requestId });
310
+ case 'pull':
311
+ return await this.gitApiClient.pullBranch({ requestId });
312
+ case 'reset-file':
313
+ return await this.gitApiClient.resetFile({ path: payload.path, requestId });
314
+ case 'commit':
315
+ return await this.gitApiClient.commit({ message: payload.message, requestId });
316
+ default:
317
+ throw new Error(`Unsupported git action: ${actionName}`);
325
318
  }
326
- return data;
327
319
  } catch (error) {
328
320
  this.pendingWorkspaceRequestIds?.delete(requestId);
329
321
  throw error;
@@ -483,7 +475,7 @@ export const gitFeature = {
483
475
  }
484
476
 
485
477
  await this.runGitActionWithStatus('Staging changes...', async () => {
486
- const result = await this.postGitAction(resolveApiUrl('/git/stage'), { path: filePath });
478
+ const result = await this.postGitAction('stage', { path: filePath });
487
479
  await this.finalizeGitAction({
488
480
  action: 'stage',
489
481
  filePath,
@@ -499,7 +491,7 @@ export const gitFeature = {
499
491
  }
500
492
 
501
493
  await this.runGitActionWithStatus('Unstaging changes...', async () => {
502
- const result = await this.postGitAction(resolveApiUrl('/git/unstage'), { path: filePath });
494
+ const result = await this.postGitAction('unstage', { path: filePath });
503
495
  await this.finalizeGitAction({
504
496
  action: 'unstage',
505
497
  filePath,
@@ -512,7 +504,7 @@ export const gitFeature = {
512
504
  async pushGitBranch() {
513
505
  await this.runGitActionWithStatus('Pushing branch...', async () => {
514
506
  try {
515
- const result = await this.postGitAction(resolveApiUrl('/git/push'), {});
507
+ const result = await this.postGitAction('push', {});
516
508
  await this.finalizeGitAction({
517
509
  action: 'push',
518
510
  result,
@@ -526,7 +518,7 @@ export const gitFeature = {
526
518
  async pullGitBranch() {
527
519
  await this.runGitActionWithStatus('Pulling branch...', async () => {
528
520
  try {
529
- const result = await this.postGitAction(resolveApiUrl('/git/pull'), {});
521
+ const result = await this.postGitAction('pull', {});
530
522
  await this.finalizeGitAction({
531
523
  action: 'pull',
532
524
  result,
@@ -598,7 +590,7 @@ export const gitFeature = {
598
590
  }
599
591
  try {
600
592
  await this.runGitActionWithStatus('Resetting file...', async () => {
601
- const result = await this.postGitAction(resolveApiUrl('/git/reset-file'), { path: filePath });
593
+ const result = await this.postGitAction('reset-file', { path: filePath });
602
594
  await this.finalizeGitAction({
603
595
  action: 'reset',
604
596
  filePath,
@@ -676,7 +668,7 @@ export const gitFeature = {
676
668
  }
677
669
  try {
678
670
  await this.runGitActionWithStatus('Committing changes...', async () => {
679
- const result = await this.postGitAction(resolveApiUrl('/git/commit'), {
671
+ const result = await this.postGitAction('commit', {
680
672
  message,
681
673
  });
682
674
  await this.finalizeGitAction({
@@ -17,9 +17,11 @@ export const presenceFeature = {
17
17
  if (!badge) return;
18
18
 
19
19
  const hasEditorSession = Boolean(this.session);
20
+ const isDrawioRoute = Boolean(this.currentFilePath && this.isDrawioFile?.(this.currentFilePath));
20
21
  const isExcalidrawRoute = Boolean(this.currentFilePath && this.isExcalidrawFile?.(this.currentFilePath));
21
22
  const isImageRoute = Boolean(this.currentFilePath && this.isImageFile?.(this.currentFilePath));
22
- const shouldUseLobbyState = !hasEditorSession && (!this.currentFilePath || isExcalidrawRoute || isImageRoute);
23
+ const shouldUseLobbyState = !hasEditorSession
24
+ && (!this.currentFilePath || isDrawioRoute || isExcalidrawRoute || isImageRoute);
23
25
  const effectiveConnectionState = shouldUseLobbyState
24
26
  ? this.lobby?.getConnectionState?.() ?? this.connectionState
25
27
  : this.connectionState;
@@ -1,4 +1,3 @@
1
- import { resolveApiUrl } from '../domain/runtime-paths.js';
2
1
  import {
3
2
  cancelIdleRender,
4
3
  IDLE_RENDER_TIMEOUT_MS,
@@ -7,6 +6,7 @@ import {
7
6
  shouldPreserveHydratedDiagram,
8
7
  syncAttribute,
9
8
  } from './preview-diagram-utils.js';
9
+ import { resolveApiUrl } from '../domain/runtime-paths.js';
10
10
 
11
11
  function datasetKeyToAttributeName(datasetKey) {
12
12
  return `data-${datasetKey.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)}`;
@@ -16,7 +16,8 @@ export class DiagramPreviewHydrator {
16
16
  constructor(renderer, {
17
17
  batchSize,
18
18
  datasetKeys,
19
- fetchFn = (...args) => fetch(...args),
19
+ fetchFn = null,
20
+ loadFileSource = null,
20
21
  filePathLabel,
21
22
  requestIdleRenderFn = requestIdleRender,
22
23
  cancelIdleRenderFn = cancelIdleRender,
@@ -29,7 +30,7 @@ export class DiagramPreviewHydrator {
29
30
  this.renderer = renderer;
30
31
  this.batchSize = batchSize;
31
32
  this.datasetKeys = datasetKeys;
32
- this.fetchFn = fetchFn;
33
+ this.loadFileSource = loadFileSource ?? this.createLegacyFileSourceLoader(fetchFn);
33
34
  this.filePathLabel = filePathLabel;
34
35
  this.requestIdleRenderFn = requestIdleRenderFn;
35
36
  this.cancelIdleRenderFn = cancelIdleRenderFn;
@@ -53,6 +54,22 @@ export class DiagramPreviewHydrator {
53
54
  this.fileInflightRequests = new Map();
54
55
  }
55
56
 
57
+ createLegacyFileSourceLoader(fetchFn) {
58
+ if (typeof fetchFn !== 'function') {
59
+ return null;
60
+ }
61
+
62
+ return async (filePath) => {
63
+ const response = await fetchFn(resolveApiUrl(`/file?path=${encodeURIComponent(filePath)}`));
64
+ const payload = await response.json().catch(() => ({}));
65
+ if (!response.ok || payload?.ok === false) {
66
+ throw new Error(payload?.error || `Failed to load ${this.filePathLabel.toLowerCase()} source`);
67
+ }
68
+
69
+ return String(payload?.content ?? '');
70
+ };
71
+ }
72
+
56
73
  destroy() {
57
74
  this.cancelHydration();
58
75
  this.preservedShells.clear();
@@ -359,19 +376,11 @@ export class DiagramPreviewHydrator {
359
376
  return this.fileInflightRequests.get(target);
360
377
  }
361
378
 
362
- const request = this.fetchFn(resolveApiUrl(`/file?path=${encodeURIComponent(target)}`), {
363
- headers: {
364
- Accept: 'application/json',
365
- },
366
- })
367
- .then(async (response) => {
368
- const data = await response.json().catch(() => null);
369
- if (!response.ok || typeof data?.content !== 'string') {
370
- throw new Error(data?.error || `Failed to load ${target}`);
371
- }
379
+ if (typeof this.loadFileSource !== 'function') {
380
+ throw new Error(`Missing ${this.filePathLabel} source loader`);
381
+ }
372
382
 
373
- return data.content;
374
- })
383
+ const request = this.loadFileSource(target)
375
384
  .finally(() => {
376
385
  this.fileInflightRequests.delete(target);
377
386
  });
@@ -12,7 +12,7 @@ import {
12
12
  } from './preview-diagram-utils.js';
13
13
 
14
14
  export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
15
- constructor(renderer) {
15
+ constructor(renderer, { fetchFn = null, loadFileSource = null } = {}) {
16
16
  super(renderer, {
17
17
  batchSize: MERMAID_BATCH_SIZE,
18
18
  datasetKeys: {
@@ -26,7 +26,9 @@ export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
26
26
  sourceLineEnd: 'sourceLineEnd',
27
27
  target: 'mermaidTarget',
28
28
  },
29
+ fetchFn,
29
30
  filePathLabel: 'Mermaid',
31
+ loadFileSource,
30
32
  shellClassName: 'mermaid-shell',
31
33
  sourceClassName: 'mermaid-source',
32
34
  });
@@ -35,6 +37,101 @@ export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
35
37
  this.loader = null;
36
38
  this.runtime = null;
37
39
  this.shellRefits = new WeakMap();
40
+ this.activeMaximizedShell = null;
41
+ this.maximizedRoot = null;
42
+ }
43
+
44
+ destroy() {
45
+ this.cancelHydration();
46
+ this.preservedShells.clear();
47
+ this.clearActiveShell();
48
+ this.maximizedRoot?.remove();
49
+ this.maximizedRoot = null;
50
+ }
51
+
52
+ cancelHydration() {
53
+ super.cancelHydration();
54
+
55
+ const activeShell = this.syncActiveShell();
56
+ if (!activeShell) {
57
+ return;
58
+ }
59
+
60
+ this.restoreShellMount(activeShell);
61
+ activeShell.classList.remove('is-maximized');
62
+ this.clearActiveShell();
63
+ document.body.classList.remove('mermaid-maximized-open');
64
+ }
65
+
66
+ clearActiveShell() {
67
+ this.activeMaximizedShell = null;
68
+ if (this.maximizedRoot && this.maximizedRoot.childElementCount === 0) {
69
+ this.maximizedRoot.hidden = true;
70
+ }
71
+ }
72
+
73
+ syncActiveShell() {
74
+ if (
75
+ this.activeMaximizedShell?.isConnected
76
+ && this.activeMaximizedShell.classList.contains('is-maximized')
77
+ ) {
78
+ return this.activeMaximizedShell;
79
+ }
80
+
81
+ this.clearActiveShell();
82
+ return null;
83
+ }
84
+
85
+ ensureMaximizedRoot() {
86
+ if (this.maximizedRoot?.isConnected && this.maximizedRoot.parentElement === document.body) {
87
+ return this.maximizedRoot;
88
+ }
89
+
90
+ let maximizedRoot = document.body.querySelector('[data-mermaid-maximized-root="true"]');
91
+ if (!maximizedRoot) {
92
+ maximizedRoot = document.createElement('div');
93
+ maximizedRoot.dataset.mermaidMaximizedRoot = 'true';
94
+ maximizedRoot.className = 'mermaid-maximized-root';
95
+ document.body.appendChild(maximizedRoot);
96
+ }
97
+
98
+ this.maximizedRoot = maximizedRoot;
99
+ return maximizedRoot;
100
+ }
101
+
102
+ mountShellInMaximizedRoot(shell) {
103
+ if (!shell) {
104
+ return;
105
+ }
106
+
107
+ const maximizedRoot = this.ensureMaximizedRoot();
108
+ maximizedRoot.hidden = false;
109
+ shell._mermaidRestoreParent = shell.parentElement || null;
110
+ shell._mermaidRestoreNextSibling = shell.nextSibling || null;
111
+ maximizedRoot.appendChild(shell);
112
+ }
113
+
114
+ restoreShellMount(shell) {
115
+ if (!shell) {
116
+ return;
117
+ }
118
+
119
+ const restoreParent = shell._mermaidRestoreParent;
120
+ const restoreNextSibling = shell._mermaidRestoreNextSibling;
121
+ if (restoreParent?.isConnected) {
122
+ if (restoreNextSibling?.parentElement === restoreParent) {
123
+ restoreParent.insertBefore(shell, restoreNextSibling);
124
+ } else {
125
+ restoreParent.appendChild(shell);
126
+ }
127
+ }
128
+
129
+ shell._mermaidRestoreParent = null;
130
+ shell._mermaidRestoreNextSibling = null;
131
+
132
+ if (this.maximizedRoot && this.maximizedRoot.childElementCount === 0) {
133
+ this.maximizedRoot.hidden = true;
134
+ }
38
135
  }
39
136
 
40
137
  applyTheme(theme) {
@@ -111,6 +208,7 @@ export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
111
208
  if (restoredMaximizedShell) {
112
209
  document.body.classList.add('mermaid-maximized-open');
113
210
  }
211
+ this.syncActiveShell();
114
212
  }
115
213
 
116
214
  async prepareHydrationBatch() {
@@ -240,6 +338,10 @@ export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
240
338
  }
241
339
 
242
340
  const hydratedShells = Array.from(previewElement.querySelectorAll('.mermaid-shell[data-mermaid-hydrated="true"]'));
341
+ const activeShell = this.syncActiveShell();
342
+ if (activeShell && !hydratedShells.includes(activeShell)) {
343
+ hydratedShells.push(activeShell);
344
+ }
243
345
  if (hydratedShells.length === 0) {
244
346
  return;
245
347
  }
@@ -447,11 +549,14 @@ export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
447
549
  };
448
550
 
449
551
  const setMaximizedState = (shouldMaximize) => {
450
- const previewElement = this.renderer.previewElement;
451
552
  if (shouldMaximize) {
452
- const activeContainer = previewElement.querySelector('.mermaid-shell.is-maximized');
553
+ const activeContainer = this.syncActiveShell();
453
554
  if (activeContainer && activeContainer !== shell) {
555
+ this.restoreShellMount(activeContainer);
454
556
  activeContainer.classList.remove('is-maximized');
557
+ if (this.activeMaximizedShell === activeContainer) {
558
+ this.clearActiveShell();
559
+ }
455
560
  this.shellRefits.get(activeContainer)?.();
456
561
  const activeButton = activeContainer.querySelector('.mermaid-maximize-btn');
457
562
  if (activeButton) {
@@ -460,15 +565,21 @@ export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
460
565
  activeButton.title = 'Maximize diagram';
461
566
  }
462
567
  }
568
+ this.mountShellInMaximizedRoot(shell);
463
569
  shell.classList.add('is-maximized');
570
+ this.activeMaximizedShell = shell;
464
571
  document.body.classList.add('mermaid-maximized-open');
465
572
  syncMaximizeButtonState();
466
573
  scheduleResetZoomToFit({ force: true });
467
574
  return;
468
575
  }
469
576
 
577
+ this.restoreShellMount(shell);
470
578
  shell.classList.remove('is-maximized');
471
- if (!previewElement.querySelector('.mermaid-shell.is-maximized')) {
579
+ if (this.activeMaximizedShell === shell) {
580
+ this.clearActiveShell();
581
+ }
582
+ if (!this.syncActiveShell()) {
472
583
  document.body.classList.remove('mermaid-maximized-open');
473
584
  }
474
585
  syncMaximizeButtonState();
@@ -548,6 +659,12 @@ export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
548
659
  }
549
660
 
550
661
  scheduleActiveRefit() {
662
+ const activeShell = this.syncActiveShell();
663
+ if (activeShell) {
664
+ this.shellRefits.get(activeShell)?.();
665
+ return;
666
+ }
667
+
551
668
  const previewElement = this.renderer.previewElement;
552
669
  if (!previewElement) {
553
670
  return;
@@ -1,6 +1,5 @@
1
1
  import { clamp } from '../domain/vault-utils.js';
2
2
  import { setDiagramActionButtonIcon } from '../domain/diagram-action-icons.js';
3
- import { resolveApiUrl } from '../domain/runtime-paths.js';
4
3
  import { DiagramPreviewHydrator } from './diagram-preview-hydrator.js';
5
4
  import {
6
5
  createPlantUmlPlaceholderCard,
@@ -13,7 +12,7 @@ import {
13
12
  } from './preview-diagram-utils.js';
14
13
 
15
14
  export class PlantUmlPreviewHydrator extends DiagramPreviewHydrator {
16
- constructor(renderer) {
15
+ constructor(renderer, { loadFileSource = null, renderClient = null } = {}) {
17
16
  super(renderer, {
18
17
  batchSize: PLANTUML_BATCH_SIZE,
19
18
  datasetKeys: {
@@ -28,10 +27,12 @@ export class PlantUmlPreviewHydrator extends DiagramPreviewHydrator {
28
27
  target: 'plantumlTarget',
29
28
  },
30
29
  filePathLabel: 'PlantUML',
30
+ loadFileSource,
31
31
  shellClassName: 'plantuml-shell',
32
32
  sourceClassName: 'plantuml-source',
33
33
  });
34
34
  this.renderer = renderer;
35
+ this.renderClient = renderClient;
35
36
  this.svgCache = new Map();
36
37
  this.svgInflightRequests = new Map();
37
38
  this.shellRefits = new WeakMap();
@@ -206,20 +207,9 @@ export class PlantUmlPreviewHydrator extends DiagramPreviewHydrator {
206
207
  return this.svgInflightRequests.get(cacheKey);
207
208
  }
208
209
 
209
- const request = fetch(resolveApiUrl('/plantuml/render'), {
210
- method: 'POST',
211
- headers: {
212
- 'Content-Type': 'application/json',
213
- },
214
- body: JSON.stringify({ source }),
215
- })
216
- .then(async (response) => {
217
- const data = await response.json().catch(() => null);
218
- if (!response.ok || !data?.ok || typeof data.svg !== 'string') {
219
- throw new Error(data?.error || 'Failed to render PlantUML');
220
- }
221
-
222
- const sanitized = sanitizeSvgMarkup(data.svg);
210
+ const request = this.renderClient.renderSvg(source)
211
+ .then((svgMarkup) => {
212
+ const sanitized = sanitizeSvgMarkup(svgMarkup);
223
213
  this.svgCache.set(cacheKey, sanitized);
224
214
  return sanitized;
225
215
  })
@@ -10,23 +10,27 @@ export class PreviewRenderer {
10
10
  constructor({
11
11
  getContent,
12
12
  getFileList,
13
+ loadFileSource = null,
13
14
  getSourceFilePath,
14
15
  onAfterRenderCommit,
15
16
  onBeforeRenderCommit,
16
17
  onPreviewLayoutChange,
17
18
  onRenderComplete,
18
19
  outlineController,
20
+ plantUmlRenderClient = null,
19
21
  previewContainer,
20
22
  previewElement,
21
23
  }) {
22
24
  this.getContent = getContent;
23
25
  this.getFileList = getFileList;
26
+ this.loadFileSource = loadFileSource;
24
27
  this.getSourceFilePath = getSourceFilePath;
25
28
  this.onAfterRenderCommit = onAfterRenderCommit;
26
29
  this.onBeforeRenderCommit = onBeforeRenderCommit;
27
30
  this.onPreviewLayoutChange = onPreviewLayoutChange;
28
31
  this.onRenderComplete = onRenderComplete;
29
32
  this.outlineController = outlineController;
33
+ this.plantUmlRenderClient = plantUmlRenderClient;
30
34
  this.previewContainer = previewContainer;
31
35
  this.previewElement = previewElement;
32
36
  this.renderHost = null;
@@ -78,8 +82,13 @@ export class PreviewRenderer {
78
82
  this.plantUmlHydrator.scheduleActiveRefit();
79
83
  };
80
84
 
81
- this.mermaidHydrator = new MermaidPreviewHydrator(this);
82
- this.plantUmlHydrator = new PlantUmlPreviewHydrator(this);
85
+ this.mermaidHydrator = new MermaidPreviewHydrator(this, {
86
+ loadFileSource,
87
+ });
88
+ this.plantUmlHydrator = new PlantUmlPreviewHydrator(this, {
89
+ loadFileSource,
90
+ renderClient: plantUmlRenderClient,
91
+ });
83
92
  this.renderScheduler = new PreviewRenderScheduler({ getRenderProfileFn: getRenderProfile });
84
93
  this.renderExecutor = new PreviewRenderExecutor({
85
94
  attachmentApiPath: resolveApiUrl('/attachment'),