feat: replace textarea with TipTap rich text editor

- Add TipTap WYSIWYG editor (headless @tiptap/core) with StarterKit,
  TaskList, TaskItem, Link, Placeholder, and tiptap-markdown extensions
- Always-visible formatting toolbar with undo/redo, H1-H3, bold, italic,
  strikethrough, inline code, code block, blockquote, bullet/ordered/task
  lists, horizontal rule, and link
- Raw markdown toggle for power users
- Render rich text (HTML) on note cards instead of stripped plain text
- Update all e2e tests for TipTap contenteditable editor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-05 22:24:33 +01:00
co-authored by Claude Opus 4.6
parent 19c2c7435a
commit 6a2e54497f
12 changed files with 954 additions and 54 deletions
+9
View File
@@ -23,17 +23,26 @@
},
"dependencies": {
"@sveltejs/adapter-node": "^5.0.0",
"@tiptap/core": "^3.20.0",
"@tiptap/extension-link": "^3.20.0",
"@tiptap/extension-placeholder": "^3.20.0",
"@tiptap/extension-task-item": "^3.20.0",
"@tiptap/extension-task-list": "^3.20.0",
"@tiptap/pm": "^3.20.0",
"@tiptap/starter-kit": "^3.20.0",
"argon2": "^0.43.0",
"better-sqlite3": "^11.0.0",
"drizzle-orm": "^0.44.0",
"idb": "^8.0.0",
"markdown-it": "^14.0.0",
"tiptap-markdown": "^0.9.0",
"uuid": "^11.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.0.0",
"@types/better-sqlite3": "^7.6.0",
"@types/markdown-it": "^14.0.0",
+585
View File
@@ -11,6 +11,27 @@ importers:
'@sveltejs/adapter-node':
specifier: ^5.0.0
version: 5.5.4(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))
'@tiptap/core':
specifier: ^3.20.0
version: 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-link':
specifier: ^3.20.0
version: 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-placeholder':
specifier: ^3.20.0
version: 3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/extension-task-item':
specifier: ^3.20.0
version: 3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/extension-task-list':
specifier: ^3.20.0
version: 3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/pm':
specifier: ^3.20.0
version: 3.20.0
'@tiptap/starter-kit':
specifier: ^3.20.0
version: 3.20.0
argon2:
specifier: ^0.43.0
version: 0.43.1
@@ -26,6 +47,9 @@ importers:
markdown-it:
specifier: ^14.0.0
version: 14.1.1
tiptap-markdown:
specifier: ^0.9.0
version: 0.9.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
uuid:
specifier: ^11.0.0
version: 11.1.0
@@ -39,6 +63,9 @@ importers:
'@sveltejs/vite-plugin-svelte':
specifier: ^6.2.4
version: 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@4.2.1)
'@tailwindcss/vite':
specifier: ^4.0.0
version: 4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
@@ -1075,6 +1102,9 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@rollup/plugin-babel@5.3.1':
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'}
@@ -1425,11 +1455,157 @@ packages:
resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==}
engines: {node: '>= 20'}
'@tailwindcss/typography@0.5.19':
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tailwindcss/vite@4.2.1':
resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
'@tiptap/core@3.20.0':
resolution: {integrity: sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==}
peerDependencies:
'@tiptap/pm': ^3.20.0
'@tiptap/extension-blockquote@3.20.0':
resolution: {integrity: sha512-LQzn6aGtL4WXz2+rYshl/7/VnP2qJTpD7fWL96GXAzhqviPEY1bJES7poqJb3MU/gzl8VJUVzVzU1VoVfUKlbA==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-bold@3.20.0':
resolution: {integrity: sha512-sQklEWiyf58yDjiHtm5vmkVjfIc/cBuSusmCsQ0q9vGYnEF1iOHKhGpvnCeEXNeqF3fiJQRlquzt/6ymle3Iwg==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-bullet-list@3.20.0':
resolution: {integrity: sha512-OcKMeopBbqWzhSi6o8nNz0aayogg1sfOAhto3NxJu3Ya32dwBFqmHXSYM6uW4jOphNvVPyjiq9aNRh3qTdd1dw==}
peerDependencies:
'@tiptap/extension-list': ^3.20.0
'@tiptap/extension-code-block@3.20.0':
resolution: {integrity: sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/pm': ^3.20.0
'@tiptap/extension-code@3.20.0':
resolution: {integrity: sha512-TYDWFeSQ9umiyrqsT6VecbuhL8XIHkUhO+gEk0sVvH67ZLwjFDhAIIgWIr1/dbIGPcvMZM19E7xUUhAdIaXaOQ==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-document@3.20.0':
resolution: {integrity: sha512-oJfLIG3vAtZo/wg29WiBcyWt22KUgddpP8wqtCE+kY5Dw8znLR9ehNmVWlSWJA5OJUMO0ntAHx4bBT+I2MBd5w==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-dropcursor@3.20.0':
resolution: {integrity: sha512-d+cxplRlktVgZPwatnc34IArlppM0IFKS1J5wLk+ba1jidizsbMVh45tP/BTK2flhyfRqcNoB5R0TArhUpbkNQ==}
peerDependencies:
'@tiptap/extensions': ^3.20.0
'@tiptap/extension-gapcursor@3.20.0':
resolution: {integrity: sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w==}
peerDependencies:
'@tiptap/extensions': ^3.20.0
'@tiptap/extension-hard-break@3.20.0':
resolution: {integrity: sha512-rqvhMOw4f+XQmEthncbvDjgLH6fz8L9splnKZC7OeS0eX8b0qd7+xI1u5kyxF3KA2Z0BnigES++jjWuecqV6mA==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-heading@3.20.0':
resolution: {integrity: sha512-JgJhurnCe3eN6a0lEsNQM/46R1bcwzwWWZEFDSb1P9dR8+t1/5v7cMZWsSInpD7R4/74iJn0+M5hcXLwCmBmYA==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-horizontal-rule@3.20.0':
resolution: {integrity: sha512-6uvcutFMv+9wPZgptDkbRDjAm3YVxlibmkhWD5GuaWwS9L/yUtobpI3GycujRSUZ8D3q6Q9J7LqpmQtQRTalWA==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/pm': ^3.20.0
'@tiptap/extension-italic@3.20.0':
resolution: {integrity: sha512-/DhnKQF8yN8RxtuL8abZ28wd5281EaGoE2Oha35zXSOF1vNYnbyt8Ymkv/7u1BcWEWTvRPgaju0YCGXisPRLYw==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-link@3.20.0':
resolution: {integrity: sha512-qI/5A+R0ZWBxo/8HxSn1uOyr7odr3xHBZ/gzOR1GUJaZqjlJxkWFX0RtXMbLKEGEvT25o345cF7b0wFznEh8qA==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/pm': ^3.20.0
'@tiptap/extension-list-item@3.20.0':
resolution: {integrity: sha512-qEtjaaGPuqaFB4VpLrGDoIe9RHnckxPfu6d3rc22ap6TAHCDyRv05CEyJogqccnFceG/v5WN4znUBER8RWnWHA==}
peerDependencies:
'@tiptap/extension-list': ^3.20.0
'@tiptap/extension-list-keymap@3.20.0':
resolution: {integrity: sha512-Z4GvKy04Ms4cLFN+CY6wXswd36xYsT2p/YL0V89LYFMZTerOeTjFYlndzn6svqL8NV1PRT5Diw4WTTxJSmcJPA==}
peerDependencies:
'@tiptap/extension-list': ^3.20.0
'@tiptap/extension-list@3.20.0':
resolution: {integrity: sha512-+V0/gsVWAv+7vcY0MAe6D52LYTIicMSHw00wz3ISZgprSb2yQhJ4+4gurOnUrQ4Du3AnRQvxPROaofwxIQ66WQ==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/pm': ^3.20.0
'@tiptap/extension-ordered-list@3.20.0':
resolution: {integrity: sha512-jVKnJvrizLk7etwBMfyoj6H2GE4M+PD4k7Bwp6Bh1ohBWtfIA1TlngdS842Mx5i1VB2e3UWIwr8ZH46gl6cwMA==}
peerDependencies:
'@tiptap/extension-list': ^3.20.0
'@tiptap/extension-paragraph@3.20.0':
resolution: {integrity: sha512-mM99zK4+RnEXIMCv6akfNATAs0Iija6FgyFA9J9NZ6N4o8y9QiNLLa6HjLpAC+W+VoCgQIekyoF/Q9ftxmAYDQ==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-placeholder@3.20.0':
resolution: {integrity: sha512-ZhYD3L5m16ydSe2z8vqz+RdtAG/iOQaFHHedFct70tKRoLqi2ajF5kgpemu8DwpaRTcyiCN4G99J/+MqehKNjQ==}
peerDependencies:
'@tiptap/extensions': ^3.20.0
'@tiptap/extension-strike@3.20.0':
resolution: {integrity: sha512-0vcTZRRAiDfon3VM1mHBr9EFmTkkUXMhm0Xtdtn0bGe+sIqufyi+hUYTEw93EQOD9XNsPkrud6jzQNYpX2H3AQ==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-task-item@3.20.0':
resolution: {integrity: sha512-UArl5ffRNKyiOGJYNtjF50+A0qgYF2DXIEFGyMgJsbp1KYQpDj+UrezWvnGOHZfX64eBWDIO6Bum5jS7o32dYQ==}
peerDependencies:
'@tiptap/extension-list': ^3.20.0
'@tiptap/extension-task-list@3.20.0':
resolution: {integrity: sha512-Ab2hfun+5xMG6kQhRjL7s16zTe3XaQRXe3/deGo9hnZrAzTQpWVwDN7H36BX8MemrWnAw3PiuwBH5iOrwNRGjA==}
peerDependencies:
'@tiptap/extension-list': ^3.20.0
'@tiptap/extension-text@3.20.0':
resolution: {integrity: sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extension-underline@3.20.0':
resolution: {integrity: sha512-LzNXuy2jwR/y+ymoUqC72TiGzbOCjioIjsDu0MNYpHuHqTWPK5aV9Mh0nbZcYFy/7fPlV1q0W139EbJeYBZEAQ==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/extensions@3.20.0':
resolution: {integrity: sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg==}
peerDependencies:
'@tiptap/core': ^3.20.0
'@tiptap/pm': ^3.20.0
'@tiptap/pm@3.20.0':
resolution: {integrity: sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==}
'@tiptap/starter-kit@3.20.0':
resolution: {integrity: sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw==}
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
@@ -1448,12 +1624,21 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/linkify-it@3.0.5':
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/markdown-it@13.0.9':
resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdurl@1.0.5':
resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
@@ -1673,6 +1858,9 @@ packages:
core-js-compat@3.48.0:
resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1681,6 +1869,11 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -1903,6 +2096,10 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
esm-env@1.2.2:
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
@@ -2322,6 +2519,9 @@ packages:
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
linkifyjs@4.3.2:
resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
@@ -2350,6 +2550,9 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
markdown-it-task-lists@2.1.1:
resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==}
markdown-it@14.1.1:
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
hasBin: true
@@ -2435,6 +2638,9 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
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'}
@@ -2485,6 +2691,10 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14}
@@ -2503,6 +2713,64 @@ packages:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
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.0:
resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==}
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==}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
@@ -2577,6 +2845,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@@ -2797,6 +3068,11 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
tiptap-markdown@0.9.0:
resolution: {integrity: sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==}
peerDependencies:
'@tiptap/core': ^3.0.1
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -2981,6 +3257,9 @@ packages:
jsdom:
optional: true
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
@@ -4007,6 +4286,8 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@remirror/core-constants@3.0.0': {}
'@rollup/plugin-babel@5.3.1(@babel/core@7.29.0)(rollup@2.80.0)':
dependencies:
'@babel/core': 7.29.0
@@ -4285,6 +4566,11 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
'@tailwindcss/typography@0.5.19(tailwindcss@4.2.1)':
dependencies:
postcss-selector-parser: 6.0.10
tailwindcss: 4.2.1
'@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@tailwindcss/node': 4.2.1
@@ -4292,6 +4578,164 @@ snapshots:
tailwindcss: 4.2.1
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
'@tiptap/core@3.20.0(@tiptap/pm@3.20.0)':
dependencies:
'@tiptap/pm': 3.20.0
'@tiptap/extension-blockquote@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-bold@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-bullet-list@3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extension-list': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-code-block@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/pm': 3.20.0
'@tiptap/extension-code@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-document@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-dropcursor@3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extensions': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-gapcursor@3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extensions': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-hard-break@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-heading@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-horizontal-rule@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/pm': 3.20.0
'@tiptap/extension-italic@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-link@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/pm': 3.20.0
linkifyjs: 4.3.2
'@tiptap/extension-list-item@3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extension-list': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-list-keymap@3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extension-list': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/pm': 3.20.0
'@tiptap/extension-ordered-list@3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extension-list': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-paragraph@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-placeholder@3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extensions': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-strike@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-task-item@3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extension-list': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-task-list@3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/extension-list': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-text@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-underline@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/pm': 3.20.0
'@tiptap/pm@3.20.0':
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.0
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/starter-kit@3.20.0':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@tiptap/extension-blockquote': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-bold': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-bullet-list': 3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/extension-code': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-code-block': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-document': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-dropcursor': 3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/extension-gapcursor': 3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/extension-hard-break': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-heading': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-horizontal-rule': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-italic': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-link': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-list': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/extension-list-item': 3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/extension-list-keymap': 3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/extension-ordered-list': 3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
'@tiptap/extension-paragraph': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-strike': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-text': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extension-underline': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/extensions': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
'@tiptap/pm': 3.20.0
'@types/better-sqlite3@7.6.13':
dependencies:
'@types/node': 25.3.3
@@ -4309,13 +4753,22 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/linkify-it@3.0.5': {}
'@types/linkify-it@5.0.0': {}
'@types/markdown-it@13.0.9':
dependencies:
'@types/linkify-it': 3.0.5
'@types/mdurl': 1.0.5
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdurl@1.0.5': {}
'@types/mdurl@2.0.0': {}
'@types/node@25.3.3':
@@ -4553,6 +5006,8 @@ snapshots:
dependencies:
browserslist: 4.28.1
crelt@1.0.6: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -4561,6 +5016,8 @@ snapshots:
crypto-random-string@2.0.0: {}
cssesc@3.0.0: {}
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -4818,6 +5275,8 @@ snapshots:
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
esm-env@1.2.2: {}
esrap@2.2.3:
@@ -5192,6 +5651,8 @@ snapshots:
dependencies:
uc.micro: 2.1.0
linkifyjs@4.3.2: {}
locate-character@3.0.0: {}
lodash.debounce@4.0.8: {}
@@ -5216,6 +5677,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
markdown-it-task-lists@2.1.1: {}
markdown-it@14.1.1:
dependencies:
argparse: 2.0.1
@@ -5284,6 +5747,8 @@ snapshots:
dependencies:
wrappy: 1.0.2
orderedmap@2.1.1: {}
own-keys@1.0.1:
dependencies:
get-intrinsic: 1.3.0
@@ -5321,6 +5786,11 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.5.8:
dependencies:
nanoid: 3.3.11
@@ -5346,6 +5816,109 @@ snapshots:
pretty-bytes@6.1.1: {}
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.0:
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
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
@@ -5460,6 +6033,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
rope-sequence@1.3.4: {}
sade@1.8.1:
dependencies:
mri: 1.2.0
@@ -5735,6 +6310,14 @@ snapshots:
tinyspy@4.0.4: {}
tiptap-markdown@0.9.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0)):
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
'@types/markdown-it': 13.0.9
markdown-it: 14.1.1
markdown-it-task-lists: 2.1.1
prosemirror-markdown: 1.13.4
totalist@3.0.1: {}
tr46@1.0.1:
@@ -5923,6 +6506,8 @@ snapshots:
- tsx
- yaml
w3c-keyname@2.2.8: {}
webidl-conversions@4.0.2: {}
whatwg-url@7.1.0:
+1
View File
@@ -1 +1,2 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@@ -0,0 +1,62 @@
<script lang="ts">
import type { Editor } from '@tiptap/core';
interface Props {
editor: Editor | undefined;
}
let { editor }: Props = $props();
function toggleLink() {
if (!editor) return;
if (editor.isActive('link')) {
editor.chain().focus().unsetLink().run();
} else {
const url = window.prompt('Enter URL:');
if (url) {
editor.chain().focus().setLink({ href: url }).run();
}
}
}
const buttons = [
{ action: () => editor?.chain().focus().undo().run(), label: '\u21A9', title: 'Undo (Ctrl+Z)', testId: 'format-undo', disabled: () => !editor?.can().undo() },
{ action: () => editor?.chain().focus().redo().run(), label: '\u21AA', title: 'Redo (Ctrl+Shift+Z)', testId: 'format-redo', disabled: () => !editor?.can().redo() },
{ separator: true },
{ action: () => editor?.chain().focus().toggleHeading({ level: 1 }).run(), label: 'H1', title: 'Heading 1', testId: 'format-h1', isActive: () => editor?.isActive('heading', { level: 1 }) },
{ action: () => editor?.chain().focus().toggleHeading({ level: 2 }).run(), label: 'H2', title: 'Heading 2', testId: 'format-h2', isActive: () => editor?.isActive('heading', { level: 2 }) },
{ action: () => editor?.chain().focus().toggleHeading({ level: 3 }).run(), label: 'H3', title: 'Heading 3', testId: 'format-h3', isActive: () => editor?.isActive('heading', { level: 3 }) },
{ action: () => editor?.chain().focus().toggleBold().run(), label: 'B', title: 'Bold (Ctrl+B)', class: 'font-bold', testId: 'format-bold', isActive: () => editor?.isActive('bold') },
{ action: () => editor?.chain().focus().toggleItalic().run(), label: 'I', title: 'Italic (Ctrl+I)', class: 'italic', testId: 'format-italic', isActive: () => editor?.isActive('italic') },
{ action: () => editor?.chain().focus().toggleStrike().run(), label: 'S', title: 'Strikethrough (Ctrl+Shift+X)', class: 'line-through', testId: 'format-strikethrough', isActive: () => editor?.isActive('strike') },
{ action: () => editor?.chain().focus().toggleCode().run(), label: '<>', title: 'Inline code (Ctrl+E)', testId: 'format-code', isActive: () => editor?.isActive('code') },
{ action: () => editor?.chain().focus().toggleCodeBlock().run(), label: '{ }', title: 'Code block', testId: 'format-code-block', isActive: () => editor?.isActive('codeBlock') },
{ action: () => editor?.chain().focus().toggleBlockquote().run(), label: '\u201C', title: 'Blockquote', testId: 'format-blockquote', isActive: () => editor?.isActive('blockquote') },
{ action: () => editor?.chain().focus().toggleBulletList().run(), label: '\u2022', title: 'Bullet list (Ctrl+Shift+8)', testId: 'format-bullet-list', isActive: () => editor?.isActive('bulletList') },
{ action: () => editor?.chain().focus().toggleOrderedList().run(), label: '1.', title: 'Ordered list (Ctrl+Shift+7)', testId: 'format-ordered-list', isActive: () => editor?.isActive('orderedList') },
{ action: () => editor?.chain().focus().toggleTaskList().run(), label: '\u2611', title: 'Task list', testId: 'format-task-list', isActive: () => editor?.isActive('taskList') },
{ action: () => editor?.chain().focus().setHorizontalRule().run(), label: '\u2015', title: 'Horizontal rule', testId: 'format-hr' },
{ action: toggleLink, label: '\uD83D\uDD17', title: 'Insert link (Ctrl+K)', testId: 'format-link', isActive: () => editor?.isActive('link') }
];
</script>
<div
class="flex flex-wrap items-center gap-0.5 border-b border-gray-200/50 px-2 py-1 dark:border-gray-700/50"
data-testid="formatting-toolbar"
>
{#each buttons as btn}
{#if btn.separator}
<div class="mx-1 h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
{:else}
<button
onclick={btn.action}
disabled={btn.disabled?.()}
class="rounded px-2 py-1 text-xs text-gray-600 hover:bg-black/10 dark:text-gray-400 dark:hover:bg-white/10 {btn.class || ''} {btn.isActive?.() ? 'bg-black/10 dark:bg-white/10' : ''} {btn.disabled?.() ? 'opacity-30' : ''}"
title={btn.title}
data-testid={btn.testId}
>
{btn.label}
</button>
{/if}
{/each}
</div>
+6 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { NOTE_COLORS } from '$lib/utils/colors.js';
import { effectiveTheme } from '$lib/stores/theme.js';
import { stripMarkdown } from '$lib/utils/markdown.js';
import { renderMarkdown } from '$lib/utils/markdown.js';
import { togglePin, trashNote, archiveNote, unarchiveNote, restoreNote, deleteNote, currentFilter } from '$lib/stores/notes.js';
import type { Note } from '$lib/types/index.js';
@@ -25,7 +25,7 @@
let cardStyle = $state('');
const preview = $derived(stripMarkdown(note.content).slice(0, 200));
const renderedContent = $derived(renderMarkdown(note.content));
const checklistItems = $derived<ChecklistItem[]>(
note.checklistMode
@@ -70,8 +70,10 @@
<li class="text-xs text-gray-400">+{checklistItems.length - 8} more</li>
{/if}
</ul>
{:else if preview}
<p class="line-clamp-6 text-sm text-gray-700 dark:text-gray-300">{preview}</p>
{:else if note.content}
<div class="prose prose-sm dark:prose-invert line-clamp-6 max-w-none text-sm text-gray-700 dark:text-gray-300" data-testid="note-content-preview">
{@html renderedContent}
</div>
{/if}
{#if note.tags && note.tags.length > 0}
+48 -34
View File
@@ -1,10 +1,12 @@
<script lang="ts">
import ColorPicker from './ColorPicker.svelte';
import Checklist from './Checklist.svelte';
import FormattingToolbar from './FormattingToolbar.svelte';
import TiptapEditor from './TiptapEditor.svelte';
import { updateNote, createNote } from '$lib/stores/notes.js';
import { renderMarkdown } from '$lib/utils/markdown.js';
import { NOTE_COLORS } from '$lib/utils/colors.js';
import { effectiveTheme } from '$lib/stores/theme.js';
import type { Editor } from '@tiptap/core';
import type { Note, NoteColor } from '$lib/types/index.js';
interface Props {
@@ -13,14 +15,20 @@
onClose: () => void;
}
let { note, isNew = false, onClose }: Props = $props();
const { note, isNew = false, onClose }: Props = $props();
let title = $state(note?.title || '');
let content = $state(note?.content || '');
let color = $state<NoteColor>(note?.color || 'default');
let checklistMode = $state(note?.checklistMode || false);
let showPreview = $state(false);
// svelte-ignore state_referenced_locally
let title = $state(note?.title ?? '');
// svelte-ignore state_referenced_locally
let content = $state(note?.content ?? '');
// svelte-ignore state_referenced_locally
let color = $state<NoteColor>(note?.color ?? 'default');
// svelte-ignore state_referenced_locally
let checklistMode = $state(note?.checklistMode ?? false);
let showColorPicker = $state(false);
let rawMarkdownMode = $state(false);
let textareaEl: HTMLTextAreaElement | undefined = $state();
let tiptapEditor: Editor | undefined = $state();
let bgStyle = $state('');
$effect(() => {
@@ -28,8 +36,6 @@
bgStyle = `background-color: ${$effectiveTheme === 'dark' ? colors.dark : colors.light}`;
});
const renderedContent = $derived(renderMarkdown(content));
async function save() {
if (!title.trim() && !content.trim()) {
onClose();
@@ -54,6 +60,13 @@
color = c;
showColorPicker = false;
}
function toggleMarkdownMode() {
rawMarkdownMode = !rawMarkdownMode;
if (rawMarkdownMode) {
requestAnimationFrame(() => textareaEl?.focus());
}
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -85,21 +98,27 @@
<div class="px-4 py-2">
<Checklist {content} onChange={(c) => (content = c)} />
</div>
{:else if showPreview}
<div
class="prose prose-sm dark:prose-invert min-h-[100px] max-w-none px-4 py-2"
data-testid="note-preview"
>
{@html renderedContent}
</div>
{:else}
{:else if rawMarkdownMode}
<textarea
bind:this={textareaEl}
placeholder="Take a note..."
bind:value={content}
class="min-h-[100px] w-full resize-none bg-transparent px-4 py-2 text-sm text-gray-800 outline-none placeholder:text-gray-500 dark:text-gray-200"
rows="6"
data-testid="note-content-input"
></textarea>
{:else}
<TiptapEditor
{content}
onUpdate={(md) => (content = md)}
onEditor={(e) => (tiptapEditor = e)}
placeholder="Take a note..."
/>
{/if}
<!-- Formatting toolbar -->
{#if !rawMarkdownMode && !checklistMode}
<FormattingToolbar editor={tiptapEditor} />
{/if}
<!-- Toolbar -->
@@ -124,23 +143,6 @@
{/if}
</div>
<!-- Preview toggle -->
<button
onclick={() => (showPreview = !showPreview)}
class="rounded-full p-2 hover:bg-black/10 dark:hover:bg-white/10"
title={showPreview ? 'Edit' : 'Preview'}
data-testid="preview-toggle"
>
<svg class="h-5 w-5 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#if showPreview}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
{:else}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
{/if}
</svg>
</button>
<!-- Checklist mode toggle -->
<button
onclick={() => (checklistMode = !checklistMode)}
@@ -152,6 +154,18 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
</button>
<!-- Raw markdown mode toggle -->
<button
onclick={toggleMarkdownMode}
class="rounded-full p-2 hover:bg-black/10 dark:hover:bg-white/10 {rawMarkdownMode ? 'text-amber-600' : ''}"
title="Markdown mode"
data-testid="markdown-toggle"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
</button>
</div>
<button
+91
View File
@@ -0,0 +1,91 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import TaskList from '@tiptap/extension-task-list';
import TaskItem from '@tiptap/extension-task-item';
import Link from '@tiptap/extension-link';
import Placeholder from '@tiptap/extension-placeholder';
import { Markdown } from 'tiptap-markdown';
interface Props {
content: string;
onUpdate: (markdown: string) => void;
onEditor?: (editor: Editor) => void;
placeholder?: string;
}
let { content, onUpdate, onEditor, placeholder = 'Take a note...' }: Props = $props();
let element: HTMLDivElement | undefined = $state();
let editor: Editor | undefined = $state();
onMount(() => {
editor = new Editor({
element: element!,
extensions: [
StarterKit,
TaskList,
TaskItem.configure({ nested: true }),
Link.configure({ openOnClick: false }),
Placeholder.configure({ placeholder }),
Markdown
],
content,
onUpdate: ({ editor: e }) => {
onUpdate(e.storage.markdown.getMarkdown());
},
onTransaction: () => {
// Trigger Svelte reactivity for active state checks
editor = editor;
}
});
// Expose editor on DOM element for e2e testing
if (element) {
(element as any).__tiptapEditor = editor;
}
onEditor?.(editor);
return () => {
editor?.destroy();
};
});
</script>
<div
bind:this={element}
class="tiptap-wrapper prose prose-sm dark:prose-invert min-h-[100px] max-w-none px-4 py-2 text-gray-800 dark:text-gray-200"
data-testid="tiptap-editor"
></div>
<style>
.tiptap-wrapper :global(.tiptap) {
outline: none;
min-height: 80px;
}
.tiptap-wrapper :global(.tiptap p.is-editor-empty:first-child::before) {
content: attr(data-placeholder);
float: left;
color: #9ca3af;
pointer-events: none;
height: 0;
}
.tiptap-wrapper :global(.tiptap ul[data-type='taskList']) {
list-style: none;
padding: 0;
}
.tiptap-wrapper :global(.tiptap ul[data-type='taskList'] li) {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.tiptap-wrapper :global(.tiptap ul[data-type='taskList'] li label) {
margin-top: 0.25rem;
}
</style>
+5 -5
View File
@@ -1,7 +1,7 @@
import { test, expect } from './helpers/fixtures.js';
test.describe('Checklist', () => {
test('Scenario: Checklist UI replaces textarea when checklist mode is enabled', async ({ authenticatedPage: page }) => {
test('Scenario: Checklist UI replaces editor when checklist mode is enabled', async ({ authenticatedPage: page }) => {
// Given the user is creating a new note
await page.getByTestId('new-note-btn').click();
@@ -11,8 +11,8 @@ test.describe('Checklist', () => {
// Then the checklist component is displayed
await expect(page.getByTestId('checklist')).toBeVisible();
// And the plain text area is hidden
await expect(page.getByTestId('note-content-input')).not.toBeVisible();
// And the rich text editor is hidden
await expect(page.getByTestId('tiptap-editor')).not.toBeVisible();
});
test('Scenario: Checklist item can be added and persisted', async ({ authenticatedPage: page }) => {
@@ -89,8 +89,8 @@ test.describe('Checklist', () => {
// When the user disables checklist mode
await page.getByTestId('checklist-toggle').click();
// Then the plain text area is displayed again
await expect(page.getByTestId('note-content-input')).toBeVisible();
// Then the rich text editor is displayed again
await expect(page.getByTestId('tiptap-editor')).toBeVisible();
await expect(page.getByTestId('checklist')).not.toBeVisible();
});
});
+121
View File
@@ -0,0 +1,121 @@
import { test, expect } from './helpers/fixtures.js';
import type { Page, Locator } from '@playwright/test';
/** Run a TipTap command chain via the exposed editor instance on the DOM element */
async function runTiptapCommand(page: Page, commandFn: string) {
await page.getByTestId('tiptap-editor').evaluate(
(el, fn) => {
const editor = (el as any).__tiptapEditor;
if (!editor) throw new Error('TipTap editor not found on element');
new Function('editor', fn)(editor);
},
commandFn
);
}
/** Type content into the TipTap editor via markdown mode (reliable for e2e) */
async function typeViaMarkdown(page: Page, content: string) {
await page.getByTestId('markdown-toggle').click();
await page.getByTestId('note-content-input').fill(content);
await page.getByTestId('markdown-toggle').click();
}
test.describe('Rich text formatting', () => {
test('Scenario: Bold shortcut formats selected text in the editor', async ({ authenticatedPage: page }) => {
// Given the user is editing a new note with content "hello world"
await page.getByTestId('new-note-btn').click();
await typeViaMarkdown(page, 'hello world');
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
// When the user selects "world" and applies bold formatting
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:7,to:12}).toggleBold().run()');
// Then the selected text appears bold in the editor
await expect(editor.locator('strong')).toHaveText('world');
// And the markdown content contains bold syntax
await page.getByTestId('markdown-toggle').click();
await expect(page.getByTestId('note-content-input')).toHaveValue('hello **world**');
});
test('Scenario: Italic shortcut formats selected text in the editor', async ({ authenticatedPage: page }) => {
// Given the user is editing a new note with content "hello world"
await page.getByTestId('new-note-btn').click();
await typeViaMarkdown(page, 'hello world');
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
// When the user selects "world" and applies italic formatting
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:7,to:12}).toggleItalic().run()');
// Then the selected text appears italic in the editor
await expect(editor.locator('em')).toHaveText('world');
});
test('Scenario: Formatting toolbar applies bold to selected text', async ({ authenticatedPage: page }) => {
// Given the user is editing a note
await page.getByTestId('new-note-btn').click();
await typeViaMarkdown(page, 'hello world');
// When the user selects "world" and clicks the bold button
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:7,to:12}).run()');
await page.getByTestId('format-bold').click();
// Then the selected text appears bold
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
await expect(editor.locator('strong')).toHaveText('world');
});
test('Scenario: Formatting toolbar is always visible when editing', async ({ authenticatedPage: page }) => {
// When the user opens a new note
await page.getByTestId('new-note-btn').click();
// Then the formatting toolbar is visible immediately
await expect(page.getByTestId('formatting-toolbar')).toBeVisible();
});
test('Scenario: Inline code shortcut wraps selected text', async ({ authenticatedPage: page }) => {
// Given the user is editing a note with content "use npm install"
await page.getByTestId('new-note-btn').click();
await typeViaMarkdown(page, 'use npm install');
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
// When the user selects "npm install" and applies code formatting
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:5,to:16}).toggleCode().run()');
// Then the selected text appears as inline code
await expect(editor.locator('code')).toHaveText('npm install');
});
test('Scenario: Existing note shows rich text in TipTap editor', async ({ authenticatedPage: page }) => {
// Given a note with bold markdown content exists
await page.getByTestId('new-note-btn').click();
await page.getByTestId('note-title-input').fill('Rich Text Note');
await typeViaMarkdown(page, 'Hello **bold** world');
await page.getByTestId('close-editor-btn').click();
// When the user reopens the note
await page.getByTestId('note-card').click();
// Then the content is rendered as rich text in the TipTap editor
await expect(page.getByTestId('tiptap-editor')).toBeVisible();
await expect(page.getByTestId('tiptap-editor').locator('strong')).toHaveText('bold');
});
test('Scenario: Markdown toggle shows raw markdown content', async ({ authenticatedPage: page }) => {
// Given the user is editing a note with bold content
await page.getByTestId('new-note-btn').click();
await typeViaMarkdown(page, 'Hello **bold** world');
// Verify the rich text renders correctly first
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
await expect(editor.locator('strong')).toHaveText('bold');
// When the user toggles markdown mode
await page.getByTestId('markdown-toggle').click();
// Then the raw markdown textarea is shown with the markdown source
await expect(page.getByTestId('note-content-input')).toBeVisible();
await expect(page.getByTestId('tiptap-editor')).not.toBeVisible();
await expect(page.getByTestId('note-content-input')).toHaveValue('Hello **bold** world');
});
});
+20 -9
View File
@@ -11,7 +11,9 @@ test.describe('Notes CRUD', () => {
// When the user creates a note titled "My First Note" with content "Hello world!"
await page.getByTestId('new-note-btn').click();
await page.getByTestId('note-title-input').fill('My First Note');
await page.getByTestId('note-content-input').fill('Hello world!');
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
await editor.click();
await editor.pressSequentially('Hello world!');
await page.getByTestId('close-editor-btn').click();
// Then the note is visible in the notes list
@@ -23,7 +25,9 @@ test.describe('Notes CRUD', () => {
// Given a note titled "Original Title" exists
await page.getByTestId('new-note-btn').click();
await page.getByTestId('note-title-input').fill('Original Title');
await page.getByTestId('note-content-input').fill('Original content');
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
await editor.click();
await editor.pressSequentially('Original content');
await page.getByTestId('close-editor-btn').click();
// When the user changes the title to "Updated Title"
@@ -50,15 +54,22 @@ test.describe('Notes CRUD', () => {
await expect(page.getByText('Delete Me')).not.toBeVisible();
});
test('Scenario: Markdown content is rendered in the note preview', async ({ authenticatedPage: page }) => {
// Given a note with markdown content "**bold text**"
test('Scenario: Bold formatting applies to selected text in the editor', async ({ authenticatedPage: page }) => {
// Given the user is editing a note with content "hello world"
await page.getByTestId('new-note-btn').click();
await page.getByTestId('note-content-input').fill('**bold text**');
await page.getByTestId('markdown-toggle').click();
await page.getByTestId('note-content-input').fill('hello world');
await page.getByTestId('markdown-toggle').click();
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
// When the user switches to preview mode
await page.getByTestId('preview-toggle').click();
// When the user selects "world" and applies bold formatting
await page.getByTestId('tiptap-editor').evaluate((el) => {
const ed = (el as any).__tiptapEditor;
if (!ed) return;
ed.chain().focus().setTextSelection({ from: 7, to: 12 }).toggleBold().run();
});
// Then the rendered preview contains the formatted text
await expect(page.getByTestId('note-preview')).toContainText('bold text');
// Then the selected text appears bold in the editor
await expect(editor.locator('strong')).toHaveText('world');
});
});
+3 -1
View File
@@ -47,7 +47,9 @@ test.describe('Organization Features', () => {
// Given a note tagged #important and an untagged note exist
await page.getByTestId('new-note-btn').click();
await page.getByTestId('note-title-input').fill('Tagged Note');
await page.getByTestId('note-content-input').fill('This is #important');
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
await editor.click();
await editor.pressSequentially('This is #important');
await page.getByTestId('close-editor-btn').click();
await page.getByTestId('new-note-btn').click();
+3 -1
View File
@@ -23,7 +23,9 @@ test.describe('Search', () => {
// Given a note titled "Recipe" with content "Pasta with tomato sauce" exists
await page.getByTestId('new-note-btn').click();
await page.getByTestId('note-title-input').fill('Recipe');
await page.getByTestId('note-content-input').fill('Pasta with tomato sauce');
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
await editor.click();
await editor.pressSequentially('Pasta with tomato sauce');
await page.getByTestId('close-editor-btn').click();
// When the user searches for "tomato"