collabmd 0.1.32 → 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.
Files changed (43) hide show
  1. package/dist/client/assets/{drawio-editor-Br4qyh3r.js → drawio-editor-BMTwP5mD.js} +1 -1
  2. package/dist/client/assets/{drawioEditor-ClmnTx5J.js → drawioEditor-D6clB5OH.js} +2 -2
  3. package/dist/client/assets/{editor-session-Cf-iV5-3.js → editor-session-Dzy_85Wp.js} +1 -1
  4. package/dist/client/assets/{excalidraw-editor-CXAxVhlw.js → excalidraw-editor-CAP9F8CB.js} +3 -3
  5. package/dist/client/assets/{excalidrawEditor-DoH_kMdu.js → excalidrawEditor-DD4AvzYP.js} +2 -2
  6. package/dist/client/assets/{exportDocument-9wTrBZNS.css → exportDocument-BbkN85c4.css} +1 -1
  7. package/dist/client/assets/index-BIt5EZXz.css +1 -0
  8. package/dist/client/assets/{index-C8QZYHvV.js → index-BLrtj9YM.js} +2 -2
  9. package/dist/client/assets/main-CfSHaGes.js +1368 -0
  10. package/dist/client/assets/{vault-api-client-Bf9tB_GW.js → vault-api-client-p7h7cXKM.js} +1 -1
  11. package/dist/client/drawio-editor.html +1 -1
  12. package/dist/client/excalidraw-editor.html +1 -1
  13. package/dist/client/export-document.html +2 -2
  14. package/dist/client/index.html +2 -2
  15. package/package.json +6 -5
  16. package/src/client/application/app-shell/git-feature.js +44 -36
  17. package/src/client/application/app-shell/ui-feature-shell.js +84 -63
  18. package/src/client/bootstrap/collabmd-app-shell.js +6 -0
  19. package/src/client/infrastructure/editor-session.js +4 -0
  20. package/src/client/infrastructure/editor-view-adapter.js +23 -0
  21. package/src/client/infrastructure/vault-api-client.js +48 -0
  22. package/src/client/presentation/bases-preview-controller.js +1388 -28
  23. package/src/client/presentation/comments-panel.js +50 -5
  24. package/src/client/presentation/excalidraw-embed-controller.js +4 -1
  25. package/src/client/presentation/file-history-view-controller.js +9 -1
  26. package/src/client/presentation/git-panel-controller.js +101 -91
  27. package/src/client/styles/components/scrollbars.css +5 -0
  28. package/src/client/styles/features/preview-markdown.css +468 -23
  29. package/src/server/domain/backlink-index.js +202 -11
  30. package/src/server/domain/bases/base-definition.js +138 -11
  31. package/src/server/domain/bases/base-expression-runtime.js +54 -17
  32. package/src/server/domain/bases/base-index-snapshot-store.js +167 -20
  33. package/src/server/domain/bases/base-query-metadata.js +242 -0
  34. package/src/server/domain/bases/base-query-results.js +18 -8
  35. package/src/server/domain/bases/base-query-service.js +360 -49
  36. package/src/server/domain/bases/base-transform.js +116 -0
  37. package/src/server/infrastructure/http/create-request-handler.js +63 -28
  38. package/src/server/infrastructure/http/create-vault-api-command-handler.js +275 -256
  39. package/src/server/infrastructure/http/create-vault-api-query-handler.js +258 -184
  40. package/src/server/infrastructure/persistence/vault-file-store.js +68 -12
  41. package/dist/client/assets/index-CxSbUTs2.css +0 -1
  42. package/dist/client/assets/main-BjHHMlv0.js +0 -1249
  43. /package/dist/client/assets/{exportDocument-Bia2hI25.js → exportDocument-Zfwq1XVx.js} +0 -0
@@ -1 +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};
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 queryBasePropertyValues({activeFilePath:t=``,path:n=``,propertyId:r=``,query:i=``,source:a=null,sourcePath:o=``,view:s=``}={}){return w(await fetch(e(`/base/property-values`),{body:JSON.stringify({activeFilePath:t,path:n,propertyId:r,query:i,source:a,sourcePath:o,view:s}),headers:{"Content-Type":`application/json`},method:`POST`}),`Failed to load base property values`)}async transformBase({activeFilePath:t=``,mutation:n=null,path:r=``,source:i=null,sourcePath:a=``,view:o=``}={}){return w(await fetch(e(`/base/transform`),{body:JSON.stringify({activeFilePath:t,mutation:n,path:r,source:i,sourcePath:a,view:o}),headers:{"Content-Type":`application/json`},method:`POST`}),`Failed to transform base`)}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};
@@ -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-ClmnTx5J.js"></script>
7
+ <script type="module" crossorigin src="./assets/drawioEditor-D6clB5OH.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-DoH_kMdu.js"></script>
7
+ <script type="module" crossorigin src="./assets/excalidrawEditor-DD4AvzYP.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-Bia2hI25.js"></script>
7
+ <script type="module" crossorigin src="./assets/exportDocument-Zfwq1XVx.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-9wTrBZNS.css">
12
+ <link rel="stylesheet" crossorigin href="./assets/exportDocument-BbkN85c4.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-C8QZYHvV.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">
22
22
  <link rel="stylesheet" crossorigin href="./assets/base-CaIEJUF9.css">
