ENH - improve mail experience and interactions

This commit is contained in:
DerEchteAlec
2026-08-06 17:39:24 +02:00
parent 5471040dd8
commit 26c4df9181
14 changed files with 2794 additions and 342 deletions
+7
View File
@@ -14,6 +14,13 @@
"test": "vitest run"
},
"dependencies": {
"@tiptap/core": "^3.29.2",
"@tiptap/extension-placeholder": "^3.29.2",
"@tiptap/markdown": "^3.29.2",
"@tiptap/pm": "^3.29.2",
"@tiptap/starter-kit": "^3.29.2",
"@tiptap/vue-3": "^3.29.2",
"dompurify": "^3.4.13",
"konsta": "~5.2.0",
"lucide-vue-next": "^0.525.0",
"pinia": "^3.0.3",
+551
View File
@@ -8,6 +8,27 @@ importers:
.:
dependencies:
'@tiptap/core':
specifier: ^3.29.2
version: 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-placeholder':
specifier: ^3.29.2
version: 3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))
'@tiptap/markdown':
specifier: ^3.29.2
version: 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/pm':
specifier: ^3.29.2
version: 3.29.2
'@tiptap/starter-kit':
specifier: ^3.29.2
version: 3.29.2
'@tiptap/vue-3':
specifier: ^3.29.2
version: 3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)(vue@3.5.40(typescript@5.8.3))
dompurify:
specifier: ^3.4.13
version: 3.4.13
konsta:
specifier: ~5.2.0
version: 5.2.0
@@ -292,6 +313,15 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.8.0':
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
'@floating-ui/dom@1.8.0':
resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -333,6 +363,7 @@ packages:
engines: {node: ^22.20 || ^24.12 || >=25}
cpu: [x64]
os: [linux]
libc: [glibc]
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -387,66 +418,79 @@ packages:
resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.62.4':
resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.62.4':
resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.62.4':
resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.62.4':
resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.62.4':
resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.62.4':
resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.62.4':
resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.62.4':
resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.62.4':
resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.62.4':
resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.62.4':
resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.62.4':
resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.62.4':
resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==}
@@ -516,24 +560,28 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.3.3':
resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.3.3':
resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.3.3':
resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.3':
resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==}
@@ -568,6 +616,165 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
'@tiptap/core@3.29.2':
resolution: {integrity: sha512-oKUkiPUB7noilVYxI9lNzUD4rX17sHub+PYjMfHMWHG9A3nvIy+FdePIVIIhThKWF7ijhr3eIqHY51Bn+GAFtw==}
peerDependencies:
'@tiptap/pm': 3.29.2
'@tiptap/extension-blockquote@3.29.2':
resolution: {integrity: sha512-ca4OzKDh0yaxg2+Z56bC2QnWsNsFp2YMRfVig1PDXyMVFMNJpLcnhxgq/9btn+xYAlYrj8RymOeCTYREOR6Zjg==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/extension-bold@3.29.2':
resolution: {integrity: sha512-elYbGxJsYnBb4leqrcjdIJuiG380BcOgN+UUzvOv+qEjfGVzHodFOMBl3qnmD6urYHNu5/qQK2S0qSSRXKCLNQ==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-bubble-menu@3.29.2':
resolution: {integrity: sha512-kzcWarcpr031rZu+R3Hzut5uxb3Qj/xxMDEYZIQ3uiq1yb5vEMXj8/SIyWautk6Nu8Rr1leV3rGdR4NzhzyFAA==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/extension-bullet-list@3.29.2':
resolution: {integrity: sha512-3bWcCUPbCHv0XttlMdnAtXLNYWx2pblByMgxmGsaP9FU0QnslGXty6A6gHCqI33ygRg1vrA6U5Wtpwbi5aKu5g==}
peerDependencies:
'@tiptap/extension-list': 3.29.2
'@tiptap/extension-code-block@3.29.2':
resolution: {integrity: sha512-w153ct8g6dLiPTdXQ6SOIMxX4SEo5Q50AmjdEEEcJ7ZcYUcde/ScSskLHfOYmyt5ZFAiyEwr121+pux+p3/oAQ==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/extension-code@3.29.2':
resolution: {integrity: sha512-c6W5UGuB7WNLpYocsgRzpO2OOTI4QjaI9jjHRMuty9z+s9DtaYM/HrRLNwVh6MopkHb+i/89Wkv8gCS34fftig==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-document@3.29.2':
resolution: {integrity: sha512-YUamvefLnsqu6124GavVTI7nqcFlQJ12ROB0oSwG69eSBZYNjg1tIs05LFrBxkwf4Xgqd6YzfJ9+FeG428RvzQ==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-dropcursor@3.29.2':
resolution: {integrity: sha512-KKno7cU9r1HdR48CRrsDu69/1UjZdoslq/UcE+Kx+tdhAv/aljXMkRSNzGMrBNOBDmHRgS1+58zm21WQWdQzwA==}
peerDependencies:
'@tiptap/extensions': 3.29.2
'@tiptap/extension-floating-menu@3.29.2':
resolution: {integrity: sha512-CBTq4Xs5aGWr65uFCIkKBms796OhmirWf/ax//iJ4fl9IfwqjwFuc2vQs9PmuwpKn9ctDHo2sMTtvyeCvIt5LQ==}
peerDependencies:
'@floating-ui/dom': ^1.0.0
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/extension-gapcursor@3.29.2':
resolution: {integrity: sha512-8Q39UR4/Tit759IeW9xZIe3NMwN11GsuA3FLheDyyGn7RrW02HD3HhUDlazE54Ki4HoosjFmChPlN4Ik2ubdRQ==}
peerDependencies:
'@tiptap/extensions': 3.29.2
'@tiptap/extension-hard-break@3.29.2':
resolution: {integrity: sha512-eUW3LN3fq8rXnjEUeI3D2QONYdLsU3yYQm4jxlErs2h4cfwrFjgf19VSUFVmm6LrFbbQ0OnDVPeVLL6iOwDw2w==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-heading@3.29.2':
resolution: {integrity: sha512-6W4aIy70Mh7BNlbG9zZ5FBLhJhU2UUEzgZJ/jwYSCcB30o8McLxJSEjhtoHiX8R78Ah2/JzBGvIe5olZlbeE4A==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-horizontal-rule@3.29.2':
resolution: {integrity: sha512-8/ZPzbB9X85Mc9/7xVLZupQKBr2UVcQTGr512xtqMW+XkCQRHCph46tRo828YE13IMWI5fWn/FaNCqXG9cULSw==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/extension-italic@3.29.2':
resolution: {integrity: sha512-iH63V/5wsaMnY4Jz0+meaAGhaec4AiOzOduzl6ZZr5IyGhZ1kthyW84ELt0dyLI3hNceAUhaNWc+I7+vX0aoXA==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-link@3.29.2':
resolution: {integrity: sha512-DcVer5SqrexKCEP6Ip1UPxJUMvcRCCItSv0wxoGytanrimBh2smvcg6X0DWnjlsi5H0updhyl+atYCmmQXUIXA==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/extension-list-item@3.29.2':
resolution: {integrity: sha512-s8vBVHHFT0Qpu7CzAZ7S1kYmSiVaDvUNvMNcZUWnxj6VPfiwmx0eXd9FsjePrRCMoM5tFmPnhFTHDxY3D/eZeQ==}
peerDependencies:
'@tiptap/extension-list': 3.29.2
'@tiptap/extension-list-keymap@3.29.2':
resolution: {integrity: sha512-R+3k8OLnxdCH7Xy9ieOwUt5m2Je74u8mikothGmsYVO2Zyq48fIbmZ+X6RBPCu7DBOI2FIUhHEFbKQeDWvDNmA==}
peerDependencies:
'@tiptap/extension-list': 3.29.2
'@tiptap/extension-list@3.29.2':
resolution: {integrity: sha512-WPZ9BHAPT6QeIm1vdVkuoOWvy9a8/EZeJwV2VhU8LXyTAttvzyj4rsbbHyJWvYWlUSTt/QF2AZ2zhKo7u1w3/A==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/extension-ordered-list@3.29.2':
resolution: {integrity: sha512-ndCunC+UsYOpkOtL7vGnDz21UNa45WUlcO9wMT1fbuYow2QnRhsuMlCWENXI52YPPARWuQ0RDgN7q6TaxPERBg==}
peerDependencies:
'@tiptap/extension-list': 3.29.2
'@tiptap/extension-paragraph@3.29.2':
resolution: {integrity: sha512-7qJj5YTr11vvjNgjDN1ypOfwTovc0QOCYcit/rskeuVgnmQZOZQzC/BbyKLLG7UGnpRLemU/mEGbW9pAqjAXkQ==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-placeholder@3.29.2':
resolution: {integrity: sha512-/BlpPIq2JZk6zLaeYVOXazi6dmd0iRriTymNEnFn1doManN1y5HED9LsMeb/AhNhauL5AgmcHgpvAjbsN9k4SA==}
peerDependencies:
'@tiptap/extensions': 3.29.2
'@tiptap/extension-strike@3.29.2':
resolution: {integrity: sha512-aEvLAbddUQZ+FukCreV3q4G2HfNI+odE7E9U+wbq6XsSWKyo8/pDu1muz+TFKNre4blSMOQ3JQmw5UeHDKy+fg==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-text@3.29.2':
resolution: {integrity: sha512-Ubko45JWWHe8glBt2PiGNF8hcbys/JNalFhiR7Y1X4iOOtAxAKJJxh3+eq+//NTlGuBPdWGp7zw8EEUp7anjKA==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extension-underline@3.29.2':
resolution: {integrity: sha512-K7XwH/xS/5AIREWQ00VTEf/W5U0olp7j6wwit7cdd/8nHv6h6AGr1+iEApHKoLXWQZLfGQKzJlT9W61LAl+fHA==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/extensions@3.29.2':
resolution: {integrity: sha512-BCz+FCAChSYtUe4BFj97HEO+nSK+J7GxbJgZG4Hg7DT/gI+hRyeNndU8efiQAx3WGdzsFi3UxRpcF1tTQM7iMQ==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/markdown@3.29.2':
resolution: {integrity: sha512-Knf9YqG3fxOyFuS3pe3eapDz0ya9eqPGzmyMSd5PuHNmR9/Ir6i5qk5+Md6bCPgDNjYskztNm7DoBXPfbXbO/w==}
peerDependencies:
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
'@tiptap/pm@3.29.2':
resolution: {integrity: sha512-GCOme7xHaS+DSoaA4CDcAD3l6JyBlvZhvCyfsy2Vp6j8tEoBkZWio7soYVosmlyn7zq8/64VeFZP5s47yfG7fQ==}
'@tiptap/starter-kit@3.29.2':
resolution: {integrity: sha512-oTu0tysiqk4zgjEtxRHjAQgxUKaAevZwueOWwSWubHdokqp7SpcbE5n9USJv89HKuTUDm3GjnQH6q8HNn/2DsA==}
'@tiptap/vue-3@3.29.2':
resolution: {integrity: sha512-rLX+V+HXbsVlk+My8mslvbnbVXZiQZHYtbOiRgxugsqZ9SK1mr1FpgZ/ZNcotJAjecT8FJmPa45ccDnVu66eGA==}
peerDependencies:
'@floating-ui/dom': ^1.0.0
'@tiptap/core': 3.29.2
'@tiptap/pm': 3.29.2
vue: ^3.0.0
'@tsconfig/node22@22.0.5':
resolution: {integrity: sha512-hLf2ld+sYN/BtOJjHUWOk568dvjFQkHnLNa6zce25GIH+vxKfvTgm3qpaH6ToF5tu/NN0IH66s+Bb5wElHrLcw==}
@@ -589,6 +796,9 @@ packages:
'@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@typescript-eslint/eslint-plugin@8.65.0':
resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -956,6 +1166,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
dompurify@3.4.13:
resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1348,24 +1561,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -1383,6 +1600,9 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
linkifyjs@4.3.3:
resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -1402,6 +1622,11 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
marked@17.0.6:
resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==}
engines: {node: '>= 20'}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -1484,6 +1709,9 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -1565,6 +1793,45 @@ packages:
engines: {node: '>=14'}
hasBin: true
prosemirror-changeset@2.4.1:
resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==}
prosemirror-commands@1.7.2:
resolution: {integrity: sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==}
prosemirror-dropcursor@1.8.3:
resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==}
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-model@1.25.11:
resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==}
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-transform@1.12.0:
resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==}
prosemirror-view@1.42.2:
resolution: {integrity: sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@@ -1608,6 +1875,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
@@ -1908,6 +2178,9 @@ packages:
typescript:
optional: true
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -2088,6 +2361,17 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
'@floating-ui/core@1.8.0':
dependencies:
'@floating-ui/utils': 0.2.12
'@floating-ui/dom@1.8.0':
dependencies:
'@floating-ui/core': 1.8.0
'@floating-ui/utils': 0.2.12
'@floating-ui/utils@0.2.12': {}
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -2285,6 +2569,182 @@ snapshots:
tailwindcss: 4.3.3
vite: 7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)
'@tiptap/core@3.29.2(@tiptap/pm@3.29.2)':
dependencies:
'@tiptap/pm': 3.29.2
'@tiptap/extension-blockquote@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
'@tiptap/extension-bold@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-bubble-menu@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@floating-ui/dom': 1.8.0
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
optional: true
'@tiptap/extension-bullet-list@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-code-block@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
'@tiptap/extension-code@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-document@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-dropcursor@3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-floating-menu@3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@floating-ui/dom': 1.8.0
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
optional: true
'@tiptap/extension-gapcursor@3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-hard-break@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-heading@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-horizontal-rule@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
'@tiptap/extension-italic@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-link@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
linkifyjs: 4.3.3
'@tiptap/extension-list-item@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-list-keymap@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
'@tiptap/extension-ordered-list@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-paragraph@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-placeholder@3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-strike@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-text@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-underline@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
'@tiptap/markdown@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
marked: 17.0.6
'@tiptap/pm@3.29.2':
dependencies:
prosemirror-changeset: 2.4.1
prosemirror-commands: 1.7.2
prosemirror-dropcursor: 1.8.3
prosemirror-gapcursor: 1.4.1
prosemirror-history: 1.5.0
prosemirror-inputrules: 1.5.1
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.11
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.4
prosemirror-tables: 1.8.5
prosemirror-transform: 1.12.0
prosemirror-view: 1.42.2
'@tiptap/starter-kit@3.29.2':
dependencies:
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/extension-blockquote': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-bold': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-bullet-list': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))
'@tiptap/extension-code': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-code-block': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-document': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-dropcursor': 3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))
'@tiptap/extension-gapcursor': 3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))
'@tiptap/extension-hard-break': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-heading': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-horizontal-rule': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-italic': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-link': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-list-item': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))
'@tiptap/extension-list-keymap': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))
'@tiptap/extension-ordered-list': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))
'@tiptap/extension-paragraph': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-strike': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-text': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extension-underline': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))
'@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
'@tiptap/vue-3@3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)(vue@3.5.40(typescript@5.8.3))':
dependencies:
'@floating-ui/dom': 1.8.0
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
'@tiptap/pm': 3.29.2
vue: 3.5.40(typescript@5.8.3)
optionalDependencies:
'@tiptap/extension-bubble-menu': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tiptap/extension-floating-menu': 3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)
'@tsconfig/node22@22.0.5': {}
'@types/chai@5.2.3':
@@ -2304,6 +2764,9 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/trusted-types@2.0.7':
optional: true
'@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -2740,6 +3203,10 @@ snapshots:
detect-libc@2.1.2: {}
dompurify@3.4.13:
optionalDependencies:
'@types/trusted-types': 2.0.7
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -3183,6 +3650,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
linkifyjs@4.3.3: {}
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -3199,6 +3668,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
marked@17.0.6: {}
math-intrinsics@1.1.0: {}
media-typer@1.1.1: {}
@@ -3267,6 +3738,8 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
orderedmap@2.1.1: {}
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@@ -3327,6 +3800,80 @@ snapshots:
prettier@3.5.3: {}
prosemirror-changeset@2.4.1:
dependencies:
prosemirror-transform: 1.12.0
prosemirror-commands@1.7.2:
dependencies:
prosemirror-model: 1.25.11
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-dropcursor@1.8.3:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.42.2
prosemirror-gapcursor@1.4.1:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.11
prosemirror-state: 1.4.4
prosemirror-view: 1.42.2
prosemirror-history@1.5.0:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.42.2
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.1:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-keymap@1.2.3:
dependencies:
prosemirror-state: 1.4.4
w3c-keyname: 2.2.8
prosemirror-model@1.25.11:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-list@1.5.1:
dependencies:
prosemirror-model: 1.25.11
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-state@1.4.4:
dependencies:
prosemirror-model: 1.25.11
prosemirror-transform: 1.12.0
prosemirror-view: 1.42.2
prosemirror-tables@1.8.5:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.11
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.42.2
prosemirror-transform@1.12.0:
dependencies:
prosemirror-model: 1.25.11
prosemirror-view@1.42.2:
dependencies:
prosemirror-model: 1.25.11
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@@ -3390,6 +3937,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.62.4
fsevents: 2.3.3
rope-sequence@1.3.4: {}
router@2.2.0:
dependencies:
debug: 4.4.3
@@ -3700,6 +4249,8 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
w3c-keyname@2.2.8: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
@@ -0,0 +1,289 @@
<script setup lang="ts">
import Placeholder from '@tiptap/extension-placeholder'
import { Markdown } from '@tiptap/markdown'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import DOMPurify from 'dompurify'
import {
Bold,
Italic,
List,
ListOrdered,
Quote,
Redo2,
Undo2,
} from 'lucide-vue-next'
import { onBeforeUnmount, watch } from 'vue'
export type MailEditorLabels = {
bold: string
bulletList: string
italic: string
numberedList: string
quote: string
redo: string
undo: string
}
const props = withDefaults(
defineProps<{
editable?: boolean
labels?: MailEditorLabels
modelValue: string
placeholder?: string
}>(),
{
editable: true,
labels: () => ({
bold: 'Bold',
bulletList: 'Bullet list',
italic: 'Italic',
numberedList: 'Numbered list',
quote: 'Quote',
redo: 'Redo',
undo: 'Undo',
}),
placeholder: '',
},
)
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function safeMarkdown(value: string): string {
return String(
DOMPurify.sanitize(value.replace(/\r\n?/g, '\n'), {
ALLOWED_ATTR: [],
ALLOWED_TAGS: [],
KEEP_CONTENT: true,
}),
)
}
const editor = useEditor({
content: safeMarkdown(props.modelValue),
contentType: 'markdown',
editable: props.editable,
extensions: [
StarterKit.configure({
codeBlock: false,
heading: { levels: [2, 3] },
horizontalRule: false,
link: {
autolink: true,
linkOnPaste: true,
openOnClick: false,
protocols: ['http', 'https'],
},
strike: false,
underline: false,
}),
Markdown.configure({ markedOptions: { breaks: true, gfm: true } }),
Placeholder.configure({ placeholder: props.placeholder }),
],
injectCSS: false,
onUpdate: ({ editor: currentEditor }) => {
emit('update:modelValue', currentEditor.getMarkdown())
},
})
watch(
() => props.modelValue,
(value) => {
if (!editor.value) return
const safeValue = safeMarkdown(value)
if (editor.value.getMarkdown() === safeValue) return
editor.value.commands.setContent(safeValue, {
contentType: 'markdown',
emitUpdate: false,
})
},
)
watch(
() => props.editable,
(value) => editor.value?.setEditable(value),
)
onBeforeUnmount(() => editor.value?.destroy())
</script>
<template>
<div class="mail-editor" :class="{ 'mail-editor--readonly': !editable }">
<div v-if="editable && editor" class="mail-editor__toolbar">
<button
type="button"
:class="{ 'is-active': editor.isActive('bold') }"
:aria-label="labels.bold"
:title="labels.bold"
@click="editor.chain().focus().toggleBold().run()"
>
<Bold :size="17" />
</button>
<button
type="button"
:class="{ 'is-active': editor.isActive('italic') }"
:aria-label="labels.italic"
:title="labels.italic"
@click="editor.chain().focus().toggleItalic().run()"
>
<Italic :size="17" />
</button>
<button
type="button"
:class="{ 'is-active': editor.isActive('bulletList') }"
:aria-label="labels.bulletList"
:title="labels.bulletList"
@click="editor.chain().focus().toggleBulletList().run()"
>
<List :size="17" />
</button>
<button
type="button"
:class="{ 'is-active': editor.isActive('orderedList') }"
:aria-label="labels.numberedList"
:title="labels.numberedList"
@click="editor.chain().focus().toggleOrderedList().run()"
>
<ListOrdered :size="17" />
</button>
<button
type="button"
:class="{ 'is-active': editor.isActive('blockquote') }"
:aria-label="labels.quote"
:title="labels.quote"
@click="editor.chain().focus().toggleBlockquote().run()"
>
<Quote :size="17" />
</button>
<span class="mail-editor__toolbar-spacer" />
<button
type="button"
:disabled="!editor.can().chain().focus().undo().run()"
:aria-label="labels.undo"
:title="labels.undo"
@click="editor.chain().focus().undo().run()"
>
<Undo2 :size="17" />
</button>
<button
type="button"
:disabled="!editor.can().chain().focus().redo().run()"
:aria-label="labels.redo"
:title="labels.redo"
@click="editor.chain().focus().redo().run()"
>
<Redo2 :size="17" />
</button>
</div>
<EditorContent v-if="editor" :editor="editor" />
</div>
</template>
<style scoped>
.mail-editor {
min-height: 210px;
color: #f5f5f7;
}
.mail-editor__toolbar {
position: sticky;
z-index: 2;
top: 0;
display: flex;
align-items: center;
gap: 3px;
min-height: 43px;
padding: 5px 7px;
border-bottom: 1px solid #ffffff14;
background: #1c1c1ee8;
backdrop-filter: blur(18px) saturate(160%);
-webkit-backdrop-filter: blur(18px) saturate(160%);
}
.mail-editor__toolbar button {
display: grid;
width: 32px;
height: 32px;
place-items: center;
border: 0;
border-radius: 9px;
background: transparent;
color: #f5f5f7;
cursor: pointer;
}
.mail-editor__toolbar button.is-active {
background: #0a84ff;
color: #fff;
}
.mail-editor__toolbar button:disabled {
opacity: 0.28;
cursor: default;
}
.mail-editor__toolbar-spacer {
flex: 1;
}
:deep(.tiptap) {
min-height: 210px;
padding: 15px 16px 92px;
outline: none;
font-size: 15px;
line-height: 1.5;
white-space: pre-wrap;
}
.mail-editor--readonly {
min-height: 0;
}
.mail-editor--readonly :deep(.tiptap) {
min-height: 0;
padding: 0;
user-select: text;
}
:deep(.tiptap p) {
margin: 0 0 0.8em;
}
:deep(.tiptap p:last-child) {
margin-bottom: 0;
}
:deep(.tiptap h2),
:deep(.tiptap h3) {
margin: 1em 0 0.4em;
line-height: 1.2;
}
:deep(.tiptap ul),
:deep(.tiptap ol) {
margin: 0.55em 0 0.85em;
padding-left: 1.45em;
}
:deep(.tiptap blockquote) {
margin: 0.85em 0;
padding-left: 0.9em;
border-left: 3px solid #5e5e63;
color: #a1a1a6;
}
:deep(.tiptap a) {
color: #0a84ff;
text-decoration: none;
}
:deep(.tiptap p.is-editor-empty:first-child::before) {
float: left;
height: 0;
color: #8e8e93;
content: attr(data-placeholder);
pointer-events: none;
}
</style>
+17
View File
@@ -281,6 +281,9 @@ const defaultLocales: LocaleTree = {
email: 'Email',
password: 'Password',
confirmPassword: 'Confirm password',
accountEyebrow: 'Your iFruit account',
emailPlaceholder: 'name@ifruit.com',
passwordPlaceholder: 'Enter password',
passwordWarning:
'Use an in-character password. Do not reuse a real-world password.',
mailboxes: 'Mailboxes',
@@ -291,9 +294,14 @@ const defaultLocales: LocaleTree = {
compose: 'New Message',
recipients: 'To',
recipientHint: 'Separate up to 10 addresses with commas.',
recipientPlaceholder: 'name@ifruit.com',
subject: 'Subject',
subjectPlaceholder: "What's this about?",
body: 'Message',
messagePlaceholder: 'Write a message...',
search: 'Search mail',
messages: 'messages',
noMessageContent: 'No message content',
noMail: 'No Mail',
noMailBody: 'Messages in this mailbox will appear here.',
noResults: 'No Results',
@@ -306,7 +314,16 @@ const defaultLocales: LocaleTree = {
reply: 'Reply',
replyAll: 'Reply All',
forward: 'Forward',
formatBold: 'Bold',
formatItalic: 'Italic',
formatBulletList: 'Bullet list',
formatNumberedList: 'Numbered list',
formatQuote: 'Quote',
undo: 'Undo',
redo: 'Redo',
markRead: 'Read',
markUnread: 'Mark as Unread',
delete: 'Delete',
moveToTrash: 'Move to Trash',
restore: 'Restore',
deleteForever: 'Delete Forever',
+33 -1
View File
@@ -4,6 +4,9 @@ import type { MailMessage } from '@/types/mail'
import {
buildForwardDraft,
buildReplyDraft,
filterMailAddressInput,
filterMailRecipientInput,
mailPlainText,
normalizeMailAddress,
parseMailRecipients,
} from '@/utils/mail'
@@ -22,6 +25,23 @@ const message: MailMessage = {
}
describe('mail addresses', () => {
it('filters account fields to the supported email character set', () => {
expect(filterMailAddressInput('Al+ex! ä@ifruit.com<script>')).toBe(
'Alex@ifruit.comscript',
)
expect(filterMailAddressInput('sky.user_name-2@ifruit.com')).toBe(
'sky.user_name-2@ifruit.com',
)
})
it('keeps separators but filters recipient-list special characters', () => {
expect(
filterMailRecipientInput(
'alex@ifruit.com, jamie+tag@ifruit.com; müller@ifruit.com',
),
).toBe('alex@ifruit.com, jamietag@ifruit.com; mller@ifruit.com')
})
it('normalizes local parts and the iFruit domain', () => {
expect(normalizeMailAddress(' Sky.User ')).toBe('sky.user@ifruit.com')
expect(normalizeMailAddress('sky.user@ifruit.com')).toBe(
@@ -48,12 +68,24 @@ describe('mail compose helpers', () => {
expect(
buildReplyDraft(message, 'alex@ifruit.com', true).recipients,
).toEqual(['morgan@ifruit.com', 'jamie@ifruit.com'])
expect(buildReplyDraft(message, 'alex@ifruit.com').body).toContain(
'> Meet at Legion Square.',
)
})
it('builds forward content and avoids duplicate subject prefixes', () => {
const forwarded = buildForwardDraft(message)
expect(forwarded.recipients).toEqual([])
expect(forwarded.subject).toBe('Fwd: Plans')
expect(forwarded.body).toContain('From: morgan@ifruit.com')
expect(forwarded.body).toContain('**From:** morgan@ifruit.com')
})
it('turns formatted markdown into a compact mailbox preview', () => {
expect(
mailPlainText('## Update\n\n**Ready** [details](https://ifruit.com).'),
).toBe('Update Ready details.')
expect(mailPlainText('<script>alert(1)</script> Hello')).toBe(
'alert(1) Hello',
)
})
})
+36 -2
View File
@@ -2,6 +2,21 @@ import type { MailComposeDraft, MailMessage } from '@/types/mail'
export const MAIL_DOMAIN = 'ifruit.com'
export const MAIL_MAX_RECIPIENTS = 10
export const MAIL_ADDRESS_INPUT_MAX_LENGTH = 64
export const MAIL_RECIPIENT_INPUT_MAX_LENGTH =
MAIL_MAX_RECIPIENTS * (MAIL_ADDRESS_INPUT_MAX_LENGTH + 2)
export function filterMailAddressInput(value: string): string {
return value
.replace(/[^a-z0-9@._-]/gi, '')
.slice(0, MAIL_ADDRESS_INPUT_MAX_LENGTH)
}
export function filterMailRecipientInput(value: string): string {
return value
.replace(/[^a-z0-9@._,; -]/gi, '')
.slice(0, MAIL_RECIPIENT_INPUT_MAX_LENGTH)
}
export function normalizeMailAddress(value: string): string | null {
const normalized = value.trim().toLocaleLowerCase('en-US')
@@ -43,6 +58,25 @@ export function parseMailRecipients(value: string): string[] | null {
return recipients
}
export function mailPlainText(value: string): string {
return value
.replace(/<[^>]*>/g, ' ')
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
.replace(/^\s{0,3}(?:#{1,6}|>|[-+*]|\d+\.)\s+/gm, '')
.replace(/[*_~`]/g, '')
.replace(/\s+/g, ' ')
.trim()
}
function quoteMarkdown(value: string): string {
return value
.trim()
.split('\n')
.map((line) => `> ${line}`)
.join('\n')
}
function prefixedSubject(prefix: 'Re' | 'Fwd', subject: string): string {
const clean = subject.trim()
if (new RegExp(`^${prefix}:`, 'i').test(clean)) return clean
@@ -62,7 +96,7 @@ export function buildReplyDraft(
)
return {
body: `\n--- ${message.sender} ---\n${message.body}`,
body: `\n\n> **${message.sender}**\n>\n${quoteMarkdown(message.body)}`,
recipients,
subject: prefixedSubject('Re', message.subject),
}
@@ -70,7 +104,7 @@ export function buildReplyDraft(
export function buildForwardDraft(message: MailMessage): MailComposeDraft {
return {
body: `\n--- Forwarded message ---\nFrom: ${message.sender}\nTo: ${message.recipients.join(', ')}\n\n${message.body}`,
body: `\n\n---\n\n**Forwarded message**\n\n**From:** ${message.sender}\n\n**To:** ${message.recipients.join(', ')}\n\n${message.body}`,
recipients: [],
subject: prefixedSubject('Fwd', message.subject),
}
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import {
clampMailSwipeOffset,
MAIL_SWIPE_ACTION_WIDTH,
resolveMailSwipeAction,
resolveMailSwipeAxis,
} from '@/utils/mailSwipe'
describe('mail swipe gestures', () => {
it('waits for a clear horizontal or vertical direction', () => {
expect(resolveMailSwipeAxis(5, 2)).toBeNull()
expect(resolveMailSwipeAxis(24, 5)).toBe('horizontal')
expect(resolveMailSwipeAxis(10, 18)).toBe('vertical')
})
it('limits horizontal movement and blocks unavailable directions', () => {
expect(clampMailSwipeOffset(70, true, true)).toBe(70)
expect(clampMailSwipeOffset(70, false, true)).toBe(0)
expect(clampMailSwipeOffset(-70, true, false)).toBe(0)
expect(clampMailSwipeOffset(-240, true, true)).toBeGreaterThan(
-MAIL_SWIPE_ACTION_WIDTH - 19,
)
})
it('only commits an action after crossing its threshold', () => {
expect(resolveMailSwipeAction(63, true, true)).toBeNull()
expect(resolveMailSwipeAction(64, true, true)).toBe('read')
expect(resolveMailSwipeAction(-64, true, true)).toBe('delete')
expect(resolveMailSwipeAction(80, false, true)).toBeNull()
})
})
+48
View File
@@ -0,0 +1,48 @@
export type MailSwipeAction = 'delete' | 'read'
export type MailSwipeAxis = 'horizontal' | 'vertical'
export const MAIL_SWIPE_ACTION_WIDTH = 84
export const MAIL_SWIPE_AXIS_LOCK = 8
export const MAIL_SWIPE_TRIGGER = 64
export function resolveMailSwipeAxis(
deltaX: number,
deltaY: number,
): MailSwipeAxis | null {
const horizontalDistance = Math.abs(deltaX)
const verticalDistance = Math.abs(deltaY)
if (Math.max(horizontalDistance, verticalDistance) < MAIL_SWIPE_AXIS_LOCK) {
return null
}
return horizontalDistance > verticalDistance * 1.15
? 'horizontal'
: 'vertical'
}
export function clampMailSwipeOffset(
deltaX: number,
canRead: boolean,
canDelete: boolean,
): number {
if ((deltaX > 0 && !canRead) || (deltaX < 0 && !canDelete)) return 0
const direction = Math.sign(deltaX)
const distance = Math.abs(deltaX)
const resistedDistance =
distance <= MAIL_SWIPE_ACTION_WIDTH
? distance
: MAIL_SWIPE_ACTION_WIDTH + (distance - MAIL_SWIPE_ACTION_WIDTH) * 0.18
return direction * Math.min(resistedDistance, MAIL_SWIPE_ACTION_WIDTH + 18)
}
export function resolveMailSwipeAction(
offset: number,
canRead: boolean,
canDelete: boolean,
): MailSwipeAction | null {
if (canRead && offset >= MAIL_SWIPE_TRIGGER) return 'read'
if (canDelete && offset <= -MAIL_SWIPE_TRIGGER) return 'delete'
return null
}
File diff suppressed because it is too large Load Diff
+26 -1
View File
@@ -48,6 +48,10 @@ import { IFRUIT_AUTH_INPUT_COLORS } from '@/config/ifruit'
import { usePhoneStore } from '@/stores/phone'
import { useAccountStore } from '@/stores/account'
import type { PhoneAppDefinition, PhoneAppId } from '@/types/apps'
import {
filterMailAddressInput,
MAIL_ADDRESS_INPUT_MAX_LENGTH,
} from '@/utils/mail'
import { nuiCall } from '@/utils/nui'
import { formatPhoneNumber } from '@/utils/phone'
import {
@@ -320,6 +324,23 @@ function eventValue(event: Event): string {
return (event.target as HTMLInputElement).value
}
function updateAccountEmail(event: Event): void {
const input = event.target as HTMLInputElement
const original = input.value
const selectionStart = input.selectionStart ?? original.length
const filtered = filterMailAddressInput(original)
if (filtered !== original) {
const nextSelection = filterMailAddressInput(
original.slice(0, selectionStart),
).length
input.value = filtered
input.setSelectionRange(nextSelection, nextSelection)
}
accountEmail.value = filtered
}
function accountError(error?: string): string {
const known = [
'invalid_email',
@@ -587,9 +608,13 @@ onBeforeUnmount(() => {
:input-class="accountMode === 'register' ? 'pr-20' : undefined"
autocomplete="username"
autocapitalize="none"
autocorrect="off"
inputmode="email"
:maxlength="MAIL_ADDRESS_INPUT_MAX_LENGTH"
pattern="[A-Za-z0-9@._-]*"
spellcheck="false"
:clear-button="accountMode === 'login'"
@input="accountEmail = eventValue($event)"
@input="updateAccountEmail"
@clear="accountEmail = ''"
>
<span
+3 -2
View File
@@ -23,13 +23,14 @@ const accountDevices = [
]
const messages = [
{
body: 'Welcome to iFruit Mail. Your shared mailbox is ready to use.',
body: '## Welcome to iFruit Mail\n\nYour shared mailbox is **ready to use**.\n\n- Send formatted messages\n- Keep drafts on every linked device\n- Reply without losing the conversation\n\n> Tip: use the small formatting bar while composing.',
created_at: '2026-08-04 11:30:00',
folder: 'inbox',
id: 1,
is_read: false,
message_id: 'b73f3872-54cc-4d74-a058-6caa24f0dfff',
preview: 'Welcome to iFruit Mail. Your shared mailbox is ready to use.',
preview:
'Your shared mailbox is **ready to use**. Send formatted messages and keep drafts on every linked device.',
recipients: ['demo@ifruit.com'],
sender: 'support@ifruit.com',
subject: 'Welcome to iFruit Mail',
+7 -2
View File
@@ -89,14 +89,19 @@ Locales["en"] = {
name = "Mail", login = "Sign In", register = "Create Account", registerLink = "Register", loginTitle = "iFruit Mail",
loginBody = "Sign in to use your shared iFruit mailbox.", registerBody = "Choose your new @ifruit.com address.",
localPart = "Email address", email = "Email", password = "Password", confirmPassword = "Confirm password",
accountEyebrow = "Your iFruit account", emailPlaceholder = "name@ifruit.com", passwordPlaceholder = "Enter password",
passwordWarning = "Use an in-character password. Do not reuse a real-world password.",
mailboxes = "Mailboxes", inbox = "Inbox", sent = "Sent", drafts = "Drafts", trash = "Trash",
compose = "New Message", recipients = "To", recipientHint = "Separate up to 10 addresses with commas.",
subject = "Subject", body = "Message", search = "Search mail", noMail = "No Mail",
recipientPlaceholder = "name@ifruit.com", subject = "Subject", subjectPlaceholder = "What's this about?",
body = "Message", messagePlaceholder = "Write a message...", search = "Search mail", messages = "messages",
noMessageContent = "No message content", noMail = "No Mail",
noMailBody = "Messages in this mailbox will appear here.", noResults = "No Results",
noResultsBody = "Try a different search.", untitled = "(no subject)", loadMore = "Load More",
logout = "Sign Out", from = "From", to = "To", reply = "Reply", replyAll = "Reply All", forward = "Forward",
markUnread = "Mark as Unread", moveToTrash = "Move to Trash", restore = "Restore",
formatBold = "Bold", formatItalic = "Italic", formatBulletList = "Bullet list",
formatNumberedList = "Numbered list", formatQuote = "Quote", undo = "Undo", redo = "Redo",
markRead = "Read", markUnread = "Mark as Unread", delete = "Delete", moveToTrash = "Move to Trash", restore = "Restore",
deleteForever = "Delete Forever", emptyTrash = "Empty Trash", emptyTrashTitle = "Empty Trash?",
emptyTrashBody = "Every message in Trash will be permanently deleted.", deleteDraft = "Delete Draft",
sentSuccess = "Message sent.", newMessage = "New mail from {sender}", passwordsMismatch = "Passwords do not match.",
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sky Phone</title>
<script type="module" crossorigin src="./assets/sky-index-D2k1S7R7.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-G8JTwQWl.css">
<script type="module" crossorigin src="./assets/sky-index-Bb3pn2fx.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-0tsbAWVw.css">
</head>
<body>
<div id="app"></div>
+1 -1
View File
@@ -373,7 +373,6 @@ Bridge.Callbacks.Register("sky_phone:mail:delete-draft", function(source, data)
broadcast_mailbox_changed(session.id)
return { success = true }
end)
end)
Bridge.Callbacks.Register("sky_phone:mail:send", function(source, data)
local session, error_response = require_session(source)
@@ -581,3 +580,4 @@ Bridge.Callbacks.Register("sky_phone:mail:empty-trash", function(source)
broadcast_mailbox_changed(session.id)
return { success = true }
end)
end)