feat: add Tiptap WYSIWYG editor component for task descriptions

Replace Textarea + Edit/Preview tabs with a reusable WYSIWYG editor
based on Tiptap v3. Integrated into TaskDetailDialog for editing
task descriptions with native markdown I/O.

Components:
- TiptapEditor: main component with EditorContext.Provider pattern
- TiptapToolbar: configurable toolbar with useEditorState for v3 reactivity
- TiptapBubbleMenu: floating formatting menu on text selection
- useTiptapEditor: core hook with markdown I/O, content sync, stale closure prevention
- presets: full/compact/minimal editor configurations
- tiptapStyles.css: ProseMirror styles matching MarkdownViewer values

Key details:
- Data format stays markdown string (not HTML)
- contentType: 'markdown' for both init and setContent
- GFM enabled via markedOptions
- ProseMirror li>p margin fix, nested list styles, gapcursor
This commit is contained in:
iliya 2026-03-14 22:13:04 +02:00
parent a29d8403d6
commit 37a4c458bb
11 changed files with 1486 additions and 61 deletions

View file

@ -96,6 +96,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@fastify/cors": "^11.2.0",
"@fastify/static": "^9.0.0",
"@floating-ui/dom": "^1.7.6",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
@ -109,6 +110,11 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-virtual": "^3.10.8",
"@tiptap/extension-placeholder": "^3.20.1",
"@tiptap/markdown": "^3.20.1",
"@tiptap/pm": "^3.20.1",
"@tiptap/react": "^3.20.1",
"@tiptap/starter-kit": "^3.20.1",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",

View file

