diff --git a/package.json b/package.json index f18c68c..607ac73 100644 --- a/package.json +++ b/package.json @@ -1,62 +1,77 @@ { - "name": "mini-cms", - "type": "module", - "version": "0.0.1", - "scripts": { - "dev:astro": "astro dev --port 4321 --remote", - "dev": "npm run dev:astro", - "build": "astro build --remote", - "preview": "astro preview", - "astro": "astro" - }, - "dependencies": { - "@astrojs/alpinejs": "^0.4.9", - "@astrojs/check": "^0.9.6", - "@astrojs/db": "^0.18.3", - "@astrojs/node": "^9.5.1", - "@astrojs/react": "^4.4.2", - "@astrojs/sitemap": "^3.6.0", - "@astrojs/vercel": "^9.0.2", - "@auth/core": "^0.37.4", - "@fontsource/inter": "^5.2.5", - "@fontsource/space-grotesk": "^5.2.6", - "@hono/node-server": "^1.13.8", - "@hono/zod-validator": "^0.4.3", - "@libsql/client": "^0.15.4", - "@nanostores/persistent": "^1.0.0", - "@octokit/rest": "^21.1.1", - "@tabler/icons": "^3.35.0", - "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/vite": "^4.1.17", - "@toast-ui/editor": "^3.2.2", - "@toast-ui/react-editor": "^3.2.3", - "@types/alpinejs": "^3.13.11", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@upstash/redis": "^1.35.6", - "@vercel/analytics": "^1.6.1", - "alpinejs": "^3.15.1", - "astro": "^5.16.4", - "axios": "^1.12.0", - "hono": "^4.10.3", - "jsonwebtoken": "^9.0.2", - "nanostores": "^0.11.3", - "nodemailer": "^7.0.11", - "octokit": "^4.1.3", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "tailwind-merge": "^3.3.1", - "tailwind-variants": "^3.1.1", - "tailwindcss": "^4.1.17", - "tw-animate-css": "^1.4.0", - "typescript": "^5.8.3", - "uuid": "^13.0.0", - "yaml": "^2.8.1", - "zod": "^3.24.2" - }, - "devDependencies": { - "@biomejs/biome": "2.3.4", - "@types/jsonwebtoken": "^9.0.10", - "@types/nodemailer": "^7.0.4" - } + "name": "mini-cms", + "type": "module", + "version": "0.1.0", + "scripts": { + "dev:astro": "astro dev --port 4321 --remote", + "dev": "npm run dev:astro", + "build": "astro build --remote", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/alpinejs": "^0.4.9", + "@astrojs/check": "^0.9.6", + "@astrojs/db": "^0.18.3", + "@astrojs/node": "^9.5.1", + "@astrojs/react": "^4.4.2", + "@astrojs/sitemap": "^3.6.0", + "@astrojs/vercel": "^9.0.2", + "@auth/core": "^0.37.4", + "@fontsource/inter": "^5.2.5", + "@fontsource/space-grotesk": "^5.2.6", + "@hocuspocus/provider": "^3.4.3", + "@hocuspocus/server": "^3.4.3", + "@hono/node-server": "^1.13.8", + "@hono/zod-validator": "^0.4.3", + "@libsql/client": "^0.15.4", + "@nanostores/persistent": "^1.0.0", + "@octokit/rest": "^21.1.1", + "@tabler/icons": "^3.35.0", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/vite": "^4.1.17", + "@tanstack/react-query": "^5.96.0", + "@tanstack/react-virtual": "^3.13.23", + "@tiptap/extension-collaboration": "^3.15.3", + "@tiptap/extension-collaboration-cursor": "^2.26.2", + "@tiptap/extension-table": "^3.15.3", + "@tiptap/extension-table-cell": "^3.15.3", + "@tiptap/extension-table-header": "^3.15.3", + "@tiptap/extension-table-row": "^3.15.3", + "@tiptap/react": "^3.15.3", + "@tiptap/starter-kit": "^3.15.3", + "@toast-ui/editor": "^3.2.2", + "@toast-ui/react-editor": "^3.2.3", + "@types/alpinejs": "^3.13.11", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@upstash/redis": "^1.35.6", + "@vercel/analytics": "^1.6.1", + "alpinejs": "^3.15.1", + "astro": "^5.16.4", + "axios": "^1.12.0", + "compression": "^1.8.1", + "hono": "^4.10.3", + "jsonwebtoken": "^9.0.2", + "nanostores": "^0.11.3", + "nodemailer": "^7.0.11", + "octokit": "^4.1.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.1.1", + "tailwindcss": "^4.1.17", + "tiptap": "^1.32.2", + "tw-animate-css": "^1.4.0", + "typescript": "^5.8.3", + "uuid": "^13.0.0", + "yaml": "^2.8.1", + "yjs": "^13.6.29", + "zod": "^3.24.2" + }, + "devDependencies": { + "@biomejs/biome": "2.3.4", + "@types/jsonwebtoken": "^9.0.10", + "@types/nodemailer": "^7.0.4" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4157146..8249033 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,12 @@ importers: '@fontsource/space-grotesk': specifier: ^5.2.6 version: 5.2.6 + '@hocuspocus/provider': + specifier: ^3.4.3 + version: 3.4.3(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) + '@hocuspocus/server': + specifier: ^3.4.3 + version: 3.4.3(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) '@hono/node-server': specifier: ^1.13.8 version: 1.14.1(hono@4.10.3) @@ -62,6 +68,36 @@ importers: '@tailwindcss/vite': specifier: ^4.1.17 version: 4.1.17(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + '@tanstack/react-query': + specifier: ^5.96.0 + version: 5.96.0(react@18.3.1) + '@tanstack/react-virtual': + specifier: ^3.13.23 + version: 3.13.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tiptap/extension-collaboration': + specifier: ^3.15.3 + version: 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29) + '@tiptap/extension-collaboration-cursor': + specifier: ^2.26.2 + version: 2.26.2(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29)) + '@tiptap/extension-table': + specifier: ^3.15.3 + version: 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + '@tiptap/extension-table-cell': + specifier: ^3.15.3 + version: 3.16.0(@tiptap/extension-table@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/extension-table-header': + specifier: ^3.15.3 + version: 3.16.0(@tiptap/extension-table@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/extension-table-row': + specifier: ^3.15.3 + version: 3.16.0(@tiptap/extension-table@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/react': + specifier: ^3.15.3 + version: 3.16.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tiptap/starter-kit': + specifier: ^3.15.3 + version: 3.16.0 '@toast-ui/editor': specifier: ^3.2.2 version: 3.2.2 @@ -92,6 +128,9 @@ importers: axios: specifier: ^1.12.0 version: 1.12.0 + compression: + specifier: ^1.8.1 + version: 1.8.1 hono: specifier: ^4.10.3 version: 4.10.3 @@ -122,6 +161,9 @@ importers: tailwindcss: specifier: ^4.1.17 version: 4.1.17 + tiptap: + specifier: ^1.32.2 + version: 1.32.2(vue-template-compiler@2.7.16) tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -134,6 +176,9 @@ importers: yaml: specifier: ^2.8.1 version: 2.8.1 + yjs: + specifier: ^13.6.29 + version: 13.6.29 zod: specifier: ^3.24.2 version: 3.24.2 @@ -832,12 +877,36 @@ packages: cpu: [x64] os: [win32] + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@fontsource/inter@5.2.5': resolution: {integrity: sha512-kbsPKj0S4p44JdYRFiW78Td8Ge2sBVxi/PIBwmih+RpSXUdvS9nbs1HIiuUSPtRMi14CqLEZ/fbk7dj7vni1Sg==} '@fontsource/space-grotesk@5.2.6': resolution: {integrity: sha512-Ejf50hUdoWYFkVdqckXxXuIY3tPgQZ/095EHa21uLBOuk7CQAlxqSdkPu5y47Xv+xa8A4TSrdAJlS9YdF5DqGg==} + '@hocuspocus/common@3.4.3': + resolution: {integrity: sha512-wnBBO9sWcVAoUPEXN1qO+zk3HaEF9VTemxB6kjuuH6e1dHnD0v12m4P4X1wiZVhmMIX/PMl/fu3MGtYWQJz8gA==} + + '@hocuspocus/provider@3.4.3': + resolution: {integrity: sha512-zt+UgVXGsEQrqnDZgavc2PT9yKJjmVjV+5YxvhlmFVFLVORqawT4l601aKmLPhvyK97un4ZApZ5rso8iO6crWg==} + peerDependencies: + y-protocols: ^1.0.6 + yjs: ^13.6.8 + + '@hocuspocus/server@3.4.3': + resolution: {integrity: sha512-a9bqAXUMBo9YBeuzqNf9C3eVbu1RIWUrtmFMGq+ZssQr3Jugt/5PCkZskgqhJNvPkyTARHcUtN80j/SDLylZmg==} + peerDependencies: + y-protocols: ^1.0.6 + yjs: ^13.6.8 + '@hono/node-server@1.14.1': resolution: {integrity: sha512-vmbuM+HPinjWzPe7FFPWMMQMsbKE9gDPhaH0FFdqbGpkT5lp++tcWDTxwBl5EgS5y6JVgIaCdjeHRfQ4XRBRjQ==} engines: {node: '>=18.14.1'} @@ -1124,6 +1193,9 @@ packages: cpu: [x64] os: [win32] + '@lifeomic/attempt@3.1.0': + resolution: {integrity: sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==} + '@mapbox/node-pre-gyp@2.0.3': resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} @@ -1295,6 +1367,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1711,6 +1786,217 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/query-core@5.96.0': + resolution: {integrity: sha512-sfO3uQeol1BU7cRP6NYY7nAiX3GiNY20lI/dtSbKLwcIkYw/X+w/tEsQAkc544AfIhBX/IvH/QYtPHrPhyAKGw==} + + '@tanstack/react-query@5.96.0': + resolution: {integrity: sha512-6qbjdm1K5kizVKv9TNqhIN3doq2anRhdF2XaFMFSn4m8L22S69RV+FilvlyVT4RoJyMxtPU5rs4RpdFa/PEC7A==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-virtual@3.13.23': + resolution: {integrity: sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.13.23': + resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + + '@tiptap/core@3.16.0': + resolution: {integrity: sha512-XegRaNuoQ/guzBQU2xHxOwFXXrtoXW9tiyXDhssSqylvZmBVSlRIPNHA6ArkHBKm6ehLf6+6Y9fF3uky1yCXYQ==} + peerDependencies: + '@tiptap/pm': ^3.16.0 + + '@tiptap/extension-blockquote@3.16.0': + resolution: {integrity: sha512-c1bhJ3KDFXyNcMweiBzu0LouBXfUC/sUMtaEafQePR98BVu+d0tmWXcGlfVarGVoRyCYFa1mHpkgtxp4SS3lag==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-bold@3.16.0': + resolution: {integrity: sha512-S61wtChbOigk2bklCJ2uEa8jbAnI9ChbW4d1z/Uv/Hr6eWo42vVBtjNZKFOsiBPDajFZbOfnvekGs731jNrHKg==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-bubble-menu@3.16.0': + resolution: {integrity: sha512-nFL7FMu1LjZ5ZGf4U3tw56JLj/SpLysZvHQ1EneGB+90TEI/WReOvTY9VwH1egGWwrl7/OvQuGKclbuLIsy+BA==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + + '@tiptap/extension-bullet-list@3.16.0': + resolution: {integrity: sha512-GjKssVf9241GLdshdYRzPPApWQIB+7GJy0TZgx7bWmFUVgypYxDoE/rQRmvb3Fhup836bgfpfUzStevJ6eIClw==} + peerDependencies: + '@tiptap/extension-list': ^3.16.0 + + '@tiptap/extension-code-block@3.16.0': + resolution: {integrity: sha512-hAsXe6fIBsvIMWlVEXKLEzFQ8h6VUEBWqEEFIQgq+SpZCkGX+KzVmFXd5V2aDqb+BoOyqYiA2w1d/frBBxVEpw==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + + '@tiptap/extension-code@3.16.0': + resolution: {integrity: sha512-U8/bz/1BhQ39LJgUqJ8u1HzLcYdtubUWVAVC8seteLz1vIhXkTyfAC8478KQ+YdIDkMzAs+0vxk5BsWcWG16zQ==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-collaboration-cursor@2.26.2': + resolution: {integrity: sha512-FdRb27mZ5Kr18hN6cbfBj1e9F0DOoHB1Gv3IYeic+g4h1C9BjDVMN0+JRBQc+4lamNA8TsHO0oKWRwaPe4sSlA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + y-prosemirror: ^1.2.11 + + '@tiptap/extension-collaboration@3.16.0': + resolution: {integrity: sha512-5hoAZ3Ooi0ESvwMPLHV46Np0U3xMIZJ2uvITY7JWgIe7O1vkqo9mhr+/PI1V1S7JN6mqX5XJBbe9D1F9YvNJVw==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + '@tiptap/y-tiptap': ^3.0.0 + yjs: ^13 + + '@tiptap/extension-document@3.16.0': + resolution: {integrity: sha512-vOwBnJIonYmmFVMEnnE1jwoUMq0P/9BcaUocIG9o5iFRTV38I8YGn8n6DiE1pjSeLXRpLrXl6LLwdOMBJewhBg==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-dropcursor@3.16.0': + resolution: {integrity: sha512-n9Gbt99K9oBChjp8puF0ffAJtBF6ZVjydG5u5QO2Z8sHNE+Hn6ARfgZqLjr11ZF4b+mLShqsmyROmITNf73W+A==} + peerDependencies: + '@tiptap/extensions': ^3.16.0 + + '@tiptap/extension-floating-menu@3.16.0': + resolution: {integrity: sha512-cokYXL8EkW+CFIlke70GLL7iKetUtYEp87muMG9oflczyj0BjmGAbO7Mskm+bcQBhxZ0dIYILTqKn2bNBvCDFw==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + + '@tiptap/extension-gapcursor@3.16.0': + resolution: {integrity: sha512-8dxE4bkfn6Jog/JHDxN/kzcRbyJB7HyFqCKdiTq0f4atzysmnEUuMswwlwMPaErkzlETD6B8NEEtMknEUqowGA==} + peerDependencies: + '@tiptap/extensions': ^3.16.0 + + '@tiptap/extension-hard-break@3.16.0': + resolution: {integrity: sha512-nwUTixlHYo9V1lfOYsRi2JiAYCRC7pObB3Kt7rEeMxB3XmcRcSpHtxYs6r+TvifsLFys8RG5wOFXIV/YXZHcDg==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-heading@3.16.0': + resolution: {integrity: sha512-du4d1Ukvhr1zvPWlU/HS3NMlRswzGRSNDNfCFUhdYgQoHOSnUXshnlKD3E5H0EHfL9UwT4JFyqAT3+1ZnahkdA==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-horizontal-rule@3.16.0': + resolution: {integrity: sha512-yyKl45UCH55pIf8G4bHiUNFxggipRVT276c3t9vrkXU6BkJhzfxxcIc5svWkiThDjdYmJs1FfVCYAtGSuKiSyA==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + + '@tiptap/extension-italic@3.16.0': + resolution: {integrity: sha512-SVNnkRUK6G+dQse5Ms8Q/wudSTh37O94p02RDc3KneEtBk6wkokqCLuwKnWLPhlEqsuOku+wTD9DSJdvoRlq9w==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-link@3.16.0': + resolution: {integrity: sha512-WPPJLtGXQadBVVwH6gcMpaXIgfvFF9NGpE2IVqleVKR3Epv2Rd4aWd4oyAdrT8KU9G6dzMXZfkrB8aArTDKxYQ==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + + '@tiptap/extension-list-item@3.16.0': + resolution: {integrity: sha512-kshssUZEPoosPWbJNQEFJnVV3iPwsDU9l/RCdHJB5SE+aNWJyUk5hQ/YwngEHjV7rS+RnAuhbrcB5swgyzROuA==} + peerDependencies: + '@tiptap/extension-list': ^3.16.0 + + '@tiptap/extension-list-keymap@3.16.0': + resolution: {integrity: sha512-AU3J9W6uo835ZdxiGmrYx1KUymzvfkU4d278X0OBAfujORXkbDNlo9er8pOrOpgXNxgtnlH32lWR4bWyKdUgwA==} + peerDependencies: + '@tiptap/extension-list': ^3.16.0 + + '@tiptap/extension-list@3.16.0': + resolution: {integrity: sha512-tpjWGugfI0XYR9iG/QlYYtCY35TFWHNwGKc94wN4s7NmAjB4xlwdTkTZQ6PdZ39x1SeHkRjxAka+6GcBIoOHGQ==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + + '@tiptap/extension-ordered-list@3.16.0': + resolution: {integrity: sha512-mNKqwEgiXSMi5afGtnodsptveukpr3GqcGsw2fqJFyNq9SITznjiiuQfULtzVnayC8qHsk0Zzbpzf0zvdHlypg==} + peerDependencies: + '@tiptap/extension-list': ^3.16.0 + + '@tiptap/extension-paragraph@3.16.0': + resolution: {integrity: sha512-JHn3ev7US5FxtQFyEOeQ8XfvKcR5NiHkwDH2Gcwe+0ttpA/Qrrr5XN3tJIgI3rXfR5DjxArq/QO0OTVBm3xlJA==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-strike@3.16.0': + resolution: {integrity: sha512-l5/4+gii53kET7ETyYpbTumoQdZ6HwJLUcDlGHutLZlBCaZPxFTi5qgHQBhNq5KAzRH3LVJeb0fEeMi+yCZBQA==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-table-cell@3.16.0': + resolution: {integrity: sha512-SMYrXI78ujRiuLN1AGHYSbH8w8dLvXbXtNMyLa194E6UQ+Q3ljd07CJ8af5fiPEqYPG5GA+r4Ka6+MVmAq5AKQ==} + peerDependencies: + '@tiptap/extension-table': ^3.16.0 + + '@tiptap/extension-table-header@3.16.0': + resolution: {integrity: sha512-MxwfzV1TufdYdQHG+W6OtTPMEuhRLPt6rlBuZU5ASGl5XATyG9ltbuDu1/NwqITpixvKvebVd0200afvNBgmgg==} + peerDependencies: + '@tiptap/extension-table': ^3.16.0 + + '@tiptap/extension-table-row@3.16.0': + resolution: {integrity: sha512-bM7UuXdMQwVf+6qk9X6gnIbhI16gLxyElSyQvecFozDaYl3NI+UYwztyHm01cvuNbWLPDqkKwrCE+25Q9P3W/A==} + peerDependencies: + '@tiptap/extension-table': ^3.16.0 + + '@tiptap/extension-table@3.16.0': + resolution: {integrity: sha512-m7h7YdffWxI0lglKUfR+39UD9psOprn/E4qYzjxOSXl1rg8DnP6zi8LF+5X+v32my9WBbizXxVBIdy8AuDWxAw==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + + '@tiptap/extension-text@3.16.0': + resolution: {integrity: sha512-KTewoX4wZq95cKnjBbogRwBFoGgM6qUg1yjCQ/M6Ajkp4Mtp8Iki9EiAxtfk76b/wtXFf3DsDhFOeVqgKyYbYg==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extension-underline@3.16.0': + resolution: {integrity: sha512-obXAPgHVZocMaW6HtKyCYsN4CxHogWr23gioyEQcpIX0LeegHDqxkoPrjIPX6Tn1isDyvXchcSKWHEfiHO3ZOA==} + peerDependencies: + '@tiptap/core': ^3.16.0 + + '@tiptap/extensions@3.16.0': + resolution: {integrity: sha512-0iVrn0FHcHIRMdsQLQbf16NgYrKz+Sup/8dDMVBy1QoHn5Hb51QZABqXJTZ6u7My34b4fNZrSggzBAE7l7N/pA==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + + '@tiptap/pm@3.16.0': + resolution: {integrity: sha512-FMxZ6Tc5ONKa/EByDV8lswct6YW2lF/wn11zqXmrfBZhdG7UQPTijpSwb6TCqaO5GOHmixaIaDPj+zimUREHQA==} + + '@tiptap/react@3.16.0': + resolution: {integrity: sha512-r1R19Ma4zxGt8ImiNOqSArAnWO239KUI9tTVeelgTyekPj7643lO8GbtuXJfAeWGPduDIpcAgR/Dd4NKieetiA==} + peerDependencies: + '@tiptap/core': ^3.16.0 + '@tiptap/pm': ^3.16.0 + '@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 + + '@tiptap/starter-kit@3.16.0': + resolution: {integrity: sha512-eWi+77SgKyhSx91Hmn32ER+gPN6FfInGtod4A+XxSG+LqS/sn6kpUEdowYrnqiZzhUXZCSTSJvC+UcMUZHOkxQ==} + + '@tiptap/y-tiptap@3.0.1': + resolution: {integrity: sha512-F3hj5X77ckmyIywbCQpKgyX3xKra2/acJPWaV5R9wqp0cUPBmm62FYbkQ6HaqxH1VhCkUhhAZcDSQjbjj7tnWw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 + '@toast-ui/editor@3.2.2': resolution: {integrity: sha512-ASX7LFjN2ZYQJrwmkUajPs7DRr9FsM1+RQ82CfTO0Y5ZXorBk1VZS4C2Dpxinx9kl55V4F8/A2h2QF4QMDtRbA==} @@ -1752,9 +2038,18 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1791,6 +2086,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -1961,6 +2259,12 @@ packages: engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -2027,6 +2331,10 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2103,6 +2411,14 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -2117,6 +2433,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2155,6 +2474,17 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2405,6 +2735,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -2431,6 +2765,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2594,6 +2932,10 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hono@4.10.3: resolution: {integrity: sha512-2LOYWUbnhdxdL8MNbNg9XZig6k+cZXm5IjHn2Aviv7honhBMOHb+jxrKIeJRZJRmn+htUCKhaicxwXuUDlchRA==} engines: {node: '>=16.9.0'} @@ -2661,6 +3003,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -2721,6 +3066,11 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} + engines: {node: '>=16'} + hasBin: true + libsql@0.5.22: resolution: {integrity: sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA==} cpu: [x64, arm64, wasm32, arm] @@ -2801,6 +3151,12 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -2844,6 +3200,10 @@ packages: magicast@0.5.1: resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -2896,6 +3256,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3024,6 +3387,9 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3044,6 +3410,10 @@ packages: resolution: {integrity: sha512-TUes3xKIX33re4QzdxwZ6tdbodjmn3tWXCEc1uokiEmo14sI1EaGYNs2k3bU2pyyGNmBqFGAVl6jAGWd06AVIg==} engines: {node: ^18.0.0 || >=20.0.0} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} @@ -3115,6 +3485,10 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -3216,9 +3590,21 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + prosemirror-changeset@2.3.1: + resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + prosemirror-commands@1.7.1: resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.0: + resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} + prosemirror-history@1.5.0: resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} @@ -3228,21 +3614,50 @@ packages: prosemirror-keymap@1.2.3: resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + prosemirror-markdown@1.13.3: + resolution: {integrity: sha512-3E+Et6cdXIH0EgN2tGYQ+EBT7N4kMiZFsW+hzx+aPtOmADDHWCdd2uUQb7yklJrfUYUOjEEu22BiN6UFgPe4cQ==} + + prosemirror-menu@1.2.5: + resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} + prosemirror-model@1.25.4: resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + prosemirror-state@1.4.4: resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + prosemirror-transform@1.10.5: resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==} prosemirror-view@1.41.3: resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==} + prosemirror-view@1.41.5: + resolution: {integrity: sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3503,6 +3918,18 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tiptap-commands@1.17.1: + resolution: {integrity: sha512-CyGvMD/c6fNer5LThWGtrVMXHAqHn93ivGQpqJ58x3HNZFuoIiF9QTWXAiWbY/4QrG0ANYHKCSe9n5afickTqw==} + + tiptap-utils@1.13.1: + resolution: {integrity: sha512-RoCvMfkdu7fp9u7nsRr1OgsYU8RFjoHKHEKpx075rJ9X0t+j5Vxah9n6QzTTr4yjvcavq22WO2flFacm36zYtA==} + + tiptap@1.32.2: + resolution: {integrity: sha512-5IwVj8nGo8y5V3jbdtoEd7xNUsi8Q0N6WV2Nfs70olqz3fldXkiImBrDhZJ4Anx8vhyP6PIBttrg0prFVmwIvw==} + peerDependencies: + vue: ^2.5.17 + vue-template-compiler: ^2.5.17 + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3555,6 +3982,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} @@ -3686,10 +4116,19 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + uuid@13.0.0: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -3839,6 +4278,9 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-template-compiler@2.7.16: + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -3895,6 +4337,22 @@ packages: xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y-prosemirror@1.3.7: + resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 + + y-protocols@1.0.7: + resolution: {integrity: sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3928,6 +4386,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yjs@13.6.29: + resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -4895,10 +5357,54 @@ snapshots: '@esbuild/win32-x64@0.25.2': optional: true + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + optional: true + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + optional: true + + '@floating-ui/utils@0.2.10': + optional: true + '@fontsource/inter@5.2.5': {} '@fontsource/space-grotesk@5.2.6': {} + '@hocuspocus/common@3.4.3': + dependencies: + lib0: 0.2.117 + + '@hocuspocus/provider@3.4.3(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + '@hocuspocus/common': 3.4.3 + '@lifeomic/attempt': 3.1.0 + lib0: 0.2.117 + ws: 8.18.1 + y-protocols: 1.0.7(yjs@13.6.29) + yjs: 13.6.29 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@hocuspocus/server@3.4.3(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + '@hocuspocus/common': 3.4.3 + async-lock: 1.4.1 + async-mutex: 0.5.0 + kleur: 4.1.5 + lib0: 0.2.117 + ws: 8.18.1 + y-protocols: 1.0.7(yjs@13.6.29) + yjs: 13.6.29 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@hono/node-server@1.14.1(hono@4.10.3)': dependencies: hono: 4.10.3 @@ -5148,6 +5654,8 @@ snapshots: '@libsql/win32-x64-msvc@0.5.7': optional: true + '@lifeomic/attempt@3.1.0': {} + '@mapbox/node-pre-gyp@2.0.3': dependencies: consola: 3.4.2 @@ -5360,6 +5868,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@remirror/core-constants@3.0.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.3.0(rollup@4.53.3)': @@ -5822,6 +6332,236 @@ snapshots: tailwindcss: 4.1.17 vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + '@tanstack/query-core@5.96.0': {} + + '@tanstack/react-query@5.96.0(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.96.0 + react: 18.3.1 + + '@tanstack/react-virtual@3.13.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/virtual-core@3.13.23': {} + + '@tiptap/core@3.16.0(@tiptap/pm@3.16.0)': + dependencies: + '@tiptap/pm': 3.16.0 + + '@tiptap/extension-blockquote@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-bold@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-bubble-menu@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + optional: true + + '@tiptap/extension-bullet-list@3.16.0(@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extension-list': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-code-block@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + + '@tiptap/extension-code@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-collaboration-cursor@2.26.2(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + y-prosemirror: 1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) + + '@tiptap/extension-collaboration@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + '@tiptap/y-tiptap': 3.0.1(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) + yjs: 13.6.29 + + '@tiptap/extension-document@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-dropcursor@3.16.0(@tiptap/extensions@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extensions': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-floating-menu@3.16.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + optional: true + + '@tiptap/extension-gapcursor@3.16.0(@tiptap/extensions@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extensions': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-hard-break@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-heading@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-horizontal-rule@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + + '@tiptap/extension-italic@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-link@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.16.0(@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extension-list': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-list-keymap@3.16.0(@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extension-list': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + + '@tiptap/extension-ordered-list@3.16.0(@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extension-list': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-paragraph@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-strike@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-table-cell@3.16.0(@tiptap/extension-table@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extension-table': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-table-header@3.16.0(@tiptap/extension-table@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extension-table': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-table-row@3.16.0(@tiptap/extension-table@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/extension-table': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + + '@tiptap/extension-table@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + + '@tiptap/extension-text@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extension-underline@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + + '@tiptap/extensions@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + + '@tiptap/pm@3.16.0': + dependencies: + prosemirror-changeset: 2.3.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.0 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.3 + prosemirror-menu: 1.2.5 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.3 + + '@tiptap/react@3.16.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + '@tiptap/extension-floating-menu': 3.16.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.16.0': + dependencies: + '@tiptap/core': 3.16.0(@tiptap/pm@3.16.0) + '@tiptap/extension-blockquote': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-bold': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-bullet-list': 3.16.0(@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/extension-code': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-code-block': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + '@tiptap/extension-document': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-dropcursor': 3.16.0(@tiptap/extensions@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/extension-gapcursor': 3.16.0(@tiptap/extensions@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/extension-hard-break': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-heading': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-horizontal-rule': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + '@tiptap/extension-italic': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-link': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + '@tiptap/extension-list': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + '@tiptap/extension-list-item': 3.16.0(@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/extension-list-keymap': 3.16.0(@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/extension-ordered-list': 3.16.0(@tiptap/extension-list@3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0)) + '@tiptap/extension-paragraph': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-strike': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-text': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extension-underline': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) + '@tiptap/extensions': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) + '@tiptap/pm': 3.16.0 + + '@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + lib0: 0.2.117 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.3 + y-protocols: 1.0.7(yjs@13.6.29) + yjs: 13.6.29 + '@toast-ui/editor@3.2.2': dependencies: dompurify: 2.5.8 @@ -5882,10 +6622,19 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 22.14.0 + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} '@types/nlcst@2.0.3': @@ -5930,6 +6679,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/ws@8.18.1': dependencies: '@types/node': 24.10.1 @@ -6211,6 +6962,12 @@ snapshots: - uploadthing - yaml + async-lock@1.4.1: {} + + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + async-sema@3.1.1: {} asynckit@0.4.0: {} @@ -6280,6 +7037,8 @@ snapshots: buffer-equal-constant-time@1.0.1: {} + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6335,6 +7094,22 @@ snapshots: common-ancestor-path@1.0.1: {} + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + consola@3.4.2: {} convert-source-map@2.0.0: {} @@ -6343,6 +7118,8 @@ snapshots: cookie@1.1.1: {} + crelt@1.0.6: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -6383,6 +7160,12 @@ snapshots: data-uri-to-buffer@4.0.1: {} + de-indent@1.0.2: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -6565,6 +7348,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} estree-walker@2.0.2: {} @@ -6583,6 +7368,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6816,6 +7603,8 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + he@1.2.0: {} + hono@4.10.3: {} html-escaper@3.0.3: {} @@ -6869,6 +7658,8 @@ snapshots: isexe@2.0.0: {} + isomorphic.js@0.2.5: {} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -6928,6 +7719,10 @@ snapshots: kleur@4.1.5: {} + lib0@0.2.117: + dependencies: + isomorphic.js: 0.2.5 + libsql@0.5.22: dependencies: '@neon-rs/load': 0.0.4 @@ -7005,6 +7800,12 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -7043,6 +7844,15 @@ snapshots: '@babel/types': 7.28.5 source-map-js: 1.2.1 + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} math-intrinsics@1.1.0: {} @@ -7171,6 +7981,8 @@ snapshots: mdn-data@2.12.2: {} + mdurl@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -7395,6 +8207,8 @@ snapshots: mrmime@2.0.1: {} + ms@2.0.0: {} + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -7405,6 +8219,8 @@ snapshots: nanostores@0.11.3: {} + negotiator@0.6.4: {} + neotraverse@0.6.18: {} nlcst-to-string@4.0.0: @@ -7470,6 +8286,8 @@ snapshots: dependencies: ee-first: 1.1.1 + on-headers@1.1.0: {} + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.4: @@ -7558,12 +8376,33 @@ snapshots: property-information@7.1.0: {} + prosemirror-changeset@2.3.1: + dependencies: + prosemirror-transform: 1.10.5 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-commands@1.7.1: dependencies: prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 prosemirror-transform: 1.10.5 + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.3 + + prosemirror-gapcursor@1.4.0: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.3 + prosemirror-history@1.5.0: dependencies: prosemirror-state: 1.4.4 @@ -7581,16 +8420,55 @@ snapshots: prosemirror-state: 1.4.4 w3c-keyname: 2.2.8 + prosemirror-markdown@1.13.3: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.2.5: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + prosemirror-model@1.25.4: dependencies: orderedmap: 2.1.1 + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-state@1.4.4: dependencies: prosemirror-model: 1.25.4 prosemirror-transform: 1.10.5 prosemirror-view: 1.41.3 + prosemirror-tables@1.8.5: + 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.5 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.3 + prosemirror-transform@1.10.5: dependencies: prosemirror-model: 1.25.4 @@ -7601,8 +8479,16 @@ snapshots: prosemirror-state: 1.4.4 prosemirror-transform: 1.10.5 + prosemirror-view@1.41.5: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + proxy-from-env@1.1.0: {} + punycode.js@2.3.1: {} + punycode@2.3.1: optional: true @@ -7951,6 +8837,36 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tiptap-commands@1.17.1: + dependencies: + prosemirror-commands: 1.7.1 + prosemirror-inputrules: 1.5.1 + prosemirror-model: 1.25.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + tiptap-utils: 1.13.1 + + tiptap-utils@1.13.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + + tiptap@1.32.2(vue-template-compiler@2.7.16): + dependencies: + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.3 + tiptap-commands: 1.17.1 + tiptap-utils: 1.13.1 + vue-template-compiler: 2.7.16 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7983,6 +8899,8 @@ snapshots: typescript@5.8.3: {} + uc.micro@2.1.0: {} + ufo@1.6.1: {} ultrahtml@1.6.0: {} @@ -8090,8 +9008,14 @@ snapshots: punycode: 2.3.1 optional: true + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + uuid@13.0.0: {} + vary@1.1.2: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -8223,6 +9147,11 @@ snapshots: vscode-uri@3.1.0: {} + vue-template-compiler@2.7.16: + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + w3c-keyname@2.2.8: {} web-namespaces@2.0.1: {} @@ -8268,6 +9197,20 @@ snapshots: xxhash-wasm@1.1.0: {} + y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29): + dependencies: + lib0: 0.2.117 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.3 + y-protocols: 1.0.7(yjs@13.6.29) + yjs: 13.6.29 + + y-protocols@1.0.7(yjs@13.6.29): + dependencies: + lib0: 0.2.117 + yjs: 13.6.29 + y18n@5.0.8: {} yallist@3.1.1: {} @@ -8305,6 +9248,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yjs@13.6.29: + dependencies: + lib0: 0.2.117 + yocto-queue@1.2.2: {} yocto-spinner@0.2.3: diff --git a/public/favicon/browserconfig.xml b/public/favicon/browserconfig.xml index c554148..1a8136d 100644 --- a/public/favicon/browserconfig.xml +++ b/public/favicon/browserconfig.xml @@ -1,2 +1,2 @@ -#ffffff \ No newline at end of file +#ffffff \ No newline at end of file diff --git a/public/favicon/manifest.json b/public/favicon/manifest.json index 013d4a6..8cf848d 100644 --- a/public/favicon/manifest.json +++ b/public/favicon/manifest.json @@ -1,41 +1,41 @@ { - "name": "App", - "icons": [ - { - "src": "\/android-icon-36x36.png", - "sizes": "36x36", - "type": "image\/png", - "density": "0.75" - }, - { - "src": "\/android-icon-48x48.png", - "sizes": "48x48", - "type": "image\/png", - "density": "1.0" - }, - { - "src": "\/android-icon-72x72.png", - "sizes": "72x72", - "type": "image\/png", - "density": "1.5" - }, - { - "src": "\/android-icon-96x96.png", - "sizes": "96x96", - "type": "image\/png", - "density": "2.0" - }, - { - "src": "\/android-icon-144x144.png", - "sizes": "144x144", - "type": "image\/png", - "density": "3.0" - }, - { - "src": "\/android-icon-192x192.png", - "sizes": "192x192", - "type": "image\/png", - "density": "4.0" - } - ] + "name": "App", + "icons": [ + { + "src": "/favicon/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "/favicon/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "/favicon/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "/favicon/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "/favicon/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "/favicon/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] } \ No newline at end of file diff --git a/src/actions/projects.ts b/src/actions/projects.ts index b11f696..a418fcd 100644 --- a/src/actions/projects.ts +++ b/src/actions/projects.ts @@ -11,8 +11,10 @@ export const projectsActions = { getProjectActivity: defineAction({ input: z.object({ projectId: z.string(), + page: z.number().optional().default(1), + limit: z.number().optional().default(20), }), - handler: async ({ projectId }, context) => { + handler: async ({ projectId, page, limit }, context) => { const userId = context.locals.currentUser?.id; if (!userId) { throw new ActionError({ @@ -26,8 +28,8 @@ export const projectsActions = { const projectService = new ProjectService(projectRepository); const activity = await projectService.getProjectActivity( projectId, - 1, - 20, + page, + limit, userId, ); return activity; diff --git a/src/components/dashboard/DashboardMain.tsx b/src/components/dashboard/DashboardMain.tsx new file mode 100644 index 0000000..a2bcae1 --- /dev/null +++ b/src/components/dashboard/DashboardMain.tsx @@ -0,0 +1,172 @@ +import React, { useState, useEffect } from 'react'; +import ProjectsList from './ProjectsList'; +import RecentActivities from './RecentActivities'; +import DashboardStats from './DashboardStats'; +import { userProjectsSWR } from '@/utils/cachedFn'; +import { $orgState, currentOrgIdState } from '@/store/navStore'; + +interface DashboardMainProps { + userId: string; + currentOrg: any; + userOrgs?: any[]; + userName: string; + installationSuccessMessage?: string; + installationErrorMessage?: string; + needsGitHubInstallation?: boolean; +} + +const DashboardMain: React.FC = ({ + userId, + currentOrg, + userOrgs = [], + userName, + installationSuccessMessage, + installationErrorMessage, + needsGitHubInstallation +}) => { + const [activeTab, setActiveTab] = useState<'projects' | 'activity'>('projects'); + const [projectIds, setProjectIds] = useState([]); + + // Update Nanostores on mount/org change + useEffect(() => { + if (currentOrg?.id) { + currentOrgIdState.set(currentOrg.id); + } + if (userOrgs && userOrgs.length > 0) { + $orgState.set(userOrgs); + } + }, [currentOrg?.id, userOrgs]); + + // Pre-fetch projects to get IDs for RecentActivities + useEffect(() => { + const fetchProjectIds = async () => { + try { + const response = await userProjectsSWR.fetch(userId); + const allProjects = response?.data || []; + const orgProjects = allProjects.filter((p: any) => p.org_id === currentOrg?.id); + setProjectIds(orgProjects.map((p: any) => p.id)); + } catch (err) { + console.error('Failed to fetch project IDs for activity feed:', err); + } + }; + + if (userId && currentOrg?.id) { + fetchProjectIds(); + } + }, [userId, currentOrg?.id]); + + return ( +
+ {/* Welcome Section */} +
+

+ Welcome back, {userName}! +

+

+ Here's what's happening with your projects in{" "} + + {currentOrg?.name || "your organization"} + {" "} + today. +

+
+ + {/* Stats Cards Section */} + + + {/* Projects & Recent Activity Tabs */} +
+
+

+ Projects & Activity +

+
+
+ + +
+ {activeTab === 'activity' && ( + + )} +
+
+ + {activeTab === 'projects' && ( +
+ +
+ )} + + {activeTab === 'activity' && ( +
+ +
+ )} +
+ + {/* Quick Actions */} +
+

+ Quick Actions +

+ +
+
+ ); +}; + +export default DashboardMain; diff --git a/src/components/dashboard/DashboardStats.tsx b/src/components/dashboard/DashboardStats.tsx new file mode 100644 index 0000000..fd6d505 --- /dev/null +++ b/src/components/dashboard/DashboardStats.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react'; +import { userProjectsSWR } from '@/utils/cachedFn'; +import { calculateActivityGrowth } from '@/lib/server/shared/utils'; +import { actions } from 'astro:actions'; + +interface DashboardStatsProps { + userId: string; + currentOrgId: string; +} + +const DashboardStats: React.FC = ({ userId, currentOrgId }) => { + const [projectCount, setProjectCount] = useState(null); + const [activityGrowth, setActivityGrowth] = useState<{ growth: number; sign: string } | null>(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let isMounted = true; + + const fetchStats = async () => { + setIsLoading(true); + try { + const response = await fetch(`/api/dashboard/stats?orgId=${currentOrgId}`); + if (!response.ok) throw new Error('Failed to fetch stats'); + + const result = await response.json(); + const data = result.data; + + if (isMounted) { + setProjectCount(data.projectCount); + setActivityGrowth(data.activityGrowth); + } + } catch (err) { + console.error('Error fetching dashboard stats:', err); + } finally { + if (isMounted) { + setIsLoading(false); + } + } + }; + + fetchStats(); + + return () => { + isMounted = false; + }; + }, [userId, currentOrgId]); + + const StatCard = ({ title, value, icon, growth }: any) => ( +
+
+
+ {icon} +
+
+

{title}

+
+

+ {isLoading ? ( + + ) : ( + value + )} +

+ {growth && !isLoading && ( + = 0 ? 'text-green-600' : 'text-earth-accent'}`}> + {growth.sign}{growth.growth}% + + )} +
+
+
+
+ ); + + return ( +
+ + + + } + /> + + + + } + /> + + + + } + /> +
+ ); +}; + +export default DashboardStats; diff --git a/src/components/dashboard/FileExplorer.tsx b/src/components/dashboard/FileExplorer.tsx index ed747c4..b2e7491 100644 --- a/src/components/dashboard/FileExplorer.tsx +++ b/src/components/dashboard/FileExplorer.tsx @@ -3,730 +3,682 @@ import { useCallback, useEffect, useState } from "react"; import api from "@/lib/clients"; interface FileItem { - name: string; - path: string; - type: "file" | "dir"; - size: number; - sha: string; + name: string; + path: string; + type: "file" | "dir"; + size: number; + sha: string; } interface DraftData { - content: string; - timestamp: number; - filePath: string; + content: string; + timestamp: number; + filePath: string; +} + +interface DirectoryConfig { + path: string; + schema?: Record; + naming_convention?: string; + base_image_path?: string; } interface FileExplorerProps { - projectId: string; - repoOwner: string; - repoName: string; - onFileSelect: (file: FileItem) => void; - selectedFile: FileItem | null; - drafts: Record; - pendingCreates?: Array<{ path: string; type: "file" | "folder" }>; - onFileCreate?: (path: string, type: "file" | "folder") => void; + projectId: string; + repoOwner: string; + repoName: string; + onFileSelect: (file: FileItem) => void; + selectedFile: FileItem | null; + drafts: Record; + pendingCreates?: Array<{ path: string; type: "file" | "folder" }>; + onFileCreate?: (path: string, type: "file" | "folder") => void; } interface DirectoryNode { - name: string; - path: string; - type: "file" | "dir"; - children?: DirectoryNode[]; - expanded?: boolean; - size: number; - sha: string; - isPending?: boolean; + name: string; + path: string; + type: "file" | "dir"; + children?: DirectoryNode[]; + expanded?: boolean; + size: number; + sha: string; + isPending?: boolean; } const FileExplorer: React.FC = ({ - projectId, - repoOwner, - repoName, - onFileSelect, - selectedFile, - drafts, - pendingCreates = [], - onFileCreate, + projectId, + repoOwner, + repoName, + onFileSelect, + selectedFile, + drafts, + pendingCreates = [], + onFileCreate, }) => { - const [rootContents, setRootContents] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [expandedDirs, setExpandedDirs] = useState>(new Set()); - const [allowedDirectories, setAllowedDirectories] = useState( - null, - ); - const [settingsLoaded, setSettingsLoaded] = useState(false); - const [showCreateModal, setShowCreateModal] = useState(false); - const [createType, setCreateType] = useState<"file" | "folder">("file"); - const [createPath, setCreatePath] = useState(""); - const [newItemName, setNewItemName] = useState(""); - const [contextMenu, setContextMenu] = useState<{ - x: number; - y: number; - path: string; - type: "file" | "dir"; - } | null>(null); - - const loadDirectoryContents = useCallback( - async (path: string = "") => { - try { - const response = await (api.projects as any)[projectId].repo[repoOwner][ - repoName - ].contents.$get({ - query: path ? { path } : {}, - }); - - if (!response.ok) { - throw new Error(`Failed to load directory: ${response.statusText}`); - } - - const data = await response.json(); - return data.data as FileItem[]; - } catch (err) { - console.error("Error loading directory:", err); - throw err; - } - }, - [projectId, repoOwner, repoName], - ); - - const fetchSettings = useCallback(async () => { - try { - // console.log("[FileExplorer] Fetching settings for project:", projectId); - const response = await (api.projects as any)[projectId].settings.$get(); - - // console.log("[FileExplorer] Settings response status:", response.status); - if (!response.ok) { - const errorText = await response.text(); - console.error("[FileExplorer] Settings fetch failed:", errorText); - throw new Error("Failed to fetch project settings"); - } - - const data = await response.json(); - // console.log("[FileExplorer] Settings data:", data); - const settings = data.data; - const dirs = JSON.parse(settings.public_directories || "[]"); - // console.log("[FileExplorer] Allowed directories:", dirs); - setAllowedDirectories(dirs); - setSettingsLoaded(true); - return dirs; - } catch (err) { - console.error("[FileExplorer] Error fetching settings:", err); - // Fallback to empty if error - setAllowedDirectories([]); - setSettingsLoaded(true); - return []; - } - }, [projectId]); - - // build tree structure from flat file list - const buildTree = useCallback( - (items: FileItem[], dirPath: string = ""): DirectoryNode[] => { - const nodes: DirectoryNode[] = items.map((item) => ({ - name: item.name, - path: item.path, - type: item.type, - size: item.size, - sha: item.sha, - children: item.type === "dir" ? [] : undefined, - expanded: expandedDirs.has(item.path), - })); - - const pendingInDir = pendingCreates.filter((p) => { - const parentPath = p.path.split("/").slice(0, -1).join("/"); - return parentPath === dirPath; - }); - - pendingInDir.forEach((p) => { - // avoid duplicates if file already exists on server - if (!nodes.some((n) => n.path === p.path)) { - nodes.push({ - name: p.path.split("/").pop() || "", - path: p.path, - type: p.type === "folder" ? "dir" : "file", - size: 0, - sha: "", - children: p.type === "folder" ? [] : undefined, - expanded: expandedDirs.has(p.path), - isPending: true, - }); - } - }); - - nodes.sort((a, b) => { - if (a.type !== b.type) { - return a.type === "dir" ? -1 : 1; - } - return a.name.localeCompare(b.name); - }); - - return nodes; - }, - [pendingCreates, expandedDirs], - ); - - // Load root directory on mount - useEffect(() => { - const loadRoot = async () => { - setIsLoading(true); - setError(null); - - try { - const dirs = await fetchSettings(); - - if (dirs && dirs.length > 0) { - - const nodes: DirectoryNode[] = dirs.map((dir: string) => ({ - name: dir, - path: dir, - type: "dir", - size: 0, - sha: "", - children: undefined, - expanded: false, - })); - setRootContents(nodes); - } - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load files"); - } finally { - setIsLoading(false); - } - }; - - loadRoot(); - }, [loadDirectoryContents, fetchSettings]); - - - useEffect(() => { - const handleSettingsUpdate = () => { - const reload = async () => { - setIsLoading(true); - try { - const dirs = await fetchSettings(); - if (dirs && dirs.length > 0) { - const nodes: DirectoryNode[] = dirs.map((dir: string) => ({ - name: dir, - path: dir, - type: "dir", - size: 0, - sha: "", - children: undefined, - expanded: false, - })); - setRootContents(nodes); - } else { - setRootContents([]); - } - } catch (err) { - console.error("Error reloading:", err); - } finally { - setIsLoading(false); - } - }; - reload(); - }; - - window.addEventListener("project-settings-updated", handleSettingsUpdate); - return () => { - window.removeEventListener( - "project-settings-updated", - handleSettingsUpdate, - ); - }; - }, [fetchSettings, loadDirectoryContents]); - - const toggleDirectory = async (dirPath: string) => { - const isExpanded = expandedDirs.has(dirPath); - const newExpandedDirs = new Set(expandedDirs); - - if (isExpanded) { - newExpandedDirs.delete(dirPath); - setRootContents((prevTree) => { - const updateNode = (nodes: DirectoryNode[]): DirectoryNode[] => { - return nodes.map((node) => { - if (node.path === dirPath) { - return { - ...node, - expanded: false, - }; - } - if (node.children) { - return { - ...node, - children: updateNode(node.children), - }; - } - return node; - }); - }; - return updateNode(prevTree); - }); - } else { - newExpandedDirs.add(dirPath); - try { - const isPendingDir = pendingCreates.some( - (p) => p.path === dirPath && p.type === "folder", - ); - - let contents: FileItem[] = []; - if (!isPendingDir) { - contents = await loadDirectoryContents(dirPath); - } - - setRootContents((prevTree) => { - const updateNode = (nodes: DirectoryNode[]): DirectoryNode[] => { - return nodes.map((node) => { - if (node.path === dirPath && node.type === "dir") { - return { - ...node, - children: buildTree(contents, dirPath), - expanded: true, - }; - } - if (node.children) { - return { - ...node, - children: updateNode(node.children), - }; - } - return node; - }); - }; - return updateNode(prevTree); - }); - } catch (err) { - console.error("Failed to load directory contents:", err); - return; - } - } - - setExpandedDirs(newExpandedDirs); - }; - - const handleFileClick = (file: DirectoryNode) => { - if (file.type === "dir") { - toggleDirectory(file.path); - } else { - if (selectedFile?.path === file.path) { - return; - } - onFileSelect({ - name: file.name, - path: file.path, - type: "file", - size: file.size, - sha: file.sha, - }); - } - }; - - const handleContextMenu = (e: React.MouseEvent, node: DirectoryNode) => { - e.preventDefault(); - setContextMenu({ - x: e.clientX, - y: e.clientY, - path: node.path, - type: node.type === "dir" ? "dir" : "file", - }); - }; - - useEffect(() => { - const handleClick = () => setContextMenu(null); - document.addEventListener("click", handleClick); - return () => document.removeEventListener("click", handleClick); - }, []); - - const [isCreating, setIsCreating] = useState(false); - - const handleCreate = async () => { - if (!newItemName.trim() || isCreating) return; - - setIsCreating(true); - const fullPath = createPath ? `${createPath}/${newItemName}` : newItemName; - - try { - if (onFileCreate) { - onFileCreate(fullPath, createType); - } else { - if (createType === "file") { - const response = await (api.projects as any)[projectId].repo[ - repoOwner - ][repoName]["create-file"].$post({ - json: { - path: fullPath, - content: btoa(""), - message: `Create ${fullPath}`, - }, - }); - if (!response.ok) throw new Error(response.statusText); - } else { - alert( - "Cannot create empty folder on server. Please create a file inside it.", - ); - return; - } - const contents = await loadDirectoryContents(); - } - - setShowCreateModal(false); - setNewItemName(""); - setCreatePath(""); - } catch (err) { - alert(err instanceof Error ? err.message : "Failed to create item"); - } finally { - setIsCreating(false); - } - }; - - const hasDraft = (filePath: string) => { - const draftKey = `${repoOwner}/${repoName}/${filePath}`; - return !!drafts[draftKey]; - }; - - const mergePending = useCallback( - (nodes: DirectoryNode[] = [], dirPath: string): DirectoryNode[] => { - const pendingInDir = pendingCreates.filter((p) => { - const parentPath = p.path.split("/").slice(0, -1).join("/"); - if (dirPath === "" && !p.path.includes("/")) return true; - return parentPath === dirPath; - }); - - const pendingNodes: DirectoryNode[] = pendingInDir.map((p) => ({ - name: p.path.split("/").pop() || "", - path: p.path, - type: p.type === "folder" ? "dir" : "file", - size: 0, - sha: "", - children: p.type === "folder" ? [] : undefined, - expanded: expandedDirs.has(p.path), - isPending: true, - })); - - // filter out pending nodes that might duplicate existing server nodes - const uniquePending = pendingNodes.filter( - (p) => !nodes.some((n) => n.path === p.path), - ); - - const merged = [...nodes, ...uniquePending]; - - merged.sort((a, b) => { - if (a.type !== b.type) { - return a.type === "dir" ? -1 : 1; - } - return a.name.localeCompare(b.name); - }); - - return merged; - }, - [pendingCreates, expandedDirs], - ); - - const renderTree = ( - nodes: DirectoryNode[], - level: number = 0, - parentPath: string = "", - ): React.ReactNode => { - const nodesToRender = level === 0 ? nodes : mergePending(nodes, parentPath); - - return nodesToRender.map((node) => ( -
-
handleFileClick(node)} - onContextMenu={(e) => handleContextMenu(e, node)} - > - {node.type === "dir" ? ( - - - - ) : ( - - - - )} - - - {node.name} - {node.isPending && " (pending)"} - - - {node.type === "file" && hasDraft(node.path) && ( -
- )} - - {node.type === "dir" && ( -
- - -
- )} -
- - {node.type === "dir" && node.expanded && ( -
{renderTree(node.children || [], level + 1, node.path)}
- )} -
- )); - }; - - if (isLoading) { - return ( -
-
Loading files...
-
- ); - } - - if ( - settingsLoaded && - (!allowedDirectories || allowedDirectories.length === 0) - ) { - return ( -
-
- - - -
-

- No directories configured -

-

- No directory to fetch from. Try updating your settings to include dirs - you want to work with. -

-
- ); - } - - if (error) { - return ( -
-
{error}
-
- ); - } - - return ( -
- - {/* File Tree */} -
- {renderTree(rootContents)} -
- - {/* Context Menu */} - {contextMenu && ( -
- - -
- )} - - {/* Create Modal */} - {showCreateModal && ( -
-
-

- Create New {createType === "file" ? "File" : "Folder"} -

- -
- - setNewItemName(e.target.value)} - placeholder={ - createType === "file" ? "example.md" : "folder-name" - } - className="w-full px-3 py-2 border border-earth-200 rounded-md focus:outline-none focus:ring-2 focus:ring-earth-400 focus:border-transparent" - autoFocus - disabled={isCreating} - onKeyPress={(e) => { - if (e.key === "Enter") { - handleCreate(); - } - }} - /> - {createPath && ( -
- Will be created in: {createPath}/ -
- )} -
- -
- - -
-
-
- )} -
- ); + const [rootContents, setRootContents] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [expandedDirs, setExpandedDirs] = useState>(new Set()); + const [repoConfig, setRepoConfig] = useState([]); + const [settingsLoaded, setSettingsLoaded] = useState(false); + const [showCreateModal, setShowCreateModal] = useState(false); + const [createType, setCreateType] = useState<"file" | "folder">("file"); + const [createPath, setCreatePath] = useState(""); + const [newItemName, setNewItemName] = useState(""); + const [contextMenu, setContextMenu] = useState<{ + x: number; + y: number; + path: string; + type: "file" | "dir"; + } | null>(null); + const [isCreating, setIsCreating] = useState(false); + const [baseImagePath, setBaseImagePath] = useState(""); + + // Load entire file tree recursively on mount + const loadFileTree = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + const response = await (api.projects as any)[projectId].repo[repoOwner][ + repoName + ].tree.$get(); + + if (!response.ok) { + throw new Error(`Failed to load file tree: ${response.statusText}`); + } + + const data = await response.json(); + const { tree, config } = data.data; + + // Set config + setRepoConfig(config || []); + + // Extract base image path from config (first dir with base_image_path or default) + const firstWithImagePath = config?.find( + (c: DirectoryConfig) => c.base_image_path, + ); + if (firstWithImagePath?.base_image_path) { + setBaseImagePath(firstWithImagePath.base_image_path); + } + // Mark all directories as expanded by default + const expandAll = (nodes: DirectoryNode[]): DirectoryNode[] => { + return nodes.map((node) => ({ + ...node, + expanded: true, + children: node.children ? expandAll(node.children) : undefined, + })); + }; + const expandedTree = expandAll(tree || []); + setRootContents(expandedTree); + + // Pre-expand all paths + const collectPaths = (nodes: DirectoryNode[]): string[] => { + return nodes.reduce((acc: string[], node) => { + if (node.type === "dir") { + acc.push(node.path); + if (node.children) { + acc.push(...collectPaths(node.children)); + } + } + return acc; + }, []); + }; + setExpandedDirs(new Set(collectPaths(expandedTree))); + } catch (err) { + console.error("Error loading file tree:", err); + setError(err instanceof Error ? err.message : "Failed to load files"); + } finally { + setIsLoading(false); + setSettingsLoaded(true); + } + }, [projectId, repoOwner, repoName]); + + useEffect(() => { + loadFileTree(); + }, [loadFileTree]); + + // Reload on settings update + useEffect(() => { + const handleSettingsUpdate = () => { + loadFileTree(); + }; + + window.addEventListener("project-settings-updated", handleSettingsUpdate); + return () => { + window.removeEventListener( + "project-settings-updated", + handleSettingsUpdate, + ); + }; + }, [loadFileTree]); + + const toggleDirectory = (dirPath: string) => { + const isExpanded = expandedDirs.has(dirPath); + const newExpandedDirs = new Set(expandedDirs); + + if (isExpanded) { + newExpandedDirs.delete(dirPath); + } else { + newExpandedDirs.add(dirPath); + } + + setExpandedDirs(newExpandedDirs); + + // Update tree expanded state + setRootContents((prevTree) => { + const updateNode = (nodes: DirectoryNode[]): DirectoryNode[] => { + return nodes.map((node) => { + if (node.path === dirPath) { + return { ...node, expanded: !isExpanded }; + } + if (node.children) { + return { ...node, children: updateNode(node.children) }; + } + return node; + }); + }; + return updateNode(prevTree); + }); + }; + + const handleFileClick = (file: DirectoryNode) => { + if (file.type === "dir") { + toggleDirectory(file.path); + } else { + if (selectedFile?.path === file.path) { + return; + } + onFileSelect({ + name: file.name, + path: file.path, + type: "file", + size: file.size, + sha: file.sha, + }); + } + }; + + const handleContextMenu = (e: React.MouseEvent, node: DirectoryNode) => { + e.preventDefault(); + setContextMenu({ + x: e.clientX, + y: e.clientY, + path: node.path, + type: node.type === "dir" ? "dir" : "file", + }); + }; + + useEffect(() => { + const handleClick = () => setContextMenu(null); + document.addEventListener("click", handleClick); + return () => document.removeEventListener("click", handleClick); + }, []); + + const validateFileName = (name: string, convention: string) => { + switch (convention) { + case "kebab-case": + return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name); + case "snake_case": + return /^[a-z0-9]+(_[a-z0-9]+)*$/.test(name); + case "camelCase": + return /^[a-z][a-zA-Z0-9]*$/.test(name); + default: + return true; + } + }; + + const handleCreate = async () => { + if (!newItemName.trim() || isCreating) return; + + setIsCreating(true); + const fullPath = createPath ? `${createPath}/${newItemName}` : newItemName; + + try { + // Find applicable config + const config = repoConfig.find( + (c) => + fullPath.startsWith(c.path + "/") || + fullPath === c.path || + createPath === c.path, + ); + + if (createType === "file" && config?.naming_convention) { + // Remove extension + const nameWithoutExt = newItemName.includes(".") + ? newItemName.split(".").slice(0, -1).join(".") + : newItemName; + + if (!validateFileName(nameWithoutExt, config.naming_convention)) { + throw new Error( + `File name must follow ${config.naming_convention} convention`, + ); + } + } + + if (onFileCreate) { + onFileCreate(fullPath, createType); + } else { + if (createType === "file") { + let initialContent = ""; + if (config?.schema) { + try { + const yaml = await import("yaml"); + const frontmatter: Record = {}; + + Object.entries(config.schema).forEach( + ([key, schema]: [string, any]) => { + // Skip optional fields (required defaults to true if not specified) + if (schema.required === false) { + return; + } + if (schema.type === "date") { + frontmatter[key] = new Date().toISOString().split("T")[0]; + } else if (schema.type === "string") { + let placeholder = ""; + if (schema.min) placeholder = "x".repeat(schema.min); + frontmatter[key] = placeholder || ""; + } else if (schema.type === "boolean") { + frontmatter[key] = false; + } else { + frontmatter[key] = ""; + } + }, + ); + + initialContent = `---\n${yaml.stringify(frontmatter)}---\n\n# New Content\n`; + } catch (e) { + console.warn("Failed to generate frontmatter", e); + } + } + + const response = await (api.projects as any)[projectId].repo[ + repoOwner + ][repoName]["create-file"].$post({ + json: { + path: fullPath, + content: btoa(initialContent), + message: `Create ${fullPath}`, + }, + }); + if (!response.ok) { + const errData = await response.json(); + throw new Error(errData.message || response.statusText); + } + // Reload tree to show new file + loadFileTree(); + } else { + alert( + "Cannot create empty folder on server. Please create a file inside it.", + ); + return; + } + } + + setShowCreateModal(false); + setNewItemName(""); + setCreatePath(""); + } catch (err) { + alert(err instanceof Error ? err.message : "Failed to create item"); + } finally { + setIsCreating(false); + } + }; + + const hasDraft = (filePath: string) => { + const draftKey = `${repoOwner}/${repoName}/${filePath}`; + return !!drafts[draftKey]; + }; + + const mergePending = useCallback( + (nodes: DirectoryNode[] = [], dirPath: string): DirectoryNode[] => { + const pendingInDir = pendingCreates.filter((p) => { + const parentPath = p.path.split("/").slice(0, -1).join("/"); + if (dirPath === "" && !p.path.includes("/")) return true; + return parentPath === dirPath; + }); + + const pendingNodes: DirectoryNode[] = pendingInDir.map((p) => ({ + name: p.path.split("/").pop() || "", + path: p.path, + type: p.type === "folder" ? "dir" : "file", + size: 0, + sha: "", + children: p.type === "folder" ? [] : undefined, + expanded: expandedDirs.has(p.path), + isPending: true, + })); + + // filter out pending nodes that might duplicate existing server nodes + const uniquePending = pendingNodes.filter( + (p) => !nodes.some((n) => n.path === p.path), + ); + + const merged = [...nodes, ...uniquePending]; + + merged.sort((a, b) => { + if (a.type !== b.type) { + return a.type === "dir" ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); + + return merged; + }, + [pendingCreates, expandedDirs], + ); + + const renderTree = ( + nodes: DirectoryNode[], + level: number = 0, + parentPath: string = "", + ): React.ReactNode => { + const nodesToRender = level === 0 ? nodes : mergePending(nodes, parentPath); + + return nodesToRender.map((node) => ( +
+
handleFileClick(node)} + onContextMenu={(e) => handleContextMenu(e, node)} + > + {node.type === "dir" ? ( + + + + ) : ( + + + + )} + + + {node.name} + {node.isPending && " (pending)"} + + + {node.type === "file" && hasDraft(node.path) && ( +
+ )} + + {node.type === "dir" && ( +
+ + +
+ )} +
+ + {node.type === "dir" && + expandedDirs.has(node.path) && + node.children && + node.children.length > 0 && ( +
{renderTree(node.children, level + 1, node.path)}
+ )} +
+ )); + }; + + if (isLoading) { + return ( +
+
Loading files...
+
+ ); + } + + if (settingsLoaded && (!repoConfig || repoConfig.length === 0)) { + return ( +
+
+ + + +
+

+ No directories configured +

+

+ No directory to fetch from. Try updating your settings to include dirs + you want to work with. +

+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + return ( +
+ {/* File Tree */} +
+ {rootContents.length === 0 ? ( +
+ No files found in allowed directories. +
+ ) : ( + renderTree(rootContents) + )} +
+ + {/* Context Menu */} + {contextMenu && ( +
+ + +
+ )} + + {/* Create Modal */} + {showCreateModal && ( +
+
+

+ Create New {createType === "file" ? "File" : "Folder"} +

+ +
+ + setNewItemName(e.target.value)} + placeholder={ + createType === "file" ? "example.md" : "folder-name" + } + className="w-full px-3 py-2 border border-earth-200 rounded-md focus:outline-none focus:ring-2 focus:ring-earth-400 focus:border-transparent" + autoFocus + disabled={isCreating} + onKeyPress={(e) => { + if (e.key === "Enter") { + handleCreate(); + } + }} + /> + {createPath && ( +
+ Will be created in: {createPath}/ +
+ )} +
+ +
+ + +
+
+
+ )} +
+ ); }; export default FileExplorer; - diff --git a/src/components/dashboard/Header.astro b/src/components/dashboard/Header.astro index 2f69b36..3f22636 100644 --- a/src/components/dashboard/Header.astro +++ b/src/components/dashboard/Header.astro @@ -18,14 +18,35 @@ const { currentUser } = Astro.props;
- User pfp + { + currentUser?.pfp ? ( + User pfp + ) : ( +
+ + + +
+ ) + }

= ({ - projectId, - repoOwner, - repoName, + projectId, + repoOwner, + repoName, }) => { - - const computeHash = async (content: string): Promise => { - const msgBuffer = new TextEncoder().encode(content); - const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); - }; - - const getSize = (content: string): number => { - return new TextEncoder().encode(content).length; - }; - - const [selectedFile, setSelectedFile] = useState(null); - const [fileContent, setFileContent] = useState(""); - const [originalHash, setOriginalHash] = useState(""); - const [originalSize, setOriginalSize] = useState(0); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [isCommitPanelOpen, setIsCommitPanelOpen] = useState(false); - const [drafts, setDrafts] = useState>({}); - const [mdxError, setMdxError] = useState(null); - const [isMounted, setIsMounted] = useState(false); - const [editorRef, setEditorRef] = useState(null); - const [showDraftModal, setShowDraftModal] = useState(false); - const [pendingDraft, setPendingDraft] = useState<{ - content: string; - timestamp: number; - filePath: string; - } | null>(null); - const [pendingCreates, setPendingCreates] = useState< - Array<{ path: string; type: "file" | "folder" }> - >([]); - - // Mount tracking for hydration fix - useEffect(() => { - // console.log("[MDXEditorToast] Component mounted", { - // projectId, - // repoOwner, - // repoName, - // }); - setIsMounted(true); - }, [projectId, repoOwner, repoName]); - - - useEffect(() => { - const savedDrafts = localStorage.getItem(`mdx-drafts-${projectId}`); - if (savedDrafts) { - try { - setDrafts(JSON.parse(savedDrafts)); - } catch (e) { - console.error("Failed to load drafts:", e); - } - } - }, [projectId]); - - - useEffect(() => { - if (Object.keys(drafts).length > 0) { - localStorage.setItem(`mdx-drafts-${projectId}`, JSON.stringify(drafts)); - } - }, [drafts, projectId]); - - const loadFileContent = useCallback( - async (file: FileItem) => { - if (file.type === "dir") return; - - const isPending = pendingCreates.some( - (p) => p.path === file.path && p.type === "file", - ); - - if (isPending) { - // for pending files, content is in drafts or empty - const draftKey = `${repoOwner}/${repoName}/${file.path}`; - const content = drafts[draftKey]?.content || "# New File Content"; - setFileContent(content); - // for new files, original is empty - setOriginalHash(await computeHash("")); - setOriginalSize(0); - setSelectedFile(file); - setHasUnsavedChanges(!!content); - return; - } - - setIsLoading(true); - setError(null); - - try { - const response = await (api.projects as any)[projectId].repo[repoOwner][ - repoName - ].file.$get({ - query: { - path: file.path, - }, - }); - - if (!response.ok) { - throw new Error(`Failed to load file: ${response.statusText}`); - } - - const data = await response.json(); - const content = data.data.content || ""; - - setFileContent(content); - - // compute hash and size for original content - const hash = await computeHash(content); - const size = getSize(content); - setOriginalHash(hash); - setOriginalSize(size); - - setSelectedFile(file); - setHasUnsavedChanges(false); - - // check if there's a draft for this file - const draftKey = `${repoOwner}/${repoName}/${file.path}`; - if (drafts[draftKey]) { - const draft = drafts[draftKey]; - if (draft.content !== content) { - setPendingDraft(draft); - setShowDraftModal(true); - } - } - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load file"); - } finally { - setIsLoading(false); - } - }, - [projectId, repoOwner, repoName, drafts, pendingCreates], - ); - - const handleContentChange = useCallback((content: string) => { - setFileContent(content); - }, []); - - // async change detection using hash and size - useEffect(() => { - const checkChanges = async () => { - if (!selectedFile) return; - - // metadata check (size) - const currentSize = getSize(fileContent); - if (currentSize !== originalSize) { - setHasUnsavedChanges(true); - return; - } - - // cryptographic hash check - const currentHash = await computeHash(fileContent); - setHasUnsavedChanges(currentHash !== originalHash); - }; - - checkChanges(); - }, [fileContent, originalSize, originalHash, selectedFile]); - - // auto-save draft - useEffect(() => { - if (hasUnsavedChanges && selectedFile) { - const timeoutId = setTimeout(() => { - saveDraft(); - }, 2000); // auto-save after 2 seconds of inactivity - - return () => clearTimeout(timeoutId); - } - }, [fileContent, hasUnsavedChanges, selectedFile]); - - // Save draft manually - const saveDraft = useCallback(() => { - if (!selectedFile) return; - - const draftKey = `${repoOwner}/${repoName}/${selectedFile.path}`; - const newDrafts = { - ...drafts, - [draftKey]: { - content: fileContent, - timestamp: Date.now(), - filePath: selectedFile.path, - }, - }; - setDrafts(newDrafts); - }, [drafts, fileContent, repoOwner, repoName, selectedFile]); - - - const handleFileCreate = useCallback( - (path: string, type: "file" | "folder") => { - setPendingCreates((prev) => [...prev, { path, type }]); - - if (type === "file") { - const newFile: FileItem = { - name: path.split("/").pop() || "", - path, - type: "file", - size: 0, - sha: "", - }; - - const draftKey = `${repoOwner}/${repoName}/${path}`; - setDrafts((prev) => ({ - ...prev, - [draftKey]: { - content: "# New File Content", - timestamp: Date.now(), - filePath: path, - }, - })); - setSelectedFile(newFile); - setFileContent("# New File Content"); - - computeHash("").then((hash) => { - setOriginalHash(hash); - setOriginalSize(0); - }); - setHasUnsavedChanges(true); // treat new file as having changes so it can be committed - } - }, - [repoOwner, repoName], - ); - - const handleCommit = useCallback( - async ( - commitMessage: string, - filesToCommit: { - path: string; - content: string; - }[], - ) => { - try { - - const allFilesToCommit = [...filesToCommit]; - - pendingCreates.forEach((item) => { - if (item.type === "file") { - // ensure pending files are included even if empty/unchanged - // (though usually they'd be in filesToCommit via getUnsavedFiles) - const exists = allFilesToCommit.some((f) => f.path === item.path); - if (!exists) { - const draftKey = `${repoOwner}/${repoName}/${item.path}`; - const content = drafts[draftKey]?.content || ""; - allFilesToCommit.push({ - path: item.path, - content, - }); - } - } - }); - - if (allFilesToCommit.length === 0) { - alert("No changes to commit."); - return; - } - - const response = await (api.projects as any)[projectId].repo[repoOwner][ - repoName - ]["bulk-update"].$post({ - json: { - files: allFilesToCommit, - message: commitMessage, - }, - }); - - if (!response.ok) { - throw new Error(`Failed to commit changes: ${response.statusText}`); - } - - allFilesToCommit.forEach(async (file) => { - try { - await actions.projectsActions.logActivity({ - projectId, - actionType: "commit", - filePath: file.path, - fileName: file.path.split("/").pop() || "", - fileSize: getSize(file.content), - changesSummary: commitMessage, - }); - } catch (error) { - console.error("Failed to log activity:", error); - } - }); - - const newDrafts = { ...drafts }; - allFilesToCommit.forEach((file) => { - const draftKey = `${repoOwner}/${repoName}/${file.path}`; - delete newDrafts[draftKey]; - }); - setDrafts(newDrafts); - setPendingCreates([]); - - if ( - selectedFile && - allFilesToCommit.some((f) => f.path === selectedFile.path) - ) { - // update original hash/size to match committed content - const newHash = await computeHash(fileContent); - const newSize = getSize(fileContent); - setOriginalHash(newHash); - setOriginalSize(newSize); - setHasUnsavedChanges(false); - } - - setIsCommitPanelOpen(false); - alert("Changes committed successfully!"); - - // trigger reload in FileExplorer (via window event as implemented before) - could be improved - window.dispatchEvent(new Event("project-settings-updated")); - } catch (err) { - alert(err instanceof Error ? err.message : "Failed to commit changes"); - } - }, - [ - projectId, - repoOwner, - repoName, - drafts, - selectedFile, - fileContent, - pendingCreates, - ], - ); - - - const getUnsavedFiles = useCallback(() => { - const unsavedFiles: { - path: string; - content: string; - }[] = []; - - if (hasUnsavedChanges && selectedFile) { - unsavedFiles.push({ - path: selectedFile.path, - content: fileContent, - }); - } - - Object.entries(drafts).forEach(([draftKey, draft]) => { - const filePath = draft.filePath; - if (!selectedFile || filePath !== selectedFile.path) { - unsavedFiles.push({ - path: filePath, - content: draft.content, - }); - } - }); - - return unsavedFiles; - }, [hasUnsavedChanges, selectedFile, fileContent, drafts]); - - return ( -

- -
-
-

Files

-
- {repoOwner}/{repoName} -
-
- -
- - {/* Editor Area */} -
- {/* Editor Header */} -
-
- {selectedFile ? ( -
-

- {selectedFile.name} -

-
- {selectedFile.path} -
-
- ) : ( -
Select a file to edit
- )} - {hasUnsavedChanges && ( -
-
- Unsaved changes -
- )} -
-
- {hasUnsavedChanges && ( - - )} - {Object.keys(drafts).length > 0 && ( - - )} - -
-
- - {/* Editor Content */} -
- {error && ( -
-
{error}
-
- )} - - {mdxError && ( -
-
- MDX Parsing Error: {mdxError} -
- - You can fix the errors in source mode and switch to rich - text mode when you are ready. - -
- - -
-
-
-
- )} - - {isLoading ? ( -
-
Loading...
-
- ) : selectedFile ? ( -
- {isMounted ? ( - // Toast UI Editor -
- { - if (editorRef) { - const content = editorRef.getInstance().getMarkdown(); - handleContentChange(content); - } - }} - toolbarItems={[ - ["heading", "bold", "italic", "strike"], - ["hr", "quote"], - ["ul", "ol", "task", "indent", "outdent"], - ["table", "image", "link"], - ["code", "codeblock"], - ["scrollSync"], - ]} - /> -
- ) : ( - // Loading state during hydration -
-
Loading editor...
-
- )} -
- ) : ( -
-
- - - -

No file selected

-

Choose a file from the explorer to start editing

-
-
- )} -
-
- - {/* Commit Panel */} - {isCommitPanelOpen && ( - setIsCommitPanelOpen(false)} - /> - )} - - {/* Draft Restore Modal */} - {showDraftModal && pendingDraft && ( -
-
-

- Unsaved Changes Found -

-

- You have unsaved changes for this file from{" "} - - {new Date(pendingDraft.timestamp).toLocaleString()} - - . Would you like to restore them? -

-
- - -
-
-
- )} -
- ); + const computeHash = async (content: string): Promise => { + const msgBuffer = new TextEncoder().encode(content); + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); + }; + + const getSize = (content: string): number => { + return new TextEncoder().encode(content).length; + }; + + const [selectedFile, setSelectedFile] = useState(null); + const [fileContent, setFileContent] = useState(""); + const [originalHash, setOriginalHash] = useState(""); + const [originalSize, setOriginalSize] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [isCommitPanelOpen, setIsCommitPanelOpen] = useState(false); + const [drafts, setDrafts] = useState>({}); + const [mdxError, setMdxError] = useState(null); + const [repoConfig, setRepoConfig] = useState([]); + const [schemaErrors, setSchemaErrors] = useState([]); + const [isMounted, setIsMounted] = useState(false); + const [EditorComponent, setEditorComponent] = useState(null); + const [editorRef, setEditorRef] = useState(null); + const validationTimeoutRef = useRef(null); + const [showDraftModal, setShowDraftModal] = useState(false); + const [pendingDraft, setPendingDraft] = useState<{ + content: string; + timestamp: number; + filePath: string; + } | null>(null); + const [pendingCreates, setPendingCreates] = useState< + Array<{ path: string; type: "file" | "folder" }> + >([]); + const [mediaPath, setMediaPath] = useState(""); + const [showMediaPanel, setShowMediaPanel] = useState(false); + + // Fetch media base path from config + useEffect(() => { + const fetchMediaPath = async () => { + try { + const response = await (api.projects as any)[projectId].repo[repoOwner][ + repoName + ].config.$get(); + if (response.ok) { + const data = await response.json(); + const config = data.data; + const firstWithImagePath = config.find((c: any) => c.base_image_path); + if (firstWithImagePath?.base_image_path) { + setMediaPath(firstWithImagePath.base_image_path); + } + } + } catch (err) { + console.error("Failed to fetch media path:", err); + } + }; + fetchMediaPath(); + }, [projectId, repoOwner, repoName]); + + const insertImageMarkdown = useCallback( + (imageUrl: string) => { + if (!editorRef) return; + + const imageMarkdown = `\n![Image](${imageUrl})\n`; + const currentContent = fileContent; + const newContent = currentContent + imageMarkdown; + setFileContent(newContent); + setHasUnsavedChanges(true); + }, + [editorRef, fileContent], + ); + + useEffect(() => { + const savedDrafts = localStorage.getItem(`mdx-drafts-${projectId}`); + if (savedDrafts) { + try { + setDrafts(JSON.parse(savedDrafts)); + } catch (e) { + console.error("Failed to load drafts:", e); + } + } + }, [projectId]); + + useEffect(() => { + if (Object.keys(drafts).length > 0) { + localStorage.setItem(`mdx-drafts-${projectId}`, JSON.stringify(drafts)); + } + }, [drafts, projectId]); + + // Fetch repo config for validation + useEffect(() => { + const fetchConfig = async () => { + try { + const response = await (api.projects as any)[projectId].repo[repoOwner][ + repoName + ].config.$get(); + if (response.ok) { + const data = await response.json(); + setRepoConfig(data.data); + } + } catch (err) { + console.error("Failed to fetch repo config:", err); + } + }; + fetchConfig(); + }, [projectId, repoOwner, repoName]); + + const validateContent = useCallback( + async (content: string, path: string) => { + const config = repoConfig.find( + (c) => path.startsWith(c.path + "/") || path === c.path, + ); + if (!config?.schema) { + setSchemaErrors([]); + return; + } + + const errors: string[] = []; + const match = content.match(/^---\n([\s\S]*?)\n---/); + + if (!match) { + errors.push( + "Missing frontmatter (---) correctly positioned at the start.", + ); + setSchemaErrors(errors); + return; + } + + try { + const YAML = (await import("yaml")).default; + const frontmatter = YAML.parse(match[1]); + + Object.entries(config.schema).forEach( + ([key, schema]: [string, any]) => { + const value = frontmatter[key]; + + // Only validate required fields (required defaults to true if not specified) + if ( + schema.required !== false && + (value === undefined || value === null || value === "") + ) { + errors.push(`Field "${key}" is required.`); + return; + } + + // Skip further validation if field is empty and not required + if ( + schema.required === false && + (value === undefined || value === null || value === "") + ) { + return; + } + + if (schema.type === "string") { + const strValue = String(value); + if (schema.min && strValue.length < schema.min) { + errors.push( + `"${key}" must be at least ${schema.min} characters.`, + ); + } + if (schema.max && strValue.length > schema.max) { + errors.push( + `"${key}" must be at most ${schema.max} characters.`, + ); + } + } else if (schema.type === "date") { + const date = new Date(value); + if (isNaN(date.getTime())) { + errors.push(`"${key}" must be a valid date.`); + } + if (schema.format) { + // Simple format check (e.g. YYYY-MM-DD vs YYYY/MM/DD) + if ( + schema.format === "YYYY-MM-DD" && + !/^\d{4}-\d{2}-\d{2}$/.test(value) + ) { + errors.push(`"${key}" must be in YYYY-MM-DD format.`); + } + } + } + }, + ); + } catch (e) { + errors.push("Invalid YAML in frontmatter."); + } + + setSchemaErrors(errors); + }, + [repoConfig], + ); + + // Mount tracking and dynamic editor loading + useEffect(() => { + setIsMounted(true); + + const loadEditor = async () => { + try { + const [editorModule] = await Promise.all([ + import("@toast-ui/react-editor"), + import("@toast-ui/editor/dist/toastui-editor.css"), + ]); + setEditorComponent(() => editorModule.Editor); + } catch (err) { + console.error("Failed to load editor modules:", err); + setError("Failed to load editor component"); + } + }; + + loadEditor(); + }, [projectId, repoOwner, repoName]); + + useEffect(() => { + if (selectedFile) { + if (validationTimeoutRef.current) { + clearTimeout(validationTimeoutRef.current); + } + + validationTimeoutRef.current = setTimeout(() => { + validateContent(fileContent, selectedFile.path); + }, 500); // 500ms debounce + } + + return () => { + if (validationTimeoutRef.current) { + clearTimeout(validationTimeoutRef.current); + } + }; + }, [fileContent, selectedFile, validateContent]); + + const loadFileContent = useCallback( + async (file: FileItem) => { + if (file.type === "dir") return; + + const isPending = pendingCreates.some( + (p) => p.path === file.path && p.type === "file", + ); + + if (isPending) { + // for pending files, content is in drafts or empty + const draftKey = `${repoOwner}/${repoName}/${file.path}`; + const content = drafts[draftKey]?.content || "# New File Content"; + setFileContent(content); + // for new files, original is empty + setOriginalHash(await computeHash("")); + setOriginalSize(0); + setSelectedFile(file); + setHasUnsavedChanges(!!content); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await (api.projects as any)[projectId].repo[repoOwner][ + repoName + ].file.$get({ + query: { + path: file.path, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to load file: ${response.statusText}`); + } + + const data = await response.json(); + const content = data.data.content || ""; + + setFileContent(content); + + // compute hash and size for original content + const hash = await computeHash(content); + const size = getSize(content); + setOriginalHash(hash); + setOriginalSize(size); + + setSelectedFile(file); + setHasUnsavedChanges(false); + + // check if there's a draft for this file + const draftKey = `${repoOwner}/${repoName}/${file.path}`; + if (drafts[draftKey]) { + const draft = drafts[draftKey]; + if (draft.content !== content) { + setPendingDraft(draft); + setShowDraftModal(true); + } + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load file"); + } finally { + setIsLoading(false); + } + }, + [projectId, repoOwner, repoName, drafts, pendingCreates], + ); + + const handleContentChange = useCallback((content: string) => { + setFileContent(content); + }, []); + + // async change detection using hash and size + useEffect(() => { + const checkChanges = async () => { + if (!selectedFile) return; + + // metadata check (size) + const currentSize = getSize(fileContent); + if (currentSize !== originalSize) { + setHasUnsavedChanges(true); + return; + } + + // cryptographic hash check + const currentHash = await computeHash(fileContent); + setHasUnsavedChanges(currentHash !== originalHash); + }; + + checkChanges(); + }, [fileContent, originalSize, originalHash, selectedFile]); + + // auto-save draft + useEffect(() => { + if (hasUnsavedChanges && selectedFile) { + const timeoutId = setTimeout(() => { + saveDraft(); + }, 2000); // auto-save after 2 seconds of inactivity + + return () => clearTimeout(timeoutId); + } + }, [fileContent, hasUnsavedChanges, selectedFile]); + + // Save draft manually + const saveDraft = useCallback(() => { + if (!selectedFile) return; + + const draftKey = `${repoOwner}/${repoName}/${selectedFile.path}`; + const newDrafts = { + ...drafts, + [draftKey]: { + content: fileContent, + timestamp: Date.now(), + filePath: selectedFile.path, + }, + }; + setDrafts(newDrafts); + }, [drafts, fileContent, repoOwner, repoName, selectedFile]); + + const handleFileCreate = useCallback( + (path: string, type: "file" | "folder") => { + setPendingCreates((prev) => [...prev, { path, type }]); + + if (type === "file") { + const newFile: FileItem = { + name: path.split("/").pop() || "", + path, + type: "file", + size: 0, + sha: "", + }; + + const draftKey = `${repoOwner}/${repoName}/${path}`; + setDrafts((prev) => ({ + ...prev, + [draftKey]: { + content: "# New File Content", + timestamp: Date.now(), + filePath: path, + }, + })); + setSelectedFile(newFile); + setFileContent("# New File Content"); + + computeHash("").then((hash) => { + setOriginalHash(hash); + setOriginalSize(0); + }); + setHasUnsavedChanges(true); // treat new file as having changes so it can be committed + } + }, + [repoOwner, repoName], + ); + + const handleCommit = useCallback( + async ( + commitMessage: string, + filesToCommit: { + path: string; + content: string; + }[], + ) => { + try { + if (schemaErrors.length > 0) { + alert("Please fix schema errors before committing."); + return; + } + + const allFilesToCommit = [...filesToCommit]; + + pendingCreates.forEach((item) => { + if (item.type === "file") { + // ensure pending files are included even if empty/unchanged + // (though usually they'd be in filesToCommit via getUnsavedFiles) + const exists = allFilesToCommit.some((f) => f.path === item.path); + if (!exists) { + const draftKey = `${repoOwner}/${repoName}/${item.path}`; + const content = drafts[draftKey]?.content || ""; + allFilesToCommit.push({ + path: item.path, + content, + }); + } + } + }); + + if (allFilesToCommit.length === 0) { + alert("No changes to commit."); + return; + } + + const response = await (api.projects as any)[projectId].repo[repoOwner][ + repoName + ]["bulk-update"].$post({ + json: { + files: allFilesToCommit, + message: commitMessage, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to commit changes: ${response.statusText}`); + } + + allFilesToCommit.forEach(async (file) => { + try { + await actions.projectsActions.logActivity({ + projectId, + actionType: "commit", + filePath: file.path, + fileName: file.path.split("/").pop() || "", + fileSize: getSize(file.content), + changesSummary: commitMessage, + }); + } catch (error) { + console.error("Failed to log activity:", error); + } + }); + + const newDrafts = { ...drafts }; + allFilesToCommit.forEach((file) => { + const draftKey = `${repoOwner}/${repoName}/${file.path}`; + delete newDrafts[draftKey]; + }); + setDrafts(newDrafts); + setPendingCreates([]); + + if ( + selectedFile && + allFilesToCommit.some((f) => f.path === selectedFile.path) + ) { + // update original hash/size to match committed content + const newHash = await computeHash(fileContent); + const newSize = getSize(fileContent); + setOriginalHash(newHash); + setOriginalSize(newSize); + setHasUnsavedChanges(false); + } + + setIsCommitPanelOpen(false); + alert("Changes committed successfully!"); + + // trigger reload in FileExplorer (via window event as implemented before) - could be improved + window.dispatchEvent(new Event("project-settings-updated")); + } catch (err) { + alert(err instanceof Error ? err.message : "Failed to commit changes"); + } + }, + [ + projectId, + repoOwner, + repoName, + drafts, + selectedFile, + fileContent, + pendingCreates, + schemaErrors, + ], + ); + + const getUnsavedFiles = useCallback(() => { + const unsavedFiles: { + path: string; + content: string; + }[] = []; + + if (hasUnsavedChanges && selectedFile) { + unsavedFiles.push({ + path: selectedFile.path, + content: fileContent, + }); + } + + Object.entries(drafts).forEach(([draftKey, draft]) => { + const filePath = draft.filePath; + if (!selectedFile || filePath !== selectedFile.path) { + unsavedFiles.push({ + path: filePath, + content: draft.content, + }); + } + }); + + return unsavedFiles; + }, [hasUnsavedChanges, selectedFile, fileContent, drafts]); + + return ( +
+
+
+

Files

+
+ {repoOwner}/{repoName} +
+
+ +
+ + {/* Media Panel */} + {showMediaPanel && mediaPath && ( +
+ +
+ )} + + {/* Editor Area */} +
+ {/* Editor Header */} +
+
+ {selectedFile ? ( +
+

+ {selectedFile.name} +

+
+ {selectedFile.path} +
+
+ ) : ( +
Select a file to edit
+ )} + {hasUnsavedChanges && ( +
+
+ Unsaved changes +
+ )} + {schemaErrors.length > 0 && ( +
+ + + + + Schema Mismatch + +
+ )} +
+
+ {hasUnsavedChanges && ( + + )} + {Object.keys(drafts).length > 0 && ( + + )} + + {/* Media Panel Toggle */} + +
+
+ + {/* Editor Content */} +
+ {error && ( +
+
{error}
+
+ )} + + {schemaErrors.length > 0 && ( +
+
+ Schema Errors: +
+
    + {schemaErrors.map((err, i) => ( +
  • + {err} +
  • + ))} +
+
+ )} + + {mdxError && ( +
+
+ MDX Parsing Error: {mdxError} +
+ + You can fix the errors in source mode and switch to rich + text mode when you are ready. + +
+ + +
+
+
+
+ )} + + {isLoading ? ( +
+ + + + +
+ Loading file content... +
+
+ ) : selectedFile ? ( +
+ {isMounted && EditorComponent ? ( + // Toast UI Editor +
+ { + if (editorRef) { + const content = editorRef.getInstance().getMarkdown(); + handleContentChange(content); + } + }} + toolbarItems={[ + ["heading", "bold", "italic", "strike"], + ["hr", "quote"], + ["ul", "ol", "task", "indent", "outdent"], + ["table", "image", "link"], + ["code", "codeblock"], + ["scrollSync"], + ]} + /> +
+ ) : ( + // Loading state during hydration +
+ + + + +
+ Preparing editor... +
+
+ )} +
+ ) : ( +
+
+ + + +

No file selected

+

Choose a file from the explorer to start editing

+
+
+ )} +
+
+ + {/* Commit Panel */} + {isCommitPanelOpen && ( + setIsCommitPanelOpen(false)} + /> + )} + + {/* Draft Restore Modal */} + {showDraftModal && pendingDraft && ( +
+
+

+ Unsaved Changes Found +

+

+ You have unsaved changes for this file from{" "} + + {new Date(pendingDraft.timestamp).toLocaleString()} + + . Would you like to restore them? +

+
+ + +
+
+
+ )} +
+ ); }; export default MDXEditorToast; diff --git a/src/components/dashboard/MediaManager.tsx b/src/components/dashboard/MediaManager.tsx new file mode 100644 index 0000000..15f921f --- /dev/null +++ b/src/components/dashboard/MediaManager.tsx @@ -0,0 +1,398 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import api from "@/lib/clients"; +import { + optimizeImage, + blobToBase64, + generateImageFilename, + isImageFile, + formatFileSize, + getOptimalFormat, +} from "@/utils/imageOptimization"; + +interface MediaFile { + name: string; + path: string; + size: number; + sha: string; + rawUrl: string; + githubUrl: string; +} + +interface MediaManagerProps { + projectId: string; + repoOwner: string; + repoName: string; + mediaPath?: string; + onImageSelect?: (imageUrl: string) => void; +} + +const MediaManager: React.FC = ({ + projectId, + repoOwner, + repoName, + mediaPath, + onImageSelect, +}) => { + const [files, setFiles] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [uploading, setUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [selectedImage, setSelectedImage] = useState(null); + const [currentMediaPath, setCurrentMediaPath] = useState( + mediaPath + ); + const fileInputRef = useRef(null); + + // Fetch media files from the media path + const fetchMediaFiles = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + const query = currentMediaPath ? `?path=${encodeURIComponent(currentMediaPath)}` : ""; + const response = await (api.projects as any)[projectId].repo[ + repoOwner + ][repoName].media.$get({ query }); + + if (!response.ok) { + throw new Error(`Failed to load media: ${response.statusText}`); + } + + const data = await response.json(); + setFiles(data.data.files || []); + if (data.data.path) { + setCurrentMediaPath(data.data.path); + } + } catch (err) { + console.error("Error loading media:", err); + setError(err instanceof Error ? err.message : "Failed to load media files"); + } finally { + setIsLoading(false); + } + }, [projectId, repoOwner, repoName, currentMediaPath]); + + useEffect(() => { + fetchMediaFiles(); + }, [fetchMediaFiles]); + + // Drag and drop handlers + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDrop = useCallback( + async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const droppedFiles = Array.from(e.dataTransfer.files).filter(isImageFile); + if (droppedFiles.length > 0) { + await uploadFiles(droppedFiles); + } + }, + [projectId, repoOwner, repoName, currentMediaPath] + ); + + // File upload handler + const uploadFiles = async (filesToUpload: File[]) => { + if (!currentMediaPath) { + setError("No media path configured"); + return; + } + + setUploading(true); + setUploadProgress(0); + + try { + const totalFiles = filesToUpload.length; + const uploadedFiles = []; + + for (let i = 0; i < filesToUpload.length; i++) { + const file = filesToUpload[i]; + + // Optimize image + const format = getOptimalFormat(file); + const optimized = await optimizeImage(file, { + maxWidth: 1920, + maxHeight: 1080, + quality: 0.85, + format, + }); + + // Convert to base64 + const base64Content = await blobToBase64(optimized); + + // Generate filename + const filename = generateImageFilename(file.name); + + uploadedFiles.push({ + filename, + content: base64Content, + }); + + setUploadProgress(Math.round(((i + 1) / totalFiles) * 100)); + } + + // Upload to server + const response = await (api.projects as any)[projectId].repo[ + repoOwner + ][repoName]["media/upload"].$post({ + json: { + files: uploadedFiles, + mediaPath: currentMediaPath, + }, + }); + + if (!response.ok) { + throw new Error(`Upload failed: ${response.statusText}`); + } + + // Refresh file list + await fetchMediaFiles(); + } catch (err) { + console.error("Upload error:", err); + setError(err instanceof Error ? err.message : "Upload failed"); + } finally { + setUploading(false); + setUploadProgress(0); + } + }; + + // Handle file input change + const handleFileInputChange = async ( + e: React.ChangeEvent + ) => { + const selectedFiles = Array.from(e.target.files || []).filter(isImageFile); + if (selectedFiles.length > 0) { + await uploadFiles(selectedFiles); + } + // Reset input + e.target.value = ""; + }; + + // Handle toolbar button click + const handleToolbarClick = () => { + fileInputRef.current?.click(); + }; + + // Handle image click + const handleImageClick = (file: MediaFile) => { + setSelectedImage(file); + if (onImageSelect) { + onImageSelect(file.rawUrl); + } + }; + + // Copy image URL to clipboard + const copyImageUrl = (url: string) => { + navigator.clipboard.writeText(url); + }; + + if (isLoading) { + return ( +
+ Loading media... +
+ ); + } + + if (error) { + return ( +
+

{error}

+ +
+ ); + } + + return ( +
+ {/* Header */} +
+

Media Library

+
+ {files.length} images + +
+
+ + {/* Upload Progress */} + {uploading && ( +
+
+ Uploading... + {uploadProgress}% +
+
+
+
+
+ )} + + {/* Drag Overlay */} + {isDragging && ( +
+
+ + + +

Drop images here

+
+
+ )} + + {/* File Grid */} +
+ {files.length === 0 ? ( +
+ + + +

No images yet

+

+ Drag and drop images here or click the + button +

+
+ ) : ( +
+ {files.map((file) => ( +
handleImageClick(file)} + > + {file.name} + + {/* Hover Overlay */} +
+ +
+ + {/* Filename on hover */} +
+

{file.name}

+

+ {formatFileSize(file.size)} +

+
+
+ ))} +
+ )} +
+ + {/* Hidden File Input */} + +
+ ); +}; + +export default MediaManager; diff --git a/src/components/dashboard/ProjectSettingsModal.tsx b/src/components/dashboard/ProjectSettingsModal.tsx index 7cf3a33..f3cc1a9 100644 --- a/src/components/dashboard/ProjectSettingsModal.tsx +++ b/src/components/dashboard/ProjectSettingsModal.tsx @@ -1,234 +1,364 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import api from "@/lib/clients"; import Collaborators from "./Collaborators"; +interface DirectoryConfig { + path: string; + schema?: Record; + naming_convention?: string; + base_image_path?: string; +} + interface ProjectSettingsModalProps { - projectId: string; + projectId: string; } export default function ProjectSettingsModal({ - projectId, + projectId, }: ProjectSettingsModalProps) { - const [isOpen, setIsOpen] = useState(false); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [allowedDirs, setAllowedDirs] = useState(""); - const [settings, setSettings] = useState(null); - const [activeTab, setActiveTab] = useState<"general" | "collaborators">("general"); - - const fetchSettings = async () => { - setLoading(true); - try { - const res = await (api.projects as any)[projectId].settings.$get(); - if (res.ok) { - const data = await res.json(); - setSettings(data.data); - const dirs = JSON.parse(data.data.public_directories || "[]"); - setAllowedDirs(dirs.join("\n")); - } - } catch (e) { - console.error("Failed to fetch settings", e); - } finally { - setLoading(false); - } - }; - - const handleOpen = () => { - setIsOpen(true); - setActiveTab("general"); - fetchSettings(); - }; - - const handleSave = async () => { - setSaving(true); - try { - const dirs = allowedDirs - .split("\n") - .map((d) => d.trim()) - .filter((d) => d); - - const payload = { - public_directories: JSON.stringify(dirs), - allow_file_creation: settings?.allow_file_creation ?? false, - allow_file_editing: settings?.allow_file_editing ?? true, - allow_file_deletion: settings?.allow_file_deletion ?? false, - require_approval: settings?.require_approval ?? true, - auto_merge: settings?.auto_merge ?? false, - max_file_size: settings?.max_file_size ?? 1048576, - allowed_extensions: - settings?.allowed_extensions ?? - JSON.stringify([".md"]), - collaborator_message: settings?.collaborator_message ?? "", - }; - - const res = await (api.projects as any)[projectId].settings.$put({ - json: payload, - }); - - if (res.ok) { - setIsOpen(false); - window.dispatchEvent(new Event("project-settings-updated")); - } else { - alert("Failed to save settings"); - } - } catch (e) { - console.error("Failed to save settings", e); - alert("An error occurred while saving"); - } finally { - setSaving(false); - } - }; - - return ( - <> - - - {isOpen && ( -
-
-
-

- Project Settings -

- -
- - {/* Tabs */} -
- - -
- - {loading ? ( -
-
Loading settings...
-
- ) : ( -
- {activeTab === "general" && ( -
-
- -

- Enter one directory path per line (e.g.,{" "} - content/blog). Only files in these - directories will be accessible. -

-