Implement interactive Notion-style slash commands menu (/ for formatting)
This commit is contained in:
1 parent
2eb283b9ec
commit
069a210bee
4 files changed
+492
-37
No files matched your search
Generated
+51
-36
@@ -31,6 +31,7 @@
|
|||||||
"@tiptap/pm": "^2.11.5",
|
"@tiptap/pm": "^2.11.5",
|
||||||
"@tiptap/react": "^2.11.5",
|
"@tiptap/react": "^2.11.5",
|
||||||
"@tiptap/starter-kit": "^2.11.5",
|
"@tiptap/starter-kit": "^2.11.5",
|
||||||
|
"@tiptap/suggestion": "^2.27.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"imapflow": "^1.0.171",
|
"imapflow": "^1.0.171",
|
||||||
@@ -1237,7 +1238,7 @@
|
|||||||
"version": "6.19.3",
|
"version": "6.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz",
|
||||||
"integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==",
|
"integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"c12": "3.1.0",
|
"c12": "3.1.0",
|
||||||
@@ -1250,14 +1251,14 @@
|
|||||||
"version": "6.19.3",
|
"version": "6.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz",
|
||||||
"integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==",
|
"integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.19.3",
|
"version": "6.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz",
|
||||||
"integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==",
|
"integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1271,14 +1272,14 @@
|
|||||||
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
||||||
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
|
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.19.3",
|
"version": "6.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz",
|
||||||
"integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==",
|
"integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.19.3",
|
"@prisma/debug": "6.19.3",
|
||||||
@@ -1290,7 +1291,7 @@
|
|||||||
"version": "6.19.3",
|
"version": "6.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz",
|
||||||
"integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==",
|
"integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.19.3"
|
"@prisma/debug": "6.19.3"
|
||||||
@@ -1322,7 +1323,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
@@ -1962,6 +1963,20 @@
|
|||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/suggestion": {
|
||||||
|
"version": "2.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.27.3.tgz",
|
||||||
|
"integrity": "sha512-j8mmf1JF48gKm7sGi0cmmYAnFHa2gXMYCYyaP0e4OaHzQyCEB6HBJbFiFCa8olbO0anZW0KdbA4H5y2FXLVy+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^2.7.0",
|
||||||
|
"@tiptap/pm": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/jsdom": {
|
"node_modules/@types/jsdom": {
|
||||||
"version": "21.1.7",
|
"version": "21.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
|
||||||
@@ -2273,7 +2288,7 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||||
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@@ -2332,7 +2347,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"readdirp": "^4.0.1"
|
||||||
@@ -2348,7 +2363,7 @@
|
|||||||
"version": "0.1.6",
|
"version": "0.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"consola": "^3.2.3"
|
"consola": "^3.2.3"
|
||||||
@@ -2383,14 +2398,14 @@
|
|||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/consola": {
|
"node_modules/consola": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||||
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.10.0"
|
"node": "^14.18.0 || >=16.10.0"
|
||||||
@@ -2491,7 +2506,7 @@
|
|||||||
"version": "7.1.5",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||||
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
@@ -2501,14 +2516,14 @@
|
|||||||
"version": "6.1.7",
|
"version": "6.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/destr": {
|
"node_modules/destr": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
@@ -2606,7 +2621,7 @@
|
|||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -2619,7 +2634,7 @@
|
|||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz",
|
||||||
"integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==",
|
"integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
@@ -2637,7 +2652,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||||
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -2700,14 +2715,14 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz",
|
||||||
"integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==",
|
"integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-check": {
|
"node_modules/fast-check": {
|
||||||
"version": "3.23.2",
|
"version": "3.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||||
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -2846,7 +2861,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||||
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"citty": "^0.1.6",
|
"citty": "^0.1.6",
|
||||||
@@ -3157,7 +3172,7 @@
|
|||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
@@ -3657,7 +3672,7 @@
|
|||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch/node_modules/tr46": {
|
"node_modules/node-fetch/node_modules/tr46": {
|
||||||
@@ -3721,7 +3736,7 @@
|
|||||||
"version": "0.6.11",
|
"version": "0.6.11",
|
||||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.11.tgz",
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.11.tgz",
|
||||||
"integrity": "sha512-wrVBTLWDu7nTUKGWplQebWOXWnhaIwyKQhBxobH/CGxJHoVw27r9vG5ba7kdZnxG+f4rC6dZd6m2oHmpkOcO7g==",
|
"integrity": "sha512-wrVBTLWDu7nTUKGWplQebWOXWnhaIwyKQhBxobH/CGxJHoVw27r9vG5ba7kdZnxG+f4rC6dZd6m2oHmpkOcO7g==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"citty": "^0.2.2",
|
"citty": "^0.2.2",
|
||||||
@@ -3739,7 +3754,7 @@
|
|||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
|
||||||
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
|
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
@@ -3766,7 +3781,7 @@
|
|||||||
"version": "2.0.12",
|
"version": "2.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz",
|
||||||
"integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==",
|
"integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/on-exit-leak-free": {
|
"node_modules/on-exit-leak-free": {
|
||||||
@@ -3835,7 +3850,7 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pdf-parse": {
|
"node_modules/pdf-parse": {
|
||||||
@@ -3883,7 +3898,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
@@ -3956,7 +3971,7 @@
|
|||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.4.0.tgz",
|
||||||
"integrity": "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA==",
|
"integrity": "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"confbox": "^0.3.1",
|
"confbox": "^0.3.1",
|
||||||
@@ -3968,7 +3983,7 @@
|
|||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz",
|
||||||
"integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==",
|
"integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
@@ -4138,7 +4153,7 @@
|
|||||||
"version": "6.19.3",
|
"version": "6.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz",
|
||||||
"integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
|
"integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4399,7 +4414,7 @@
|
|||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||||
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -4443,7 +4458,7 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
@@ -4497,7 +4512,7 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">= 14.18.0"
|
||||||
@@ -5026,7 +5041,7 @@
|
|||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
|
||||||
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
|
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -5152,7 +5167,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"@tiptap/pm": "^2.11.5",
|
"@tiptap/pm": "^2.11.5",
|
||||||
"@tiptap/react": "^2.11.5",
|
"@tiptap/react": "^2.11.5",
|
||||||
"@tiptap/starter-kit": "^2.11.5",
|
"@tiptap/starter-kit": "^2.11.5",
|
||||||
|
"@tiptap/suggestion": "^2.27.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"imapflow": "^1.0.171",
|
"imapflow": "^1.0.171",
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import LinkModal from './LinkModal';
|
import LinkModal from './LinkModal';
|
||||||
|
import {
|
||||||
|
SlashCommandsExtension,
|
||||||
|
SlashCommandMenu,
|
||||||
|
SlashMenuState,
|
||||||
|
initialSlashMenuState,
|
||||||
|
getSlashCommandItems,
|
||||||
|
} from './SlashCommands';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import Underline from '@tiptap/extension-underline';
|
import Underline from '@tiptap/extension-underline';
|
||||||
@@ -103,6 +110,13 @@ export default function RichNoteEditor({
|
|||||||
const [linkModalUrl, setLinkModalUrl] = useState('');
|
const [linkModalUrl, setLinkModalUrl] = useState('');
|
||||||
const [linkModalText, setLinkModalText] = useState('');
|
const [linkModalText, setLinkModalText] = useState('');
|
||||||
|
|
||||||
|
// Slash commands menu state
|
||||||
|
const [slashMenu, setSlashMenu] = useState<SlashMenuState>(initialSlashMenuState);
|
||||||
|
const slashMenuRef = useRef<SlashMenuState>(initialSlashMenuState);
|
||||||
|
slashMenuRef.current = slashMenu;
|
||||||
|
|
||||||
|
const handleOpenLinkModalRef = useRef<() => void>(() => {});
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
editable: !readOnly,
|
editable: !readOnly,
|
||||||
@@ -152,6 +166,88 @@ export default function RichNoteEditor({
|
|||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder: 'Start writing, drag files here, or type / for formatting…',
|
placeholder: 'Start writing, drag files here, or type / for formatting…',
|
||||||
}),
|
}),
|
||||||
|
SlashCommandsExtension.configure({
|
||||||
|
suggestion: {
|
||||||
|
char: '/',
|
||||||
|
startOfLine: false,
|
||||||
|
items: ({ query }: { query: string }) => {
|
||||||
|
const items = getSlashCommandItems({
|
||||||
|
onOpenLinkModal: () => handleOpenLinkModalRef.current(),
|
||||||
|
onTriggerUpload: () => fileInputRef.current?.click(),
|
||||||
|
});
|
||||||
|
const q = query.toLowerCase().trim();
|
||||||
|
if (!q) return items;
|
||||||
|
return items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.title.toLowerCase().includes(q) ||
|
||||||
|
item.keywords.some((k) => k.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
return {
|
||||||
|
onStart: (props: any) => {
|
||||||
|
setSlashMenu({
|
||||||
|
isOpen: true,
|
||||||
|
query: props.query,
|
||||||
|
range: props.range,
|
||||||
|
clientRect: props.clientRect,
|
||||||
|
items: props.items,
|
||||||
|
selectedIndex: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onUpdate: (props: any) => {
|
||||||
|
setSlashMenu((prev) => ({
|
||||||
|
...prev,
|
||||||
|
query: props.query,
|
||||||
|
range: props.range,
|
||||||
|
clientRect: props.clientRect,
|
||||||
|
items: props.items,
|
||||||
|
selectedIndex: 0,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
onKeyDown: (props: any) => {
|
||||||
|
if (props.event.key === 'Escape') {
|
||||||
|
setSlashMenu(initialSlashMenuState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const current = slashMenuRef.current;
|
||||||
|
if (!current.isOpen || current.items.length === 0) return false;
|
||||||
|
|
||||||
|
if (props.event.key === 'ArrowUp') {
|
||||||
|
props.event.preventDefault();
|
||||||
|
setSlashMenu((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedIndex:
|
||||||
|
(prev.selectedIndex + prev.items.length - 1) % prev.items.length,
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.event.key === 'ArrowDown') {
|
||||||
|
props.event.preventDefault();
|
||||||
|
setSlashMenu((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedIndex: (prev.selectedIndex + 1) % prev.items.length,
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.event.key === 'Enter') {
|
||||||
|
props.event.preventDefault();
|
||||||
|
const selected = current.items[current.selectedIndex];
|
||||||
|
if (selected && current.range && props.view.state) {
|
||||||
|
selected.command(props.editor, current.range);
|
||||||
|
}
|
||||||
|
setSlashMenu(initialSlashMenuState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onExit: () => {
|
||||||
|
setSlashMenu(initialSlashMenuState);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
content: sanitizeOrUnescapeLinks(contentHtml || '<p></p>'),
|
content: sanitizeOrUnescapeLinks(contentHtml || '<p></p>'),
|
||||||
editorProps: {
|
editorProps: {
|
||||||
@@ -349,6 +445,7 @@ export default function RichNoteEditor({
|
|||||||
setLinkModalText(selectedText);
|
setLinkModalText(selectedText);
|
||||||
setLinkModalOpen(true);
|
setLinkModalOpen(true);
|
||||||
};
|
};
|
||||||
|
handleOpenLinkModalRef.current = handleOpenLinkModal;
|
||||||
|
|
||||||
const handleApplyLink = (url: string, text?: string) => {
|
const handleApplyLink = (url: string, text?: string) => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
@@ -800,6 +897,18 @@ export default function RichNoteEditor({
|
|||||||
onApplyLink={handleApplyLink}
|
onApplyLink={handleApplyLink}
|
||||||
onUnlink={handleUnlink}
|
onUnlink={handleUnlink}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SlashCommandMenu
|
||||||
|
state={slashMenu}
|
||||||
|
editor={editor}
|
||||||
|
onClose={() => setSlashMenu(initialSlashMenuState)}
|
||||||
|
onSelect={(item) => {
|
||||||
|
if (slashMenu.range && editor) {
|
||||||
|
item.command(editor, slashMenu.range);
|
||||||
|
}
|
||||||
|
setSlashMenu(initialSlashMenuState);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Extension } from '@tiptap/core';
|
||||||
|
import Suggestion, { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion';
|
||||||
|
import { Editor, Range } from '@tiptap/core';
|
||||||
|
import {
|
||||||
|
Heading1,
|
||||||
|
Heading2,
|
||||||
|
Heading3,
|
||||||
|
Type,
|
||||||
|
CheckSquare,
|
||||||
|
List,
|
||||||
|
ListOrdered,
|
||||||
|
Quote,
|
||||||
|
Code,
|
||||||
|
Minus,
|
||||||
|
Table as TableIcon,
|
||||||
|
Link2,
|
||||||
|
Paperclip,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export interface SlashCommandItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
keywords: string[];
|
||||||
|
command: (editor: Editor, range: Range) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSlashCommandItems(handlers: {
|
||||||
|
onOpenLinkModal?: () => void;
|
||||||
|
onTriggerUpload?: () => void;
|
||||||
|
}): SlashCommandItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'text',
|
||||||
|
title: 'Text',
|
||||||
|
description: 'Start writing with plain body text',
|
||||||
|
icon: Type,
|
||||||
|
keywords: ['text', 'paragraph', 'p', 'plain', 'normal'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setParagraph().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heading-1',
|
||||||
|
title: 'Heading 1',
|
||||||
|
description: 'Large section heading',
|
||||||
|
icon: Heading1,
|
||||||
|
keywords: ['h1', 'heading', 'title', 'large'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setHeading({ level: 1 }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heading-2',
|
||||||
|
title: 'Heading 2',
|
||||||
|
description: 'Medium section heading',
|
||||||
|
icon: Heading2,
|
||||||
|
keywords: ['h2', 'heading', 'subtitle', 'medium'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setHeading({ level: 2 }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heading-3',
|
||||||
|
title: 'Heading 3',
|
||||||
|
description: 'Small subsection heading',
|
||||||
|
icon: Heading3,
|
||||||
|
keywords: ['h3', 'heading', 'small'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setHeading({ level: 3 }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'todo',
|
||||||
|
title: 'To-do List',
|
||||||
|
description: 'Interactive checklist with task boxes',
|
||||||
|
icon: CheckSquare,
|
||||||
|
keywords: ['todo', 'task', 'check', 'checkbox', 'list'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleTaskList().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bullet-list',
|
||||||
|
title: 'Bullet List',
|
||||||
|
description: 'Create an unordered bulleted list',
|
||||||
|
icon: List,
|
||||||
|
keywords: ['bullet', 'ul', 'list', 'unordered'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ordered-list',
|
||||||
|
title: 'Numbered List',
|
||||||
|
description: 'Create a sequential numbered list',
|
||||||
|
icon: ListOrdered,
|
||||||
|
keywords: ['ordered', 'ol', 'number', 'numbered', 'list'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quote',
|
||||||
|
title: 'Quote',
|
||||||
|
description: 'Capture a stylized blockquote',
|
||||||
|
icon: Quote,
|
||||||
|
keywords: ['quote', 'blockquote', 'cite'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'code',
|
||||||
|
title: 'Code Block',
|
||||||
|
description: 'Syntax-friendly code snippet block',
|
||||||
|
icon: Code,
|
||||||
|
keywords: ['code', 'snippet', 'pre', 'block'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'divider',
|
||||||
|
title: 'Divider',
|
||||||
|
description: 'Horizontal line separator',
|
||||||
|
icon: Minus,
|
||||||
|
keywords: ['divider', 'hr', 'line', 'separator', 'rule'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'table',
|
||||||
|
title: 'Table',
|
||||||
|
description: 'Insert a 3x3 table with header row',
|
||||||
|
icon: TableIcon,
|
||||||
|
keywords: ['table', 'grid', 'rows', 'columns'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange(range)
|
||||||
|
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'note-link',
|
||||||
|
title: 'Note Link',
|
||||||
|
description: 'Link to another Everclone note',
|
||||||
|
icon: Link2,
|
||||||
|
keywords: ['link', 'internal', 'note', 'evernote'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).run();
|
||||||
|
if (handlers.onOpenLinkModal) handlers.onOpenLinkModal();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'upload',
|
||||||
|
title: 'Upload / Attach File',
|
||||||
|
description: 'Add photos, receipts, or PDF files',
|
||||||
|
icon: Paperclip,
|
||||||
|
keywords: ['upload', 'file', 'image', 'photo', 'pdf', 'receipt', 'attachment'],
|
||||||
|
command: (editor, range) => {
|
||||||
|
editor.chain().focus().deleteRange(range).run();
|
||||||
|
if (handlers.onTriggerUpload) handlers.onTriggerUpload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlashMenuState {
|
||||||
|
isOpen: boolean;
|
||||||
|
query: string;
|
||||||
|
range: Range | null;
|
||||||
|
clientRect: (() => DOMRect | null) | null;
|
||||||
|
items: SlashCommandItem[];
|
||||||
|
selectedIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialSlashMenuState: SlashMenuState = {
|
||||||
|
isOpen: false,
|
||||||
|
query: '',
|
||||||
|
range: null,
|
||||||
|
clientRect: null,
|
||||||
|
items: [],
|
||||||
|
selectedIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SlashCommandsExtension = Extension.create<{
|
||||||
|
suggestion: any;
|
||||||
|
}>({
|
||||||
|
name: 'slashCommands',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
suggestion: {
|
||||||
|
char: '/',
|
||||||
|
startOfLine: false,
|
||||||
|
command: ({ editor, range, props }: any) => {
|
||||||
|
props.command(editor, range);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
Suggestion({
|
||||||
|
editor: this.editor,
|
||||||
|
...this.options.suggestion,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SlashCommandMenuProps {
|
||||||
|
state: SlashMenuState;
|
||||||
|
editor: Editor | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSelect: (item: SlashCommandItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SlashCommandMenu({
|
||||||
|
state,
|
||||||
|
onClose,
|
||||||
|
onSelect,
|
||||||
|
}: SlashCommandMenuProps) {
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.isOpen) return;
|
||||||
|
const handleOutsideClick = (e: MouseEvent) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('mousedown', handleOutsideClick);
|
||||||
|
return () => window.removeEventListener('mousedown', handleOutsideClick);
|
||||||
|
}, [state.isOpen, onClose]);
|
||||||
|
|
||||||
|
if (!state.isOpen || !state.clientRect) return null;
|
||||||
|
|
||||||
|
const rect = state.clientRect();
|
||||||
|
if (!rect) return null;
|
||||||
|
|
||||||
|
// Calculate smart floating position beneath cursor
|
||||||
|
const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 1000;
|
||||||
|
const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 800;
|
||||||
|
|
||||||
|
const menuWidth = 280;
|
||||||
|
const menuHeight = 320;
|
||||||
|
|
||||||
|
let left = rect.left;
|
||||||
|
if (left + menuWidth > viewportWidth - 16) {
|
||||||
|
left = viewportWidth - menuWidth - 16;
|
||||||
|
}
|
||||||
|
if (left < 16) left = 16;
|
||||||
|
|
||||||
|
let top = rect.bottom + 6;
|
||||||
|
if (top + menuHeight > viewportHeight - 16) {
|
||||||
|
top = Math.max(16, rect.top - menuHeight - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: `${top}px`,
|
||||||
|
left: `${left}px`,
|
||||||
|
}}
|
||||||
|
className="z-50 w-72 max-h-80 overflow-y-auto bg-white rounded-xl shadow-2xl border border-gray-200 py-1.5 text-xs animate-in fade-in zoom-in-95 duration-100"
|
||||||
|
>
|
||||||
|
<div className="px-3 py-1 text-[10px] uppercase font-bold tracking-wider text-gray-400 border-b border-gray-100 mb-1">
|
||||||
|
Basic Blocks
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state.items.length === 0 ? (
|
||||||
|
<div className="px-3 py-3 text-center text-gray-400 text-xs">
|
||||||
|
No matching commands for “{state.query}”
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
state.items.map((item, idx) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isSelected = idx === state.selectedIndex;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelect(item)}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
// optional: update hover
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center gap-2.5 px-3 py-2 text-left transition ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-emerald-50 text-gray-900 font-medium'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-7 h-7 rounded-lg flex items-center justify-center shrink-0 border ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-white border-emerald-300 text-evernote-green shadow-xs'
|
||||||
|
: 'bg-gray-50 border-gray-200 text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-xs font-semibold text-gray-900 leading-tight">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-gray-500 truncate leading-tight mt-0.5">
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user