@ -101,6 +101,9 @@ importers:
'@fastify/static':
specifier: ^9.0.0
version: 9.0.0
'@floating-ui/dom':
specifier: ^1.7.6
version: 1.7.6
'@radix-ui/react-alert-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -140,6 +143,21 @@ importers:
'@tanstack/react-virtual':
specifier: ^3.10.8
version: 3.13.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tiptap/extension-placeholder':
specifier: ^3.20.1
version: 3.20.1(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))
'@tiptap/markdown':
specifier: ^3.20.1
version: 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/pm':
specifier: ^3.20.1
version: 3.20.1
'@tiptap/react':
specifier: ^3.20.1
version: 3.20.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tiptap/starter-kit':
specifier: ^3.20.1
version: 3.20.1
'@xterm/addon-fit':
specifier: ^0.11.0
version: 0.11.0
@ -1105,11 +1123,11 @@ packages:
'@fastify/static@9.0.0':
resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==}
'@floating-ui/core@1.7.4':
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
'@floating-ui/dom@1.7.5':
resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==}
'@floating-ui/dom@1.7.6':
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
'@floating-ui/react-dom@2.1.7':
resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==}
@ -1117,8 +1135,8 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@ -1864,6 +1882,9 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@ -2027,6 +2048,166 @@ packages:
'@tanstack/virtual-core@3.13.18':
resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==}
'@tiptap/core@3.20.1':
resolution: {integrity: sha512-SwkPEWIfaDEZjC8SEIi4kZjqIYUbRgLUHUuQezo5GbphUNC8kM1pi3C3EtoOPtxXrEbY6e4pWEzW54Pcrd+rVA==}
peerDependencies:
'@tiptap/pm': ^3.20.1
'@tiptap/extension-blockquote@3.20.1':
resolution: {integrity: sha512-WzNXk/63PQI2fav4Ta6P0GmYRyu8Gap1pV3VUqaVK829iJ6Zt1T21xayATHEHWMK27VT1GLPJkx9Ycr2jfDyQw==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-bold@3.20.1':
resolution: {integrity: sha512-fz++Qv6Rk/Hov0IYG/r7TJ1Y4zWkuGONe0UN5g0KY32NIMg3HeOHicbi4xsNWTm9uAOl3eawWDkezEMrleObMw==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-bubble-menu@3.20.1':
resolution: {integrity: sha512-XaPvO6aCoWdFnCBus0s88lnj17NR/OopV79i8Qhgz3WMR0vrsL5zsd45l0lZuu9pSvm5VW47SoxakkJiZC1suw==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.1
'@tiptap/extension-bullet-list@3.20.1':
resolution: {integrity: sha512-mbrlvOZo5OF3vLhp+3fk9KuL/6J/wsN0QxF6ZFRAHzQ9NkJdtdfARcBeBnkWXGN8inB6YxbTGY1/E4lmBkOpOw==}
peerDependencies:
'@tiptap/extension-list': ^3.20.1
'@tiptap/extension-code-block@3.20.1':
resolution: {integrity: sha512-vKejwBq+Nlj4Ybd3qOyDxIQKzYymdNH+8eXkKwGShk2nfLJIxq69DCyGvmuHgipIO1qcYPJ149UNpGN+YGcdmA==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.1
'@tiptap/extension-code@3.20.1':
resolution: {integrity: sha512-509DHINIA/Gg+eTG7TEkfsS8RUiPLH5xZNyLRT0A1oaoaJmECKfrV6aAm05IdfTyqDqz6LW5pbnX6DdUC4keug==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-document@3.20.1':
resolution: {integrity: sha512-9vrqdGmRV7bQCSY3NLgu7UhIwgOCDp4sKqMNsoNRX0aZ021QQMTvBQDPkiRkCf7MNsnWrNNnr52PVnULEn3vFQ==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-dropcursor@3.20.1':
resolution: {integrity: sha512-K18L9FX4znn+ViPSIbTLOGcIaXMx/gLNwAPE8wPLwswbHhQqdiY1zzdBw6drgOc1Hicvebo2dIoUlSXOZsOEcw==}
peerDependencies:
'@tiptap/extensions': ^3.20.1
'@tiptap/extension-floating-menu@3.20.1':
resolution: {integrity: sha512-BeDC6nfOesIMn5pFuUnkEjOxGv80sOJ8uk1mdt9/3Fkvra8cB9NIYYCVtd6PU8oQFmJ8vFqPrRkUWrG5tbqnOg==}
peerDependencies:
'@floating-ui/dom': ^1.0.0
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.1
'@tiptap/extension-gapcursor@3.20.1':
resolution: {integrity: sha512-kZOtttV6Ai8VUAgEng3h4WKFbtdSNJ6ps7r0cRPY+FctWhVmgNb/JJwwyC+vSilR7nRENAhrA/Cv/RxVlvLw+g==}
peerDependencies:
'@tiptap/extensions': ^3.20.1
'@tiptap/extension-hard-break@3.20.1':
resolution: {integrity: sha512-9sKpmg/IIdlLXimYWUZ3PplIRcehv4Oc7V1miTqlnAthMzjMqigDkjjgte4JZV67RdnDJTQkRw8TklCAU28Emg==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-heading@3.20.1':
resolution: {integrity: sha512-unudyfQP6FxnyWinxvPqe/51DG91J6AaJm666RnAubgYMCgym+33kBftx4j4A6qf+ddWYbD00thMNKOnVLjAEQ==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-horizontal-rule@3.20.1':
resolution: {integrity: sha512-rjFKFXNntdl0jay8oIGFvvykHlpyQTLmrH3Ag2fj3i8yh6MVvqhtaDomYQbw5sxECd5hBkL+T4n2d2DRuVw/QQ==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.1
'@tiptap/extension-italic@3.20.1':
resolution: {integrity: sha512-ZYRX13Kt8tR8JOzSXirH3pRpi8x30o7LHxZY58uXBdUvr3tFzOkh03qbN523+diidSVeHP/aMd/+IrplHRkQug==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-link@3.20.1':
resolution: {integrity: sha512-oYTTIgsQMqpkSnJAuAc+UtIKMuI4lv9e1y4LfI1iYm6NkEUHhONppU59smhxHLzb3Ww7YpDffbp5IgDTAiJztA==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.1
'@tiptap/extension-list-item@3.20.1':
resolution: {integrity: sha512-tzgnyTW82lYJkUnadYbatwkI9dLz/OWRSWuFpQPRje/ItmFMWuQ9c9NDD8qLbXPdEYnvrgSAA+ipCD/1G0qA0Q==}
peerDependencies:
'@tiptap/extension-list': ^3.20.1
'@tiptap/extension-list-keymap@3.20.1':
resolution: {integrity: sha512-Dr0xsQKx0XPOgDg7xqoWwfv7FFwZ3WeF3eOjqh3rDXlNHMj1v+UW5cj1HLphrsAZHTrVTn2C+VWPJkMZrSbpvQ==}
peerDependencies:
'@tiptap/extension-list': ^3.20.1
'@tiptap/extension-list@3.20.1':
resolution: {integrity: sha512-euBRAn0mkV7R2VEE+AuOt3R0j9RHEMFXamPFmtvTo8IInxDClusrm6mJoDjS8gCGAXsQCRiAe1SCQBPgGbOOwg==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.1
'@tiptap/extension-ordered-list@3.20.1':
resolution: {integrity: sha512-Y+3Ad7OwAdagqdYwCnbqf7/to5ypD4NnUNHA0TXRCs7cAHRA8AdgPoIcGFpaaSpV86oosNU3yfeJouYeroffog==}
peerDependencies:
'@tiptap/extension-list': ^3.20.1
'@tiptap/extension-paragraph@3.20.1':
resolution: {integrity: sha512-QFrAtXNyv7JSnomMQc1nx5AnG9mMznfbYJAbdOQYVdbLtAzTfiTuNPNbQrufy5ZGtGaHxDCoaygu2QEfzaKG+Q==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-placeholder@3.20.1':
resolution: {integrity: sha512-k+jfbCugYGuIFBdojukgEopGazIMOgHrw46FnyN2X/6ICOIjQP2rh2ObslrsUOsJYoEevxCsNF9hZl1HvWX66g==}
peerDependencies:
'@tiptap/extensions': ^3.20.1
'@tiptap/extension-strike@3.20.1':
resolution: {integrity: sha512-EYgyma10lpsY+rwbVQL9u+gA7hBlKLSMFH7Zgd37FSxukOjr+HE8iKPQQ+SwbGejyDsPlLT8Z5Jnuxo5Ng90Pg==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-text@3.20.1':
resolution: {integrity: sha512-7PlIbYW8UenV6NPOXHmv8IcmPGlGx6HFq66RmkJAOJRPXPkTLAiX0N8rQtzUJ6jDEHqoJpaHFEHJw0xzW1yF+A==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extension-underline@3.20.1':
resolution: {integrity: sha512-fmHvDKzwCgnZUwRreq8tYkb1YyEwgzZ6QQkAQ0CsCRtvRMqzerr3Duz0Als4i8voZTuGDEL3VR6nAJbLAb/wPg==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/extensions@3.20.1':
resolution: {integrity: sha512-JRc/v+OBH0qLTdvQ7HvHWTxGJH73QOf1MC0R8NhOX2QnAbg2mPFv1h+FjGa2gfLGuCXBdWQomjekWkUKbC4e5A==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.1
'@tiptap/markdown@3.20.1':
resolution: {integrity: sha512-dNrtP7kmabDomgjv9G/6+JSFL6WraPaFbmKh1eHSYKdDGvIwBfJnVPTV2VS3bP1OuYJEDJN/2ydtiCHyOTrQsQ==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.1
'@tiptap/pm@3.20.1':
resolution: {integrity: sha512-6kCiGLvpES4AxcEuOhb7HR7/xIeJWMjZlb6J7e8zpiIh5BoQc7NoRdctsnmFEjZvC19bIasccshHQ7H2zchWqw==}
'@tiptap/react@3.20.1':
resolution: {integrity: sha512-UH1NpVpCaZBGB3Yr5N6aTS+rsCMDl9wHfrt/w+6+Gz4KHFZ2OILA82hELxZzhNc1Lmjz8vgCArKcsYql9gbzJA==}
peerDependencies:
'@tiptap/core': ^3.20.1
'@tiptap/pm': ^3.20.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.20.1':
resolution: {integrity: sha512-opqWxL/4OTEiqmVC0wsU4o3JhAf6LycJ2G/gRIZVAIFLljI9uHfpPMTFGxZ5w9IVVJaP5PJysfwW/635kKqkrw==}
'@tokenizer/inflate@0.4.1':
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
engines: {node: '>=18'}
@ -2185,9 +2366,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==}
@ -2235,6 +2425,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/verror@1.10.11':
resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==}
@ -3504,6 +3697,10 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@ -3798,6 +3995,10 @@ packages:
fast-equals@4.0.3:
resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==}
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'}
@ -4665,6 +4866,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.2.7:
resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==}
engines: {node: '>=20.17'}
@ -4779,6 +4986,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==}
@ -4787,6 +4998,11 @@ packages:
engines: {node: '>= 20'}
hasBin: true
marked@17.0.4:
resolution: {integrity: sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==}
engines: {node: '>= 20'}
hasBin: true
matcher@3.0.0:
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
engines: {node: '>=10'}
@ -4844,6 +5060,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'}
@ -5235,6 +5454,9 @@ packages:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
own-keys@1.0.1:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
@ -5547,6 +5769,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.11.0:
resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==}
prosemirror-view@1.41.6:
resolution: {integrity: sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@ -5557,6 +5837,10 @@ packages:
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
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'}
@ -5805,6 +6089,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
roughjs@4.6.6:
resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
@ -6380,6 +6667,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
@ -7654,22 +7944,22 @@ snapshots:
fastq: 1.20.1
glob: 13.0.2
'@floating-ui/core@1.7.4':
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/utils': 0.2.11
'@floating-ui/dom@1.7.5':
'@floating-ui/dom@1.7.6':
dependencies:
'@floating-ui/core': 1.7.4
'@floating-ui/utils': 0.2.10
'@floating-ui/core': 1.7.5
'@floating-ui/utils': 0.2.11
'@floating-ui/react-dom@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/dom': 1.7.5
'@floating-ui/dom': 1.7.6
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@floating-ui/utils@0.2.10': {}
'@floating-ui/utils@0.2.11': {}
'@gar/promisify@1.1.3': {}
@ -8461,6 +8751,8 @@ snapshots:
'@radix-ui/rect@1.1.1': {}
'@remirror/core-constants@3.0.0': {}
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.55.1':
@ -8565,6 +8857,193 @@ snapshots:
'@tanstack/virtual-core@3.13.18': {}
'@tiptap/core@3.20.1(@tiptap/pm@3.20.1)':
dependencies:
'@tiptap/pm': 3.20.1
'@tiptap/extension-blockquote@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-bold@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-bubble-menu@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)':
dependencies:
'@floating-ui/dom': 1.7.6
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
optional: true
'@tiptap/extension-bullet-list@3.20.1(@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/extension-list': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
'@tiptap/extension-code@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-document@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-dropcursor@3.20.1(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/extensions': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-floating-menu@3.20.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)':
dependencies:
'@floating-ui/dom': 1.7.6
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
optional: true
'@tiptap/extension-gapcursor@3.20.1(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/extensions': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-hard-break@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-heading@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-horizontal-rule@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
'@tiptap/extension-italic@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-link@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
linkifyjs: 4.3.2
'@tiptap/extension-list-item@3.20.1(@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/extension-list': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-list-keymap@3.20.1(@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/extension-list': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
'@tiptap/extension-ordered-list@3.20.1(@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/extension-list': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-paragraph@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-placeholder@3.20.1(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/extensions': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-strike@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-text@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-underline@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
'@tiptap/markdown@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
marked: 17.0.4
'@tiptap/pm@3.20.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.6)
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
'@tiptap/react@3.20.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@types/use-sync-external-store': 0.0.6
fast-equals: 5.4.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.6.0(react@18.3.1)
optionalDependencies:
'@tiptap/extension-bubble-menu': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-floating-menu': 3.20.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
transitivePeerDependencies:
- '@floating-ui/dom'
'@tiptap/starter-kit@3.20.1':
dependencies:
'@tiptap/core': 3.20.1(@tiptap/pm@3.20.1)
'@tiptap/extension-blockquote': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-bold': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-bullet-list': 3.20.1(@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))
'@tiptap/extension-code': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-code-block': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-document': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-dropcursor': 3.20.1(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))
'@tiptap/extension-gapcursor': 3.20.1(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))
'@tiptap/extension-hard-break': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-heading': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-horizontal-rule': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-italic': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-link': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-list': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/extension-list-item': 3.20.1(@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))
'@tiptap/extension-list-keymap': 3.20.1(@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))
'@tiptap/extension-ordered-list': 3.20.1(@tiptap/extension-list@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))
'@tiptap/extension-paragraph': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-strike': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-text': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extension-underline': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))
'@tiptap/extensions': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)
'@tiptap/pm': 3.20.1
'@tokenizer/inflate@0.4.1':
dependencies:
debug: 4.4.3
@ -8763,10 +9242,19 @@ snapshots:
dependencies:
'@types/node': 25.0.7
'@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@18.19.130':
@ -8821,6 +9309,8 @@ snapshots:
'@types/unist@3.0.3': {}
'@types/use-sync-external-store@0.0.6': {}
'@types/verror@1.10.11':
optional: true
@ -10339,6 +10829,8 @@ snapshots:
dependencies:
once: 1.4.0
entities@4.5.0: {}
entities@6.0.1: {}
env-paths@2.2.1: {}
@ -10850,6 +11342,8 @@ snapshots:
fast-equals@4.0.3: {}
fast-equals@5.4.0: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -11865,6 +12359,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.2.7:
dependencies:
commander: 14.0.3
@ -12009,10 +12509,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@16.4.2: {}
marked@17.0.4: {}
matcher@3.0.0:
dependencies:
escape-string-regexp: 4.0.0
@ -12179,6 +12690,8 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
mdurl@2.0.0: {}
media-typer@1.1.0: {}
merge-descriptors@2.0.0: {}
@ -12703,6 +13216,8 @@ snapshots:
strip-ansi: 6.0.1
wcwidth: 1.0.1
orderedmap@2.1.1: {}
own-keys@1.0.1:
dependencies:
get-intrinsic: 1.3.0
@ -12964,6 +13479,109 @@ snapshots:
property-information@7.1.0: {}
prosemirror-changeset@2.4.0:
dependencies:
prosemirror-transform: 1.11.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.11.0
prosemirror-dropcursor@1.8.2:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
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.6
prosemirror-history@1.5.0:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.1:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.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.11.0
prosemirror-state@1.4.4:
dependencies:
prosemirror-model: 1.25.4
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
prosemirror-tables@1.8.5:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6):
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.6
prosemirror-transform@1.11.0:
dependencies:
prosemirror-model: 1.25.4
prosemirror-view@1.41.6:
dependencies:
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@ -12976,6 +13594,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:
@ -13306,6 +13926,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.55.1
fsevents: 2.3.3
rope-sequence@1.3.4: {}
roughjs@4.6.6:
dependencies:
hachure-fill: 0.5.2
@ -14005,6 +14627,8 @@ snapshots:
typescript@5.9.3: {}
uc.micro@2.1.0: {}
ufo@1.6.3: {}
uglify-js@3.19.3:

View file

@ -22,7 +22,7 @@ import {
import { ExpandableContent } from '@renderer/components/ui/ExpandableContent';
import { Input } from '@renderer/components/ui/input';
import { MemberSelect } from '@renderer/components/ui/MemberSelect';
import { Textarea } from '@renderer/components/ui/textarea';
import { TiptapEditor } from '@renderer/components/ui/tiptap';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getLastReadTimestamp } from '@renderer/services/commentReadStorage';
import { useStore } from '@renderer/store';
@ -132,7 +132,6 @@ export const TaskDetailDialog = ({
// Inline editing: description
const [editingDescription, setEditingDescription] = useState(false);
const [descriptionDraft, setDescriptionDraft] = useState('');
const [descriptionPreview, setDescriptionPreview] = useState(false);
const [savingDescription, setSavingDescription] = useState(false);
const startEditSubject = useCallback(() => {
@ -160,7 +159,6 @@ export const TaskDetailDialog = ({
const startEditDescription = useCallback(() => {
if (!currentTask) return;
setDescriptionDraft(currentTask.description ?? '');
setDescriptionPreview(false);
setEditingDescription(true);
}, [currentTask]);
@ -715,51 +713,16 @@ export const TaskDetailDialog = ({
>
{editingDescription ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<button
type="button"
className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${
!descriptionPreview
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
}`}
onClick={() => setDescriptionPreview(false)}
>
<Pencil size={12} />
Edit
</button>
<button
type="button"
className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${
descriptionPreview
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
}`}
onClick={() => setDescriptionPreview(true)}
>
<Eye size={12} />
Preview
</button>
</div>
{descriptionPreview ? (
<div className="max-h-[200px] overflow-y-auto rounded border border-[var(--color-border)] p-2">
{descriptionDraft.trim() ? (
<MarkdownViewer content={descriptionDraft} maxHeight="max-h-[180px]" />
) : (
<p className="text-xs text-[var(--color-text-muted)]">Nothing to preview</p>
)}
</div>
) : (
<Textarea
autoFocus
value={descriptionDraft}
onChange={(e) => setDescriptionDraft(e.target.value)}
disabled={savingDescription}
rows={6}
className="text-xs"
placeholder="Task description (supports markdown)"
/>
)}
<TiptapEditor
content={descriptionDraft}
onChange={setDescriptionDraft}
placeholder="Task description (supports markdown)"
autoFocus
minHeight="120px"
maxHeight="200px"
toolbar
disabled={savingDescription}
/>
<div className="flex items-center gap-2">
<Button
size="sm"

View file

@ -0,0 +1,76 @@
import { useCurrentEditor, useEditorState } from '@tiptap/react';
import { BubbleMenu } from '@tiptap/react/menus';
import { Bold, Code, Italic, Strikethrough } from 'lucide-react';
import { cn } from '@renderer/lib/utils';
export function TiptapBubbleMenu() {
const { editor } = useCurrentEditor();
const state = useEditorState({
editor,
selector: ({ editor: e }) => {
if (!e) return null;
return {
isBold: e.isActive('bold'),
isItalic: e.isActive('italic'),
isStrike: e.isActive('strike'),
isCode: e.isActive('code'),
};
},
});
if (!editor || !state) return null;
const btnClass = (active: boolean) =>
cn(
'rounded p-1 transition-colors text-[var(--color-text-muted)]',
'hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text-secondary)]',
active && 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
);
return (
<BubbleMenu
editor={editor}
options={{ placement: 'top', offset: 8 }}
className={cn(
'flex items-center gap-0.5 rounded-lg p-1 shadow-lg',
'border border-[var(--color-border-emphasis)]',
'bg-[var(--color-surface-overlay)]'
)}
>
<button
type="button"
className={btnClass(state.isBold)}
onClick={() => editor.chain().focus().toggleBold().run()}
aria-label="Bold"
>
<Bold size={12} />
</button>
<button
type="button"
className={btnClass(state.isItalic)}
onClick={() => editor.chain().focus().toggleItalic().run()}
aria-label="Italic"
>
<Italic size={12} />
</button>
<button
type="button"
className={btnClass(state.isStrike)}
onClick={() => editor.chain().focus().toggleStrike().run()}
aria-label="Strike"
>
<Strikethrough size={12} />
</button>
<button
type="button"
className={btnClass(state.isCode)}
onClick={() => editor.chain().focus().toggleCode().run()}
aria-label="Code"
>
<Code size={12} />
</button>
</BubbleMenu>
);
}

View file

@ -0,0 +1,72 @@
import { EditorContent, EditorContext } from '@tiptap/react';
import { useMemo } from 'react';
import { cn } from '@renderer/lib/utils';
import { TiptapBubbleMenu } from './TiptapBubbleMenu';
import { TiptapToolbar } from './TiptapToolbar';
import type { TiptapEditorProps } from './types';
import { useTiptapEditor } from './useTiptapEditor';
import './tiptapStyles.css';
export function TiptapEditor({
content,
onChange,
placeholder,
editable = true,
minHeight,
maxHeight,
autoFocus = false,
toolbar = true,
bubbleMenu = true,
extensions,
className,
disabled = false,
}: TiptapEditorProps) {
const isEditable = editable && !disabled;
const { editor } = useTiptapEditor({
content,
onChange,
editable: isEditable,
autoFocus,
placeholder,
extensions,
});
// EditorContext.Provider — v3 паттерн для sharing editor instance
// TiptapToolbar и TiptapBubbleMenu получают editor через useCurrentEditor()
const providerValue = useMemo(() => ({ editor }), [editor]);
if (!editor) return null;
const showToolbar = toolbar !== false && isEditable;
const showBubble = bubbleMenu && isEditable;
const toolbarConfig = typeof toolbar === 'object' ? toolbar : undefined;
return (
<EditorContext.Provider value={providerValue}>
<div
className={cn(
'tiptap-editor-wrapper rounded-md border border-[var(--color-border)] bg-transparent',
disabled && 'cursor-not-allowed opacity-50',
className
)}
>
{showToolbar && <TiptapToolbar config={toolbarConfig} />}
<div
className="overflow-y-auto px-3 py-2"
style={{
minHeight: minHeight ?? '60px',
maxHeight: maxHeight ?? 'none',
}}
>
<EditorContent editor={editor} />
</div>
{showBubble && <TiptapBubbleMenu />}
</div>
</EditorContext.Provider>
);
}

View file

@ -0,0 +1,271 @@
import { useCurrentEditor, useEditorState } from '@tiptap/react';
import {
Bold,
Code,
FileCode2,
Heading1,
Heading2,
Heading3,
Italic,
List,
ListOrdered,
Minus,
Quote,
Redo2,
Strikethrough,
Undo2,
} from 'lucide-react';
import { cn } from '@renderer/lib/utils';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
import type { ToolbarConfig } from './types';
interface TiptapToolbarProps {
config?: ToolbarConfig;
}
function ToolbarButton({
icon,
active,
disabled,
onClick,
label,
}: {
icon: React.ReactNode;
active?: boolean;
disabled?: boolean;
onClick: () => void;
label: string;
}) {
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onClick}
disabled={disabled}
aria-label={label}
aria-pressed={active}
className={cn(
'rounded p-1.5 transition-colors',
'text-[var(--color-text-muted)]',
'hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text-secondary)]',
active && 'bg-[var(--color-surface-raised)] text-[var(--color-text)]',
disabled &&
'cursor-not-allowed opacity-30 hover:bg-transparent hover:text-[var(--color-text-muted)]'
)}
>
{icon}
</button>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{label}
</TooltipContent>
</Tooltip>
);
}
function Divider() {
return <div className="mx-0.5 h-4 w-px bg-[var(--color-border)]" />;
}
export function TiptapToolbar({ config }: TiptapToolbarProps) {
const { editor } = useCurrentEditor();
// useEditorState — КРИТИЧНО для v3!
// Без этого active state НЕ обновляется (shouldRerenderOnTransaction: false)
const state = useEditorState({
editor,
selector: ({ editor: e }) => {
if (!e) return null;
return {
isBold: e.isActive('bold'),
isItalic: e.isActive('italic'),
isStrike: e.isActive('strike'),
isCode: e.isActive('code'),
isCodeBlock: e.isActive('codeBlock'),
isBulletList: e.isActive('bulletList'),
isOrderedList: e.isActive('orderedList'),
isBlockquote: e.isActive('blockquote'),
headingLevel:
([1, 2, 3] as const).find((l) => e.isActive('heading', { level: l })) ?? 0,
canUndo: e.can().undo(),
canRedo: e.can().redo(),
};
},
});
if (!editor || !state) return null;
const c = {
bold: true,
italic: true,
strike: true,
code: true,
codeBlock: true,
heading: { levels: [1, 2, 3] as (1 | 2 | 3)[] },
bulletList: true,
orderedList: true,
blockquote: true,
horizontalRule: true,
undoRedo: true,
...config,
};
const headingLevels = c.heading === false ? [] : (c.heading?.levels ?? [1, 2, 3]);
const groups: React.ReactNode[][] = [];
// Group 1: Text formatting
const textGroup: React.ReactNode[] = [];
if (c.bold)
textGroup.push(
<ToolbarButton
key="bold"
icon={<Bold size={14} />}
active={state.isBold}
onClick={() => editor.chain().focus().toggleBold().run()}
label="Bold (⌘B)"
/>
);
if (c.italic)
textGroup.push(
<ToolbarButton
key="italic"
icon={<Italic size={14} />}
active={state.isItalic}
onClick={() => editor.chain().focus().toggleItalic().run()}
label="Italic (⌘I)"
/>
);
if (c.strike)
textGroup.push(
<ToolbarButton
key="strike"
icon={<Strikethrough size={14} />}
active={state.isStrike}
onClick={() => editor.chain().focus().toggleStrike().run()}
label="Strikethrough (⌘⇧S)"
/>
);
if (textGroup.length) groups.push(textGroup);
// Group 2: Code
const codeGroup: React.ReactNode[] = [];
if (c.code)
codeGroup.push(
<ToolbarButton
key="code"
icon={<Code size={14} />}
active={state.isCode}
onClick={() => editor.chain().focus().toggleCode().run()}
label="Code (⌘E)"
/>
);
if (c.codeBlock)
codeGroup.push(
<ToolbarButton
key="codeBlock"
icon={<FileCode2 size={14} />}
active={state.isCodeBlock}
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
label="Code Block (⌘⌥C)"
/>
);
if (codeGroup.length) groups.push(codeGroup);
// Group 3: Headings
const headingIcons = { 1: Heading1, 2: Heading2, 3: Heading3 } as const;
const headingGroup: React.ReactNode[] = headingLevels.map((level) => {
const Icon = headingIcons[level];
return (
<ToolbarButton
key={`h${level}`}
icon={<Icon size={14} />}
active={state.headingLevel === level}
onClick={() => editor.chain().focus().toggleHeading({ level }).run()}
label={`Heading ${level}`}
/>
);
});
if (headingGroup.length) groups.push(headingGroup);
// Group 4: Lists
const listGroup: React.ReactNode[] = [];
if (c.bulletList)
listGroup.push(
<ToolbarButton
key="bullet"
icon={<List size={14} />}
active={state.isBulletList}
onClick={() => editor.chain().focus().toggleBulletList().run()}
label="Bullet List (⌘⇧8)"
/>
);
if (c.orderedList)
listGroup.push(
<ToolbarButton
key="ordered"
icon={<ListOrdered size={14} />}
active={state.isOrderedList}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
label="Ordered List (⌘⇧7)"
/>
);
if (listGroup.length) groups.push(listGroup);
// Group 5: Blocks
const blockGroup: React.ReactNode[] = [];
if (c.blockquote)
blockGroup.push(
<ToolbarButton
key="quote"
icon={<Quote size={14} />}
active={state.isBlockquote}
onClick={() => editor.chain().focus().toggleBlockquote().run()}
label="Blockquote (⌘⇧B)"
/>
);
if (c.horizontalRule)
blockGroup.push(
<ToolbarButton
key="hr"
icon={<Minus size={14} />}
onClick={() => editor.chain().focus().setHorizontalRule().run()}
label="Horizontal Rule"
/>
);
if (blockGroup.length) groups.push(blockGroup);
// Group 6: Undo/Redo
if (c.undoRedo) {
groups.push([
<ToolbarButton
key="undo"
icon={<Undo2 size={14} />}
disabled={!state.canUndo}
onClick={() => editor.chain().focus().undo().run()}
label="Undo (⌘Z)"
/>,
<ToolbarButton
key="redo"
icon={<Redo2 size={14} />}
disabled={!state.canRedo}
onClick={() => editor.chain().focus().redo().run()}
label="Redo (⌘⇧Z)"
/>,
]);
}
return (
<div className="flex flex-wrap items-center gap-0.5 border-b border-[var(--color-border)] px-1.5 py-1">
{groups.map((group, i) => (
<div key={i} className="contents">
{i > 0 && <Divider />}
{group}
</div>
))}
</div>
);
}

View file

@ -0,0 +1,3 @@
export { TiptapEditor } from './TiptapEditor';
export type { EditorPreset, TiptapEditorProps, ToolbarConfig } from './types';
export { EDITOR_PRESETS } from './presets';

View file

@ -0,0 +1,46 @@
import type { TiptapEditorProps } from './types';
export const EDITOR_PRESETS = {
full: {
toolbar: true,
bubbleMenu: true,
minHeight: '120px',
maxHeight: '400px',
},
compact: {
toolbar: {
bold: true,
italic: true,
strike: true,
code: true,
bulletList: true,
orderedList: true,
undoRedo: true,
codeBlock: false,
heading: false,
blockquote: false,
horizontalRule: false,
},
bubbleMenu: true,
minHeight: '60px',
maxHeight: '200px',
},
minimal: {
toolbar: {
bold: true,
italic: true,
code: true,
strike: false,
codeBlock: false,
heading: false,
bulletList: false,
orderedList: false,
blockquote: false,
horizontalRule: false,
undoRedo: false,
},
bubbleMenu: false,
minHeight: '40px',
maxHeight: '120px',
},
} as const satisfies Record<string, Partial<TiptapEditorProps>>;

View file

@ -0,0 +1,235 @@
/* =============================================================================
TiptapEditor ProseMirror DOM styles
Значения извлечены из MarkdownViewer.tsx для pixel-perfect совпадения
============================================================================= */
/* === Placeholder === */
.tiptap-editor-wrapper .tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
color: var(--color-text-muted);
font-size: 0.875rem; /* text-sm */
}
/* === Base text === */
.tiptap-editor-wrapper .tiptap {
outline: none;
font-size: 0.875rem; /* text-sm */
line-height: 1.625; /* leading-relaxed */
color: var(--prose-body);
}
/* === Headings === */
.tiptap-editor-wrapper .tiptap h1 {
font-size: 1.25rem; /* text-xl */
font-weight: 600; /* font-semibold */
color: var(--prose-heading);
margin-top: 1rem; /* mt-4 */
margin-bottom: 0.5rem; /* mb-2 */
}
.tiptap-editor-wrapper .tiptap h2 {
font-size: 1.125rem; /* text-lg */
font-weight: 600;
color: var(--prose-heading);
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.tiptap-editor-wrapper .tiptap h3 {
font-size: 1rem; /* text-base */
font-weight: 600;
color: var(--prose-heading);
margin-top: 0.75rem; /* mt-3 */
margin-bottom: 0.5rem;
}
.tiptap-editor-wrapper .tiptap :is(h1, h2, h3):first-child {
margin-top: 0;
}
/* === Paragraphs === */
.tiptap-editor-wrapper .tiptap p {
margin-top: 0.5rem; /* my-2 */
margin-bottom: 0.5rem;
color: var(--prose-body);
}
.tiptap-editor-wrapper .tiptap p:first-child {
margin-top: 0;
}
.tiptap-editor-wrapper .tiptap p:last-child {
margin-bottom: 0;
}
/* === Strong/Em/Strike === */
.tiptap-editor-wrapper .tiptap strong {
font-weight: 600;
color: var(--prose-heading);
}
.tiptap-editor-wrapper .tiptap em {
font-style: italic;
}
.tiptap-editor-wrapper .tiptap s {
text-decoration: line-through;
}
/* === Inline code === */
.tiptap-editor-wrapper .tiptap code {
border-radius: 0.25rem; /* rounded */
padding: 0.125rem 0.375rem; /* px-1.5 py-0.5 */
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.75rem; /* text-xs */
background-color: var(--prose-code-bg);
color: var(--prose-code-text);
word-break: break-all;
}
/* === Code blocks (pre > code) === */
.tiptap-editor-wrapper .tiptap pre {
margin-top: 0.75rem; /* my-3 */
margin-bottom: 0.75rem;
max-width: 100%;
overflow-x: auto;
border-radius: 0.5rem; /* rounded-lg */
padding: 0.75rem; /* p-3 */
font-size: 0.75rem; /* text-xs */
line-height: 1.625;
background-color: var(--prose-pre-bg);
border: 1px solid var(--prose-pre-border);
}
.tiptap-editor-wrapper .tiptap pre code {
background: none;
padding: 0;
border-radius: 0;
font-size: inherit;
color: var(--color-text);
word-break: normal;
}
/* === Blockquote === */
.tiptap-editor-wrapper .tiptap blockquote {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
border-left: 4px solid var(--prose-blockquote-border);
padding-left: 1rem; /* pl-4 */
font-style: italic;
color: var(--prose-muted);
}
/* Reset inner margins at blockquote boundaries */
.tiptap-editor-wrapper .tiptap blockquote > :first-child {
margin-top: 0;
}
.tiptap-editor-wrapper .tiptap blockquote > :last-child {
margin-bottom: 0;
}
/* === Lists === */
.tiptap-editor-wrapper .tiptap ul {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
list-style-type: disc;
padding-left: 1.25rem; /* pl-5 */
color: var(--prose-body);
}
.tiptap-editor-wrapper .tiptap ol {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
list-style-type: decimal;
padding-left: 1.25rem;
color: var(--prose-body);
}
/* Nested list bullet styles: disc → circle → square */
.tiptap-editor-wrapper .tiptap ul ul {
list-style-type: circle;
}
.tiptap-editor-wrapper .tiptap ul ul ul {
list-style-type: square;
}
/* Nested lists inside li — reduce top margin */
.tiptap-editor-wrapper .tiptap li > ul,
.tiptap-editor-wrapper .tiptap li > ol {
margin-top: 0.25rem;
margin-bottom: 0;
}
.tiptap-editor-wrapper .tiptap li {
font-size: 0.875rem;
color: var(--prose-body);
}
.tiptap-editor-wrapper .tiptap li + li {
margin-top: 0.25rem; /* space-y-1 */
}
/* ProseMirror wraps li content in <p> — reset paragraph margins inside list items */
.tiptap-editor-wrapper .tiptap li > p {
margin-top: 0;
margin-bottom: 0;
}
.tiptap-editor-wrapper .tiptap li > p + p {
margin-top: 0.25rem;
}
/* === Horizontal rule === */
.tiptap-editor-wrapper .tiptap hr {
margin-top: 1rem;
margin-bottom: 1rem;
border-color: var(--prose-table-border);
}
/* === Links === */
.tiptap-editor-wrapper .tiptap a {
color: var(--prose-link);
text-decoration: none;
cursor: pointer;
}
.tiptap-editor-wrapper .tiptap a:hover {
text-decoration: underline;
}
/* === Gap cursor — ProseMirror invisible without styles === */
.tiptap-editor-wrapper .tiptap .ProseMirror-gapcursor {
position: relative;
}
.tiptap-editor-wrapper .tiptap .ProseMirror-gapcursor::after {
content: '';
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid var(--color-text);
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
/* === Selection styles — для readonly mode === */
.tiptap-editor-wrapper .tiptap ::selection {
background-color: rgba(99, 102, 241, 0.3); /* indigo with opacity */
}
/* === Focus ring для wrapper при фокусе внутри === */
.tiptap-editor-wrapper:focus-within {
outline: none;
box-shadow: 0 0 0 1px var(--color-border-emphasis);
}

View file

@ -0,0 +1,32 @@
import type { Extension } from '@tiptap/react';
export interface ToolbarConfig {
bold?: boolean;
italic?: boolean;
strike?: boolean;
code?: boolean;
codeBlock?: boolean;
heading?: false | { levels: (1 | 2 | 3)[] };
bulletList?: boolean;
orderedList?: boolean;
blockquote?: boolean;
horizontalRule?: boolean;
undoRedo?: boolean;
}
export interface TiptapEditorProps {
content: string;
onChange: (markdown: string) => void;
placeholder?: string;
editable?: boolean;
minHeight?: string;
maxHeight?: string;
autoFocus?: boolean;
toolbar?: boolean | ToolbarConfig;
bubbleMenu?: boolean;
extensions?: Extension[];
className?: string;
disabled?: boolean;
}
export type EditorPreset = 'full' | 'compact' | 'minimal';

View file

@ -0,0 +1,97 @@
import Placeholder from '@tiptap/extension-placeholder';
import { Markdown } from '@tiptap/markdown';
import { type Extension, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect, useRef } from 'react';
interface UseTiptapEditorOptions {
content: string;
onChange?: (markdown: string) => void;
editable?: boolean;
autoFocus?: boolean;
placeholder?: string;
extensions?: Extension[];
}
export function useTiptapEditor({
content,
onChange,
editable = true,
autoFocus = false,
placeholder = '',
extensions: extraExtensions = [],
}: UseTiptapEditorOptions) {
// Ref для стабильной ссылки — избегаем stale closure в onUpdate
const onChangeRef = useRef(onChange);
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
// Double safety: ref guard для programmatic setContent (emitUpdate: false — основной механизм)
const isProgrammaticUpdate = useRef(false);
const editor = useEditor({
extensions: [
StarterKit.configure({}),
Markdown.configure({
markedOptions: { gfm: true },
}),
Placeholder.configure({
placeholder,
showOnlyWhenEditable: true,
}),
...extraExtensions,
],
content,
contentType: 'markdown',
editable,
shouldRerenderOnTransaction: false, // v3 performance — toolbar использует useEditorState
autofocus: autoFocus ? 'end' : false,
enableContentCheck: true,
onContentError: ({ error }) => {
console.error('[TiptapEditor] Content error:', error);
},
onUpdate: ({ editor: e }) => {
if (isProgrammaticUpdate.current) return;
try {
const md = e.getMarkdown();
onChangeRef.current?.(md);
} catch {
console.error('[TiptapEditor] getMarkdown() failed, skipping onChange');
}
},
});
// === Content sync ===
// Когда внешний content меняется, обновляем editor БЕЗ триггера onUpdate
useEffect(() => {
if (!editor || editor.isDestroyed) return;
let currentMd: string;
try {
currentMd = editor.getMarkdown();
} catch {
return;
}
if (currentMd.trim() === content.trim()) return;
isProgrammaticUpdate.current = true;
try {
editor.commands.setContent(content, { contentType: 'markdown', emitUpdate: false });
} catch {
console.error('[TiptapEditor] setContent() failed');
} finally {
isProgrammaticUpdate.current = false;
}
}, [content, editor]);
// === Editable toggle ===
useEffect(() => {
if (editor && !editor.isDestroyed) {
editor.setEditable(editable);
}
}, [editor, editable]);
return { editor };
}