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 ( + +
+
+
+
+
+ +
+
+

+ New Controlled Document +

+

+ Document will be created in Draft status. +

+
+
+ +
+ +
+ setTitle(e.target.value)} + placeholder="Enter document title" + /> + setDocumentNumber(e.target.value)} + placeholder="Auto-generated if empty" + /> +
+ + setDescription(e.target.value)} + placeholder="Brief description of this document" + /> +
+ +
+

+ Classification +

+
+ ({ value: type.code, label: type.name }))} + placeholder="Select type" + /> + ({ + value: category.id, + label: `${category.name} (${category.code})`, + }))} + placeholder="Select category" + /> + setDepartment(e.target.value)} + placeholder="Optional" + /> + setTags(e.target.value)} + placeholder="Comma separated tags (e.g. quality, sop)" + /> +
+
+ +
+
+

Initial Content

+ + {content.length} characters + +
+ { + setContentHtml(html); + setContent(text); + }} + /> +
+ + + You can create new versions later from the document detail page. + +
+
+ +
+
+ + + + {isSaving ? "Creating..." : "Create Document"} + +
+
+
+
+ ); +}; + +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 ( + +
+
+
+ setName(e.target.value)} + placeholder="e.g. SOP" + /> + setCode(e.target.value)} + placeholder="e.g. SOP" + /> + setDescription(e.target.value)} + placeholder="Optional" + /> +
+
+ + {isSubmitting ? "Saving..." : "Add Category"} + + +
+
+ +
+ 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" + /> + +
+
+
+ +