diff --git a/package-lock.json b/package-lock.json
index cced288..27a5cbf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,16 @@
"@radix-ui/react-slot": "^1.2.4",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.1.18",
+ "@tiptap/extension-color": "^3.20.4",
+ "@tiptap/extension-font-family": "^3.20.4",
+ "@tiptap/extension-highlight": "^3.20.4",
+ "@tiptap/extension-link": "^3.20.4",
+ "@tiptap/extension-placeholder": "^3.20.4",
+ "@tiptap/extension-text-align": "^3.20.4",
+ "@tiptap/extension-text-style": "^3.20.4",
+ "@tiptap/extension-underline": "^3.20.4",
+ "@tiptap/react": "^3.20.4",
+ "@tiptap/starter-kit": "^3.20.4",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -899,6 +909,34 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@hookform/resolvers": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
@@ -1067,6 +1105,12 @@
}
}
},
+ "node_modules/@remirror/core-constants": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
+ "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
+ "license": "MIT"
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.2",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
@@ -1668,6 +1712,518 @@
"vite": "^5.2.0 || ^6 || ^7"
}
},
+ "node_modules/@tiptap/core": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz",
+ "integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-blockquote": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.4.tgz",
+ "integrity": "sha512-9sskyyhYj2oKat//lyZVXCp9YrPt4oJAZnGHYWXS0xlskjsLElrfKKlM4vpbhGss3VrhQRoEGqWLnIaJYPF1zw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bold": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.4.tgz",
+ "integrity": "sha512-Md7/mNAeJCY+VLJc8JRGI+8XkVPKiOGB1NgqQPdh3aYtxXQDChQOZoJEQl6TuudDxZ85bLZB67NjZlx3jo8/0g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bubble-menu": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.4.tgz",
+ "integrity": "sha512-EXywPlI8wjPcAb8ozymgVhjtMjFrnhtoyNTy8ZcObdpUi5CdO9j892Y7aPbKe5hLhlDpvJk7rMfir4FFKEmfng==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bullet-list": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.4.tgz",
+ "integrity": "sha512-1RTGrur1EKoxfnLZ3M6xeNj8GITAz74jH2DHGcjLsd2Xr7Q7BozGaIq6GkkvKguMwbI1zCOxTHFCpUETXAIQQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-code": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.4.tgz",
+ "integrity": "sha512-7j8Hi964bH1SZ9oLdZC1fkqWz27mliSDV7M8lmL/M14+Qw42D/VOAKS4Aw9OCFtHMlTsjLR6qsoVxL8Lpkt6NA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-code-block": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.4.tgz",
+ "integrity": "sha512-Zlw3FrXTy01+o1yISeX/LC+iJeHA+ym602bMXGmtA6lyl7QSOSO7WExweJ6xeJGhbCjldwT5al6fkRAs8iGJZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-color": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.20.4.tgz",
+ "integrity": "sha512-+OT9wWEJnqoWmzfqPYt0oWm8LZcH+D44Z3jA2TNzBj4tLGQ2YPxN2SyS12AlRi7MuguVT7utFy7qDXrfir8eUA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-text-style": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-document": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.4.tgz",
+ "integrity": "sha512-zF1CIFVLt8MfSpWWnPwtGyxPOsT0xYM2qJKcXf2yZcTG37wDKmUi6heG53vGigIavbQlLaAFvs+1mNdOu2x/0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-dropcursor": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.4.tgz",
+ "integrity": "sha512-TgMwvZ8myXYdmd6bUV7qkpZXv7ZUiSmX/8eo+iPEzYo2CnDLAGvDKgC50nfq/g87SDvfBgPuAiBfFvsMQQWaTw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-floating-menu": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.4.tgz",
+ "integrity": "sha512-AaPTFhoO8DBIElJyd/RTVJjkctvJuL+GHURX0npbtTxXq5HXbebVwf2ARNR7jMd/GThsmBaNJiGxZg4A2oeDqQ==",
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@floating-ui/dom": "^1.0.0",
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-font-family": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.20.4.tgz",
+ "integrity": "sha512-u5HjpNVBK7N9glR4Sz/HyVvTTAprEiion0oyyBWPBlgZvLrJta0zNvhfwG9ZUoubvqou3fBRbZwVosfonN2fAw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-text-style": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-gapcursor": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.4.tgz",
+ "integrity": "sha512-JJ6f1iQ1e0s4kISgq55U3UYGwWV/N9f0PYMtB6e3L+SBQjXnywaLK0g6vfN6IvTCC2vdIuqeSOX8VlSO97sJLw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-hard-break": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.4.tgz",
+ "integrity": "sha512-gJbq58d8zB1gzyqVEopowej5CpW4/Fpg6oGJvlZxaCukqd0gJRWGC89K+jE62YA1Td4sfcKrekKvN7jm2y/ZUg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-heading": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.4.tgz",
+ "integrity": "sha512-xsnkmTGggJc5P2iCwS1lv8KFG31xC/GNPJKoi/3UH67j/lKDhA3AdtshsLeyv2FKtTtYDb8oV0IqzHB1MM6a7w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-highlight": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.20.4.tgz",
+ "integrity": "sha512-CyTVPorVWwE4v89+k1nmaoAvjXLo7/fYWBsYlHW6b9Y1Un0iLANgKMFmmuapyfpaqpvg7V0Eg5ElG9U9+rogVA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-horizontal-rule": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.4.tgz",
+ "integrity": "sha512-y6joCi49haAA0bo3EGUY+dWUMHH1GPUc84hxrBY/0pMs+Bn+kQ1+DQJErZDTWGJrlHPWU/yekBZT72SNdp0DNA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-italic": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.4.tgz",
+ "integrity": "sha512-4ZqiWr7cmqPFux8tj1ZLiYytyWf343IvQemNX6AvVWvscrJcrfj3YX4Le2BA0RW3A3M6RpLQXXozuF8vxYFDeQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-link": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.4.tgz",
+ "integrity": "sha512-JNDSkWrVdb8NSvbQXwHWvK5tCMbTWwOHFOweknQZ1JPK4dei9FJVofYQaHyW4bJBdcCjds3NZSnXE8DM9iAWmg==",
+ "license": "MIT",
+ "dependencies": {
+ "linkifyjs": "^4.3.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz",
+ "integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list-item": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.4.tgz",
+ "integrity": "sha512-QoTc5RACXaZF+vIIBBxjGO7D0oWFUDgBKJCpvUZ0CoGGKosnfe4a9I5THFyLj4201cf0oUqgf1oZhTqETGxlVw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list-keymap": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.4.tgz",
+ "integrity": "sha512-RIqXM649+8IP7p/KVfaGlJiwjCylm1m6OPlaoM3K8O7oEOGRQzNeexexECCD2jsXRxew4E+vBNMD2orXqJmu8A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-ordered-list": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.4.tgz",
+ "integrity": "sha512-3budNL8BgBon3TcXZ4hjT0YpFvx1Ka3uSIECKDxHgES+OQcR+6cagxSb60gFEccf3Dr0PIwcVTY6g14lC1qKRQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-paragraph": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.4.tgz",
+ "integrity": "sha512-lm6fOScWuZAF/Sfp97igUwFd3L1QHIVLAWP5NVdh0DTLrEIt4rMBmsww+yOpMQRhvz2uTgMbMXynrimhzi/QVw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-placeholder": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.20.4.tgz",
+ "integrity": "sha512-GB0KWtqm83YHG8cnqBLijvUBm+xvLfQHDfFRRH2fb3EzH3eIsM9jKRC31ADT27RSV1zVpHMFGcP3/pWpdrN1Lw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-strike": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.4.tgz",
+ "integrity": "sha512-It1Px9uDGTsVqyyg6cy7DigLoenljpQwqdI0jssM7QclZrHnsrye9fZxBBiiuCzzV1305MxKgHvratkHwqmVNA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.4.tgz",
+ "integrity": "sha512-jchJcBZixDEO2J66Zx5dchsI2mA6IYsROqF8P1poxL4ienH7RVQRCTsBNnSfIeOtREKKWeOU/tEs5fcpvvGwIQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text-align": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.20.4.tgz",
+ "integrity": "sha512-6ZuRyClIyCimXu+S5LQ54DueEsYg5VOVOmubOVbG+WAjM9svn9Z8gv2sNDah2yEqXrX06B02zYcSyMiD7CHbfA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text-style": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.4.tgz",
+ "integrity": "sha512-PvW0Ja7ahWpo4bRuR8YCCVv4PH8lXjzhzlBAa4bMbsumOg+GbhX8Su7fwqd+IIPrHqfPXz9HTBMApSfzP6/08A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-underline": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.4.tgz",
+ "integrity": "sha512-0OjMc3FDujX16G+jhvqcY/mLot8SrNtDu8ggUwNLAfiI/QIvMVgk7giFD71DATC/4Nb8i/iwAEegTD8MxBIXCg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extensions": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz",
+ "integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/pm": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz",
+ "integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-changeset": "^2.3.0",
+ "prosemirror-collab": "^1.3.1",
+ "prosemirror-commands": "^1.6.2",
+ "prosemirror-dropcursor": "^1.8.1",
+ "prosemirror-gapcursor": "^1.3.2",
+ "prosemirror-history": "^1.4.1",
+ "prosemirror-inputrules": "^1.4.0",
+ "prosemirror-keymap": "^1.2.2",
+ "prosemirror-markdown": "^1.13.1",
+ "prosemirror-menu": "^1.2.4",
+ "prosemirror-model": "^1.24.1",
+ "prosemirror-schema-basic": "^1.2.3",
+ "prosemirror-schema-list": "^1.5.0",
+ "prosemirror-state": "^1.4.3",
+ "prosemirror-tables": "^1.6.4",
+ "prosemirror-trailing-node": "^3.0.0",
+ "prosemirror-transform": "^1.10.2",
+ "prosemirror-view": "^1.38.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
+ "node_modules/@tiptap/react": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.4.tgz",
+ "integrity": "sha512-1B8iWsHWwb5TeyVaUs8BRPzwWo4PsLQcl03urHaz0zTJ8DauopqvxzV3+lem1OkzRHn7wnrapDvwmIGoROCaQw==",
+ "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.20.4",
+ "@tiptap/extension-floating-menu": "^3.20.4"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4",
+ "@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.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.4.tgz",
+ "integrity": "sha512-WcyK6hsTl8eBsQhQ+d9Sq8fYZKOYdL+D45MyH3hz583elXqJlW3h3JPFYb0o87gddGxn8Mm57OA/gA1zEdeDMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/extension-blockquote": "^3.20.4",
+ "@tiptap/extension-bold": "^3.20.4",
+ "@tiptap/extension-bullet-list": "^3.20.4",
+ "@tiptap/extension-code": "^3.20.4",
+ "@tiptap/extension-code-block": "^3.20.4",
+ "@tiptap/extension-document": "^3.20.4",
+ "@tiptap/extension-dropcursor": "^3.20.4",
+ "@tiptap/extension-gapcursor": "^3.20.4",
+ "@tiptap/extension-hard-break": "^3.20.4",
+ "@tiptap/extension-heading": "^3.20.4",
+ "@tiptap/extension-horizontal-rule": "^3.20.4",
+ "@tiptap/extension-italic": "^3.20.4",
+ "@tiptap/extension-link": "^3.20.4",
+ "@tiptap/extension-list": "^3.20.4",
+ "@tiptap/extension-list-item": "^3.20.4",
+ "@tiptap/extension-list-keymap": "^3.20.4",
+ "@tiptap/extension-ordered-list": "^3.20.4",
+ "@tiptap/extension-paragraph": "^3.20.4",
+ "@tiptap/extension-strike": "^3.20.4",
+ "@tiptap/extension-text": "^3.20.4",
+ "@tiptap/extension-underline": "^3.20.4",
+ "@tiptap/extensions": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1789,6 +2345,28 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "24.10.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.10.tgz",
@@ -1803,7 +2381,6 @@
"version": "19.2.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1813,7 +2390,6 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -2175,7 +2751,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
"license": "Python-2.0"
},
"node_modules/asynckit": {
@@ -2398,6 +2973,12 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/crelt": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2417,7 +2998,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/d3-array": {
@@ -2624,6 +3204,18 @@
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2734,7 +3326,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -2940,6 +3531,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-equals": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3673,6 +4273,21 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/linkifyjs": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
+ "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
+ "license": "MIT"
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3724,6 +4339,23 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/markdown-it": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+ "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -3733,6 +4365,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "license": "MIT"
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -3824,6 +4462,12 @@
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -3945,6 +4589,201 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prosemirror-changeset": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
+ "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-collab": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
+ "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-commands": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
+ "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
+ "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
+ "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-markdown": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
+ "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/markdown-it": "^14.0.0",
+ "markdown-it": "^14.0.0",
+ "prosemirror-model": "^1.25.0"
+ }
+ },
+ "node_modules/prosemirror-menu": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz",
+ "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==",
+ "license": "MIT",
+ "dependencies": {
+ "crelt": "^1.0.0",
+ "prosemirror-commands": "^1.0.0",
+ "prosemirror-history": "^1.0.0",
+ "prosemirror-state": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-model": {
+ "version": "1.25.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
+ "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
+ "license": "MIT",
+ "dependencies": {
+ "orderedmap": "^2.0.0"
+ }
+ },
+ "node_modules/prosemirror-schema-basic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
+ "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.25.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-trailing-node": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
+ "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remirror/core-constants": "3.0.0",
+ "escape-string-regexp": "^4.0.0"
+ },
+ "peerDependencies": {
+ "prosemirror-model": "^1.22.1",
+ "prosemirror-state": "^1.4.2",
+ "prosemirror-view": "^1.33.8"
+ }
+ },
+ "node_modules/prosemirror-transform": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz",
+ "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.21.0"
+ }
+ },
+ "node_modules/prosemirror-view": {
+ "version": "1.41.7",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz",
+ "integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.20.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3961,6 +4800,15 @@
"node": ">=6"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@@ -4200,6 +5048,12 @@
"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/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -4415,6 +5269,12 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -4568,6 +5428,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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 34a0d1b..d694629 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,16 @@
"@radix-ui/react-slot": "^1.2.4",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.1.18",
+ "@tiptap/extension-color": "^3.20.4",
+ "@tiptap/extension-font-family": "^3.20.4",
+ "@tiptap/extension-highlight": "^3.20.4",
+ "@tiptap/extension-link": "^3.20.4",
+ "@tiptap/extension-placeholder": "^3.20.4",
+ "@tiptap/extension-text-align": "^3.20.4",
+ "@tiptap/extension-text-style": "^3.20.4",
+ "@tiptap/extension-underline": "^3.20.4",
+ "@tiptap/react": "^3.20.4",
+ "@tiptap/starter-kit": "^3.20.4",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index e27ea16..4f5eea4 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -86,6 +86,12 @@ const tenantAdminPlatformMenu: MenuItem[] = [
path: "/tenant/suppliers",
requiredPermission: { resource: "supplier" },
},
+ {
+ icon: FileText,
+ label: "Document Service",
+ path: "/tenant/documents",
+ requiredPermission: { resource: "document" },
+ },
{ icon: Package, label: "Modules", path: "/tenant/modules" },
];
@@ -231,7 +237,11 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{items.map((item) => {
const Icon = item.icon;
- const isActive = location.pathname === item.path;
+ const isTenantDashboardPath = item.path === "/tenant";
+ const isActive = isTenantDashboardPath
+ ? location.pathname === "/tenant"
+ : location.pathname === item.path ||
+ location.pathname.startsWith(`${item.path}/`);
return (
{
const location = useLocation();
const { roles } = useAppSelector((state) => state.auth);
@@ -45,14 +45,17 @@ export const PageHeader = ({
}
const isSuperAdmin = rolesArray.includes('super_admin');
- const isActiveTab = (path: string): boolean => {
- // Exact match for dashboard
- if (path === '/dashboard') {
- return location.pathname === '/dashboard';
- }
- // For other paths, check if current path starts with the tab path
- return location.pathname.startsWith(path);
- };
+ const isPathMatch = (tabPath: string): boolean =>
+ location.pathname === tabPath || location.pathname.startsWith(`${tabPath}/`);
+
+ const resolvedTabs = tabs ?? (isSuperAdmin ? defaultTabs : []);
+
+ // Pick the most specific matching tab (longest path), so parent tabs
+ // like /tenant/documents don't stay active on child routes.
+ const activeTabPath =
+ resolvedTabs
+ .filter((tab) => isPathMatch(tab.path))
+ .sort((a, b) => b.path.length - a.path.length)[0]?.path ?? null;
return (
@@ -69,10 +72,10 @@ export const PageHeader = ({
{/* Tabs Navigation - Only show for super_admin */}
- {isSuperAdmin && tabs.length > 0 && (
+ {resolvedTabs.length > 0 && (
- {tabs.map((tab) => {
- const isActive = isActiveTab(tab.path);
+ {resolvedTabs.map((tab) => {
+ const isActive = tab.path === activeTabPath;
return (
void;
+ placeholder?: string;
+ required?: boolean;
+ error?: string;
+ minHeightClassName?: string;
+}
+
+const FontSize = Extension.create({
+ name: "fontSize",
+ addGlobalAttributes() {
+ return [
+ {
+ types: ["textStyle"],
+ attributes: {
+ fontSize: {
+ default: null,
+ parseHTML: (element: HTMLElement) => element.style.fontSize || null,
+ renderHTML: (attributes: Record
) => {
+ if (!attributes.fontSize) return {};
+ return { style: `font-size: ${attributes.fontSize}` };
+ },
+ },
+ },
+ },
+ ];
+ },
+ addCommands() {
+ return {
+ setFontSize:
+ (fontSize: string) =>
+ ({ chain }: { chain: any }) => {
+ return chain().setMark("textStyle", { fontSize }).run();
+ },
+ unsetFontSize:
+ () =>
+ ({ chain }: { chain: any }) => {
+ return chain().setMark("textStyle", { fontSize: null }).run();
+ },
+ } as any;
+ },
+});
+
+const ToolButton = ({
+ isActive,
+ onClick,
+ title,
+ children,
+}: {
+ isActive?: boolean;
+ onClick: () => void;
+ title: string;
+ children: React.ReactNode;
+}): ReactElement => (
+
+);
+
+export const RichTextEditor = ({
+ label,
+ value,
+ onChange,
+ placeholder = "Start writing...",
+ required = false,
+ error,
+ minHeightClassName = "min-h-[300px]",
+}: RichTextEditorProps): ReactElement => {
+ const [showMoreTools, setShowMoreTools] = useState(false);
+ const editor = useEditor({
+ extensions: [
+ StarterKit.configure({
+ heading: {
+ levels: [1, 2, 3],
+ },
+ }),
+ Underline,
+ TextStyle,
+ FontFamily.configure({
+ types: ["textStyle"],
+ }),
+ FontSize,
+ Color,
+ Highlight.configure({
+ multicolor: true,
+ }),
+ Link.configure({
+ openOnClick: false,
+ autolink: true,
+ }),
+ TextAlign.configure({
+ types: ["heading", "paragraph"],
+ }),
+ Placeholder.configure({
+ placeholder,
+ }),
+ ],
+ content: value || "",
+ onUpdate: ({ editor: currentEditor }) => {
+ onChange(currentEditor.getHTML(), currentEditor.getText());
+ },
+ });
+
+ useEffect(() => {
+ if (!editor) return;
+ if (value !== editor.getHTML()) {
+ editor.commands.setContent(value || "", { emitUpdate: false });
+ }
+ }, [value, editor]);
+
+ const activeBlock = useMemo((): string => {
+ if (!editor) return "paragraph";
+ if (editor.isActive("heading", { level: 1 })) return "h1";
+ if (editor.isActive("heading", { level: 2 })) return "h2";
+ if (editor.isActive("heading", { level: 3 })) return "h3";
+ return "paragraph";
+ }, [editor, editor?.state]);
+
+ const activeFontFamily = editor?.getAttributes("textStyle").fontFamily || "Verdana";
+ const activeFontSize = editor?.getAttributes("textStyle").fontSize || "12pt";
+
+ const applyBlockStyle = (value: string): void => {
+ if (!editor) return;
+ const chain = editor.chain().focus();
+ if (value === "paragraph") {
+ chain.setParagraph().run();
+ return;
+ }
+ const level = Number(value.replace("h", ""));
+ if ([1, 2, 3].includes(level)) {
+ chain.setHeading({ level: level as 1 | 2 | 3 }).run();
+ }
+ };
+
+ return (
+
+
+
+
+
+ editor
+ ?.chain()
+ .focus()
+ .unsetAllMarks()
+ .clearNodes()
+ .setParagraph()
+ .unsetColor()
+ .run()
+ }
+ >
+
+
+
+
editor?.chain().focus().toggleBold().run()}
+ >
+
+
+
editor?.chain().focus().toggleItalic().run()}
+ >
+
+
+
editor?.chain().focus().toggleUnderline().run()}
+ >
+
+
+
+
editor?.chain().focus().setTextAlign("left").run()}
+ >
+
+
+
editor?.chain().focus().setTextAlign("center").run()}
+ >
+
+
+
editor?.chain().focus().setTextAlign("right").run()}
+ >
+
+
+
editor?.chain().focus().setTextAlign("justify").run()}
+ >
+
+
+
+
+
+
+
+
+ editor?.chain().focus().setColor(event.target.value).run()
+ }
+ />
+
+ editor
+ ?.chain()
+ .focus()
+ .setHighlight({ color: event.target.value })
+ .run()
+ }
+ />
+
setShowMoreTools((prev) => !prev)}
+ >
+ {showMoreTools ? (
+
+ ) : (
+
+ )}
+
+
+ {showMoreTools && (
+
+ editor?.chain().focus().toggleBulletList().run()}
+ >
+
+
+ editor?.chain().focus().toggleOrderedList().run()}
+ >
+
+
+
+ editor?.chain().focus().liftListItem("listItem").run()
+ }
+ >
+
+
+
+ editor?.chain().focus().sinkListItem("listItem").run()
+ }
+ >
+
+
+ editor?.chain().focus().toggleBlockquote().run()}
+ >
+
+
+ {
+ if (!editor) return;
+ const selection = editor.state.selection;
+ const url = window.prompt("Enter URL");
+ if (url) {
+ const normalizedUrl = /^https?:\/\//i.test(url)
+ ? url
+ : `https://${url}`;
+
+ const isSelectionEmpty = selection.empty;
+ const from = selection.from;
+ const to = selection.to;
+
+ if (isSelectionEmpty) {
+ editor
+ .chain()
+ .focus()
+ .setTextSelection({ from, to })
+ .insertContent(normalizedUrl)
+ .setTextSelection({
+ from,
+ to: from + normalizedUrl.length,
+ })
+ .setLink({ href: normalizedUrl })
+ .run();
+ return;
+ }
+
+ editor
+ .chain()
+ .focus()
+ .setTextSelection({ from, to })
+ .extendMarkRange("link")
+ .setLink({ href: normalizedUrl })
+ .run();
+ }
+ }}
+ >
+
+
+ editor?.chain().focus().unsetLink().run()}
+ >
+
+
+ editor?.chain().focus().toggleHighlight().run()}
+ >
+
+
+ editor?.chain().focus().toggleStrike().run()}
+ >
+
+
+
+ editor?.chain().focus().unsetAllMarks().clearNodes().run()
+ }
+ >
+
+
+ editor?.chain().focus().undo().run()}
+ >
+
+
+ editor?.chain().focus().redo().run()}
+ >
+
+
+
+ )}
+
+
+ {error &&
{error}
}
+
+ );
+};
+
diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts
index d059c57..5b6587a 100644
--- a/src/components/shared/index.ts
+++ b/src/components/shared/index.ts
@@ -33,4 +33,5 @@ export { SupplierModal } from './SupplierModal';
export { ViewSupplierModal } from './ViewSupplierModal';
export { SupplierContactsModal } from './SupplierContactsModal';
export { SupplierScorecardsModal } from './SupplierScorecardsModal';
-export { FormTextArea } from './FormTextArea';
\ No newline at end of file
+export { FormTextArea } from './FormTextArea';
+export { RichTextEditor } from './RichTextEditor';
\ No newline at end of file
diff --git a/src/pages/tenant/CreateDocument.tsx b/src/pages/tenant/CreateDocument.tsx
new file mode 100644
index 0000000..121dcca
--- /dev/null
+++ b/src/pages/tenant/CreateDocument.tsx
@@ -0,0 +1,234 @@
+import { useEffect, useState, type ReactElement } from "react";
+import { useNavigate } from "react-router-dom";
+import { Layout } from "@/components/layout/Layout";
+import { FormField, FormSelect, FormTextArea, PrimaryButton, RichTextEditor } from "@/components/shared";
+import { documentService } from "@/services/document-service";
+import type { DocumentCategory } from "@/types/document";
+import { showToast } from "@/utils/toast";
+import { ArrowLeft, FileText, Info } from "lucide-react";
+
+const CreateDocument = (): ReactElement => {
+ const navigate = useNavigate();
+ const [title, setTitle] = useState("");
+ const [description, setDescription] = useState("");
+ const [documentNumber, setDocumentNumber] = useState("");
+ const [documentType, setDocumentType] = useState("");
+ const [categoryId, setCategoryId] = useState("");
+ const [department, setDepartment] = useState("");
+ const [tags, setTags] = useState("");
+ const [content, setContent] = useState("");
+ const [contentHtml, setContentHtml] = useState("");
+ const [isSaving, setIsSaving] = useState(false);
+ const [types, setTypes] = useState>([]);
+ const [categories, setCategories] = useState([]);
+
+ useEffect(() => {
+ const loadLookups = async (): Promise => {
+ try {
+ const [typesRes, categoriesRes] = await Promise.all([
+ documentService.getTypes(),
+ documentService.getCategories(),
+ ]);
+ setTypes(typesRes.data || []);
+ setCategories(categoriesRes.data || []);
+ } catch {
+ showToast.error("Failed to load document metadata");
+ }
+ };
+ void loadLookups();
+ }, []);
+
+ const onSubmit = async (event: React.FormEvent): Promise => {
+ event.preventDefault();
+ if (!title.trim() || !documentType) {
+ showToast.error("Title and document type are required");
+ return;
+ }
+
+ try {
+ setIsSaving(true);
+ const response = await documentService.create({
+ title: title.trim(),
+ description: description.trim() || undefined,
+ document_number: documentNumber.trim() || undefined,
+ document_type: documentType,
+ category_id: categoryId || undefined,
+ department: department.trim() || undefined,
+ tags: tags
+ .split(",")
+ .map((tag) => tag.trim())
+ .filter(Boolean),
+ content: content.trim() || undefined,
+ content_html: contentHtml.trim() || undefined,
+ });
+ showToast.success("Document created successfully");
+ navigate(`/tenant/documents/${response.data.id}`);
+ } catch (err: any) {
+ showToast.error(
+ err?.response?.data?.error?.message || "Failed to create document",
+ );
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default CreateDocument;
+
diff --git a/src/pages/tenant/DocumentCategories.tsx b/src/pages/tenant/DocumentCategories.tsx
new file mode 100644
index 0000000..3b76dcd
--- /dev/null
+++ b/src/pages/tenant/DocumentCategories.tsx
@@ -0,0 +1,186 @@
+import { useEffect, useMemo, useState, type ReactElement } from "react";
+import { useNavigate } from "react-router-dom";
+import { Layout } from "@/components/layout/Layout";
+import { DataTable, FormField, PrimaryButton, type Column } from "@/components/shared";
+import { documentService } from "@/services/document-service";
+import type { DocumentCategory } from "@/types/document";
+import { showToast } from "@/utils/toast";
+
+const DocumentCategories = (): ReactElement => {
+ const navigate = useNavigate();
+ const [categories, setCategories] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [name, setName] = useState("");
+ const [code, setCode] = useState("");
+ const [description, setDescription] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const loadCategories = async (): Promise => {
+ try {
+ setIsLoading(true);
+ const response = await documentService.getCategories();
+ setCategories(response.data || []);
+ } catch (err: any) {
+ showToast.error(
+ err?.response?.data?.error?.message || "Failed to load categories",
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ void loadCategories();
+ }, []);
+
+ const columns: Column[] = useMemo(
+ () => [
+ { key: "name", label: "Name" },
+ { key: "code", label: "Code" },
+ {
+ key: "description",
+ label: "Description",
+ render: (category) => category.description || "-",
+ },
+ {
+ key: "review_frequency_months",
+ label: "Review (months)",
+ render: (category) =>
+ category.review_frequency_months?.toString() || "-",
+ },
+ {
+ key: "retention_years",
+ label: "Retention (years)",
+ render: (category) => category.retention_years?.toString() || "-",
+ },
+ {
+ key: "actions",
+ label: "Actions",
+ align: "right",
+ render: (category) => (
+
+ ),
+ },
+ ],
+ [],
+ );
+
+ const onCreateCategory = async (event: React.FormEvent): Promise => {
+ event.preventDefault();
+ if (!name.trim() || !code.trim()) {
+ showToast.error("Name and code are required");
+ return;
+ }
+
+ try {
+ setIsSubmitting(true);
+ await documentService.createCategory({
+ name: name.trim(),
+ code: code.trim().toUpperCase(),
+ description: description.trim() || undefined,
+ });
+ showToast.success("Category created");
+ setName("");
+ setCode("");
+ setDescription("");
+ await loadCategories();
+ } catch (err: any) {
+ showToast.error(
+ err?.response?.data?.error?.message || "Failed to create category",
+ );
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ category.id}
+ emptyMessage="No categories found"
+ isLoading={isLoading}
+ />
+
+
+
+ );
+};
+
+export default DocumentCategories;
+
diff --git a/src/pages/tenant/Documents.tsx b/src/pages/tenant/Documents.tsx
new file mode 100644
index 0000000..69083cc
--- /dev/null
+++ b/src/pages/tenant/Documents.tsx
@@ -0,0 +1,268 @@
+import { useEffect, useMemo, useState, type ReactElement } from "react";
+import { useNavigate } from "react-router-dom";
+import { Layout } from "@/components/layout/Layout";
+import {
+ DataTable,
+ FilterDropdown,
+ Pagination,
+ PrimaryButton,
+ type Column,
+} from "@/components/shared";
+import { documentService } from "@/services/document-service";
+import type { DocumentCategory, DocumentSummary } from "@/types/document";
+import { Plus } from "lucide-react";
+
+const formatDate = (value?: string | null): string => {
+ if (!value) return "-";
+ return new Date(value).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+};
+
+const toLabel = (value: string): string =>
+ value
+ .split("_")
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(" ");
+
+const Documents = (): ReactElement => {
+ const navigate = useNavigate();
+ const [documents, setDocuments] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [statuses, setStatuses] = useState>(
+ [],
+ );
+ const [types, setTypes] = useState>([]);
+ const [search, setSearch] = useState("");
+ const [statusFilter, setStatusFilter] = useState(null);
+ const [categoryFilter, setCategoryFilter] = useState(null);
+ const [typeFilter, setTypeFilter] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [limit, setLimit] = useState(10);
+ const [total, setTotal] = useState(0);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const offset = (currentPage - 1) * limit;
+ const totalPages = Math.max(1, Math.ceil(total / limit));
+
+ useEffect(() => {
+ const loadDropdownData = async (): Promise => {
+ try {
+ const [categoriesRes, statusesRes, typesRes] = await Promise.all([
+ documentService.getCategories(),
+ documentService.getStatuses(),
+ documentService.getTypes(),
+ ]);
+ setCategories(categoriesRes.data || []);
+ setStatuses(statusesRes.data || []);
+ setTypes(typesRes.data || []);
+ } catch {
+ // Keep page usable even if some filter metadata endpoints fail.
+ }
+ };
+
+ void loadDropdownData();
+ }, []);
+
+ useEffect(() => {
+ const loadDocuments = async (): Promise => {
+ try {
+ setIsLoading(true);
+ setError(null);
+ const response = await documentService.list({
+ status: statusFilter || undefined,
+ category_id: categoryFilter || undefined,
+ document_type: typeFilter || undefined,
+ search: search.trim() || undefined,
+ limit,
+ offset,
+ });
+ setDocuments(response.data || []);
+ setTotal(response.pagination?.total || 0);
+ } catch (err: any) {
+ setError(
+ err?.response?.data?.error?.message || "Failed to load documents",
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ void loadDocuments();
+ }, [statusFilter, categoryFilter, typeFilter, search, limit, offset]);
+
+ const columns: Column[] = useMemo(
+ () => [
+ {
+ key: "document_number",
+ label: "Document No",
+ render: (doc) => (
+
+ ),
+ },
+ {
+ key: "title",
+ label: "Title",
+ render: (doc) => {doc.title},
+ },
+ {
+ key: "document_type",
+ label: "Type",
+ render: (doc) => (
+ {doc.document_type || "-"}
+ ),
+ },
+ {
+ key: "category",
+ label: "Category",
+ render: (doc) => {doc.category || "-"},
+ },
+ {
+ key: "status",
+ label: "Status",
+ render: (doc) => (
+
+ {toLabel(doc.status)}
+
+ ),
+ },
+ {
+ key: "current_version",
+ label: "Version",
+ render: (doc) => (
+ {doc.current_version || "-"}
+ ),
+ },
+ {
+ key: "updated_at",
+ label: "Updated",
+ render: (doc) => (
+ {formatDate(doc.updated_at)}
+ ),
+ },
+ ],
+ [navigate],
+ );
+
+ return (
+
+
+
+
+
+ ({
+ value: status.code,
+ label: status.name,
+ }))}
+ value={statusFilter}
+ onChange={(value) => {
+ setStatusFilter(value as string | null);
+ setCurrentPage(1);
+ }}
+ placeholder="All"
+ />
+ ({
+ value: category.id,
+ label: category.name,
+ }))}
+ value={categoryFilter}
+ onChange={(value) => {
+ setCategoryFilter(value as string | null);
+ setCurrentPage(1);
+ }}
+ placeholder="All"
+ />
+ ({
+ value: type.code,
+ label: type.name,
+ }))}
+ value={typeFilter}
+ onChange={(value) => {
+ setTypeFilter(value as string | null);
+ setCurrentPage(1);
+ }}
+ placeholder="All"
+ />
+
+
+
+
navigate("/tenant/documents/create")}>
+
+ New Document
+
+
+
+
{
+ setSearch(e.target.value);
+ setCurrentPage(1);
+ }}
+ placeholder="Search by title, description or document number"
+ className="h-10 w-full max-w-xl px-3 border border-[rgba(0,0,0,0.08)] rounded-md text-sm"
+ />
+
+
+
doc.id}
+ emptyMessage="No documents found"
+ isLoading={isLoading}
+ error={error}
+ />
+
+ {total > 0 && (
+ {
+ setLimit(value);
+ setCurrentPage(1);
+ }}
+ />
+ )}
+
+
+ );
+};
+
+export default Documents;
+
diff --git a/src/pages/tenant/ViewDocument.tsx b/src/pages/tenant/ViewDocument.tsx
new file mode 100644
index 0000000..45a2e17
--- /dev/null
+++ b/src/pages/tenant/ViewDocument.tsx
@@ -0,0 +1,616 @@
+import { useEffect, useMemo, useState, type ReactElement } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { Layout } from "@/components/layout/Layout";
+import {
+ DataTable,
+ FormSelect,
+ Modal,
+ PrimaryButton,
+ RichTextEditor,
+ SecondaryButton,
+ type Column,
+} from "@/components/shared";
+import { documentService } from "@/services/document-service";
+import { workflowService } from "@/services/workflow-service";
+import type { DocumentDetail, DocumentVersion } from "@/types/document";
+import { showToast } from "@/utils/toast";
+import { ChevronDown, Plus } from "lucide-react";
+
+const formatDateTime = (value?: string | null): string => {
+ if (!value) return "-";
+ return new Date(value).toLocaleString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+};
+
+type DocumentAction =
+ | "submit"
+ | "approve"
+ | "reject"
+ | "effective"
+ | "obsolete"
+ | "checkout"
+ | "checkin";
+
+const ACTION_LABELS: Record = {
+ submit: "Submit For Review",
+ approve: "Approve",
+ reject: "Reject",
+ effective: "Make Effective",
+ obsolete: "Make Obsolete",
+ checkout: "Checkout",
+ checkin: "Checkin",
+};
+
+const ViewDocument = (): ReactElement => {
+ const navigate = useNavigate();
+ const { id } = useParams();
+ const [document, setDocument] = useState(null);
+ const [versions, setVersions] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [activeTab, setActiveTab] = useState<"overview" | "version-history">(
+ "overview",
+ );
+ const [actionMenuOpen, setActionMenuOpen] = useState(false);
+ const [activeAction, setActiveAction] = useState(null);
+ const [isActionLoading, setIsActionLoading] = useState(false);
+ const [workflowDefinitionId, setWorkflowDefinitionId] = useState("");
+ const [workflowOptions, setWorkflowOptions] = useState<
+ Array<{ value: string; label: string }>
+ >([]);
+ const [actionComment, setActionComment] = useState("");
+ const [effectiveDate, setEffectiveDate] = useState("");
+ const [signatureId, setSignatureId] = useState("");
+ const [showNewVersionForm, setShowNewVersionForm] = useState(false);
+ const [newVersionContent, setNewVersionContent] = useState("");
+ const [newVersionContentHtml, setNewVersionContentHtml] = useState("");
+ const [newVersionChangeReason, setNewVersionChangeReason] = useState("minor_edit");
+ const [newVersionChangeSummary, setNewVersionChangeSummary] = useState("");
+ const [isMajorVersion, setIsMajorVersion] = useState(false);
+ const [isVersionSaving, setIsVersionSaving] = useState(false);
+
+ useEffect(() => {
+ if (!id) return;
+
+ const loadDocument = async (): Promise => {
+ try {
+ setIsLoading(true);
+ setError(null);
+ const [documentRes, versionsRes] = await Promise.all([
+ documentService.getById(id),
+ documentService.getVersions(id),
+ ]);
+ setDocument(documentRes.data);
+ setVersions(versionsRes.data || []);
+ } catch (err: any) {
+ const message =
+ err?.response?.data?.error?.message || "Failed to load document details";
+ setError(message);
+ showToast.error(message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ void loadDocument();
+ }, [id]);
+
+ const refreshData = async (): Promise => {
+ if (!id) return;
+ const [documentRes, versionsRes] = await Promise.all([
+ documentService.getById(id),
+ documentService.getVersions(id),
+ ]);
+ setDocument(documentRes.data);
+ setVersions(versionsRes.data || []);
+ };
+
+ const resetActionModal = (): void => {
+ setActiveAction(null);
+ setWorkflowDefinitionId("");
+ setActionComment("");
+ setEffectiveDate("");
+ setSignatureId("");
+ setWorkflowOptions([]);
+ };
+
+ const openActionModal = async (action: DocumentAction): Promise => {
+ setActionMenuOpen(false);
+ if (action === "checkin") {
+ await handleAction(action);
+ return;
+ }
+ setActiveAction(action);
+ if (action === "submit") {
+ try {
+ const response = await workflowService.listDefinitions({
+ status: "active",
+ entity_type: "document",
+ limit: 100,
+ offset: 0,
+ });
+ setWorkflowOptions(
+ (response.data || []).map((definition) => ({
+ value: definition.id,
+ label: `${definition.name} (${definition.code})`,
+ })),
+ );
+ } catch {
+ setWorkflowOptions([]);
+ showToast.error("Failed to load active workflow definitions");
+ }
+ }
+ };
+
+ const handleAction = async (action: DocumentAction): Promise => {
+ if (!id) return;
+
+ if (action === "submit" && !workflowDefinitionId) {
+ showToast.error("workflow_definition_id is required");
+ return;
+ }
+ if (action === "reject" && !actionComment.trim()) {
+ showToast.error("Reason is required for reject");
+ return;
+ }
+ if (action === "effective" && !effectiveDate) {
+ showToast.error("Effective date is required");
+ return;
+ }
+ if (action === "effective" && !signatureId.trim()) {
+ showToast.error("signature_id is required");
+ return;
+ }
+ if (action === "obsolete" && !actionComment.trim()) {
+ showToast.error("Reason is required to obsolete");
+ return;
+ }
+ if (action === "checkout" && !actionComment.trim()) {
+ showToast.error("Reason is required for checkout");
+ return;
+ }
+
+ try {
+ setIsActionLoading(true);
+ if (action === "submit")
+ await documentService.submitForReview(id, workflowDefinitionId);
+ if (action === "approve") await documentService.approve(id, actionComment);
+ if (action === "reject") {
+ await documentService.reject(id, actionComment.trim());
+ }
+ if (action === "effective") {
+ await documentService.makeEffective(id, effectiveDate, signatureId.trim());
+ }
+ if (action === "obsolete") {
+ await documentService.makeObsolete(id, actionComment.trim());
+ }
+ if (action === "checkout")
+ await documentService.checkout(id, actionComment.trim());
+ if (action === "checkin") await documentService.checkin(id);
+
+ showToast.success("Document action completed");
+ await refreshData();
+ resetActionModal();
+ } catch (err: any) {
+ showToast.error(
+ err?.response?.data?.error?.message || "Failed to execute action",
+ );
+ } finally {
+ setIsActionLoading(false);
+ }
+ };
+
+ const handleCreateVersion = async (): Promise => {
+ if (!id) return;
+ if (!newVersionChangeReason) {
+ showToast.error("Change reason is required");
+ return;
+ }
+ if (!newVersionContent.trim()) {
+ showToast.error("Document content is required");
+ return;
+ }
+
+ try {
+ setIsVersionSaving(true);
+ await documentService.createVersion(id, {
+ content: newVersionContent.trim(),
+ content_html: newVersionContentHtml.trim() || undefined,
+ change_reason: newVersionChangeReason,
+ change_summary: newVersionChangeSummary.trim() || undefined,
+ is_major_version: isMajorVersion,
+ });
+ showToast.success("New version created successfully");
+ setShowNewVersionForm(false);
+ setNewVersionChangeSummary("");
+ setIsMajorVersion(false);
+ await refreshData();
+ } catch (err: any) {
+ showToast.error(
+ err?.response?.data?.error?.message || "Failed to create version",
+ );
+ } finally {
+ setIsVersionSaving(false);
+ }
+ };
+
+ const versionColumns: Column[] = [
+ { key: "version_number", label: "Version" },
+ { key: "status", label: "Status" },
+ {
+ key: "change_summary",
+ label: "Change Summary",
+ render: (version) => version.change_summary || "-",
+ },
+ {
+ key: "created_by",
+ label: "Author",
+ render: (version) => version.created_by || "-",
+ },
+ {
+ key: "created_at",
+ label: "Created At",
+ render: (version) => formatDateTime(version.created_at),
+ },
+ ];
+
+ const actionOptions: DocumentAction[] = useMemo(
+ () => [
+ "submit",
+ "approve",
+ "reject",
+ "effective",
+ "obsolete",
+ "checkout",
+ "checkin",
+ ],
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+ {document?.title || "Document Detail"}
+
+
+ {document?.document_number || "-"}
+
+
+
+
+ {actionMenuOpen && (
+
+ {actionOptions.map((action) => (
+
+ ))}
+
+ )}
+
+
+
+
+ {document?.status || "draft"}
+
+
+ {document?.document_type || "-"}
+
+
+ v{document?.current_version || "-"}
+
+
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
Loading document...
+ ) : error ? (
+
{error}
+ ) : document ? (
+ <>
+ {activeTab === "overview" && (
+
+
+
+
Document Number:
+
{document.document_number}
+
+
+
Title:
+
{document.title}
+
+
+
Category:
+
{document.category?.name || "-"}
+
+
+
Department:
+
{document.department || "-"}
+
+
+
Effective Date:
+
{formatDateTime(document.effective_date)}
+
+
+
Next Review Date:
+
{formatDateTime(document.next_review_date)}
+
+
+
Tags:
+
+ {document.tags && document.tags.length > 0
+ ? document.tags.join(", ")
+ : "-"}
+
+
+
+
+
Document Content
+
+ {document.content_html ? (
+
+ ) : (
+
{document.content || "-"}
+ )}
+
+
+
+ )}
+ {activeTab === "version-history" && (
+
+
+
+
+ {showNewVersionForm && (
+
+
Create New Version
+
{
+ setNewVersionContentHtml(html);
+ setNewVersionContent(text);
+ }}
+ />
+
+
+
+ setIsMajorVersion(event.target.checked)}
+ className="w-4 h-4"
+ />
+
+
+
+
+
+
+
+
setShowNewVersionForm(false)}>
+ Cancel
+
+
void handleCreateVersion()}
+ disabled={isVersionSaving}
+ >
+ {isVersionSaving ? "Creating..." : "Create Version"}
+
+
+
+ )}
+
+ version.id}
+ emptyMessage="No versions found"
+ isLoading={isLoading}
+ />
+
+
+ )}
+ >
+ ) : null}
+
+
+
+
+
+
+
+
+ Cancel
+
+ activeAction && void handleAction(activeAction)}
+ disabled={isActionLoading || !activeAction}
+ >
+ {isActionLoading ? "Submitting..." : "Submit"}
+
+ >
+ }
+ >
+
+ {activeAction === "submit" && (
+
+ )}
+ {activeAction === "approve" && (
+
+
+
+ )}
+ {(activeAction === "reject" ||
+ activeAction === "obsolete" ||
+ activeAction === "checkout") && (
+
+
+
+ )}
+ {activeAction === "effective" && (
+
+ )}
+
+
+
+ );
+};
+
+export default ViewDocument;
+
diff --git a/src/routes/tenant-admin-routes.tsx b/src/routes/tenant-admin-routes.tsx
index 04fff4d..a307ef5 100644
--- a/src/routes/tenant-admin-routes.tsx
+++ b/src/routes/tenant-admin-routes.tsx
@@ -14,6 +14,10 @@ const WorkflowDefination = lazy(
() => import("@/pages/tenant/WorkflowDefination"),
);
const Suppliers = lazy(() => import("@/pages/tenant/Suppliers"));
+const Documents = lazy(() => import("@/pages/tenant/Documents"));
+const CreateDocument = lazy(() => import("@/pages/tenant/CreateDocument"));
+const ViewDocument = lazy(() => import("@/pages/tenant/ViewDocument"));
+const DocumentCategories = lazy(() => import("@/pages/tenant/DocumentCategories"));
// Loading fallback component
const RouteLoader = (): ReactElement => (
@@ -80,4 +84,20 @@ export const tenantAdminRoutes: RouteConfig[] = [
path: "/tenant/suppliers",
element: ,
},
+ {
+ path: "/tenant/documents",
+ element: ,
+ },
+ {
+ path: "/tenant/documents/create",
+ element: ,
+ },
+ {
+ path: "/tenant/documents/:id",
+ element: ,
+ },
+ {
+ path: "/tenant/documents/categories",
+ element: ,
+ },
];
diff --git a/src/services/document-service.ts b/src/services/document-service.ts
new file mode 100644
index 0000000..53dd026
--- /dev/null
+++ b/src/services/document-service.ts
@@ -0,0 +1,254 @@
+import apiClient from "@/services/api-client";
+import type {
+ DocumentCategory,
+ DocumentDetail,
+ DocumentListResponse,
+ DocumentResponse,
+ DocumentVersion,
+} from "@/types/document";
+
+export interface ListDocumentsParams {
+ status?: string;
+ category_id?: string;
+ document_type?: string;
+ owner_id?: string;
+ search?: string;
+ limit?: number;
+ offset?: number;
+}
+
+export interface CreateDocumentPayload {
+ title: string;
+ description?: string;
+ document_number?: string;
+ category_id?: string;
+ document_type: string;
+ owner_id?: string;
+ department?: string;
+ tags?: string[];
+ custom_fields?: Record;
+ content?: string;
+ content_html?: string;
+}
+
+export interface CreateCategoryPayload {
+ name: string;
+ code: string;
+ description?: string;
+ parent_id?: string | null;
+ requires_training?: boolean;
+ retention_years?: number;
+ review_frequency_months?: number;
+}
+
+export interface CreateDocumentVersionPayload {
+ content?: string;
+ content_html?: string;
+ change_summary?: string;
+ change_reason?: string;
+ is_major_version?: boolean;
+}
+
+export const documentService = {
+ list: async (params: ListDocumentsParams): Promise => {
+ const queryParams = new URLSearchParams();
+ if (params.status) queryParams.append("status", params.status);
+ if (params.category_id) queryParams.append("category_id", params.category_id);
+ if (params.document_type)
+ queryParams.append("document_type", params.document_type);
+ if (params.owner_id) queryParams.append("owner_id", params.owner_id);
+ if (params.search) queryParams.append("search", params.search);
+ if (params.limit !== undefined)
+ queryParams.append("limit", params.limit.toString());
+ if (params.offset !== undefined)
+ queryParams.append("offset", params.offset.toString());
+
+ const response = await apiClient.get(
+ `/documents?${queryParams.toString()}`,
+ );
+ return response.data;
+ },
+
+ getById: async (id: string): Promise> => {
+ const response = await apiClient.get>(
+ `/documents/${id}`,
+ );
+ return response.data;
+ },
+
+ create: async (
+ payload: CreateDocumentPayload,
+ ): Promise> => {
+ const response = await apiClient.post>(
+ "/documents",
+ payload,
+ );
+ return response.data;
+ },
+
+ update: async (
+ id: string,
+ payload: Partial,
+ ): Promise> => {
+ const response = await apiClient.put>(
+ `/documents/${id}`,
+ payload,
+ );
+ return response.data;
+ },
+
+ getTypes: async (): Promise<
+ DocumentResponse>
+ > => {
+ const response = await apiClient.get<
+ DocumentResponse>
+ >("/documents/types");
+ return response.data;
+ },
+
+ getStatuses: async (): Promise<
+ DocumentResponse<
+ Array<{ code: string; name: string; description?: string }>
+ >
+ > => {
+ const response = await apiClient.get<
+ DocumentResponse<
+ Array<{ code: string; name: string; description?: string }>
+ >
+ >("/documents/statuses");
+ return response.data;
+ },
+
+ getCategories: async (): Promise> => {
+ const response = await apiClient.get>(
+ "/documents/categories",
+ );
+ return response.data;
+ },
+
+ createCategory: async (
+ payload: CreateCategoryPayload,
+ ): Promise> => {
+ const response = await apiClient.post>(
+ "/documents/categories",
+ payload,
+ );
+ return response.data;
+ },
+
+ updateCategory: async (
+ id: string,
+ payload: Partial,
+ ): Promise> => {
+ const response = await apiClient.put>(
+ `/documents/categories/${id}`,
+ payload,
+ );
+ return response.data;
+ },
+
+ deleteCategory: async (
+ id: string,
+ ): Promise<{ success: boolean; message: string }> => {
+ const response = await apiClient.delete<{ success: boolean; message: string }>(
+ `/documents/categories/${id}`,
+ );
+ return response.data;
+ },
+
+ getVersions: async (
+ id: string,
+ ): Promise> => {
+ const response = await apiClient.get>(
+ `/documents/${id}/versions`,
+ );
+ return response.data;
+ },
+
+ createVersion: async (
+ id: string,
+ payload: CreateDocumentVersionPayload,
+ ): Promise> => {
+ const response = await apiClient.post>(
+ `/documents/${id}/versions`,
+ payload,
+ );
+ return response.data;
+ },
+
+ submitForReview: async (
+ id: string,
+ workflow_definition_id: string,
+ ): Promise> => {
+ const response = await apiClient.post>(
+ `/documents/${id}/submit`,
+ { workflow_definition_id },
+ );
+ return response.data;
+ },
+
+ approve: async (
+ id: string,
+ comments?: string,
+ ): Promise> => {
+ const response = await apiClient.post>(
+ `/documents/${id}/approve`,
+ { comments: comments || "" },
+ );
+ return response.data;
+ },
+
+ reject: async (
+ id: string,
+ reason: string,
+ ): Promise> => {
+ const response = await apiClient.post>(
+ `/documents/${id}/reject`,
+ { reason },
+ );
+ return response.data;
+ },
+
+ makeEffective: async (
+ id: string,
+ effective_date: string,
+ signature_id?: string,
+ ): Promise> => {
+ const response = await apiClient.post>(
+ `/documents/${id}/make-effective`,
+ { effective_date, signature_id: signature_id || null },
+ );
+ return response.data;
+ },
+
+ makeObsolete: async (
+ id: string,
+ reason: string,
+ ): Promise> => {
+ const response = await apiClient.post>(
+ `/documents/${id}/obsolete`,
+ { reason },
+ );
+ return response.data;
+ },
+
+ checkout: async (
+ id: string,
+ reason?: string,
+ ): Promise> => {
+ const response = await apiClient.post<
+ DocumentResponse<{ checked_out?: boolean; expires_at?: string }>
+ >(`/documents/${id}/checkout`, { reason: reason || "" });
+ return response.data;
+ },
+
+ checkin: async (
+ id: string,
+ ): Promise> => {
+ const response = await apiClient.post<
+ DocumentResponse<{ checked_in?: boolean }>
+ >(`/documents/${id}/checkin`);
+ return response.data;
+ },
+};
+
diff --git a/src/types/document.ts b/src/types/document.ts
new file mode 100644
index 0000000..458b734
--- /dev/null
+++ b/src/types/document.ts
@@ -0,0 +1,90 @@
+export interface DocumentCategory {
+ id: string;
+ name: string;
+ code: string;
+ description?: string | null;
+ parent_id?: string | null;
+ requires_training?: boolean;
+ retention_years?: number;
+ review_frequency_months?: number;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface DocumentSummary {
+ id: string;
+ document_number: string;
+ title: string;
+ document_type: string;
+ category: string | null;
+ status: string;
+ current_version?: string | null;
+ owner?: string | null;
+ next_review_date?: string | null;
+ updated_at?: string;
+}
+
+export interface DocumentDetail {
+ id: string;
+ document_number: string;
+ title: string;
+ description?: string | null;
+ document_type: string;
+ category?: {
+ id: string;
+ name: string;
+ code?: string;
+ } | null;
+ status: string;
+ current_version?: string | null;
+ version_status?: string | null;
+ owner?: {
+ id: string;
+ email: string;
+ name?: string;
+ } | null;
+ department?: string | null;
+ effective_date?: string | null;
+ next_review_date?: string | null;
+ tags?: string[];
+ content?: string | null;
+ content_html?: string | null;
+ custom_fields?: Record;
+ workflow_instance_id?: string | null;
+ created_by?: string | null;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface DocumentVersion {
+ id: string;
+ version_number: string;
+ major_version: number;
+ minor_version: number;
+ status: string;
+ change_summary?: string | null;
+ change_reason?: string | null;
+ content?: string | null;
+ content_html?: string | null;
+ approved_by?: string | null;
+ approved_at?: string | null;
+ created_by?: string | null;
+ created_at?: string;
+}
+
+export interface DocumentListResponse {
+ success: boolean;
+ data: DocumentSummary[];
+ pagination: {
+ total: number;
+ limit: number;
+ offset: number;
+ };
+}
+
+export interface DocumentResponse {
+ success: boolean;
+ data: T;
+ message?: string;
+}
+