From 22f916e42177dfd0b56acf0cf4fa554dddb8209a Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:53:47 -0700 Subject: [PATCH] feat: add rich markdown editor mode (#264) * feat: add rich markdown editor mode * fix: preserve rich search navigation focus --- package.json | 9 + pnpm-lock.yaml | 710 +++++++++++++++++- src/renderer/src/assets/main.css | 288 +++++++ .../src/components/editor/EditorContent.tsx | 29 +- .../src/components/editor/EditorPanel.tsx | 4 +- .../components/editor/MarkdownViewToggle.tsx | 6 +- .../components/editor/RichMarkdownEditor.tsx | 399 ++++++++++ .../editor/RichMarkdownSearchBar.tsx | 112 +++ .../editor/markdown-rich-mode.test.ts | 39 + .../components/editor/markdown-rich-mode.ts | 87 +++ .../editor/rich-markdown-commands.tsx | 142 ++++ .../components/editor/rich-markdown-search.ts | 116 +++ .../editor/useRichMarkdownSearch.ts | 194 +++++ src/renderer/src/store/slices/editor.test.ts | 4 +- src/renderer/src/store/slices/editor.ts | 2 +- 15 files changed, 2089 insertions(+), 52 deletions(-) create mode 100644 src/renderer/src/components/editor/RichMarkdownEditor.tsx create mode 100644 src/renderer/src/components/editor/RichMarkdownSearchBar.tsx create mode 100644 src/renderer/src/components/editor/markdown-rich-mode.test.ts create mode 100644 src/renderer/src/components/editor/markdown-rich-mode.ts create mode 100644 src/renderer/src/components/editor/rich-markdown-commands.tsx create mode 100644 src/renderer/src/components/editor/rich-markdown-search.ts create mode 100644 src/renderer/src/components/editor/useRichMarkdownSearch.ts diff --git a/package.json b/package.json index 227ecdae55d..e73c4edc593 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,15 @@ "@electron-toolkit/utils": "^4.0.0", "@monaco-editor/react": "^4.7.0", "@tanstack/react-virtual": "^3.13.23", + "@tiptap/extension-image": "^3.22.1", + "@tiptap/extension-link": "^3.22.1", + "@tiptap/extension-placeholder": "^3.22.1", + "@tiptap/extension-task-item": "^3.22.1", + "@tiptap/extension-task-list": "^3.22.1", + "@tiptap/markdown": "^3.22.1", + "@tiptap/pm": "^3.22.1", + "@tiptap/react": "^3.22.1", + "@tiptap/starter-kit": "^3.22.1", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-search": "^0.16.0", "@xterm/addon-serialize": "^0.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa10e4130bf..889716fd648 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,33 @@ importers: '@tanstack/react-virtual': specifier: ^3.13.23 version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/extension-image': + specifier: ^3.22.1 + version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-link': + specifier: ^3.22.1 + version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-placeholder': + specifier: ^3.22.1 + version: 3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-task-item': + specifier: ^3.22.1 + version: 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-task-list': + specifier: ^3.22.1 + version: 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/markdown': + specifier: ^3.22.1 + version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/pm': + specifier: ^3.22.1 + version: 3.22.1 + '@tiptap/react': + specifier: ^3.22.1 + version: 3.22.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/starter-kit': + specifier: ^3.22.1 + version: 3.22.1 '@xterm/addon-fit': specifier: ^0.11.0 version: 0.11.0 @@ -934,56 +961,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.41.0': resolution: {integrity: sha512-VfVZxL0+6RU86T8F8vKiDBa+iHsr8PAjQmKGBzSCAX70b6x+UOMFl+2dNihmKmUwqkCazCPfYjt6SuAPOeQJ3g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.41.0': resolution: {integrity: sha512-bwzokz2eGvdfJbc0i+zXMJ4BBjQPqg13jyWpEEZDOrBCQ91r8KeY2Mi2kUeuMTZNFXju+jcAbAbpyJxRGla0eg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.41.0': resolution: {integrity: sha512-POLM//PCH9uqDeNDwWL3b3DkMmI3oI2cU6hwc2lnztD1o7dzrQs3R9nq555BZ6wI7t2lyhT9CS+CRaz5X0XqLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.41.0': resolution: {integrity: sha512-NNK7PzhFqLUwx/G12Xtm6scGv7UITvyGdAR5Y+TlqsG+essnuRWR4jRNODWRjzLZod0T3SayRbnkSIWMBov33w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.41.0': resolution: {integrity: sha512-qVf/zDC5cN9eKe4qI/O/m445er1IRl6swsSl7jHkqmOSVfknwCe5JXitYjZca+V/cNJSU/xPlC5EFMabMMFDpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.41.0': resolution: {integrity: sha512-ojxYWu7vUb6ysYqVCPHuAPVZHAI40gfZ0PDtZAMwVmh2f0V8ExpPIKoAKr7/8sNbAXJBBpZhs2coypIo2jJX4w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.41.0': resolution: {integrity: sha512-O2exZLBxoCMIv2vlvcbkdedazJPTdG0VSup+0QUCfYQtx751zCZNboX2ZUOiQ/gDTdhtXvSiot0h6GEGkOyalA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxfmt/binding-openharmony-arm64@0.41.0': resolution: {integrity: sha512-N+31/VoL+z+NNBt8viy3I4NaIdPbiYeOnB884LKqvXldaE2dRztdPv3q5ipfZYv0RwFp7JfqS4I27K/DSHCakg==} @@ -1056,56 +1075,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.56.0': resolution: {integrity: sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.56.0': resolution: {integrity: sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.56.0': resolution: {integrity: sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.56.0': resolution: {integrity: sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.56.0': resolution: {integrity: sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.56.0': resolution: {integrity: sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxlint/binding-linux-x64-musl@1.56.0': resolution: {integrity: sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxlint/binding-openharmony-arm64@1.56.0': resolution: {integrity: sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==} @@ -1829,6 +1840,9 @@ packages: resolution: {integrity: sha512-IE4bTeH0mCq0FBRaYRUtdiIfvO7NCv14lHS4TiTY8YNG5tPYwHnLZZtniud0ElmLIWGP95Kk6E7A6J2uDmOY+Q==} hasBin: true + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -1866,79 +1880,66 @@ packages: resolution: {integrity: sha512-KUizqxpwaR2AZdAUsMWfL/C94pUu7TKpoPd88c8yFVixJ+l9hejkrwoK5Zj3wiNh65UeyryKnJyxL1b7yNqFQA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.1': resolution: {integrity: sha512-MZoQ/am77ckJtZGFAtPucgUuJWiop3m2R3lw7tC0QCcbfl4DRhQUBUkHWCkcrT3pqy5Mzv5QQgY6Dmlba6iTWg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.1': resolution: {integrity: sha512-Sez95TP6xGjkWB1608EfhCX1gdGrO5wzyN99VqzRtC17x/1bhw5VU1V0GfKUwbW/Xr1J8mSasoFoJa6Y7aGGSA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.1': resolution: {integrity: sha512-9Cs2Seq98LWNOJzR89EGTZoiP8EkZ9UbQhBlDgfAkM6asVna1xJ04W2CLYWDN/RpUgOjtQvcv8wQVi1t5oQazA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.1': resolution: {integrity: sha512-n9yqttftgFy7IrNEnHy1bOp6B4OSe8mJDiPkT7EqlM9FnKOwUMnCK62ixW0Kd9Clw0/wgvh8+SqaDXMFvw3KqQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.1': resolution: {integrity: sha512-SfpNXDzVTqs/riak4xXcLpq5gIQWsqGWMhN1AGRQKB4qGSs4r0sEs3ervXPcE1O9RsQ5bm8Muz6zmQpQnPss1g==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.1': resolution: {integrity: sha512-LjaChED0wQnjKZU+tsmGbN+9nN1XhaWUkAlSbTdhpEseCS4a15f/Q8xC2BN4GDKRzhhLZpYtJBZr2NZhR0jvNw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.1': resolution: {integrity: sha512-ojW7iTJSIs4pwB2xV6QXGwNyDctvXOivYllttuPbXguuKDX5vwpqYJsHc6D2LZzjDGHML414Tuj3LvVPe1CT1A==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.1': resolution: {integrity: sha512-FP+Q6WTcxxvsr0wQczhSE+tOZvFPV8A/mUE6mhZYFW9/eea/y/XqAgRoLLMuE9Cz0hfX5bi7p116IWoB+P237A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.1': resolution: {integrity: sha512-L1uD9b/Ig8Z+rn1KttCJjwhN1FgjRMBKsPaBsDKkfUl7GfFq71pU4vWCnpOsGljycFEbkHWARZLf4lMYg3WOLw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.1': resolution: {integrity: sha512-EZc9NGTk/oSUzzOD4nYY4gIjteo2M3CiozX6t1IXGCOdgxJTlVu/7EdPeiqeHPSIrxkLhavqpBAUCfvC6vBOug==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.1': resolution: {integrity: sha512-NQ9KyU1Anuy59L8+HHOKM++CoUxrQWrZWXRik4BJFm+7i5NP6q/SW43xIBr80zzt+PDBJ7LeNmloQGfa0JGk0w==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.1': resolution: {integrity: sha512-GZkLk2t6naywsveSFBsEb0PLU+JC9ggVjbndsbG20VPhar6D1gkMfCx4NfP9owpovBXTN+eRdqGSkDGIxPHhmQ==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.1': resolution: {integrity: sha512-1hjG9Jpl2KDOetr64iQd8AZAEjkDUUK5RbDkYWsViYLC1op1oNzdjMJeFiofcGhqbNTaY2kfgqowE7DILifsrA==} @@ -2026,28 +2027,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -2091,6 +2088,181 @@ packages: '@tanstack/virtual-core@3.13.23': resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + '@tiptap/core@3.22.1': + resolution: {integrity: sha512-6wPNhkdLIGYiKAGqepDCRtR0TYGJxV40SwOEN2vlPhsXqAgzmyG37UyREj5pGH5xTekugqMCgCnyRg7m5nYoYQ==} + peerDependencies: + '@tiptap/pm': ^3.22.1 + + '@tiptap/extension-blockquote@3.22.1': + resolution: {integrity: sha512-omPsJ/IMAZYhXqOaEenYE+HA9U2zju5rQbAn6Xktynvr4A5P95jqkgAwncXB82pCkNYU/uYxi51vyTweTeEUHA==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-bold@3.22.1': + resolution: {integrity: sha512-0+q6Apu1Vx2+ReB2ktTpBrQ5/dCvGzTkJCy+MZ/t8WBcybqFXOKYRCr/i/VGPDpXZttxpk0EPl0+ao+NVcUTAA==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-bubble-menu@3.22.1': + resolution: {integrity: sha512-JJI63N55hLPjfqHgBnbG1ORZTXJiswnfBkfNd8YKytCC8D++g5qX3UMObxmJKLMBRGyqjEi6krzOyYtOix5ALA==} + peerDependencies: + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + + '@tiptap/extension-bullet-list@3.22.1': + resolution: {integrity: sha512-83L+4N2JziWORbWtlsM0xBm3LOKIw4YtIm+Kh4amV5kGvIgIL5I1KYzoxv20qjgFX2k08LtLMwPdvPSPSh4e7g==} + peerDependencies: + '@tiptap/extension-list': ^3.22.1 + + '@tiptap/extension-code-block@3.22.1': + resolution: {integrity: sha512-fr3b1seFsAeYHtPAb9fbATkGcgyfStD05GHsZXFLh7yCpf2ejWLNxdWJT/g+FggSEHYFKCXT06aixk0WbtRcWw==} + peerDependencies: + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + + '@tiptap/extension-code@3.22.1': + resolution: {integrity: sha512-Ze+hjSLLCn+5gVpuE/Uv7mQ83AlG5A9OPsuDoyzTpJ2XNvZP2iZdwQMGqwXKC8eH7fIOJN6XQ3IDv/EhltQx/Q==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-document@3.22.1': + resolution: {integrity: sha512-fBI/+PGtK6pzitqjSSSYL2+uZglX6T53zb5nLEmN/q8q7FzUuUpglp8toHVhBG05WDk4vx6Z7bC95uyxkYdoAA==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-dropcursor@3.22.1': + resolution: {integrity: sha512-PuSNoTROZB564KpTG9ExVB3CsfRa0ridHx+1sWZajOBVZJiXSn4QlS/ShS509SOx8z17DyxEw06IH//OHY9XyQ==} + peerDependencies: + '@tiptap/extensions': ^3.22.1 + + '@tiptap/extension-floating-menu@3.22.1': + resolution: {integrity: sha512-TaZqmaoKv36FzbKTrBkkv74o0t8dYTftNZ7NotBqfSki0BB2PupTCJHafdu1YI0zmJ3xEzjB/XKcKPz2+10sDA==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + + '@tiptap/extension-gapcursor@3.22.1': + resolution: {integrity: sha512-qqsyy7unWM3elv+7ru+6paKAnw1PZTvjNVQu3UzB6d556Gx2uE4isXJNdBaslBZdp2EoaYdIkhhEccW9B/Nwqg==} + peerDependencies: + '@tiptap/extensions': ^3.22.1 + + '@tiptap/extension-hard-break@3.22.1': + resolution: {integrity: sha512-hzLwLEZVbZODa9q5UiCQpOUmDnyxN19FA4LhlqLP0/JSHewP/aol5igFZwuw0XVFp425BuzPjrB7tmr0GRTDWw==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-heading@3.22.1': + resolution: {integrity: sha512-EaIihzrOfXUHQlL6fFyJCkDrjgg0e/eD4jpkjhKpeuJDcqf7eJ1c0E2zcNRAiZkeXdN/hTQFaXKsSyNUE7T7Sg==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-horizontal-rule@3.22.1': + resolution: {integrity: sha512-Q18A8IN+gnfptIksPeVAI6oOBGYKAGf+QN0FEJ5OXO4BEAmA3hflflA1rWNfPC4aQNry/N7sAl8Gpd6HuIbz2w==} + peerDependencies: + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + + '@tiptap/extension-image@3.22.1': + resolution: {integrity: sha512-FtZCOWyyaEvSfaOPoH78IKb1BlG/Vao4PARdlrVCD1FlV1YGLAgSW5YkQAJ/vPTLwyNNZtqryaBpZrA8Wm25nQ==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-italic@3.22.1': + resolution: {integrity: sha512-EXPZWEsWJK9tUMypddOBvayaBeu8wFV2uH5PNrtDKrfRZ1Bf8GQ3lfcO0blHssaQ9nWqa9HwBC1mdfWcmfpxig==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-link@3.22.1': + resolution: {integrity: sha512-RHch/Bqv+QDvW3J1CXmiTB54pyrQYNQq8Vfa7is/O209dNPA8tdbkRP44rDjqn8NeDCriC/oJ4avWeXL4qNDVw==} + peerDependencies: + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + + '@tiptap/extension-list-item@3.22.1': + resolution: {integrity: sha512-v0FgSX3cqLY3L1hIe2PFRTR3/+wlFOdFjv0p3fSJ5Tl7cgU7DR1OcljFqpw0exePcmt6dXqXVQua3PxSVV15eA==} + peerDependencies: + '@tiptap/extension-list': ^3.22.1 + + '@tiptap/extension-list-keymap@3.22.1': + resolution: {integrity: sha512-00Nz4jJygYGJg6N1mdbQUslFG9QaGZq5P9MFwqoduWku7gYHWkZoZvrkxZrYtxGTHVIlLnF8LIfblAlOwNd76g==} + peerDependencies: + '@tiptap/extension-list': ^3.22.1 + + '@tiptap/extension-list@3.22.1': + resolution: {integrity: sha512-6bVI5A12sFeyb0EngABV8/qCtC2IgiDbWC8mtNNLh5dAVGaUKo1KucL6vRYDhzXhyO/eHuGYepXZDLOOdS9LIQ==} + peerDependencies: + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + + '@tiptap/extension-ordered-list@3.22.1': + resolution: {integrity: sha512-sbd99ZUa1lIemH7N6dLB+9aYxUgduwW2216VM3dLJBS9hmTA4iDRxWx0a1ApnAVv+sZasRSbb/wpYLtXviA1XQ==} + peerDependencies: + '@tiptap/extension-list': ^3.22.1 + + '@tiptap/extension-paragraph@3.22.1': + resolution: {integrity: sha512-mnvGEZfZFysHGvmEqrSLjeddaNPB3UmomTInv9gsImw8hlB4/gQedvB6Qf2tFfIjl4ISKC5AbFxraSnJfjaL5g==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-placeholder@3.22.1': + resolution: {integrity: sha512-f8NJNEJTDuT9UIZdVIAPoySgzQ/nKxR/gWRqCnwtR4O26zo/JdKI2XvrTE/iNrV3Khme8rjCtO7/8CQgTeMMxA==} + peerDependencies: + '@tiptap/extensions': ^3.22.1 + + '@tiptap/extension-strike@3.22.1': + resolution: {integrity: sha512-LTdnGmglK1f/AW//36k+Km8URA1wrTLENi3R5N+/ipv+yP2rZ2Ki1R1m6yJx3KSFzR55c91xE6659/vz1uZ6iA==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-task-item@3.22.1': + resolution: {integrity: sha512-63/Gx4HyLVC89RbEBbn+C/IZBiMsLZqazx1fqOpE/Ife3Nr09qb8fLQITRGnzS9JnRjrO4ErFgBQF8WSsCB5qg==} + peerDependencies: + '@tiptap/extension-list': ^3.22.1 + + '@tiptap/extension-task-list@3.22.1': + resolution: {integrity: sha512-4RCDf8hOCkAah+wR+QKZEX/AcZ1a0LsDInDjevpuEt23Yqb4pcWAvnqF6S/INvuy6nQaDkm2e5G+8rZq3I1RTA==} + peerDependencies: + '@tiptap/extension-list': ^3.22.1 + + '@tiptap/extension-text@3.22.1': + resolution: {integrity: sha512-wFCNCATSTTFhvA9wOPkAgzPVyG3RM6+jOlDeRhHUCHsFWFWj0w9ZPwA/nP+Qi5hEW7kGG9V8o62RjBdHNvK2PQ==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extension-underline@3.22.1': + resolution: {integrity: sha512-p8/ErqQInWJbpncBycIggmtCjdrMwHmA3GNhOugo6F4fYfeVxgy7pVb7ZF+ss62d0mpQvEd81pyrzhkBtb0nBg==} + peerDependencies: + '@tiptap/core': ^3.22.1 + + '@tiptap/extensions@3.22.1': + resolution: {integrity: sha512-BKpp371Pl1CVcLRLrWH7PC1I+IsXOhet80+pILqCMlwkJnsVtOOVRr5uCF6rbPP4xK5H/ehkQWmxA8rqpv42aA==} + peerDependencies: + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + + '@tiptap/markdown@3.22.1': + resolution: {integrity: sha512-0w4d6HRKeIsUlemxsxzgdiCURTGJhONrNFyL777zZIgCAbDsTKrUeI+2WNdRJBOIiNdpQiZzUL36vm2JiIDZqw==} + peerDependencies: + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + + '@tiptap/pm@3.22.1': + resolution: {integrity: sha512-OSqSg2974eLJT5PNKFLM7156lBXCUf/dsKTQXWSzsLTf6HOP4dYP6c0YbAk6lgbNI+BdszsHNClmLVLA8H/L9A==} + + '@tiptap/react@3.22.1': + resolution: {integrity: sha512-1pIRfgK9wape4nDXVJRfgUcYVZdPPkuECbGtz8bo0rgtdsVN7B8PBVCDyuitZ7acdLbMuuX5+TxeUOvME8np7Q==} + peerDependencies: + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.22.1': + resolution: {integrity: sha512-1fFmURkgofxgP9GW993bSpxf2rIJzQbWZ9rPw17qbAVuGouIArG+Fd/A1WUD95Vdbx6JIrc1QxbNlLs7bhcoPA==} + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -2136,9 +2308,18 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -2179,6 +2360,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} @@ -2626,6 +2810,9 @@ packages: crc@3.8.0: resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-dirname@0.1.0: resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} @@ -2835,6 +3022,10 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2962,6 +3153,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3504,28 +3699,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -3546,6 +3737,12 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + lint-staged@16.4.0: resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} engines: {node: '>=20.17'} @@ -3613,6 +3810,10 @@ packages: resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==} engines: {node: ^18.17.0 || >=20.5.0} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -3621,6 +3822,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@17.0.5: + resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==} + engines: {node: '>= 20'} + hasBin: true + matcher@3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} @@ -3677,6 +3883,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -3999,6 +4208,9 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -4148,6 +4360,64 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + prosemirror-changeset@2.4.0: + resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.4: + resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} + + prosemirror-menu@1.3.0: + resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.41.8: + resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -4155,6 +4425,10 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4337,6 +4611,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -4717,6 +4994,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -4904,6 +5184,9 @@ packages: jsdom: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -6614,6 +6897,8 @@ snapshots: prompts: 2.4.2 smol-toml: 1.6.0 + '@remirror/core-constants@3.0.0': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.59.1': @@ -6779,6 +7064,205 @@ snapshots: '@tanstack/virtual-core@3.13.23': {} + '@tiptap/core@3.22.1(@tiptap/pm@3.22.1)': + dependencies: + '@tiptap/pm': 3.22.1 + + '@tiptap/extension-blockquote@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-bold@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-bubble-menu@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + optional: true + + '@tiptap/extension-bullet-list@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-code-block@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + + '@tiptap/extension-code@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-document@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-dropcursor@3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extensions': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-floating-menu@3.22.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + optional: true + + '@tiptap/extension-gapcursor@3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extensions': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-hard-break@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-heading@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-horizontal-rule@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + + '@tiptap/extension-image@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-italic@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-link@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-list-keymap@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + + '@tiptap/extension-ordered-list@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-paragraph@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-placeholder@3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extensions': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-strike@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-task-item@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-task-list@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + + '@tiptap/extension-text@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extension-underline@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + + '@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + + '@tiptap/markdown@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + marked: 17.0.5 + + '@tiptap/pm@3.22.1': + dependencies: + prosemirror-changeset: 2.4.0 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.4 + prosemirror-menu: 1.3.0 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + '@tiptap/react@3.22.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-floating-menu': 3.22.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.22.1': + dependencies: + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/extension-blockquote': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-bold': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-bullet-list': 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-code': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-code-block': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-document': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-dropcursor': 3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-gapcursor': 3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-hard-break': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-heading': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-horizontal-rule': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-italic': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-link': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-list-item': 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-list-keymap': 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-ordered-list': 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-paragraph': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-strike': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-text': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-underline': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extensions': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -6844,10 +7328,19 @@ snapshots: dependencies: '@types/node': 25.5.0 + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} '@types/node@24.12.0': @@ -6889,6 +7382,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/validate-npm-package-name@4.0.2': {} '@types/verror@1.10.11': @@ -7369,6 +7864,8 @@ snapshots: buffer: 5.7.1 optional: true + crelt@1.0.6: {} + cross-dirname@0.1.0: optional: true @@ -7618,6 +8115,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + entities@4.5.0: {} + env-paths@2.2.1: {} environment@1.1.0: {} @@ -7710,8 +8209,7 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@4.0.0: - optional: true + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -7819,6 +8317,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8365,6 +8865,12 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + lint-staged@16.4.0: dependencies: commander: 14.0.3 @@ -8453,10 +8959,21 @@ snapshots: transitivePeerDependencies: - supports-color + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} marked@14.0.0: {} + marked@17.0.5: {} + matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 @@ -8628,6 +9145,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdurl@2.0.0: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -9072,6 +9591,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 + orderedmap@2.1.1: {} + outvariant@1.4.3: {} oxfmt@0.41.0: @@ -9236,6 +9757,109 @@ snapshots: property-information@7.1.0: {} + prosemirror-changeset@2.4.0: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.4: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.3.0: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.8: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -9246,6 +9870,8 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode.js@2.3.1: {} + punycode@2.3.1: {} qs@6.15.0: @@ -9543,6 +10169,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.1 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + router@2.2.0: dependencies: debug: 4.4.3 @@ -9971,6 +10599,8 @@ snapshots: typescript@5.9.3: {} + uc.micro@2.1.0: {} + undici-types@7.16.0: {} undici-types@7.18.2: {} @@ -10127,6 +10757,8 @@ snapshots: transitivePeerDependencies: - msw + w3c-keyname@2.2.8: {} + wcwidth@1.0.1: dependencies: defaults: 1.0.4 diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index f7ebdde7666..bec8e10429f 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -442,6 +442,294 @@ transform: translateY(0); } +/* ── Rich Markdown ─────────────────────────────────── */ + +.rich-markdown-editor-shell { + position: relative; + display: flex; + min-height: 0; + height: 100%; + flex-direction: column; + background: var(--editor-surface); +} + +.rich-markdown-editor-toolbar { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + border-bottom: 1px solid color-mix(in srgb, var(--border) 72%, transparent); + background: color-mix(in srgb, var(--background) 84%, transparent); +} + +.rich-markdown-toolbar-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 28px; + padding: 0 8px; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: var(--muted-foreground); + font-size: 12px; + font-weight: 600; +} + +.rich-markdown-toolbar-button:hover, +.rich-markdown-toolbar-button.is-active { + border-color: color-mix(in srgb, var(--border) 82%, transparent); + background: var(--accent); + color: var(--foreground); +} + +.rich-markdown-search { + position: sticky; + top: 0; + z-index: 20; + display: flex; + align-items: center; + gap: 0; + width: fit-content; + max-width: min(100%, 460px); + margin-left: auto; + margin-right: 12px; + margin-top: 8px; + margin-bottom: 6px; + padding: 0 2px 0 4px; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--background); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16); +} + +.rich-markdown-search-field { + display: flex; + align-items: center; + min-width: 0; + width: 220px; + border: 1px solid var(--ring); + background: var(--background); + flex: 0 1 auto; +} + +.rich-markdown-search-status { + min-width: 0; + padding: 0 6px; + font-size: 12px; + color: var(--muted-foreground); + font-variant-numeric: tabular-nums; + white-space: nowrap; + line-height: 1; + flex: 0 0 auto; +} + +.rich-markdown-search-divider { + width: 1px; + height: 16px; + margin: 0 2px; + background: var(--border); +} + +.rich-markdown-search-input { + font-size: 13px; + line-height: 1; +} + +.rich-markdown-search-button { + width: 22px; + height: 22px; + min-width: 22px; + padding: 0; + border-radius: 2px; + color: var(--muted-foreground); +} + +.rich-markdown-search-button:hover:not(:disabled) { + background: var(--accent); + color: var(--foreground); +} + +.rich-markdown-search-match { + padding: 0; + border-radius: 2px; + background: color-mix(in srgb, #facc15 50%, transparent); +} + +.rich-markdown-search-match[data-active='true'] { + background: color-mix(in srgb, #fb923c 60%, transparent); +} + +.rich-markdown-editor { + min-height: 100%; + padding: 24px 32px 96px; + font-size: 14px; + line-height: 1.7; + outline: none; +} + +.rich-markdown-editor p.is-editor-empty:first-child::before { + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + color: var(--muted-foreground); +} + +.rich-markdown-editor h1, +.rich-markdown-editor h2, +.rich-markdown-editor h3, +.rich-markdown-editor h4, +.rich-markdown-editor h5, +.rich-markdown-editor h6 { + margin-top: 1.5em; + margin-bottom: 0.5em; + font-weight: 600; + line-height: 1.3; +} + +.rich-markdown-editor h1 { + font-size: 1.75em; +} + +.rich-markdown-editor h2 { + font-size: 1.4em; + padding-bottom: 0.3em; + border-bottom: 1px solid var(--border); +} + +.rich-markdown-editor h3 { + font-size: 1.15em; +} + +.rich-markdown-editor p, +.rich-markdown-editor ul, +.rich-markdown-editor ol, +.rich-markdown-editor blockquote, +.rich-markdown-editor pre { + margin: 0.75em 0; +} + +.rich-markdown-editor ul, +.rich-markdown-editor ol { + padding-left: 1.5em; +} + +.rich-markdown-editor ul { + list-style: disc; +} + +.rich-markdown-editor ol { + list-style: decimal; +} + +.rich-markdown-editor ul[data-type='taskList'] { + padding-left: 0; + list-style: none; +} + +.rich-markdown-editor ul[data-type='taskList'] li { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.rich-markdown-editor ul[data-type='taskList'] li > label { + margin-top: 0.35em; +} + +.rich-markdown-editor blockquote { + padding: 0.25em 1em; + border-left: 3px solid var(--border); + color: var(--muted-foreground); +} + +.rich-markdown-editor code { + padding: 0.2em 0.4em; + border-radius: 4px; + background: color-mix(in srgb, var(--foreground) 8%, transparent); + font-size: 0.9em; + font-family: var(--font-mono, monospace); +} + +.rich-markdown-editor pre { + padding: 12px 16px; + border-radius: 8px; + overflow-x: auto; + background: color-mix(in srgb, var(--foreground) 6%, transparent); +} + +.rich-markdown-editor pre code { + padding: 0; + background: transparent; +} + +.rich-markdown-editor hr { + margin: 1.5em 0; + border: none; + border-top: 1px solid var(--border); +} + +.rich-markdown-editor a { + color: var(--primary); + text-decoration: underline; +} + +.rich-markdown-editor img { + max-width: 100%; + border-radius: 8px; +} + +.rich-markdown-editor .ProseMirror-selectednode { + outline: 2px solid color-mix(in srgb, var(--ring) 78%, transparent); + outline-offset: 2px; +} + +.rich-markdown-slash-menu { + position: absolute; + z-index: 30; + display: flex; + width: min(320px, calc(100% - 24px)); + max-height: 280px; + flex-direction: column; + overflow-y: auto; + border: 1px solid color-mix(in srgb, var(--border) 76%, transparent); + border-radius: 12px; + background: color-mix(in srgb, var(--background) 92%, transparent); + box-shadow: 0 18px 44px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(16px); +} + +.rich-markdown-slash-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px; + border: none; + background: transparent; + color: inherit; + text-align: left; +} + +.rich-markdown-slash-item:hover, +.rich-markdown-slash-item.is-active { + background: var(--accent); +} + +.rich-markdown-slash-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border-radius: 8px; + background: color-mix(in srgb, var(--foreground) 6%, transparent); + color: var(--muted-foreground); +} + /* ── Markdown Preview ────────────────────────────────── */ .markdown-preview { diff --git a/src/renderer/src/components/editor/EditorContent.tsx b/src/renderer/src/components/editor/EditorContent.tsx index 5ff6f5a5360..21d1ce1fbb7 100644 --- a/src/renderer/src/components/editor/EditorContent.tsx +++ b/src/renderer/src/components/editor/EditorContent.tsx @@ -4,11 +4,12 @@ import { useAppStore } from '@/store' import { ConflictBanner, ConflictPlaceholderView, ConflictReviewPanel } from './ConflictComponents' import type { OpenFile } from '@/store/slices/editor' import type { GitStatusEntry, GitDiffResult } from '../../../../shared/types' +import { getMarkdownRichModeUnsupportedMessage } from './markdown-rich-mode' const MonacoEditor = lazy(() => import('./MonacoEditor')) const DiffViewer = lazy(() => import('./DiffViewer')) const CombinedDiffViewer = lazy(() => import('./CombinedDiffViewer')) -const MarkdownPreview = lazy(() => import('./MarkdownPreview')) +const RichMarkdownEditor = lazy(() => import('./RichMarkdownEditor')) const ImageViewer = lazy(() => import('./ImageViewer')) const ImageDiffViewer = lazy(() => import('./ImageDiffViewer')) @@ -19,7 +20,7 @@ type FileContent = { mimeType?: string } -type MarkdownViewMode = 'source' | 'preview' +type MarkdownViewMode = 'source' | 'rich' export function EditorContent({ activeFile, @@ -92,10 +93,28 @@ export function EditorContent({ const renderMarkdownContent = (fc: FileContent): React.JSX.Element => { const currentContent = editBuffers[activeFile.id] ?? fc.content - if (mdViewMode === 'preview') { - return + const richModeUnsupportedMessage = getMarkdownRichModeUnsupportedMessage(currentContent) + + if (mdViewMode === 'rich' && !richModeUnsupportedMessage) { + return ( + + ) } - return renderMonacoEditor(fc) + + return ( +
+ {mdViewMode === 'rich' && richModeUnsupportedMessage ? ( +
+ {richModeUnsupportedMessage} +
+ ) : null} +
{renderMonacoEditor(fc)}
+
+ ) } if (activeFile.mode === 'conflict-review') { diff --git a/src/renderer/src/components/editor/EditorPanel.tsx b/src/renderer/src/components/editor/EditorPanel.tsx index 467688dcad0..3913c9e4535 100644 --- a/src/renderer/src/components/editor/EditorPanel.tsx +++ b/src/renderer/src/components/editor/EditorPanel.tsx @@ -313,7 +313,7 @@ export default function EditorPanel(): React.JSX.Element | null { const isMarkdown = resolvedLanguage === 'markdown' const mdViewMode: MarkdownViewMode = isMarkdown && activeFile.mode === 'edit' - ? (markdownViewMode[activeFile.id] ?? 'source') + ? (markdownViewMode[activeFile.id] ?? 'rich') : 'source' const handleOpenDiffTargetFile = (): void => { @@ -374,7 +374,7 @@ export default function EditorPanel(): React.JSX.Element | null { {openFileState.canOpen ? isMarkdown - ? 'Open file tab to use markdown preview' + ? 'Open file tab to use rich markdown editing' : 'Open file tab' : 'This diff has no modified-side file to open'} diff --git a/src/renderer/src/components/editor/MarkdownViewToggle.tsx b/src/renderer/src/components/editor/MarkdownViewToggle.tsx index 1da6afc148f..8bd53acbaef 100644 --- a/src/renderer/src/components/editor/MarkdownViewToggle.tsx +++ b/src/renderer/src/components/editor/MarkdownViewToggle.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Code, BookOpen } from 'lucide-react' +import { Code, Eye } from 'lucide-react' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import type { MarkdownViewMode } from '@/store/slices/editor' @@ -28,8 +28,8 @@ export default function MarkdownViewToggle({ - - + + ) diff --git a/src/renderer/src/components/editor/RichMarkdownEditor.tsx b/src/renderer/src/components/editor/RichMarkdownEditor.tsx new file mode 100644 index 00000000000..b0ec5d94cb4 --- /dev/null +++ b/src/renderer/src/components/editor/RichMarkdownEditor.tsx @@ -0,0 +1,399 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { EditorContent, useEditor } from '@tiptap/react' +import type { Editor } from '@tiptap/react' +import StarterKit from '@tiptap/starter-kit' +import Link from '@tiptap/extension-link' +import Image from '@tiptap/extension-image' +import Placeholder from '@tiptap/extension-placeholder' +import TaskList from '@tiptap/extension-task-list' +import TaskItem from '@tiptap/extension-task-item' +import { Markdown } from '@tiptap/markdown' +import { List, ListOrdered, Quote } from 'lucide-react' +import { cn } from '@/lib/utils' +import { isMarkdownPreviewFindShortcut } from './markdown-preview-search' +import { slashCommands } from './rich-markdown-commands' +import type { SlashCommand } from './rich-markdown-commands' +import { RichMarkdownSearchBar } from './RichMarkdownSearchBar' +import { useRichMarkdownSearch } from './useRichMarkdownSearch' + +type RichMarkdownEditorProps = { + content: string + onContentChange: (content: string) => void + onSave: (content: string) => void +} + +type SlashMenuState = { + query: string + from: number + to: number + left: number + top: number +} + +export default function RichMarkdownEditor({ + content, + onContentChange, + onSave +}: RichMarkdownEditorProps): React.JSX.Element { + const rootRef = useRef(null) + const [slashMenu, setSlashMenu] = useState(null) + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0) + const isMac = navigator.userAgent.includes('Mac') + const lastCommittedMarkdownRef = useRef(content) + const slashMenuRef = useRef(null) + const filteredSlashCommandsRef = useRef(slashCommands) + const selectedCommandIndexRef = useRef(0) + const onContentChangeRef = useRef(onContentChange) + const onSaveRef = useRef(onSave) + + useEffect(() => { + onContentChangeRef.current = onContentChange + }, [onContentChange]) + + useEffect(() => { + onSaveRef.current = onSave + }, [onSave]) + + const editor = useEditor({ + immediatelyRender: false, + extensions: [ + StarterKit, + Link.configure({ + openOnClick: false + }), + Image, + Placeholder.configure({ + placeholder: 'Write markdown… Type / for blocks.' + }), + TaskList, + TaskItem.configure({ + nested: true + }), + Markdown.configure({ + markedOptions: { + gfm: true + } + }) + ], + content, + contentType: 'markdown', + editorProps: { + attributes: { + class: 'rich-markdown-editor' + }, + handleKeyDown: (_view, event) => { + const mod = isMac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey + if (isMarkdownPreviewFindShortcut(event, isMac)) { + event.preventDefault() + openSearch() + return true + } + if (mod && event.key.toLowerCase() === 's') { + event.preventDefault() + const markdown = editor?.getMarkdown() ?? lastCommittedMarkdownRef.current + onSaveRef.current(markdown) + return true + } + + const currentSlashMenu = slashMenuRef.current + if (!currentSlashMenu) { + return false + } + + const currentFilteredSlashCommands = filteredSlashCommandsRef.current + if (currentFilteredSlashCommands.length === 0) { + return false + } + + const activeEditor = editor + if (!activeEditor) { + return false + } + + if (event.key === 'ArrowDown') { + event.preventDefault() + setSelectedCommandIndex( + (currentIndex) => (currentIndex + 1) % currentFilteredSlashCommands.length + ) + return true + } + if (event.key === 'ArrowUp') { + event.preventDefault() + setSelectedCommandIndex( + (currentIndex) => + (currentIndex - 1 + currentFilteredSlashCommands.length) % + currentFilteredSlashCommands.length + ) + return true + } + if (event.key === 'Enter' || event.key === 'Tab') { + event.preventDefault() + // Why: ProseMirror keeps this key handler stable for the editor's + // lifetime, so reading React state directly here can use a stale + // slash-menu index and execute the wrong command after keyboard + // navigation. The ref mirrors the latest highlighted item. + const selectedCommand = currentFilteredSlashCommands[selectedCommandIndexRef.current] + if (selectedCommand) { + runSlashCommand(activeEditor, currentSlashMenu, selectedCommand) + } + return true + } + if (event.key === 'Escape') { + event.preventDefault() + setSlashMenu(null) + return true + } + + return false + } + }, + onCreate: ({ editor: nextEditor }) => { + lastCommittedMarkdownRef.current = nextEditor.getMarkdown() + }, + onUpdate: ({ editor: nextEditor }) => { + const markdown = nextEditor.getMarkdown() + lastCommittedMarkdownRef.current = markdown + onContentChangeRef.current(markdown) + syncSlashMenu(nextEditor, rootRef.current, setSlashMenu) + }, + onSelectionUpdate: ({ editor: nextEditor }) => { + syncSlashMenu(nextEditor, rootRef.current, setSlashMenu) + } + }) + + const { + activeMatchIndex, + closeSearch, + isSearchOpen, + matchCount, + moveToMatch, + openSearch, + searchInputRef, + searchQuery, + setSearchQuery + } = useRichMarkdownSearch({ + editor, + isMac, + rootRef + }) + + const filteredSlashCommands = useMemo(() => { + const query = slashMenu?.query.trim().toLowerCase() ?? '' + if (!query) { + return slashCommands + } + return slashCommands.filter((command) => { + const haystack = [command.label, ...command.aliases].join(' ').toLowerCase() + return haystack.includes(query) + }) + }, [slashMenu?.query]) + + useEffect(() => { + slashMenuRef.current = slashMenu + }, [slashMenu]) + + useEffect(() => { + filteredSlashCommandsRef.current = filteredSlashCommands + }, [filteredSlashCommands]) + + useEffect(() => { + selectedCommandIndexRef.current = selectedCommandIndex + }, [selectedCommandIndex]) + + useEffect(() => { + if (filteredSlashCommands.length === 0) { + setSelectedCommandIndex(0) + return + } + + setSelectedCommandIndex((currentIndex) => + Math.min(currentIndex, filteredSlashCommands.length - 1) + ) + }, [filteredSlashCommands.length]) + + useEffect(() => { + if (!editor) { + return + } + + const currentMarkdown = editor.getMarkdown() + if (currentMarkdown === content) { + return + } + + // Why: markdown files on disk remain the source of truth for rich mode in + // Orca. External file changes, tab replacement, and save-after-reload must + // overwrite the editor state so the rich view never drifts from repo text. + editor.commands.setContent(content, { contentType: 'markdown' }) + lastCommittedMarkdownRef.current = content + syncSlashMenu(editor, rootRef.current, setSlashMenu) + }, [content, editor]) + + return ( +
+
+ editor?.chain().focus().toggleBold().run()} + > + B + + editor?.chain().focus().toggleItalic().run()} + > + I + + editor?.chain().focus().toggleStrike().run()} + > + S + + editor?.chain().focus().toggleBulletList().run()} + > + + + editor?.chain().focus().toggleOrderedList().run()} + > + + + editor?.chain().focus().toggleBlockquote().run()} + > + + +
+ + + {slashMenu && filteredSlashCommands.length > 0 ? ( +
+ {filteredSlashCommands.map((command, index) => { + const Icon = command.icon + return ( + + ) + })} +
+ ) : null} +
+ ) +} + +function ToolbarButton({ + active, + label, + onClick, + children +}: { + active: boolean + label: string + onClick: () => void + children: React.ReactNode +}): React.JSX.Element { + return ( + + ) +} + +function syncSlashMenu( + editor: Editor, + root: HTMLDivElement | null, + setSlashMenu: React.Dispatch> +): void { + if (!root || editor.view.composing || !editor.isEditable) { + setSlashMenu(null) + return + } + + const { state, view } = editor + const { selection } = state + if (!selection.empty) { + setSlashMenu(null) + return + } + + const { $from } = selection + if (!$from.parent.isTextblock) { + setSlashMenu(null) + return + } + + const blockTextBeforeCursor = $from.parent.textBetween(0, $from.parentOffset, '\0', '\0') + const slashMatch = blockTextBeforeCursor.match(/^\s*\/([a-z0-9-]*)$/i) + if (!slashMatch) { + setSlashMenu(null) + return + } + + const slashOffset = blockTextBeforeCursor.lastIndexOf('/') + const start = selection.from - ($from.parentOffset - slashOffset) + const coords = view.coordsAtPos(selection.from) + const rect = root.getBoundingClientRect() + + setSlashMenu({ + query: slashMatch[1] ?? '', + from: start, + to: selection.from, + left: coords.left - rect.left, + top: coords.bottom - rect.top + 8 + }) +} + +function runSlashCommand(editor: Editor, slashMenu: SlashMenuState, command: SlashCommand): void { + editor.chain().focus().deleteRange({ from: slashMenu.from, to: slashMenu.to }).run() + command.run(editor) +} diff --git a/src/renderer/src/components/editor/RichMarkdownSearchBar.tsx b/src/renderer/src/components/editor/RichMarkdownSearchBar.tsx new file mode 100644 index 00000000000..13def2b9bc0 --- /dev/null +++ b/src/renderer/src/components/editor/RichMarkdownSearchBar.tsx @@ -0,0 +1,112 @@ +import React from 'react' +import { ChevronDown, ChevronUp, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' + +type RichMarkdownSearchBarProps = { + activeMatchIndex: number + isOpen: boolean + matchCount: number + onClose: () => void + onMoveToMatch: (direction: 1 | -1) => void + onQueryChange: (query: string) => void + query: string + searchInputRef: React.RefObject +} + +export function RichMarkdownSearchBar({ + activeMatchIndex, + isOpen, + matchCount, + onClose, + onMoveToMatch, + onQueryChange, + query, + searchInputRef +}: RichMarkdownSearchBarProps): React.JSX.Element | null { + if (!isOpen) { + return null + } + + const keepSearchFocus = (event: React.MouseEvent): void => { + // Why: rich-mode find drives navigation through the ProseMirror selection. + // Letting the toolbar buttons take focus interrupts that selection flow and + // makes mouse-based next/previous navigation appear broken. + event.preventDefault() + } + + return ( +
event.stopPropagation()}> +
+ onQueryChange(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && event.shiftKey) { + event.preventDefault() + onMoveToMatch(-1) + return + } + if (event.key === 'Enter') { + event.preventDefault() + onMoveToMatch(1) + return + } + if (event.key === 'Escape') { + event.preventDefault() + onClose() + } + }} + placeholder="Find in rich editor" + className="rich-markdown-search-input h-7 !border-0 bg-transparent px-2 shadow-none focus-visible:!border-0 focus-visible:ring-0" + aria-label="Find in rich markdown editor" + /> +
+
+ {query && matchCount === 0 + ? 'No results' + : `${matchCount === 0 ? 0 : activeMatchIndex + 1}/${matchCount}`} +
+ + +
+ +
+ ) +} diff --git a/src/renderer/src/components/editor/markdown-rich-mode.test.ts b/src/renderer/src/components/editor/markdown-rich-mode.test.ts new file mode 100644 index 00000000000..0bdbe635ef0 --- /dev/null +++ b/src/renderer/src/components/editor/markdown-rich-mode.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import { getMarkdownRichModeUnsupportedMessage } from './markdown-rich-mode' + +describe('getMarkdownRichModeUnsupportedMessage', () => { + it('blocks markdown tables that rich mode cannot round-trip', () => { + expect(getMarkdownRichModeUnsupportedMessage('| a | b |\n| - | - |\n| 1 | 2 |\n')).toBe( + 'Markdown tables are only editable in source mode.' + ) + }) + + it('allows plain markdown content', () => { + expect(getMarkdownRichModeUnsupportedMessage('# Title\n\n- one\n- two\n')).toBeNull() + }) + + it('allows common raw html in markdown files', () => { + expect(getMarkdownRichModeUnsupportedMessage('Before hi after\n')).toBeNull() + }) + + it('allows markdown autolinks wrapped in angle brackets', () => { + expect( + getMarkdownRichModeUnsupportedMessage('See for details.\n') + ).toBeNull() + }) + + it('allows code fences with language info strings', () => { + expect(getMarkdownRichModeUnsupportedMessage('```ts\nconst answer = 42\n```\n')).toBeNull() + }) + + it('ignores table syntax inside fenced code blocks', () => { + expect( + getMarkdownRichModeUnsupportedMessage('```md\n| a | b |\n| - | - |\n| 1 | 2 |\n```\n') + ).toBeNull() + }) + + it('ignores jsx-looking tags inside code spans and fences', () => { + expect(getMarkdownRichModeUnsupportedMessage('Use `` in docs.\n')).toBeNull() + expect(getMarkdownRichModeUnsupportedMessage('```tsx\n\n```\n')).toBeNull() + }) +}) diff --git a/src/renderer/src/components/editor/markdown-rich-mode.ts b/src/renderer/src/components/editor/markdown-rich-mode.ts new file mode 100644 index 00000000000..43edc4b7fa2 --- /dev/null +++ b/src/renderer/src/components/editor/markdown-rich-mode.ts @@ -0,0 +1,87 @@ +export type MarkdownRichModeUnsupportedReason = + | 'frontmatter' + | 'jsx' + | 'reference-links' + | 'footnotes' + | 'tables' + +type UnsupportedMatch = { + reason: MarkdownRichModeUnsupportedReason + message: string + pattern: RegExp +} + +const UNSUPPORTED_PATTERNS: UnsupportedMatch[] = [ + { + reason: 'frontmatter', + message: 'Frontmatter is only editable in source mode.', + // Why: Tiptap markdown support is beta and frontmatter is often consumed by + // static-site tooling. Falling back to source mode avoids silently dropping + // metadata that rich mode does not explicitly own. + pattern: /^(?:---|\+\+\+)\r?\n[\s\S]*?\r?\n(?:---|\+\+\+)(?:\r?\n|$)/ + }, + { + reason: 'jsx', + message: 'JSX or MDX content is only editable in source mode.', + // Why: uppercase tags are a strong MDX/JSX signal, and those files usually + // carry component semantics that the markdown editor does not own. Raw HTML + // is intentionally allowed because blocking ordinary .md files with common + // inline tags proved too disruptive; source mode remains the fallback if a + // specific document does not round-trip the way the user expects. + pattern: /<[A-Z][\w.]*(?:\s[^<>]*)?\/?>/ + }, + { + reason: 'reference-links', + message: 'Reference-style links are only editable in source mode.', + pattern: /^\[[^\]]+\]:\s+\S+/m + }, + { + reason: 'footnotes', + message: 'Footnotes are only editable in source mode.', + pattern: /^\[\^[^\]]+\]:\s+/m + }, + { + reason: 'tables', + message: 'Markdown tables are only editable in source mode.', + // Why: the current rich-mode extension set does not include table nodes, so + // Tiptap collapses GFM tables instead of round-tripping them verbatim. + pattern: /^(?:\|?.+\|.+\|?.*)\r?\n\|?(?:\s*:?-{1,}:?\s*\|){1,}\s*:?-{1,}:?\s*\|?/m + } +] + +export function getMarkdownRichModeUnsupportedMessage(content: string): string | null { + const contentWithoutCode = stripMarkdownCode(content) + + for (const matcher of UNSUPPORTED_PATTERNS) { + if (matcher.pattern.test(contentWithoutCode)) { + return matcher.message + } + } + + return null +} + +function stripMarkdownCode(content: string): string { + const lines = content.split(/\r?\n/) + const sanitizedLines: string[] = [] + let activeFence: '`' | '~' | null = null + + for (const line of lines) { + const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/) + if (fenceMatch) { + const fenceMarker = fenceMatch[1][0] as '`' | '~' + activeFence = activeFence === fenceMarker ? null : fenceMarker + sanitizedLines.push('') + continue + } + + if (activeFence) { + sanitizedLines.push('') + continue + } + + sanitizedLines.push(line.replace(/`+[^`\n]*`+/g, '')) + } + + return sanitizedLines.join('\n') +} diff --git a/src/renderer/src/components/editor/rich-markdown-commands.tsx b/src/renderer/src/components/editor/rich-markdown-commands.tsx new file mode 100644 index 00000000000..ecdbb9dcb66 --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-commands.tsx @@ -0,0 +1,142 @@ +import React from 'react' +import type { Editor } from '@tiptap/react' +import { Heading1, Heading2, Heading3, ImageIcon, List, ListOrdered, Quote } from 'lucide-react' + +export type SlashCommandId = + | 'text' + | 'heading-1' + | 'heading-2' + | 'heading-3' + | 'task-list' + | 'bullet-list' + | 'ordered-list' + | 'blockquote' + | 'code-block' + | 'divider' + | 'image' + +export type SlashCommand = { + id: SlashCommandId + label: string + aliases: string[] + icon: React.ComponentType<{ className?: string }> + description: string + run: (editor: Editor) => void +} + +export const slashCommands: SlashCommand[] = [ + { + id: 'text', + label: 'Text', + aliases: ['paragraph', 'plain'], + icon: List, + description: 'Start a normal paragraph.', + run: (editor) => { + editor.chain().focus().setParagraph().run() + } + }, + { + id: 'heading-1', + label: 'Heading 1', + aliases: ['h1', 'title'], + icon: Heading1, + description: 'Large section heading.', + run: (editor) => { + editor.chain().focus().toggleHeading({ level: 1 }).run() + } + }, + { + id: 'heading-2', + label: 'Heading 2', + aliases: ['h2'], + icon: Heading2, + description: 'Medium section heading.', + run: (editor) => { + editor.chain().focus().toggleHeading({ level: 2 }).run() + } + }, + { + id: 'heading-3', + label: 'Heading 3', + aliases: ['h3'], + icon: Heading3, + description: 'Small section heading.', + run: (editor) => { + editor.chain().focus().toggleHeading({ level: 3 }).run() + } + }, + { + id: 'task-list', + label: 'To-do List', + aliases: ['todo', 'task', 'checkbox'], + icon: List, + description: 'Create a checklist.', + run: (editor) => { + editor.chain().focus().toggleTaskList().run() + } + }, + { + id: 'bullet-list', + label: 'Bullet List', + aliases: ['bullet', 'ul', 'list'], + icon: List, + description: 'Create an unordered list.', + run: (editor) => { + editor.chain().focus().toggleBulletList().run() + } + }, + { + id: 'ordered-list', + label: 'Numbered List', + aliases: ['ordered', 'ol', 'numbered'], + icon: ListOrdered, + description: 'Create an ordered list.', + run: (editor) => { + editor.chain().focus().toggleOrderedList().run() + } + }, + { + id: 'blockquote', + label: 'Quote', + aliases: ['quote', 'blockquote'], + icon: Quote, + description: 'Insert a blockquote.', + run: (editor) => { + editor.chain().focus().toggleBlockquote().run() + } + }, + { + id: 'code-block', + label: 'Code Block', + aliases: ['code', 'snippet'], + icon: List, + description: 'Insert a fenced code block.', + run: (editor) => { + editor.chain().focus().toggleCodeBlock().run() + } + }, + { + id: 'divider', + label: 'Divider', + aliases: ['divider', 'rule', 'hr'], + icon: List, + description: 'Insert a horizontal rule.', + run: (editor) => { + editor.chain().focus().setHorizontalRule().run() + } + }, + { + id: 'image', + label: 'Image', + aliases: ['image', 'img'], + icon: ImageIcon, + description: 'Insert an image from a URL.', + run: (editor) => { + const src = window.prompt('Image URL') + if (!src) { + return + } + editor.chain().focus().setImage({ src }).run() + } + } +] diff --git a/src/renderer/src/components/editor/rich-markdown-search.ts b/src/renderer/src/components/editor/rich-markdown-search.ts new file mode 100644 index 00000000000..de73a42c52c --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-search.ts @@ -0,0 +1,116 @@ +import type { Node as ProseMirrorNode } from '@tiptap/pm/model' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' +import { findTextMatchRanges } from './markdown-preview-search' + +export type RichMarkdownSearchMatch = { + from: number + to: number +} + +type RichMarkdownSearchState = { + activeIndex: number + decorations: DecorationSet + query: string +} + +type RichMarkdownSearchMeta = { + activeIndex: number + query: string +} + +export const richMarkdownSearchPluginKey = new PluginKey( + 'richMarkdownSearch' +) + +export function findRichMarkdownSearchMatches( + doc: ProseMirrorNode, + query: string +): RichMarkdownSearchMatch[] { + if (!query) { + return [] + } + + const matches: RichMarkdownSearchMatch[] = [] + doc.descendants((node, pos) => { + if (!node.isText) { + return + } + + const text = node.text ?? '' + if (!text.trim()) { + return + } + + const ranges = findTextMatchRanges(text, query) + for (const range of ranges) { + matches.push({ + from: pos + range.start, + to: pos + range.end + }) + } + }) + + return matches +} + +export function createRichMarkdownSearchPlugin(): Plugin { + return new Plugin({ + key: richMarkdownSearchPluginKey, + state: { + init: () => ({ + activeIndex: -1, + decorations: DecorationSet.empty, + query: '' + }), + apply: (tr, pluginState) => { + const meta = tr.getMeta(richMarkdownSearchPluginKey) as RichMarkdownSearchMeta | undefined + const query = meta?.query ?? pluginState.query + const activeIndex = meta?.activeIndex ?? pluginState.activeIndex + + if (!query) { + return { + activeIndex: -1, + decorations: DecorationSet.empty, + query: '' + } + } + + if (!meta && !tr.docChanged) { + return pluginState + } + + return { + activeIndex, + decorations: buildSearchDecorations(tr.doc, query, activeIndex), + query + } + } + }, + props: { + decorations(state) { + return richMarkdownSearchPluginKey.getState(state)?.decorations ?? DecorationSet.empty + } + } + }) +} + +function buildSearchDecorations( + doc: ProseMirrorNode, + query: string, + activeIndex: number +): DecorationSet { + const matches = findRichMarkdownSearchMatches(doc, query) + if (matches.length === 0) { + return DecorationSet.empty + } + + const decorations = matches.map((match, index) => + Decoration.inline(match.from, match.to, { + class: 'rich-markdown-search-match', + 'data-active': index === activeIndex ? 'true' : undefined + }) + ) + + return DecorationSet.create(doc, decorations) +} diff --git a/src/renderer/src/components/editor/useRichMarkdownSearch.ts b/src/renderer/src/components/editor/useRichMarkdownSearch.ts new file mode 100644 index 00000000000..aef6297a703 --- /dev/null +++ b/src/renderer/src/components/editor/useRichMarkdownSearch.ts @@ -0,0 +1,194 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import type { RefObject } from 'react' +import type { Editor } from '@tiptap/react' +import { TextSelection } from '@tiptap/pm/state' +import { isMarkdownPreviewFindShortcut } from './markdown-preview-search' +import { + createRichMarkdownSearchPlugin, + findRichMarkdownSearchMatches, + richMarkdownSearchPluginKey +} from './rich-markdown-search' + +export function useRichMarkdownSearch({ + editor, + isMac, + rootRef +}: { + editor: Editor | null + isMac: boolean + rootRef: RefObject +}) { + const searchInputRef = useRef(null) + const [isSearchOpen, setIsSearchOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [matchCount, setMatchCount] = useState(0) + const [activeMatchIndex, setActiveMatchIndex] = useState(-1) + const [searchRevision, setSearchRevision] = useState(0) + + const openSearch = useCallback(() => { + setIsSearchOpen(true) + }, []) + + const closeSearch = useCallback(() => { + setIsSearchOpen(false) + setSearchQuery('') + setActiveMatchIndex(-1) + setMatchCount(0) + }, []) + + const moveToMatch = useCallback( + (direction: 1 | -1) => { + if (matchCount === 0) { + return + } + + setActiveMatchIndex((currentIndex) => { + const baseIndex = currentIndex >= 0 ? currentIndex : direction === 1 ? -1 : 0 + return (baseIndex + direction + matchCount) % matchCount + }) + }, + [matchCount] + ) + + const handleEditorUpdate = useCallback(() => { + setSearchRevision((current) => current + 1) + }, []) + + useEffect(() => { + if (!editor) { + return + } + + const plugin = createRichMarkdownSearchPlugin() + editor.registerPlugin(plugin) + + return () => { + editor.unregisterPlugin(richMarkdownSearchPluginKey) + } + }, [editor]) + + useEffect(() => { + if (!editor) { + return + } + + editor.on('update', handleEditorUpdate) + return () => { + editor.off('update', handleEditorUpdate) + } + }, [editor, handleEditorUpdate]) + + useEffect(() => { + if (!isSearchOpen) { + return + } + searchInputRef.current?.focus() + searchInputRef.current?.select() + }, [isSearchOpen]) + + useEffect(() => { + if (!editor) { + return + } + + if (!isSearchOpen || !searchQuery) { + setMatchCount(0) + setActiveMatchIndex(-1) + editor.view.dispatch( + editor.state.tr.setMeta(richMarkdownSearchPluginKey, { + activeIndex: -1, + query: '' + }) + ) + return + } + + const matches = findRichMarkdownSearchMatches(editor.state.doc, searchQuery) + setMatchCount(matches.length) + setActiveMatchIndex((currentIndex) => { + if (matches.length === 0) { + return -1 + } + if (currentIndex >= 0 && currentIndex < matches.length) { + return currentIndex + } + return 0 + }) + }, [editor, isSearchOpen, searchQuery, searchRevision]) + + useEffect(() => { + if (!editor) { + return + } + + const query = isSearchOpen ? searchQuery : '' + editor.view.dispatch( + editor.state.tr.setMeta(richMarkdownSearchPluginKey, { + activeIndex: activeMatchIndex, + query + }) + ) + + if (!query || activeMatchIndex < 0) { + return + } + + const matches = findRichMarkdownSearchMatches(editor.state.doc, query) + const activeMatch = matches[activeMatchIndex] + if (!activeMatch) { + return + } + + // Why: rich-mode find should navigate within the editor model instead of + // the rendered DOM so highlight positions stay correct while the user edits. + // Updating the ProseMirror selection keeps scroll-to-match aligned with the + // actual markdown document rather than the transient browser layout. + const tr = editor.state.tr + tr.setSelection(TextSelection.create(tr.doc, activeMatch.from, activeMatch.to)) + tr.scrollIntoView() + editor.view.dispatch(tr) + }, [activeMatchIndex, editor, isSearchOpen, searchQuery]) + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent): void => { + const root = rootRef.current + if (!root) { + return + } + + const target = event.target + const targetInsideEditor = target instanceof Node && root.contains(target) + if (isMarkdownPreviewFindShortcut(event, isMac) && targetInsideEditor) { + event.preventDefault() + event.stopPropagation() + openSearch() + return + } + + if ( + event.key === 'Escape' && + isSearchOpen && + (targetInsideEditor || target === searchInputRef.current) + ) { + event.preventDefault() + event.stopPropagation() + closeSearch() + } + } + + window.addEventListener('keydown', handleKeyDown, { capture: true }) + return () => window.removeEventListener('keydown', handleKeyDown, { capture: true }) + }, [closeSearch, isMac, isSearchOpen, openSearch, rootRef]) + + return { + activeMatchIndex, + closeSearch, + isSearchOpen, + matchCount, + moveToMatch, + openSearch, + searchInputRef, + searchQuery, + setSearchQuery + } +} diff --git a/src/renderer/src/store/slices/editor.test.ts b/src/renderer/src/store/slices/editor.test.ts index 1310838d712..65f2f1f254c 100644 --- a/src/renderer/src/store/slices/editor.test.ts +++ b/src/renderer/src/store/slices/editor.test.ts @@ -88,7 +88,7 @@ describe('createEditorSlice openDiff', () => { }) }) -describe('createEditorSlice markdown preview state', () => { +describe('createEditorSlice markdown view state', () => { it('drops markdown view mode for a replaced preview tab', () => { const store = createEditorStore() @@ -102,7 +102,7 @@ describe('createEditorSlice markdown preview state', () => { }, { preview: true } ) - store.getState().setMarkdownViewMode('/repo/docs/README.md', 'preview') + store.getState().setMarkdownViewMode('/repo/docs/README.md', 'rich') store.getState().openFile( { diff --git a/src/renderer/src/store/slices/editor.ts b/src/renderer/src/store/slices/editor.ts index a0d9f275e0a..fe6bc389ef2 100644 --- a/src/renderer/src/store/slices/editor.ts +++ b/src/renderer/src/store/slices/editor.ts @@ -90,7 +90,7 @@ export type OpenFile = { export type RightSidebarTab = 'explorer' | 'search' | 'source-control' | 'checks' export type ActivityBarPosition = 'top' | 'side' -export type MarkdownViewMode = 'source' | 'preview' +export type MarkdownViewMode = 'source' | 'rich' export type EditorSlice = { // Markdown view mode per file (fileId -> mode)