فرانتاند: ویرایشگر WYSIWYG وبلاگ (TipTap) با آپلود تصویر در متن
- ArticleEditor: ویرایشگر TipTap v3 راستبهچسب با نوار ابزار فارسی — بولد/ایتالیک/زیرخط/خطخورده، تیتر H2/H3، لیست نقطهای/شمارهای، نقلقول، لینک (مسدودسازی javascript: با isAllowedUri)، واگرد/ازنو - درج تصویر داخل متن: دکمه آپلود، چسباندن یا کشیدن فایل — ذخیره در MinIO از طریق /api/admin/upload و درج URL نسبی /media/... - ذخیره خودکار: debounce ۸۰۰ms + flush هنگام بستن کشو - کشوی مقاله عریضتر (max-w-3xl)؛ فیلد body مقالات بهجای لیست متنی، ویرایشگر WYSIWYG رندر میکند - رندر صفحه مقاله: تبدیل سند به HTML با @tiptap/static-renderer (فقط تگهای شناختهشده — بدون ریسک XSS) + استایلهای .article-body (تیتر با خط مسی، لیست، نقلقول، تصویر) + پشتیبانی از ردیفهای قدیمی - وابستگیهای جدید: @tiptap/react، @tiptap/pm، @tiptap/starter-kit، @tiptap/extension-image، @tiptap/extension-link، @tiptap/static-renderer
This commit is contained in:
Generated
+680
-3
@@ -9,6 +9,12 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
|
"@tiptap/extension-image": "^3.31.3",
|
||||||
|
"@tiptap/extension-link": "^3.31.3",
|
||||||
|
"@tiptap/pm": "^3.31.3",
|
||||||
|
"@tiptap/react": "^3.31.3",
|
||||||
|
"@tiptap/starter-kit": "^3.31.3",
|
||||||
|
"@tiptap/static-renderer": "^3.31.3",
|
||||||
"framer-motion": "^11.5.4",
|
"framer-motion": "^11.5.4",
|
||||||
"lucide-react": "0.522.0",
|
"lucide-react": "0.522.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -922,6 +928,34 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.8.0",
|
||||||
|
"@floating-ui/utils": "^0.2.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||||
|
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@@ -1427,6 +1461,465 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-Cz50pvciQrxdSxgTkHOVz0uD0Yl/8Xt0QatGD6ILm47jW8EzyHR9RkUGs/D5IqzXKuVPntfw1ttaT926vXfiRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-fyY2XMbyDDDfOTQ1Qdrnqa1qwC9DWE4n7AfE0EKQI0G8MfLV8RaDlLDcOZDJ9JbMPY7/Gx7EjyK4NKxSW9n2hQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-dIuYhKk8TitKU/FeDpoTeWZhU42YgDN5npgWNjAmMmRktPdoxnH3/wGSiwXlqZgJWjehN7kPWDePWpJeAImGpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-EV6ZnwKc++2OM/OcD54n8s1B7C9LP7GKAtdEPwu0t3BYJf3sG8Ueoinf7SgGPO9oMPg96GneOkDNm1urMV167g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-qEyyoPapPef4LO8XKaN83bxtaNzkJ4kFn/IxLnEKd4BJ3Mvi4MH2yJYlyDqwFnMoev4pMA1zBHRAt/C0THRmZw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-SzxOqchrD2AcN3uT67PjKmRFEMOU3vNNiwNaamZJUbZrI6Hmy+bvdHJrc3jrIddyyCrAtsnAfRI0cmorW1jGfg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-nvknt4FhyJQjYcvxptmeUlFsIAc8ibua3E5BN4Pim374/9RWepH4cdE9X0/qUTtguHElLx0iKtSIY3rW2qGTFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-EexgmqnyDNyGlISxo7SMrp5MygpJYmqD+0cY5jB6L1U6L4CpKKRWUt8OO1sWzEHDE1+TTvwt+WIFoIWAziOtEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-NWomSfu5CSC7VacnMSDzKT8qm66SzMfZwVPEtwY5bPpRTJgTiT1rNK0neDrrzfMN27MfylGyKWWf7Q5Qf8w/fg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-rd4VJ9PGSP9Eop8ZTEwaLZcMzMXLuJKe3hUNf58rq+zANpwM+9fI+vB7g9MAc3eXwUNDxNDVACFIL2oeBqpQ3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-EBXKb1FrVStsNYCcRGtd9jmzveCvR+eqgg1rVqoONrqFK6U7bga6LN+1dMKroP1kliDVgveYkP3vRYxqw+rFqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-QAdCvNO4+yW9ATwsrej11NTkDYFqPLIEQr3ARNrKOK1qaiS7A0fia2SEukb/hrkP3A6mbozhoQt2r2RGUf/DpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-rk5VHMAeQcg06SLauN6EGdD2jc0O2qY8QkZYPd0LxNvLblw2BxBx+lxUQSYwLAT9Ie5914gKIK2YbRyO2Ts3ig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-YnHGy2KShRwvCseAmmxl9VP7R0qaj8QMp3DA6DJWZqp7r5gLGvDkAqhedxqqefqsE4Y43hjkBdjtB9Ce78LkIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-image": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-wWNG9BOtx2Cg4vwxPVGDIJ5DGX+ETkldeoaFJLB1NXUL4OPUiVTf8cHZ+hdYgJWP704BEB7jQgjlpvdi6VigTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-ibGvdvAPyfxBMUVNRI43eb9h2/Jka1MRG5GtnGqbcCX/2+Y/y0EOfFrPQPkuYphiWQYyu9+FzPKMB/pjuaLuKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-986wOQzTL9Zr5lf84LCLpm+YOms8A0K39/8DVoqRfebqcOe0/eq4bnztmAlfabOd+kJY92g3AgZERFUx/w+dcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-LoveGnC0FVdCV4jUNBaG1ZA+KWE07+adzV3kGy6uUYFcJEjbVUHTnPDrBOob3IwOSO3sCwIkvQb6EVYeXn/4yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-4QlKOriJJMvg95QJTEsy9BUYPBQ6UvJyb8WURRwdUtQUkkqb8h32lg/eyQUv2FzW9IT1AdUyNZfVaxH64kRfQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-If8UOEdDZbPJU6iYTvLtH6DOp2KBy6BKxg9UELL1AevVetGHEF/7lW8hP50Gn1tHMVpPRqmhSzVrJRpEJJgb/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-mp3g11NgA/PYu8rj7J7Ez3l4qBy6WfTSmHIG4PZvEGG5w2oUAIkgb9DU7nPPzjmeme27oazFYZw+AtZA0+u4tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-+iPku7wJfy5hbNDNLX8dveFtYVsZMmh7vztjuq8hT3mipSC4IByDehDbU8fzUCjfXiEzmI7mQn7c8LGmHULuzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-G29bhKttYwcKHT+BI6emWVFol3RO/gUXxQVcmr/iT8LXXy7j8J6HFUnsKM+Kg5YlP1rxMRgsa65dbrQVahZi0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-gdsWtF+taeaCu6V+5Ct10fGo0ACUy1GnYtbb+mcathBt8OqbT+Ws60p/yEmKesBDz2Hn+B5IcWy6+2BBZl5ZTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-HghdJaOwRqYzsAxqSyNyb+IWyOMcdCl8IoiBETA9BZCJAqdXzFLcuWp7CqCqJPam9dgkWosSoHqTCAO4nTBfpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-8sJNPGGUe8f3aDojcOW5cfVL7I5NrBbE0UWxG08qoi9Tea6qWbvQJsCR9tsrOapr/DaLr3kpbGZ9s1gEEfcNcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-sZime0SWsz/k62W2WvHx5Ig7G2h7kVhrrmnqy+wEgIHfDwEfOlelRjaWCiBCFlF7dxGUntJusCh9FxlLhni0Ag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.4.1",
|
||||||
|
"prosemirror-commands": "^1.7.1",
|
||||||
|
"prosemirror-dropcursor": "^1.8.2",
|
||||||
|
"prosemirror-gapcursor": "^1.4.1",
|
||||||
|
"prosemirror-history": "^1.5.0",
|
||||||
|
"prosemirror-inputrules": "^1.5.1",
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.11",
|
||||||
|
"prosemirror-schema-list": "^1.5.1",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-tables": "^1.8.5",
|
||||||
|
"prosemirror-transform": "^1.12.0",
|
||||||
|
"prosemirror-view": "^1.42.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/react": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-QiwQqvaLFLm5EMFu5tg7nAgXJxCUiUTLD8EsK+TqVV5P4bqOoMOCM39khbhXTJyahCuYpiFWx5YOSDtC/JiPtg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"fast-equals": "^5.3.3",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.31.3",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.31.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-WKof9RewdmGHvWJ1wn0/HVNG2mV+HOgVRyJkKekuM9fgr6BZAAH/xZsWE1eon+94JnQ+KtK2ThydXQM/qc6b2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/extension-blockquote": "3.31.3",
|
||||||
|
"@tiptap/extension-bold": "3.31.3",
|
||||||
|
"@tiptap/extension-bullet-list": "3.31.3",
|
||||||
|
"@tiptap/extension-code": "3.31.3",
|
||||||
|
"@tiptap/extension-code-block": "3.31.3",
|
||||||
|
"@tiptap/extension-document": "3.31.3",
|
||||||
|
"@tiptap/extension-dropcursor": "3.31.3",
|
||||||
|
"@tiptap/extension-gapcursor": "3.31.3",
|
||||||
|
"@tiptap/extension-hard-break": "3.31.3",
|
||||||
|
"@tiptap/extension-heading": "3.31.3",
|
||||||
|
"@tiptap/extension-horizontal-rule": "3.31.3",
|
||||||
|
"@tiptap/extension-italic": "3.31.3",
|
||||||
|
"@tiptap/extension-link": "3.31.3",
|
||||||
|
"@tiptap/extension-list": "3.31.3",
|
||||||
|
"@tiptap/extension-list-item": "3.31.3",
|
||||||
|
"@tiptap/extension-list-keymap": "3.31.3",
|
||||||
|
"@tiptap/extension-ordered-list": "3.31.3",
|
||||||
|
"@tiptap/extension-paragraph": "3.31.3",
|
||||||
|
"@tiptap/extension-strike": "3.31.3",
|
||||||
|
"@tiptap/extension-text": "3.31.3",
|
||||||
|
"@tiptap/extension-underline": "3.31.3",
|
||||||
|
"@tiptap/extensions": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/static-renderer": {
|
||||||
|
"version": "3.31.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/static-renderer/-/static-renderer-3.31.3.tgz",
|
||||||
|
"integrity": "sha512-TzbN/aSTQBhGZL94aaEnfbwZw/gIiQd8/q3TucTYuzdbsrsZNfBMg7CCA9W9dOg4vlKxCTLRVas21ivsrUm7og==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1506,14 +1999,12 @@
|
|||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.31",
|
"version": "18.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
||||||
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@@ -1524,7 +2015,6 @@
|
|||||||
"version": "18.3.7",
|
"version": "18.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
@@ -1537,6 +2027,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.62.0",
|
"version": "5.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
||||||
@@ -2578,6 +3074,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "5.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.2.tgz",
|
||||||
|
"integrity": "sha512-Ywe6jodPTWOTL9/k0bV7gdfP8twKL5Y8I8CZ933fAY5gBekICZSUQTbyH6ut2NZCNyB05mSUwAuEqdEIaOOlDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
@@ -3168,6 +3673,12 @@
|
|||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||||||
|
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -3393,6 +3904,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
@@ -3721,6 +4238,145 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-ViYrjMSg3YFiXwIhKaluu+/mi3Yrxt6AR8ri14ulTaGcZtXO1CThl7A2gv79qx5fQnOw8woKwyBU2u+9PVCm3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz",
|
||||||
|
"integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-inputrules": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz",
|
||||||
|
"integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.1.tgz",
|
||||||
|
"integrity": "sha512-t4F5615FycnCqsX7ShTUs8+jfnwcf46kuFRvSl/3qFx2QTZ4DjgowesLHQXcFP7G//TjesqZG3WtgL7vdyVJyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.42.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.3.tgz",
|
||||||
|
"integrity": "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.25.8",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -3952,6 +4608,12 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -4393,6 +5055,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -4460,6 +5131,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
|
"@tiptap/extension-image": "^3.31.3",
|
||||||
|
"@tiptap/extension-link": "^3.31.3",
|
||||||
|
"@tiptap/pm": "^3.31.3",
|
||||||
|
"@tiptap/react": "^3.31.3",
|
||||||
|
"@tiptap/starter-kit": "^3.31.3",
|
||||||
|
"@tiptap/static-renderer": "^3.31.3",
|
||||||
"framer-motion": "^11.5.4",
|
"framer-motion": "^11.5.4",
|
||||||
"lucide-react": "0.522.0",
|
"lucide-react": "0.522.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { PlusIcon, Trash2Icon, XIcon } from 'lucide-react'
|
import { PlusIcon, Trash2Icon, XIcon } from 'lucide-react'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
@@ -7,6 +7,7 @@ import { useAdmin, useFieldSaver } from './AdminContext'
|
|||||||
import { adminApi } from './adminClient'
|
import { adminApi } from './adminClient'
|
||||||
import type { EntityConfig, FieldConfig } from './entities'
|
import type { EntityConfig, FieldConfig } from './entities'
|
||||||
import { ImageField } from '../components/admin/ImageField'
|
import { ImageField } from '../components/admin/ImageField'
|
||||||
|
import { ArticleEditor } from '../components/admin/ArticleEditor'
|
||||||
|
|
||||||
type EntityDrawerProps = {
|
type EntityDrawerProps = {
|
||||||
config: EntityConfig
|
config: EntityConfig
|
||||||
@@ -22,6 +23,12 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
|
|||||||
const { runSave } = useAdmin()
|
const { runSave } = useAdmin()
|
||||||
const { reload } = useSiteData()
|
const { reload } = useSiteData()
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const flushRef = useRef<() => void>(() => {})
|
||||||
|
|
||||||
|
const closeWithFlush = () => {
|
||||||
|
flushRef.current()
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
const saveField = (name: string, value: unknown) =>
|
const saveField = (name: string, value: unknown) =>
|
||||||
save(config.resource, item.id, { [name]: value }, (d) => {
|
save(config.resource, item.id, { [name]: value }, (d) => {
|
||||||
@@ -69,7 +76,7 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
onClick={onClose}
|
onClick={closeWithFlush}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<motion.aside
|
<motion.aside
|
||||||
@@ -77,7 +84,9 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
|
|||||||
dir="rtl"
|
dir="rtl"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label={`ویرایش ${config.title}`}
|
aria-label={`ویرایش ${config.title}`}
|
||||||
className="fixed inset-y-0 left-0 z-[96] flex w-full max-w-md flex-col border-e border-ivory/10 bg-ink shadow-2xl"
|
className={`fixed inset-y-0 left-0 z-[96] flex w-full flex-col border-e border-ivory/10 bg-ink shadow-2xl ${
|
||||||
|
config.resource === 'articles' ? 'sm:max-w-3xl' : 'sm:max-w-md'
|
||||||
|
}`}
|
||||||
initial={{ x: '-100%' }}
|
initial={{ x: '-100%' }}
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
exit={{ x: '-100%' }}
|
exit={{ x: '-100%' }}
|
||||||
@@ -92,7 +101,7 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={closeWithFlush}
|
||||||
aria-label="بستن"
|
aria-label="بستن"
|
||||||
className="flex h-9 w-9 items-center justify-center border border-ivory/20 text-ivory/70 transition-colors hover:border-copper hover:text-copper"
|
className="flex h-9 w-9 items-center justify-center border border-ivory/20 text-ivory/70 transition-colors hover:border-copper hover:text-copper"
|
||||||
>
|
>
|
||||||
@@ -108,6 +117,13 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
|
|||||||
field={field}
|
field={field}
|
||||||
value={item[field.name]}
|
value={item[field.name]}
|
||||||
onSave={(value) => saveField(field.name, value)}
|
onSave={(value) => saveField(field.name, value)}
|
||||||
|
registerFlush={
|
||||||
|
field.type === 'wysiwyg'
|
||||||
|
? (fn) => {
|
||||||
|
flushRef.current = fn
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -135,10 +151,12 @@ function FieldControl({
|
|||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
onSave,
|
onSave,
|
||||||
|
registerFlush,
|
||||||
}: {
|
}: {
|
||||||
field: FieldConfig
|
field: FieldConfig
|
||||||
value: unknown
|
value: unknown
|
||||||
onSave: (value: unknown) => Promise<boolean>
|
onSave: (value: unknown) => Promise<boolean>
|
||||||
|
registerFlush?: (fn: () => void) => void
|
||||||
}) {
|
}) {
|
||||||
const [text, setText] = useState<string>(String(value ?? ''))
|
const [text, setText] = useState<string>(String(value ?? ''))
|
||||||
|
|
||||||
@@ -152,6 +170,10 @@ function FieldControl({
|
|||||||
return <ImageField label={field.label} value={String(value ?? '')} onSave={(url) => onSave(url)} />
|
return <ImageField label={field.label} value={String(value ?? '')} onSave={(url) => onSave(url)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'wysiwyg') {
|
||||||
|
return <WysiwygField field={field} value={value} onSave={onSave} registerFlush={registerFlush} />
|
||||||
|
}
|
||||||
|
|
||||||
if (field.type === 'list') {
|
if (field.type === 'list') {
|
||||||
return <ListField field={field} value={value} onSave={onSave} />
|
return <ListField field={field} value={value} onSave={onSave} />
|
||||||
}
|
}
|
||||||
@@ -248,3 +270,70 @@ function ListField({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** فیلد WYSIWYG مقاله — ذخیره خودکار با تاخیر کوتاه و flush هنگام بستن کشو */
|
||||||
|
function WysiwygField({
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
onSave,
|
||||||
|
registerFlush,
|
||||||
|
}: {
|
||||||
|
field: FieldConfig
|
||||||
|
value: unknown
|
||||||
|
onSave: (value: unknown) => Promise<boolean>
|
||||||
|
registerFlush?: (fn: () => void) => void
|
||||||
|
}) {
|
||||||
|
const [doc, setDoc] = useState<Record<string, unknown>>(() =>
|
||||||
|
value && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: { type: 'doc', content: [{ type: 'paragraph' }] },
|
||||||
|
)
|
||||||
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
const pending = useRef<Record<string, unknown> | null>(null)
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (timer.current) {
|
||||||
|
clearTimeout(timer.current)
|
||||||
|
timer.current = null
|
||||||
|
}
|
||||||
|
if (pending.current) {
|
||||||
|
const next = pending.current
|
||||||
|
pending.current = null
|
||||||
|
void onSave(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ثبت آخرین flush در کشو + ذخیره تغییرات در انتظار هنگام unmount
|
||||||
|
useEffect(() => {
|
||||||
|
registerFlush?.(() => flush())
|
||||||
|
return () => {
|
||||||
|
if (timer.current) clearTimeout(timer.current)
|
||||||
|
if (pending.current) {
|
||||||
|
const next = pending.current
|
||||||
|
pending.current = null
|
||||||
|
void onSave(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = (json: Record<string, unknown>) => {
|
||||||
|
setDoc(json)
|
||||||
|
pending.current = json
|
||||||
|
if (timer.current) clearTimeout(timer.current)
|
||||||
|
timer.current = setTimeout(flush, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold text-ivory/80">{field.label}</p>
|
||||||
|
<p className="mt-1 text-[10px] font-light text-ivory/40">
|
||||||
|
ذخیره خودکار — تصویر را داخل متن بچسبانید یا بکشید
|
||||||
|
</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<ArticleEditor doc={doc} onChange={handleChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SiteData } from '../api'
|
import type { SiteData } from '../api'
|
||||||
|
|
||||||
export type FieldType = 'text' | 'textarea' | 'number' | 'image' | 'list'
|
export type FieldType = 'text' | 'textarea' | 'number' | 'image' | 'list' | 'wysiwyg'
|
||||||
|
|
||||||
export type FieldConfig = {
|
export type FieldConfig = {
|
||||||
name: string
|
name: string
|
||||||
@@ -57,9 +57,9 @@ export const ENTITIES: Record<string, EntityConfig> = {
|
|||||||
fields: [
|
fields: [
|
||||||
F('title', 'عنوان'), F('slug', 'نشانی (slug)'), F('date', 'تاریخ'), F('category', 'دسته'),
|
F('title', 'عنوان'), F('slug', 'نشانی (slug)'), F('date', 'تاریخ'), F('category', 'دسته'),
|
||||||
F('image', 'تصویر شاخص', 'image'), F('read', 'زمان مطالعه'), F('excerpt', 'چکیده', 'textarea'),
|
F('image', 'تصویر شاخص', 'image'), F('read', 'زمان مطالعه'), F('excerpt', 'چکیده', 'textarea'),
|
||||||
F('body', 'متن مقاله (هر پاراگراف یک خط)', 'list'),
|
F('body', 'متن مقاله', 'wysiwyg'),
|
||||||
],
|
],
|
||||||
defaults: { title: 'مقاله جدید', slug: `article-${Date.now()}`, date: '', category: '', image: '', read: '', excerpt: '', body: [] },
|
defaults: { title: 'مقاله جدید', slug: `article-${Date.now()}`, date: '', category: '', image: '', read: '', excerpt: '', body: { type: 'doc', content: [{ type: 'paragraph' }] } },
|
||||||
},
|
},
|
||||||
testimonials: {
|
testimonials: {
|
||||||
resource: 'testimonials', dataKey: 'testimonials', title: 'نظرات مشتریان', kind: 'collection',
|
resource: 'testimonials', dataKey: 'testimonials', title: 'نظرات مشتریان', kind: 'collection',
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { EditorContent, useEditor } from '@tiptap/react'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import Image from '@tiptap/extension-image'
|
||||||
|
import Link from '@tiptap/extension-link'
|
||||||
|
import {
|
||||||
|
BoldIcon,
|
||||||
|
ItalicIcon,
|
||||||
|
Heading2Icon,
|
||||||
|
Heading3Icon,
|
||||||
|
ImagePlusIcon,
|
||||||
|
Link2Icon,
|
||||||
|
ListIcon,
|
||||||
|
ListOrderedIcon,
|
||||||
|
QuoteIcon,
|
||||||
|
Redo2Icon,
|
||||||
|
StrikethroughIcon,
|
||||||
|
UnderlineIcon,
|
||||||
|
Undo2Icon,
|
||||||
|
Loader2Icon,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { useAdmin } from '../../admin/AdminContext'
|
||||||
|
import { adminApi } from '../../admin/adminClient'
|
||||||
|
|
||||||
|
/** اکستنشنهای مشترک ویرایشگر و رندر مقاله — هر دو باید یکسان باشند */
|
||||||
|
export const ARTICLE_EXTENSIONS = [
|
||||||
|
StarterKit.configure({
|
||||||
|
link: false,
|
||||||
|
heading: { levels: [2, 3] },
|
||||||
|
}),
|
||||||
|
Image.configure({ allowBase64: false }),
|
||||||
|
Link.configure({
|
||||||
|
openOnClick: false,
|
||||||
|
HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
|
||||||
|
isAllowedUri: (uri: string) => /^(https?:\/\/|\/|#|mailto:|tel:)/i.test(uri),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
type ArticleEditorProps = {
|
||||||
|
doc: Record<string, unknown>
|
||||||
|
onChange: (json: Record<string, unknown>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolButton({
|
||||||
|
onClick,
|
||||||
|
active = false,
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
onClick: () => void
|
||||||
|
active?: boolean
|
||||||
|
label: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`flex h-8 w-8 items-center justify-center border transition-colors duration-150 ${
|
||||||
|
active
|
||||||
|
? 'border-copper/50 bg-copper/15 text-copper'
|
||||||
|
: 'border-transparent text-ivory/60 hover:border-ivory/15 hover:text-copper'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Divider = () => <span className="mx-1 h-5 w-px bg-ivory/10" aria-hidden="true" />
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ویرایشگر WYSIWYG مقاله (TipTap) — راستبهچپ، با آپلود مستقیم تصویر به MinIO
|
||||||
|
* (انتخاب فایل، چسباندن یا کشیدن تصویر داخل متن).
|
||||||
|
*/
|
||||||
|
export function ArticleEditor({ doc, onChange }: ArticleEditorProps) {
|
||||||
|
const { runSave } = useAdmin()
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: ARTICLE_EXTENSIONS,
|
||||||
|
content: doc && Object.keys(doc).length ? doc : undefined,
|
||||||
|
immediatelyRender: false,
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
dir: 'rtl',
|
||||||
|
class: 'article-body min-h-[300px] px-4 py-4 focus:outline-none',
|
||||||
|
'data-wysiwyg': 'true',
|
||||||
|
},
|
||||||
|
handlePaste: (_event_ignored, event) => {
|
||||||
|
const file = event.clipboardData?.files?.[0]
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
event.preventDefault()
|
||||||
|
void uploadAndInsert(file)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
handleDrop: (_view, event, _slice, moved) => {
|
||||||
|
if (moved) return false
|
||||||
|
const file = event.dataTransfer?.files?.[0]
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
event.preventDefault()
|
||||||
|
void uploadAndInsert(file)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onUpdate: ({ editor: instance }) => onChange(instance.getJSON()),
|
||||||
|
})
|
||||||
|
|
||||||
|
async function uploadAndInsert(file: File) {
|
||||||
|
if (!editor) return
|
||||||
|
setUploading(true)
|
||||||
|
await runSave(async () => {
|
||||||
|
const { url } = await adminApi.upload(file)
|
||||||
|
editor.chain().focus().setImage({ src: url, alt: file.name.replace(/\.[^.]+$/, '') }).run()
|
||||||
|
})
|
||||||
|
setUploading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLink = () => {
|
||||||
|
if (!editor) return
|
||||||
|
if (editor.isActive('link')) {
|
||||||
|
editor.chain().focus().unsetLink().run()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void Swal.fire({
|
||||||
|
title: 'نشانی لینک',
|
||||||
|
input: 'text',
|
||||||
|
inputValue: 'https://',
|
||||||
|
background: '#1e1a16',
|
||||||
|
color: '#F5F3EF',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'ثبت',
|
||||||
|
cancelButtonText: 'انصراف',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
popup: 'font-pinar border border-ivory/10',
|
||||||
|
title: 'font-pinar font-bold text-ivory',
|
||||||
|
input: 'font-pinar',
|
||||||
|
confirmButton: 'bg-copper text-char font-bold px-8 py-2.5 mx-2',
|
||||||
|
cancelButton: 'border border-ivory/30 text-ivory font-light px-8 py-2.5 mx-2',
|
||||||
|
},
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed || !result.value) return
|
||||||
|
editor.chain().focus().extendMarkRange('link').setLink({ href: result.value }).run()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editor) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-ivory/15 bg-char">
|
||||||
|
<div className="flex flex-wrap items-center gap-0.5 border-b border-ivory/10 px-2 py-1.5">
|
||||||
|
<ToolButton
|
||||||
|
label="بولد"
|
||||||
|
active={editor.isActive('bold')}
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
>
|
||||||
|
<BoldIcon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
label="ایتالیک"
|
||||||
|
active={editor.isActive('italic')}
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
>
|
||||||
|
<ItalicIcon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
label="زیرخط"
|
||||||
|
active={editor.isActive('underline')}
|
||||||
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
>
|
||||||
|
<UnderlineIcon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
label="خطخورده"
|
||||||
|
active={editor.isActive('strike')}
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
>
|
||||||
|
<StrikethroughIcon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ToolButton
|
||||||
|
label="تیتر میانی"
|
||||||
|
active={editor.isActive('heading', { level: 2 })}
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
>
|
||||||
|
<Heading2Icon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
label="تیتر کوچک"
|
||||||
|
active={editor.isActive('heading', { level: 3 })}
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||||
|
>
|
||||||
|
<Heading3Icon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ToolButton
|
||||||
|
label="لیست نقطهای"
|
||||||
|
active={editor.isActive('bulletList')}
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
>
|
||||||
|
<ListIcon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
label="لیست شمارهای"
|
||||||
|
active={editor.isActive('orderedList')}
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
>
|
||||||
|
<ListOrderedIcon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
label="نقلقول"
|
||||||
|
active={editor.isActive('blockquote')}
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
>
|
||||||
|
<QuoteIcon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ToolButton label="لینک" active={editor.isActive('link')} onClick={setLink}>
|
||||||
|
<Link2Icon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton label="درج تصویر" onClick={() => fileRef.current?.click()}>
|
||||||
|
{uploading ? <Loader2Icon className="h-4 w-4 animate-spin" /> : <ImagePlusIcon className="h-4 w-4" />}
|
||||||
|
</ToolButton>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ToolButton label="واگرد" onClick={() => editor.chain().focus().undo().run()}>
|
||||||
|
<Undo2Icon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton label="ازنو" onClick={() => editor.chain().focus().redo().run()}>
|
||||||
|
<Redo2Icon className="h-4 w-4" />
|
||||||
|
</ToolButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileRef}
|
||||||
|
type="file"
|
||||||
|
data-editor-file-input
|
||||||
|
accept="image/jpeg,image/png,image/webp,image/gif,image/avif"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) void uploadAndInsert(file)
|
||||||
|
e.target.value = ''
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,7 +26,12 @@ export type Project = {
|
|||||||
body: string[];
|
body: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** سند TipTap (ProseMirror) — بدنه ساختاریافته مقاله */
|
||||||
|
export type ArticleBodyDoc = { type: 'doc'; content: unknown[] };
|
||||||
|
|
||||||
export type Article = {
|
export type Article = {
|
||||||
id: number;
|
id: number;
|
||||||
slug: string; title: string; date: string; category: string; image: string; read: string; excerpt: string; body: string[];
|
slug: string; title: string; date: string; category: string; image: string; read: string; excerpt: string;
|
||||||
|
/** سند TipTap — ردیفهای قدیمی ممکن است هنوز لیست پاراگراف باشند */
|
||||||
|
body: ArticleBodyDoc | string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,3 +40,102 @@ body {
|
|||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- بدنه مقاله — رندر سند TipTap و ویرایشگر WYSIWYG ---------- */
|
||||||
|
.article-body {
|
||||||
|
line-height: 2.4;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.article-body p {
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
color: rgba(245, 243, 239, 0.7);
|
||||||
|
}
|
||||||
|
.article-body > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.article-body h2,
|
||||||
|
.article-body h3 {
|
||||||
|
color: #f5f3ef;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
line-height: 1.9;
|
||||||
|
}
|
||||||
|
.article-body h2 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
padding-right: 0.85rem;
|
||||||
|
border-right: 3px solid #d5874c;
|
||||||
|
}
|
||||||
|
.article-body h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #e8c9a8;
|
||||||
|
}
|
||||||
|
.article-body ul,
|
||||||
|
.article-body ol {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
.article-body ul {
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
.article-body ol {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
.article-body li {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
color: rgba(245, 243, 239, 0.7);
|
||||||
|
}
|
||||||
|
.article-body li p {
|
||||||
|
margin-top: 0;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.article-body blockquote {
|
||||||
|
margin-top: 2rem;
|
||||||
|
border-right: 3px solid #d5874c;
|
||||||
|
background: rgba(30, 26, 22, 0.5);
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
font-style: normal;
|
||||||
|
color: rgba(245, 243, 239, 0.85);
|
||||||
|
}
|
||||||
|
.article-body blockquote p {
|
||||||
|
margin-top: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.article-body a {
|
||||||
|
color: #d5874c;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 4px;
|
||||||
|
}
|
||||||
|
.article-body img {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 2rem;
|
||||||
|
border: 1px solid rgba(245, 243, 239, 0.1);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.article-body strong {
|
||||||
|
color: #f5f3ef;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.article-body hr {
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(245, 243, 239, 0.12);
|
||||||
|
}
|
||||||
|
.article-body code {
|
||||||
|
background: rgba(30, 26, 22, 0.7);
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.article-body pre {
|
||||||
|
background: rgba(30, 26, 22, 0.7);
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.article-body pre code {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { ArrowLeftIcon, ChevronLeftIcon, ClockIcon } from 'lucide-react';
|
import { ArrowLeftIcon, ChevronLeftIcon, ClockIcon } from 'lucide-react';
|
||||||
|
import { renderToHTMLString } from '@tiptap/static-renderer';
|
||||||
import { Reveal } from '../components/Reveal';
|
import { Reveal } from '../components/Reveal';
|
||||||
import { ButtonLink } from '../components/Button';
|
import { ButtonLink } from '../components/Button';
|
||||||
import { ArticleCard } from '../components/ArticleCard';
|
import { ArticleCard } from '../components/ArticleCard';
|
||||||
import { Newsletter } from '../components/home/Newsletter';
|
import { Newsletter } from '../components/home/Newsletter';
|
||||||
import { useSite } from '../api';
|
import { useSite } from '../api';
|
||||||
|
import { ARTICLE_EXTENSIONS } from '../components/admin/ArticleEditor';
|
||||||
|
import type { Article } from '../data/site';
|
||||||
|
|
||||||
|
/** بدنه مقاله را به HTML تبدیل میکند — سند TipTap جدید یا پاراگرافهای قدیمی */
|
||||||
|
function renderBody(article: Article): { html: string | null; legacy: string[] } {
|
||||||
|
const body = article.body as unknown;
|
||||||
|
if (Array.isArray(body)) return { html: null, legacy: body };
|
||||||
|
if (body && typeof body === 'object' && (body as { type?: string }).type === 'doc') {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
html: renderToHTMLString({ extensions: ARTICLE_EXTENSIONS, content: body as never }),
|
||||||
|
legacy: [],
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { html: null, legacy: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { html: null, legacy: [] };
|
||||||
|
}
|
||||||
|
|
||||||
export function ArticleDetail() {
|
export function ArticleDetail() {
|
||||||
const { slug } = useParams<{slug: string;}>();
|
const { slug } = useParams<{slug: string;}>();
|
||||||
@@ -25,6 +45,7 @@ export function ArticleDetail() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { html, legacy } = renderBody(article);
|
||||||
const related = articles.filter((a) => a.slug !== article.slug).slice(0, 3);
|
const related = articles.filter((a) => a.slug !== article.slug).slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,10 +88,18 @@ export function ArticleDetail() {
|
|||||||
{article.excerpt}
|
{article.excerpt}
|
||||||
</p>
|
</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
{article.body.map((paragraph, i) =>
|
{html ? (
|
||||||
<Reveal key={paragraph} delay={0.03 * i}>
|
<Reveal delay={0.05}>
|
||||||
<p className="mt-8 text-base font-light leading-10 text-ivory/70">{paragraph}</p>
|
<div
|
||||||
|
className="article-body mt-4 text-base font-light text-ivory/70"
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }} />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
) : (
|
||||||
|
legacy.map((paragraph, i) =>
|
||||||
|
<Reveal key={i} delay={0.03 * i}>
|
||||||
|
<p className="mt-8 text-base font-light leading-10 text-ivory/70">{paragraph}</p>
|
||||||
|
</Reveal>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
Reference in New Issue
Block a user