23
- <link rel="stylesheet" crossorigin href="./assets/index-CxSbUTs2.css">
23
+ <link rel="stylesheet" crossorigin href="./assets/index-BIt5EZXz.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.32",
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",
@@ -98,11 +99,11 @@
98
99
  "devDependencies": {
99
100
  "@eslint/js": "^10.0.1",
100
101
  "@playwright/test": "^1.58.2",
101
- "@vitest/browser": "^4.1.1",
102
- "@vitest/browser-playwright": "^4.1.1",
102
+ "@vitest/browser": "^4.1.2",
103
+ "@vitest/browser-playwright": "^4.1.2",
103
104
  "eslint": "^10.1.0",
104
105
  "globals": "^17.4.0",
105
- "vite": "^8.0.2",
106
- "vitest": "^4.1.1"
106
+ "vite": "^8.0.3",
107
+ "vitest": "^4.1.2"
107
108
  }
108
109
  }
@@ -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,
@@ -306,8 +306,14 @@ export class CollabMdAppShell {
306
306
  });
307
307
  this.basesPreview = new BasesPreviewController({
308
308
  getActiveFilePath: () => this.currentFilePath,
309
+ getSession: () => this.session,
309
310
  onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
310
311
  previewElement: this.elements.previewContent,
312
+ replaceBaseSource: ({ path, source }) => {
313
+ if (path && path === this.currentFilePath) {
314
+ this.session?.replaceText?.(source);
315
+ }
316
+ },
311
317
  toastController: this.toastController,
312
318
  vaultApiClient: this.vaultApiClient,
313
319
  });
@@ -272,6 +272,10 @@ export class EditorSession {
272
272
  return this.viewAdapter.insertText(text);
273
273
  }
274
274
 
275
+ replaceText(text) {
276
+ return this.viewAdapter.replaceText(text);
277
+ }
278
+
275
279
  toggleTaskListItem(lineNumber) {
276
280
  if (!this.collaborationClient.initialSyncComplete) {
277
281
  return false;
@@ -934,6 +934,29 @@ export class EditorViewAdapter {
934
934
  return true;
935
935
  }
936
936
 
937
+ replaceText(text) {
938
+ if (!this.editorView) {
939
+ return false;
940
+ }
941
+
942
+ const nextText = String(text ?? '');
943
+ const { state } = this.editorView;
944
+ this.editorView.dispatch({
945
+ changes: {
946
+ from: 0,
947
+ insert: nextText,
948
+ to: state.doc.length,
949
+ },
950
+ selection: {
951
+ anchor: 0,
952
+ head: 0,
953
+ },
954
+ userEvent: 'input',
955
+ });
956
+ this.editorView.focus();
957
+ return true;
958
+ }
959
+
937
960
  toggleTaskListItem(lineNumber) {
938
961
  const state = this.editorView?.state;
939
962
  if (!this.editorView || !state) {
@@ -141,6 +141,54 @@ export class VaultApiClient {
141
141
  };
142
142
  }
143
143
 
144
+ async queryBasePropertyValues({
145
+ activeFilePath = '',
146
+ path = '',
147
+ propertyId = '',
148
+ query = '',
149
+ source = null,
150
+ sourcePath = '',
151
+ view = '',
152
+ } = {}) {
153
+ const response = await fetch(resolveApiUrl('/base/property-values'), {
154
+ body: JSON.stringify({
155
+ activeFilePath,
156
+ path,
157
+ propertyId,
158
+ query,
159
+ source,
160
+ sourcePath,
161
+ view,
162
+ }),
163
+ headers: { 'Content-Type': 'application/json' },
164
+ method: 'POST',
165
+ });
166
+ return parseApiResponse(response, 'Failed to load base property values');
167
+ }
168
+
169
+ async transformBase({
170
+ activeFilePath = '',
171
+ mutation = null,
172
+ path = '',
173
+ source = null,
174
+ sourcePath = '',
175
+ view = '',
176
+ } = {}) {
177
+ const response = await fetch(resolveApiUrl('/base/transform'), {
178
+ body: JSON.stringify({
179
+ activeFilePath,
180
+ mutation,
181
+ path,
182
+ source,
183
+ sourcePath,
184
+ view,
185
+ }),
186
+ headers: { 'Content-Type': 'application/json' },
187
+ method: 'POST',
188
+ });
189
+ return parseApiResponse(response, 'Failed to transform base');
190
+ }
191
+
144
192
  async createFile({ content, path, requestId = null }) {
145
193
  const response = await fetch(resolveApiUrl('/file'), {
146
194
  body: JSON.stringify({ content, path }),