collabmd 0.1.54 → 0.1.55
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.
- package/bin/collabmd.js +19 -38
- package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-cMeDxAEL.js +1 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-DxNxad6-.js → abnfDiagram-VCTEODGH-D2UdmYvj.js} +1 -1
- package/dist/client/assets/{abnfDiagram-O67JEVCF-Dwt6a0Ds.js → abnfDiagram-YUKMZFIV-DbmU4Q-X.js} +1 -1
- package/dist/client/assets/api-client-utils-C5ESDKMG.js +1 -0
- package/dist/client/assets/architecture-7GRP2DOG-BBwRXFv3.js +1 -1
- package/dist/client/assets/architecture-WOLXFQ4H-C0NxYiG7.js +1 -0
- package/dist/client/assets/{architectureDiagram-NJMV4G6O-D5CGiX3G.js → architectureDiagram-47P4ROYG-C7OjXK9S.js} +3 -3
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-C1uvg5id.js → architectureDiagram-5GKGNRK7-CSiUzzeN.js} +1 -1
- package/dist/client/assets/base-CaIEJUF9.css +1 -0
- package/dist/client/assets/{blockDiagram-BEXU5L5S-BgTM0var.js → blockDiagram-E7TT5TSB-DMG-YlZw.js} +3 -3
- package/dist/client/assets/{blockDiagram-I7D4REHJ-B2YonsoG.js → blockDiagram-I7D4REHJ-sXfAgK2w.js} +1 -1
- package/dist/client/assets/{browser-BKLM0ThC-BCLQGPD6-K2seJgRL.js → browser-BKLM0ThC-BCLQGPD6-CcMAHZXi.js} +1 -1
- package/dist/client/assets/{c4Diagram-7LVT6UL2-r4IwomZm.js → c4Diagram-7LVT6UL2-C88V_K-8.js} +1 -1
- package/dist/client/assets/{c4Diagram-YGBWAQC7-_xZPrNkB.js → c4Diagram-CRA5TL53-D3F9ElJv.js} +2 -2
- package/dist/client/assets/canvasEditor-BJkwGzO1.css +1 -0
- package/dist/client/assets/canvasEditor-sBK7KfrD.js +1 -0
- package/dist/client/assets/{chunk-6AZGARVD--pTGpDnx.js → chunk-24IY7LWP-flnn6SWX.js} +1 -1
- package/dist/client/assets/chunk-2BW5OAIV-CNFO1U_e.js +10 -0
- package/dist/client/assets/{chunk-2E4U76K2-CIQUuV9n.js → chunk-2E4U76K2-DEnS5IbF.js} +1 -1
- package/dist/client/assets/{chunk-5V3GS4D5-B5uC2oxu.js → chunk-3M4EKCLJ-D-sZGCtO.js} +1 -1
- package/dist/client/assets/chunk-3YJQHVM4-Ctp__8EK.js +2 -0
- package/dist/client/assets/{chunk-7PRAP22T-9uTvtc_-.js → chunk-4EA7E6EY-bU4wRJot.js} +1 -1
- package/dist/client/assets/{chunk-4HAMMTFA-q0ldNVvr.js → chunk-4HAMMTFA-CQy6Skzw.js} +1 -1
- package/dist/client/assets/{chunk-6K3QC6MW-JnuvEGV4.js → chunk-4S7OKTRW-BpUW13a3.js} +1 -1
- package/dist/client/assets/{chunk-ICYGCRZG-odvlDZyA.js → chunk-53FOQ5SW-5lPdGYP4.js} +1 -1
- package/dist/client/assets/{chunk-75Z2AOVW-DitkSoSG.js → chunk-75Z2AOVW-BRdhvVTZ.js} +1 -1
- package/dist/client/assets/{chunk-GWA4HPMP-CGmv2yIq.js → chunk-7TKQ45FW-rA2u-WUl.js} +1 -1
- package/dist/client/assets/{chunk-GTNCS2PH-ChgkzEIi.js → chunk-AW2ZBBNX-DL0KGAdi.js} +3 -3
- package/dist/client/assets/{chunk-747NJXEK-BjqqAsS9.js → chunk-BP2KR52E-DAQ1pzDV.js} +1 -1
- package/dist/client/assets/{chunk-CLGD4ZFX-BfYJtJm1.js → chunk-CLGD4ZFX-DJiSE0gD.js} +1 -1
- package/dist/client/assets/{chunk-5DYCD2WN-BKWCKnK-.js → chunk-DBDB3WZW-CXleNlok.js} +1 -1
- package/dist/client/assets/{chunk-DU6HZSFF-CWlP1Dk_.js → chunk-DU6HZSFF-C-18UZkS.js} +1 -1
- package/dist/client/assets/{chunk-MBY4JIJT-DTJ4UcAm.js → chunk-E2ZNV5FY-D_07zfWd.js} +3 -3
- package/dist/client/assets/chunk-EU5HNXII-CETVNUXF.js +213 -0
- package/dist/client/assets/{chunk-F27PBJKO-DPcsPcEs.js → chunk-F27PBJKO-_beIJos4.js} +1 -1
- package/dist/client/assets/{chunk-2ZTRR5NV-DcPRfelM.js → chunk-F6S3BTY2-DVWKCAHp.js} +1 -1
- package/dist/client/assets/{chunk-ISPTBLVO-BMsDq9Ha.js → chunk-FLFP27SU-CydzbSsY.js} +2 -2
- package/dist/client/assets/chunk-FVRAUYC3-C7b9SJ81.js +1 -0
- package/dist/client/assets/{chunk-GMAD6QVW-Bgk8qnty.js → chunk-GMAD6QVW-DnZH7DxV.js} +1 -1
- package/dist/client/assets/{chunk-GVQU2GXP-CQ8cl-Uu.js → chunk-GVQU2GXP-B0Vm415C.js} +1 -1
- package/dist/client/assets/chunk-H5A3YXKD-ChP7mNJq.js +41 -0
- package/dist/client/assets/{chunk-742MDFTN-D1nh6vNv.js → chunk-HJ2JQQFS-DQX7RePR.js} +1 -1
- package/dist/client/assets/{chunk-NETBCI7D-CmUwDWUZ.js → chunk-HTAEGDNF-C0PA_N-c.js} +1 -1
- package/dist/client/assets/{chunk-IMKFNOWR-CBqxfpcP.js → chunk-IMKFNOWR-DoO8IHzF.js} +1 -1
- package/dist/client/assets/{chunk-IH6LHLGP-BNevEHZK.js → chunk-KPI5JJXK-B2ld5-w4.js} +1 -1
- package/dist/client/assets/{chunk-WEXAMYUT-DDlVNp8J.js → chunk-KQW6MTUR-OQ4MyMr_.js} +1 -1
- package/dist/client/assets/{chunk-L3NEJ4N5-BflzkdTI.js → chunk-L3NEJ4N5-CvHossEU.js} +1 -1
- package/dist/client/assets/chunk-NGNAAXSQ-DqoPQk1a.js +136 -0
- package/dist/client/assets/{chunk-UA2S7LBM-DicYDjDM.js → chunk-NTY3LDVX-CVWLK_fh.js} +1 -1
- package/dist/client/assets/{chunk-UY3FDG6J-QT-rkt0y.js → chunk-OD3NTWWA-Dz9nCUGX.js} +1 -1
- package/dist/client/assets/{chunk-OSK3NFVY-DqeKV_pg.js → chunk-OSK3NFVY-C3JOcBao.js} +1 -1
- package/dist/client/assets/{chunk-P2QGCYS3-BA--DhpW.js → chunk-P2QGCYS3-Bfo8qS8r.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-V_NEc45n.js → chunk-POPQ4Y6H-sv6rCftH.js} +1 -1
- package/dist/client/assets/{chunk-PWAF6VOD-D_kLse-N.js → chunk-PWAF6VOD-B-AT-L2H.js} +1 -1
- package/dist/client/assets/{chunk-3Z5EZCMW-BAnA6eQ7.js → chunk-QNH66VMT-Cb9xxTwQ.js} +1 -1
- package/dist/client/assets/{chunk-SHT3W25Y-YM3S-A_M.js → chunk-SHT3W25Y-C1lu518T.js} +1 -1
- package/dist/client/assets/{chunk-GNY47TPC-CTrH5O5R.js → chunk-SVEVUXB4-BnM-LXop.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-DdNhDOmW.js → chunk-SVP7TREG-CKqt0syj.js} +1 -1
- package/dist/client/assets/{chunk-TICWLB2K-BGGgCr4x.js → chunk-TICWLB2K-D2ZnWAfF.js} +1 -1
- package/dist/client/assets/{chunk-TLUHSLCS-DCZhIDYA.js → chunk-TLUHSLCS-BMIhqv1X.js} +1 -1
- package/dist/client/assets/{chunk-6EIED4P4-DBVWq7Tz.js → chunk-TPMEZKFX-DIPHIqGb.js} +1 -1
- package/dist/client/assets/{chunk-6TQVIW2G-BSj0l_pu.js → chunk-VPELOWWC-DXayM28-.js} +1 -1
- package/dist/client/assets/chunk-VPRB5NB3-BqaJfj_v.js +127 -0
- package/dist/client/assets/{chunk-I5DQTOEV-aBZooNp1.js → chunk-VTWWFHGB-SZnJiyWY.js} +1 -1
- package/dist/client/assets/{chunk-KI3K4JFJ-FTtDwx3-.js → chunk-VZTHESGH-eHyXmK8W.js} +1 -1
- package/dist/client/assets/{chunk-7INBJB4K-CDmiHbaX.js → chunk-XC4XBNZT-D9oehSn8.js} +20 -20
- package/dist/client/assets/{chunk-XHIXRSVI-DN_fEwws.js → chunk-YK26KJH5-DVy3MGMd.js} +1 -1
- package/dist/client/assets/{chunk-6AEJRKK7-DTVZj5bY.js → chunk-Z6ETV63D-DOcrnz-P.js} +1 -1
- package/dist/client/assets/{chunk-OUJLGHUK-75UXlPjl.js → chunk-ZO67DCNQ-BAOMIkLF.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-BmETdFmC.js +1 -0
- package/dist/client/assets/{classDiagram-v2-NBCMYWYE-3TQORq33.js → classDiagram-v2-K4WV4PDN-eaadfJDB.js} +3 -3
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BmETdFmC.js +1 -0
- package/dist/client/assets/{comment-markdown-renderer-Bxi_JRSR.js → comment-markdown-renderer-CpBKaj7H.js} +1 -1
- package/dist/client/assets/comment-thread-store-DUQsWtnu.js +1 -0
- package/dist/client/assets/comment-threads-DnfkLVJA.js +4 -0
- package/dist/client/assets/comment-ui-shared-D_pgPUti.js +7 -0
- package/dist/client/assets/cynefin-EF2NZ3EQ-DVkJeZ8i.js +1 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CF3N9j1_.js +1 -1
- package/dist/client/assets/{cynefinDiagram-VND7K2PF-DwfMZ3vk.js → cynefinDiagram-3GCD6N5R-DjpAsBHC.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-DTnLZaO7.js → cynefinDiagram-5FMLGOSQ-D2rFQG67.js} +1 -1
- package/dist/client/assets/dagre-C-rnJpZF.js +1 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-Vtyx08Qh.js → dagre-GXQ25YYZ-Cku9eia9.js} +2 -2
- package/dist/client/assets/{dagre-6A5THRUB-DewyAGK6.js → dagre-W4DXFKR2-DOMOIsPd.js} +2 -2
- package/dist/client/assets/deferred-collab-C9aa9cvT.css +1 -0
- package/dist/client/assets/deferred-git-BPQ-UgrN.css +1 -0
- package/dist/client/assets/deferred-preview-B-RebLry.css +1 -0
- package/dist/client/assets/{diagram-22UHCM2B-DbZirFfl.js → diagram-2UJZ2QOL-D8WvnEyx.js} +6 -6
- package/dist/client/assets/{diagram-3UASUU5V-DmhdYv6_.js → diagram-OVF4WLC6-D8_escYE.js} +2 -2
- package/dist/client/assets/{diagram-CDSNMT55-D_qyS11R.js → diagram-PFPMY2P6-B1izZfrN.js} +3 -3
- package/dist/client/assets/{diagram-S7CK7UJ4-CGTzfo2f.js → diagram-S7CK7UJ4-BzicH2t-.js} +1 -1
- package/dist/client/assets/{diagram-MLGK6HIB-DIBG6Ivu.js → diagram-UMYRVEAY-Djf7WDjZ.js} +1 -1
- package/dist/client/assets/{diagram-UQ7AKVKN-CWZS8QtB.js → diagram-UQ7AKVKN-DqX4zTtr.js} +1 -1
- package/dist/client/assets/{diagram-VSXAHHWV-DaO1nOZF.js → diagram-VSXAHHWV-DnVbfI4Y.js} +1 -1
- package/dist/client/assets/{diagram-VX7I27RA-B54P6Cyf.js → diagram-VX7I27RA-D-Dj0TQC.js} +1 -1
- package/dist/client/assets/{diagram-MPIPVDR6-D9XsALDV.js → diagram-YEKJPTXX-B40bP3Yr.js} +2 -2
- package/dist/client/assets/{diagram-Z3DM3KII-Cl2EXiXp.js → diagram-Z3DM3KII-_iT7Uz5p.js} +1 -1
- package/dist/client/assets/diagram-ZIFT7M5P-BUUK0oNw.js +3 -0
- package/dist/client/assets/{direct-engine-DVLMOJWp-C-YL5NFb.js → direct-engine-DVLMOJWp-DWtWIifK.js} +1 -1
- package/dist/client/assets/dist-26Ce-5t4.js +1 -0
- package/dist/client/assets/dist-8MLNn-I6.js +1 -0
- package/dist/client/assets/dist-Amm5Pr1C.js +1 -0
- package/dist/client/assets/dist-BXXPaoUV.js +1 -0
- package/dist/client/assets/{dist-CoXN2D3R.js → dist-Bkb-_VdB.js} +1 -1
- package/dist/client/assets/dist-Bl4yuUS9.js +1 -0
- package/dist/client/assets/{dist-lw5TGjI6.js → dist-C2CGvCPd.js} +1 -1
- package/dist/client/assets/dist-C2niyEh0.js +1 -0
- package/dist/client/assets/{dist-DOHWxIty.js → dist-CFCSUmJk.js} +1 -1
- package/dist/client/assets/dist-CHUI56fT.js +6 -0
- package/dist/client/assets/dist-CJh3Qrid.js +13 -0
- package/dist/client/assets/{dist-Ch6Sr9KF.js → dist-CKxETpSC.js} +3 -3
- package/dist/client/assets/dist-CSHJthAI.js +2 -0
- package/dist/client/assets/dist-CbKnZGN4.js +1 -0
- package/dist/client/assets/{dist-KrdpB_GB.js → dist-Cik4RJXj.js} +1 -1
- package/dist/client/assets/{dist-uk11ih8u.js → dist-CqkAY6Ot.js} +2 -2
- package/dist/client/assets/dist-D2ql2LCd.js +1 -0
- package/dist/client/assets/{dist-BpAUe9sL.js → dist-D47MOYFN.js} +1 -1
- package/dist/client/assets/dist-DBhUaexM.js +1 -0
- package/dist/client/assets/dist-DJux6Vc5.js +1 -0
- package/dist/client/assets/{dist-9Eodxf4G.js → dist-DLNdMWPH.js} +1 -1
- package/dist/client/assets/dist-DPJj-Y3l.js +1 -0
- package/dist/client/assets/{dist-BSAZ-Hek.js → dist-DRYFMufe.js} +6 -6
- package/dist/client/assets/{dist-BrKhPlc9.js → dist-DTGcVSU8.js} +1 -1
- package/dist/client/assets/dist-DlOxjzWi.js +9 -0
- package/dist/client/assets/{dist-BT9gJXCb.js → dist-DogjjM_B.js} +1 -1
- package/dist/client/assets/dist-Dyv_RvyB.js +23 -0
- package/dist/client/assets/dist-FUFeH5Cx.js +1 -0
- package/dist/client/assets/dist-It70ktCU.js +1 -0
- package/dist/client/assets/dist-QMvcnJHf.js +1 -0
- package/dist/client/assets/dist-QN1K2czv.js +11 -0
- package/dist/client/assets/dist-W8ilOKl3.js +1 -0
- package/dist/client/assets/dist-_17-t9qP.js +1 -0
- package/dist/client/assets/{dist-DeCV9FU4.js → dist-uE774WWs.js} +1 -1
- package/dist/client/assets/document-formatter-DtXmKZcy.js +1 -0
- package/dist/client/assets/drawioEditor-DO6IOaJ1.js +9 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-PfC84FyI.js → ebnfDiagram-PWID7BFC-IemGPf9X.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-ZINNZB2B-DYvm_QtE.js → ebnfDiagram-VR2GEFS7-9fXqy1DI.js} +1 -1
- package/dist/client/assets/editor-session-CPsqXY_A.js +13 -0
- package/dist/client/assets/elk-IJKZMXRS-DsbrbBYg.js +27 -0
- package/dist/client/assets/embedded-editor-base-C44ZcCZv.css +1 -0
- package/dist/client/assets/{embedpdf-TkbBYowM.js → embedpdf-BfUI-3pv.js} +1 -1
- package/dist/client/assets/{embedpdf-cl9qiF45-itpgqvGY.js → embedpdf-cl9qiF45-CLMxJTMP.js} +2 -2
- package/dist/client/assets/en-7ZVUCEYH-C_VXKxxb.js +1 -0
- package/dist/client/assets/{erDiagram-OPXOYQCR-o8GRv8A_.js → erDiagram-O2IAPWRE-Cdadm-HN.js} +2 -2
- package/dist/client/assets/{erDiagram-RLTQ6QDP-C9cJl5NB.js → erDiagram-RLTQ6QDP-CGGmjm8t.js} +1 -1
- package/dist/client/assets/eventmodeling-K75KTNOO-BWBlZ5cR.js +1 -0
- package/dist/client/assets/eventmodeling-NTZA5JFV-CDy1bN6j.js +1 -1
- package/dist/client/assets/excalidraw-element-link-NQnskRkm.js +1 -0
- package/dist/client/assets/excalidraw-scene-DLlw4jyc.js +1 -0
- package/dist/client/assets/excalidrawEditor-DiKOIudt.css +1 -0
- package/dist/client/assets/excalidrawEditor-W2lZ5Mej.js +9 -0
- package/dist/client/assets/export-page-Bomh9oEr.js +46 -0
- package/dist/client/assets/exportDocument-BFIOECJ3.js +2 -0
- package/dist/client/assets/exportDocument-D0mONgQ4.css +1 -0
- package/dist/client/assets/file-kind-BGxTVHan.js +1 -0
- package/dist/client/assets/{file-search-BA8o_gs0.js → file-search-Dk2O14Vw.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-DcVVYJUZ.js +1 -0
- package/dist/client/assets/flowDiagram-OXPTDLAJ-Dy6y46oF.js +1 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-x40PW0Ah.js → ganttDiagram-EL5Y4UJY-Duu4KnsU.js} +1 -1
- package/dist/client/assets/ganttDiagram-R7TSEDQI-BWy52pjn.js +292 -0
- package/dist/client/assets/gitGraph-4MIJSDKK-B-rpDYbz.js +1 -1
- package/dist/client/assets/gitGraph-VSP46ZUC-1499lQAI.js +1 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-ahsNaZsp.js → gitGraphDiagram-WWUBYQGX-CaJ0vC1a.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-XJZIOB7I-DJwp9ilx.js +106 -0
- package/dist/client/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/client/assets/image-6XY3GD3F-DCWbu6d4.js +1 -0
- package/dist/client/assets/image-lightbox-controller-DAcXEXxT.js +184 -0
- package/dist/client/assets/index-CglpXOg7.css +1 -0
- package/dist/client/assets/index-D3WGkxfI.js +960 -0
- package/dist/client/assets/info-A6RAGUB7-BlutK7BT.js +1 -1
- package/dist/client/assets/info-OHQRW6UA-BDFQOo4H.js +1 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-MrZyptIy.js → infoDiagram-27XIBGKW-DBJh7QOu.js} +1 -1
- package/dist/client/assets/{infoDiagram-VRGFBTTK-KkUjDRAQ.js → infoDiagram-5W2HQ5XZ-DmWsqJ0w.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-Bnt9yM0O.js → ishikawaDiagram-5VMMS53U-CeEbSJem.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-OU5B5YK6-Di9BcpOY.js → ishikawaDiagram-K3B6WC7H-DH1NI35G.js} +2 -2
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-Bl0abK1r.js → journeyDiagram-3NMN7TZE-BoIEEr6U.js} +1 -1
- package/dist/client/assets/{journeyDiagram-ZHPQQLJL-J-sGrs9e.js → journeyDiagram-COXZFDF6-CnHV2f9V.js} +3 -3
- package/dist/client/assets/{kanban-definition-PNTS6WVX-5ZDV_yIa.js → kanban-definition-P3RFRI5V-Cxvn5ZAG.js} +4 -4
- package/dist/client/assets/{kanban-definition-UXKFOSKX-DMgUfNt0.js → kanban-definition-UXKFOSKX-BY16Tgbr.js} +1 -1
- package/dist/client/assets/keyboard-shortcuts-BWNCBJqw.js +1 -0
- package/dist/client/assets/lobby-presence-DNNXcuwR.js +1 -0
- package/dist/client/assets/{markdown-CZ1tYEW8.js → markdown-DU84620G.js} +1 -1
- package/dist/client/assets/markdown-headings-zYEUahfW.js +41 -0
- package/dist/client/assets/{markdown-math-CyyBVuDO.js → markdown-math-B96cnrz1.js} +11 -11
- package/dist/client/assets/{mermaid-parser.core-Bpogunsh.js → mermaid-parser.core-R-uwU5kf.js} +3 -3
- package/dist/client/assets/{mermaid-parser.core-DBy--Jc0.js → mermaid-parser.core-mE6t1qEO.js} +1 -1
- package/dist/client/assets/mermaid.core-D8Z6LN7O.js +10 -0
- package/dist/client/assets/{mindmap-definition-NLK3R4M7-ByVb-0vj.js → mindmap-definition-LZFPQGKD-DVfFenfJ.js} +3 -3
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-DBZN5_GH.js → mindmap-definition-YA3MSWOX-DtiE80UJ.js} +1 -1
- package/dist/client/assets/packet-AYTQ26CC-DvbY8dI6.js +1 -1
- package/dist/client/assets/packet-JDAUHWVQ-DJ9K2MA4.js +1 -0
- package/dist/client/assets/{pegDiagram-GJSIUBJH-aost1sSO.js → pegDiagram-BGZESJAR-DxfvEcwv.js} +1 -1
- package/dist/client/assets/{pegDiagram-XKGWAZYB-DRo8CZu3.js → pegDiagram-XKGWAZYB-CyozY-Kt.js} +1 -1
- package/dist/client/assets/pie-WAS4IAKB-DWSm9SSD.js +1 -1
- package/dist/client/assets/pie-XZMESJXO-CQIQasY-.js +1 -0
- package/dist/client/assets/{pieDiagram-5QR66LMP-BICdpEuz.js → pieDiagram-CAPJLFHJ-CRlMcrv3.js} +2 -2
- package/dist/client/assets/{pieDiagram-E7YTZNPT-CZThaHsc.js → pieDiagram-E7YTZNPT-BRdmTkDq.js} +1 -1
- package/dist/client/assets/preload-helper-CYtfOYgs.js +1 -0
- package/dist/client/assets/preview-render-compiler-D_aJv7Ew.js +3 -0
- package/dist/client/assets/{preview-render-worker-BA4VK_GY.js → preview-render-worker-dFNyrVyB.js} +46 -42
- package/dist/client/assets/prod-ItolaHmf.js +145 -0
- package/dist/client/assets/prod-rdly4JJQ.js +1 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-C5iowkl_.js → quadrantDiagram-AXDQQJYC-7QyPIXkF.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-O4NWA36T-Bgx5bF97.js → quadrantDiagram-GDMTTRAM-DE30uqTl.js} +3 -3
- package/dist/client/assets/quick-switcher-controller-DsK70I7K.js +1 -0
- package/dist/client/assets/radar-ABXABTNO-ByfpCXh9.js +1 -0
- package/dist/client/assets/radar-RG4KPBEZ-BfNQUEQy.js +1 -1
- package/dist/client/assets/railroad-74A4TZTK-BaQ6U6eJ.js +1 -1
- package/dist/client/assets/railroad-I3PHUGI6-CTUn9HhI.js +1 -0
- package/dist/client/assets/railroad-abnf-HS5TGJTU-BJ3KZw7J.js +1 -1
- package/dist/client/assets/railroad-abnf-LNEFI6M7-Ct-b-y0F.js +1 -0
- package/dist/client/assets/railroad-ebnf-DWYD2UWJ-BZMYMWOe.js +1 -0
- package/dist/client/assets/railroad-ebnf-LZEXJU2U-DA0sEq7w.js +1 -1
- package/dist/client/assets/railroad-peg-JR7BVNR7-Dj23PxYc.js +1 -0
- package/dist/client/assets/railroad-peg-WCYAUIDC-DCmlICzO.js +1 -1
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-BTt_rKjT.js → railroadDiagram-O6MQD6OU-BPk3RS2D.js} +1 -1
- package/dist/client/assets/{railroadDiagram-XR7U4H2S-DbVoNgSz.js → railroadDiagram-SM67HX2B-CZRXJ647.js} +1 -1
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-BkvazG7r.js → requirementDiagram-BXWQKSXE-BRwfym6z.js} +1 -1
- package/dist/client/assets/{requirementDiagram-PLB6GJNP-BT0j9C40.js → requirementDiagram-X7JNWC4A-BofCuLJA.js} +4 -4
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-C_IemB9h.js → sankeyDiagram-P5KCCOFB-D4LcrKpW.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-IPEJSGJF-X1BCUIrf.js → sankeyDiagram-UM26HJRW-CLKMDbej.js} +3 -3
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-CxFBgLGd.js → sequenceDiagram-WJ2MYXX4-DQjtv-Bs.js} +1 -1
- package/dist/client/assets/sequenceDiagram-ZO4K6R2Y-CDP_m6ma.js +169 -0
- package/dist/client/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/client/assets/sizeCapture-INFHLROL-BMR1ovSU.js +1 -0
- package/dist/client/assets/src-D5mvpivG.js +2 -0
- package/dist/client/assets/{standalone-DbyFjvTU.js → standalone-D_lg5NC9.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-BaSQ6Owj.js → stateDiagram-D77RDMKH-DSMvV-kF.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-ClmOOUsT.js +1 -0
- package/dist/client/assets/{stateDiagram-v2-GCMORJYK-CsVcfwVp.js → stateDiagram-v2-TBUQTH76-Dc5-FKzd.js} +2 -2
- package/dist/client/assets/{swimlanes-42K2YHIH-CALFEmiS.js → swimlanes-42K2YHIH-BUz31lO3.js} +1 -1
- package/dist/client/assets/{swimlanes-2SLR337P-CnYTVqRX.js → swimlanes-N4OXWK64-Dd3dFdPD.js} +1 -1
- package/dist/client/assets/swimlanesDiagram-K3J5GTZL-Bz9gFdls.js +8 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DoDs3zod.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-zse-5Dbm.js → timeline-definition-24CTP7MA-BvqCvSjy.js} +1 -1
- package/dist/client/assets/{timeline-definition-EJHVYXUP-BhqPlKkN.js → timeline-definition-YOQAKGHF-D2XWPSLs.js} +3 -3
- package/dist/client/assets/treeView-D4JQ5SDB-DcX7Bx0H.js +1 -0
- package/dist/client/assets/treeView-Q6P3EWNA-BvHOzbu9.js +1 -1
- package/dist/client/assets/treemap-SAJKECNS-BcCHmcAX.js +1 -0
- package/dist/client/assets/treemap-WGGIJYW6-oety7paS.js +1 -1
- package/dist/client/assets/usecaseDiagram-VIAY4XPW-CDe-IMjS.js +328 -0
- package/dist/client/assets/vault-api-client-DrRW0QwO.js +7 -0
- package/dist/client/assets/{vennDiagram-4TSXK5OY-DgHr6cJD.js → vennDiagram-4TSXK5OY-BW6Qp12d.js} +1 -1
- package/dist/client/assets/{vennDiagram-UO4OBE2U-BRwLn2Q1.js → vennDiagram-BLWOH2XV-KxIPKpHi.js} +3 -3
- package/dist/client/assets/wardley-7MLQ67FV-DOahSwbZ.js +1 -0
- package/dist/client/assets/wardley-WFR3VGLG-BdwDpX_I.js +1 -1
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-9A60XZis.js → wardleyDiagram-VM6X3IG4-BoCETVsK.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-VNRHLVJA-B_tMcVFG.js → wardleyDiagram-YMQ3BMBF-BNYqEy7M.js} +4 -4
- package/dist/client/assets/{wiki-link-resolver-Dch88erO.js → wiki-link-resolver-BCdGUpdu.js} +1 -1
- package/dist/client/assets/{worker-engine-BCCrxWYm-CX0qJ_cG.js → worker-engine-BCCrxWYm-1fphds7O.js} +1 -1
- package/dist/client/assets/workspace-sync-client-C_P9wtjf.js +1 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-D00iy-wB.js → xychartDiagram-S5SC5T6Z-eB5Fu9P7.js} +1 -1
- package/dist/client/assets/xychartDiagram-TAQBALBS-Sv6aaptV.js +7 -0
- package/dist/client/assets/yjs-Jd9fna_Y.js +4 -0
- package/dist/client/assets/yjs-provider-reset-guard-Cqu0Z6xH.js +1 -0
- package/dist/client/canvas-editor.html +16 -0
- package/dist/client/drawio-editor.html +7 -6
- package/dist/client/excalidraw-editor.html +31 -30
- package/dist/client/export-document.html +4 -3
- package/dist/client/index.html +2 -2
- package/package.json +27 -29
- package/scripts/local-compose.mjs +101 -0
- package/src/client/app/canvas-editor.html +13 -0
- package/src/client/application/app-shell/git-feature.js +3 -2
- package/src/client/application/app-shell/ui-feature-identity.js +1 -0
- package/src/client/application/app-shell/ui-feature-shell.js +2 -0
- package/src/client/application/app-shell/ui-feature-tab-activity.js +4 -0
- package/src/client/application/app-shell/workspace-feature.js +8 -34
- package/src/client/application/diagram-chrome.js +8 -11
- package/src/client/application/diagram-preview-hydrator.js +5 -5
- package/src/client/application/mermaid-preview-hydrator.js +3 -2
- package/src/client/application/plantuml-preview-hydrator.js +6 -0
- package/src/client/application/preview-render-compiler.js +34 -98
- package/src/client/application/preview-render-executor.js +3 -0
- package/src/client/application/preview-render-worker.js +2 -0
- package/src/client/application/preview-renderer.js +9 -5
- package/src/client/application/workspace-coordinator.js +18 -10
- package/src/client/application/workspace-preview-controller.js +105 -158
- package/src/client/application/workspace-route-controller.js +29 -54
- package/src/client/bootstrap/canvas-markdown-preview.js +87 -0
- package/src/client/bootstrap/collabmd-app-shell.js +28 -13
- package/src/client/canvas-editor.js +143 -0
- package/src/client/domain/canvas-view.js +50 -0
- package/src/client/domain/excalidraw-scene.js +4 -49
- package/src/client/domain/highlight-runtime.js +0 -4
- package/src/client/domain/markdown-formatting.js +12 -94
- package/src/client/domain/markdown-headings.js +98 -0
- package/src/client/domain/wiki-link-completions.js +42 -10
- package/src/client/infrastructure/browser-preferences-port.js +27 -33
- package/src/client/infrastructure/canvas-room-client.js +210 -0
- package/src/client/infrastructure/canvas-text-editor.js +36 -0
- package/src/client/infrastructure/comment-thread-store.js +19 -77
- package/src/client/infrastructure/editor-view-adapter.js +0 -30
- package/src/client/infrastructure/excalidraw-room-client.js +22 -10
- package/src/client/infrastructure/hosted-api-client.js +4 -2
- package/src/client/infrastructure/vault-api-client.js +4 -4
- package/src/client/presentation/canvas-editor-view.js +1028 -0
- package/src/client/presentation/canvas-embed-controller.js +116 -0
- package/src/client/presentation/canvas-picker.js +111 -0
- package/src/client/presentation/drawio-embed-controller.js +1 -3
- package/src/client/presentation/excalidraw-embed-controller.js +10 -3
- package/src/client/presentation/file-action-controller.js +37 -0
- package/src/client/presentation/file-explorer-controller.js +1 -2
- package/src/client/presentation/file-explorer-view.js +7 -1
- package/src/client/presentation/file-icon-svg.js +1 -1
- package/src/client/presentation/file-tree-state.js +4 -1
- package/src/client/presentation/image-lightbox-controller.js +1 -5
- package/src/client/presentation/team-settings-controller.js +44 -10
- package/src/client/styles/components/actions.css +4 -16
- package/src/client/styles/components/surfaces.css +0 -7
- package/src/client/styles/features/comment-card.css +1 -2
- package/src/client/styles/features/comment-markdown.css +0 -18
- package/src/client/styles/features/diagram-preview.css +0 -18
- package/src/client/styles/features/git-branch.css +0 -4
- package/src/client/styles/foundation/themes.css +6 -0
- package/src/client/styles/foundation/utilities.css +1 -2
- package/src/client/styles/layout/editor-page.css +0 -13
- package/src/client/styles/layout/responsive.css +2 -7
- package/src/client/styles/layout/shell.css +13 -1
- package/src/client/styles/layout/view-modes.css +1 -0
- package/src/client/styles/surfaces/canvas-editor.css +239 -0
- package/src/domain/canvas-room-codec.js +359 -0
- package/src/domain/collabmd-content-capabilities.js +10 -1
- package/src/domain/comment-threads.js +49 -5
- package/src/domain/excalidraw-room-codec.js +23 -73
- package/src/domain/excalidraw-scene.js +48 -0
- package/src/domain/file-kind.js +13 -1
- package/src/server/application/workspace-reconciliation.js +0 -14
- package/src/server/domain/backlink-index.js +1 -36
- package/src/server/domain/bases/base-index-snapshot-store.js +41 -24
- package/src/server/domain/bases/base-query-service.js +1 -47
- package/src/server/domain/collaboration/collaboration-room.js +149 -15
- package/src/server/domain/collaboration/room-registry.js +7 -7
- package/src/server/domain/hosted-workspace.js +29 -2
- package/src/server/domain/plantuml-encoder.js +7 -43
- package/src/server/infrastructure/git/diff-service.js +1 -5
- package/src/server/infrastructure/git/history-service.js +18 -2
- package/src/server/infrastructure/git/untracked-files.js +1 -1
- package/src/server/infrastructure/http/create-git-api-command-handler.js +60 -149
- package/src/server/infrastructure/http/create-hosted-api-handler.js +5 -2
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +2 -2
- package/src/server/infrastructure/http/http-response.js +2 -36
- package/src/server/infrastructure/http/request-body.js +14 -49
- package/src/server/infrastructure/persistence/hosted-metadata-store.js +12 -3
- package/src/server/infrastructure/persistence/path-utils.js +8 -2
- package/src/server/infrastructure/persistence/vault-file-store.js +26 -60
- package/src/server/{infrastructure/git → shared}/image-mime.js +2 -2
- package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-kJfwukbn.js +0 -1
- package/dist/client/assets/_basePickBy-QJXUw_pr.js +0 -1
- package/dist/client/assets/_baseUniq-DR4vVjyC.js +0 -1
- package/dist/client/assets/architecture-7GRP2DOG-DJVtYNt-.js +0 -1
- package/dist/client/assets/browser-utils-S4QYY31r.js +0 -1
- package/dist/client/assets/chunk-3FUC2YCW-8tZlhTGX.js +0 -1
- package/dist/client/assets/chunk-7M6MHVWA-BDakkknV.js +0 -213
- package/dist/client/assets/chunk-FOHPRMQF-Dmmo5Kak.js +0 -161
- package/dist/client/assets/chunk-O7XYJQB3-D0_ZY2za.js +0 -126
- package/dist/client/assets/chunk-T5WUORNS-DD7KlyY6.js +0 -40
- package/dist/client/assets/chunk-Z7XXMR3K-P4Vaq8hD.js +0 -10
- package/dist/client/assets/chunk-ZIGJFQKS-b8OUHKK5.js +0 -2
- package/dist/client/assets/classDiagram-ZZMXUADV-QLbh_Nkf.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-QLbh_Nkf.js +0 -1
- package/dist/client/assets/comment-thread-store-CHvy8GF8.js +0 -1
- package/dist/client/assets/comment-threads-nJhK1yfc.js +0 -4
- package/dist/client/assets/cynefin-OW5HDTMX-e-Quc9Yl.js +0 -1
- package/dist/client/assets/dagre-D7FiCLLP.js +0 -1
- package/dist/client/assets/deferred-collab-D4cxEdad.css +0 -1
- package/dist/client/assets/deferred-git-BHKtkFYi.css +0 -1
- package/dist/client/assets/deferred-preview-BAXYgf4T.css +0 -1
- package/dist/client/assets/diagram-ATOU4E4O-B5uX-jVO.js +0 -3
- package/dist/client/assets/dist-B2ZNQifU.js +0 -1
- package/dist/client/assets/dist-B6QCMNze.js +0 -1
- package/dist/client/assets/dist-BIAv19LO.js +0 -1
- package/dist/client/assets/dist-BKYp1-dl.js +0 -13
- package/dist/client/assets/dist-BSTbcioM.js +0 -11
- package/dist/client/assets/dist-BTVg1m-V.js +0 -1
- package/dist/client/assets/dist-Bi9ZIYXq.js +0 -9
- package/dist/client/assets/dist-Bqo-XRVG.js +0 -1
- package/dist/client/assets/dist-CBsvKPcT.js +0 -1
- package/dist/client/assets/dist-CJBYh9Wy.js +0 -23
- package/dist/client/assets/dist-CRwfjtwz.js +0 -2
- package/dist/client/assets/dist-CX7Kc61C.js +0 -1
- package/dist/client/assets/dist-CYfPeupF.js +0 -1
- package/dist/client/assets/dist-CucpLxUY.js +0 -1
- package/dist/client/assets/dist-DHBMeCxC.js +0 -6
- package/dist/client/assets/dist-DLEg9d4u.js +0 -1
- package/dist/client/assets/dist-DYFBD-wW.js +0 -1
- package/dist/client/assets/dist-DugWGOjQ.js +0 -1
- package/dist/client/assets/dist-NNrRnB-f.js +0 -1
- package/dist/client/assets/dist-PUxZqhFe.js +0 -1
- package/dist/client/assets/dist-cYiJMWHJ.js +0 -1
- package/dist/client/assets/dist-iKC7go4m.js +0 -1
- package/dist/client/assets/document-formatter-6I63vstk.js +0 -1
- package/dist/client/assets/drawioEditor-DNTjsjzJ.js +0 -9
- package/dist/client/assets/editor-session-D8GGkJak.js +0 -14
- package/dist/client/assets/elk-276RUBZZ-B8LB3Pac.js +0 -27
- package/dist/client/assets/embedded-editor-base-elU0Avik.css +0 -1
- package/dist/client/assets/en-JTHJ2SIP-DBz9bRq4.js +0 -1
- package/dist/client/assets/eventmodeling-NTZA5JFV-Dxtv5D-P.js +0 -1
- package/dist/client/assets/excalidraw-element-link-BRZOTIeI.js +0 -7
- package/dist/client/assets/excalidraw-scene-6uROebca.js +0 -1
- package/dist/client/assets/excalidrawEditor-BCTFp_CV.css +0 -1
- package/dist/client/assets/excalidrawEditor-D0orhhku.js +0 -9
- package/dist/client/assets/export-page-CVJNMERq.js +0 -46
- package/dist/client/assets/exportDocument-Ba-n1X4U.css +0 -1
- package/dist/client/assets/exportDocument-DSshnmnZ.js +0 -2
- package/dist/client/assets/file-kind-BwMIcZbi.js +0 -1
- package/dist/client/assets/flowDiagram-HODETNUW-Bq4470NR.js +0 -1
- package/dist/client/assets/flowDiagram-KWPJA3E3-DQG9qGiU.js +0 -1
- package/dist/client/assets/ganttDiagram-FUAMR5RP-BQpsI7gQ.js +0 -292
- package/dist/client/assets/gitGraph-4MIJSDKK-ZaBIE4Lb.js +0 -1
- package/dist/client/assets/gitGraphDiagram-X574FWY7-wbs_2m9f.js +0 -106
- package/dist/client/assets/graphlib-C9Tl7QrK.js +0 -1
- package/dist/client/assets/image-I42VHMUI-B2-c_ifV.js +0 -1
- package/dist/client/assets/index-BCgNcVNs.css +0 -1
- package/dist/client/assets/index-BpmR5Qqk.js +0 -1140
- package/dist/client/assets/info-A6RAGUB7-BcGxhjED.js +0 -1
- package/dist/client/assets/lobby-presence-KJrYhMdN.js +0 -1
- package/dist/client/assets/mermaid.core-BeOUPE3Q.js +0 -10
- package/dist/client/assets/packet-AYTQ26CC-CqpSjTWl.js +0 -1
- package/dist/client/assets/pie-WAS4IAKB-Bg2eCNl5.js +0 -1
- package/dist/client/assets/preload-helper-BAlEavIC.js +0 -1
- package/dist/client/assets/preview-render-compiler-BmWQStaL.js +0 -43
- package/dist/client/assets/prod-CA0ie9yD.js +0 -145
- package/dist/client/assets/prod-yKJZ3E4s.js +0 -1
- package/dist/client/assets/quick-switcher-controller-B5vT9Vj5.js +0 -1
- package/dist/client/assets/radar-RG4KPBEZ-DxExotg6.js +0 -1
- package/dist/client/assets/railroad-74A4TZTK-FfHmeIQk.js +0 -1
- package/dist/client/assets/railroad-abnf-HS5TGJTU-CLkdDRsJ.js +0 -1
- package/dist/client/assets/railroad-ebnf-LZEXJU2U-B162zvcG.js +0 -1
- package/dist/client/assets/railroad-peg-WCYAUIDC-CGs5GeJ5.js +0 -1
- package/dist/client/assets/sequenceDiagram-PO4LG4MO-B-2QfXhV.js +0 -169
- package/dist/client/assets/sizeCapture-INFHLROL-B0uUizjq.js +0 -1
- package/dist/client/assets/sizeCapture-INFHLROL-DrzwYt5z.js +0 -1
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CLlwrdvU.js +0 -1
- package/dist/client/assets/swimlanesDiagram-TC7HE7FX-Bh4tmz27.js +0 -8
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-xhehUG9e.js +0 -8
- package/dist/client/assets/treeView-Q6P3EWNA-Cccy0ngL.js +0 -1
- package/dist/client/assets/treemap-WGGIJYW6-sMRCjCtX.js +0 -1
- package/dist/client/assets/usecaseDiagram-POWQR4AR-DR2LHQYs.js +0 -328
- package/dist/client/assets/vault-api-client-NS_EVHJ3.js +0 -7
- package/dist/client/assets/wardley-WFR3VGLG-Y7xty_3m.js +0 -1
- package/dist/client/assets/workspace-sync-client-BrMtf6zN.js +0 -1
- package/dist/client/assets/xychartDiagram-PMCCYNJV-fRpZ1MA5.js +0 -7
- package/dist/client/assets/yjs-B41tM6Sz.js +0 -4
- package/dist/client/assets/yjs-provider-reset-guard-DpntzUDR.js +0 -1
- package/scripts/local-plantuml-compose.mjs +0 -92
- package/scripts/local-structurizr-compose.mjs +0 -101
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
import { canvasColor, canvasEdgeGeometry, canvasFilePath, canvasLinkUrl, canvasSideAtPoint } from '../domain/canvas-view.js';
|
|
2
|
+
import { getMarkdownHeadings } from '../domain/markdown-headings.js';
|
|
3
|
+
import { flattenTree } from './file-tree-state.js';
|
|
4
|
+
import { openCanvasPicker } from './canvas-picker.js';
|
|
5
|
+
|
|
6
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
7
|
+
const NODE_NAMES = { text: 'Text card', file: 'File card', link: 'Link card', group: 'Group' };
|
|
8
|
+
const ICON_PATHS = {
|
|
9
|
+
text: 'M14 3H5v18h14V8Z M14 3v5h5',
|
|
10
|
+
file: 'M14 3H5v18h14V8Z M14 3v5h5 M8 12h8 M8 16h6',
|
|
11
|
+
link: 'M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-2 2 M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l2-2',
|
|
12
|
+
group: 'M8 3H3v5 M16 3h5v5 M21 16v5h-5 M8 21H3v-5 M11 3h2 M21 11v2 M11 21h2 M3 11v2',
|
|
13
|
+
edit: 'm16 3 5 5-12 12-6 1 1-6Z M13 6l5 5',
|
|
14
|
+
color: 'M12 3a9 9 0 1 0 0 18h1a2 2 0 0 0 0-4 2 2 0 0 1 0-4h4a4 4 0 0 0 4-4c0-3-4-6-9-6Z M7 10h.01 M10 7h.01 M15 7h.01',
|
|
15
|
+
section: 'M9 3 7 21 M17 3l-2 18 M4 8h16 M3 16h16',
|
|
16
|
+
properties: 'M4 6h6 M14 6h6 M4 12h10 M18 12h2 M4 18h2 M10 18h10 M10 3v6 M14 9v6 M6 15v6',
|
|
17
|
+
delete: 'M3 6h18 M9 6V3h6v3 M5 6l1 15h12l1-15 M10 10v7 M14 10v7',
|
|
18
|
+
undo: 'M3 10h10a7 7 0 0 1 7 7 M3 10l5-5 M3 10l5 5',
|
|
19
|
+
redo: 'M21 10H11a7 7 0 0 0-7 7 M21 10l-5-5 M21 10l-5 5',
|
|
20
|
+
plus: 'M12 5v14 M5 12h14',
|
|
21
|
+
minus: 'M5 12h14',
|
|
22
|
+
fit: 'M8 3H3v5 M16 3h5v5 M21 16v5h-5 M8 21H3v-5',
|
|
23
|
+
open: 'M14 3h7v7 M21 3 10 14 M10 3H3v18h18v-7',
|
|
24
|
+
close: 'm6 6 12 12 M18 6 6 18',
|
|
25
|
+
resize: 'm9 20 11-11 M15 20l5-5',
|
|
26
|
+
help: 'M21 12a9 9 0 1 0-18 0 9 9 0 0 0 18 0 M9 9a3 3 0 0 1 6 0c0 2-3 2-3 4 M12 17h.01',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function icon(name) {
|
|
30
|
+
const svg = svgElement('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
31
|
+
'stroke-width': 1.6, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true' });
|
|
32
|
+
svg.append(svgElement('path', { d: ICON_PATHS[name] }));
|
|
33
|
+
return svg;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function element(tag, className, text) {
|
|
37
|
+
const node = document.createElement(tag);
|
|
38
|
+
if (className) node.className = className;
|
|
39
|
+
if (text !== undefined) node.textContent = text;
|
|
40
|
+
return node;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function button(label, action, iconName) {
|
|
44
|
+
const node = element('button', 'canvas-button');
|
|
45
|
+
node.type = 'button';
|
|
46
|
+
node.title = label;
|
|
47
|
+
node.setAttribute('aria-label', label);
|
|
48
|
+
if (iconName) {
|
|
49
|
+
node.classList.add('canvas-icon-button');
|
|
50
|
+
node.append(icon(iconName));
|
|
51
|
+
} else node.textContent = label;
|
|
52
|
+
node.addEventListener('click', action);
|
|
53
|
+
return node;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function svgElement(tag, attributes = {}) {
|
|
57
|
+
const node = document.createElementNS(SVG_NS, tag);
|
|
58
|
+
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, value));
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class CanvasEditorView {
|
|
63
|
+
constructor({ root, actions, createTextEditor, createMarkdownPreview, getText, readFile, readTree, attachmentUrl, onOpenFile, vaultId = '' }) {
|
|
64
|
+
Object.assign(this, { root, actions, createTextEditor, createMarkdownPreview, getText, readFile, readTree, attachmentUrl, onOpenFile, vaultId });
|
|
65
|
+
this.nodes = new Map();
|
|
66
|
+
this.selection = new Set();
|
|
67
|
+
this.document = { nodes: [], edges: [] };
|
|
68
|
+
this.viewport = { x: 80, y: 80, zoom: 1 };
|
|
69
|
+
this.lastWheelPan = -Infinity;
|
|
70
|
+
this.canEdit = false;
|
|
71
|
+
this.selectedEdge = null;
|
|
72
|
+
this.activeEditor = null;
|
|
73
|
+
this.users = [];
|
|
74
|
+
this.inspectorOpen = false;
|
|
75
|
+
this.abort = new AbortController();
|
|
76
|
+
this.mount();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
mount() {
|
|
80
|
+
this.root.replaceChildren();
|
|
81
|
+
this.root.classList.add('canvas-editor');
|
|
82
|
+
this.toolbar = element('div', 'canvas-toolbar');
|
|
83
|
+
this.toolbar.setAttribute('role', 'toolbar');
|
|
84
|
+
this.toolbar.setAttribute('aria-label', 'Canvas tools');
|
|
85
|
+
this.editButtons = [];
|
|
86
|
+
Object.entries(NODE_NAMES).forEach(([type, label]) => {
|
|
87
|
+
const control = button(`+ ${label}`, () => this.addNode(type), type);
|
|
88
|
+
control.title = `Add ${label.toLowerCase()}`;
|
|
89
|
+
this.toolbar.append(control);
|
|
90
|
+
this.editButtons.push(control);
|
|
91
|
+
});
|
|
92
|
+
this.connectButton = button('Connect', () => this.beginConnection(), 'link');
|
|
93
|
+
this.deleteButton = button('Delete', () => this.removeSelection(), 'delete');
|
|
94
|
+
this.undoButton = button('Undo', () => this.actions.undo(), 'undo');
|
|
95
|
+
this.redoButton = button('Redo', () => this.actions.redo(), 'redo');
|
|
96
|
+
this.status = element('span', 'canvas-status', 'Connecting…');
|
|
97
|
+
this.status.setAttribute('role', 'status');
|
|
98
|
+
this.banner = element('div', 'canvas-banner');
|
|
99
|
+
this.banner.hidden = true;
|
|
100
|
+
this.banner.setAttribute('role', 'alert');
|
|
101
|
+
this.body = element('div', 'canvas-body');
|
|
102
|
+
this.stage = element('div', 'canvas-stage');
|
|
103
|
+
this.stage.tabIndex = 0;
|
|
104
|
+
this.stage.setAttribute('aria-label', 'Canvas. Drag to select. Space-drag to pan. Use arrow keys to move selected cards.');
|
|
105
|
+
this.world = element('div', 'canvas-world');
|
|
106
|
+
this.edges = svgElement('svg', { class: 'canvas-edges', 'aria-label': 'Connections' });
|
|
107
|
+
this.world.append(this.edges);
|
|
108
|
+
this.cursors = element('div', 'canvas-cursors');
|
|
109
|
+
this.world.append(this.cursors);
|
|
110
|
+
this.stage.append(this.world);
|
|
111
|
+
this.emptyState = element('div', 'canvas-empty-state');
|
|
112
|
+
this.emptyState.hidden = true;
|
|
113
|
+
this.emptyState.append(element('strong', '', 'Start your canvas'),
|
|
114
|
+
element('span', '', 'Double-click to add text, or choose a card below.'));
|
|
115
|
+
this.mountSelectionToolbar();
|
|
116
|
+
this.inspector = element('aside', 'canvas-inspector');
|
|
117
|
+
this.inspector.setAttribute('aria-label', 'Canvas selection');
|
|
118
|
+
this.inspector.id = 'canvas-properties';
|
|
119
|
+
this.inspector.hidden = true;
|
|
120
|
+
this.body.append(this.stage, this.inspector);
|
|
121
|
+
this.navigation = element('div', 'canvas-navigation');
|
|
122
|
+
this.navigation.setAttribute('role', 'toolbar');
|
|
123
|
+
this.navigation.setAttribute('aria-label', 'Canvas navigation');
|
|
124
|
+
const zoomTools = element('div', 'canvas-tool-group');
|
|
125
|
+
this.zoomLabel = button('Reset zoom', () => this.zoomBy(1 / this.viewport.zoom));
|
|
126
|
+
this.zoomLabel.classList.add('canvas-zoom-label');
|
|
127
|
+
zoomTools.append(button('Zoom in', () => this.zoomBy(1.25), 'plus'), this.zoomLabel,
|
|
128
|
+
button('Fit canvas', () => this.fit(), 'fit'), button('Zoom out', () => this.zoomBy(0.8), 'minus'));
|
|
129
|
+
const historyTools = element('div', 'canvas-tool-group');
|
|
130
|
+
historyTools.append(this.undoButton, this.redoButton);
|
|
131
|
+
this.helpMenu = element('details', 'canvas-help-menu canvas-tool-group');
|
|
132
|
+
const helpToggle = element('summary', 'canvas-button canvas-icon-button');
|
|
133
|
+
helpToggle.title = 'Canvas help';
|
|
134
|
+
helpToggle.setAttribute('aria-label', 'Canvas help');
|
|
135
|
+
helpToggle.append(icon('help'));
|
|
136
|
+
const helpContent = element('div', 'canvas-help-content');
|
|
137
|
+
helpContent.append(element('strong', '', 'Canvas shortcuts'));
|
|
138
|
+
for (const [gesture, description] of [
|
|
139
|
+
['Double-click', 'Add or edit text'], ['Drag a card', 'Move it'], ['Drag background', 'Select cards'],
|
|
140
|
+
['Drag a group', 'Move its enclosed cards'], ['Shift-drag', 'Select inside a group'],
|
|
141
|
+
['Space / middle-drag', 'Pan canvas'], ['Ctrl / ⌘ + scroll', 'Zoom'],
|
|
142
|
+
['Shift + 1 / 2', 'Fit canvas / selection'], ['Drag a dot', 'Connect cards'],
|
|
143
|
+
['Choose section', 'Show a Markdown heading'],
|
|
144
|
+
]) {
|
|
145
|
+
const row = element('div');
|
|
146
|
+
row.append(element('span', '', gesture), element('span', '', description));
|
|
147
|
+
helpContent.append(row);
|
|
148
|
+
}
|
|
149
|
+
this.helpMenu.append(helpToggle, helpContent);
|
|
150
|
+
this.navigation.append(zoomTools, historyTools, this.helpMenu);
|
|
151
|
+
this.help = element('span', 'canvas-help');
|
|
152
|
+
this.help.setAttribute('role', 'status');
|
|
153
|
+
this.stage.append(this.emptyState, this.toolbar, this.navigation, this.status, this.help);
|
|
154
|
+
this.root.append(this.banner, this.body);
|
|
155
|
+
const signal = this.abort.signal;
|
|
156
|
+
this.stage.addEventListener('pointerdown', (event) => this.startPan(event), { signal });
|
|
157
|
+
this.stage.addEventListener('dblclick', (event) => {
|
|
158
|
+
if (event.target === this.stage) {
|
|
159
|
+
const bounds = this.stage.getBoundingClientRect();
|
|
160
|
+
this.addNode('text', this.toWorld(event.clientX - bounds.left, event.clientY - bounds.top));
|
|
161
|
+
}
|
|
162
|
+
}, { signal });
|
|
163
|
+
this.stage.addEventListener('pointermove', (event) => this.publishPointer(event), { signal });
|
|
164
|
+
this.stage.addEventListener('pointerleave', () => this.actions.presence('pointer', null), { signal });
|
|
165
|
+
this.stage.addEventListener('dragover', (event) => {
|
|
166
|
+
if (this.canEdit && event.dataTransfer?.types.includes('application/x-collabmd-vault-file')) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
169
|
+
}
|
|
170
|
+
}, { signal });
|
|
171
|
+
this.stage.addEventListener('drop', (event) => { void this.dropFile(event); }, { signal });
|
|
172
|
+
this.stage.addEventListener('wheel', (event) => {
|
|
173
|
+
if (event.defaultPrevented || event.target.closest('.canvas-selection-toolbar, .canvas-navigation, .canvas-toolbar')) return;
|
|
174
|
+
const zooming = event.ctrlKey || event.metaKey || this.spaceHeld;
|
|
175
|
+
const content = event.target.closest('.canvas-card-content');
|
|
176
|
+
// Keep panning across cards until a pause allows a new content-scroll gesture.
|
|
177
|
+
if (!zooming && content && event.timeStamp - this.lastWheelPan > 200) {
|
|
178
|
+
const horizontal = event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY);
|
|
179
|
+
const delta = horizontal && !event.shiftKey ? event.deltaX : event.deltaY;
|
|
180
|
+
for (let target = event.target; content.contains(target); target = target.parentElement) {
|
|
181
|
+
const style = getComputedStyle(target);
|
|
182
|
+
if (!/(auto|scroll)/.test(style[horizontal ? 'overflowX' : 'overflowY'])) continue;
|
|
183
|
+
const position = horizontal ? target.scrollLeft : target.scrollTop;
|
|
184
|
+
const maximum = horizontal ? target.scrollWidth - target.clientWidth : target.scrollHeight - target.clientHeight;
|
|
185
|
+
if (delta < 0 ? position > 0 : delta > 0 && position < maximum - 1) return;
|
|
186
|
+
if (style[horizontal ? 'overscrollBehaviorX' : 'overscrollBehaviorY'] !== 'auto') break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
if (zooming) {
|
|
191
|
+
this.zoomBy(Math.exp(-event.deltaY * 0.005), event.clientX, event.clientY);
|
|
192
|
+
} else {
|
|
193
|
+
this.lastWheelPan = event.timeStamp;
|
|
194
|
+
this.viewport.x -= event.shiftKey ? event.deltaY : event.deltaX;
|
|
195
|
+
this.viewport.y -= event.shiftKey ? 0 : event.deltaY;
|
|
196
|
+
this.applyViewport();
|
|
197
|
+
}
|
|
198
|
+
}, { signal, passive: false });
|
|
199
|
+
this.root.addEventListener('keydown', (event) => this.onKeyDown(event), { signal });
|
|
200
|
+
this.root.addEventListener('pointerdown', (event) => {
|
|
201
|
+
this.lastWheelPan = -Infinity;
|
|
202
|
+
[this.colorMenu, this.helpMenu].forEach((menu) => { if (!menu.contains(event.target)) menu.open = false; });
|
|
203
|
+
}, { signal, capture: true });
|
|
204
|
+
window.addEventListener('keyup', (event) => { if (event.code === 'Space') this.spaceHeld = false; }, { signal });
|
|
205
|
+
window.addEventListener('blur', () => { this.spaceHeld = false; this.cancelGesture?.(); }, { signal });
|
|
206
|
+
this.resizeObserver = new ResizeObserver(() => this.positionSelectionToolbar());
|
|
207
|
+
this.resizeObserver.observe(this.stage);
|
|
208
|
+
this.renderInspector();
|
|
209
|
+
this.applyViewport();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
mountSelectionToolbar() {
|
|
213
|
+
this.selectionToolbar = element('div', 'canvas-selection-toolbar');
|
|
214
|
+
this.selectionToolbar.setAttribute('role', 'toolbar');
|
|
215
|
+
this.selectionToolbar.setAttribute('aria-label', 'Selection actions');
|
|
216
|
+
this.selectionToolbar.hidden = true;
|
|
217
|
+
this.editTextButton = button('Edit text', () => this.openTextEditor([...this.selection][0]), 'edit');
|
|
218
|
+
this.fileButton = button('Change file', () => this.openFilePicker([...this.selection][0]), 'file');
|
|
219
|
+
this.headingButton = button('Choose section', () => this.openHeadingPicker([...this.selection][0]));
|
|
220
|
+
this.headingButton.prepend(icon('section'));
|
|
221
|
+
this.labelInput = element('input', 'canvas-connection-label');
|
|
222
|
+
this.labelInput.placeholder = 'Connection label';
|
|
223
|
+
this.labelInput.setAttribute('aria-label', 'Connection label');
|
|
224
|
+
this.labelInput.addEventListener('change', () => {
|
|
225
|
+
if (this.canEdit && this.selectedEdge) this.actions.updateEdge(this.selectedEdge, { label: this.labelInput.value || undefined });
|
|
226
|
+
});
|
|
227
|
+
this.labelInput.addEventListener('keydown', (event) => {
|
|
228
|
+
if (event.key === 'Enter') { event.preventDefault(); this.labelInput.blur(); }
|
|
229
|
+
if (event.key === 'Escape') {
|
|
230
|
+
this.labelInput.value = (this.document.edges ?? []).find((edge) => edge.id === this.selectedEdge)?.label || '';
|
|
231
|
+
this.labelInput.blur();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
this.colorMenu = element('details', 'canvas-color-menu');
|
|
235
|
+
const colorToggle = element('summary', 'canvas-button canvas-icon-button');
|
|
236
|
+
colorToggle.title = 'Set color';
|
|
237
|
+
colorToggle.setAttribute('aria-label', 'Set color');
|
|
238
|
+
colorToggle.append(icon('color'));
|
|
239
|
+
this.colorMenu.append(colorToggle);
|
|
240
|
+
const colors = element('div', 'canvas-colors');
|
|
241
|
+
['', '1', '2', '3', '4', '5', '6'].forEach((color) => {
|
|
242
|
+
const swatch = button('', () => { this.setSelectionColor(color || undefined); this.colorMenu.open = false; });
|
|
243
|
+
swatch.setAttribute('aria-label', color ? `Color ${color}` : 'Default color');
|
|
244
|
+
swatch.title = color ? `Color ${color}` : 'Default color';
|
|
245
|
+
swatch.style.setProperty('--canvas-color', canvasColor(color));
|
|
246
|
+
colors.append(swatch);
|
|
247
|
+
});
|
|
248
|
+
const customColor = element('input');
|
|
249
|
+
customColor.type = 'color';
|
|
250
|
+
customColor.setAttribute('aria-label', 'Custom color');
|
|
251
|
+
customColor.addEventListener('change', () => this.setSelectionColor(customColor.value));
|
|
252
|
+
colors.append(customColor);
|
|
253
|
+
this.colorMenu.append(colors);
|
|
254
|
+
this.propertiesButton = button('Properties', () => {
|
|
255
|
+
this.inspectorOpen = !this.inspectorOpen;
|
|
256
|
+
this.renderInspector();
|
|
257
|
+
this.positionSelectionToolbar();
|
|
258
|
+
}, 'properties');
|
|
259
|
+
this.propertiesButton.setAttribute('aria-controls', 'canvas-properties');
|
|
260
|
+
this.fitSelectionButton = button('Fit selection', () => this.fit([...this.selection]), 'fit');
|
|
261
|
+
this.selectionToolbar.append(this.editTextButton, this.fileButton, this.headingButton,
|
|
262
|
+
this.labelInput, this.colorMenu, this.connectButton, this.fitSelectionButton, this.propertiesButton, this.deleteButton);
|
|
263
|
+
this.stage.append(this.selectionToolbar);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
updateSelectionToolbar() {
|
|
267
|
+
const node = this.selection.size === 1 ? this.getNode([...this.selection][0]) : null;
|
|
268
|
+
const edge = (this.document.edges ?? []).find((entry) => entry.id === this.selectedEdge);
|
|
269
|
+
this.selectionToolbar.hidden = !node && !edge && !this.selection.size;
|
|
270
|
+
this.editTextButton.hidden = node?.type !== 'text';
|
|
271
|
+
this.fileButton.hidden = node?.type !== 'file';
|
|
272
|
+
this.headingButton.hidden = node?.type !== 'file' || !/\.(md|markdown|mdx)$/i.test(node.file);
|
|
273
|
+
this.labelInput.hidden = !edge;
|
|
274
|
+
this.connectButton.hidden = !this.selection.size;
|
|
275
|
+
this.fitSelectionButton.hidden = !this.selection.size;
|
|
276
|
+
if (document.activeElement !== this.labelInput) this.labelInput.value = edge?.label || '';
|
|
277
|
+
[this.editTextButton, this.fileButton, this.headingButton, this.labelInput].forEach((control) => { control.disabled = !this.canEdit; });
|
|
278
|
+
this.colorMenu.querySelectorAll('button, input').forEach((control) => { control.disabled = !this.canEdit; });
|
|
279
|
+
this.propertiesButton.setAttribute('aria-expanded', String(!this.inspector.hidden));
|
|
280
|
+
this.positionSelectionToolbar();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
positionSelectionToolbar() {
|
|
284
|
+
if (this.selectionToolbar.hidden) return;
|
|
285
|
+
const bounds = this.stage.getBoundingClientRect();
|
|
286
|
+
const rectangles = [...this.selection].map((id) => this.nodes.get(id)?.card.getBoundingClientRect()).filter(Boolean);
|
|
287
|
+
if (!rectangles.length) {
|
|
288
|
+
const edge = [...this.edges.querySelectorAll('[data-edge-id]')].find((item) => item.dataset.edgeId === this.selectedEdge);
|
|
289
|
+
if (edge) rectangles.push(edge.getBoundingClientRect());
|
|
290
|
+
}
|
|
291
|
+
if (!rectangles.length) return;
|
|
292
|
+
const left = Math.min(...rectangles.map((rect) => rect.left));
|
|
293
|
+
const right = Math.max(...rectangles.map((rect) => rect.right));
|
|
294
|
+
const labelSpace = [...this.selection].some((id) => this.getNode(id)?.type !== 'text') ? 28 : 0;
|
|
295
|
+
const top = Math.min(...rectangles.map((rect) => rect.top)) - labelSpace;
|
|
296
|
+
const width = this.selectionToolbar.offsetWidth;
|
|
297
|
+
this.selectionToolbar.style.left = `${Math.max(8, Math.min(bounds.width - width - 64, (left + right) / 2 - bounds.left - width / 2))}px`;
|
|
298
|
+
this.selectionToolbar.style.top = `${Math.max(8, Math.min(bounds.height - this.selectionToolbar.offsetHeight - 8, top - bounds.top - this.selectionToolbar.offsetHeight - 10))}px`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
setSelectionColor(color) {
|
|
302
|
+
if (!this.canEdit) return;
|
|
303
|
+
if (this.selectedEdge) this.actions.updateEdge(this.selectedEdge, { color });
|
|
304
|
+
else this.actions.updateNodes([...this.selection].map((id) => ({ id, patch: { color } })));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
openFilePicker(id = null, position = null) {
|
|
308
|
+
if (!this.canEdit) return;
|
|
309
|
+
this.picker?.close();
|
|
310
|
+
this.picker = openCanvasPicker({
|
|
311
|
+
root: this.root, title: 'Choose a Vault file',
|
|
312
|
+
canChoose: () => this.canEdit && (!id || this.getNode(id)?.type === 'file'),
|
|
313
|
+
loadChoices: async () => flattenTree((await this.readTree()).tree ?? []).files
|
|
314
|
+
.filter(canvasFilePath).sort().map((path) => ({ value: path, label: path.split('/').pop(), detail: path, searchText: path })),
|
|
315
|
+
onChoose: (file) => {
|
|
316
|
+
if (id) {
|
|
317
|
+
if (this.getNode(id)?.file !== file) this.actions.updateNode(id, { file, subpath: undefined });
|
|
318
|
+
} else this.addNode('file', position, { file });
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
openHeadingPicker(id) {
|
|
324
|
+
const node = this.getNode(id);
|
|
325
|
+
if (!this.canEdit || node?.type !== 'file') return;
|
|
326
|
+
const file = node.file;
|
|
327
|
+
this.picker?.close();
|
|
328
|
+
this.picker = openCanvasPicker({
|
|
329
|
+
root: this.root, title: 'Choose a heading',
|
|
330
|
+
canChoose: () => this.canEdit && this.getNode(id)?.type === 'file' && this.getNode(id).file === file,
|
|
331
|
+
loadChoices: async () => {
|
|
332
|
+
const { content } = await this.readFile(file);
|
|
333
|
+
return [{ value: '', label: 'Whole file', detail: file },
|
|
334
|
+
...getMarkdownHeadings(content).map(({ id: anchor, text }) => ({ value: `#${anchor}`, label: text, detail: `#${anchor}` }))];
|
|
335
|
+
},
|
|
336
|
+
onChoose: (subpath) => this.actions.updateNode(id, { subpath: subpath || undefined }),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async dropFile(event) {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
if (!this.canEdit) return;
|
|
343
|
+
try {
|
|
344
|
+
const payload = JSON.parse(event.dataTransfer?.getData('application/x-collabmd-vault-file') || 'null');
|
|
345
|
+
if (!payload || payload.vaultId !== this.vaultId || !canvasFilePath(payload.path)) return;
|
|
346
|
+
const bounds = this.stage.getBoundingClientRect();
|
|
347
|
+
const position = this.toWorld(event.clientX - bounds.left, event.clientY - bounds.top);
|
|
348
|
+
const files = flattenTree((await this.readTree()).tree ?? []).files;
|
|
349
|
+
if (!this.abort.signal.aborted && this.canEdit && files.includes(payload.path)) this.addNode('file', position, { file: payload.path });
|
|
350
|
+
} catch { this.status.textContent = 'Unable to add this file. Choose a file from this Vault.'; }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
update({ document: next, canEdit, synced, conflict, error, undoAvailable, redoAvailable }) {
|
|
354
|
+
this.canEdit = canEdit;
|
|
355
|
+
this.status.textContent = error || (conflict ? 'External file conflict' : (canEdit ? 'Live' : (synced ? 'Read only' : 'Reconnecting…')));
|
|
356
|
+
this.status.dataset.state = canEdit ? 'live' : 'readonly';
|
|
357
|
+
this.root.dataset.ready = String(Boolean(next));
|
|
358
|
+
this.root.dataset.editable = String(canEdit);
|
|
359
|
+
this.editButtons.forEach((control) => { control.disabled = !canEdit; });
|
|
360
|
+
this.connectButton.disabled = !canEdit || this.selection.size === 0;
|
|
361
|
+
this.deleteButton.disabled = !canEdit || (!this.selection.size && !this.selectedEdge);
|
|
362
|
+
this.undoButton.disabled = !canEdit || !undoAvailable;
|
|
363
|
+
this.redoButton.disabled = !canEdit || !redoAvailable;
|
|
364
|
+
this.activeEditor?.editor.setReadOnly(!canEdit);
|
|
365
|
+
if (!canEdit) this.cancelGesture?.();
|
|
366
|
+
this.inspector.querySelectorAll('input, select, .canvas-order button').forEach((control) => { control.disabled = !canEdit; });
|
|
367
|
+
this.renderConflict(conflict, synced);
|
|
368
|
+
if (!next) return;
|
|
369
|
+
this.document = next;
|
|
370
|
+
this.emptyState.hidden = Boolean(next.nodes?.length);
|
|
371
|
+
if (this.activeEditor && (this.getNode(this.activeEditor.id)?.type !== 'text'
|
|
372
|
+
|| this.getText(this.activeEditor.id) !== this.activeEditor.editor.text)) this.closeTextEditor();
|
|
373
|
+
const present = new Set((next.nodes ?? []).map((node) => node.id));
|
|
374
|
+
for (const [id, entry] of this.nodes) {
|
|
375
|
+
if (present.has(id)) continue;
|
|
376
|
+
if (this.activeEditor?.id === id) this.closeTextEditor();
|
|
377
|
+
entry.preview?.destroy();
|
|
378
|
+
entry.card.remove();
|
|
379
|
+
this.nodes.delete(id);
|
|
380
|
+
this.selection.delete(id);
|
|
381
|
+
}
|
|
382
|
+
(next.nodes ?? []).forEach((node, index) => this.renderNode(node, index));
|
|
383
|
+
if (this.selectedEdge && !(next.edges ?? []).some((edge) => edge.id === this.selectedEdge)) this.selectedEdge = null;
|
|
384
|
+
this.renderEdges();
|
|
385
|
+
if (!this.inspector.contains(document.activeElement)) this.renderInspector();
|
|
386
|
+
else this.syncInspectorFields?.();
|
|
387
|
+
this.updateSelectionToolbar();
|
|
388
|
+
this.renderPresence(this.users);
|
|
389
|
+
if (!this.didFit && this.stage.clientWidth) {
|
|
390
|
+
this.fit();
|
|
391
|
+
this.didFit = true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
renderConflict(conflict, synced) {
|
|
396
|
+
const signature = JSON.stringify([conflict, synced]);
|
|
397
|
+
if (signature === this.conflictSignature) return;
|
|
398
|
+
this.conflictSignature = signature;
|
|
399
|
+
this.banner.replaceChildren();
|
|
400
|
+
this.banner.hidden = !conflict;
|
|
401
|
+
if (!conflict) return;
|
|
402
|
+
this.banner.append(element('span', '', conflict.invalid
|
|
403
|
+
? 'The file changed outside CollabMD and is invalid. Your canvas edits are preserved.'
|
|
404
|
+
: 'The file changed outside CollabMD. Choose which version to keep.'));
|
|
405
|
+
const keep = button('Keep canvas edits', () => this.actions.resolveConflict('local'));
|
|
406
|
+
keep.disabled = !synced;
|
|
407
|
+
this.banner.append(keep);
|
|
408
|
+
const external = button('Use external version', () => this.actions.resolveConflict('external'));
|
|
409
|
+
external.disabled = !synced || conflict.invalid;
|
|
410
|
+
this.banner.append(external);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
renderNode(node, index) {
|
|
414
|
+
let entry = this.nodes.get(node.id);
|
|
415
|
+
if (!entry) {
|
|
416
|
+
const card = element('article', 'canvas-card');
|
|
417
|
+
card.dataset.nodeId = node.id;
|
|
418
|
+
card.tabIndex = 0;
|
|
419
|
+
const header = element('div', 'canvas-card-header');
|
|
420
|
+
const title = element('span', 'canvas-card-title');
|
|
421
|
+
const openFile = button('Open file', () => {
|
|
422
|
+
const current = this.getNode(node.id);
|
|
423
|
+
if (current?.type === 'file') this.onOpenFile(current.file, current.subpath);
|
|
424
|
+
}, 'open');
|
|
425
|
+
openFile.classList.add('canvas-open-file');
|
|
426
|
+
header.append(title, openFile);
|
|
427
|
+
const content = element('div', 'canvas-card-content');
|
|
428
|
+
const resize = button('Resize', () => {}, 'resize');
|
|
429
|
+
resize.classList.add('canvas-resize');
|
|
430
|
+
card.append(header, content, resize);
|
|
431
|
+
this.world.append(card);
|
|
432
|
+
entry = { card, header, title, content, resize, openFile };
|
|
433
|
+
this.nodes.set(node.id, entry);
|
|
434
|
+
card.addEventListener('pointerdown', (event) => this.startNodeDrag(event, node.id));
|
|
435
|
+
card.addEventListener('dblclick', (event) => {
|
|
436
|
+
if (event.target.closest('a, button, input, textarea, select, .diagram-preview-shell')) return;
|
|
437
|
+
if (this.getNode(node.id)?.type === 'text') this.openTextEditor(node.id);
|
|
438
|
+
});
|
|
439
|
+
card.addEventListener('keydown', (event) => {
|
|
440
|
+
if (event.target !== card || !['Enter', ' '].includes(event.key)) return;
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
event.stopPropagation();
|
|
443
|
+
if (event.key === ' ') this.spaceHeld = true;
|
|
444
|
+
this.select(node.id, event.shiftKey);
|
|
445
|
+
if (event.key === 'Enter' && this.getNode(node.id)?.type === 'text') this.openTextEditor(node.id);
|
|
446
|
+
});
|
|
447
|
+
resize.addEventListener('pointerdown', (event) => this.startNodeDrag(event, node.id, true));
|
|
448
|
+
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
449
|
+
const handle = button('', (event) => {
|
|
450
|
+
if (event.detail === 0) this.beginConnection(node.id, side);
|
|
451
|
+
});
|
|
452
|
+
handle.classList.add('canvas-connection-handle');
|
|
453
|
+
handle.dataset.side = side;
|
|
454
|
+
handle.setAttribute('aria-label', `Connect from ${side}`);
|
|
455
|
+
handle.title = `Drag to connect from ${side}`;
|
|
456
|
+
handle.addEventListener('pointerdown', (event) => this.startConnection(event, node.id, side));
|
|
457
|
+
card.append(handle);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const signature = JSON.stringify([node.type, node.text, node.file, node.subpath, node.url, node.background, node.backgroundStyle]);
|
|
461
|
+
if (entry.signature !== signature) entry.headingLabel = '';
|
|
462
|
+
entry.card.dataset.type = node.type;
|
|
463
|
+
entry.card.style.left = `${node.x}px`;
|
|
464
|
+
entry.card.style.top = `${node.y}px`;
|
|
465
|
+
entry.card.style.width = `${Math.max(1, node.width)}px`;
|
|
466
|
+
entry.card.style.height = `${Math.max(1, node.height)}px`;
|
|
467
|
+
entry.card.style.zIndex = String(index + 1);
|
|
468
|
+
entry.card.style.setProperty('--canvas-color', canvasColor(node.color));
|
|
469
|
+
entry.header.hidden = node.type === 'text';
|
|
470
|
+
entry.title.textContent = node.type === 'group' ? node.label || 'Group'
|
|
471
|
+
: (node.type === 'file' ? `${node.file.split('/').pop() || 'Choose a file'}${node.subpath ? ` › ${entry.headingLabel || node.subpath.slice(1)}` : ''}` : node.url);
|
|
472
|
+
entry.title.title = node.type === 'file' ? `${node.file}${node.subpath || ''}` : entry.title.textContent;
|
|
473
|
+
entry.openFile.hidden = node.type !== 'file' || !canvasFilePath(node.file);
|
|
474
|
+
entry.openFile.setAttribute('aria-label', `Open ${node.file || ''}${node.subpath || ''}`);
|
|
475
|
+
entry.openFile.title = `Open ${node.file || ''}${node.subpath || ''}`;
|
|
476
|
+
entry.card.setAttribute('aria-label', `${NODE_NAMES[node.type]}: ${node.text?.slice(0, 80) || node.label || node.file || node.url || ''}`);
|
|
477
|
+
entry.card.classList.toggle('is-selected', this.selection.has(node.id));
|
|
478
|
+
entry.resize.disabled = !this.canEdit;
|
|
479
|
+
entry.card.querySelectorAll('.canvas-connection-handle').forEach((handle) => { handle.disabled = !this.canEdit; });
|
|
480
|
+
if (this.activeEditor?.id === node.id) return;
|
|
481
|
+
if (entry.signature === signature) return;
|
|
482
|
+
entry.signature = signature;
|
|
483
|
+
entry.preview?.destroy();
|
|
484
|
+
entry.preview = null;
|
|
485
|
+
entry.content.replaceChildren();
|
|
486
|
+
entry.content.style.backgroundImage = '';
|
|
487
|
+
if (node.type === 'text') entry.preview = this.createMarkdownPreview(entry.content, node.text);
|
|
488
|
+
if (node.type === 'link') {
|
|
489
|
+
const href = canvasLinkUrl(node.url);
|
|
490
|
+
const link = element(href ? 'a' : 'span', 'canvas-card-link', node.url);
|
|
491
|
+
if (href) Object.assign(link, { href, target: '_blank', rel: 'noopener noreferrer' });
|
|
492
|
+
entry.content.append(link);
|
|
493
|
+
}
|
|
494
|
+
if (node.type === 'file') void this.renderFile(entry, node, signature);
|
|
495
|
+
if (node.type === 'group' && node.background) {
|
|
496
|
+
const path = canvasFilePath(node.background);
|
|
497
|
+
if (path) entry.content.style.backgroundImage = `url("${this.attachmentUrl(path)}")`;
|
|
498
|
+
entry.content.dataset.backgroundStyle = node.backgroundStyle || 'cover';
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async renderFile(entry, node, signature) {
|
|
503
|
+
const path = canvasFilePath(node.file);
|
|
504
|
+
if (!path) {
|
|
505
|
+
entry.content.append(element('span', '', 'This file path is unavailable in the Vault.'));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (/\.(png|jpe?g|gif|webp|svg)$/i.test(path)) {
|
|
509
|
+
const image = element('img', 'canvas-file-image');
|
|
510
|
+
image.src = this.attachmentUrl(path);
|
|
511
|
+
image.alt = path;
|
|
512
|
+
image.loading = 'lazy';
|
|
513
|
+
image.addEventListener('error', () => { image.replaceWith(element('p', '', 'Image unavailable')); });
|
|
514
|
+
entry.content.append(image);
|
|
515
|
+
} else if (/\.(md|markdown|mdx)$/i.test(path)) {
|
|
516
|
+
const preview = element('div', 'canvas-file-markdown');
|
|
517
|
+
entry.content.append(preview);
|
|
518
|
+
try {
|
|
519
|
+
const result = await this.readFile(path);
|
|
520
|
+
if (entry.signature === signature && preview.isConnected) {
|
|
521
|
+
entry.headingLabel = getMarkdownHeadings(result.content).find(({ id }) => `#${id}` === node.subpath)?.text || '';
|
|
522
|
+
entry.title.textContent = `${path.split('/').pop()}${node.subpath ? ` › ${entry.headingLabel || node.subpath.slice(1)}` : ''}`;
|
|
523
|
+
entry.preview = this.createMarkdownPreview(preview, result.content, path, node.subpath);
|
|
524
|
+
entry.content.scrollTop = 0;
|
|
525
|
+
}
|
|
526
|
+
} catch {
|
|
527
|
+
if (entry.signature === signature) preview.textContent = 'File unavailable';
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
entry.content.append(element('p', '', 'Open this file in the workspace.'));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
getNode(id) { return (this.document.nodes ?? []).find((node) => node.id === id); }
|
|
535
|
+
|
|
536
|
+
select(id, additive = false) {
|
|
537
|
+
if (this.connectFrom && id !== this.connectFrom.id && this.canEdit) {
|
|
538
|
+
this.actions.addEdge({ fromNode: this.connectFrom.id, toNode: id, ...(this.connectFrom.side ? { fromSide: this.connectFrom.side } : {}) });
|
|
539
|
+
this.connectFrom = null;
|
|
540
|
+
this.help.textContent = 'Connection added';
|
|
541
|
+
}
|
|
542
|
+
this.closeTextEditor();
|
|
543
|
+
this.selectedEdge = null;
|
|
544
|
+
if (additive) {
|
|
545
|
+
if (this.selection.has(id)) this.selection.delete(id);
|
|
546
|
+
else this.selection.add(id);
|
|
547
|
+
} else this.selection = new Set([id]);
|
|
548
|
+
this.syncSelection();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
syncSelection() {
|
|
552
|
+
this.nodes.forEach(({ card }, id) => card.classList.toggle('is-selected', this.selection.has(id)));
|
|
553
|
+
this.actions.presence('selectedNodeIds', [...this.selection]);
|
|
554
|
+
this.connectButton.disabled = !this.canEdit || !this.selection.size;
|
|
555
|
+
this.deleteButton.disabled = !this.canEdit || (!this.selection.size && !this.selectedEdge);
|
|
556
|
+
this.renderInspector();
|
|
557
|
+
this.edges.querySelectorAll('[data-edge-id]').forEach((edge) => edge.classList.toggle('is-selected', edge.dataset.edgeId === this.selectedEdge));
|
|
558
|
+
this.updateSelectionToolbar();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
openTextEditor(id) {
|
|
562
|
+
if (!this.canEdit || this.activeEditor?.id === id) return;
|
|
563
|
+
this.select(id);
|
|
564
|
+
const entry = this.nodes.get(id);
|
|
565
|
+
entry.preview?.destroy();
|
|
566
|
+
entry.preview = null;
|
|
567
|
+
entry.content.replaceChildren();
|
|
568
|
+
entry.content.classList.add('canvas-text-editor');
|
|
569
|
+
const editor = this.createTextEditor(id, entry.content, () => this.closeTextEditor());
|
|
570
|
+
if (editor) this.activeEditor = { id, editor };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
closeTextEditor() {
|
|
574
|
+
if (!this.activeEditor) return;
|
|
575
|
+
const { id, editor } = this.activeEditor;
|
|
576
|
+
this.activeEditor = null;
|
|
577
|
+
editor.destroy();
|
|
578
|
+
const entry = this.nodes.get(id);
|
|
579
|
+
if (entry) {
|
|
580
|
+
entry.signature = null;
|
|
581
|
+
entry.content.classList.remove('canvas-text-editor');
|
|
582
|
+
const node = this.getNode(id);
|
|
583
|
+
if (node) this.renderNode(node, (this.document.nodes ?? []).indexOf(node));
|
|
584
|
+
entry.card.focus({ preventScroll: true });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
beginConnection(id = null, side = null) {
|
|
589
|
+
if (!this.canEdit) return;
|
|
590
|
+
if (id) this.select(id);
|
|
591
|
+
if (!id && this.selection.size >= 2) {
|
|
592
|
+
const [fromNode, toNode] = this.selection;
|
|
593
|
+
this.actions.addEdge({ fromNode, toNode });
|
|
594
|
+
} else {
|
|
595
|
+
this.connectFrom = { id: id || [...this.selection][0], side };
|
|
596
|
+
this.help.textContent = 'Select another card to connect. Escape cancels.';
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
addNode(type, position = null, values = null) {
|
|
601
|
+
if (!this.canEdit) return;
|
|
602
|
+
if (type === 'file' && !values) { this.openFilePicker(null, position); return; }
|
|
603
|
+
const center = position || this.toWorld(this.stage.clientWidth / 2, this.stage.clientHeight / 2);
|
|
604
|
+
const width = type === 'group' ? 480 : 280;
|
|
605
|
+
const height = type === 'group' ? 360 : 200;
|
|
606
|
+
const defaults = { text: { text: '' }, file: { file: '' }, link: { url: 'https://' }, group: { label: 'Group' } };
|
|
607
|
+
const id = this.actions.addNode({ type, x: Math.round(center.x - width / 2), y: Math.round(center.y - height / 2), width, height, ...defaults[type], ...values });
|
|
608
|
+
if (id) {
|
|
609
|
+
this.select(id);
|
|
610
|
+
if (type === 'text') this.openTextEditor(id);
|
|
611
|
+
else if (type !== 'file') { this.inspectorOpen = true; this.renderInspector(); this.inspector.querySelector('input')?.focus(); }
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
removeSelection() {
|
|
616
|
+
if (!this.canEdit) return;
|
|
617
|
+
this.closeTextEditor();
|
|
618
|
+
if (this.selectedEdge) this.actions.deleteEdges([this.selectedEdge]);
|
|
619
|
+
else this.actions.deleteNodes([...this.selection]);
|
|
620
|
+
this.selection.clear();
|
|
621
|
+
this.selectedEdge = null;
|
|
622
|
+
this.syncSelection();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
renderEdges() {
|
|
626
|
+
this.edges.replaceChildren();
|
|
627
|
+
const definitions = svgElement('defs');
|
|
628
|
+
const arrow = svgElement('marker', { id: 'canvas-arrow', markerWidth: 10, markerHeight: 10,
|
|
629
|
+
refX: 9, refY: 5, orient: 'auto-start-reverse', markerUnits: 'userSpaceOnUse' });
|
|
630
|
+
arrow.append(svgElement('path', { d: 'M 0 0 L 10 5 L 0 10 Z', fill: 'context-stroke' }));
|
|
631
|
+
definitions.append(arrow);
|
|
632
|
+
this.edges.append(definitions);
|
|
633
|
+
const nodes = new Map((this.document.nodes ?? []).map((node) => [node.id, node]));
|
|
634
|
+
(this.document.edges ?? []).forEach((edge) => {
|
|
635
|
+
const geometry = canvasEdgeGeometry(edge, nodes);
|
|
636
|
+
if (!geometry) return;
|
|
637
|
+
const group = svgElement('g', { class: `canvas-edge${this.selectedEdge === edge.id ? ' is-selected' : ''}`,
|
|
638
|
+
'data-edge-id': edge.id, tabindex: 0, role: 'button', 'aria-label': `Connection ${edge.label || edge.id}` });
|
|
639
|
+
if (edge.color) group.style.setProperty('--canvas-color', canvasColor(edge.color));
|
|
640
|
+
const path = svgElement('path', { d: geometry.path, class: 'canvas-edge-line' });
|
|
641
|
+
if (edge.fromEnd === 'arrow') path.setAttribute('marker-start', 'url(#canvas-arrow)');
|
|
642
|
+
if (edge.toEnd !== 'none') path.setAttribute('marker-end', 'url(#canvas-arrow)');
|
|
643
|
+
const hit = svgElement('path', { d: geometry.path, class: 'canvas-edge-hit' });
|
|
644
|
+
group.append(path, hit);
|
|
645
|
+
if (edge.label) {
|
|
646
|
+
const text = svgElement('text', { x: geometry.x, y: geometry.y - 10, 'text-anchor': 'middle' });
|
|
647
|
+
text.textContent = edge.label;
|
|
648
|
+
group.append(text);
|
|
649
|
+
}
|
|
650
|
+
const select = () => {
|
|
651
|
+
this.closeTextEditor();
|
|
652
|
+
this.selection.clear();
|
|
653
|
+
this.selectedEdge = edge.id;
|
|
654
|
+
this.syncSelection();
|
|
655
|
+
};
|
|
656
|
+
group.addEventListener('click', select);
|
|
657
|
+
group.addEventListener('dblclick', (event) => { event.stopPropagation(); select(); this.labelInput.focus(); this.labelInput.select(); });
|
|
658
|
+
group.addEventListener('keydown', (event) => {
|
|
659
|
+
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); select(); }
|
|
660
|
+
});
|
|
661
|
+
this.edges.append(group);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
renderInspector() {
|
|
666
|
+
this.inspector.hidden = !this.inspectorOpen || (!this.selection.size && !this.selectedEdge);
|
|
667
|
+
this.propertiesButton.setAttribute('aria-expanded', String(!this.inspector.hidden));
|
|
668
|
+
this.inspector.replaceChildren();
|
|
669
|
+
this.syncInspectorFields = null;
|
|
670
|
+
const node = this.selection.size === 1 ? this.getNode([...this.selection][0]) : null;
|
|
671
|
+
const edge = (this.document.edges ?? []).find((entry) => entry.id === this.selectedEdge);
|
|
672
|
+
const selected = node || edge;
|
|
673
|
+
const header = element('div', 'canvas-inspector-header');
|
|
674
|
+
header.append(element('h2', '', node ? NODE_NAMES[node.type] : (edge ? 'Connection' : 'Canvas')),
|
|
675
|
+
button('Close properties', () => { this.propertiesButton.click(); this.propertiesButton.focus(); }, 'close'));
|
|
676
|
+
this.inspector.append(header);
|
|
677
|
+
if (!selected) {
|
|
678
|
+
this.inspector.append(element('p', '', this.selection.size ? `${this.selection.size} cards selected` : 'Select a card or connection to edit its properties.'));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const currentSelection = () => node ? this.getNode(node.id) : (this.document.edges ?? []).find((entry) => entry.id === edge.id);
|
|
682
|
+
const fields = [];
|
|
683
|
+
this.syncInspectorFields = () => {
|
|
684
|
+
const current = currentSelection();
|
|
685
|
+
if (!current || current.type !== selected.type) { this.renderInspector(); return; }
|
|
686
|
+
fields.forEach(({ input, key }) => {
|
|
687
|
+
if (input === document.activeElement && input.value !== input.dataset.savedValue) return;
|
|
688
|
+
const value = String(current[key] ?? '');
|
|
689
|
+
if (input.value !== value) input.value = value;
|
|
690
|
+
input.dataset.savedValue = input.value;
|
|
691
|
+
});
|
|
692
|
+
};
|
|
693
|
+
const update = (key, value) => {
|
|
694
|
+
if (!this.canEdit) return;
|
|
695
|
+
if (node) this.actions.updateNode(node.id, { [key]: value });
|
|
696
|
+
else this.actions.updateEdge(edge.id, { [key]: value });
|
|
697
|
+
};
|
|
698
|
+
const field = (label, key, { options, numeric = false, optional = false } = {}) => {
|
|
699
|
+
const wrapper = element('label', 'canvas-field');
|
|
700
|
+
wrapper.append(element('span', '', label));
|
|
701
|
+
const input = element(options ? 'select' : 'input');
|
|
702
|
+
input.setAttribute('aria-label', label);
|
|
703
|
+
if (options) options.forEach(([value, text]) => input.append(new Option(text, value)));
|
|
704
|
+
else { input.type = numeric ? 'number' : 'text'; if (numeric) input.step = '1'; }
|
|
705
|
+
input.value = selected[key] ?? '';
|
|
706
|
+
input.dataset.savedValue = input.value;
|
|
707
|
+
fields.push({ input, key });
|
|
708
|
+
input.disabled = !this.canEdit;
|
|
709
|
+
input.addEventListener('change', () => {
|
|
710
|
+
const value = numeric ? Number(input.value) : input.value;
|
|
711
|
+
if (numeric && (!Number.isSafeInteger(value) || ((key === 'width' || key === 'height') && value < 1))) {
|
|
712
|
+
input.value = currentSelection()?.[key] ?? '';
|
|
713
|
+
input.dataset.savedValue = input.value;
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
try { update(key, optional && value === '' ? undefined : value); }
|
|
717
|
+
catch (error) { this.status.textContent = error.message; input.value = currentSelection()?.[key] ?? ''; }
|
|
718
|
+
input.dataset.savedValue = input.value;
|
|
719
|
+
});
|
|
720
|
+
wrapper.append(input);
|
|
721
|
+
this.inspector.append(wrapper);
|
|
722
|
+
};
|
|
723
|
+
if (node) {
|
|
724
|
+
if (node.type === 'file') {
|
|
725
|
+
field('Vault file', 'file');
|
|
726
|
+
field('Heading or block (#...)', 'subpath', { optional: true });
|
|
727
|
+
}
|
|
728
|
+
if (node.type === 'link') field('URL', 'url');
|
|
729
|
+
if (node.type === 'group') {
|
|
730
|
+
field('Label', 'label', { optional: true });
|
|
731
|
+
field('Background image', 'background', { optional: true });
|
|
732
|
+
field('Background style', 'backgroundStyle', { optional: true, options: [['', 'Default'], ['cover', 'Cover'], ['ratio', 'Keep ratio'], ['repeat', 'Repeat']] });
|
|
733
|
+
}
|
|
734
|
+
['x', 'y', 'width', 'height'].forEach((key) => field(key[0].toUpperCase() + key.slice(1), key, { numeric: true }));
|
|
735
|
+
const order = element('div', 'canvas-order');
|
|
736
|
+
['back', 'front'].forEach((direction) => {
|
|
737
|
+
const control = button(`Send to ${direction}`, () => this.actions.reorderNode(node.id, direction));
|
|
738
|
+
control.disabled = !this.canEdit;
|
|
739
|
+
order.append(control);
|
|
740
|
+
});
|
|
741
|
+
this.inspector.append(order);
|
|
742
|
+
} else {
|
|
743
|
+
field('Label', 'label', { optional: true });
|
|
744
|
+
const nodeOptions = (this.document.nodes ?? []).map((entry) => [entry.id, `${NODE_NAMES[entry.type]}: ${(entry.text || entry.label || entry.file || entry.url || entry.id).slice(0, 40)}`]);
|
|
745
|
+
field('From card', 'fromNode', { options: nodeOptions });
|
|
746
|
+
field('To card', 'toNode', { options: nodeOptions });
|
|
747
|
+
['from', 'to'].forEach((end) => {
|
|
748
|
+
field(`${end === 'from' ? 'Start' : 'End'} side`, `${end}Side`, { optional: true,
|
|
749
|
+
options: [['', 'Automatic'], ...['top', 'right', 'bottom', 'left'].map((value) => [value, value])] });
|
|
750
|
+
field(`${end === 'from' ? 'Start' : 'End'} marker`, `${end}End`, { optional: true,
|
|
751
|
+
options: [['', 'Default'], ['none', 'None'], ['arrow', 'Arrow']] });
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
field('Color (1–6 or #RRGGBB)', 'color', { optional: true });
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
startNodeDrag(event, id, resize = false) {
|
|
758
|
+
if (event.button !== 0 || this.spaceHeld || (!resize && event.target.closest(
|
|
759
|
+
'a, button, input, textarea, select, summary, img, audio, video, [contenteditable="true"], .canvas-text-editor, .diagram-preview-shell, .drawio-embed, .excalidraw-embed',
|
|
760
|
+
))) return;
|
|
761
|
+
event.preventDefault();
|
|
762
|
+
event.stopPropagation();
|
|
763
|
+
if (event.shiftKey || !this.selection.has(id)) this.select(id, event.shiftKey);
|
|
764
|
+
if (!this.canEdit || !this.selection.has(id)) return;
|
|
765
|
+
this.closeTextEditor();
|
|
766
|
+
const origin = { x: event.clientX, y: event.clientY };
|
|
767
|
+
const selected = [...this.selection].map((key) => this.getNode(key)).filter(Boolean);
|
|
768
|
+
const moving = resize ? [this.getNode(id)] : [...selected];
|
|
769
|
+
// A group's geometric containment controls group movement; no parent IDs go into the file.
|
|
770
|
+
if (!resize) (this.document.nodes ?? []).forEach((node) => {
|
|
771
|
+
if (moving.some((entry) => entry.id === node.id)) return;
|
|
772
|
+
if (selected.some((group) => group.type === 'group' && node.x >= group.x && node.y >= group.y
|
|
773
|
+
&& node.x + node.width <= group.x + group.width && node.y + node.height <= group.y + group.height)) moving.push(node);
|
|
774
|
+
});
|
|
775
|
+
let moved = false;
|
|
776
|
+
const move = (next) => {
|
|
777
|
+
if (!moved && Math.hypot(next.clientX - origin.x, next.clientY - origin.y) < 4) return;
|
|
778
|
+
moved = true;
|
|
779
|
+
const dx = Math.round((next.clientX - origin.x) / this.viewport.zoom);
|
|
780
|
+
const dy = Math.round((next.clientY - origin.y) / this.viewport.zoom);
|
|
781
|
+
const patches = moving.map((node) => ({ id: node.id, patch: resize
|
|
782
|
+
? { width: Math.max(80, node.width + dx), height: Math.max(64, node.height + dy) }
|
|
783
|
+
: { x: node.x + dx, y: node.y + dy } }));
|
|
784
|
+
// Commit property deltas while dragging so collaborators see movement immediately.
|
|
785
|
+
this.actions.updateNodes(patches.filter(({ id: nodeId }) => this.getNode(nodeId)), false);
|
|
786
|
+
};
|
|
787
|
+
this.actions.stopUndoCapture();
|
|
788
|
+
this.trackPointer(event, move, () => {
|
|
789
|
+
if (!moved && !event.shiftKey) this.select(id);
|
|
790
|
+
this.actions.stopUndoCapture();
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
trackPointer(event, move, finish) {
|
|
795
|
+
this.cancelGesture?.();
|
|
796
|
+
this.stage.focus({ preventScroll: true });
|
|
797
|
+
const target = event.currentTarget;
|
|
798
|
+
const end = (next, cancelled = false) => {
|
|
799
|
+
target.removeEventListener('pointermove', move);
|
|
800
|
+
target.removeEventListener('pointerup', end);
|
|
801
|
+
target.removeEventListener('pointercancel', cancel);
|
|
802
|
+
target.removeEventListener('lostpointercapture', cancel);
|
|
803
|
+
this.cancelGesture = null;
|
|
804
|
+
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
|
805
|
+
finish(next, cancelled);
|
|
806
|
+
};
|
|
807
|
+
const cancel = (next = event) => end(next, true);
|
|
808
|
+
this.cancelGesture = cancel;
|
|
809
|
+
target.setPointerCapture(event.pointerId);
|
|
810
|
+
target.addEventListener('pointermove', move);
|
|
811
|
+
target.addEventListener('pointerup', end);
|
|
812
|
+
target.addEventListener('pointercancel', cancel);
|
|
813
|
+
target.addEventListener('lostpointercapture', cancel);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
startConnection(event, id, side) {
|
|
817
|
+
if (event.button !== 0 || !this.canEdit || this.spaceHeld) return;
|
|
818
|
+
event.preventDefault();
|
|
819
|
+
event.stopPropagation();
|
|
820
|
+
this.select(id);
|
|
821
|
+
this.connectFrom = null;
|
|
822
|
+
const overlay = svgElement('svg', { class: 'canvas-connection-preview' });
|
|
823
|
+
const line = svgElement('path', { class: 'canvas-edge-line' });
|
|
824
|
+
overlay.append(line);
|
|
825
|
+
this.world.append(overlay);
|
|
826
|
+
const endpointId = Symbol();
|
|
827
|
+
const pointFor = (next) => {
|
|
828
|
+
const bounds = this.stage.getBoundingClientRect();
|
|
829
|
+
return this.toWorld(next.clientX - bounds.left, next.clientY - bounds.top);
|
|
830
|
+
};
|
|
831
|
+
const targetFor = (next) => document.elementsFromPoint(next.clientX, next.clientY)
|
|
832
|
+
.map((element) => element.closest('.canvas-card')).find((element) => element && element.dataset.nodeId !== id);
|
|
833
|
+
const move = (next) => {
|
|
834
|
+
const source = this.getNode(id);
|
|
835
|
+
if (!source) { this.cancelGesture?.(); return; }
|
|
836
|
+
const point = pointFor(next);
|
|
837
|
+
const target = this.getNode(targetFor(next)?.dataset.nodeId) || { ...point, width: 0, height: 0 };
|
|
838
|
+
const geometry = canvasEdgeGeometry({ fromNode: id, fromSide: side, toNode: endpointId, toSide: canvasSideAtPoint(target, point) },
|
|
839
|
+
new Map([[id, source], [endpointId, target]]));
|
|
840
|
+
line.setAttribute('d', geometry.path);
|
|
841
|
+
};
|
|
842
|
+
this.trackPointer(event, move, (next, cancelled) => {
|
|
843
|
+
overlay.remove();
|
|
844
|
+
const target = this.getNode(targetFor(next)?.dataset.nodeId);
|
|
845
|
+
if (!cancelled && this.canEdit && this.getNode(id) && target) {
|
|
846
|
+
this.actions.addEdge({ fromNode: id, fromSide: side, toNode: target.id, toSide: canvasSideAtPoint(target, pointFor(next)) });
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
startPan(event) {
|
|
852
|
+
if (event.defaultPrevented || event.target.closest('.canvas-selection-toolbar, .canvas-navigation, .canvas-toolbar, .canvas-text-editor')) return;
|
|
853
|
+
const onCard = event.target.closest('.canvas-card, .canvas-edge');
|
|
854
|
+
const bounds = this.stage.getBoundingClientRect();
|
|
855
|
+
const group = !onCard && this.groupAtPoint(this.toWorld(event.clientX - bounds.left, event.clientY - bounds.top));
|
|
856
|
+
const pan = this.spaceHeld || event.button === 1 || (event.pointerType === 'touch' && !onCard && (!group || !this.canEdit));
|
|
857
|
+
if (!pan && onCard) return;
|
|
858
|
+
if (event.button !== 0 && event.button !== 1) return;
|
|
859
|
+
// Group surfaces stay transparent to links and connections; only blank-space gestures reach here.
|
|
860
|
+
if (group && !pan && !event.shiftKey) { this.startNodeDrag(event, group.id); return; }
|
|
861
|
+
event.preventDefault();
|
|
862
|
+
this.closeTextEditor();
|
|
863
|
+
this.stage.focus();
|
|
864
|
+
const initial = { ...this.viewport };
|
|
865
|
+
const client = { x: event.clientX, y: event.clientY };
|
|
866
|
+
const originalSelection = new Set(event.shiftKey || pan ? this.selection : []);
|
|
867
|
+
const marquee = element('div', 'canvas-marquee');
|
|
868
|
+
if (!pan) {
|
|
869
|
+
this.selection = new Set(originalSelection);
|
|
870
|
+
this.selectedEdge = null;
|
|
871
|
+
this.syncSelection();
|
|
872
|
+
this.stage.append(marquee);
|
|
873
|
+
}
|
|
874
|
+
const move = (next) => {
|
|
875
|
+
if (pan) {
|
|
876
|
+
this.viewport.x = initial.x + next.clientX - client.x;
|
|
877
|
+
this.viewport.y = initial.y + next.clientY - client.y;
|
|
878
|
+
this.applyViewport();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const bounds = this.stage.getBoundingClientRect();
|
|
882
|
+
const left = Math.min(client.x, next.clientX) - bounds.left;
|
|
883
|
+
const top = Math.min(client.y, next.clientY) - bounds.top;
|
|
884
|
+
const width = Math.abs(next.clientX - client.x);
|
|
885
|
+
const height = Math.abs(next.clientY - client.y);
|
|
886
|
+
Object.assign(marquee.style, { left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px` });
|
|
887
|
+
const from = this.toWorld(left, top);
|
|
888
|
+
const to = this.toWorld(left + width, top + height);
|
|
889
|
+
this.selection = new Set(originalSelection);
|
|
890
|
+
for (const node of this.document.nodes ?? []) {
|
|
891
|
+
if (node.type === 'group' && (node.x < from.x || node.y < from.y
|
|
892
|
+
|| node.x + node.width > to.x || node.y + node.height > to.y)) continue;
|
|
893
|
+
if (node.x < to.x && node.x + node.width > from.x && node.y < to.y && node.y + node.height > from.y) this.selection.add(node.id);
|
|
894
|
+
}
|
|
895
|
+
this.syncSelection();
|
|
896
|
+
};
|
|
897
|
+
this.trackPointer(event, move, () => marquee.remove());
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
toWorld(x, y) { return { x: (x - this.viewport.x) / this.viewport.zoom, y: (y - this.viewport.y) / this.viewport.zoom }; }
|
|
901
|
+
|
|
902
|
+
groupAtPoint(point) {
|
|
903
|
+
return (this.document.nodes ?? []).findLast((node) => node.type === 'group'
|
|
904
|
+
&& point.x >= node.x && point.x <= node.x + node.width
|
|
905
|
+
&& point.y >= node.y && point.y <= node.y + node.height);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
zoomBy(factor, clientX, clientY) {
|
|
909
|
+
const bounds = this.stage.getBoundingClientRect();
|
|
910
|
+
const x = clientX === undefined ? bounds.width / 2 : clientX - bounds.left;
|
|
911
|
+
const y = clientY === undefined ? bounds.height / 2 : clientY - bounds.top;
|
|
912
|
+
const center = this.toWorld(x, y);
|
|
913
|
+
this.viewport.zoom = Math.max(0.1, Math.min(3, this.viewport.zoom * factor));
|
|
914
|
+
this.viewport.x = x - center.x * this.viewport.zoom;
|
|
915
|
+
this.viewport.y = y - center.y * this.viewport.zoom;
|
|
916
|
+
this.applyViewport();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
fit(ids = null) {
|
|
920
|
+
const nodes = (this.document.nodes ?? []).filter((node) => !ids || ids.includes(node.id));
|
|
921
|
+
if (!nodes.length) { this.viewport = { x: 80, y: 80, zoom: 1 }; this.applyViewport(); return; }
|
|
922
|
+
const left = Math.min(...nodes.map((node) => node.x));
|
|
923
|
+
const top = Math.min(...nodes.map((node) => node.y));
|
|
924
|
+
const width = Math.max(...nodes.map((node) => node.x + node.width)) - left;
|
|
925
|
+
const height = Math.max(...nodes.map((node) => node.y + node.height)) - top;
|
|
926
|
+
const zoom = Math.max(0.1, Math.min(1, (this.stage.clientWidth - 100) / Math.max(1, width), (this.stage.clientHeight - 100) / Math.max(1, height)));
|
|
927
|
+
this.viewport = { zoom, x: (this.stage.clientWidth - width * zoom) / 2 - left * zoom,
|
|
928
|
+
y: (this.stage.clientHeight - height * zoom) / 2 - top * zoom };
|
|
929
|
+
this.applyViewport();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
applyViewport() {
|
|
933
|
+
this.world.style.transform = `translate(${this.viewport.x}px, ${this.viewport.y}px) scale(${this.viewport.zoom})`;
|
|
934
|
+
this.stage.style.backgroundPosition = `${this.viewport.x}px ${this.viewport.y}px`;
|
|
935
|
+
this.stage.style.backgroundSize = `${24 * this.viewport.zoom}px ${24 * this.viewport.zoom}px`;
|
|
936
|
+
this.stage.style.setProperty('--canvas-grid-dot', `${0.8 * this.viewport.zoom}px`);
|
|
937
|
+
this.zoomLabel.textContent = `${Math.round(this.viewport.zoom * 100)}%`;
|
|
938
|
+
this.positionSelectionToolbar();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
publishPointer(event) {
|
|
942
|
+
const now = performance.now();
|
|
943
|
+
if (now - (this.lastPointerAt || 0) < 50) return;
|
|
944
|
+
this.lastPointerAt = now;
|
|
945
|
+
const bounds = this.stage.getBoundingClientRect();
|
|
946
|
+
const point = this.toWorld(event.clientX - bounds.left, event.clientY - bounds.top);
|
|
947
|
+
this.stage.classList.toggle('is-over-group', event.target === this.stage && !this.spaceHeld && !event.shiftKey && Boolean(this.groupAtPoint(point)));
|
|
948
|
+
this.actions.presence('pointer', point);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
renderPresence(users) {
|
|
952
|
+
this.users = users;
|
|
953
|
+
this.cursors.replaceChildren();
|
|
954
|
+
this.nodes.forEach(({ card }) => { card.classList.remove('is-remote-selected'); });
|
|
955
|
+
users.filter((user) => !user.isLocal).forEach((user) => {
|
|
956
|
+
(Array.isArray(user.selectedNodeIds) ? user.selectedNodeIds : []).forEach((id) => this.nodes.get(id)?.card.classList.add('is-remote-selected'));
|
|
957
|
+
const { x, y } = user.pointer ?? {};
|
|
958
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
959
|
+
const cursor = element('span', 'canvas-cursor', `↖ ${String(user.name || 'Collaborator').slice(0, 32)}`);
|
|
960
|
+
cursor.style.left = `${x}px`;
|
|
961
|
+
cursor.style.top = `${y}px`;
|
|
962
|
+
cursor.style.setProperty('--canvas-color', canvasColor(user.color));
|
|
963
|
+
this.cursors.append(cursor);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
onKeyDown(event) {
|
|
968
|
+
if (event.key === 'Escape') {
|
|
969
|
+
const menu = [this.colorMenu, this.helpMenu].find((entry) => entry.open);
|
|
970
|
+
if (menu) { event.preventDefault(); menu.open = false; menu.querySelector('summary').focus(); return; }
|
|
971
|
+
}
|
|
972
|
+
if (event.defaultPrevented || event.target.closest('input, textarea, select, .diagram-preview-shell, [contenteditable="true"]')) return;
|
|
973
|
+
if (event.key === 'Escape') {
|
|
974
|
+
this.cancelGesture?.();
|
|
975
|
+
this.connectFrom = null;
|
|
976
|
+
this.help.textContent = '';
|
|
977
|
+
this.closeTextEditor();
|
|
978
|
+
this.selection.clear();
|
|
979
|
+
this.selectedEdge = null;
|
|
980
|
+
this.syncSelection();
|
|
981
|
+
this.colorMenu.open = false;
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (event.target.closest('a, button, summary, .canvas-help-menu')) return;
|
|
985
|
+
if (event.code === 'Space' && event.target === this.stage) { event.preventDefault(); this.spaceHeld = true; }
|
|
986
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
|
|
987
|
+
event.preventDefault();
|
|
988
|
+
this.selection = new Set((this.document.nodes ?? []).map((node) => node.id));
|
|
989
|
+
this.selectedEdge = null;
|
|
990
|
+
this.syncSelection();
|
|
991
|
+
}
|
|
992
|
+
if (event.shiftKey && event.code === 'Digit1') { event.preventDefault(); this.fit(); }
|
|
993
|
+
if (event.shiftKey && event.code === 'Digit2') { event.preventDefault(); this.fit([...this.selection]); }
|
|
994
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
|
|
995
|
+
event.preventDefault();
|
|
996
|
+
if (event.shiftKey) this.actions.redo(); else this.actions.undo();
|
|
997
|
+
}
|
|
998
|
+
if (event.key === 'Delete' || event.key === 'Backspace') { event.preventDefault(); this.removeSelection(); }
|
|
999
|
+
if (!this.canEdit) return;
|
|
1000
|
+
const delta = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[event.key];
|
|
1001
|
+
if (delta && this.selection.size) {
|
|
1002
|
+
event.preventDefault();
|
|
1003
|
+
const step = event.shiftKey ? 10 : 1;
|
|
1004
|
+
this.actions.updateNodes([...this.selection].map((id) => {
|
|
1005
|
+
const node = this.getNode(id);
|
|
1006
|
+
return { id, patch: { x: node.x + delta[0] * step, y: node.y + delta[1] * step } };
|
|
1007
|
+
}));
|
|
1008
|
+
}
|
|
1009
|
+
if (event.key === 'Enter' && this.selection.size === 1) {
|
|
1010
|
+
const id = [...this.selection][0];
|
|
1011
|
+
if (this.getNode(id)?.type === 'text') { event.preventDefault(); this.openTextEditor(id); }
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
applyTheme(theme) {
|
|
1016
|
+
this.nodes.forEach((entry) => entry.preview?.applyTheme(theme));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
destroy() {
|
|
1020
|
+
this.cancelGesture?.();
|
|
1021
|
+
this.picker?.close();
|
|
1022
|
+
this.resizeObserver.disconnect();
|
|
1023
|
+
this.abort.abort();
|
|
1024
|
+
this.closeTextEditor();
|
|
1025
|
+
this.nodes.forEach((entry) => entry.preview?.destroy());
|
|
1026
|
+
this.root.replaceChildren();
|
|
1027
|
+
}
|
|
1028
|
+
}
|