mirror of
https://github.com/mroxso/zelo-news.git
synced 2026-06-04 09:31:14 +02:00
Editor (#1)
* add editor * implement pt1 * Add Lexical plugins and update editor functionality in ProfessionalBlogPostForm * Comment out client tag in usePublishBlogPost hook --------- Co-authored-by: highperfocused <highperfocused@pm.me>
This commit is contained in:
@@ -10,11 +10,15 @@
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {
|
||||
"@shadcn-editor": "https://shadcn-editor.vercel.app/r/{name}.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
764
package-lock.json
generated
764
package-lock.json
generated
@@ -11,6 +11,13 @@
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@lexical/code": "^0.36.2",
|
||||
"@lexical/link": "^0.36.2",
|
||||
"@lexical/list": "^0.36.2",
|
||||
"@lexical/markdown": "^0.36.2",
|
||||
"@lexical/react": "^0.36.2",
|
||||
"@lexical/rich-text": "^0.36.2",
|
||||
"@lexical/selection": "^0.36.2",
|
||||
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
|
||||
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
@@ -33,13 +40,13 @@
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slider": "^1.2.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"@unhead/addons": "^2.0.10",
|
||||
"@unhead/react": "^2.0.10",
|
||||
@@ -49,6 +56,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"lexical": "^0.36.2",
|
||||
"lucide-react": "^0.462.0",
|
||||
"nostr-tools": "^2.13.0",
|
||||
"qrcode": "^1.5.4",
|
||||
@@ -909,31 +917,46 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
|
||||
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz",
|
||||
"integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==",
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.16",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
|
||||
"integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.6",
|
||||
"@floating-ui/utils": "^0.2.10",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
@@ -941,9 +964,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fontsource-variable/inter": {
|
||||
@@ -1201,6 +1224,281 @@
|
||||
"resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/0.224.3.tgz",
|
||||
"integrity": "sha512-zAuX2QV1zwJ5RSmrnDGVerAtN3pBXpYYNlGzhERW9AiQ1UJd2/xruyB3i5NdTWy2OK2pjETswOj+0+prYTPlxQ=="
|
||||
},
|
||||
"node_modules/@lexical/clipboard": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.36.2.tgz",
|
||||
"integrity": "sha512-l7z52jltlMz1HmJRmG7ZdxySPjheRRxdV/75QEnzalMtqfLPgh4G5IpycISjbX+95PgEaC6rXbcjPix0CyHDJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/html": "0.36.2",
|
||||
"@lexical/list": "0.36.2",
|
||||
"@lexical/selection": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/code": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.36.2.tgz",
|
||||
"integrity": "sha512-dfS62rNo3uKwNAJQ39zC+8gYX0k8UAoW7u+JPIqx+K2VPukZlvpsPLNGft15pdWBkHc7Pv+o9gJlB6gGv+EBfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2",
|
||||
"prismjs": "^1.30.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/devtools-core": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.36.2.tgz",
|
||||
"integrity": "sha512-G+XW7gR/SCx3YgX4FK9wAIn6AIOkC+j8zRPWrS3GQNZ15CE0QkwQl3IyQ7XW9KzWmdRMs6yTmTVnENFa1JLzXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/html": "0.36.2",
|
||||
"@lexical/link": "0.36.2",
|
||||
"@lexical/mark": "0.36.2",
|
||||
"@lexical/table": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.x",
|
||||
"react-dom": ">=17.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/dragon": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.36.2.tgz",
|
||||
"integrity": "sha512-VWNjYaH74uQ8MFKkl80pTofojpEnTYSX2sgHyZmo1Lk1cKLHK25pMnWgAxPAMLQD5/RW/2PtZcK+j0Kfoe5lSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/extension": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.36.2.tgz",
|
||||
"integrity": "sha512-NWxtqMFMzScq4Eemqp1ST2KREIfj57fUbn7qHv+mMnYgQZK4iIhrHKo5klonxi1oBURcxUZMIbdtH7MJ4BdisA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/utils": "0.36.2",
|
||||
"@preact/signals-core": "^1.11.0",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/hashtag": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.36.2.tgz",
|
||||
"integrity": "sha512-WdmKtzXFcahQT3ShFDeHF6LCR5C8yvFCj3ImI09rZwICrYeonbMrzsBUxS1joBz0HQ+ufF9Tx+RxLvGWx6WxzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/text": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/history": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.36.2.tgz",
|
||||
"integrity": "sha512-pnS36gyMWz1yq/3Z2jv0gUxjJfas5j0GZOM4rFTzDAHjRVc5q3Ua4ElwekdcLaPPGpUlcg3jghIGWa2pSeoPvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/html": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.36.2.tgz",
|
||||
"integrity": "sha512-fgqALzgKnoy93G0yFyYD4C4qJTSMZyUt4JE5kj/POFwWNOnXThIqJhQGwBvH/ibImpIfOeds2TrSr8PbStlrNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/selection": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/link": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.36.2.tgz",
|
||||
"integrity": "sha512-Zb+DeHA1po8VMiOAAXsBmAHhfWmQttsUkI5oiZUmOXJruRuQ2rVr01NoxHpoEpLwHOABVNzD3PMbwov+g3c7lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/list": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.36.2.tgz",
|
||||
"integrity": "sha512-JpaIaE0lgNUrAR7iaCaIoETcCKG9EvZjM3G71VxiexTs7PltmEMq36LUlO2goafWurP7knG2rUpVnTcuSbYYeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.36.2",
|
||||
"@lexical/selection": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/mark": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.36.2.tgz",
|
||||
"integrity": "sha512-n0MNXtGH+1i43hglgHjpQV0093HmIiFR7Budg2BJb8ZNzO1KZRqeXAHlA5ZzJ698FkAnS4R5bqG9tZ0JJHgAuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/markdown": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.36.2.tgz",
|
||||
"integrity": "sha512-jI4McaVKUo8ADOYNCB5LnYyxXDyOWBOofM05r42R9QIMyUxGryo43WNPMAYXzCgtHlkQv+FNles9OlQY0IlAag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/code": "0.36.2",
|
||||
"@lexical/link": "0.36.2",
|
||||
"@lexical/list": "0.36.2",
|
||||
"@lexical/rich-text": "0.36.2",
|
||||
"@lexical/text": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/offset": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.36.2.tgz",
|
||||
"integrity": "sha512-+QQNwzFW/joes3DhNINpGdEX6O5scUTs4n8pYDyM/3pWb+8oCHRaRtEmpUU9HStbdy/pK2kQ9XdztkrNvP/ilA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/overflow": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.36.2.tgz",
|
||||
"integrity": "sha512-bLaEe93iZIJH5wDh6e/DTZVNz7xO7lMS5akcJW8CIwopr4I/Qv2uCvc4G1bMMHx2xM1gVxstn5rFgIUP8/Gqlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/plain-text": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.36.2.tgz",
|
||||
"integrity": "sha512-c9F/+WHl2QuXVhu+1bBVo6BIrSjCcixLe5ePKxoUpy+B7W72s3VCoAQZp+pmtPIyodDLmZAx78hZBBlzoIOeeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.36.2",
|
||||
"@lexical/dragon": "0.36.2",
|
||||
"@lexical/selection": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/react": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.36.2.tgz",
|
||||
"integrity": "sha512-mPVm1BmeuMsMpVyUplgc0btOI8+Vm9bZj4AftgfMSkvzkr8i6NkLn8LV5IlEnoRvxXkjOExwlwBwdQte5ZGvNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.16",
|
||||
"@lexical/devtools-core": "0.36.2",
|
||||
"@lexical/dragon": "0.36.2",
|
||||
"@lexical/extension": "0.36.2",
|
||||
"@lexical/hashtag": "0.36.2",
|
||||
"@lexical/history": "0.36.2",
|
||||
"@lexical/link": "0.36.2",
|
||||
"@lexical/list": "0.36.2",
|
||||
"@lexical/mark": "0.36.2",
|
||||
"@lexical/markdown": "0.36.2",
|
||||
"@lexical/overflow": "0.36.2",
|
||||
"@lexical/plain-text": "0.36.2",
|
||||
"@lexical/rich-text": "0.36.2",
|
||||
"@lexical/table": "0.36.2",
|
||||
"@lexical/text": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"@lexical/yjs": "0.36.2",
|
||||
"lexical": "0.36.2",
|
||||
"react-error-boundary": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.x",
|
||||
"react-dom": ">=17.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/rich-text": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.36.2.tgz",
|
||||
"integrity": "sha512-dZ7zAIv5NBrh1ApxIT9bayn96zfQHHdnT+oaqmR+q100Vo2uROeR/ZF5igeAuwYGM1Z3ZWDBvNxRKd1d6FWiZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.36.2",
|
||||
"@lexical/dragon": "0.36.2",
|
||||
"@lexical/selection": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/selection": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.36.2.tgz",
|
||||
"integrity": "sha512-n96joW3HCKBmPeESR172BxVE+m8V9SdidQm4kKb9jOZ1Ota+tnam2386TeI6795TWwgjDQJPK3HZNKcX6Gb+Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/table": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.36.2.tgz",
|
||||
"integrity": "sha512-96rNNPiVbC65i+Jn1QzIsehCS7UVUc69ovrh9Bt4+pXDebZSdZai153Q7RUq8q3AQ5ocK4/SA2kLQfMu0grj3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.36.2",
|
||||
"@lexical/extension": "0.36.2",
|
||||
"@lexical/utils": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/text": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.36.2.tgz",
|
||||
"integrity": "sha512-IbbqgRdMAD6Uk9b2+qSVoy+8RVcczrz6OgXvg39+EYD+XEC7Rbw7kDTWzuNSJJpP7vxSO8YDZSaIlP5gNH3qKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/utils": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.36.2.tgz",
|
||||
"integrity": "sha512-P9+t2Ob10YNGYT/PWEER+1EqH8SAjCNRn+7SBvKbr0IdleGF2JvzbJwAWaRwZs1c18P11XdQZ779dGvWlfwBIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/list": "0.36.2",
|
||||
"@lexical/selection": "0.36.2",
|
||||
"@lexical/table": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/yjs": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.36.2.tgz",
|
||||
"integrity": "sha512-gZ66Mw+uKXTO8KeX/hNKAinXbFg3gnNYraG76lBXCwb/Ka3q34upIY9FUeGOwGVaau3iIDQhE49I+6MugAX2FQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/offset": "0.36.2",
|
||||
"@lexical/selection": "0.36.2",
|
||||
"lexical": "0.36.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"yjs": ">=13.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||
@@ -1317,6 +1615,16 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@preact/signals-core": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.12.1.tgz",
|
||||
"integrity": "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
@@ -1388,6 +1696,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz",
|
||||
@@ -1547,6 +1873,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@@ -1641,6 +1985,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
@@ -1864,6 +2226,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menubar": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.12.tgz",
|
||||
@@ -1969,6 +2349,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz",
|
||||
@@ -2072,6 +2470,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-progress": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.4.tgz",
|
||||
@@ -2233,6 +2649,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz",
|
||||
@@ -2290,9 +2724,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
@@ -2455,23 +2889,205 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.4.tgz",
|
||||
"integrity": "sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w==",
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
||||
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.7",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.4",
|
||||
"@radix-ui/react-portal": "1.1.6",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.1.0",
|
||||
"@radix-ui/react-slot": "1.2.0",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-visually-hidden": "1.2.0"
|
||||
"@radix-ui/react-visually-hidden": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-rect": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1",
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -5907,6 +6523,17 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isomorphic.js": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
||||
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
@@ -6047,6 +6674,34 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lexical": {
|
||||
"version": "0.36.2",
|
||||
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.36.2.tgz",
|
||||
"integrity": "sha512-gIDJCmSAhtxD7h95WK17Nz19wCZu92Zn0p1/R45X01S/KAsLCwEtVJ2fTvIJNFTyx3QNJTuGcm5mYgRMUwq8rg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lib0": {
|
||||
"version": "0.2.114",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
|
||||
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"isomorphic.js": "^0.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
|
||||
"0gentesthtml": "bin/gentesthtml.js",
|
||||
"0serve": "bin/0serve.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
@@ -7674,6 +8329,15 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -7787,6 +8451,18 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-error-boundary": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
|
||||
"integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.56.1",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz",
|
||||
@@ -8580,6 +9256,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||
@@ -9835,6 +10517,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yjs": {
|
||||
"version": "13.6.27",
|
||||
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
|
||||
"integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.99"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
12
package.json
12
package.json
@@ -13,6 +13,13 @@
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@lexical/code": "^0.36.2",
|
||||
"@lexical/link": "^0.36.2",
|
||||
"@lexical/list": "^0.36.2",
|
||||
"@lexical/markdown": "^0.36.2",
|
||||
"@lexical/react": "^0.36.2",
|
||||
"@lexical/rich-text": "^0.36.2",
|
||||
"@lexical/selection": "^0.36.2",
|
||||
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
|
||||
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
@@ -35,13 +42,13 @@
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slider": "^1.2.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"@unhead/addons": "^2.0.10",
|
||||
"@unhead/react": "^2.0.10",
|
||||
@@ -51,6 +58,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"lexical": "^0.36.2",
|
||||
"lucide-react": "^0.462.0",
|
||||
"nostr-tools": "^2.13.0",
|
||||
"qrcode": "^1.5.4",
|
||||
|
||||
485
src/components/ProfessionalBlogPostForm.tsx
Normal file
485
src/components/ProfessionalBlogPostForm.tsx
Normal file
@@ -0,0 +1,485 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { SerializedEditorState } from 'lexical';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { usePublishBlogPost } from '@/hooks/usePublishBlogPost';
|
||||
import { useBlogPost } from '@/hooks/useBlogPost';
|
||||
import { useUploadFile } from '@/hooks/useUploadFile';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { AlertCircle, Loader2, Upload, Image as ImageIcon, FileText, Hash, Calendar } from 'lucide-react';
|
||||
import { Editor } from '@/components/blocks/editor-00/editor';
|
||||
|
||||
interface ProfessionalBlogPostFormProps {
|
||||
/** Existing post identifier for editing (optional) */
|
||||
editIdentifier?: string;
|
||||
}
|
||||
|
||||
const initialEditorState = {
|
||||
root: {
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
direction: "ltr",
|
||||
format: "",
|
||||
indent: 0,
|
||||
type: "paragraph",
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: "ltr",
|
||||
format: "",
|
||||
indent: 0,
|
||||
type: "root",
|
||||
version: 1,
|
||||
},
|
||||
} as unknown as SerializedEditorState;
|
||||
|
||||
export function ProfessionalBlogPostForm({ editIdentifier }: ProfessionalBlogPostFormProps) {
|
||||
const { user } = useCurrentUser();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const { mutateAsync: publishPost, isPending: isPublishing } = usePublishBlogPost();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
|
||||
// Load existing post if editing (using the current user's pubkey)
|
||||
const { data: existingPost, isLoading: isLoadingPost } = useBlogPost(
|
||||
user?.pubkey || '',
|
||||
editIdentifier || ''
|
||||
);
|
||||
|
||||
const [editorState, setEditorState] = useState<SerializedEditorState>(initialEditorState);
|
||||
const [metadata, setMetadata] = useState({
|
||||
identifier: '',
|
||||
title: '',
|
||||
summary: '',
|
||||
image: '',
|
||||
hashtags: '',
|
||||
});
|
||||
const [showMetadata, setShowMetadata] = useState(true);
|
||||
|
||||
// Load existing post data when editing
|
||||
useEffect(() => {
|
||||
if (existingPost && editIdentifier) {
|
||||
const d = existingPost.tags.find(([name]) => name === 'd')?.[1] || '';
|
||||
const title = existingPost.tags.find(([name]) => name === 'title')?.[1] || '';
|
||||
const summary = existingPost.tags.find(([name]) => name === 'summary')?.[1] || '';
|
||||
const image = existingPost.tags.find(([name]) => name === 'image')?.[1] || '';
|
||||
const hashtags = existingPost.tags
|
||||
.filter(([name]) => name === 't')
|
||||
.map(([, value]) => value)
|
||||
.join(', ');
|
||||
|
||||
setMetadata({
|
||||
identifier: d,
|
||||
title,
|
||||
summary,
|
||||
image,
|
||||
hashtags,
|
||||
});
|
||||
|
||||
// Convert markdown content to editor state
|
||||
// We'll use a simple approach - the editor will handle the markdown
|
||||
// For now, we'll just set it as the initial state
|
||||
if (existingPost.content) {
|
||||
try {
|
||||
// Create a simple editor state with the markdown content as text
|
||||
// The Lexical markdown plugin should handle conversion
|
||||
const contentState = {
|
||||
root: {
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: "normal",
|
||||
style: "",
|
||||
text: existingPost.content,
|
||||
type: "text",
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: "ltr",
|
||||
format: "",
|
||||
indent: 0,
|
||||
type: "paragraph",
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: "ltr",
|
||||
format: "",
|
||||
indent: 0,
|
||||
type: "root",
|
||||
version: 1,
|
||||
},
|
||||
} as unknown as SerializedEditorState;
|
||||
setEditorState(contentState);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse existing content:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [existingPost, editIdentifier]);
|
||||
|
||||
const handleMetadataChange = (field: keyof typeof metadata, value: string) => {
|
||||
setMetadata(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const [[_, url]] = await uploadFile(file);
|
||||
setMetadata(prev => ({ ...prev, image: url }));
|
||||
} catch (error) {
|
||||
console.error('Failed to upload image:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getMarkdownFromEditor = (): string => {
|
||||
// Extract text content from the editor state
|
||||
// In a full implementation, you'd use $convertToMarkdownString with proper transformers
|
||||
try {
|
||||
interface EditorNode {
|
||||
children?: Array<{ text?: string }>;
|
||||
}
|
||||
|
||||
const root = editorState.root as { children?: EditorNode[] };
|
||||
const content = (root.children || [])
|
||||
.map((child: EditorNode) => {
|
||||
if (child.children && Array.isArray(child.children)) {
|
||||
return child.children
|
||||
.map((textNode) => textNode.text || '')
|
||||
.join('');
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('\n\n');
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error('Failed to extract markdown:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!metadata.identifier.trim()) {
|
||||
alert('Please provide a unique identifier for your post');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!metadata.title.trim()) {
|
||||
alert('Please provide a title for your post');
|
||||
return;
|
||||
}
|
||||
|
||||
const markdownContent = getMarkdownFromEditor();
|
||||
|
||||
if (!markdownContent.trim()) {
|
||||
alert('Please write some content for your post');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const publishedAt = editIdentifier && existingPost
|
||||
? parseInt(existingPost.tags.find(([name]) => name === 'published_at')?.[1] || '0')
|
||||
: Math.floor(Date.now() / 1000);
|
||||
|
||||
const event = await publishPost({
|
||||
identifier: metadata.identifier,
|
||||
title: metadata.title,
|
||||
summary: metadata.summary || undefined,
|
||||
image: metadata.image || undefined,
|
||||
content: markdownContent,
|
||||
hashtags: metadata.hashtags
|
||||
? metadata.hashtags.split(',').map(t => t.trim()).filter(Boolean)
|
||||
: undefined,
|
||||
publishedAt: publishedAt || undefined,
|
||||
});
|
||||
|
||||
// Navigate to the post
|
||||
const naddr = nip19.naddrEncode({
|
||||
kind: 30023,
|
||||
pubkey: event.pubkey,
|
||||
identifier: metadata.identifier,
|
||||
});
|
||||
navigate(`/${naddr}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to publish post:', error);
|
||||
alert('Failed to publish post. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You must be logged in to create a blog post.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user is trying to edit someone else's post
|
||||
if (editIdentifier && existingPost && existingPost.pubkey !== user.pubkey) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You can only edit your own posts.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoadingPost) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
{editIdentifier ? 'Edit Article' : 'New Article'}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{editIdentifier ? 'Update your article' : 'Share your thoughts with the world'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
{isPublishing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Publishing...
|
||||
</>
|
||||
) : (
|
||||
editIdentifier ? 'Update Article' : 'Publish Article'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Metadata Section */}
|
||||
<Card>
|
||||
<CardHeader className="cursor-pointer" onClick={() => setShowMetadata(!showMetadata)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Article Metadata
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="sm">
|
||||
{showMetadata ? 'Hide' : 'Show'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{showMetadata && (
|
||||
<CardContent className="space-y-6">
|
||||
{/* Identifier */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="identifier" className="flex items-center gap-2">
|
||||
<Hash className="h-4 w-4" />
|
||||
Identifier <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="identifier"
|
||||
value={metadata.identifier}
|
||||
onChange={(e) => handleMetadataChange('identifier', e.target.value)}
|
||||
placeholder="my-awesome-article"
|
||||
required
|
||||
disabled={!!editIdentifier}
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
URL-friendly identifier (e.g., "my-awesome-article"). Cannot be changed after publishing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title" className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
Title <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={metadata.title}
|
||||
onChange={(e) => handleMetadataChange('title', e.target.value)}
|
||||
placeholder="The Amazing Story of..."
|
||||
required
|
||||
className="text-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="summary">
|
||||
Summary
|
||||
</Label>
|
||||
<Textarea
|
||||
id="summary"
|
||||
value={metadata.summary}
|
||||
onChange={(e) => handleMetadataChange('summary', e.target.value)}
|
||||
placeholder="A compelling summary that will appear in previews and search results..."
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Brief description of your article (recommended for better discoverability)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cover Image */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="image" className="flex items-center gap-2">
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
Cover Image
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="image"
|
||||
value={metadata.image}
|
||||
onChange={(e) => handleMetadataChange('image', e.target.value)}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
type="url"
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageUpload}
|
||||
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||
disabled={isUploading}
|
||||
/>
|
||||
<Button type="button" variant="outline" disabled={isUploading}>
|
||||
{isUploading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{metadata.image && (
|
||||
<div className="mt-2 rounded-lg overflow-hidden border">
|
||||
<img
|
||||
src={metadata.image}
|
||||
alt="Cover preview"
|
||||
className="w-full max-h-64 object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hashtags */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hashtags" className="flex items-center gap-2">
|
||||
<Hash className="h-4 w-4" />
|
||||
Topics & Tags
|
||||
</Label>
|
||||
<Input
|
||||
id="hashtags"
|
||||
value={metadata.hashtags}
|
||||
onChange={(e) => handleMetadataChange('hashtags', e.target.value)}
|
||||
placeholder="bitcoin, technology, innovation"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Comma-separated tags to help readers discover your content
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{editIdentifier && existingPost && (
|
||||
<div className="pt-2 border-t">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>
|
||||
Originally published: {new Date(parseInt(existingPost.tags.find(([name]) => name === 'published_at')?.[1] || '0') * 1000).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Editor Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Content</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div >
|
||||
<Editor
|
||||
editorSerializedState={editorState}
|
||||
onSerializedChange={(value) => setEditorState(value)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-4">
|
||||
Write your article using the rich text editor. Markdown formatting is supported.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Mobile Action Buttons */}
|
||||
{isMobile && (
|
||||
<Card className="sticky bottom-4 shadow-lg">
|
||||
<CardContent className="py-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate('/')}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isPublishing}
|
||||
className="flex-1"
|
||||
>
|
||||
{isPublishing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Publishing...
|
||||
</>
|
||||
) : (
|
||||
editIdentifier ? 'Update' : 'Publish'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
src/components/blocks/editor-00/editor.tsx
Normal file
61
src/components/blocks/editor-00/editor.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
InitialConfigType,
|
||||
LexicalComposer,
|
||||
} from "@lexical/react/LexicalComposer"
|
||||
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"
|
||||
import { EditorState, SerializedEditorState } from "lexical"
|
||||
|
||||
import { editorTheme } from "@/components/editor/themes/editor-theme"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
|
||||
import { nodes } from "./nodes"
|
||||
import { Plugins } from "./plugins"
|
||||
|
||||
const editorConfig: InitialConfigType = {
|
||||
namespace: "Editor",
|
||||
theme: editorTheme,
|
||||
nodes,
|
||||
onError: (error: Error) => {
|
||||
console.error(error)
|
||||
},
|
||||
}
|
||||
|
||||
export function Editor({
|
||||
editorState,
|
||||
editorSerializedState,
|
||||
onChange,
|
||||
onSerializedChange,
|
||||
}: {
|
||||
editorState?: EditorState
|
||||
editorSerializedState?: SerializedEditorState
|
||||
onChange?: (editorState: EditorState) => void
|
||||
onSerializedChange?: (editorSerializedState: SerializedEditorState) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-background overflow-hidden rounded-lg border shadow">
|
||||
<LexicalComposer
|
||||
initialConfig={{
|
||||
...editorConfig,
|
||||
...(editorState ? { editorState } : {}),
|
||||
...(editorSerializedState
|
||||
? { editorState: JSON.stringify(editorSerializedState) }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Plugins />
|
||||
|
||||
<OnChangePlugin
|
||||
ignoreSelectionChange={true}
|
||||
onChange={(editorState) => {
|
||||
onChange?.(editorState)
|
||||
onSerializedChange?.(editorState.toJSON())
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</LexicalComposer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
src/components/blocks/editor-00/nodes.ts
Normal file
25
src/components/blocks/editor-00/nodes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { HeadingNode, QuoteNode } from "@lexical/rich-text"
|
||||
import { ListNode, ListItemNode } from "@lexical/list"
|
||||
import { CodeNode, CodeHighlightNode } from "@lexical/code"
|
||||
import { LinkNode, AutoLinkNode } from "@lexical/link"
|
||||
import {
|
||||
Klass,
|
||||
LexicalNode,
|
||||
LexicalNodeReplacement,
|
||||
ParagraphNode,
|
||||
TextNode,
|
||||
} from "lexical"
|
||||
|
||||
export const nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement> =
|
||||
[
|
||||
HeadingNode,
|
||||
ParagraphNode,
|
||||
TextNode,
|
||||
QuoteNode,
|
||||
ListNode,
|
||||
ListItemNode,
|
||||
CodeNode,
|
||||
CodeHighlightNode,
|
||||
LinkNode,
|
||||
AutoLinkNode,
|
||||
]
|
||||
49
src/components/blocks/editor-00/plugins.tsx
Normal file
49
src/components/blocks/editor-00/plugins.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useState } from "react"
|
||||
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
|
||||
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
|
||||
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
|
||||
import { ListPlugin } from "@lexical/react/LexicalListPlugin"
|
||||
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"
|
||||
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"
|
||||
import { TRANSFORMERS } from "@lexical/markdown"
|
||||
|
||||
import { ContentEditable } from "@/components/editor/editor-ui/content-editable"
|
||||
import { ToolbarPlugin } from "@/components/editor/plugins/toolbar/ToolbarPlugin"
|
||||
|
||||
export function Plugins() {
|
||||
const [_floatingAnchorElem, setFloatingAnchorElem] =
|
||||
useState<HTMLDivElement | null>(null)
|
||||
|
||||
const onRef = (_floatingAnchorElem: HTMLDivElement) => {
|
||||
if (_floatingAnchorElem !== null) {
|
||||
setFloatingAnchorElem(_floatingAnchorElem)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Toolbar */}
|
||||
<ToolbarPlugin />
|
||||
|
||||
{/* Core Plugins */}
|
||||
<HistoryPlugin />
|
||||
<ListPlugin />
|
||||
<LinkPlugin />
|
||||
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
|
||||
|
||||
{/* Editor */}
|
||||
<div className="relative">
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<div className="min-h-[400px]">
|
||||
<div className="" ref={onRef}>
|
||||
<ContentEditable placeholder={"Start typing your article..."} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
src/components/editor/editor-ui/content-editable.tsx
Normal file
34
src/components/editor/editor-ui/content-editable.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { JSX } from "react"
|
||||
import { ContentEditable as LexicalContentEditable } from "@lexical/react/LexicalContentEditable"
|
||||
|
||||
type Props = {
|
||||
placeholder: string
|
||||
className?: string
|
||||
placeholderClassName?: string
|
||||
}
|
||||
|
||||
export function ContentEditable({
|
||||
placeholder,
|
||||
className,
|
||||
placeholderClassName,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<LexicalContentEditable
|
||||
className={
|
||||
className ??
|
||||
`ContentEditable__root relative block min-h-72 min-h-full overflow-auto px-8 py-4 focus:outline-none`
|
||||
}
|
||||
aria-placeholder={placeholder}
|
||||
placeholder={
|
||||
<div
|
||||
className={
|
||||
placeholderClassName ??
|
||||
`text-muted-foreground pointer-events-none absolute top-0 left-0 overflow-hidden px-8 py-[18px] text-ellipsis select-none`
|
||||
}
|
||||
>
|
||||
{placeholder}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
3
src/components/editor/plugins/actions/actions-plugin.tsx
Normal file
3
src/components/editor/plugins/actions/actions-plugin.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function ActionsPlugin({ children }: { children: React.ReactNode }) {
|
||||
return children
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useCallback } from "react"
|
||||
import { $createCodeNode, $isCodeNode } from "@lexical/code"
|
||||
import {
|
||||
$convertFromMarkdownString,
|
||||
$convertToMarkdownString,
|
||||
Transformer,
|
||||
} from "@lexical/markdown"
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
||||
import { $createTextNode, $getRoot } from "lexical"
|
||||
import { FileTextIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function MarkdownTogglePlugin({
|
||||
shouldPreserveNewLinesInMarkdown,
|
||||
transformers,
|
||||
}: {
|
||||
shouldPreserveNewLinesInMarkdown: boolean
|
||||
transformers: Array<Transformer>
|
||||
}) {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const handleMarkdownToggle = useCallback(() => {
|
||||
editor.update(() => {
|
||||
const root = $getRoot()
|
||||
const firstChild = root.getFirstChild()
|
||||
if ($isCodeNode(firstChild) && firstChild.getLanguage() === "markdown") {
|
||||
$convertFromMarkdownString(
|
||||
firstChild.getTextContent(),
|
||||
transformers,
|
||||
undefined, // node
|
||||
shouldPreserveNewLinesInMarkdown
|
||||
)
|
||||
} else {
|
||||
const markdown = $convertToMarkdownString(
|
||||
transformers,
|
||||
undefined, //node
|
||||
shouldPreserveNewLinesInMarkdown
|
||||
)
|
||||
const codeNode = $createCodeNode("markdown")
|
||||
codeNode.append($createTextNode(markdown))
|
||||
root.clear().append(codeNode)
|
||||
if (markdown.length === 0) {
|
||||
codeNode.select()
|
||||
}
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editor, shouldPreserveNewLinesInMarkdown])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
onClick={handleMarkdownToggle}
|
||||
title="Convert From Markdown"
|
||||
aria-label="Convert from markdown"
|
||||
size={"sm"}
|
||||
className="p-2"
|
||||
>
|
||||
<FileTextIcon className="size-4" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
477
src/components/editor/plugins/toolbar/ToolbarPlugin.tsx
Normal file
477
src/components/editor/plugins/toolbar/ToolbarPlugin.tsx
Normal file
@@ -0,0 +1,477 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
FORMAT_ELEMENT_COMMAND,
|
||||
REDO_COMMAND,
|
||||
UNDO_COMMAND,
|
||||
$createParagraphNode,
|
||||
} from 'lexical';
|
||||
import { $setBlocksType } from '@lexical/selection';
|
||||
import {
|
||||
INSERT_ORDERED_LIST_COMMAND,
|
||||
INSERT_UNORDERED_LIST_COMMAND,
|
||||
REMOVE_LIST_COMMAND,
|
||||
$isListNode,
|
||||
ListNode,
|
||||
} from '@lexical/list';
|
||||
import {
|
||||
$createHeadingNode,
|
||||
$createQuoteNode,
|
||||
$isHeadingNode,
|
||||
type HeadingTagType,
|
||||
} from '@lexical/rich-text';
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
||||
import { $isCodeNode, $createCodeNode } from '@lexical/code';
|
||||
import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Underline,
|
||||
Strikethrough,
|
||||
Code,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Link as LinkIcon,
|
||||
AlignLeft,
|
||||
AlignCenter,
|
||||
AlignRight,
|
||||
AlignJustify,
|
||||
Undo,
|
||||
Redo,
|
||||
FileCode,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Toggle } from '@/components/ui/toggle';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
|
||||
const LowPriority = 1;
|
||||
|
||||
type BlockType = 'paragraph' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'quote' | 'code' | 'ul' | 'ol';
|
||||
|
||||
export function ToolbarPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [blockType, setBlockType] = useState<BlockType>('paragraph');
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
const [isItalic, setIsItalic] = useState(false);
|
||||
const [isUnderline, setIsUnderline] = useState(false);
|
||||
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false);
|
||||
|
||||
const updateToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
// Update text format
|
||||
setIsBold(selection.hasFormat('bold'));
|
||||
setIsItalic(selection.hasFormat('italic'));
|
||||
setIsUnderline(selection.hasFormat('underline'));
|
||||
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
||||
setIsCode(selection.hasFormat('code'));
|
||||
|
||||
// Update link
|
||||
const node = selection.anchor.getNode();
|
||||
const parent = node.getParent();
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true);
|
||||
} else {
|
||||
setIsLink(false);
|
||||
}
|
||||
|
||||
// Update block type
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
const element =
|
||||
anchorNode.getKey() === 'root'
|
||||
? anchorNode
|
||||
: anchorNode.getTopLevelElementOrThrow();
|
||||
|
||||
const elementKey = element.getKey();
|
||||
const elementDOM = editor.getElementByKey(elementKey);
|
||||
|
||||
if (elementDOM !== null) {
|
||||
if ($isListNode(element)) {
|
||||
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
|
||||
const type = parentList ? parentList.getListType() : element.getListType();
|
||||
setBlockType(type === 'number' ? 'ol' : 'ul');
|
||||
} else {
|
||||
const type = $isHeadingNode(element)
|
||||
? element.getTag()
|
||||
: element.getType();
|
||||
setBlockType(type as BlockType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
updateToolbar();
|
||||
});
|
||||
}),
|
||||
editor.registerCommand(
|
||||
REDO_COMMAND,
|
||||
() => {
|
||||
setCanRedo(editor.getEditorState()._nodeMap.size > 0);
|
||||
return false;
|
||||
},
|
||||
LowPriority
|
||||
),
|
||||
editor.registerCommand(
|
||||
UNDO_COMMAND,
|
||||
() => {
|
||||
setCanUndo(editor.getEditorState()._nodeMap.size > 0);
|
||||
return false;
|
||||
},
|
||||
LowPriority
|
||||
)
|
||||
);
|
||||
}, [editor, updateToolbar]);
|
||||
|
||||
const formatText = (format: 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code') => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
||||
};
|
||||
|
||||
const formatBlockType = (type: BlockType) => {
|
||||
if (type === 'paragraph') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createParagraphNode());
|
||||
}
|
||||
});
|
||||
} else if (type === 'h1' || type === 'h2' || type === 'h3' || type === 'h4' || type === 'h5' || type === 'h6') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode(type as HeadingTagType));
|
||||
}
|
||||
});
|
||||
} else if (type === 'quote') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createQuoteNode());
|
||||
}
|
||||
});
|
||||
} else if (type === 'code') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
if ($isCodeNode(selection.anchor.getNode())) {
|
||||
$setBlocksType(selection, () => $createParagraphNode());
|
||||
} else {
|
||||
$setBlocksType(selection, () => $createCodeNode());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (type === 'ul') {
|
||||
if (blockType !== 'ul') {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
} else if (type === 'ol') {
|
||||
if (blockType !== 'ol') {
|
||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatAlignment = (alignment: 'left' | 'center' | 'right' | 'justify') => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment);
|
||||
};
|
||||
|
||||
const insertLink = () => {
|
||||
if (!isLink) {
|
||||
setIsLinkPopoverOpen(true);
|
||||
} else {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinkSubmit = () => {
|
||||
if (linkUrl) {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
|
||||
setLinkUrl('');
|
||||
setIsLinkPopoverOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1 p-2 border-b bg-muted/30">
|
||||
{/* Undo/Redo */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
|
||||
disabled={!canUndo}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Undo"
|
||||
>
|
||||
<Undo className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
|
||||
disabled={!canRedo}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Redo"
|
||||
>
|
||||
<Redo className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Block Type Selector */}
|
||||
<Select value={blockType} onValueChange={(value) => formatBlockType(value as BlockType)}>
|
||||
<SelectTrigger className="h-8 w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="paragraph">Paragraph</SelectItem>
|
||||
<SelectItem value="h1">Heading 1</SelectItem>
|
||||
<SelectItem value="h2">Heading 2</SelectItem>
|
||||
<SelectItem value="h3">Heading 3</SelectItem>
|
||||
<SelectItem value="quote">Quote</SelectItem>
|
||||
<SelectItem value="code">Code Block</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Text Formatting */}
|
||||
<div className="flex gap-1">
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isBold}
|
||||
onPressedChange={() => formatText('bold')}
|
||||
title="Bold"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Bold className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isItalic}
|
||||
onPressedChange={() => formatText('italic')}
|
||||
title="Italic"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Italic className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isUnderline}
|
||||
onPressedChange={() => formatText('underline')}
|
||||
title="Underline"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Underline className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isStrikethrough}
|
||||
onPressedChange={() => formatText('strikethrough')}
|
||||
title="Strikethrough"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Strikethrough className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isCode}
|
||||
onPressedChange={() => formatText('code')}
|
||||
title="Inline Code"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
</Toggle>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Quick Headings */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant={blockType === 'h1' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('h1')}
|
||||
title="Heading 1"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Heading1 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'h2' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('h2')}
|
||||
title="Heading 2"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'h3' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('h3')}
|
||||
title="Heading 3"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Heading3 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Lists and Quote */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant={blockType === 'ul' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('ul')}
|
||||
title="Bullet List"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'ol' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('ol')}
|
||||
title="Numbered List"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'quote' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('quote')}
|
||||
title="Quote"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Quote className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'code' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('code')}
|
||||
title="Code Block"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<FileCode className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Link */}
|
||||
<Popover open={isLinkPopoverOpen} onOpenChange={setIsLinkPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={isLink ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={insertLink}
|
||||
title="Insert Link"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm">Insert Link</h4>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleLinkSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" onClick={handleLinkSubmit}>
|
||||
Insert
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Text Alignment */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => formatAlignment('left')}
|
||||
title="Align Left"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<AlignLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => formatAlignment('center')}
|
||||
title="Align Center"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<AlignCenter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => formatAlignment('right')}
|
||||
title="Align Right"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<AlignRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => formatAlignment('justify')}
|
||||
title="Justify"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<AlignJustify className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
src/components/editor/themes/editor-theme.css
Normal file
91
src/components/editor/themes/editor-theme.css
Normal file
@@ -0,0 +1,91 @@
|
||||
.EditorTheme__code {
|
||||
background-color: transparent;
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
display: block;
|
||||
padding: 8px 8px 8px 52px;
|
||||
line-height: 1.53;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #ccc;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
tab-size: 2;
|
||||
}
|
||||
.EditorTheme__code:before {
|
||||
content: attr(data-gutter);
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
border-right: 1px solid #ccc;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 8px;
|
||||
color: #777;
|
||||
white-space: pre-wrap;
|
||||
text-align: right;
|
||||
min-width: 25px;
|
||||
}
|
||||
.EditorTheme__table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
table-layout: fixed;
|
||||
width: fit-content;
|
||||
width: 100%;
|
||||
margin: 0px 0px 30px 0px;
|
||||
}
|
||||
.EditorTheme__tokenComment {
|
||||
color: slategray;
|
||||
}
|
||||
.EditorTheme__tokenPunctuation {
|
||||
color: #999;
|
||||
}
|
||||
.EditorTheme__tokenProperty {
|
||||
color: #905;
|
||||
}
|
||||
.EditorTheme__tokenSelector {
|
||||
color: #690;
|
||||
}
|
||||
.EditorTheme__tokenOperator {
|
||||
color: #9a6e3a;
|
||||
}
|
||||
.EditorTheme__tokenAttr {
|
||||
color: #07a;
|
||||
}
|
||||
.EditorTheme__tokenVariable {
|
||||
color: #e90;
|
||||
}
|
||||
.EditorTheme__tokenFunction {
|
||||
color: #dd4a68;
|
||||
}
|
||||
|
||||
.Collapsible__container {
|
||||
background-color: var(--background);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.Collapsible__title{
|
||||
padding: 0.25rem;
|
||||
padding-left: 1rem;
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
list-style-type: disclosure-closed;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.Collapsible__title p{
|
||||
display: inline-flex;
|
||||
}
|
||||
.Collapsible__title::marker{
|
||||
color: lightgray;
|
||||
}
|
||||
.Collapsible__container[open] >.Collapsible__title {
|
||||
list-style-type: disclosure-open;
|
||||
}
|
||||
130
src/components/editor/themes/editor-theme.ts
Normal file
130
src/components/editor/themes/editor-theme.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { EditorThemeClasses } from "lexical"
|
||||
|
||||
import "./editor-theme.css"
|
||||
|
||||
export const editorTheme: EditorThemeClasses = {
|
||||
ltr: "text-left",
|
||||
rtl: "text-right",
|
||||
heading: {
|
||||
h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
|
||||
h2: "scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0",
|
||||
h3: "scroll-m-20 text-2xl font-semibold tracking-tight",
|
||||
h4: "scroll-m-20 text-xl font-semibold tracking-tight",
|
||||
h5: "scroll-m-20 text-lg font-semibold tracking-tight",
|
||||
h6: "scroll-m-20 text-base font-semibold tracking-tight",
|
||||
},
|
||||
paragraph: "leading-7 [&:not(:first-child)]:mt-6",
|
||||
quote: "mt-6 border-l-2 pl-6 italic",
|
||||
link: "text-blue-600 hover:underline hover:cursor-pointer",
|
||||
list: {
|
||||
checklist: "relative",
|
||||
listitem: "mx-8",
|
||||
listitemChecked:
|
||||
'relative mx-2 px-6 list-none outline-none line-through before:content-[""] before:w-4 before:h-4 before:top-0.5 before:left-0 before:cursor-pointer before:block before:bg-cover before:absolute before:border before:border-primary before:rounded before:bg-primary before:bg-no-repeat after:content-[""] after:cursor-pointer after:border-white after:border-solid after:absolute after:block after:top-[6px] after:w-[3px] after:left-[7px] after:right-[7px] after:h-[6px] after:rotate-45 after:border-r-2 after:border-b-2 after:border-l-0 after:border-t-0',
|
||||
listitemUnchecked:
|
||||
'relative mx-2 px-6 list-none outline-none before:content-[""] before:w-4 before:h-4 before:top-0.5 before:left-0 before:cursor-pointer before:block before:bg-cover before:absolute before:border before:border-primary before:rounded',
|
||||
nested: {
|
||||
listitem: "list-none before:hidden after:hidden",
|
||||
},
|
||||
ol: "m-0 p-0 list-decimal [&>li]:mt-2",
|
||||
olDepth: [
|
||||
"list-outside !list-decimal",
|
||||
"list-outside !list-[upper-roman]",
|
||||
"list-outside !list-[lower-roman]",
|
||||
"list-outside !list-[upper-alpha]",
|
||||
"list-outside !list-[lower-alpha]",
|
||||
],
|
||||
ul: "m-0 p-0 list-outside [&>li]:mt-2",
|
||||
ulDepth: [
|
||||
"list-outside !list-disc",
|
||||
"list-outside !list-disc",
|
||||
"list-outside !list-disc",
|
||||
"list-outside !list-disc",
|
||||
"list-outside !list-disc",
|
||||
],
|
||||
},
|
||||
hashtag: "text-blue-600 bg-blue-100 rounded-md px-1",
|
||||
text: {
|
||||
bold: "font-bold",
|
||||
code: "bg-gray-100 p-1 rounded-md",
|
||||
italic: "italic",
|
||||
strikethrough: "line-through",
|
||||
subscript: "sub",
|
||||
superscript: "sup",
|
||||
underline: "underline",
|
||||
underlineStrikethrough: "underline line-through",
|
||||
},
|
||||
image: "relative inline-block user-select-none cursor-default editor-image",
|
||||
inlineImage:
|
||||
"relative inline-block user-select-none cursor-default inline-editor-image",
|
||||
keyword: "text-purple-900 font-bold",
|
||||
code: "EditorTheme__code",
|
||||
codeHighlight: {
|
||||
atrule: "EditorTheme__tokenAttr",
|
||||
attr: "EditorTheme__tokenAttr",
|
||||
boolean: "EditorTheme__tokenProperty",
|
||||
builtin: "EditorTheme__tokenSelector",
|
||||
cdata: "EditorTheme__tokenComment",
|
||||
char: "EditorTheme__tokenSelector",
|
||||
class: "EditorTheme__tokenFunction",
|
||||
"class-name": "EditorTheme__tokenFunction",
|
||||
comment: "EditorTheme__tokenComment",
|
||||
constant: "EditorTheme__tokenProperty",
|
||||
deleted: "EditorTheme__tokenProperty",
|
||||
doctype: "EditorTheme__tokenComment",
|
||||
entity: "EditorTheme__tokenOperator",
|
||||
function: "EditorTheme__tokenFunction",
|
||||
important: "EditorTheme__tokenVariable",
|
||||
inserted: "EditorTheme__tokenSelector",
|
||||
keyword: "EditorTheme__tokenAttr",
|
||||
namespace: "EditorTheme__tokenVariable",
|
||||
number: "EditorTheme__tokenProperty",
|
||||
operator: "EditorTheme__tokenOperator",
|
||||
prolog: "EditorTheme__tokenComment",
|
||||
property: "EditorTheme__tokenProperty",
|
||||
punctuation: "EditorTheme__tokenPunctuation",
|
||||
regex: "EditorTheme__tokenVariable",
|
||||
selector: "EditorTheme__tokenSelector",
|
||||
string: "EditorTheme__tokenSelector",
|
||||
symbol: "EditorTheme__tokenProperty",
|
||||
tag: "EditorTheme__tokenProperty",
|
||||
url: "EditorTheme__tokenOperator",
|
||||
variable: "EditorTheme__tokenVariable",
|
||||
},
|
||||
characterLimit: "!bg-destructive/50",
|
||||
table: "EditorTheme__table w-fit overflow-scroll border-collapse",
|
||||
tableCell:
|
||||
'EditorTheme__tableCell w-24 relative border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"',
|
||||
tableCellActionButton:
|
||||
"EditorTheme__tableCellActionButton bg-background block border-0 rounded-2xl w-5 h-5 text-foreground cursor-pointer",
|
||||
tableCellActionButtonContainer:
|
||||
"EditorTheme__tableCellActionButtonContainer block right-1 top-1.5 absolute z-10 w-5 h-5",
|
||||
tableCellEditing: "EditorTheme__tableCellEditing rounded-sm shadow-sm",
|
||||
tableCellHeader:
|
||||
"EditorTheme__tableCellHeader bg-muted border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right",
|
||||
tableCellPrimarySelected:
|
||||
"EditorTheme__tableCellPrimarySelected border border-primary border-solid block h-[calc(100%-2px)] w-[calc(100%-2px)] absolute -left-[1px] -top-[1px] z-10 ",
|
||||
tableCellResizer:
|
||||
"EditorTheme__tableCellResizer absolute -right-1 h-full w-2 cursor-ew-resize z-10 top-0",
|
||||
tableCellSelected: "EditorTheme__tableCellSelected bg-muted",
|
||||
tableCellSortedIndicator:
|
||||
"EditorTheme__tableCellSortedIndicator block opacity-50 bsolute bottom-0 left-0 w-full h-1 bg-muted",
|
||||
tableResizeRuler:
|
||||
"EditorTheme__tableCellResizeRuler block absolute w-[1px] h-full bg-primary top-0",
|
||||
tableRowStriping:
|
||||
"EditorTheme__tableRowStriping m-0 border-t p-0 even:bg-muted",
|
||||
tableSelected: "EditorTheme__tableSelected ring-2 ring-primary ring-offset-2",
|
||||
tableSelection: "EditorTheme__tableSelection bg-transparent",
|
||||
layoutItem: "border border-dashed px-4 py-2",
|
||||
layoutContainer: "grid gap-2.5 my-2.5 mx-0",
|
||||
autocomplete: "text-muted-foreground",
|
||||
blockCursor: "",
|
||||
embedBlock: {
|
||||
base: "user-select-none",
|
||||
focus: "ring-2 ring-primary ring-offset-2",
|
||||
},
|
||||
hr: 'p-0.5 border-none my-1 mx-0 cursor-pointer after:content-[""] after:block after:h-0.5 after:bg-muted selected:ring-2 selected:ring-primary selected:ring-offset-2 selected:user-select-none',
|
||||
indent: "[--lexical-indent-base-value:40px]",
|
||||
mark: "",
|
||||
markOverlap: "",
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export function usePublishBlogPost() {
|
||||
}
|
||||
|
||||
// Add client tag
|
||||
tags.push(['client', 'Nostr Blog']);
|
||||
// tags.push(['client', 'zelo.news']);
|
||||
|
||||
const eventTemplate = {
|
||||
kind: 30023,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BlogPostForm } from '@/components/BlogPostForm';
|
||||
import { ProfessionalBlogPostForm } from '@/components/ProfessionalBlogPostForm';
|
||||
|
||||
export default function CreatePostPage() {
|
||||
return (
|
||||
<div className="container max-w-4xl py-8">
|
||||
<BlogPostForm />
|
||||
<div className="container max-w-7xl py-6 px-4 sm:px-6 lg:px-8">
|
||||
<ProfessionalBlogPostForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useParams, Navigate } from 'react-router-dom';
|
||||
import { BlogPostForm } from '@/components/BlogPostForm';
|
||||
import { ProfessionalBlogPostForm } from '@/components/ProfessionalBlogPostForm';
|
||||
|
||||
export default function EditPostPage() {
|
||||
const { identifier } = useParams<{ identifier: string }>();
|
||||
@@ -9,8 +9,8 @@ export default function EditPostPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl py-8">
|
||||
<BlogPostForm editIdentifier={identifier} />
|
||||
<div className="container max-w-7xl py-6 px-4 sm:px-6 lg:px-8">
|
||||
<ProfessionalBlogPostForm editIdentifier={identifier} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user