diff --git a/package-lock.json b/package-lock.json index 60f82ca..a4918d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,6 +85,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -847,9 +848,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -934,9 +935,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1093,9 +1094,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -1107,9 +1108,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -1121,9 +1122,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1135,9 +1136,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1149,9 +1150,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1163,9 +1164,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1177,9 +1178,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1191,9 +1192,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1205,9 +1206,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1219,9 +1220,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -1233,9 +1234,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -1247,9 +1248,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1261,9 +1262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -1275,9 +1276,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1289,9 +1290,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1303,9 +1304,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1317,9 +1318,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1331,9 +1332,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1345,9 +1346,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1359,9 +1360,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -1373,9 +1374,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -1387,9 +1388,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1401,9 +1402,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1415,9 +1416,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -1429,9 +1430,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1528,6 +1529,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1546,9 +1548,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1672,6 +1674,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1956,6 +1959,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2091,9 +2095,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2742,9 +2746,9 @@ } }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -2927,6 +2931,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3017,6 +3022,7 @@ "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" }, @@ -3082,9 +3088,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -3098,31 +3104,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -3389,6 +3395,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/src/components/MitigationCard.jsx b/src/components/MitigationCard.jsx index 435d0b9..09827ae 100644 --- a/src/components/MitigationCard.jsx +++ b/src/components/MitigationCard.jsx @@ -4,12 +4,13 @@ import styles from "./MitigationCard.module.css"; export default function MitigationCard({ group, active, accent, t }) { const [open, setOpen] = useState(false); + return (
- {group.icon} + + {active ? group.icon : "🔒"} +
-
+
{group.title}
-
- {group.measures.length} {group.measures.length !== 1 ? t.measures : t.measure} +
+ {active + ? `${group.measures.length} ${group.measures.length !== 1 ? t.measures : t.measure}` + : `Unlocks at Tier ${group.tier}`}
{active && }
- {active && open && ( -
- {group.measures.map((m, i) => { - const tc = TYPE_COLORS[m.type]; - return ( -
-
- {m.name} - - {t.typeBadges[m.type]} - + + {/* Always rendered — grid-template-rows animates open/close smoothly */} + {active && ( +
+
+ {group.measures.map((m, i) => { + const tc = TYPE_COLORS[m.type]; + return ( +
+
+ {m.name} + + {t.typeBadges[m.type]} + +
+
{m.desc}
-
{m.desc}
-
- ); - })} + ); + })} +
)}
diff --git a/src/components/MitigationCard.module.css b/src/components/MitigationCard.module.css index 1aaf1a2..31ea0b2 100644 --- a/src/components/MitigationCard.module.css +++ b/src/components/MitigationCard.module.css @@ -1,11 +1,23 @@ .card { - border-radius: 12px; - padding: 12px 14px; - transition: all 0.3s; + border-radius: 10px; + padding: 10px 14px; + transition: + border-color 0.25s ease, + background 0.25s ease, + box-shadow 0.25s ease, + transform 0.25s cubic-bezier(0.34, 1.2, 0.64, 1); +} + +.card:not(.cardInactive):hover { + transform: translateY(-2px) scale(1.01); + box-shadow: 0 6px 24px -4px rgba(0, 0, 0, 0.25); + border-width: 2px !important; + filter: brightness(1.08); } .cardInactive { - opacity: 0.5; + padding: 7px 14px; + border-radius: 8px; } .header { @@ -21,40 +33,75 @@ } .icon { - font-size: 24px; + font-size: 20px; + transition: opacity 0.3s ease; } .title { - font-weight: 700; - font-size: 18px; + font-weight: 600; + font-size: 16px; + transition: + color 0.3s ease, + opacity 0.3s ease; } .subtitle { - font-size: 13px; + font-size: 12px; color: var(--text-secondary); + transition: opacity 0.3s ease; } .chevron { - font-size: 22px; + font-size: 20px; color: var(--text-secondary); - transition: transform 0.2s; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } .chevronOpen { transform: rotate(180deg); } +/* ── Smooth expand/collapse ──────────────────────────────────────────────── */ +/* + grid-template-rows trick: animating from 0fr → 1fr smoothly collapses + and expands content of any height without needing to know it in advance. + The inner div needs overflow:hidden to clip during animation. +*/ + +.measuresWrapper { + max-height: 0; + overflow: hidden; + transition: max-height 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.measuresWrapperOpen { + max-height: 600px; + transition: max-height 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + .measures { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; + padding-bottom: 10px; } .measure { background: var(--bg-card); border-radius: 8px; padding: 8px 10px; + transition: + box-shadow 0.2s ease, + transform 0.2s cubic-bezier(0.34, 1.2, 0.64, 1), + background 0.2s ease; +} + +.measure:hover { + transform: translateX(3px); + box-shadow: 0 2px 12px -2px rgba(0, 0, 0, 0.2); + background: var(--bg-sidebar); + filter: brightness(1.1); } .measureHeader { @@ -67,7 +114,7 @@ .measureName { font-weight: 600; - font-size: 16px; + font-size: 15px; color: var(--text-primary); } @@ -81,7 +128,7 @@ } .measureDesc { - font-size: 15px; + font-size: 14px; color: var(--text-secondary); margin-top: 3px; line-height: 1.4; diff --git a/src/components/RadarChart.jsx b/src/components/RadarChart.jsx index f0eea72..6a20703 100644 --- a/src/components/RadarChart.jsx +++ b/src/components/RadarChart.jsx @@ -1,70 +1,441 @@ +import { useRef, useEffect, useState, useCallback } from "react"; import { TIER_BG } from "../constants.js"; import { getTierIndex, polarToCartesian } from "../utils.js"; import styles from "./RadarChart.module.css"; -export default function RadarChart({ values, dimensions, size = 320 }) { - const cx = size / 2, - cy = size / 2, - maxR = size / 2 - 48, - levels = 5, - n = dimensions.length, - step = 360 / n; - const ti = getTierIndex(values); - const tc = TIER_BG[ti]; +// ── Animation helpers ──────────────────────────────────────────────────────── +function animatePoints(el, toStr, duration = 800) { + if (!el) return; + const from = el.getAttribute("points"); + if (!from || from === toStr) { + el.setAttribute("points", toStr); + return; + } + const fromPts = from + .trim() + .split(/\s+/) + .map((p) => p.split(",").map(Number)); + const toPts = toStr + .trim() + .split(/\s+/) + .map((p) => p.split(",").map(Number)); + if (fromPts.length !== toPts.length) { + el.setAttribute("points", toStr); + return; + } + if (el._rafId) cancelAnimationFrame(el._rafId); + let start = null; + function step(ts) { + if (!start) start = ts; + const t = Math.min((ts - start) / duration, 1); + const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + el.setAttribute( + "points", + fromPts + .map((fp, i) => { + const tp = toPts[i]; + return `${fp[0] + (tp[0] - fp[0]) * ease},${fp[1] + (tp[1] - fp[1]) * ease}`; + }) + .join(" "), + ); + if (t < 1) { + el._rafId = requestAnimationFrame(step); + } else { + el.setAttribute("points", toStr); + el._rafId = null; + } + } + el._rafId = requestAnimationFrame(step); +} + +function animateAttr(el, attr, from, to, duration = 800) { + if (!el) return; + const key = `_raf_${attr}`; + if (el[key]) cancelAnimationFrame(el[key]); + let start = null; + function step(ts) { + if (!start) start = ts; + const t = Math.min((ts - start) / duration, 1); + const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + el.setAttribute(attr, from + (to - from) * ease); + if (t < 1) { + el[key] = requestAnimationFrame(step); + } else { + el.setAttribute(attr, to); + el[key] = null; + } + } + el[key] = requestAnimationFrame(step); +} + +function animateColor(el, fromHex, toHex, duration = 800, attrs = ["fill", "stroke"]) { + if (!el || !fromHex || !toHex || fromHex === toHex) return; + const parse = (h) => { + const s = h.replace("#", ""); + return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)]; + }; + const toHexStr = (r, g, b) => "#" + [r, g, b].map((v) => Math.round(v).toString(16).padStart(2, "0")).join(""); + const f = parse(fromHex), + t = parse(toHex); + if (el._colorRafId) cancelAnimationFrame(el._colorRafId); + let start = null; + function step(ts) { + if (!start) start = ts; + const p = Math.min((ts - start) / duration, 1); + const ease = p < 0.5 ? 2 * p * p : -1 + (4 - 2 * p) * p; + const color = toHexStr(f[0] + (t[0] - f[0]) * ease, f[1] + (t[1] - f[1]) * ease, f[2] + (t[2] - f[2]) * ease); + attrs.forEach((a) => el.setAttribute(a, color)); + if (p < 1) { + el._colorRafId = requestAnimationFrame(step); + } else { + attrs.forEach((a) => el.setAttribute(a, toHex)); + el._colorRafId = null; + } + } + el._colorRafId = requestAnimationFrame(step); +} + +function getValueLabelPos(dotX, dotY, cx, cy, offset = 18) { + const dx = dotX - cx; + const dy = dotY - cy; + const len = Math.sqrt(dx * dx + dy * dy) || 1; + const px = -dy / len; + const py = dx / len; + const rx = dx / len; + const ry = dy / len; + return { + x: dotX + px * offset * 0.9 + rx * 4, + y: dotY + py * offset * 0.9 + ry * 4, + }; +} + +// ── Component ──────────────────────────────────────────────────────────────── + +export default function RadarChart({ + values = {}, + dimensions = [], + size = 460, + determiningKey = null, + registerUpdater = null, +}) { + // Safe fallbacks to prevent math from crashing if data is missing + const safeDims = dimensions || []; + const safeVals = values || {}; + + const cx = size / 2; + const cy = size / 2; + const maxR = 130; + const levels = 5; + const n = safeDims.length || 1; // prevents division by zero + const angStep = 360 / n; + + const ti = getTierIndex(safeVals); + const tc = TIER_BG[ti] || "#6b7280"; + + const [tooltip, setTooltip] = useState(null); + + const svgRef = useRef(null); + const riskPolygonRef = useRef(null); + const gridRingRefs = useRef([]); + const dotRefs = useRef([]); + const dotGroupRefs = useRef([]); + const labelRefs = useRef([]); + const prevTcRef = useRef(tc); + const mountedRef = useRef(false); + const dimensionsRef = useRef(safeDims); + + useEffect(() => { + if (!registerUpdater) return; + registerUpdater((newValues) => { + if (!mountedRef.current) return; + const dims = dimensionsRef.current; + const newTi = getTierIndex(newValues); + const newTc = TIER_BG[newTi] || "#6b7280"; + const newRiskPts = dims.map((d, i) => + polarToCartesian(cx, cy, (maxR / levels) * (newValues[d.key] + 1), i * angStep), + ); + const newRiskPointsStr = newRiskPts.map((p) => `${p.x},${p.y}`).join(" "); + + if (riskPolygonRef.current) { + riskPolygonRef.current.setAttribute("points", newRiskPointsStr); + riskPolygonRef.current.setAttribute("fill", newTc); + riskPolygonRef.current.setAttribute("stroke", newTc); + } + dotRefs.current.forEach((el, i) => { + if (!el) return; + el.setAttribute("cx", newRiskPts[i].x); + el.setAttribute("cy", newRiskPts[i].y); + el.setAttribute("fill", newTc); + el.style.color = newTc; + }); + labelRefs.current.forEach((el, i) => { + if (!el) return; + const lp = getValueLabelPos(newRiskPts[i].x, newRiskPts[i].y, cx, cy, 18); + el.setAttribute("x", lp.x); + el.setAttribute("y", lp.y); + el.setAttribute("fill", newTc); + el.textContent = Math.round(newValues[dimensionsRef.current[i].key]); + }); + prevTcRef.current = newTc; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [registerUpdater]); + + const riskPts = safeDims.map((d, i) => + polarToCartesian(cx, cy, (maxR / levels) * ((safeVals[d.key] || 0) + 1), i * angStep), + ); + const riskPointsStr = riskPts.map((p) => `${p.x},${p.y}`).join(" "); + + const gridPointStrs = [1, 2, 3, 4, 5].map((l) => { + const r = (maxR / levels) * l; + return Array.from({ length: safeDims.length }, (_, i) => polarToCartesian(cx, cy, r, i * angStep)) + .map((p) => `${p.x},${p.y}`) + .join(" "); + }); + + useEffect(() => { + mountedRef.current = false; + + riskPolygonRef.current?.setAttribute("points", riskPointsStr); + riskPolygonRef.current?.setAttribute("fill", tc); + riskPolygonRef.current?.setAttribute("stroke", tc); + + gridRingRefs.current.forEach((el, i) => el?.setAttribute("points", gridPointStrs[i])); + + dotRefs.current.forEach((el, i) => { + if (!el) return; + el.setAttribute("cx", riskPts[i].x); + el.setAttribute("cy", riskPts[i].y); + el.setAttribute("fill", tc); + el.style.color = tc; + }); + + labelRefs.current.forEach((el, i) => { + if (!el) return; + const lp = getValueLabelPos(riskPts[i].x, riskPts[i].y, cx, cy, 18); + el.setAttribute("x", lp.x); + el.setAttribute("y", lp.y); + el.setAttribute("fill", tc); + }); + + dotGroupRefs.current.forEach((g, i) => { + if (!g) return; + const ox = riskPts[i].x; + const oy = riskPts[i].y; + g.style.transformOrigin = `${ox}px ${oy}px`; + g.style.opacity = "0"; + g.style.transform = "scale(0.2)"; + g.style.transition = "none"; + requestAnimationFrame(() => + requestAnimationFrame(() => { + const delay = 300 + i * 90; + g.style.transition = `opacity 0.35s ease ${delay}ms, transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) ${delay}ms`; + g.style.opacity = "1"; + g.style.transform = "scale(1)"; + }), + ); + }); + + mountedRef.current = true; + + return () => { + mountedRef.current = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!mountedRef.current) return; + + const prevTc = prevTcRef.current; + const colorChanged = prevTc !== tc; + + animatePoints(riskPolygonRef.current, riskPointsStr); + + if (colorChanged) { + animateColor(riskPolygonRef.current, prevTc, tc, 800, ["fill", "stroke"]); + } + + dotRefs.current.forEach((el, i) => { + if (!el) return; + const fromCx = parseFloat(el.getAttribute("cx") ?? riskPts[i].x); + const fromCy = parseFloat(el.getAttribute("cy") ?? riskPts[i].y); + animateAttr(el, "cx", fromCx, riskPts[i].x); + animateAttr(el, "cy", fromCy, riskPts[i].y); + if (colorChanged) animateColor(el, prevTc, tc, 800, ["fill"]); + }); + + labelRefs.current.forEach((el, i) => { + if (!el) return; + const lp = getValueLabelPos(riskPts[i].x, riskPts[i].y, cx, cy, 18); + const fromX = parseFloat(el.getAttribute("x") ?? lp.x); + const fromY = parseFloat(el.getAttribute("y") ?? lp.y); + animateAttr(el, "x", fromX, lp.x); + animateAttr(el, "y", fromY, lp.y); + if (colorChanged) animateColor(el, prevTc, tc, 800, ["fill"]); + }); + + if (colorChanged) prevTcRef.current = tc; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [riskPointsStr, tc]); + + const handleDotEnter = useCallback( + (e, i) => { + if (!safeDims[i]) return; + const rect = svgRef.current.getBoundingClientRect(); + setTooltip({ + x: (e.clientX - rect.left) * (size / rect.width), + y: (e.clientY - rect.top) * (size / rect.height), + label: safeDims[i].label || safeDims[i].shortLabel, + value: safeVals[safeDims[i].key], + }); + }, + [safeDims, safeVals, size], + ); + + const handleDotLeave = useCallback(() => setTooltip(null), []); + + const tooltipW = 120, + tooltipH = 56; + const tipX = tooltip ? Math.min(Math.max(tooltip.x - tooltipW / 2, 8), size - tooltipW - 8) : 0; + const tipY = tooltip ? tooltip.y - tooltipH - 18 : 0; + + // ── THE FIX: Early return is now safely AFTER all hooks! ─────────────── + if (!dimensions?.length || !values) return null; + + // ── Render ─────────────────────────────────────────────────────────────── return ( - - {[1, 2, 3, 4, 5].map((l) => { - const r = (maxR / levels) * l; - const pts = Array.from({ length: n }, (_, i) => polarToCartesian(cx, cy, r, i * step)); + + + + + + + + + {gridPointStrs.map((_, idx) => { + const l = idx + 1; return ( `${p.x},${p.y}`).join(" ")} + key={`grid-ring-${l}`} + ref={(el) => (gridRingRefs.current[idx] = el)} fill="none" stroke={l === 5 ? "var(--grid-line-outer)" : "var(--grid-line)"} - strokeWidth={l === 5 ? 1.5 : 0.7} + strokeWidth={l === 5 ? 1.8 : 1} /> ); })} - {dimensions.map((_, i) => { - const p = polarToCartesian(cx, cy, maxR, i * step); - return ; - })} - {(() => { - const pts = dimensions.map((d, i) => polarToCartesian(cx, cy, (maxR / levels) * (values[d.key] + 1), i * step)); + + {dimensions.map((d, i) => { + const p = polarToCartesian(cx, cy, maxR, i * angStep); return ( - <> - `${p.x},${p.y}`).join(" ")} - fill={tc} - fillOpacity={0.25} - stroke={tc} - strokeWidth={2.5} - /> - {pts.map((p, i) => ( - - ))} - + ); - })()} + })} + + + + {dimensions.map((d, i) => ( + (dotGroupRefs.current[i] = el)}> + (dotRefs.current[i] = el)} + r={7} + stroke="white" + strokeWidth={2.5} + className={styles.circle} + style={{ cursor: "pointer", color: tc }} + onMouseEnter={(e) => handleDotEnter(e, i)} + onMouseLeave={handleDotLeave} + /> + (labelRefs.current[i] = el)} + textAnchor="middle" + dominantBaseline="middle" + fontSize="13" + fontWeight="bold" + className={styles.valueLabel} + > + {Math.round(values[d.key])} + + + ))} + {dimensions.map((d, i) => { - const lp = polarToCartesian(cx, cy, maxR + 26, i * step); + const lp = polarToCartesian(cx, cy, maxR + 45, i * angStep); + const isDetermining = determiningKey && d.key === determiningKey; return ( - {d.shortLabel} + {d.label || d.shortLabel} ); })} + + {tooltip && ( + + + + + + + + + + {tooltip.label} + + + {tooltip.value} + + {" "} + / 4 + + + + + + )} ); } diff --git a/src/components/RadarChart.module.css b/src/components/RadarChart.module.css index b3bae18..df3a99c 100644 --- a/src/components/RadarChart.module.css +++ b/src/components/RadarChart.module.css @@ -1,3 +1,115 @@ +/* src/components/RadarChart.module.css */ + +/* ── Container ──────────────────────────────────────────────────────────── */ + .chart { width: 100%; + background: var(--bg-card); + border-radius: 16px; + padding: 28px 28px 20px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3); +} + +/* ── Grid visibility ────────────────────────────────────────────────────── */ + +/* ── Risk polygon ───────────────────────────────────────────────────────── */ + +/* + points / fill / stroke → all JS-animated via requestAnimationFrame. + No CSS transitions on these — JS owns them exclusively. + Only the entrance draw animation is CSS (runs once on mount). +*/ +.polygon { + animation: draw 1.4s ease-out forwards; +} + +/* ── Dots ───────────────────────────────────────────────────────────────── */ + +/* + cx / cy / fill → JS-animated via animateAttr / animateColor. + No CSS transitions on these — JS owns them exclusively. + The wrapper handles the entrance scale via inline style transitions (set in JS). +*/ +.circle { + transition: r 0.3s ease; + animation: pulse 2.4s ease-in-out infinite; +} + +.circle:hover { + r: 9px; + animation: pulseHover 0.8s ease-in-out infinite; +} + +/* ── Pulse glow keyframes ────────────────────────────────────────────────── */ + +@keyframes pulse { + 0% { + filter: drop-shadow(0 0 1px currentColor); + opacity: 1; + } + 50% { + filter: drop-shadow(0 0 4px currentColor); + opacity: 0.9; + } + 100% { + filter: drop-shadow(0 0 1px currentColor); + opacity: 1; + } +} + +@keyframes pulseHover { + 0% { + filter: drop-shadow(0 0 3px currentColor); + } + 50% { + filter: drop-shadow(0 0 7px currentColor); + } + 100% { + filter: drop-shadow(0 0 3px currentColor); + } +} + +/* ── Value labels ────────────────────────────────────────────────────────── */ + +/* + x / y / fill → JS-animated. No CSS transitions. +*/ +.valueLabel { + /* intentionally empty — JS owns x, y, fill */ +} + +/* ── Axis labels ─────────────────────────────────────────────────────────── */ + +.chart text { + transition: fill 0.6s ease; +} + +/* ── Tooltip ─────────────────────────────────────────────────────────────── */ + +.tooltip { + animation: tooltipIn 0.2s cubic-bezier(0.34, 1.4, 0.64, 1) forwards; +} + +@keyframes tooltipIn { + from { + opacity: 0; + transform: scale(0.88) translateY(4px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* ── Entrance animations ─────────────────────────────────────────────────── */ + +@keyframes draw { + from { + stroke-dasharray: 1200; + stroke-dashoffset: 1200; + } + to { + stroke-dasharray: 1200; + stroke-dashoffset: 0; + } } diff --git a/src/components/RiskRadar.jsx b/src/components/RiskRadar.jsx index fb9ad1f..c55945e 100644 --- a/src/components/RiskRadar.jsx +++ b/src/components/RiskRadar.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef, useCallback } from "react"; import T from "../i18n.js"; import { useTheme } from "../theme.js"; import { VERSION, TIER_BG, TYPE_COLORS } from "../constants.js"; @@ -16,12 +16,91 @@ export default function RiskRadar() { }); const { setTheme, isDark } = useTheme(); const [docsOpen, setDocsOpen] = useState(false); + + // React state holds only the final integer values (for tier badge, level labels etc.) const [values, setValues] = useState({ codeType: 0, language: 1, deployment: 0, data: 0, blastRadius: 0 }); + + // Animated float values live in a ref — updated every rAF frame + // Both sliders and RadarChart read from this ref via their own refs + const floatValuesRef = useRef({ ...values }); + const rafRef = useRef(null); + + // Slider input refs — we write value directly to DOM in the rAF loop + const sliderRefs = useRef({}); + + // RadarChart ref — we call its updateValues method directly + const chartValuesRef = useRef(null); + const t = T[lang]; - const ti = getTierIndex(values); + const keys = Object.keys(values); + + // Animate from current float values to target integers + const animateTo = useCallback( + (target, duration = 600) => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + const from = { ...floatValuesRef.current }; + let start = null; + + function step(ts) { + if (!start) start = ts; + const p = Math.min((ts - start) / duration, 1); + const ease = p < 0.5 ? 2 * p * p : -1 + (4 - 2 * p) * p; + + const next = {}; + keys.forEach((k) => { + next[k] = from[k] + (target[k] - from[k]) * ease; + }); + + // Update float ref + floatValuesRef.current = next; + + // Update slider DOM directly — same frame, zero lag + keys.forEach((k) => { + const el = sliderRefs.current[k]; + if (el) el.value = next[k]; + }); + + // Update chart ref so RadarChart reads new float values this frame + if (chartValuesRef.current) chartValuesRef.current(next); + + if (p < 1) { + rafRef.current = requestAnimationFrame(step); + } else { + floatValuesRef.current = target; + keys.forEach((k) => { + const el = sliderRefs.current[k]; + if (el) el.value = target[k]; + }); + if (chartValuesRef.current) chartValuesRef.current(target); + // Only update React state at the end — triggers re-render once + setValues(target); + rafRef.current = null; + } + } + + rafRef.current = requestAnimationFrame(step); + }, + [keys], + ); + + const roundedValues = {}; + keys.forEach((k) => { + roundedValues[k] = Math.round(values[k]); + }); + + const ti = getTierIndex(roundedValues); const tier = t.tiers[ti]; const tc = TIER_BG[ti]; - const set = (k, v) => setValues((p) => ({ ...p, [k]: v })); + + const set = (k, v) => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + const rounded = Math.round(v); + const next = { ...floatValuesRef.current, [k]: rounded }; + floatValuesRef.current = next; + if (chartValuesRef.current) chartValuesRef.current(next); + setValues((p) => ({ ...p, [k]: rounded })); + }; + const activeCount = t.mitigations.filter((g) => g.tier <= ti + 1).reduce((s, g) => s + g.measures.length, 0); const toggleLang = () => { @@ -59,11 +138,11 @@ export default function RiskRadar() { {/* Presets */}
{t.presets.map((p) => { - const active = JSON.stringify(values) === JSON.stringify(p.values); + const active = JSON.stringify(roundedValues) === JSON.stringify(p.values); return (