diff --git a/site/package-lock.json b/site/package-lock.json index 160e521..f1a7f98 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -85,7 +85,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2047,7 +2046,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2091,7 +2089,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2279,7 +2276,6 @@ "resolved": "https://registry.npmjs.org/@gatsbyjs/reach-router/-/reach-router-2.0.1.tgz", "integrity": "sha512-gmSZniS9/phwgEgpFARMpNg21PkYDZEpfgEzvkgpE/iku4uvXqCrxr86fXbTpI9mkrhKS1SCTYmLGe60VdHcdQ==", "license": "MIT", - "peer": true, "dependencies": { "invariant": "^2.2.4", "prop-types": "^15.8.1" @@ -3159,7 +3155,6 @@ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz", "integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -3186,7 +3181,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.18.0", @@ -3603,7 +3597,6 @@ "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.8.3.tgz", "integrity": "sha512-Euf/un4ZAiClnlUXqPB9phQlKbveU+2CotZv7m7i+qkgvFn5nAGnrV4h1OzQU42j9dpgOxWi7AttUDMrvkbhCQ==", "license": "MIT", - "peer": true, "dependencies": { "@mischnic/json-sourcemap": "^0.1.0", "@parcel/cache": "2.8.3", @@ -4566,33 +4559,6 @@ "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", "license": "MIT" }, - "node_modules/@reduxjs/toolkit": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", - "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^10.2.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -4918,18 +4884,6 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, "node_modules/@svgdotjs/svg.draw.js": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draw.js/-/svg.draw.js-3.0.2.tgz", @@ -4947,7 +4901,6 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz", "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/Fuzzyma" @@ -5164,7 +5117,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -5200,227 +5152,12 @@ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", "license": "MIT" }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", - "license": "MIT" - }, "node_modules/@types/yoga-layout": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==", "license": "MIT" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", - "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/experimental-utils": "4.33.0", - "@typescript-eslint/scope-manager": "4.33.0", - "debug": "^4.3.1", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.1.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^4.0.0", - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", - "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "debug": "^4.3.1" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", - "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", - "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", - "license": "MIT", - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", - "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0", - "debug": "^4.3.1", - "globby": "^11.0.3", - "is-glob": "^4.0.1", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", - "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "4.33.0", - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@vercel/webpack-asset-relocator-loader": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@vercel/webpack-asset-relocator-loader/-/webpack-asset-relocator-loader-1.7.3.tgz", @@ -5576,26 +5313,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@xstate/react": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@xstate/react/-/react-5.0.5.tgz", - "integrity": "sha512-MfF/cPHa3lNKJmGFpUycMbNP25qBXyZXrxc8VYNroAu0Nnk0DV5WzAkTcQXma0xEC4dSwsoA+YQuKbZATtqvgg==", - "license": "MIT", - "peer": true, - "dependencies": { - "use-isomorphic-layout-effect": "^1.1.2", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "xstate": "^5.19.4" - }, - "peerDependenciesMeta": { - "xstate": { - "optional": true - } - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5653,7 +5370,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5732,7 +5448,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6245,36 +5960,6 @@ "node": ">= 0.4" } }, - "node_modules/babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - }, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "eslint": ">= 4.12.1" - } - }, - "node_modules/babel-eslint/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, "node_modules/babel-jsx-utils": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/babel-jsx-utils/-/babel-jsx-utils-1.1.0.tgz", @@ -6410,7 +6095,6 @@ "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", @@ -6839,7 +6523,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7732,7 +7415,6 @@ "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", "hasInstallScript": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -8322,7 +8004,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -9358,7 +9039,6 @@ "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -9495,7 +9175,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.10.0.tgz", "integrity": "sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "lodash": "^4.17.15", "string-natural-compare": "^3.0.1" @@ -9512,7 +9191,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9567,7 +9245,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "license": "MIT", - "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -9597,7 +9274,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -9630,7 +9306,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -10825,7 +10500,6 @@ "integrity": "sha512-eqtq5S7AhS0SROTdOycOATYwT6GLYTcvpzZnzK2pFfhe3knLX0XJoSResUGGIj9kqfi8/kXEH29RU/2Lr7nPbw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/core": "^7.20.12", @@ -11496,7 +11170,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -11619,7 +11292,6 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -11945,7 +11617,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -12330,16 +12001,6 @@ "node": ">= 4" } }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/immutable": { "version": "3.7.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", @@ -15233,7 +14894,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -16191,7 +15851,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -16354,7 +16013,6 @@ "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz", "integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==", "license": "MIT", - "peer": true, "dependencies": { "@react-dnd/shallowequal": "^2.0.0", "@types/hoist-non-react-statics": "^3.3.1", @@ -16380,7 +16038,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -16421,58 +16078,11 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "license": "MIT" }, - "node_modules/react-redux": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", - "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.12.1", - "@types/hoist-non-react-statics": "^3.3.1", - "@types/use-sync-external-store": "^0.0.3", - "hoist-non-react-statics": "^3.3.2", - "react-is": "^18.0.0", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^16.8 || ^17.0 || ^18.0", - "@types/react-dom": "^16.8 || ^17.0 || ^18.0", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0", - "react-native": ">=0.59", - "redux": "^4 || ^5.0.0-beta.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-redux/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -16548,7 +16158,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -16581,7 +16190,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -16760,22 +16368,6 @@ "node": ">=6.0.0" } }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -16997,12 +16589,6 @@ "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", "license": "MIT" }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -17277,7 +16863,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -18341,7 +17926,6 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", "license": "MIT", - "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -18941,7 +18525,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -19057,20 +18640,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/ua-parser-js": { "version": "1.0.41", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", @@ -19357,29 +18926,6 @@ "react": "*" } }, - "node_modules/use-isomorphic-layout-effect": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", - "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -19473,7 +19019,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -19843,7 +19388,6 @@ "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.24.0.tgz", "integrity": "sha512-h/213ThFfZbOefUWrLc9ZvYggEVBr0jrD2dNxErxNMLQfZRN19v+80TaXFho17hs8Q2E1mULtm/6nv12um0C4A==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/xstate" diff --git a/site/src/components/ShapeBuilder/index.js b/site/src/components/ShapeBuilder/index.js index 82b022f..81b1cd5 100644 --- a/site/src/components/ShapeBuilder/index.js +++ b/site/src/components/ShapeBuilder/index.js @@ -1,23 +1,25 @@ -// /* global window */ +// Updated ShapeBuilder with Curved Drawing Support (Figma-like) +// Style preserved from your original component + import React, { useEffect, useRef, useState } from "react"; -import { Wrapper, CanvasContainer, OutputBox, StyledSVG, CopyButton } from "./shapeBuilder.styles"; -import { Button, Typography, Box, CopyIcon } from "@sistent/sistent"; -import { SVG, extend as SVGextend } from "@svgdotjs/svg.js"; -import draw from "@svgdotjs/svg.draw.js"; +import { Wrapper, CanvasContainer, OutputBox, StyledSVG } from "./shapeBuilder.styles"; +import { Button, Typography, Box } from "@sistent/sistent"; + +const defaultStroke = "#00B39F"; -SVGextend(SVG.Polygon, draw); +function getSvgPoint(svg, clientX, clientY) { + if (!svg) return { x: clientX, y: clientY }; + const pt = svg.createSVGPoint(); + pt.x = clientX; + pt.y = clientY; + return pt.matrixTransform(svg.getScreenCTM().inverse()); +} const ShapeBuilder = () => { - const boardRef = useRef(null); - const polyRef = useRef(null); - const keyHandlersRef = useRef({}); - const [result, setResult] = useState(""); - const [error, setError] = useState(null); const [showCopied, setShowCopied] = useState(false); const handleCopyToClipboard = async () => { if (!result.trim()) return; - try { await navigator.clipboard.writeText(result); setShowCopied(true); @@ -26,148 +28,348 @@ const ShapeBuilder = () => { console.error("Failed to copy to clipboard:", err); } }; + const boardRef = useRef(null); + const [mousePoint, setMousePoint] = useState(null); + const [nearFirst, setNearFirst] = useState(false); + const [anchors, setAnchors] = useState([]); // {x,y, handleIn:{x,y}, handleOut:{x,y}} + const [isClosed, setIsClosed] = useState(false); + const [dragState, setDragState] = useState(null); + const [result, setResult] = useState(""); - const getPlottedPoints = (poly) => { - if (!poly) return null; - const plotted = poly.plot(); - const points = Array.isArray(plotted) ? plotted : plotted?.value; - return Array.isArray(points) ? points : null; + // deep clone anchors helper + const cloneAnchors = (arr) => arr.map(a => ({ + x: a.x, y: a.y, + handleIn: { x: a.handleIn.x, y: a.handleIn.y }, + handleOut: { x: a.handleOut.x, y: a.handleOut.y } + })); + + // Add a new anchor and optionally begin placing (dragging handle) + const addAnchor = (x, y, placing = true) => { + const newAnchor = { x, y, handleIn: { x, y }, handleOut: { x, y } }; + setAnchors(prev => { + const next = cloneAnchors(prev); + next.push(newAnchor); + return next; + }); + + if (placing) { + // index will be previous length + setDragState(prev => ({ type: "placing", index: (anchors.length), start: { x, y } })); + } }; - const showCytoArray = () => { - const poly = polyRef.current; - if (!poly) return; + const updateAnchorHandle = (index, handleKey, hx, hy, symmetric = true) => { + setAnchors(prev => { + const next = cloneAnchors(prev); + if (!next[index]) return prev; + next[index][handleKey] = { x: hx, y: hy }; + if (symmetric) { + const ax = next[index].x; + const ay = next[index].y; + const dx = hx - ax; + const dy = hy - ay; + const opposite = handleKey === "handleOut" ? "handleIn" : "handleOut"; + next[index][opposite] = { x: ax - dx, y: ay - dy }; + } + return next; + }); + }; - try { - const points = getPlottedPoints(poly); - if (!points) throw new Error("Invalid or empty polygon points"); - - const normalized = points - .map(([x, y]) => [(x - 260) / 260, (y - 260) / 260]) - .flat() - .join(" "); - setResult(normalized); - setError(null); - } catch (err) { - setError("Failed to extract and normalize polygon points."); - console.error("showCytoArray error:", err); + const updatePathOnMove = (clientX, clientY) => { + if (!boardRef.current) return; + const pt = getSvgPoint(boardRef.current, clientX, clientY); + if (!dragState) return; + + if (dragState.type === "placing") { + updateAnchorHandle(dragState.index, "handleOut", pt.x, pt.y, true); + } else if (dragState.type === "handle") { + updateAnchorHandle(dragState.index, dragState.handleKey, pt.x, pt.y, dragState.symmetric); } }; - const handleMaximize = () => { - const poly = polyRef.current; - if (!poly) return; + // Mouse handlers + const onMouseDown = (e) => { + // left button only + if (e.button !== 0) return; + if (isClosed) return; + + const pt = getSvgPoint(boardRef.current, e.clientX, e.clientY); + + // If we're near the first point and already have a polygon, auto-close + if (anchors.length >= 3) { + const first = anchors[0]; + const dx = pt.x - first.x; + const dy = pt.y - first.y; + const distanceSq = dx * dx + dy * dy; + const threshold = 6; // tighter pixels radius for snapping/closing + + if (distanceSq <= threshold * threshold) { + setIsClosed(true); + setDragState(null); + return; + } + } - const points = getPlottedPoints(poly); - if (!points) return; - const xs = points.map(p => p[0]); - const ys = points.map(p => p[1]); + // Otherwise, create a new anchor + addAnchor(pt.x, pt.y, true); + }; - const width = Math.abs(Math.max(...xs) - Math.min(...xs)); - const height = Math.abs(Math.max(...ys) - Math.min(...ys)); + const onMouseMove = (e) => { + // update preview point + if (!boardRef.current) return; + const pt = getSvgPoint(boardRef.current, e.clientX, e.clientY); + + // detect proximity to first anchor for hover-close indication + if (!isClosed && anchors.length >= 3) { + const first = anchors[0]; + const dx = pt.x - first.x; + const dy = pt.y - first.y; + const distanceSq = dx * dx + dy * dy; + const threshold = 6; // tighter radius in pixels + setNearFirst(distanceSq <= threshold * threshold); + } else { + setNearFirst(false); + } - poly.size(width > height ? 520 : undefined, height >= width ? 520 : undefined); - poly.move(0, 0); - showCytoArray(); + setMousePoint(pt); + if (dragState) { + updatePathOnMove(e.clientX, e.clientY); + } }; - const handleKeyDown = (e) => { - const poly = polyRef.current; - if (!poly) return; + const onMouseUp = (e) => { + // finalize placing/dragging + setDragState(null); + }; - if (e.ctrlKey) { - poly.draw("param", "snapToGrid", 0.001); - } + const onHandleMouseDown = (e, index, handleKey) => { + e.stopPropagation(); + const symmetric = !e.shiftKey; // shift decouples handles + setDragState({ type: "handle", index, handleKey, symmetric }); + }; - if (e.key === "Enter" || e.key === "Escape") { - poly.draw("done"); - poly.fill("#00B39F"); - showCytoArray(); - } + const onAnchorMouseDown = (e, index) => { + e.stopPropagation(); - if (e.ctrlKey && e.key.toLowerCase() === "z") { - const points = getPlottedPoints(poly); - if (!points) return; - poly.plot(points.slice(0, -1)); + // If we click the first point while drawing (and have enough anchors), close the shape instead of moving it + if (!isClosed && index === 0 && anchors.length >= 3) { + setIsClosed(true); + setDragState(null); + return; } - }; - const handleKeyUp = (e) => { - const poly = polyRef.current; - if (!poly || e.ctrlKey) return; - poly.draw("param", "snapToGrid", 16); + // Otherwise, start moving this anchor + const start = getSvgPoint(boardRef.current, e.clientX, e.clientY); + setDragState({ type: "moveAnchor", index, start }); }; - const attachKeyListeners = () => { - document.addEventListener("keydown", handleKeyDown); - document.addEventListener("keyup", handleKeyUp); - keyHandlersRef.current = { handleKeyDown, handleKeyUp }; + // move anchor effect + useEffect(() => { + if (!dragState || dragState.type !== "moveAnchor") return; + + const move = (ev) => { + const pt = getSvgPoint(boardRef.current, ev.clientX, ev.clientY); + setAnchors(prev => { + const next = cloneAnchors(prev); + const idx = dragState.index; + if (!next[idx]) return prev; + const dx = pt.x - dragState.start.x; + const dy = pt.y - dragState.start.y; + next[idx].x += dx; next[idx].y += dy; + next[idx].handleIn.x += dx; next[idx].handleIn.y += dy; + next[idx].handleOut.x += dx; next[idx].handleOut.y += dy; + return next; + }); + setDragState(s => ({ ...s, start: pt })); + }; + + const up = () => setDragState(null); + window.addEventListener("mousemove", move); + window.addEventListener("mouseup", up); + return () => { + window.removeEventListener("mousemove", move); + window.removeEventListener("mouseup", up); + }; + }, [dragState]); + + // global handle/placing drag listeners + useEffect(() => { + if (!dragState) return; + if (dragState.type !== "handle" && dragState.type !== "placing") return; + + const onMove = (ev) => updatePathOnMove(ev.clientX, ev.clientY); + const onUp = () => setDragState(null); + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + return () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; + }, [dragState]); + + // keyboard handlers + useEffect(() => { + const onKeyDown = (e) => { + if (e.key === "Enter" && anchors.length >= 3) { + setIsClosed(true); + } + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") { + setAnchors(prev => prev.slice(0, -1)); + setIsClosed(false); + } + if (e.key === "Escape") { + // Close shape on ESC + if (anchors.length >= 3) { + setIsClosed(true); + } + setDragState(null); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [anchors]); + + const buildPathD = () => { + if (anchors.length === 0) return ""; + let d = `M ${anchors[0].x} ${anchors[0].y}`; + for (let i = 1; i < anchors.length; i++) { + const prev = anchors[i - 1]; + const curr = anchors[i]; + d += ` C ${prev.handleOut.x} ${prev.handleOut.y}, ${curr.handleIn.x} ${curr.handleIn.y}, ${curr.x} ${curr.y}`; + } + if (isClosed && anchors.length >= 2) { + const last = anchors[anchors.length - 1]; + const first = anchors[0]; + d += ` C ${last.handleOut.x} ${last.handleOut.y}, ${first.handleIn.x} ${first.handleIn.y}, ${first.x} ${first.y} Z`; + } + return d; }; - const detachKeyListeners = () => { - const { handleKeyDown, handleKeyUp } = keyHandlersRef.current; - if (handleKeyDown) document.removeEventListener("keydown", handleKeyDown); - if (handleKeyUp) document.removeEventListener("keyup", handleKeyUp); - keyHandlersRef.current = {}; + // ---- Cytoscape-compatible export ---- + // 1) Flatten cubic Bezier curves into straight segments + // 2) Normalize points to range [-1, 1] with center at (0,0) + + const BEZIER_SEGMENTS = 20; // accuracy per curve + + // cubic Bezier interpolation + const cubicAt = (p0, p1, p2, p3, t) => { + const mt = 1 - t; + return ( + mt * mt * mt * p0 + + 3 * mt * mt * t * p1 + + 3 * mt * t * t * p2 + + t * t * t * p3 + ); }; - const initializeDrawing = () => { - if (!boardRef.current) { - setError("Canvas reference not found"); - return; + const flattenToPoints = () => { + if (anchors.length === 0) return []; + + const pts = []; + + for (let i = 1; i < anchors.length; i++) { + const prev = anchors[i - 1]; + const curr = anchors[i]; + + for (let s = 0; s <= BEZIER_SEGMENTS; s++) { + const t = s / BEZIER_SEGMENTS; + const x = cubicAt(prev.x, prev.handleOut.x, curr.handleIn.x, curr.x, t); + const y = cubicAt(prev.y, prev.handleOut.y, curr.handleIn.y, curr.y, t); + pts.push([x, y]); + } } - try { - const draw = SVG() - .addTo(boardRef.current) - .size("100%", "100%") - .polygon() - .draw() - .attr({ stroke: "#00B39F", "stroke-width": 1, fill: "none" }); - - draw.draw("param", "snapToGrid", 16); - draw.on("drawstart", attachKeyListeners); - draw.on("drawdone", detachKeyListeners); - - polyRef.current = draw; - setError(null); - } catch (err) { - setError(`Failed to initialize drawing: ${err.message}`); + if (isClosed && anchors.length >= 2) { + const last = anchors[anchors.length - 1]; + const first = anchors[0]; + for (let s = 0; s <= BEZIER_SEGMENTS; s++) { + const t = s / BEZIER_SEGMENTS; + const x = cubicAt(last.x, last.handleOut.x, first.handleIn.x, first.x, t); + const y = cubicAt(last.y, last.handleOut.y, first.handleIn.y, first.y, t); + pts.push([x, y]); + } } + + return pts; }; - const clearShape = () => { - const poly = polyRef.current; - if (!poly) return; + const normalizePoints = (pts) => { + if (!pts.length) return []; - poly.draw("cancel"); - poly.remove(); - detachKeyListeners(); - polyRef.current = null; - setResult(""); - initializeDrawing(); - }; + const xs = pts.map(p => p[0]); + const ys = pts.map(p => p[1]); + + const minX = Math.min(...xs); + const maxX = Math.max(...xs); + const minY = Math.min(...ys); + const maxY = Math.max(...ys); - const closeShape = () => { - const poly = polyRef.current; - if (!poly) return; + const cx = (minX + maxX) / 2; + const cy = (minY + maxY) / 2; + const size = Math.max(maxX - minX, maxY - minY) || 1; + + return pts.map(([x, y]) => [ + Number(((x - cx) * 2 / size).toFixed(4)), + Number(((y - cy) * 2 / size).toFixed(4)) + ]); + }; - poly.draw("done"); - poly.fill("#00B39F"); - showCytoArray(); + const computeExportString = () => { + const flat = flattenToPoints(); + const normalized = normalizePoints(flat); + return normalized.flat().join(" "); }; useEffect(() => { - initializeDrawing(); - return () => { - detachKeyListeners(); - if (polyRef.current) { - polyRef.current.draw("cancel"); - polyRef.current.remove(); - polyRef.current = null; + setResult(computeExportString()); + }, [anchors, isClosed]); + + // Maximize: scale + translate shape to fit 520x520 + const maximize = () => { + if (anchors.length === 0) return; + + const xs = []; + const ys = []; + anchors.forEach(a => { + xs.push(a.x, a.handleIn.x, a.handleOut.x); + ys.push(a.y, a.handleIn.y, a.handleOut.y); + }); + + const minX = Math.min(...xs); + const maxX = Math.max(...xs); + const minY = Math.min(...ys); + const maxY = Math.max(...ys); + + const width = maxX - minX; + const height = maxY - minY; + if (width === 0 || height === 0) return; + + const target = 520; + const scale = Math.min(target / width, target / height); + + const offsetX = -minX; + const offsetY = -minY; + + setAnchors(prev => prev.map(a => ({ + x: (a.x + offsetX) * scale, + y: (a.y + offsetY) * scale, + handleIn: { + x: (a.handleIn.x + offsetX) * scale, + y: (a.handleIn.y + offsetY) * scale + }, + handleOut: { + x: (a.handleOut.x + offsetX) * scale, + y: (a.handleOut.y + offsetY) * scale } - }; - }, []); + }))); + }; + + const clear = () => { + setAnchors([]); + setIsClosed(false); + setDragState(null); + setResult(""); + }; return ( @@ -176,57 +378,112 @@ const ShapeBuilder = () => { ref={boardRef} width="100%" height="100%" - onDoubleClick={closeShape} + onMouseDown={onMouseDown} + onMouseMove={onMouseMove} + onMouseUp={onMouseUp} + onDoubleClick={() => { + if (!isClosed && anchors.length >= 3) setIsClosed(true); + }} > + + + + {/* path preview */} + + + {/* preview mouse point */} + {nearFirst && anchors.length > 0 && !isClosed && ( + // highlight halo around first point when close enough to auto-close + + )} + + {mousePoint && anchors.length > 0 && !isClosed && ( + + )} + + {mousePoint && !isClosed && ( + + )} + + {/* anchors, handles */} + {anchors.map((a, idx) => ( + + + + + onHandleMouseDown(e, idx, "handleIn")} + /> + + onHandleMouseDown(e, idx, "handleOut")} + /> + + onAnchorMouseDown(e, idx)} + /> + + ))} - {error && ( -
- {error} -
- )} - - - + + + Polygon Coordinates (SVG format): -
-