Compare commits
22 Commits
4d0ee8e578
...
ae0f84fde7
| Author | SHA1 | Date |
|---|---|---|
|
|
ae0f84fde7 | |
|
|
7270d72fa4 | |
|
|
718180fd35 | |
|
|
e9916d9897 | |
|
|
3453ba7492 | |
|
|
1ad3c183a7 | |
|
|
64d67b9aed | |
|
|
9082e15949 | |
|
|
200b998485 | |
|
|
4355601bdb | |
|
|
09327c2410 | |
|
|
91b627907b | |
|
|
77c85deb1d | |
|
|
99e2d5aa15 | |
|
|
9d46aa744d | |
|
|
e31a0b7bae | |
|
|
037c67849a | |
|
|
9aa2aa48f0 | |
|
|
e530606cef | |
|
|
25627ddb0f | |
|
|
c314ae7b8c | |
|
|
db742e7fe8 |
|
|
@ -1,18 +1,20 @@
|
|||
{
|
||||
"name": "webvnwrite-temp",
|
||||
"name": "webvnwrite",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "webvnwrite-temp",
|
||||
"name": "webvnwrite",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@supabase/ssr": "^0.8.0",
|
||||
"@supabase/supabase-js": "^2.91.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "16.1.4",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"react-dom": "19.2.3",
|
||||
"reactflow": "^11.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
|
@ -1228,6 +1230,108 @@
|
|||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/background": {
|
||||
"version": "11.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
|
||||
"integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/controls": {
|
||||
"version": "11.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
|
||||
"integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/core": {
|
||||
"version": "11.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
|
||||
"integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-drag": "^3.0.1",
|
||||
"@types/d3-selection": "^3.0.3",
|
||||
"@types/d3-zoom": "^3.0.1",
|
||||
"classcat": "^5.0.3",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/minimap": {
|
||||
"version": "11.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
|
||||
"integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"@types/d3-selection": "^3.0.3",
|
||||
"@types/d3-zoom": "^3.0.1",
|
||||
"classcat": "^5.0.3",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/node-resizer": {
|
||||
"version": "2.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
|
||||
"integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.4",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/node-toolbar": {
|
||||
"version": "1.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
|
||||
"integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
|
|
@ -1618,6 +1722,259 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/d3-axis": "*",
|
||||
"@types/d3-brush": "*",
|
||||
"@types/d3-chord": "*",
|
||||
"@types/d3-color": "*",
|
||||
"@types/d3-contour": "*",
|
||||
"@types/d3-delaunay": "*",
|
||||
"@types/d3-dispatch": "*",
|
||||
"@types/d3-drag": "*",
|
||||
"@types/d3-dsv": "*",
|
||||
"@types/d3-ease": "*",
|
||||
"@types/d3-fetch": "*",
|
||||
"@types/d3-force": "*",
|
||||
"@types/d3-format": "*",
|
||||
"@types/d3-geo": "*",
|
||||
"@types/d3-hierarchy": "*",
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-path": "*",
|
||||
"@types/d3-polygon": "*",
|
||||
"@types/d3-quadtree": "*",
|
||||
"@types/d3-random": "*",
|
||||
"@types/d3-scale": "*",
|
||||
"@types/d3-scale-chromatic": "*",
|
||||
"@types/d3-selection": "*",
|
||||
"@types/d3-shape": "*",
|
||||
"@types/d3-time": "*",
|
||||
"@types/d3-time-format": "*",
|
||||
"@types/d3-timer": "*",
|
||||
"@types/d3-transition": "*",
|
||||
"@types/d3-zoom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-axis": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
||||
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-brush": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
||||
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-chord": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
||||
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-contour": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
||||
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-delaunay": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dispatch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-dsv": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-fetch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-dsv": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-force": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-format": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-geo": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-hierarchy": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-polygon": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
||||
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-quadtree": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-random": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
||||
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-time-format": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
||||
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
|
@ -1625,6 +1982,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
|
|
@ -1658,7 +2021,7 @@
|
|||
"version": "19.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
||||
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
|
|
@ -2678,6 +3041,12 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
|
|
@ -2750,9 +3119,114 @@
|
|||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
|
|
@ -5038,9 +5512,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -5049,10 +5523,10 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
|
|
@ -5131,6 +5605,24 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
|
|
@ -5455,6 +5947,25 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss/node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
@ -5536,6 +6047,24 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reactflow": {
|
||||
"version": "11.11.4",
|
||||
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
|
||||
"integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/background": "11.3.14",
|
||||
"@reactflow/controls": "11.2.14",
|
||||
"@reactflow/core": "11.11.4",
|
||||
"@reactflow/minimap": "11.7.14",
|
||||
"@reactflow/node-resizer": "2.2.14",
|
||||
"@reactflow/node-toolbar": "1.3.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
|
|
@ -6505,6 +7034,15 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
@ -6683,6 +7221,34 @@
|
|||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@
|
|||
"dependencies": {
|
||||
"@supabase/ssr": "^0.8.0",
|
||||
"@supabase/supabase-js": "^2.91.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "16.1.4",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"react-dom": "19.2.3",
|
||||
"reactflow": "^11.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
|
|
|||
22
prd.json
22
prd.json
|
|
@ -301,7 +301,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 17,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -318,7 +318,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 18,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -335,7 +335,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 19,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -354,7 +354,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 20,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -371,7 +371,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 21,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -390,7 +390,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 22,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -408,7 +408,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 23,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -427,7 +427,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 24,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -447,7 +447,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 25,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -465,7 +465,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 26,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
@ -483,7 +483,7 @@
|
|||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 27,
|
||||
"passes": false,
|
||||
"passes": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
|
|
|
|||
149
progress.txt
149
progress.txt
|
|
@ -17,6 +17,13 @@
|
|||
- Toast component in `src/components/Toast.tsx` for success/error notifications (auto-dismiss after 3s)
|
||||
- Admin operations use SUPABASE_SERVICE_ROLE_KEY (server-side only via server actions)
|
||||
- Admin users have is_admin=true in profiles table; check via .select('is_admin').eq('id', user.id).single()
|
||||
- React Flow editor is in `src/app/editor/[projectId]/` with page.tsx (server) and FlowchartEditor.tsx (client)
|
||||
- React Flow requires 'use client' and importing 'reactflow/dist/style.css'
|
||||
- Use toReactFlowNodes/toReactFlowEdges helpers to convert app types to React Flow types
|
||||
- Custom node components go in `src/components/editor/nodes/` with NodeProps<T> typing and useReactFlow() for updates
|
||||
- Register custom node types in nodeTypes object (memoized with useMemo) and pass to ReactFlow component
|
||||
- FlowchartEditor uses ReactFlowProvider wrapper + inner component pattern for useReactFlow() hook access
|
||||
- Use nanoid for generating unique node IDs (import from 'nanoid')
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -260,3 +267,145 @@
|
|||
- Admin check should happen both in server component (redirect) and server action (double check)
|
||||
- Admin page uses its own layout (not dashboard layout) to have custom styling
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-017
|
||||
- What was implemented: Editor page with React Flow canvas
|
||||
- Files changed:
|
||||
- package.json - added reactflow dependency
|
||||
- src/app/editor/[projectId]/page.tsx - new server component that fetches project from Supabase, handles auth/not found, renders header with back link
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - new client component with React Flow canvas, Background component, type converters for nodes/edges
|
||||
- src/app/editor/[projectId]/loading.tsx - new loading state component with spinner
|
||||
- **Learnings for future iterations:**
|
||||
- React Flow requires 'use client' directive since it uses browser APIs
|
||||
- Import 'reactflow/dist/style.css' for default React Flow styling
|
||||
- Use useNodesState and useEdgesState hooks for managing nodes/edges state
|
||||
- Convert app types (FlowchartNode, FlowchartEdge) to React Flow types with helper functions
|
||||
- Next.js dynamic route params come as Promise in App Router 16+ (need to await params)
|
||||
- Use notFound() from next/navigation for 404 responses
|
||||
- React Flow canvas needs parent container with explicit height (h-full, h-screen)
|
||||
- Background component accepts variant (Dots, Lines, Cross) and gap/size props
|
||||
- Loading page (loading.tsx) provides automatic loading UI for async server components
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-018
|
||||
- What was implemented: Canvas pan and zoom controls
|
||||
- Files changed:
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - added Controls import and component
|
||||
- **Learnings for future iterations:**
|
||||
- React Flow Controls component provides zoom +/-, fitView, and lock buttons out of the box
|
||||
- Use position="bottom-right" prop to position controls in bottom-right corner
|
||||
- Pan (click-and-drag) and zoom (mouse wheel) are React Flow defaults, no extra config needed
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-019
|
||||
- What was implemented: Editor toolbar with add/save/export/import buttons
|
||||
- Files changed:
|
||||
- src/components/editor/Toolbar.tsx - new toolbar component with styled buttons
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - integrated toolbar with placeholder handlers
|
||||
- **Learnings for future iterations:**
|
||||
- Toolbar component accepts callback props for actions (onAddDialogue, onSave, etc.)
|
||||
- Node type buttons use color coding: blue (Dialogue), green (Choice), orange (Variable)
|
||||
- Action buttons (Save, Export, Import) use neutral bordered styling
|
||||
- FlowchartEditor now uses flex-col layout to stack toolbar above canvas
|
||||
- Placeholder handlers with TODO comments help track future implementation work
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-020
|
||||
- What was implemented: Custom DialogueNode component for displaying/editing character dialogue
|
||||
- Files changed:
|
||||
- src/components/editor/nodes/DialogueNode.tsx - new custom node component with editable speaker and text fields
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - registered DialogueNode as custom node type
|
||||
- **Learnings for future iterations:**
|
||||
- Custom React Flow nodes use NodeProps<T> for typing, where T is the data shape
|
||||
- Use useReactFlow() hook to get setNodes for updating node data from within the node component
|
||||
- Handle components need Position enum (Position.Top, Position.Bottom) for positioning
|
||||
- Custom handles can be styled with className and TailwindCSS, use ! prefix to override defaults (e.g., !h-3, !w-3)
|
||||
- Node types must be registered in a nodeTypes object and passed to ReactFlow component
|
||||
- Memoize nodeTypes with useMemo to prevent unnecessary re-renders
|
||||
- Custom node components go in src/components/editor/nodes/ directory
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-021
|
||||
- What was implemented: Add dialogue node from toolbar functionality
|
||||
- Files changed:
|
||||
- package.json - added nanoid dependency for unique ID generation
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - implemented handleAddDialogue to create new dialogue nodes at viewport center
|
||||
- **Learnings for future iterations:**
|
||||
- useReactFlow() hook requires ReactFlowProvider wrapper, so split component into inner component and outer wrapper
|
||||
- getViewport() returns { x, y, zoom } representing the current pan/zoom state
|
||||
- Calculate viewport center: centerX = (-viewport.x + halfWidth) / viewport.zoom
|
||||
- nanoid v5+ generates unique IDs synchronously with no dependencies
|
||||
- Node creation pattern: create Node object with { id, type, position, data }, then add to state via setNodes
|
||||
- React Flow nodes are draggable by default, no extra configuration needed
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-022
|
||||
- What was implemented: Custom ChoiceNode component for displaying branching decisions
|
||||
- Files changed:
|
||||
- src/components/editor/nodes/ChoiceNode.tsx - new custom node component with green styling, editable prompt, and dynamic option handles
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - registered ChoiceNode as custom node type
|
||||
- **Learnings for future iterations:**
|
||||
- ChoiceNode follows same pattern as DialogueNode: NodeProps<T> typing, useReactFlow() for updates
|
||||
- Dynamic handles positioned using style={{ left: `${((index + 1) / (options.length + 1)) * 100}%` }} for even spacing
|
||||
- Handle id format for options: 'option-0', 'option-1', etc. (matching the index)
|
||||
- Each option needs a unique id (string) and label (string) per the ChoiceOption type
|
||||
- updateOptionLabel callback pattern: find option by id, map over options array to update matching one
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-023
|
||||
- What was implemented: Add choice node from toolbar functionality
|
||||
- Files changed:
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - implemented handleAddChoice to create new choice nodes at viewport center
|
||||
- **Learnings for future iterations:**
|
||||
- handleAddChoice follows same pattern as handleAddDialogue: get viewport center, create node with nanoid, add to state
|
||||
- Choice nodes must be initialized with 2 options (each with unique id via nanoid and empty label)
|
||||
- Node data structure for choice: { prompt: '', options: [{ id, label }, { id, label }] }
|
||||
- React Flow nodes are draggable by default after being added to state
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-024
|
||||
- What was implemented: Add/remove choice options functionality (2-6 options supported)
|
||||
- Files changed:
|
||||
- src/components/editor/nodes/ChoiceNode.tsx - added addOption and removeOption callbacks, '+' button to add options, 'x' button per option to remove
|
||||
- **Learnings for future iterations:**
|
||||
- Define MIN_OPTIONS and MAX_OPTIONS constants for clear limits
|
||||
- Use disabled prop on buttons to enforce min/max constraints with appropriate styling (opacity-30, cursor-not-allowed)
|
||||
- Remove button uses × character for simple cross icon
|
||||
- Add button styled with border-dashed for visual distinction from action buttons
|
||||
- Handles update dynamically via React Flow re-render when options array changes
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-025
|
||||
- What was implemented: Custom VariableNode component for setting/modifying story variables
|
||||
- Files changed:
|
||||
- src/components/editor/nodes/VariableNode.tsx - new custom node component with orange styling, editable variable name, operation dropdown, and numeric value input
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - imported and registered VariableNode in nodeTypes
|
||||
- **Learnings for future iterations:**
|
||||
- VariableNode follows same pattern as DialogueNode: NodeProps<T> typing, useReactFlow() for updates
|
||||
- Use parseFloat() with fallback to 0 for number input handling: `parseFloat(e.target.value) || 0`
|
||||
- Operation dropdown uses select element with options for 'set', 'add', 'subtract'
|
||||
- Type assertion needed for select value: `e.target.value as 'set' | 'add' | 'subtract'`
|
||||
- Use `??` (nullish coalescing) for number defaults instead of `||` to allow 0 values: `data.value ?? 0`
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-026
|
||||
- What was implemented: Add variable node from toolbar functionality
|
||||
- Files changed:
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - implemented handleAddVariable to create new variable nodes at viewport center
|
||||
- **Learnings for future iterations:**
|
||||
- handleAddVariable follows same pattern as handleAddDialogue and handleAddChoice: get viewport center, create node with nanoid, add to state
|
||||
- Variable nodes initialized with { variableName: '', operation: 'set', value: 0 }
|
||||
- All add node handlers share the same pattern and use the getViewportCenter helper
|
||||
---
|
||||
|
||||
## 2026-01-21 - US-027
|
||||
- What was implemented: Connect nodes with edges including arrow markers and smooth styling
|
||||
- Files changed:
|
||||
- src/app/editor/[projectId]/FlowchartEditor.tsx - added MarkerType import, updated onConnect to create edges with smoothstep type and arrow markers, updated toReactFlowEdges to apply same styling to loaded edges
|
||||
- **Learnings for future iterations:**
|
||||
- Use `type: 'smoothstep'` for cleaner edge curves instead of default bezier
|
||||
- Use `markerEnd: { type: MarkerType.ArrowClosed }` to add directional arrows to edges
|
||||
- Connection type has nullable source/target, but Edge requires non-null strings - guard with early return
|
||||
- Apply consistent edge styling in both onConnect (new edges) and toReactFlowEdges (loaded edges)
|
||||
- Generate unique edge IDs with nanoid in onConnect callback
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Controls,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
useReactFlow,
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
Connection,
|
||||
Node,
|
||||
Edge,
|
||||
NodeTypes,
|
||||
MarkerType,
|
||||
} from 'reactflow'
|
||||
import { nanoid } from 'nanoid'
|
||||
import 'reactflow/dist/style.css'
|
||||
import Toolbar from '@/components/editor/Toolbar'
|
||||
import DialogueNode from '@/components/editor/nodes/DialogueNode'
|
||||
import ChoiceNode from '@/components/editor/nodes/ChoiceNode'
|
||||
import VariableNode from '@/components/editor/nodes/VariableNode'
|
||||
import type { FlowchartData, FlowchartNode, FlowchartEdge } from '@/types/flowchart'
|
||||
|
||||
type FlowchartEditorProps = {
|
||||
projectId: string
|
||||
initialData: FlowchartData
|
||||
}
|
||||
|
||||
// Convert our FlowchartNode type to React Flow Node type
|
||||
function toReactFlowNodes(nodes: FlowchartNode[]): Node[] {
|
||||
return nodes.map((node) => ({
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
data: node.data,
|
||||
}))
|
||||
}
|
||||
|
||||
// Convert our FlowchartEdge type to React Flow Edge type
|
||||
function toReactFlowEdges(edges: FlowchartEdge[]): Edge[] {
|
||||
return edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
target: edge.target,
|
||||
targetHandle: edge.targetHandle,
|
||||
data: edge.data,
|
||||
type: 'smoothstep',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// Inner component that uses useReactFlow hook
|
||||
function FlowchartEditorInner({ initialData }: FlowchartEditorProps) {
|
||||
// Define custom node types - memoized to prevent re-renders
|
||||
const nodeTypes: NodeTypes = useMemo(
|
||||
() => ({
|
||||
dialogue: DialogueNode,
|
||||
choice: ChoiceNode,
|
||||
variable: VariableNode,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const { getViewport } = useReactFlow()
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||
toReactFlowNodes(initialData.nodes)
|
||||
)
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
||||
toReactFlowEdges(initialData.edges)
|
||||
)
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => {
|
||||
if (!params.source || !params.target) return
|
||||
const newEdge: Edge = {
|
||||
id: nanoid(),
|
||||
source: params.source,
|
||||
target: params.target,
|
||||
sourceHandle: params.sourceHandle,
|
||||
targetHandle: params.targetHandle,
|
||||
type: 'smoothstep',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
}
|
||||
setEdges((eds) => addEdge(newEdge, eds))
|
||||
},
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
// Get center position of current viewport for placing new nodes
|
||||
const getViewportCenter = useCallback(() => {
|
||||
const viewport = getViewport()
|
||||
// Calculate center based on viewport dimensions (assume ~800x600 visible area)
|
||||
// Adjust based on zoom level
|
||||
const centerX = (-viewport.x + 400) / viewport.zoom
|
||||
const centerY = (-viewport.y + 300) / viewport.zoom
|
||||
return { x: centerX, y: centerY }
|
||||
}, [getViewport])
|
||||
|
||||
// Add dialogue node at viewport center
|
||||
const handleAddDialogue = useCallback(() => {
|
||||
const position = getViewportCenter()
|
||||
const newNode: Node = {
|
||||
id: nanoid(),
|
||||
type: 'dialogue',
|
||||
position,
|
||||
data: { speaker: '', text: '' },
|
||||
}
|
||||
setNodes((nodes) => [...nodes, newNode])
|
||||
}, [getViewportCenter, setNodes])
|
||||
|
||||
const handleAddChoice = useCallback(() => {
|
||||
const position = getViewportCenter()
|
||||
const newNode: Node = {
|
||||
id: nanoid(),
|
||||
type: 'choice',
|
||||
position,
|
||||
data: {
|
||||
prompt: '',
|
||||
options: [
|
||||
{ id: nanoid(), label: '' },
|
||||
{ id: nanoid(), label: '' },
|
||||
],
|
||||
},
|
||||
}
|
||||
setNodes((nodes) => [...nodes, newNode])
|
||||
}, [getViewportCenter, setNodes])
|
||||
|
||||
const handleAddVariable = useCallback(() => {
|
||||
const position = getViewportCenter()
|
||||
const newNode: Node = {
|
||||
id: nanoid(),
|
||||
type: 'variable',
|
||||
position,
|
||||
data: {
|
||||
variableName: '',
|
||||
operation: 'set',
|
||||
value: 0,
|
||||
},
|
||||
}
|
||||
setNodes((nodes) => [...nodes, newNode])
|
||||
}, [getViewportCenter, setNodes])
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
// TODO: Implement in US-034
|
||||
}, [])
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
// TODO: Implement in US-035
|
||||
}, [])
|
||||
|
||||
const handleImport = useCallback(() => {
|
||||
// TODO: Implement in US-036
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<Toolbar
|
||||
onAddDialogue={handleAddDialogue}
|
||||
onAddChoice={handleAddChoice}
|
||||
onAddVariable={handleAddVariable}
|
||||
onSave={handleSave}
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
||||
<Controls position="bottom-right" />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Outer wrapper component with ReactFlowProvider
|
||||
export default function FlowchartEditor(props: FlowchartEditorProps) {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<FlowchartEditorInner {...props} />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
export default function EditorLoading() {
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<header className="flex items-center justify-between border-b border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-5 w-5 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
|
||||
<div className="h-6 w-32 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 items-center justify-center bg-zinc-100 dark:bg-zinc-800">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<svg
|
||||
className="h-8 w-8 animate-spin text-blue-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Loading editor...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { createClient } from '@/lib/supabase/server'
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import FlowchartEditor from './FlowchartEditor'
|
||||
import type { FlowchartData } from '@/types/flowchart'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ projectId: string }>
|
||||
}
|
||||
|
||||
export default async function EditorPage({ params }: PageProps) {
|
||||
const { projectId } = await params
|
||||
const supabase = await createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { data: project, error } = await supabase
|
||||
.from('projects')
|
||||
.select('id, name, flowchart_data')
|
||||
.eq('id', projectId)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (error || !project) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const flowchartData = (project.flowchart_data || {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
}) as FlowchartData
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<header className="flex items-center justify-between border-b border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<h1 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
|
||||
{project.name}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1">
|
||||
<FlowchartEditor
|
||||
projectId={project.id}
|
||||
initialData={flowchartData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
'use client'
|
||||
|
||||
type ToolbarProps = {
|
||||
onAddDialogue: () => void
|
||||
onAddChoice: () => void
|
||||
onAddVariable: () => void
|
||||
onSave: () => void
|
||||
onExport: () => void
|
||||
onImport: () => void
|
||||
}
|
||||
|
||||
export default function Toolbar({
|
||||
onAddDialogue,
|
||||
onAddChoice,
|
||||
onAddVariable,
|
||||
onSave,
|
||||
onExport,
|
||||
onImport,
|
||||
}: ToolbarProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-4 py-2 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="mr-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">
|
||||
Add Node:
|
||||
</span>
|
||||
<button
|
||||
onClick={onAddDialogue}
|
||||
className="rounded bg-blue-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||
>
|
||||
Dialogue
|
||||
</button>
|
||||
<button
|
||||
onClick={onAddChoice}
|
||||
className="rounded bg-green-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||
>
|
||||
Choice
|
||||
</button>
|
||||
<button
|
||||
onClick={onAddVariable}
|
||||
className="rounded bg-orange-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||
>
|
||||
Variable
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onSave}
|
||||
className="rounded border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600 dark:focus:ring-offset-zinc-800"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={onExport}
|
||||
className="rounded border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600 dark:focus:ring-offset-zinc-800"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={onImport}
|
||||
className="rounded border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600 dark:focus:ring-offset-zinc-800"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, ChangeEvent } from 'react'
|
||||
import { Handle, Position, NodeProps, useReactFlow } from 'reactflow'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
type ChoiceOption = {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
type ChoiceNodeData = {
|
||||
prompt: string
|
||||
options: ChoiceOption[]
|
||||
}
|
||||
|
||||
const MIN_OPTIONS = 2
|
||||
const MAX_OPTIONS = 6
|
||||
|
||||
export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
|
||||
const { setNodes } = useReactFlow()
|
||||
|
||||
const updatePrompt = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === id
|
||||
? { ...node, data: { ...node.data, prompt: e.target.value } }
|
||||
: node
|
||||
)
|
||||
)
|
||||
},
|
||||
[id, setNodes]
|
||||
)
|
||||
|
||||
const updateOptionLabel = useCallback(
|
||||
(optionId: string, label: string) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === id
|
||||
? {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
options: node.data.options.map((opt: ChoiceOption) =>
|
||||
opt.id === optionId ? { ...opt, label } : opt
|
||||
),
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
)
|
||||
},
|
||||
[id, setNodes]
|
||||
)
|
||||
|
||||
const addOption = useCallback(() => {
|
||||
if (data.options.length >= MAX_OPTIONS) return
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === id
|
||||
? {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
options: [
|
||||
...node.data.options,
|
||||
{ id: nanoid(), label: '' },
|
||||
],
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
)
|
||||
}, [id, data.options.length, setNodes])
|
||||
|
||||
const removeOption = useCallback(
|
||||
(optionId: string) => {
|
||||
if (data.options.length <= MIN_OPTIONS) return
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === id
|
||||
? {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
options: node.data.options.filter(
|
||||
(opt: ChoiceOption) => opt.id !== optionId
|
||||
),
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
)
|
||||
},
|
||||
[id, data.options.length, setNodes]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-w-[220px] rounded-lg border-2 border-green-500 bg-green-50 p-3 shadow-md dark:border-green-400 dark:bg-green-950">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="input"
|
||||
className="!h-3 !w-3 !border-2 !border-green-500 !bg-white dark:!bg-zinc-800"
|
||||
/>
|
||||
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-green-700 dark:text-green-300">
|
||||
Choice
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={data.prompt || ''}
|
||||
onChange={updatePrompt}
|
||||
placeholder="What do you choose?"
|
||||
className="mb-3 w-full rounded border border-green-300 bg-white px-2 py-1 text-sm focus:border-green-500 focus:outline-none focus:ring-1 focus:ring-green-500 dark:border-green-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
{data.options.map((option, index) => (
|
||||
<div key={option.id} className="relative flex items-center gap-1">
|
||||
<input
|
||||
type="text"
|
||||
value={option.label}
|
||||
onChange={(e) => updateOptionLabel(option.id, e.target.value)}
|
||||
placeholder={`Option ${index + 1}`}
|
||||
className="flex-1 rounded border border-green-300 bg-white px-2 py-1 text-sm focus:border-green-500 focus:outline-none focus:ring-1 focus:ring-green-500 dark:border-green-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeOption(option.id)}
|
||||
disabled={data.options.length <= MIN_OPTIONS}
|
||||
className="flex h-6 w-6 items-center justify-center rounded text-red-500 hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:hover:bg-red-900/30"
|
||||
title="Remove option"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id={`option-${index}`}
|
||||
className="!h-3 !w-3 !border-2 !border-green-500 !bg-white dark:!bg-zinc-800"
|
||||
style={{
|
||||
left: `${((index + 1) / (data.options.length + 1)) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addOption}
|
||||
disabled={data.options.length >= MAX_OPTIONS}
|
||||
className="mt-2 flex w-full items-center justify-center gap-1 rounded border border-dashed border-green-400 py-1 text-sm text-green-600 hover:bg-green-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:border-green-500 dark:text-green-400 dark:hover:bg-green-900/30"
|
||||
title="Add option"
|
||||
>
|
||||
+ Add Option
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, ChangeEvent } from 'react'
|
||||
import { Handle, Position, NodeProps, useReactFlow } from 'reactflow'
|
||||
|
||||
type DialogueNodeData = {
|
||||
speaker?: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export default function DialogueNode({ id, data }: NodeProps<DialogueNodeData>) {
|
||||
const { setNodes } = useReactFlow()
|
||||
|
||||
const updateNodeData = useCallback(
|
||||
(field: keyof DialogueNodeData, value: string) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === id
|
||||
? { ...node, data: { ...node.data, [field]: value } }
|
||||
: node
|
||||
)
|
||||
)
|
||||
},
|
||||
[id, setNodes]
|
||||
)
|
||||
|
||||
const handleSpeakerChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
updateNodeData('speaker', e.target.value)
|
||||
},
|
||||
[updateNodeData]
|
||||
)
|
||||
|
||||
const handleTextChange = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateNodeData('text', e.target.value)
|
||||
},
|
||||
[updateNodeData]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-w-[200px] rounded-lg border-2 border-blue-500 bg-blue-50 p-3 shadow-md dark:border-blue-400 dark:bg-blue-950">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="input"
|
||||
className="!h-3 !w-3 !border-2 !border-blue-500 !bg-white dark:!bg-zinc-800"
|
||||
/>
|
||||
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
|
||||
Dialogue
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={data.speaker || ''}
|
||||
onChange={handleSpeakerChange}
|
||||
placeholder="Speaker"
|
||||
className="mb-2 w-full rounded border border-blue-300 bg-white px-2 py-1 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-blue-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
value={data.text || ''}
|
||||
onChange={handleTextChange}
|
||||
placeholder="Dialogue text..."
|
||||
rows={3}
|
||||
className="w-full resize-none rounded border border-blue-300 bg-white px-2 py-1 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-blue-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
||||
/>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="output"
|
||||
className="!h-3 !w-3 !border-2 !border-blue-500 !bg-white dark:!bg-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, ChangeEvent } from 'react'
|
||||
import { Handle, Position, NodeProps, useReactFlow } from 'reactflow'
|
||||
|
||||
type VariableNodeData = {
|
||||
variableName: string
|
||||
operation: 'set' | 'add' | 'subtract'
|
||||
value: number
|
||||
}
|
||||
|
||||
export default function VariableNode({ id, data }: NodeProps<VariableNodeData>) {
|
||||
const { setNodes } = useReactFlow()
|
||||
|
||||
const updateVariableName = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === id
|
||||
? { ...node, data: { ...node.data, variableName: e.target.value } }
|
||||
: node
|
||||
)
|
||||
)
|
||||
},
|
||||
[id, setNodes]
|
||||
)
|
||||
|
||||
const updateOperation = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === id
|
||||
? { ...node, data: { ...node.data, operation: e.target.value as 'set' | 'add' | 'subtract' } }
|
||||
: node
|
||||
)
|
||||
)
|
||||
},
|
||||
[id, setNodes]
|
||||
)
|
||||
|
||||
const updateValue = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value) || 0
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === id
|
||||
? { ...node, data: { ...node.data, value } }
|
||||
: node
|
||||
)
|
||||
)
|
||||
},
|
||||
[id, setNodes]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-w-[200px] rounded-lg border-2 border-orange-500 bg-orange-50 p-3 shadow-md dark:border-orange-400 dark:bg-orange-950">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="input"
|
||||
className="!h-3 !w-3 !border-2 !border-orange-500 !bg-white dark:!bg-zinc-800"
|
||||
/>
|
||||
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-orange-700 dark:text-orange-300">
|
||||
Variable
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={data.variableName || ''}
|
||||
onChange={updateVariableName}
|
||||
placeholder="variableName"
|
||||
className="mb-2 w-full rounded border border-orange-300 bg-white px-2 py-1 text-sm focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 dark:border-orange-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
||||
/>
|
||||
|
||||
<div className="mb-2 flex gap-2">
|
||||
<select
|
||||
value={data.operation || 'set'}
|
||||
onChange={updateOperation}
|
||||
className="flex-1 rounded border border-orange-300 bg-white px-2 py-1 text-sm focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 dark:border-orange-600 dark:bg-zinc-800 dark:text-white"
|
||||
>
|
||||
<option value="set">set</option>
|
||||
<option value="add">add</option>
|
||||
<option value="subtract">subtract</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
value={data.value ?? 0}
|
||||
onChange={updateValue}
|
||||
className="w-20 rounded border border-orange-300 bg-white px-2 py-1 text-sm focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 dark:border-orange-600 dark:bg-zinc-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="output"
|
||||
className="!h-3 !w-3 !border-2 !border-orange-500 !bg-white dark:!bg-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue