From 5234ba5662fa8177f856d311df82d1e2de7cbfad Mon Sep 17 00:00:00 2001 From: duckietm Date: Tue, 28 Apr 2026 09:43:00 +0200 Subject: [PATCH 01/20] =?UTF-8?q?=E3=8A=99=EF=B8=8F=20Updates=20for=20the?= =?UTF-8?q?=20packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit please remove the node_modules dir and do yarn install --- package.json | 40 +- yarn.lock | 1311 +++++++++++++++++++++----------------------------- 2 files changed, 559 insertions(+), 792 deletions(-) diff --git a/package.json b/package.json index efca2c3..f43f8b0 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,18 @@ "eslint": "eslint ./src" }, "dependencies": { - "@babel/runtime": "^7.26.9", + "@babel/runtime": "^7.29.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", - "@tanstack/react-virtual": "3.2.0", - "@types/react-transition-group": "^4.4.10", - "dompurify": "^3.1.5", + "@tanstack/react-virtual": "3.13.24", + "@types/react-transition-group": "^4.4.12", + "dompurify": "^3.4.1", "emoji-mart": "^5.6.0", "emoji-toolkit": "10.0.0", - "framer-motion": "^11.2.12", - "react": "^19.2.4", + "framer-motion": "^12.38.0", + "react": "^19.2.5", "react-bootstrap": "^2.10.10", - "react-dom": "^19.2.4", + "react-dom": "^19.2.5", "react-icons": "^5.5.0", "react-slider": "^2.0.6", "react-tiny-popover": "^8.1.6", @@ -30,23 +30,23 @@ }, "devDependencies": { "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/postcss": "^4.2.0", - "@types/node": "^25.3.0", + "@tailwindcss/postcss": "^4.2.4", + "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-slider": "^1.3.6", - "@typescript-eslint/eslint-plugin": "^8.56.0", - "@typescript-eslint/parser": "^8.56.0", - "@vitejs/plugin-react": "^5.1.4", - "eslint": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.59.1", + "@typescript-eslint/parser": "^8.59.1", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", - "postcss": "^8.5.6", + "eslint-plugin-react-hooks": "^7.1.1", + "postcss": "^8.5.12", "postcss-nested": "^7.0.2", - "sass": "^1.97.3", - "tailwindcss": "^4.2.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.56.0", - "vite": "^8.0.8" + "sass": "^1.99.0", + "tailwindcss": "^4.2.4", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.1", + "vite": "^8.0.10" } } diff --git a/yarn.lock b/yarn.lock index 4c4c563..880df0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,7 +21,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== -"@babel/core@^7.24.4", "@babel/core@^7.29.0": +"@babel/core@^7.24.4": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== @@ -86,11 +86,6 @@ "@babel/helper-validator-identifier" "^7.28.5" "@babel/traverse" "^7.28.6" -"@babel/helper-plugin-utils@^7.27.1": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" - integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== - "@babel/helper-string-parser@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" @@ -114,28 +109,14 @@ "@babel/template" "^7.28.6" "@babel/types" "^7.29.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.4", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": +"@babel/parser@^7.24.4", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": version "7.29.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1" integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA== dependencies: "@babel/types" "^7.29.0" -"@babel/plugin-transform-react-jsx-self@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92" - integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== - dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - -"@babel/plugin-transform-react-jsx-source@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0" - integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== - dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - -"@babel/runtime@^7.24.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.24.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.29.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": version "7.29.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e" integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== @@ -162,7 +143,7 @@ "@babel/types" "^7.29.0" debug "^4.3.1" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.29.0": +"@babel/types@^7.28.6", "@babel/types@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== @@ -170,25 +151,25 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" -"@emnapi/core@^1.8.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.9.1.tgz#2143069c744ca2442074f8078462e51edd63c7bd" - integrity sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA== +"@emnapi/core@1.10.0", "@emnapi/core@^1.8.1": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" + integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== dependencies: - "@emnapi/wasi-threads" "1.2.0" + "@emnapi/wasi-threads" "1.2.1" tslib "^2.4.0" -"@emnapi/runtime@^1.8.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.9.1.tgz#115ff2a0d589865be6bd8e9d701e499c473f2a8d" - integrity sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA== +"@emnapi/runtime@1.10.0", "@emnapi/runtime@^1.8.1": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" + integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.2.0", "@emnapi/wasi-threads@^1.1.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz#a19d9772cc3d195370bf6e2a805eec40aa75e18e" - integrity sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg== +"@emnapi/wasi-threads@1.2.1", "@emnapi/wasi-threads@^1.1.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" + integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== dependencies: tslib "^2.4.0" @@ -202,136 +183,6 @@ resolved "https://registry.yarnpkg.com/@emoji-mart/react/-/react-1.1.1.tgz#ddad52f93a25baf31c5383c3e7e4c6e05554312a" integrity sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g== -"@esbuild/aix-ppc64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz#4c585002f7ad694d38fe0e8cbf5cfd939ccff327" - integrity sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q== - -"@esbuild/android-arm64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz#7625d0952c3b402d3ede203a16c9f2b78f8a4827" - integrity sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw== - -"@esbuild/android-arm@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.4.tgz#9a0cf1d12997ec46dddfb32ce67e9bca842381ac" - integrity sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ== - -"@esbuild/android-x64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.4.tgz#06e1fdc6283fccd6bc6aadd6754afce6cf96f42e" - integrity sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw== - -"@esbuild/darwin-arm64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz#6c550ee6c0273bcb0fac244478ff727c26755d80" - integrity sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ== - -"@esbuild/darwin-x64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz#ed7a125e9f25ce0091b9aff783ee943f6ba6cb86" - integrity sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw== - -"@esbuild/freebsd-arm64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz#597dc8e7161dba71db4c1656131c1f1e9d7660c6" - integrity sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw== - -"@esbuild/freebsd-x64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz#ea171f9f4f00efaa8e9d3fe8baa1b75d757d1b36" - integrity sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ== - -"@esbuild/linux-arm64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz#e52d57f202369386e6dbcb3370a17a0491ab1464" - integrity sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA== - -"@esbuild/linux-arm@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz#5e0c0b634908adbce0a02cebeba8b3acac263fb6" - integrity sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg== - -"@esbuild/linux-ia32@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz#5f90f01f131652473ec06b038a14c49683e14ec7" - integrity sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA== - -"@esbuild/linux-loong64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz#63bacffdb99574c9318f9afbd0dd4fff76a837e3" - integrity sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA== - -"@esbuild/linux-mips64el@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz#c4b6952eca6a8efff67fee3671a3536c8e67b7eb" - integrity sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw== - -"@esbuild/linux-ppc64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz#6dea67d3d98c6986f1b7769e4f1848e5ae47ad58" - integrity sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA== - -"@esbuild/linux-riscv64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz#9ad2b4c3c0502c6bada9c81997bb56c597853489" - integrity sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw== - -"@esbuild/linux-s390x@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz#c43d3cfd073042ca6f5c52bb9bc313ed2066ce28" - integrity sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA== - -"@esbuild/linux-x64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz#45fa173e0591ac74d80d3cf76704713e14e2a4a6" - integrity sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA== - -"@esbuild/netbsd-arm64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz#366b0ef40cdb986fc751cbdad16e8c25fe1ba879" - integrity sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q== - -"@esbuild/netbsd-x64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz#e985d49a3668fd2044343071d52e1ae815112b3e" - integrity sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg== - -"@esbuild/openbsd-arm64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz#6fb4ab7b73f7e5572ce5ec9cf91c13ff6dd44842" - integrity sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow== - -"@esbuild/openbsd-x64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz#641f052040a0d79843d68898f5791638a026d983" - integrity sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ== - -"@esbuild/openharmony-arm64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz#fc1d33eac9d81ae0a433b3ed1dd6171a20d4e317" - integrity sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg== - -"@esbuild/sunos-x64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz#af2cd5ca842d6d057121f66a192d4f797de28f53" - integrity sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g== - -"@esbuild/win32-arm64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz#78ec7e59bb06404583d4c9511e621db31c760de3" - integrity sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg== - -"@esbuild/win32-ia32@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz#0e616aa488b7ee5d2592ab070ff9ec06a9fddf11" - integrity sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw== - -"@esbuild/win32-x64@0.27.4": - version "0.27.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz#1f7ba71a3d6155d44a6faa8dbe249c62ab3e408c" - integrity sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg== - "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" @@ -344,55 +195,63 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== -"@eslint/config-array@^0.23.3": - version "0.23.3" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.3.tgz#3f4a93dd546169c09130cbd10f2415b13a20a219" - integrity sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw== +"@eslint/config-array@^0.23.5": + version "0.23.5" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.5.tgz#56e86d243049195d8acc0c06a1b3dfdc3fa3de95" + integrity sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA== dependencies: - "@eslint/object-schema" "^3.0.3" + "@eslint/object-schema" "^3.0.5" debug "^4.3.1" minimatch "^10.2.4" -"@eslint/config-helpers@^0.5.3": - version "0.5.3" - resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.3.tgz#721fe6bbb90d74b0c80d6ff2428e5bbcb002becb" - integrity sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw== +"@eslint/config-helpers@^0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.5.tgz#ae16134e4792ac5fbdc533548a24ac1ea9f7f3ae" + integrity sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w== dependencies: - "@eslint/core" "^1.1.1" + "@eslint/core" "^1.2.1" -"@eslint/core@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.1.1.tgz#450f3d2be2d463ccd51119544092256b4e88df32" - integrity sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ== +"@eslint/core@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.2.1.tgz#c1da7cd1b82fa8787f98b5629fb811848a1b63ce" + integrity sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ== dependencies: "@types/json-schema" "^7.0.15" -"@eslint/object-schema@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.3.tgz#5bf671e52e382e4adc47a9906f2699374637db6b" - integrity sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ== +"@eslint/object-schema@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.5.tgz#88e9bf4d11d2b19c082e78ebe7ce88724a5eb091" + integrity sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw== -"@eslint/plugin-kit@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz#eb9e6689b56ce8bc1855bb33090e63f3fc115e8e" - integrity sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ== +"@eslint/plugin-kit@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz#c4125fd015eceeb09b793109fdbcd4dd0a02d346" + integrity sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ== dependencies: - "@eslint/core" "^1.1.1" + "@eslint/core" "^1.2.1" levn "^0.4.1" -"@humanfs/core@^0.19.1": - version "0.19.1" - resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" - integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== +"@humanfs/core@^0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.2.tgz#a8272ca03b2acf492670222b2320b6c421bfde60" + integrity sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA== + dependencies: + "@humanfs/types" "^0.15.0" "@humanfs/node@^0.16.6": - version "0.16.7" - resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" - integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== + version "0.16.8" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.8.tgz#8f800cccc13f4f8cd3116e2d9c0a94939da3e3ed" + integrity sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ== dependencies: - "@humanfs/core" "^0.19.1" + "@humanfs/core" "^0.19.2" + "@humanfs/types" "^0.15.0" "@humanwhocodes/retry" "^0.4.0" +"@humanfs/types@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@humanfs/types/-/types-0.15.0.tgz#f2a09f62012390b2bff3fc6fb248ddec8c09a090" + integrity sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q== + "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" @@ -403,6 +262,27 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== +"@internationalized/date@^3.12.1": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.12.1.tgz#6e07ff34fafcba9a155cb8d4e0505f362e57a56b" + integrity sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@internationalized/number@^3.6.6": + version "3.6.6" + resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.6.tgz#8bd41940dd9c08e265ec13c9331bad4fd57267e1" + integrity sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@internationalized/string@^3.2.8": + version "3.2.8" + resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.2.8.tgz#ca27e2ba127528d6b55a20fb5302bfebb852e1d9" + integrity sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA== + dependencies: + "@swc/helpers" "^0.5.0" + "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" @@ -437,13 +317,18 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@napi-rs/wasm-runtime@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz#e25454b4d44cfabd21d1bc801705359870e33ecc" - integrity sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw== +"@napi-rs/wasm-runtime@^1.1.1", "@napi-rs/wasm-runtime@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1" + integrity sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow== dependencies: "@tybys/wasm-util" "^0.10.1" +"@oxc-project/types@=0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.127.0.tgz#8374fcdfb4a641861218daa5700c447c00b66663" + integrity sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ== + "@parcel/watcher-android-arm64@2.5.6": version "2.5.6" resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564" @@ -539,11 +424,17 @@ integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== "@react-aria/ssr@^3.5.0": - version "3.9.10" - resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.10.tgz#7fdc09e811944ce0df1d7e713de1449abd7435e6" - integrity sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ== + version "3.10.0" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.10.0.tgz#1cea873259f3dbbb5afcf1b2ff1e20e37653085b" + integrity sha512-mnelvACtfNWWKFCT1YHebxJRmfBmmANGwHQhCFPByMVTx1L8RumcaLxChYkE87g2KPuP5xX2il/oRn1DytW+qQ== dependencies: "@swc/helpers" "^0.5.0" + react-aria "3.48.0" + +"@react-types/shared@^3.34.0": + version "3.34.0" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.34.0.tgz#f9688443fdf8e29f7a06b499598a930be96a2f65" + integrity sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ== "@restart/hooks@^0.4.9": version "0.4.16" @@ -574,140 +465,99 @@ uncontrollable "^8.0.4" warning "^4.0.3" -"@rolldown/pluginutils@1.0.0-rc.3": - version "1.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz#8a88cc92a0f741befc7bc109cb1a4c6b9408e1c5" - integrity sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q== +"@rolldown/binding-android-arm64@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz#0a502a88c39d0ffa81aa30b561dade6f6217dcc5" + integrity sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ== -"@rollup/rollup-android-arm-eabi@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz#043f145716234529052ef9e1ce1d847ffbe9e674" - integrity sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA== +"@rolldown/binding-darwin-arm64@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz#8b7f05ac9000ab19161a79a0346b1b64a1bc7ba3" + integrity sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw== -"@rollup/rollup-android-arm64@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz#023e1bd146e7519087dfd9e8b29e4cf9f8ecd35c" - integrity sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA== +"@rolldown/binding-darwin-x64@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz#f8b465b3a4e992053890b162f1ae19e4f1719a6a" + integrity sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw== -"@rollup/rollup-darwin-arm64@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz#55ccb5487c02419954c57a7a80602885d616e1ee" - integrity sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw== +"@rolldown/binding-freebsd-x64@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz#a8281e14fa9c243fe22dc2d0e54900e66b31935e" + integrity sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw== -"@rollup/rollup-darwin-x64@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz#254b65404b14488c83225e88b8819376ad71a784" - integrity sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew== +"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz#cd29cf869ddd4fac8d6929abf94b91ddb0494650" + integrity sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ== -"@rollup/rollup-freebsd-arm64@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz#6377ff38c052c76fcaffb7b2728d3172fe676fe6" - integrity sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w== +"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz#91c331236ec3728366218d61a62f0bd226546c6c" + integrity sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q== -"@rollup/rollup-freebsd-x64@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz#ba3902309d088eaf7139b916f09b7140b28b406d" - integrity sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g== +"@rolldown/binding-linux-arm64-musl@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz#80108957db752e7826836e22240e56b8140e9684" + integrity sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg== -"@rollup/rollup-linux-arm-gnueabihf@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz#e011b9a14638267e53b446286e838dbdaf53f167" - integrity sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g== +"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz#1dce51148cbc6bab3c3f9157b5323d2a31aac924" + integrity sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA== -"@rollup/rollup-linux-arm-musleabihf@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz#0bce9ce9a009490abd28fd922dd97ed521311afe" - integrity sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg== +"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz#d4a0d2e01d8d441e4ac3af3fa68eec17a7d0e9cd" + integrity sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA== -"@rollup/rollup-linux-arm64-gnu@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz#6f6cfbbf324fbb4ceff213abdf7f322fd45d25ff" - integrity sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ== +"@rolldown/binding-linux-x64-gnu@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz#0ac8b3139cefeea798ad147f30ea70572b133af1" + integrity sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA== -"@rollup/rollup-linux-arm64-musl@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz#f7cb3eecaea9c151ef77342af05f38ae924bf795" - integrity sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA== +"@rolldown/binding-linux-x64-musl@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz#2af61bee087571728f58f1c47734bbbd41dd7050" + integrity sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw== -"@rollup/rollup-linux-loong64-gnu@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz#499bfac6bb669fd88bb664357bf6be996a28b92f" - integrity sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ== +"@rolldown/binding-openharmony-arm64@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz#56c1afbf6c592819abf47b4a983987dc288b30c1" + integrity sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA== -"@rollup/rollup-linux-loong64-musl@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz#127dfac08764764396bbe04453c545d38a3ab518" - integrity sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw== +"@rolldown/binding-wasm32-wasi@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz#5d112ff4dd0d268a60fb4e0eb3077e3ea2531f0d" + integrity sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA== + dependencies: + "@emnapi/core" "1.10.0" + "@emnapi/runtime" "1.10.0" + "@napi-rs/wasm-runtime" "^1.1.4" -"@rollup/rollup-linux-ppc64-gnu@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz#6a72f4d95852aac18326c5bf708393e8f3a41b70" - integrity sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw== +"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz#5125a85222d64a543201d28e16a395cc45bf4d17" + integrity sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA== -"@rollup/rollup-linux-ppc64-musl@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz#ba8674666b00d6f9066cb9a5771a8430c34d2de6" - integrity sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg== +"@rolldown/binding-win32-x64-msvc@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz#fc6b78e759a0bb2054b5c0a3489da12b2cae54b4" + integrity sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg== -"@rollup/rollup-linux-riscv64-gnu@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz#17cc38b2a71e302547cad29bcf78d0db2618c922" - integrity sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg== +"@rolldown/pluginutils@1.0.0-rc.17": + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz#a89b30833fb628bc834fe2e89fea93a2da9fa69a" + integrity sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg== -"@rollup/rollup-linux-riscv64-musl@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz#e36a41e2d8bd247331bd5cfc13b8c951d33454a2" - integrity sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg== - -"@rollup/rollup-linux-s390x-gnu@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz#1687265f1f4bdea0726c761a58c2db9933609d68" - integrity sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ== - -"@rollup/rollup-linux-x64-gnu@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz#56a6a0d9076f2a05a976031493b24a20ddcc0e77" - integrity sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg== - -"@rollup/rollup-linux-x64-musl@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz#bc240ebb5b9fd8d41ca8a80cb458452e8c187e0f" - integrity sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w== - -"@rollup/rollup-openbsd-x64@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz#6f80d48a006c4b2ffa7724e95a3e33f6975872af" - integrity sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw== - -"@rollup/rollup-openharmony-arm64@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz#8f6db6f70d0a48abd833b263cd6dd3e7199c4c0e" - integrity sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA== - -"@rollup/rollup-win32-arm64-msvc@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz#b68989bfa815d0b3d4e302ecd90bda744438b177" - integrity sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g== - -"@rollup/rollup-win32-ia32-msvc@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz#c098e45338c50f22f1b288476354f025b746285b" - integrity sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg== - -"@rollup/rollup-win32-x64-gnu@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz#2c9e15be155b79d05999953b1737b2903842e903" - integrity sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg== - -"@rollup/rollup-win32-x64-msvc@4.60.1": - version "4.60.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz#23b860113e9f87eea015d1fa3a4240a52b42fcd4" - integrity sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ== +"@rolldown/pluginutils@1.0.0-rc.7": + version "1.0.0-rc.7" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz#0414869467f0e471a6515d4f506c85fde867e022" + integrity sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA== "@swc/helpers@^0.5.0": - version "0.5.20" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.20.tgz#d1d0f1e18ff6592c96a4931b4031298619129585" - integrity sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.21.tgz#0b1b020317ee1282860ca66f7e9a7c7790f05ae0" + integrity sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg== dependencies: tslib "^2.8.0" @@ -718,10 +568,10 @@ dependencies: mini-svg-data-uri "^1.2.3" -"@tailwindcss/node@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.2.2.tgz#840e904226dc1b379609de8a72323fc211568993" - integrity sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA== +"@tailwindcss/node@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.2.4.tgz#1f7fc0c1741037ded1fa92fbe62a786a197771ce" + integrity sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA== dependencies: "@jridgewell/remapping" "^2.3.5" enhanced-resolve "^5.19.0" @@ -729,57 +579,57 @@ lightningcss "1.32.0" magic-string "^0.30.21" source-map-js "^1.2.1" - tailwindcss "4.2.2" + tailwindcss "4.2.4" -"@tailwindcss/oxide-android-arm64@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz#61d9ec5c18394fe7a972e99e19e6065e833da77c" - integrity sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg== +"@tailwindcss/oxide-android-arm64@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz#d533e52ee98d58f55d1d4753774251513ba8a911" + integrity sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g== -"@tailwindcss/oxide-darwin-arm64@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz#9ad7b141789dae235c85d2f7874592bf869f636e" - integrity sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg== +"@tailwindcss/oxide-darwin-arm64@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz#2a6250aa7d8791fc1b5797e64e09e51da57514a6" + integrity sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg== -"@tailwindcss/oxide-darwin-x64@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz#a5899f1fbe55c4eddcbc871b835d5183ba34658c" - integrity sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw== +"@tailwindcss/oxide-darwin-x64@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz#d647299812946b6ab5140c61a334c8ebc8d877de" + integrity sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg== -"@tailwindcss/oxide-freebsd-x64@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz#76185bb1bea9af915a5b9f465323861646587e21" - integrity sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ== +"@tailwindcss/oxide-freebsd-x64@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz#019b7fce37aaf5ddfed0f231c536108292e87ffb" + integrity sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw== -"@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz#74c17c69b2015f7600d566ab0990aaac8701128e" - integrity sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ== +"@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz#c88a95d69095e84f811b302daa66f5287ad8ce0f" + integrity sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA== -"@tailwindcss/oxide-linux-arm64-gnu@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz#38a846d9d5795bc3b57951172044d8dbb3c79aa6" - integrity sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw== +"@tailwindcss/oxide-linux-arm64-gnu@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz#1292f1c222994bfe4a5e990ac0a701de6487dd02" + integrity sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw== -"@tailwindcss/oxide-linux-arm64-musl@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz#f4cc4129c17d3f2bcb01efef4d7a2f381e5e3f53" - integrity sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag== +"@tailwindcss/oxide-linux-arm64-musl@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz#afb6492b22616f0d9d3346d39c1a6e285f994a08" + integrity sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g== -"@tailwindcss/oxide-linux-x64-gnu@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz#7c4a00b0829e12736bd72ec74e1c08205448cc2e" - integrity sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg== +"@tailwindcss/oxide-linux-x64-gnu@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz#400b0ccfc53937c7804ed8e0e9652b42bd86f2eb" + integrity sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA== -"@tailwindcss/oxide-linux-x64-musl@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz#711756d7bbe97e221fc041b63a4f385b85ba4321" - integrity sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ== +"@tailwindcss/oxide-linux-x64-musl@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz#5c23c476e5de4ed9cd6ab39c2718b9a4be2bbb2b" + integrity sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA== -"@tailwindcss/oxide-wasm32-wasi@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz#ed6d28567b7abb8505f824457c236d2cd07ee18e" - integrity sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q== +"@tailwindcss/oxide-wasm32-wasi@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz#21b7f53ba7c6c03f26ccb8cef5d09f5c2973ae5e" + integrity sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw== dependencies: "@emnapi/core" "^1.8.1" "@emnapi/runtime" "^1.8.1" @@ -788,56 +638,56 @@ "@tybys/wasm-util" "^0.10.1" tslib "^2.8.1" -"@tailwindcss/oxide-win32-arm64-msvc@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz#f2d0360e5bc06fe201537fb08193d3780e7dd24f" - integrity sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ== +"@tailwindcss/oxide-win32-arm64-msvc@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz#13bc1cf3818e3345a965d36b40c237817124d070" + integrity sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ== -"@tailwindcss/oxide-win32-x64-msvc@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz#10fc71b73883f9c3999b5b8c338fd96a45240dcb" - integrity sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA== +"@tailwindcss/oxide-win32-x64-msvc@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz#5476dbbbf6b8934d58452340cec737fdaa5ec8c6" + integrity sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw== -"@tailwindcss/oxide@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.2.2.tgz#c6534cb4b22650df605a58258235523a6abd7de8" - integrity sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg== +"@tailwindcss/oxide@4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.2.4.tgz#e2ca51d04e8ad94d569222fa727de479b097db39" + integrity sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q== optionalDependencies: - "@tailwindcss/oxide-android-arm64" "4.2.2" - "@tailwindcss/oxide-darwin-arm64" "4.2.2" - "@tailwindcss/oxide-darwin-x64" "4.2.2" - "@tailwindcss/oxide-freebsd-x64" "4.2.2" - "@tailwindcss/oxide-linux-arm-gnueabihf" "4.2.2" - "@tailwindcss/oxide-linux-arm64-gnu" "4.2.2" - "@tailwindcss/oxide-linux-arm64-musl" "4.2.2" - "@tailwindcss/oxide-linux-x64-gnu" "4.2.2" - "@tailwindcss/oxide-linux-x64-musl" "4.2.2" - "@tailwindcss/oxide-wasm32-wasi" "4.2.2" - "@tailwindcss/oxide-win32-arm64-msvc" "4.2.2" - "@tailwindcss/oxide-win32-x64-msvc" "4.2.2" + "@tailwindcss/oxide-android-arm64" "4.2.4" + "@tailwindcss/oxide-darwin-arm64" "4.2.4" + "@tailwindcss/oxide-darwin-x64" "4.2.4" + "@tailwindcss/oxide-freebsd-x64" "4.2.4" + "@tailwindcss/oxide-linux-arm-gnueabihf" "4.2.4" + "@tailwindcss/oxide-linux-arm64-gnu" "4.2.4" + "@tailwindcss/oxide-linux-arm64-musl" "4.2.4" + "@tailwindcss/oxide-linux-x64-gnu" "4.2.4" + "@tailwindcss/oxide-linux-x64-musl" "4.2.4" + "@tailwindcss/oxide-wasm32-wasi" "4.2.4" + "@tailwindcss/oxide-win32-arm64-msvc" "4.2.4" + "@tailwindcss/oxide-win32-x64-msvc" "4.2.4" -"@tailwindcss/postcss@^4.2.0": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@tailwindcss/postcss/-/postcss-4.2.2.tgz#56570116b136f32c135357544fec6776a6a49dfd" - integrity sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ== +"@tailwindcss/postcss@^4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@tailwindcss/postcss/-/postcss-4.2.4.tgz#548ed07584a41411574e8b1ec5f1543d09c439a4" + integrity sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg== dependencies: "@alloc/quick-lru" "^5.2.0" - "@tailwindcss/node" "4.2.2" - "@tailwindcss/oxide" "4.2.2" + "@tailwindcss/node" "4.2.4" + "@tailwindcss/oxide" "4.2.4" postcss "^8.5.6" - tailwindcss "4.2.2" + tailwindcss "4.2.4" -"@tanstack/react-virtual@3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.2.0.tgz#fb70f9c6baee753a5a0f7618ac886205d5a02af9" - integrity sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg== +"@tanstack/react-virtual@3.13.24": + version "3.13.24" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz#77af3d5dcf77358d805b7b3b06d3221af7bd3f6f" + integrity sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg== dependencies: - "@tanstack/virtual-core" "3.2.0" + "@tanstack/virtual-core" "3.14.0" -"@tanstack/virtual-core@3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz#874d36135e4badce2719e7bdc556ce240cbaff14" - integrity sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ== +"@tanstack/virtual-core@3.14.0": + version "3.14.0" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz#c8839d0d702b8af47c0e57d4ab72fc3ba8bbf3da" + integrity sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q== "@tybys/wasm-util@^0.10.1": version "0.10.1" @@ -846,45 +696,12 @@ dependencies: tslib "^2.4.0" -"@types/babel__core@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" - integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" - integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" - integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" - integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== - dependencies: - "@babel/types" "^7.28.2" - "@types/esrecurse@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== -"@types/estree@1.0.8", "@types/estree@^1.0.6", "@types/estree@^1.0.8": +"@types/estree@^1.0.6", "@types/estree@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -894,12 +711,12 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/node@^25.3.0": - version "25.5.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.5.0.tgz#5c99f37c443d9ccc4985866913f1ed364217da31" - integrity sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw== +"@types/node@^25.6.0": + version "25.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" + integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== dependencies: - undici-types "~7.18.0" + undici-types "~7.19.0" "@types/prop-types@^15.7.12": version "15.7.15" @@ -918,7 +735,7 @@ dependencies: "@types/react" "*" -"@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.6": +"@types/react-transition-group@^4.4.12", "@types/react-transition-group@^4.4.6": version "4.4.12" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== @@ -940,113 +757,108 @@ resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.4.tgz#ebc0c83180dc83994d902bbd51ab0af8a445b1f9" integrity sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg== -"@typescript-eslint/eslint-plugin@8.58.0", "@typescript-eslint/eslint-plugin@^8.56.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz#ad40e492f1931f46da1bd888e52b9e56df9063aa" - integrity sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg== +"@typescript-eslint/eslint-plugin@8.59.1", "@typescript-eslint/eslint-plugin@^8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz#781bc6f9002982cfaf75a185240e24ad7276628a" + integrity sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.58.0" - "@typescript-eslint/type-utils" "8.58.0" - "@typescript-eslint/utils" "8.58.0" - "@typescript-eslint/visitor-keys" "8.58.0" + "@typescript-eslint/scope-manager" "8.59.1" + "@typescript-eslint/type-utils" "8.59.1" + "@typescript-eslint/utils" "8.59.1" + "@typescript-eslint/visitor-keys" "8.59.1" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.5.0" -"@typescript-eslint/parser@8.58.0", "@typescript-eslint/parser@^8.56.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.58.0.tgz#da04ece1967b6c2fe8f10c3473dabf3825795ef7" - integrity sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA== +"@typescript-eslint/parser@8.59.1", "@typescript-eslint/parser@^8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.1.tgz#835d20a62350659a082a1ae2a60b822c40488905" + integrity sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA== dependencies: - "@typescript-eslint/scope-manager" "8.58.0" - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/typescript-estree" "8.58.0" - "@typescript-eslint/visitor-keys" "8.58.0" + "@typescript-eslint/scope-manager" "8.59.1" + "@typescript-eslint/types" "8.59.1" + "@typescript-eslint/typescript-estree" "8.59.1" + "@typescript-eslint/visitor-keys" "8.59.1" debug "^4.4.3" -"@typescript-eslint/project-service@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.0.tgz#66ceda0aabf7427aec3e2713fa43eb278dead2aa" - integrity sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg== +"@typescript-eslint/project-service@8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.1.tgz#49efe87c37ef84262f23df8bf62fdc56698ca6fe" + integrity sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.58.0" - "@typescript-eslint/types" "^8.58.0" + "@typescript-eslint/tsconfig-utils" "^8.59.1" + "@typescript-eslint/types" "^8.59.1" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz#e304142775e49a1b7ac3c8bf2536714447c72cab" - integrity sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ== +"@typescript-eslint/scope-manager@8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz#ed90d054fc3db2d0c81464db3a953a94fb85bb58" + integrity sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg== dependencies: - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/visitor-keys" "8.58.0" + "@typescript-eslint/types" "8.59.1" + "@typescript-eslint/visitor-keys" "8.59.1" -"@typescript-eslint/tsconfig-utils@8.58.0", "@typescript-eslint/tsconfig-utils@^8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz#c5a8edb21f31e0fdee565724e1b984171c559482" - integrity sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A== +"@typescript-eslint/tsconfig-utils@8.59.1", "@typescript-eslint/tsconfig-utils@^8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz#ba2a779a444f1d5cb92a606f9b209d239fd4cab1" + integrity sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA== -"@typescript-eslint/type-utils@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz#ce0e72cd967ffbbe8de322db6089bd4374be352f" - integrity sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg== +"@typescript-eslint/type-utils@8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz#9c83d3f2ed9187a815e8120f72c08317e513e409" + integrity sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w== dependencies: - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/typescript-estree" "8.58.0" - "@typescript-eslint/utils" "8.58.0" + "@typescript-eslint/types" "8.59.1" + "@typescript-eslint/typescript-estree" "8.59.1" + "@typescript-eslint/utils" "8.59.1" debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.58.0", "@typescript-eslint/types@^8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.0.tgz#e94ae7abdc1c6530e71183c1007b61fa93112a5a" - integrity sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww== +"@typescript-eslint/types@8.59.1", "@typescript-eslint/types@^8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.1.tgz#c1d014d3f03a97e0113a8899fc9d4e45a7fb0ca9" + integrity sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A== -"@typescript-eslint/typescript-estree@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz#ed233faa8e2f2a2e1357c3e7d553d6465a0ee59a" - integrity sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA== +"@typescript-eslint/typescript-estree@8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz#4391fadf98a22c869c5b6522dbf4e491e53e351a" + integrity sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g== dependencies: - "@typescript-eslint/project-service" "8.58.0" - "@typescript-eslint/tsconfig-utils" "8.58.0" - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/visitor-keys" "8.58.0" + "@typescript-eslint/project-service" "8.59.1" + "@typescript-eslint/tsconfig-utils" "8.59.1" + "@typescript-eslint/types" "8.59.1" + "@typescript-eslint/visitor-keys" "8.59.1" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.0.tgz#21a74a7963b0d288b719a4121c7dd555adaab3c3" - integrity sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA== +"@typescript-eslint/utils@8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.1.tgz#cf6204d69701bbbc5b150f98c18aeef0a42c10bd" + integrity sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.58.0" - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/typescript-estree" "8.58.0" + "@typescript-eslint/scope-manager" "8.59.1" + "@typescript-eslint/types" "8.59.1" + "@typescript-eslint/typescript-estree" "8.59.1" -"@typescript-eslint/visitor-keys@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz#2abd55a4be70fd55967aceaba4330b9ba9f45189" - integrity sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ== +"@typescript-eslint/visitor-keys@8.59.1": + version "8.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz#b5cba576287a3eeb0b400b62813189abcc3f976a" + integrity sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg== dependencies: - "@typescript-eslint/types" "8.58.0" + "@typescript-eslint/types" "8.59.1" eslint-visitor-keys "^5.0.0" -"@vitejs/plugin-react@^5.1.4": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz#108bd0f566f288ce3566982df4eff137ded7b15f" - integrity sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw== +"@vitejs/plugin-react@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz#d9113b71a0a592714913eafd9e5e63bcafd0ff15" + integrity sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ== dependencies: - "@babel/core" "^7.29.0" - "@babel/plugin-transform-react-jsx-self" "^7.27.1" - "@babel/plugin-transform-react-jsx-source" "^7.27.1" - "@rolldown/pluginutils" "1.0.0-rc.3" - "@types/babel__core" "^7.20.5" - react-refresh "^0.18.0" + "@rolldown/pluginutils" "1.0.0-rc.7" acorn-jsx@^5.3.2: version "5.3.2" @@ -1059,15 +871,22 @@ acorn@^8.16.0: integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== ajv@^6.14.0: - version "6.14.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" - integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== + version "6.15.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.15.0.tgz#07e982c74626167aa7a2495c53817892d7139492" + integrity sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" +aria-hidden@^1.2.3: + version "1.2.6" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" + integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== + dependencies: + tslib "^2.0.0" + array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" @@ -1151,17 +970,6 @@ async-function@^1.0.0: resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== -autoprefixer@^10.4.24: - version "10.4.27" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.27.tgz#51ea301a5c3c5f8642f8e564759c4f573be486f2" - integrity sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA== - dependencies: - browserslist "^4.28.1" - caniuse-lite "^1.0.30001774" - fraction.js "^5.3.4" - picocolors "^1.1.1" - postcss-value-parser "^4.2.0" - available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -1180,14 +988,14 @@ balanced-match@^4.0.2: integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== baseline-browser-mapping@^2.10.12: - version "2.10.13" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz#5a154cc4589193015a274e3d18319b0d76b9224e" - integrity sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw== + version "2.10.23" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz#3a1a55d1a691a8c8d74688af7f1fd17eac23c184" + integrity sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g== brace-expansion@^1.1.7: - version "1.1.13" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6" - integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w== + version "1.1.14" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.14.tgz#d9de602370d91347cd9ddad1224d4fd701eb348b" + integrity sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -1199,7 +1007,7 @@ brace-expansion@^5.0.5: dependencies: balanced-match "^4.0.2" -browserslist@^4.24.0, browserslist@^4.28.1: +browserslist@^4.24.0: version "4.28.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2" integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== @@ -1210,7 +1018,7 @@ browserslist@^4.24.0, browserslist@^4.28.1: node-releases "^2.0.36" update-browserslist-db "^1.2.3" -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -1218,14 +1026,14 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply- es-errors "^1.3.0" function-bind "^1.1.2" -call-bind@^1.0.7, call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== +call-bind@^1.0.7, call-bind@^1.0.8, call-bind@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.9.tgz#39a644700c80bc7d0ca9102fc6d1d43b2fd7eee7" + integrity sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ== dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + get-intrinsic "^1.3.0" set-function-length "^1.2.2" call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: @@ -1236,10 +1044,10 @@ call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: call-bind-apply-helpers "^1.0.2" get-intrinsic "^1.3.0" -caniuse-lite@^1.0.30001774, caniuse-lite@^1.0.30001782: - version "1.0.30001784" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz#bdf9733a0813ccfb5ab4d02f2127e62ee4c6b718" - integrity sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw== +caniuse-lite@^1.0.30001782: + version "1.0.30001791" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz#dfb93d85c40ad380c57123e72e10f3c575786b51" + integrity sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ== chokidar@^4.0.0: version "4.0.3" @@ -1253,6 +1061,11 @@ classnames@^2.3.2: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== +clsx@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1316,7 +1129,7 @@ debug@^2.6.6: dependencies: ms "2.0.0" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.4.3: +debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -1371,10 +1184,10 @@ dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" -dompurify@^3.1.5: - version "3.3.3" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6" - integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA== +dompurify@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.1.tgz#521d04483ac12631b2aedf434a5f5390933b8789" + integrity sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw== optionalDependencies: "@types/trusted-types" "^2.0.7" @@ -1388,9 +1201,9 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1: gopd "^1.2.0" electron-to-chromium@^1.5.328: - version "1.5.330" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz#0efe031938fc8fc82126162a7bd466ba7e24cd38" - integrity sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA== + version "1.5.344" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz#6437cc08a7d9b914a98120e182f37793c9eaffd4" + integrity sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg== emoji-mart@^5.6.0: version "5.6.0" @@ -1403,17 +1216,17 @@ emoji-toolkit@10.0.0: integrity sha512-GkIAvgutEVbkqcT2HjBzV002SWvpdNaT3aP9q/YjQ6hlgDq8HhE9GcqxWkyYkRRQnLADGpwDoj1heTw9KzO9wQ== enhanced-resolve@^5.19.0: - version "5.20.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz#eeeb3966bea62c348c40a0cc9e7912e2557d0be0" - integrity sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA== + version "5.21.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz#bb8e6fabaf74930de70e61397798750429e5b1ae" + integrity sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA== dependencies: graceful-fs "^4.2.4" - tapable "^2.3.0" + tapable "^2.3.3" -es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0, es-abstract@^1.24.1: - version "1.24.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.1.tgz#f0c131ed5ea1bb2411134a8dd94def09c46c7899" - integrity sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw== +es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0, es-abstract@^1.24.2: + version "1.24.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.2.tgz#2dbd38c180735ee983f77585140a2706a963ed9a" + integrity sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg== dependencies: array-buffer-byte-length "^1.0.2" arraybuffer.prototype.slice "^1.0.4" @@ -1481,14 +1294,14 @@ es-errors@^1.3.0: integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-iterator-helpers@^1.2.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz#3be0f4e63438d6c5a1fb5f33b891aaad3f7dae06" - integrity sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ== + version "1.3.2" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz#8f4ff1f3603cbd09fbdb72c747a679779a65cc7f" + integrity sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw== dependencies: - call-bind "^1.0.8" + call-bind "^1.0.9" call-bound "^1.0.4" define-properties "^1.2.1" - es-abstract "^1.24.1" + es-abstract "^1.24.2" es-errors "^1.3.0" es-set-tostringtag "^2.1.0" function-bind "^1.1.2" @@ -1501,7 +1314,6 @@ es-iterator-helpers@^1.2.1: internal-slot "^1.1.0" iterator.prototype "^1.1.5" math-intrinsics "^1.1.0" - safe-array-concat "^1.1.3" es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" @@ -1536,38 +1348,6 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" -esbuild@^0.27.0: - version "0.27.4" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.4.tgz#b9591dd7e0ab803a11c9c3b602850403bef22f00" - integrity sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ== - optionalDependencies: - "@esbuild/aix-ppc64" "0.27.4" - "@esbuild/android-arm" "0.27.4" - "@esbuild/android-arm64" "0.27.4" - "@esbuild/android-x64" "0.27.4" - "@esbuild/darwin-arm64" "0.27.4" - "@esbuild/darwin-x64" "0.27.4" - "@esbuild/freebsd-arm64" "0.27.4" - "@esbuild/freebsd-x64" "0.27.4" - "@esbuild/linux-arm" "0.27.4" - "@esbuild/linux-arm64" "0.27.4" - "@esbuild/linux-ia32" "0.27.4" - "@esbuild/linux-loong64" "0.27.4" - "@esbuild/linux-mips64el" "0.27.4" - "@esbuild/linux-ppc64" "0.27.4" - "@esbuild/linux-riscv64" "0.27.4" - "@esbuild/linux-s390x" "0.27.4" - "@esbuild/linux-x64" "0.27.4" - "@esbuild/netbsd-arm64" "0.27.4" - "@esbuild/netbsd-x64" "0.27.4" - "@esbuild/openbsd-arm64" "0.27.4" - "@esbuild/openbsd-x64" "0.27.4" - "@esbuild/openharmony-arm64" "0.27.4" - "@esbuild/sunos-x64" "0.27.4" - "@esbuild/win32-arm64" "0.27.4" - "@esbuild/win32-ia32" "0.27.4" - "@esbuild/win32-x64" "0.27.4" - escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -1578,10 +1358,10 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-plugin-react-hooks@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169" - integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA== +eslint-plugin-react-hooks@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz#e6742cad75d970c0a3f30d7d3fa80a4784f55927" + integrity sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g== dependencies: "@babel/core" "^7.24.4" "@babel/parser" "^7.24.4" @@ -1633,17 +1413,17 @@ eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== -eslint@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.1.0.tgz#9ca98e654e642ab2e1af6d1e9d8613857ac341b4" - integrity sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA== +eslint@^10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.2.1.tgz#224b2a6caeb34473eddcf918762363e2e063222a" + integrity sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q== dependencies: "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.2" - "@eslint/config-array" "^0.23.3" - "@eslint/config-helpers" "^0.5.3" - "@eslint/core" "^1.1.1" - "@eslint/plugin-kit" "^0.6.1" + "@eslint/config-array" "^0.23.5" + "@eslint/config-helpers" "^0.5.5" + "@eslint/core" "^1.2.1" + "@eslint/plugin-kit" "^0.7.1" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" @@ -1757,21 +1537,16 @@ for-each@^0.3.3, for-each@^0.3.5: dependencies: is-callable "^1.2.7" -fraction.js@^5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" - integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== - -framer-motion@^11.2.12: - version "11.18.2" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718" - integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w== +framer-motion@^12.38.0: + version "12.38.0" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.38.0.tgz#cf28e072a95942881ca4e33fd33be41192fd146b" + integrity sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g== dependencies: - motion-dom "^11.18.1" - motion-utils "^11.18.1" + motion-dom "^12.38.0" + motion-utils "^12.36.0" tslib "^2.4.0" -fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -1856,11 +1631,6 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -globrex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" - integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== - gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -1903,9 +1673,9 @@ has-tostringtag@^1.0.2: has-symbols "^1.0.3" hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + version "2.0.3" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c" + integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg== dependencies: function-bind "^1.1.2" @@ -2272,7 +2042,7 @@ lightningcss-win32-x64-msvc@1.32.0: resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a" integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== -lightningcss@1.32.0: +lightningcss@1.32.0, lightningcss@^1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9" integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ== @@ -2348,17 +2118,17 @@ minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -motion-dom@^11.18.1: - version "11.18.1" - resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2" - integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw== +motion-dom@^12.38.0: + version "12.38.0" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.38.0.tgz#9ef3253ea0fb28b6757588327073848d940e9aab" + integrity sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA== dependencies: - motion-utils "^11.18.1" + motion-utils "^12.36.0" -motion-utils@^11.18.1: - version "11.18.1" - resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.18.1.tgz#671227669833e991c55813cf337899f41327db5b" - integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA== +motion-utils@^12.36.0: + version "12.36.0" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.36.0.tgz#cff2df2a28c3fe53a3de7e0103ba7f73ff7d77a7" + integrity sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg== ms@2.0.0: version "2.0.0" @@ -2396,9 +2166,9 @@ node-exports-info@^1.6.0: semver "^6.3.1" node-releases@^2.0.36: - version "2.0.36" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.36.tgz#99fd6552aaeda9e17c4713b57a63964a2e325e9d" - integrity sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA== + version "2.0.38" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.38.tgz#791569b9e4424a044e12c3abfad418ed83ce9947" + integrity sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw== object-assign@^4.1.1: version "4.1.1" @@ -2512,7 +2282,7 @@ picocolors@^1.1.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^4.0.3: +picomatch@^4.0.3, picomatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== @@ -2537,15 +2307,10 @@ postcss-selector-parser@^7.0.0: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@^8.5.6: - version "8.5.8" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399" - integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg== +postcss@^8.5.10, postcss@^8.5.12, postcss@^8.5.6: + version "8.5.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.12.tgz#cd0c0f667f7cb0521e2313234ea6e707a9ec1ddb" + integrity sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA== dependencies: nanoid "^3.3.11" picocolors "^1.1.1" @@ -2578,6 +2343,21 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +react-aria@3.48.0: + version "3.48.0" + resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.48.0.tgz#ede91d3b247d34ea35216e246f0cb7de074303bd" + integrity sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w== + dependencies: + "@internationalized/date" "^3.12.1" + "@internationalized/number" "^3.6.6" + "@internationalized/string" "^3.2.8" + "@react-types/shared" "^3.34.0" + "@swc/helpers" "^0.5.0" + aria-hidden "^1.2.3" + clsx "^2.0.0" + react-stately "3.46.0" + use-sync-external-store "^1.6.0" + react-bootstrap@^2.10.10: version "2.10.10" resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.10.10.tgz#be0b0d951a69987152d75c0e6986c80425efdf21" @@ -2597,10 +2377,10 @@ react-bootstrap@^2.10.10: uncontrollable "^7.2.1" warning "^4.0.3" -react-dom@^19.2.4: - version "19.2.4" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591" - integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== +react-dom@^19.2.5: + version "19.2.5" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.5.tgz#b8768b10837d0b8e9ca5b9e2d58dff3d880ea25e" + integrity sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag== dependencies: scheduler "^0.27.0" @@ -2619,11 +2399,6 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-refresh@^0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" - integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== - react-slider@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-2.0.6.tgz#8c7ff0301211f7c3ff32aa0163b33bdab6258559" @@ -2631,6 +2406,18 @@ react-slider@^2.0.6: dependencies: prop-types "^15.8.1" +react-stately@3.46.0: + version "3.46.0" + resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.46.0.tgz#9ce293b765c246c398a1765d6290acd0a77caa49" + integrity sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA== + dependencies: + "@internationalized/date" "^3.12.1" + "@internationalized/number" "^3.6.6" + "@internationalized/string" "^3.2.8" + "@react-types/shared" "^3.34.0" + "@swc/helpers" "^0.5.0" + use-sync-external-store "^1.6.0" + react-tiny-popover@^8.1.6: version "8.1.6" resolved "https://registry.yarnpkg.com/react-tiny-popover/-/react-tiny-popover-8.1.6.tgz#82fad10eb8f0d8197ce0944031fd03a524b78c29" @@ -2655,10 +2442,10 @@ react-youtube@^10.1.0: prop-types "15.8.1" youtube-player "5.5.2" -react@^19.2.4: - version "19.2.4" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" - integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== +react@^19.2.5: + version "19.2.5" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.5.tgz#c888ab8b8ef33e2597fae8bdb2d77edbdb42858b" + integrity sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA== readdirp@^4.0.1: version "4.1.2" @@ -2703,48 +2490,38 @@ resolve@^2.0.0-next.5: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -rollup@^4.43.0: - version "4.60.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.60.1.tgz#b4aa2bcb3a5e1437b5fad40d43fe42d4bde7a42d" - integrity sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w== +rolldown@1.0.0-rc.17: + version "1.0.0-rc.17" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.17.tgz#c524fc22f6bb37b5588aec862ab1ee11382610f3" + integrity sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA== dependencies: - "@types/estree" "1.0.8" + "@oxc-project/types" "=0.127.0" + "@rolldown/pluginutils" "1.0.0-rc.17" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.60.1" - "@rollup/rollup-android-arm64" "4.60.1" - "@rollup/rollup-darwin-arm64" "4.60.1" - "@rollup/rollup-darwin-x64" "4.60.1" - "@rollup/rollup-freebsd-arm64" "4.60.1" - "@rollup/rollup-freebsd-x64" "4.60.1" - "@rollup/rollup-linux-arm-gnueabihf" "4.60.1" - "@rollup/rollup-linux-arm-musleabihf" "4.60.1" - "@rollup/rollup-linux-arm64-gnu" "4.60.1" - "@rollup/rollup-linux-arm64-musl" "4.60.1" - "@rollup/rollup-linux-loong64-gnu" "4.60.1" - "@rollup/rollup-linux-loong64-musl" "4.60.1" - "@rollup/rollup-linux-ppc64-gnu" "4.60.1" - "@rollup/rollup-linux-ppc64-musl" "4.60.1" - "@rollup/rollup-linux-riscv64-gnu" "4.60.1" - "@rollup/rollup-linux-riscv64-musl" "4.60.1" - "@rollup/rollup-linux-s390x-gnu" "4.60.1" - "@rollup/rollup-linux-x64-gnu" "4.60.1" - "@rollup/rollup-linux-x64-musl" "4.60.1" - "@rollup/rollup-openbsd-x64" "4.60.1" - "@rollup/rollup-openharmony-arm64" "4.60.1" - "@rollup/rollup-win32-arm64-msvc" "4.60.1" - "@rollup/rollup-win32-ia32-msvc" "4.60.1" - "@rollup/rollup-win32-x64-gnu" "4.60.1" - "@rollup/rollup-win32-x64-msvc" "4.60.1" - fsevents "~2.3.2" + "@rolldown/binding-android-arm64" "1.0.0-rc.17" + "@rolldown/binding-darwin-arm64" "1.0.0-rc.17" + "@rolldown/binding-darwin-x64" "1.0.0-rc.17" + "@rolldown/binding-freebsd-x64" "1.0.0-rc.17" + "@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.17" + "@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.17" + "@rolldown/binding-linux-arm64-musl" "1.0.0-rc.17" + "@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.17" + "@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.17" + "@rolldown/binding-linux-x64-gnu" "1.0.0-rc.17" + "@rolldown/binding-linux-x64-musl" "1.0.0-rc.17" + "@rolldown/binding-openharmony-arm64" "1.0.0-rc.17" + "@rolldown/binding-wasm32-wasi" "1.0.0-rc.17" + "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.17" + "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.17" safe-array-concat@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" - integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + version "1.1.4" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz#a54cc9b61a57f33b42abad3cbdda3a2b38cc5719" + integrity sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg== dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - get-intrinsic "^1.2.6" + call-bind "^1.0.9" + call-bound "^1.0.4" + get-intrinsic "^1.3.0" has-symbols "^1.1.0" isarray "^2.0.5" @@ -2765,10 +2542,10 @@ safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" -sass@^1.97.3: - version "1.98.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.98.0.tgz#924ce85a3745ccaccd976262fdc1bc0c13aa8e57" - integrity sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A== +sass@^1.99.0: + version "1.99.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.99.0.tgz#ff9d1594da4886249dfaafabbeea2dea2dc74b26" + integrity sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q== dependencies: chokidar "^4.0.0" immutable "^5.1.5" @@ -2835,12 +2612,12 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== dependencies: es-errors "^1.3.0" - object-inspect "^1.13.3" + object-inspect "^1.13.4" side-channel-map@^1.0.1: version "1.0.1" @@ -2956,35 +2733,30 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tailwindcss@4.2.2, tailwindcss@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.2.2.tgz#688fb0751c8ca9044e890546510a2ee817308e87" - integrity sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q== +tailwindcss@4.2.4, tailwindcss@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.2.4.tgz#f7e3090edb22d56394db4d68e6464d2628dc2aa9" + integrity sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA== -tapable@^2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.2.tgz#86755feabad08d82a26b891db044808c6ad00f15" - integrity sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA== +tapable@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160" + integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== -tinyglobby@^0.2.15: - version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== +tinyglobby@^0.2.15, tinyglobby@^0.2.16: + version "0.2.16" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" + integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== dependencies: fdir "^6.5.0" - picomatch "^4.0.3" + picomatch "^4.0.4" ts-api-utils@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== -tsconfck@^3.0.3: - version "3.1.6" - resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" - integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== - -tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1: +tslib@^2.0.0, tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -3041,20 +2813,20 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript-eslint@^8.56.0: - version "8.58.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.58.0.tgz#5758b1b68ae7ec05d756b98c63a1f6953a01172b" - integrity sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA== +typescript-eslint@^8.59.1: + version "8.59.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.1.tgz#244a9fcbf27057ebbc2281d408239f1861b55b78" + integrity sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ== dependencies: - "@typescript-eslint/eslint-plugin" "8.58.0" - "@typescript-eslint/parser" "8.58.0" - "@typescript-eslint/typescript-estree" "8.58.0" - "@typescript-eslint/utils" "8.58.0" + "@typescript-eslint/eslint-plugin" "8.59.1" + "@typescript-eslint/parser" "8.59.1" + "@typescript-eslint/typescript-estree" "8.59.1" + "@typescript-eslint/utils" "8.59.1" -typescript@^5.9.3: - version "5.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== +typescript@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21" + integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw== unbox-primitive@^1.1.0: version "1.1.0" @@ -3081,10 +2853,10 @@ uncontrollable@^8.0.4: resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-8.0.4.tgz#a0a8307f638795162fafd0550f4a1efa0f8c5eb6" integrity sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ== -undici-types@~7.18.0: - version "7.18.2" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" - integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w== +undici-types@~7.19.0: + version "7.19.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" + integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== update-browserslist-db@^1.2.3: version "1.2.3" @@ -3106,31 +2878,26 @@ use-between@^1.4.0: resolved "https://registry.yarnpkg.com/use-between/-/use-between-1.4.0.tgz#d1e3b95095be2c2305709c15ed5265ee6c692935" integrity sha512-MpLUnRHxZd3CNa5EeXaMadK1+oSd2Kst57WfU15TQbsLu3vgMcfh4gjAJWKaox02pOf+7Lx1ZHK5tMXHEVH1Qw== +use-sync-external-store@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -vite-tsconfig-paths@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz#d5c28cba79c89ebf76489ef1040024b21df6da3a" - integrity sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg== +vite@^8.0.10: + version "8.0.10" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.10.tgz#fb31868526ec874101fac084172a2cdc6776319b" + integrity sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw== dependencies: - debug "^4.1.1" - globrex "^0.1.2" - tsconfck "^3.0.3" - -vite@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" - integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== - dependencies: - esbuild "^0.27.0" - fdir "^6.5.0" - picomatch "^4.0.3" - postcss "^8.5.6" - rollup "^4.43.0" - tinyglobby "^0.2.15" + lightningcss "^1.32.0" + picomatch "^4.0.4" + postcss "^8.5.10" + rolldown "1.0.0-rc.17" + tinyglobby "^0.2.16" optionalDependencies: fsevents "~2.3.3" From 0bf861ef3cb161ad6c56dd1009d14309d5e02118 Mon Sep 17 00:00:00 2001 From: duckietm Date: Tue, 28 Apr 2026 11:33:29 +0200 Subject: [PATCH 02/20] =?UTF-8?q?=F0=9F=86=99=20Added=20ban=20to=20the=20l?= =?UTF-8?q?ogin=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/login/LoginView.tsx | 86 +++++++++++++++++++++++++++++- src/css/login/LoginView.css | 27 ++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index 352014b..79a10f2 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -3,6 +3,12 @@ import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from import { GetConfigurationValue, LocalizeText } from '../../api'; import { TurnstileWidget } from './TurnstileWidget'; +/** + * Looks up a localized string. Falls back to `fallback` when the key is + * missing (LocalizeText returns the key itself) or when the localization + * manager isn't ready yet (login runs very early). Parameters are + * %name%-substituted into the fallback so the UI stays correct pre-init. + */ const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string => { try @@ -10,7 +16,7 @@ const t = (key: string, fallback: string, params?: string[], replacements?: stri const value = LocalizeText(key, params ?? null, replacements ?? null); if(value && value !== key) return value; } - catch {} + catch { /* localization manager not initialised yet */ } if(!params || !replacements) return fallback; let out = fallback; @@ -23,6 +29,39 @@ const t = (key: string, fallback: string, params?: string[], replacements?: stri type DialogMode = 'login' | 'register' | 'forgot'; +interface BanInfo +{ + type: 'account' | 'ip' | 'machine' | 'super' | string; + reason: string; + permanent: boolean; + expiresAt?: number; +} + +const parseBan = (payload: Record): BanInfo | null => +{ + const raw = payload?.ban; + if(!raw || typeof raw !== 'object') return null; + const ban = raw as Record; + const type = typeof ban.type === 'string' ? ban.type : 'account'; + const reason = typeof ban.reason === 'string' ? ban.reason : ''; + const permanent = ban.permanent === true || ban.permanent === 'true'; + const expiresAt = typeof ban.expiresAt === 'number' ? ban.expiresAt : undefined; + return { type, reason, permanent, expiresAt }; +}; + +const formatRemaining = (epochSeconds: number): string => +{ + const totalSeconds = Math.max(0, epochSeconds - Math.floor(Date.now() / 1000)); + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + if(days > 0) return `${ days }d ${ hours }h ${ minutes }m`; + if(hours > 0) return `${ hours }h ${ minutes }m`; + if(minutes > 0) return `${ minutes }m ${ seconds }s`; + return `${ seconds }s`; +}; + const interpolate = (value: string | null | undefined): string => { if(!value) return ''; @@ -66,6 +105,8 @@ export const LoginView: FC = ({ onAuthenticated }) => const [ password, setPassword ] = useState(''); const [ rememberMe, setRememberMe ] = useState(false); const [ error, setError ] = useState(null); + const [ banInfo, setBanInfo ] = useState(null); + const [ , setBanTick ] = useState(0); const [ info, setInfo ] = useState(null); const [ submitting, setSubmitting ] = useState(false); const [ loginTurnstileToken, setLoginTurnstileToken ] = useState(''); @@ -103,9 +144,25 @@ export const LoginView: FC = ({ onAuthenticated }) => useEffect(() => { setError(null); + setBanInfo(null); if(mode === 'login') resetLoginTurnstile(); }, [ mode, resetLoginTurnstile ]); + useEffect(() => + { + if(!banInfo || banInfo.permanent || !banInfo.expiresAt) return; + const interval = window.setInterval(() => + { + if(banInfo.expiresAt && banInfo.expiresAt <= Math.floor(Date.now() / 1000)) + { + setBanInfo(null); + return; + } + setBanTick(t => t + 1); + }, 1000); + return () => window.clearInterval(interval); + }, [ banInfo ]); + useEffect(() => { if(!info) return; @@ -243,6 +300,7 @@ export const LoginView: FC = ({ onAuthenticated }) => } setError(null); + setBanInfo(null); setSubmitting(true); try @@ -277,6 +335,14 @@ export const LoginView: FC = ({ onAuthenticated }) => return; } + const ban = parseBan(payload); + if(ban) + { + setBanInfo(ban); + resetLoginTurnstile(); + return; + } + recordFailure(); const message = typeof payload.error === 'string' ? payload.error : t('nitro.login.error.invalid_credentials', 'Invalid Habbo name or password.'); setError(message); @@ -503,13 +569,29 @@ export const LoginView: FC = ({ onAuthenticated }) => } + { banInfo && +
+
+ { banInfo.type === 'ip' + ? t('nitro.login.error.banned.ip.title', 'This connection is banned') + : t('nitro.login.error.banned.account.title', 'Your account is banned') } +
+ { banInfo.permanent + ?
{ t('nitro.login.error.banned.permanent', 'This is a permanent ban.') }
+ : (banInfo.expiresAt + ?
{ t('nitro.login.error.banned.temporary', 'You can log in again in %time%.', [ 'time' ], [ formatRemaining(banInfo.expiresAt) ]) }
+ : null) } + { banInfo.reason && +
{ t('nitro.login.error.banned.reason', 'Reason: %reason%', [ 'reason' ], [ banInfo.reason ]) }
} +
+ } { error &&
{ error }
} { info &&
{ info }
}
setMode('forgot') }>{ t('login.forgot_password', 'Forgotten your password?') } diff --git a/src/css/login/LoginView.css b/src/css/login/LoginView.css index c88081c..e91cbad 100644 --- a/src/css/login/LoginView.css +++ b/src/css/login/LoginView.css @@ -225,6 +225,33 @@ text-align: center; } +.nitro-login-card .error-line.ban-message { + display: flex; + flex-direction: column; + gap: 3px; + padding: 8px 10px; + text-align: left; + line-height: 1.35; +} + +.nitro-login-card .error-line.ban-message .ban-title { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.nitro-login-card .error-line.ban-message .ban-status { + font-size: 11px; + font-variant-numeric: tabular-nums; +} + +.nitro-login-card .error-line.ban-message .ban-reason { + font-size: 11px; + font-style: italic; + word-break: break-word; +} + .nitro-login-card .register-card-body a { color: #134b6e; text-decoration: underline; From a266696eb6b4289c65e596ed1440f9e939dfe7eb Mon Sep 17 00:00:00 2001 From: duckietm Date: Tue, 28 Apr 2026 13:47:39 +0200 Subject: [PATCH 03/20] =?UTF-8?q?=F0=9F=86=99=20Added=20BuildHeight=20to?= =?UTF-8?q?=20NitroV3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/friends-bar/FriendBarItemView.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/friends/views/friends-bar/FriendBarItemView.tsx b/src/components/friends/views/friends-bar/FriendBarItemView.tsx index 9e05f78..239016b 100644 --- a/src/components/friends/views/friends-bar/FriendBarItemView.tsx +++ b/src/components/friends/views/friends-bar/FriendBarItemView.tsx @@ -62,8 +62,8 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => { return (
-
- {(friend.id > 0) ? ( + {(friend.id > 0) ? ( +
= props => { className="block pointer-events-none drop-shadow-[1px_1px_0_rgba(0,0,0,0.6)]" style={ { marginLeft: '-28px', marginTop: '-10px' } } /> - ) : ( - - )} -
+
+ ) : ( +
+ +
+ )} Date: Wed, 29 Apr 2026 13:20:13 +0200 Subject: [PATCH 04/20] =?UTF-8?q?=F0=9F=86=95=20Effect=20selection=20in=20?= =?UTF-8?q?user=20dropdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MainView.tsx | 2 + .../AvatarEffectPreviewView.tsx | 76 +++++++++ .../avatar-effects/AvatarEffectsView.tsx | 156 ++++++++++++++++++ src/components/avatar-effects/index.ts | 2 + .../menu/AvatarInfoWidgetOwnAvatarView.tsx | 6 + 5 files changed, 242 insertions(+) create mode 100644 src/components/avatar-effects/AvatarEffectPreviewView.tsx create mode 100644 src/components/avatar-effects/AvatarEffectsView.tsx create mode 100644 src/components/avatar-effects/index.ts diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 5309364..70f2fd3 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -4,6 +4,7 @@ import { FC, useEffect, useState } from 'react'; import { useNitroEvent } from '../hooks'; import { AchievementsView } from './achievements/AchievementsView'; import { AvatarEditorView } from './avatar-editor'; +import { AvatarEffectsView } from './avatar-effects'; import { CameraWidgetView } from './camera/CameraWidgetView'; import { CampaignView } from './campaign/CampaignView'; import { CatalogView } from './catalog/CatalogView'; @@ -105,6 +106,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/avatar-effects/AvatarEffectPreviewView.tsx b/src/components/avatar-effects/AvatarEffectPreviewView.tsx new file mode 100644 index 0000000..b68d7d0 --- /dev/null +++ b/src/components/avatar-effects/AvatarEffectPreviewView.tsx @@ -0,0 +1,76 @@ +import { GetRoomEngine, RoomPreviewer } from '@nitrots/nitro-renderer'; +import { CSSProperties, FC, useEffect, useState } from 'react'; +import { LayoutRoomPreviewerView } from '../../common'; + +interface AvatarEffectPreviewViewProps +{ + figure: string; + gender: string; + direction: number; + effect: number; + height?: number; + zoom?: number; +} + +export const AvatarEffectPreviewView: FC = props => +{ + const { figure = '', gender = 'M', direction = 4, effect = 0, height = 280, zoom = 1 } = props; + const [ roomPreviewer, setRoomPreviewer ] = useState(null); + + const renderHeight = Math.floor(height / zoom); + + useEffect(() => + { + const previewer = new RoomPreviewer(GetRoomEngine(), ++RoomPreviewer.PREVIEW_COUNTER); + setRoomPreviewer(previewer); + + return () => + { + previewer.dispose(); + setRoomPreviewer(null); + }; + }, []); + + useEffect(() => + { + if(!roomPreviewer || !figure) return; + + roomPreviewer.addAvatarIntoRoom(figure, effect); + roomPreviewer.updateObjectUserFigure(figure, gender); + }, [ roomPreviewer, figure, gender, effect ]); + + useEffect(() => + { + if(!roomPreviewer) return; + roomPreviewer.updateAvatarDirection(direction, direction); + }, [ roomPreviewer, direction ]); + + if(!roomPreviewer) return null; + + if(zoom === 1) + { + return ; + } + + const outerStyle: CSSProperties = { + position: 'absolute', + inset: 0, + overflow: 'hidden' + }; + + const innerStyle: CSSProperties = { + width: `${ 100 / zoom }%`, + height: `${ 100 / zoom }%`, + transform: `scale(${ zoom })`, + transformOrigin: 'top left', + imageRendering: 'pixelated' + }; + + return ( +
+
+ +
+
+ ); +}; diff --git a/src/components/avatar-effects/AvatarEffectsView.tsx b/src/components/avatar-effects/AvatarEffectsView.tsx new file mode 100644 index 0000000..9070495 --- /dev/null +++ b/src/components/avatar-effects/AvatarEffectsView.tsx @@ -0,0 +1,156 @@ +import { AddLinkEventTracker, AvatarDirectionAngle, AvatarEffectActivatedComposer, GetConfiguration, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { LocalizeText, SendMessageComposer } from '../../api'; +import { Button, Column, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; +import { AvatarEffectPreviewView } from './AvatarEffectPreviewView'; + +interface EffectMapEntry +{ + id: string; + lib: string; + type: string; + revision?: string | number; +} + +const DEFAULT_DIRECTION = 4; + +export const AvatarEffectsView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ effects, setEffects ] = useState([]); + const [ loadError, setLoadError ] = useState(null); + const [ selectedId, setSelectedId ] = useState(0); + const [ direction, setDirection ] = useState(DEFAULT_DIRECTION); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': setIsVisible(true); return; + case 'hide': setIsVisible(false); return; + case 'toggle': setIsVisible(prev => !prev); return; + } + }, + eventUrlPrefix: 'avatar-effects/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + if(!isVisible || effects.length || loadError) return; + + const url = GetConfiguration().getValue('avatar.effectmap.url'); + if(!url) + { + setLoadError('Effect map URL is not configured.'); + return; + } + + let cancelled = false; + (async () => + { + try + { + const response = await fetch(url); + if(!response.ok) throw new Error(`HTTP ${ response.status }`); + const json = await response.json(); + if(cancelled) return; + + const list: EffectMapEntry[] = Array.isArray(json?.effects) + ? json.effects.filter((e: EffectMapEntry) => e?.type === 'fx' && /^\d+$/.test(String(e.id))) + : []; + + list.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)); + setEffects(list); + } + catch(error) + { + if(!cancelled) setLoadError(String((error as Error).message ?? error)); + } + })(); + + return () => { cancelled = true; }; + }, [ isVisible, effects.length, loadError ]); + + const session = GetSessionDataManager(); + const figure = session?.figure ?? ''; + const gender = session?.gender ?? 'M'; + + const rotateFigure = useCallback((delta: number) => + { + setDirection(prev => + { + let next = prev + delta; + if(next < AvatarDirectionAngle.MIN_DIRECTION) next = AvatarDirectionAngle.MAX_DIRECTION; + if(next > AvatarDirectionAngle.MAX_DIRECTION) next = AvatarDirectionAngle.MIN_DIRECTION; + return next; + }); + }, []); + + const applySelectedEffect = useCallback(() => + { + if(!selectedId) return; + SendMessageComposer(new AvatarEffectActivatedComposer(selectedId)); + setIsVisible(false); + }, [ selectedId ]); + + const onClose = useCallback(() => setIsVisible(false), []); + + if(!isVisible) return null; + + return ( + + + + +
+ +
+ + +
+
+ +
+ + { loadError &&
{ loadError }
} + { !loadError && !effects.length &&
{ LocalizeText('generic.loading') || 'Loading…' }
} + { !!effects.length && +
+ { effects.map(effect => + { + const id = parseInt(effect.id, 10); + const isSelected = (id === selectedId); + return ( + + ); + }) } +
+ } +
+
+
+ ); +}; diff --git a/src/components/avatar-effects/index.ts b/src/components/avatar-effects/index.ts new file mode 100644 index 0000000..17f1000 --- /dev/null +++ b/src/components/avatar-effects/index.ts @@ -0,0 +1,2 @@ +export * from './AvatarEffectPreviewView'; +export * from './AvatarEffectsView'; diff --git a/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetOwnAvatarView.tsx b/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetOwnAvatarView.tsx index 4eb0756..1ad76e2 100644 --- a/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetOwnAvatarView.tsx +++ b/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetOwnAvatarView.tsx @@ -55,6 +55,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC processAction('change_looks') }> { LocalizeText('widget.memenu.myclothes') } + processAction('avatar_effect') }> + { LocalizeText('product.type.effect') } + { (HasHabboClub() && !isRidingHorse) && processAction('dance_menu') }> From eb0bf80dfa330e58b778ccdbf03dd030c67245ff Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 29 Apr 2026 15:44:17 +0200 Subject: [PATCH 05/20] =?UTF-8?q?=F0=9F=86=99=20Fix=20the=20avatar-editor?= =?UTF-8?q?=20faces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/UITexts.example | 3 +- .../avatar/AvatarEditorThumbnailsHelper.ts | 48 ++++++++++++++++++- .../avatar-editor/AvatarEditorView.tsx | 2 +- .../AvatarEditorFigureSetItemView.tsx | 2 +- .../figure-set/AvatarEditorFigureSetView.tsx | 4 +- .../views/friends-bar/FriendBarItemView.tsx | 8 ++-- .../views/friends-bar/FriendsBarView.tsx | 4 +- src/css/index.css | 2 +- src/layout/InfiniteGrid.tsx | 47 ++++++++++++++++-- 9 files changed, 102 insertions(+), 18 deletions(-) diff --git a/public/UITexts.example b/public/UITexts.example index c7480c7..34719b4 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -168,6 +168,7 @@ "nitro.login.error.missing_username": "Please choose a Habbo name.", "nitro.login.error.username_length": "Habbo name must be 3–16 characters.", "nitro.login.error.username_taken": "This Habbo name is already taken.", - "nitro.login.error.missing_email": "Please enter your email address." + "nitro.login.error.missing_email": "Please enter your email address.", + "inventory.effects.activate": "Use selected effect" } } diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index c319d2a..9cb58b6 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -227,9 +227,11 @@ export class AvatarEditorThumbnailsHelper if(isDisabled) sprite.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ]; + const frame = AvatarEditorThumbnailsHelper.findOpaqueBoundsFrame(sprite, texture.width, texture.height); + const imageUrl = await TextureUtils.generateImageUrl({ target: sprite, - frame: new NitroRectangle(0, 0, texture.width, texture.height) + frame }); sprite.destroy(); @@ -244,6 +246,50 @@ export class AvatarEditorThumbnailsHelper }); } + private static findOpaqueBoundsFrame(sprite: NitroSprite, fallbackWidth: number, fallbackHeight: number): NitroRectangle + { + try + { + const data = TextureUtils.getPixels(sprite); + if(!data) return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight); + + const pixels = data.pixels as Uint8ClampedArray | Uint8Array; + const width = data.width; + const height = data.height; + if(!pixels || width <= 0 || height <= 0) return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight); + + const ALPHA_THRESHOLD = 8; + + let minX = width; + let minY = height; + let maxX = -1; + let maxY = -1; + + for(let y = 0; y < height; y++) + { + const rowStart = y * width * 4; + for(let x = 0; x < width; x++) + { + if(pixels[rowStart + (x * 4) + 3] > ALPHA_THRESHOLD) + { + if(x < minX) minX = x; + if(x > maxX) maxX = x; + if(y < minY) minY = y; + if(y > maxY) maxY = y; + } + } + } + + if(maxX < minX || maxY < minY) return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight); + + return new NitroRectangle(minX, minY, (maxX - minX) + 1, (maxY - minY) + 1); + } + catch + { + return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight); + } + } + private static sortByDrawOrder(a: IFigurePart, b: IFigurePart): number { const indexA = AvatarEditorThumbnailsHelper.DRAW_ORDER.indexOf(a.type); diff --git a/src/components/avatar-editor/AvatarEditorView.tsx b/src/components/avatar-editor/AvatarEditorView.tsx index a150bec..2270a7b 100644 --- a/src/components/avatar-editor/AvatarEditorView.tsx +++ b/src/components/avatar-editor/AvatarEditorView.tsx @@ -78,7 +78,7 @@ export const AvatarEditorView: FC<{}> = props => return ( setIsVisible(false) } /> diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx index 31c88a6..08c7c67 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -77,7 +77,7 @@ export const AvatarEditorFigureSetItemView: FC<{ itemActive={ isSelected } itemImage={ (!partItem.isClear && isHead) ? assetUrl : undefined } className={ `avatar-parts mx-auto${ isSelected ? ' part-selected' : '' }${ !partItem.isClear && isSellableNotOwned ? ' pet-sellable-locked' : '' }` } - style={ isHead ? { backgroundSize: '200%', backgroundPosition: 'center -32px' } : undefined } + style={ isHead ? { backgroundSize: 'auto 80%', backgroundPosition: 'center', imageRendering: 'pixelated' } : undefined } { ...rest } > { !partItem.isClear && assetUrl && !isHead && diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx index 179f894..3def2bc 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx @@ -30,12 +30,12 @@ export const AvatarEditorFigureSetView: FC<{ }; return ( - columnCount={ columnCount } estimateSize={ estimateSize } itemRender={ (item: IAvatarEditorCategoryPartItem) => + columnCount={ columnCount } itemMinWidth={ 42 } rowGap={ 8 } estimateSize={ estimateSize } itemRender={ (item: IAvatarEditorCategoryPartItem) => { if(!item) return null; return ( - selectEditorPart(category.setType, item.partSet?.id ?? -1) } /> + selectEditorPart(category.setType, item.partSet?.id ?? -1) } /> ); } } items={ category.partItems } overscan={ columnCount } /> ); diff --git a/src/components/friends/views/friends-bar/FriendBarItemView.tsx b/src/components/friends/views/friends-bar/FriendBarItemView.tsx index 239016b..61a6d42 100644 --- a/src/components/friends/views/friends-bar/FriendBarItemView.tsx +++ b/src/components/friends/views/friends-bar/FriendBarItemView.tsx @@ -33,7 +33,7 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => { onClick={() => setVisible(prev => !prev)} >
-
{LocalizeText('friend.bar.find.title')}
+
{LocalizeText('friend.bar.find.title')}
@@ -45,10 +45,10 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => { transition={{ type: "spring", stiffness: 400, damping: 25 }} className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel whitespace-nowrap z-[80] flex flex-col items-center gap-2 pointer-events-auto min-w-[170px]" > -
{LocalizeText('friend.bar.find.title')}
+
{LocalizeText('friend.bar.find.title')}
{LocalizeText('friend.bar.find.text')}
- +
+ +
+ { selectedEffect && +
+
#{ parseInt(selectedEffect.id, 10) }
+
{ selectedEffect.lib }
+
+ }
- - { loadError &&
{ loadError }
} - { !loadError && !effects.length &&
{ LocalizeText('generic.loading') || 'Loading…' }
} - { !!effects.length && -
- { effects.map(effect => - { - const id = parseInt(effect.id, 10); - const isSelected = (id === selectedId); - return ( - - ); - }) } -
- } + +
+ + +
+
+ { filteredEffects.length === effects.length ? `${ effects.length } effects` : `${ filteredEffects.length } of ${ effects.length }` } + { selectedId > 0 && + } +
+
+ { loadError &&
{ loadError }
} + { !loadError && !effects.length &&
{ LocalizeText('generic.loading') || 'Loading…' }
} + { !!effects.length && !filteredEffects.length && +
{ LocalizeText('generic.search.noresults') || 'No effects match your search.' }
+ } + { !!visibleEffects.length && +
    + { visibleEffects.map((effect, index) => + { + const id = parseInt(effect.id, 10); + const isSelected = (id === selectedId); + return ( +
  • + +
  • + ); + }) } + { hasMore && +
  • + +
  • + } +
+ } +
From 38470d6bec40af24deac61a5f31b0df6b0085f69 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 30 Apr 2026 10:12:16 +0200 Subject: [PATCH 07/20] =?UTF-8?q?=F0=9F=86=99=20Change=20Font=20to=20old?= =?UTF-8?q?=20school=20in=20login=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/css/icons/icons.css | 4 ++++ src/css/login/LoginView.css | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/css/icons/icons.css b/src/css/icons/icons.css index 7b85ad0..62120ea 100644 --- a/src/css/icons/icons.css +++ b/src/css/icons/icons.css @@ -4,6 +4,10 @@ background-position: center; background-repeat: no-repeat; outline: 0; + image-rendering: -webkit-optimize-contrast !important; + image-rendering: -moz-crisp-edges !important; + image-rendering: crisp-edges !important; + image-rendering: pixelated !important; } .nitro-icon:hover { diff --git a/src/css/login/LoginView.css b/src/css/login/LoginView.css index e91cbad..cd8ea96 100644 --- a/src/css/login/LoginView.css +++ b/src/css/login/LoginView.css @@ -1,3 +1,25 @@ +@font-face { + font-family: Volter; + font-weight: normal; + font-style: normal; + src: url("@/assets/webfonts/Volter.ttf") format("truetype"); +} + +@font-face { + font-family: Volter; + font-weight: bold; + font-style: normal; + src: url("@/assets/webfonts/Volter-b.ttf") format("truetype"); +} + +.nitro-login-view, +.nitro-login-view * { + font-family: Volter, "Volter (Goldfish)", monospace; + -webkit-font-smoothing: none; + -moz-osx-font-smoothing: grayscale; + font-smooth: never; +} + .nitro-login-view { position: fixed; inset: 0; From d1f696e519b70d4bb7da751ada35305dffff5278 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 30 Apr 2026 17:25:04 +0200 Subject: [PATCH 08/20] =?UTF-8?q?=F0=9F=86=95=20News=20in=20the=20UI=20Cli?= =?UTF-8?q?ent=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/login/LoginView.tsx | 170 ++++++++++- src/css/login/LoginView.css | 443 +++++++++++++++++++++++++++++ 2 files changed, 605 insertions(+), 8 deletions(-) diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index 79a10f2..db5c9ce 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -3,12 +3,6 @@ import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from import { GetConfigurationValue, LocalizeText } from '../../api'; import { TurnstileWidget } from './TurnstileWidget'; -/** - * Looks up a localized string. Falls back to `fallback` when the key is - * missing (LocalizeText returns the key itself) or when the localization - * manager isn't ready yet (login runs very early). Parameters are - * %name%-substituted into the fallback so the UI stays correct pre-init. - */ const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string => { try @@ -16,7 +10,7 @@ const t = (key: string, fallback: string, params?: string[], replacements?: stri const value = LocalizeText(key, params ?? null, replacements ?? null); if(value && value !== key) return value; } - catch { /* localization manager not initialised yet */ } + catch {} if(!params || !replacements) return fallback; let out = fallback; @@ -328,7 +322,7 @@ export const LoginView: FC = ({ onAuthenticated }) => if(rememberMe && rememberToken) window.localStorage.setItem('nitro.remember.token', rememberToken); else window.localStorage.removeItem('nitro.remember.token'); } - catch { /* localStorage may be disabled in private mode */ } + catch {} clearLock(); onAuthenticated(ssoTicket); @@ -360,6 +354,8 @@ export const LoginView: FC = ({ onAuthenticated }) => } }, [ submitting, username, password, rememberMe, turnstileEnabled, loginTurnstileToken, loginUrl, postJson, clearLock, recordFailure, onAuthenticated, resetLoginTurnstile, pingLoginServer ]); + const newsUrl = GetConfigurationValue('login.news.endpoint', '/api/auth/news'); + const checkEmailUrl = GetConfigurationValue('login.check-email.endpoint', '/api/auth/check-email'); const checkUsernameUrl = GetConfigurationValue('login.check-username.endpoint', '/api/auth/check-username'); const imagingUrl = GetConfigurationValue('login.register.imaging.url', 'https://www.habbo.com/habbo-imaging/avatarimage?figure={figure}&gender={gender}&direction=2&head_direction=2&size=l'); @@ -512,6 +508,8 @@ export const LoginView: FC = ({ onAuthenticated }) => { rightRepeat ?
: null } { right ?
: null } + +
{ t('nitro.login.firsttime.title', 'First time here?') }
@@ -1429,3 +1427,159 @@ const ForgotDialog: FC = props =>
); }; + +interface NewsItem +{ + id: number; + title: string; + body: string; + image: string | null; + linkText: string; + linkUrl: string; +} + +interface NewsWindowProps { newsUrl: string; } + +const NEWS_AUTO_ADVANCE_MS = 10000; + +const resolveNewsImage = (raw: string | null | undefined): string => +{ + const value = (raw ?? '').trim(); + if(!value) return ''; + if(/^https?:\/\//i.test(value)) return value; + if(value.startsWith('//')) return value; + if(value.startsWith('/') && !value.startsWith('//')) return value; + if(value.startsWith('data:')) + { + return /^data:image\/[a-z0-9.+-]+[,;]/i.test(value) ? value : ''; + } + + const stripped = value.replace(/\s+/g, ''); + if(!/^[A-Za-z0-9+/=]+$/.test(stripped)) return ''; + let mime = 'image/png'; + if(stripped.startsWith('/9j/')) mime = 'image/jpeg'; + else if(stripped.startsWith('R0lGOD')) mime = 'image/gif'; + else if(stripped.startsWith('UklGR')) mime = 'image/webp'; + else if(stripped.startsWith('PHN2Zy') || stripped.startsWith('PD94bWw')) mime = 'image/svg+xml'; + else if(stripped.startsWith('iVBORw0KGgo')) mime = 'image/png'; + return `data:${ mime };base64,${ stripped }`; +}; + +const resolveNewsLink = (raw: string | null | undefined): string => +{ + const value = (raw ?? '').trim(); + if(!value) return ''; + try + { + const url = new URL(value, window.location.href); + const proto = url.protocol.toLowerCase(); + if(proto !== 'http:' && proto !== 'https:') return ''; + return url.href; + } + catch { return ''; } +}; + +const NewsWindow: FC = ({ newsUrl }) => +{ + const [ items, setItems ] = useState(null); + const [ failed, setFailed ] = useState(false); + const [ index, setIndex ] = useState(0); + const [ autoTick, setAutoTick ] = useState(0); + + useEffect(() => + { + if(!newsUrl) { setFailed(true); return; } + let cancelled = false; + fetch(newsUrl, { credentials: 'omit' }) + .then(async r => + { + if(!r.ok) throw new Error('status ' + r.status); + return r.json(); + }) + .then((json: unknown) => + { + if(cancelled) return; + const list = Array.isArray((json as { news?: unknown })?.news) + ? (json as { news: NewsItem[] }).news + : []; + setItems(list); + }) + .catch(() => { if(!cancelled) setFailed(true); }); + return () => { cancelled = true; }; + }, [ newsUrl ]); + + useEffect(() => + { + if(!items || items.length < 2) return; + const id = window.setTimeout(() => + { + setIndex(i => (i + 1) % items.length); + }, NEWS_AUTO_ADVANCE_MS); + return () => window.clearTimeout(id); + }, [ items, index, autoTick ]); + + if(failed) return null; + if(!items || !items.length) return null; + + const current = items[Math.min(index, items.length - 1)]; + const hasMany = items.length > 1; + const bumpAuto = () => setAutoTick(t => t + 1); + const prev = () => { setIndex(i => (i - 1 + items.length) % items.length); bumpAuto(); }; + const next = () => { setIndex(i => (i + 1) % items.length); bumpAuto(); }; + + const safeLinkUrl = resolveNewsLink(current.linkUrl); + const safeImageSrc = resolveNewsImage(current.image); + const openLink = () => + { + if(!safeLinkUrl) return; + window.open(safeLinkUrl, '_blank', 'noopener,noreferrer'); + }; + + return ( +
+
+ + + + + + +
+
+ { t('nitro.login.news.title', 'Hotel News') } +
+
+ { safeImageSrc && +
+ { { (e.currentTarget as HTMLImageElement).style.display = 'none'; } } + /> +
+ } +
{ current.title }
+ { current.body && +
{ current.body }
} + +
+ { current.linkText && safeLinkUrl + ? + : } + + { hasMany && +
+ + { index + 1 }/{ items.length } + +
+ } +
+
+
+
+
+ ); +}; diff --git a/src/css/login/LoginView.css b/src/css/login/LoginView.css index cd8ea96..df21654 100644 --- a/src/css/login/LoginView.css +++ b/src/css/login/LoginView.css @@ -586,3 +586,446 @@ line-height: 1.3; } +/* ─── Login News Window (Habbo flavour) ─── */ + +.nitro-login-view .login-news-stack { + position: absolute; + top: 25%; + left: 8vw; + transform: translateY(-50%); + display: flex; + flex-direction: column; + width: 388px; + z-index: 50; + pointer-events: auto; +} + +.nitro-login-view .news-card-wrapper { + position: relative; + animation: news-pop-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +.nitro-login-view .news-card-wrapper > .nitro-login-card.nitro-news-card { + position: relative; + overflow: visible; + border-width: 3px; + padding-top: 22px; + background: linear-gradient(180deg, #b9d4e3 0%, #a2bfd1 60%, #93b3c8 100%); + box-shadow: + inset 0 2px rgba(255, 255, 255, 0.5), + inset 0 -2px rgba(0, 0, 0, 0.12), + 0 6px 14px rgba(0, 0, 0, 0.35), + 0 0 0 4px rgba(63, 106, 133, 0.0); + animation: news-glow 3.2s ease-in-out infinite; +} + +/* Yellow Habbo-style ribbon title */ +.nitro-login-card.nitro-news-card .card-title.news-ribbon { + position: absolute; + top: -14px; + left: -10px; + right: -10px; + margin: 0; + padding: 6px 12px; + background: linear-gradient(180deg, #ffe27a 0%, #ffc742 50%, #f0a812 100%); + color: #5a3a00; + text-shadow: 0 1px rgba(255, 255, 255, 0.55); + border: 2px solid #8a5a00; + border-radius: 6px; + box-shadow: + inset 0 1px rgba(255, 255, 255, 0.7), + inset 0 -2px rgba(0, 0, 0, 0.15), + 0 3px 0 rgba(0, 0, 0, 0.2); + font-size: 13px; + font-weight: 800; + letter-spacing: 0.6px; + text-transform: uppercase; + text-align: center; + z-index: 2; +} + +/* Pennant tails on the ribbon */ +.nitro-login-card.nitro-news-card .card-title.news-ribbon::before, +.nitro-login-card.nitro-news-card .card-title.news-ribbon::after { + content: ""; + position: absolute; + bottom: -6px; + width: 12px; + height: 12px; + background: #c47800; + border: 2px solid #8a5a00; + z-index: -1; +} + +.nitro-login-card.nitro-news-card .card-title.news-ribbon::before { + left: -2px; + clip-path: polygon(0 0, 100% 0, 100% 100%); + transform: rotate(0deg); +} + +.nitro-login-card.nitro-news-card .card-title.news-ribbon::after { + right: -2px; + clip-path: polygon(0 0, 100% 0, 0 100%); +} + +.nitro-login-card.nitro-news-card .news-ribbon-text { + display: inline-block; + animation: news-ribbon-wobble 4s ease-in-out infinite; +} + +/* "NEW!" star badge */ +.nitro-login-view .news-new-badge { + position: absolute; + top: -28px; + right: -24px; + width: 78px; + height: 78px; + background: + radial-gradient(circle at 35% 30%, #fff7c2 0%, #ffd23a 45%, #d97c00 100%); + color: #5a1900; + font-weight: 900; + font-size: 11px; + letter-spacing: 0; + text-transform: uppercase; + text-shadow: 0 1px rgba(255, 255, 255, 0.6); + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #8a3a00; + box-shadow: + inset 0 2px rgba(255, 255, 255, 0.55), + inset 0 -2px rgba(0, 0, 0, 0.2), + 0 3px 6px rgba(0, 0, 0, 0.35); + clip-path: polygon( + 50% 0%, 61% 35%, 98% 35%, 68% 57%, + 79% 91%, 50% 70%, 21% 91%, 32% 57%, + 2% 35%, 39% 35% + ); + z-index: 4; + animation: news-badge-spin 2.8s ease-in-out infinite; + pointer-events: none; +} + +.nitro-login-view .news-new-badge span { + transform: rotate(-10deg); + display: inline-block; + line-height: 1; + white-space: nowrap; +} + +/* Sparkles around the card */ +.nitro-login-view .news-sparkle { + position: absolute; + color: #fff5b0; + text-shadow: + 0 0 6px rgba(255, 220, 120, 0.9), + 0 0 12px rgba(255, 200, 60, 0.6); + pointer-events: none; + z-index: 3; + user-select: none; + font-weight: 700; +} + +.nitro-login-view .news-sparkle-1 { + top: -8px; + left: 18px; + font-size: 14px; + animation: news-sparkle 2.1s ease-in-out infinite; + animation-delay: 0s; +} + +.nitro-login-view .news-sparkle-2 { + top: 38%; + left: -12px; + font-size: 12px; + animation: news-sparkle 2.4s ease-in-out infinite; + animation-delay: 0.6s; +} + +.nitro-login-view .news-sparkle-3 { + bottom: -6px; + right: 36px; + font-size: 16px; + animation: news-sparkle 2.7s ease-in-out infinite; + animation-delay: 1.1s; +} + +/* Body */ +.nitro-login-card.nitro-news-card .card-body.news-body { + gap: 8px; + font-size: 12px; + color: #0a2e45; +} + +.nitro-login-card.nitro-news-card .news-image { + position: relative; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #3f6a85; + border-radius: 4px; + background: + repeating-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 0 6px, + rgba(255, 255, 255, 0) 6px 12px + ), + linear-gradient(180deg, #cfe1ee 0%, #a8c5d6 100%); + overflow: hidden; + box-shadow: + inset 0 2px rgba(255, 255, 255, 0.6), + inset 0 -2px rgba(0, 0, 0, 0.15); + max-height: 150px; + transition: transform 0.25s ease; +} + +.nitro-login-card.nitro-news-card .news-image:hover { + transform: translateY(-1px) scale(1.01); +} + +.nitro-login-card.nitro-news-card .news-image::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 35%); +} + +.nitro-login-card.nitro-news-card .news-image img { + max-width: 100%; + max-height: 146px; + width: auto; + height: auto; + display: block; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + position: relative; + z-index: 1; +} + +.nitro-login-card.nitro-news-card .news-headline { + font-weight: 800; + font-size: 13px; + line-height: 1.25; + color: #0a2e45; + text-shadow: 0 1px rgba(255, 255, 255, 0.5); + letter-spacing: 0.2px; + border-bottom: 1px dashed rgba(63, 106, 133, 0.4); + padding-bottom: 4px; +} + +.nitro-login-card.nitro-news-card .news-text { + font-size: 11px; + line-height: 1.45; + color: #103e5d; + white-space: pre-line; + word-break: break-word; + max-height: 120px; + overflow-y: auto; + padding-right: 2px; +} + +.nitro-login-card.nitro-news-card .news-text::-webkit-scrollbar { + width: 6px; +} + +.nitro-login-card.nitro-news-card .news-text::-webkit-scrollbar-thumb { + background: rgba(63, 106, 133, 0.6); + border-radius: 3px; +} + +.nitro-login-card.nitro-news-card .news-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: 4px; +} + +.nitro-login-card.nitro-news-card .news-link-button { + padding: 4px 14px; + font-size: 11px; + font-weight: 800; + background: linear-gradient(180deg, #ffe27a 0%, #ffc742 60%, #f0a812 100%); + color: #5a3a00; + border: 1px solid #8a5a00; + text-shadow: 0 1px rgba(255, 255, 255, 0.45); + box-shadow: + inset 0 1px rgba(255, 255, 255, 0.7), + inset 0 -1px rgba(0, 0, 0, 0.15), + 0 2px 0 rgba(0, 0, 0, 0.2); + transition: transform 0.12s ease, box-shadow 0.12s ease; +} + +.nitro-login-card.nitro-news-card .news-link-button:hover { + background: linear-gradient(180deg, #fff0a8 0%, #ffd45c 60%, #f7b822 100%); + transform: translateY(-1px); + box-shadow: + inset 0 1px rgba(255, 255, 255, 0.8), + inset 0 -1px rgba(0, 0, 0, 0.15), + 0 3px 0 rgba(0, 0, 0, 0.25); +} + +.nitro-login-card.nitro-news-card .news-link-button:active { + transform: translateY(1px); + box-shadow: + inset 0 1px rgba(0, 0, 0, 0.15), + 0 0 0 rgba(0, 0, 0, 0); +} + +.nitro-login-card.nitro-news-card .news-pager { + display: flex; + align-items: center; + gap: 6px; + margin-left: auto; +} + +.nitro-login-card.nitro-news-card .news-pager .arrow-btn { + transition: transform 0.12s ease; +} + +.nitro-login-card.nitro-news-card .news-pager .arrow-btn:hover { + transform: scale(1.15); +} + +.nitro-login-card.nitro-news-card .news-counter { + font-size: 11px; + color: #134b6e; + font-weight: 700; + font-variant-numeric: tabular-nums; + min-width: 28px; + text-align: center; + text-shadow: 0 1px rgba(255, 255, 255, 0.4); +} + +@keyframes news-pop-in { + 0% { opacity: 0; transform: scale(0.85) translateY(8px); } + 60% { opacity: 1; transform: scale(1.04) translateY(0); } + 100% { opacity: 1; transform: scale(1) translateY(0); } +} + +@keyframes news-glow { + 0%, 100% { box-shadow: + inset 0 2px rgba(255, 255, 255, 0.5), + inset 0 -2px rgba(0, 0, 0, 0.12), + 0 6px 14px rgba(0, 0, 0, 0.35), + 0 0 0 0 rgba(255, 210, 60, 0.0); } + 50% { box-shadow: + inset 0 2px rgba(255, 255, 255, 0.5), + inset 0 -2px rgba(0, 0, 0, 0.12), + 0 6px 14px rgba(0, 0, 0, 0.35), + 0 0 18px 4px rgba(255, 210, 60, 0.45); } +} + +@keyframes news-ribbon-wobble { + 0%, 100% { transform: rotate(0deg) translateY(0); } + 25% { transform: rotate(-1.2deg) translateY(-1px); } + 75% { transform: rotate(1.2deg) translateY(1px); } +} + +@keyframes news-badge-spin { + 0%, 100% { transform: rotate(-8deg) scale(1); } + 50% { transform: rotate(8deg) scale(1.08); } +} + +@keyframes news-sparkle { + 0%, 100% { opacity: 0.2; transform: scale(0.7) rotate(0deg); } + 50% { opacity: 1; transform: scale(1.2) rotate(20deg); } +} + +@media (prefers-reduced-motion: reduce) { + .nitro-login-view .news-card-wrapper, + .nitro-login-view .news-card-wrapper > .nitro-login-card.nitro-news-card, + .nitro-login-view .news-new-badge, + .nitro-login-view .news-sparkle, + .nitro-login-card.nitro-news-card .news-ribbon-text { + animation: none !important; + } +} + +@media (max-width: 900px) { + .nitro-login-view .login-news-stack { + display: none; + } +} + +/* ─── Cloud intro (plays once per session) ─── */ + +.login-intro-clouds { + position: fixed; + inset: 0; + z-index: 1000; + pointer-events: none; + overflow: hidden; + animation: cloud-overlay-fade 2.8s linear forwards; +} + +.intro-cloud-bank { + position: absolute; + left: -10%; + width: 120%; + height: 70%; + display: flex; + align-items: center; + justify-content: space-around; + will-change: transform; +} + +.intro-cloud-bank-top { + top: -70%; + animation: cloud-bank-top 2.8s cubic-bezier(0.65, 0, 0.35, 1) forwards; +} + +.intro-cloud-bank-bottom { + bottom: -70%; + animation: cloud-bank-bottom 2.8s cubic-bezier(0.65, 0, 0.35, 1) forwards; +} + +.intro-cloud-puff { + flex-shrink: 0; + background: + radial-gradient(ellipse at 45% 38%, #ffffff 0%, #fbfdff 35%, rgba(247, 251, 255, 0.85) 60%, rgba(255, 255, 255, 0) 78%); + filter: drop-shadow(0 8px 14px rgba(140, 175, 205, 0.35)); + border-radius: 50%; +} + +.intro-cloud-bank-top .intro-cloud-puff { + align-self: flex-end; +} + +.intro-cloud-bank-bottom .intro-cloud-puff { + align-self: flex-start; +} + +.intro-cloud-puff-1 { width: 360px; height: 320px; transform: translateY(-10px); } +.intro-cloud-puff-2 { width: 260px; height: 240px; transform: translateY(20px); } +.intro-cloud-puff-3 { width: 420px; height: 380px; transform: translateY(-30px); } +.intro-cloud-puff-4 { width: 300px; height: 280px; transform: translateY(15px); } +.intro-cloud-puff-5 { width: 340px; height: 300px; transform: translateY(-5px); } + +@keyframes cloud-bank-top { + 0% { transform: translateY(0); } + 35% { transform: translateY(105%); } + 55% { transform: translateY(105%); } + 100% { transform: translateY(-10%); } +} + +@keyframes cloud-bank-bottom { + 0% { transform: translateY(0); } + 35% { transform: translateY(-105%); } + 55% { transform: translateY(-105%); } + 100% { transform: translateY(10%); } +} + +@keyframes cloud-overlay-fade { + 0%, 88% { opacity: 1; } + 100% { opacity: 0; } +} + +@media (prefers-reduced-motion: reduce) { + .login-intro-clouds, + .intro-cloud-bank-top, + .intro-cloud-bank-bottom { + animation-duration: 0.4s !important; + } +} From 46eb7b45fcaeb550e5f295640307810e1b4a39cf Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 1 May 2026 07:47:12 +0200 Subject: [PATCH 09/20] =?UTF-8?q?=F0=9F=86=99=20Code=20cleanup=20News=20UI?= =?UTF-8?q?=20Login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/renderer-config.example | 1 + src/components/login/LoginView.tsx | 1049 +---------------- .../login/components/ForgotDialog.tsx | 72 ++ .../login/components/NewsWindow.tsx | 122 ++ .../login/components/RegisterDialog.tsx | 633 ++++++++++ src/components/login/components/shared.ts | 9 + src/components/login/utils/ban.ts | 32 + src/components/login/utils/figure.ts | 106 ++ src/components/login/utils/i18n.ts | 27 + src/components/login/utils/lockState.ts | 23 + src/components/login/utils/news.ts | 46 + 11 files changed, 1079 insertions(+), 1041 deletions(-) create mode 100644 src/components/login/components/ForgotDialog.tsx create mode 100644 src/components/login/components/NewsWindow.tsx create mode 100644 src/components/login/components/RegisterDialog.tsx create mode 100644 src/components/login/components/shared.ts create mode 100644 src/components/login/utils/ban.ts create mode 100644 src/components/login/utils/figure.ts create mode 100644 src/components/login/utils/i18n.ts create mode 100644 src/components/login/utils/lockState.ts create mode 100644 src/components/login/utils/news.ts diff --git a/public/renderer-config.example b/public/renderer-config.example index 5fb8068..b45973e 100644 --- a/public/renderer-config.example +++ b/public/renderer-config.example @@ -54,6 +54,7 @@ "login.room_templates.endpoint": "${api.url}/api/auth/room-templates", "login.remember.endpoint": "${api.url}/api/auth/remember", "login.server_key.endpoint": "${api.url}/api/auth/server-key", + "login.news.endpoint": "${api.url}/api/auth/news", "login.turnstile.enabled": true, "login.turnstile.sitekey": "", "avatar.mandatory.libraries": [ diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index db5c9ce..e4a8f61 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -1,92 +1,15 @@ -import { GetConfiguration } from '@nitrots/nitro-renderer'; import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { GetConfigurationValue, LocalizeText } from '../../api'; +import { GetConfigurationValue } from '../../api'; +import { ForgotDialog } from './components/ForgotDialog'; +import { NewsWindow } from './components/NewsWindow'; +import { RegisterDialog } from './components/RegisterDialog'; import { TurnstileWidget } from './TurnstileWidget'; - -const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string => -{ - try - { - const value = LocalizeText(key, params ?? null, replacements ?? null); - if(value && value !== key) return value; - } - catch {} - - if(!params || !replacements) return fallback; - let out = fallback; - for(let i = 0; i < params.length; i++) - { - if(replacements[i] !== undefined) out = out.replace('%' + params[i] + '%', replacements[i]); - } - return out; -}; +import { BanInfo, formatRemaining, parseBan } from './utils/ban'; +import { interpolate, t } from './utils/i18n'; +import { LOCK_DURATION_MS, LOCK_WINDOW_MS, MAX_ATTEMPTS, readLock, writeLock } from './utils/lockState'; type DialogMode = 'login' | 'register' | 'forgot'; -interface BanInfo -{ - type: 'account' | 'ip' | 'machine' | 'super' | string; - reason: string; - permanent: boolean; - expiresAt?: number; -} - -const parseBan = (payload: Record): BanInfo | null => -{ - const raw = payload?.ban; - if(!raw || typeof raw !== 'object') return null; - const ban = raw as Record; - const type = typeof ban.type === 'string' ? ban.type : 'account'; - const reason = typeof ban.reason === 'string' ? ban.reason : ''; - const permanent = ban.permanent === true || ban.permanent === 'true'; - const expiresAt = typeof ban.expiresAt === 'number' ? ban.expiresAt : undefined; - return { type, reason, permanent, expiresAt }; -}; - -const formatRemaining = (epochSeconds: number): string => -{ - const totalSeconds = Math.max(0, epochSeconds - Math.floor(Date.now() / 1000)); - const days = Math.floor(totalSeconds / 86400); - const hours = Math.floor((totalSeconds % 86400) / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - const seconds = totalSeconds % 60; - if(days > 0) return `${ days }d ${ hours }h ${ minutes }m`; - if(hours > 0) return `${ hours }h ${ minutes }m`; - if(minutes > 0) return `${ minutes }m ${ seconds }s`; - return `${ seconds }s`; -}; - -const interpolate = (value: string | null | undefined): string => -{ - if(!value) return ''; - try { return GetConfiguration().interpolate(value); } - catch { return value; } -}; - -const LOCK_KEY = 'nitro.login.lock'; -const MAX_ATTEMPTS = 5; -const LOCK_WINDOW_MS = 60_000; -const LOCK_DURATION_MS = 2 * 60_000; - -type AttemptState = { attempts: number; firstAt: number; lockedUntil: number }; - -const readLock = (): AttemptState => -{ - try - { - const raw = sessionStorage.getItem(LOCK_KEY); - if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 }; - return JSON.parse(raw); - } - catch { return { attempts: 0, firstAt: 0, lockedUntil: 0 }; } -}; - -const writeLock = (state: AttemptState) => -{ - try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); } - catch { } -}; - export interface LoginViewProps { onAuthenticated: (ssoTicket: string) => void; @@ -359,6 +282,7 @@ export const LoginView: FC = ({ onAuthenticated }) => const checkEmailUrl = GetConfigurationValue('login.check-email.endpoint', '/api/auth/check-email'); const checkUsernameUrl = GetConfigurationValue('login.check-username.endpoint', '/api/auth/check-username'); const imagingUrl = GetConfigurationValue('login.register.imaging.url', 'https://www.habbo.com/habbo-imaging/avatarimage?figure={figure}&gender={gender}&direction=2&head_direction=2&size=l'); + const interpretAvailability = (ok: boolean, status: number, payload: Record): { available: boolean; error?: string } => { const isTrue = (v: unknown) => v === true || v === 'true' || v === 1 || v === '1'; @@ -626,960 +550,3 @@ export const LoginView: FC = ({ onAuthenticated }) =>
); }; - -interface DialogSharedProps -{ - onCancel: () => void; - submitting: boolean; - error: string | null; - info: string | null; - turnstileEnabled: boolean; - turnstileSiteKey: string; -} - -interface RegisterDialogProps extends DialogSharedProps -{ - onSubmit: (body: { username: string; email: string; password: string; figure: string; gender: string; turnstileToken: string; templateId: number | null; }, onDialogReset: () => void) => void; - onCheckEmail: (email: string) => Promise<{ available: boolean; error?: string }>; - onCheckUsername: (username: string) => Promise<{ available: boolean; error?: string }>; - onCheckServer: () => Promise; - imagingUrl: string; - roomTemplatesUrl: string; -} - -type RegisterStep = 'credentials' | 'avatar' | 'room'; - -interface RoomTemplate { templateId: number; title: string; description: string; thumbnail: string; } - -const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - -type GenderKey = 'M' | 'F'; - -const PART_ROWS: string[] = [ 'hr', 'hd', 'ch', 'lg', 'sh' ]; - -const FALLBACK_DEFAULTS: Record> = { - M: { - hr: { partId: 180, colors: [ 45 ] }, - hd: { partId: 180, colors: [ 1 ] }, - ch: { partId: 215, colors: [ 66 ] }, - lg: { partId: 270, colors: [ 82 ] }, - sh: { partId: 290, colors: [ 80 ] } - }, - F: { - hr: { partId: 515, colors: [ 45 ] }, - hd: { partId: 600, colors: [ 1 ] }, - ch: { partId: 660, colors: [ 100 ] }, - lg: { partId: 716, colors: [ 82 ] }, - sh: { partId: 725, colors: [ 61 ] } - } -}; - -const FALLBACK_HEX: Record = { - 1: '#ffcb98', 8: '#f4ac54', 14: '#f5da88', 19: '#b87560', 20: '#9c543f', - 45: '#e8c498', 61: '#f1ece3', 66: '#96743d', 80: '#4f4d4d', 82: '#7f4f30', - 92: '#ececec', 100: '#c7ddff', 106: '#c6e6bd', 110: '#91a7c8', 143: '#ffffff' -}; - -interface FigureColor { id: number; hexCode: string; club: number; selectable: boolean; } -interface FigurePalette { id: number; colors: FigureColor[]; } -interface FigureSet { id: number; gender: 'M' | 'F' | 'U'; club: number; selectable: boolean; } -interface FigureSetType { type: string; paletteId: number; sets: FigureSet[]; } -interface FigureData { palettes: FigurePalette[]; setTypes: FigureSetType[]; } - -interface PartSelection { partId: number; colors: number[]; } -type FigureSelection = Record; - -const buildFigureString = (selection: FigureSelection): string => -{ - const seen = new Set(); - const parts: string[] = []; - const push = (setType: string) => - { - if(seen.has(setType)) return; - seen.add(setType); - const sel = selection[setType]; - if(!sel || sel.partId < 0) return; - const tail = (sel.colors && sel.colors.length) ? `-${ sel.colors.join('-') }` : ''; - parts.push(`${ setType }-${ sel.partId }${ tail }`); - }; - for(const setType of PART_ROWS) push(setType); - for(const setType of Object.keys(selection)) push(setType); - return parts.join('.'); -}; - -const buildImagingUrl = (template: string, figure: string, gender: GenderKey): string => - template - .replace(/\{figure\}/g, encodeURIComponent(figure)) - .replace(/\{gender\}/g, gender) - .replace(/\{direction\}/g, '2'); - -const HEAD_ONLY_PARTS = new Set([ 'hr', 'hd' ]); - -const buildPartPreviewUrl = ( - template: string, - setType: string, - selection: FigureSelection, - gender: GenderKey -): string => -{ - const defaults = FALLBACK_DEFAULTS[gender]; - const partSel = selection[setType] ?? defaults[setType]; - const tail = (partSel.colors && partSel.colors.length) ? `-${ partSel.colors.join('-') }` : ''; - const isHeadOnly = HEAD_ONLY_PARTS.has(setType); - - let parts: string[]; - if(isHeadOnly) - { - const hd = defaults.hd; - const pieces = new Map(); - pieces.set('hd', `hd-${ hd.partId }-${ hd.colors.join('-') }`); - pieces.set(setType, `${ setType }-${ partSel.partId }${ tail }`); - parts = Array.from(pieces.values()); - } - else - { - const hd = defaults.hd; - parts = [ - `hd-${ hd.partId }-${ hd.colors.join('-') }`, - `${ setType }-${ partSel.partId }${ tail }` - ]; - } - - const figure = parts.join('.'); - let url = template - .replace(/\{figure\}/g, encodeURIComponent(figure)) - .replace(/\{gender\}/g, gender) - .replace(/\{direction\}/g, '2'); - - url = url.replace(/size=l/, 'size=s').replace(/size=m/, 'size=s'); - if(!/size=/.test(url)) url += (url.includes('?') ? '&' : '?') + 'size=s'; - if(isHeadOnly && !/headonly=/.test(url)) url += '&headonly=1'; - - return url; -}; - -const RegisterDialog: FC = props => -{ - const { onCancel, onSubmit, onCheckEmail, onCheckUsername, onCheckServer, imagingUrl, roomTemplatesUrl, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props; - - const [ step, setStep ] = useState('credentials'); - const [ email, setEmail ] = useState(''); - const [ password, setPassword ] = useState(''); - const [ confirm, setConfirm ] = useState(''); - const [ username, setUsername ] = useState(''); - const [ gender, setGender ] = useState('F'); - const [ selection, setSelection ] = useState(() => ({ ...FALLBACK_DEFAULTS.F })); - const [ localError, setLocalError ] = useState(null); - const [ checking, setChecking ] = useState(false); - const [ turnstileToken, setTurnstileToken ] = useState(''); - const [ resetSignal, setResetSignal ] = useState(0); - const [ serverReachable, setServerReachable ] = useState(null); - const [ pingingServer, setPingingServer ] = useState(false); - - const pingServer = useCallback(async () => - { - setPingingServer(true); - try - { - const ok = await onCheckServer(); - setServerReachable(ok); - return ok; - } - finally - { - setPingingServer(false); - } - }, [ onCheckServer ]); - - useEffect(() => - { - let cancelled = false; - (async () => - { - const ok = await onCheckServer(); - if(!cancelled) setServerReachable(ok); - })(); - return () => { cancelled = true; }; - }, [ onCheckServer ]); - - const resetWidget = useCallback(() => - { - setTurnstileToken(''); - setResetSignal(prev => prev + 1); - }, []); - - useEffect(() => { setLocalError(null); }, [ step ]); - - const [ roomTemplates, setRoomTemplates ] = useState(null); - const [ roomTemplatesError, setRoomTemplatesError ] = useState(null); - const [ selectedTemplateId, setSelectedTemplateId ] = useState(null); - - const [ figureData, setFigureData ] = useState(null); - const figureDataUrlRaw = GetConfigurationValue('avatar.figuredata.url', ''); - const figureDataUrl = useMemo(() => - { - if(!figureDataUrlRaw) return ''; - try { return GetConfiguration().interpolate(figureDataUrlRaw); } - catch { return figureDataUrlRaw; } - }, [ figureDataUrlRaw ]); - - useEffect(() => - { - if(step !== 'avatar' || figureData || !figureDataUrl) return; - let cancelled = false; - fetch(figureDataUrl, { credentials: 'omit' }) - .then(r => r.ok ? r.json() : null) - .then(json => { if(!cancelled && json) setFigureData(json as FigureData); }) - .catch(() => { }); - return () => { cancelled = true; }; - }, [ step, figureData, figureDataUrl ]); - - useEffect(() => - { - if(step !== 'room' || roomTemplates !== null || !roomTemplatesUrl) return; - let cancelled = false; - setRoomTemplatesError(null); - fetch(roomTemplatesUrl, { credentials: 'include' }) - .then(async r => { - if(!r.ok) throw new Error(`status ${ r.status }`); - return r.json(); - }) - .then(json => { - if(cancelled) return; - const list = Array.isArray((json as { templates?: unknown })?.templates) - ? (json as { templates: RoomTemplate[] }).templates - : []; - setRoomTemplates(list); - }) - .catch(() => { - if(cancelled) return; - setRoomTemplates([]); - setRoomTemplatesError(t('nitro.login.register.room.error', 'Could not load room options. You can still skip this step.')); - }); - return () => { cancelled = true; }; - }, [ step, roomTemplates, roomTemplatesUrl ]); - - const partOptions = useMemo(() => - { - const result: Record> = {}; - if(!figureData) return result; - for(const st of figureData.setTypes) - { - if(!PART_ROWS.includes(st.type)) continue; - const forGender = (g: GenderKey) => st.sets - .filter(s => s.selectable && s.club === 0 && (s.gender === g || s.gender === 'U')) - .map(s => s.id); - result[st.type] = { M: forGender('M'), F: forGender('F') }; - } - return result; - }, [ figureData ]); - - const paletteOptions = useMemo(() => - { - const result: Record = {}; - if(!figureData) return result; - for(const st of figureData.setTypes) - { - if(!PART_ROWS.includes(st.type)) continue; - const palette = figureData.palettes.find(p => p.id === st.paletteId); - if(!palette) { result[st.type] = []; continue; } - result[st.type] = palette.colors - .filter(c => c.selectable && c.club === 0) - .map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() })); - } - return result; - }, [ figureData ]); - - const hexFor = useCallback((setType: string, colorId: number): string => - { - const list = paletteOptions[setType]; - if(list) - { - const found = list.find(c => c.id === colorId); - if(found) return found.hex; - } - return FALLBACK_HEX[colorId] || '#c9c9c9'; - }, [ paletteOptions ]); - - const [ hotLooks, setHotLooks ] = useState<{ gender: GenderKey; figure: string }[]>([]); - const [ hotLookIndex, setHotLookIndex ] = useState(-1); - - useEffect(() => - { - if(step !== 'avatar' || hotLooks.length) return; - let cancelled = false; - fetch('hotlooks.json', { credentials: 'omit' }) - .then(r => r.ok ? r.json() : null) - .then((json: unknown) => - { - if(cancelled || !Array.isArray(json)) return; - const parsed: { gender: GenderKey; figure: string }[] = []; - for(const entry of json as Record[]) - { - const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : ''; - const figure = typeof entry._figure === 'string' ? entry._figure : ''; - if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue; - parsed.push({ gender: rawGender as GenderKey, figure }); - } - if(parsed.length) setHotLooks(parsed); - }) - .catch(() => { }); - return () => { cancelled = true; }; - }, [ step, hotLooks.length ]); - - const applyLook = useCallback((figure: string, lookGender: GenderKey) => - { - const next: FigureSelection = {}; - for(const setPart of figure.split('.')) - { - const bits = setPart.split('-'); - if(bits.length < 2) continue; - const setType = bits[0]; - const partId = parseInt(bits[1], 10); - if(!setType || Number.isNaN(partId)) continue; - const colors: number[] = []; - for(let i = 2; i < bits.length; i++) - { - const c = parseInt(bits[i], 10); - if(!Number.isNaN(c)) colors.push(c); - } - next[setType] = { partId, colors }; - } - - for(const setType of PART_ROWS) - { - if(!next[setType]) next[setType] = { ...FALLBACK_DEFAULTS[lookGender][setType] }; - } - setGender(lookGender); - setSelection(next); - }, []); - - const cycleHotLook = useCallback(() => - { - if(!hotLooks.length) return; - const nextIdx = (hotLookIndex + 1) % hotLooks.length; - setHotLookIndex(nextIdx); - const look = hotLooks[nextIdx]; - applyLook(look.figure, look.gender); - }, [ hotLooks, hotLookIndex, applyLook ]); - - const credentialsValid = - EMAIL_REGEX.test(email.trim()) && - password.length >= 8 && - password === confirm; - - const handleCredentialsNext = async (event: FormEvent) => - { - event.preventDefault(); - setLocalError(null); - - if(!email.trim() || !password || !confirm) - { - setLocalError(t('nitro.login.error.missing_fields', 'Please fill in every field.')); - return; - } - if(!EMAIL_REGEX.test(email.trim())) - { - setLocalError(t('nitro.login.error.invalid_email', 'Please enter a valid email address.')); - return; - } - if(password.length < 8) - { - setLocalError(t('nitro.login.error.password_too_short', 'Your password must be at least 8 characters.')); - return; - } - if(password !== confirm) - { - setLocalError(t('nitro.login.error.password_mismatch', 'Passwords do not match.')); - return; - } - - setChecking(true); - try - { - const serverOk = await pingServer(); - if(!serverOk) - { - setLocalError(t('nitro.login.error.server_offline', 'The gameserver is not running. Please try again later.')); - return; - } - const result = await onCheckEmail(email.trim()); - if(!result.available) - { - setLocalError(result.error || t('nitro.login.error.email_taken', 'This email is already in use.')); - return; - } - setStep('avatar'); - } - finally - { - setChecking(false); - } - }; - - const applyGender = (newGender: GenderKey) => - { - setGender(newGender); - setSelection({ ...FALLBACK_DEFAULTS[newGender] }); - setHotLookIndex(-1); - }; - - const getPartList = useCallback((setType: string): number[] => - { - const loaded = partOptions[setType]?.[gender]; - if(loaded && loaded.length) return loaded; - const fallback = FALLBACK_DEFAULTS[gender][setType]?.partId; - return fallback !== undefined ? [ fallback ] : []; - }, [ partOptions, gender ]); - - const getColorList = useCallback((setType: string): number[] => - { - const loaded = paletteOptions[setType]; - if(loaded && loaded.length) return loaded.map(c => c.id); - const fallback = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0]; - return fallback !== undefined ? [ fallback ] : []; - }, [ paletteOptions, gender ]); - - const cyclePart = (setType: string, direction: 1 | -1) => - { - const options = getPartList(setType); - if(!options.length) return; - const current = selection[setType]?.partId ?? options[0]; - const idx = options.indexOf(current); - const nextIdx = ((idx === -1 ? 0 : idx) + direction + options.length) % options.length; - const colors = getColorList(setType); - setSelection(prev => ({ - ...prev, - [setType]: { - partId: options[nextIdx], - colors: prev[setType]?.colors ?? [ colors[0] ?? 0 ] - } - })); - }; - - const cycleColor = (setType: string, direction: 1 | -1) => - { - const colors = getColorList(setType); - if(!colors.length) return; - const currentColor = selection[setType]?.colors?.[0] ?? colors[0]; - const idx = colors.indexOf(currentColor); - const nextIdx = ((idx === -1 ? 0 : idx) + direction + colors.length) % colors.length; - const parts = getPartList(setType); - setSelection(prev => ({ - ...prev, - [setType]: { - partId: prev[setType]?.partId ?? parts[0], - colors: [ colors[nextIdx] ] - } - })); - }; - - const figure = buildFigureString(selection); - const previewSrc = buildImagingUrl(imagingUrl, figure, gender); - - const handleAvatarSubmit = async (event: FormEvent) => - { - event.preventDefault(); - setLocalError(null); - - const trimmed = username.trim(); - if(!trimmed) - { - setLocalError(t('nitro.login.error.missing_username', 'Please choose a Habbo name.')); - return; - } - if(trimmed.length < 3 || trimmed.length > 16) - { - setLocalError(t('nitro.login.error.username_length', 'Habbo name must be 3–16 characters.')); - return; - } - - if(turnstileEnabled && !turnstileToken) - { - setLocalError(t('nitro.login.error.turnstile', 'Please complete the security check.')); - return; - } - - setChecking(true); - try - { - const serverOk = await pingServer(); - if(!serverOk) - { - setLocalError(t('nitro.login.error.server_offline', 'The gameserver is not running. Please try again later.')); - return; - } - const result = await onCheckUsername(trimmed); - if(!result.available) - { - setLocalError(result.error || t('nitro.login.error.username_taken', 'This Habbo name is already taken.')); - return; - } - } - finally - { - setChecking(false); - } - - setStep('room'); - }; - - const submitRegistration = (templateId: number | null) => - { - onSubmit({ - username: username.trim(), - email: email.trim(), - password, - figure, - gender, - turnstileToken, - templateId - }, resetWidget); - }; - - const handleRoomSubmit = (event: FormEvent) => - { - event.preventDefault(); - setLocalError(null); - submitRegistration(selectedTemplateId); - }; - - const busy = submitting || checking || pingingServer; - const serverOffline = serverReachable === false; - - return ( -
-
-
-
- { t('nitro.login.register.title', 'Habbo Details') } - -
- - { step === 'credentials' && -
-
- { t('nitro.login.register.intro.credentials', 'Let\'s create your account. Enter your email and pick a password — we\'ll check that email isn\'t already in use.') } -
- { serverOffline && -
- { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') } - -
- } -
- - setEmail(e.target.value) } /> -
-
- - setPassword(e.target.value) } /> -
-
- - setConfirm(e.target.value) } /> -
- { (localError || error) &&
{ localError || error }
} - { info &&
{ info }
} -
- 1/3 - -
-
- } - - { step === 'avatar' && -
-
- { t('nitro.login.register.intro.avatar', 'Now it\'s time to make your own Habbo character! To make your own Habbo, please start by choosing your Habbo Name.') } -
- { serverOffline && -
- { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') } - -
- } -
- setUsername(e.target.value) } /> -
- -
- - -
- -
-
- { PART_ROWS.map(setType => { - const partPreviewSrc = buildPartPreviewUrl(imagingUrl, setType, selection, gender); - return ( -
- -
- { { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> -
- -
- ); - }) } -
- -
- Habbo preview { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> -
- -
- { PART_ROWS.map(setType => { - const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0; - const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor; - const swatchHex = hexFor(setType, currentColor); - return ( -
- -
- -
- ); - }) } -
-
- -
- -
- - { turnstileEnabled && - setTurnstileToken('') } - onError={ () => setTurnstileToken('') } - resetSignal={ resetSignal } - /> } - { (localError || error) &&
{ localError || error }
} - { info &&
{ info }
} - -
- - 2/3 - -
- - } - - { step === 'room' && -
-
- { t('nitro.login.register.intro.room', 'Last step — pick a starter room, or skip and create your own later.') } -
- { serverOffline && -
- { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') } - -
- } - -
- - - { roomTemplates === null &&
{ t('nitro.login.register.room.loading', 'Loading rooms…') }
} - - { roomTemplates !== null && roomTemplates.map(template => ( - - )) } -
- - { roomTemplatesError &&
{ roomTemplatesError }
} - { (localError || error) &&
{ localError || error }
} - { info &&
{ info }
} - -
- - 3/3 - -
-
- } -
-
-
- ); -}; - - -interface ForgotDialogProps extends DialogSharedProps -{ - onSubmit: (body: { email: string; turnstileToken: string; }, onDialogReset: () => void) => void; -} - -const ForgotDialog: FC = props => -{ - const { onCancel, onSubmit, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props; - const [ email, setEmail ] = useState(''); - const [ localError, setLocalError ] = useState(null); - const [ turnstileToken, setTurnstileToken ] = useState(''); - const [ resetSignal, setResetSignal ] = useState(0); - - const resetWidget = useCallback(() => - { - setTurnstileToken(''); - setResetSignal(prev => prev + 1); - }, []); - - const handle = (event: FormEvent) => - { - event.preventDefault(); - setLocalError(null); - - if(!email.trim()) - { - setLocalError(t('nitro.login.error.missing_email', 'Please enter your email address.')); - return; - } - - onSubmit({ email: email.trim(), turnstileToken }, resetWidget); - }; - - return ( -
-
-
-
- { t('nitro.login.forgot.title', 'Reset password') } - -
-
-
- - setEmail(e.target.value) } /> -
- { turnstileEnabled && - setTurnstileToken('') } - onError={ () => setTurnstileToken('') } - resetSignal={ resetSignal } - /> } - { (localError || error) &&
{ localError || error }
} - { info &&
{ info }
} -
- -
- -
-
-
- ); -}; - -interface NewsItem -{ - id: number; - title: string; - body: string; - image: string | null; - linkText: string; - linkUrl: string; -} - -interface NewsWindowProps { newsUrl: string; } - -const NEWS_AUTO_ADVANCE_MS = 10000; - -const resolveNewsImage = (raw: string | null | undefined): string => -{ - const value = (raw ?? '').trim(); - if(!value) return ''; - if(/^https?:\/\//i.test(value)) return value; - if(value.startsWith('//')) return value; - if(value.startsWith('/') && !value.startsWith('//')) return value; - if(value.startsWith('data:')) - { - return /^data:image\/[a-z0-9.+-]+[,;]/i.test(value) ? value : ''; - } - - const stripped = value.replace(/\s+/g, ''); - if(!/^[A-Za-z0-9+/=]+$/.test(stripped)) return ''; - let mime = 'image/png'; - if(stripped.startsWith('/9j/')) mime = 'image/jpeg'; - else if(stripped.startsWith('R0lGOD')) mime = 'image/gif'; - else if(stripped.startsWith('UklGR')) mime = 'image/webp'; - else if(stripped.startsWith('PHN2Zy') || stripped.startsWith('PD94bWw')) mime = 'image/svg+xml'; - else if(stripped.startsWith('iVBORw0KGgo')) mime = 'image/png'; - return `data:${ mime };base64,${ stripped }`; -}; - -const resolveNewsLink = (raw: string | null | undefined): string => -{ - const value = (raw ?? '').trim(); - if(!value) return ''; - try - { - const url = new URL(value, window.location.href); - const proto = url.protocol.toLowerCase(); - if(proto !== 'http:' && proto !== 'https:') return ''; - return url.href; - } - catch { return ''; } -}; - -const NewsWindow: FC = ({ newsUrl }) => -{ - const [ items, setItems ] = useState(null); - const [ failed, setFailed ] = useState(false); - const [ index, setIndex ] = useState(0); - const [ autoTick, setAutoTick ] = useState(0); - - useEffect(() => - { - if(!newsUrl) { setFailed(true); return; } - let cancelled = false; - fetch(newsUrl, { credentials: 'omit' }) - .then(async r => - { - if(!r.ok) throw new Error('status ' + r.status); - return r.json(); - }) - .then((json: unknown) => - { - if(cancelled) return; - const list = Array.isArray((json as { news?: unknown })?.news) - ? (json as { news: NewsItem[] }).news - : []; - setItems(list); - }) - .catch(() => { if(!cancelled) setFailed(true); }); - return () => { cancelled = true; }; - }, [ newsUrl ]); - - useEffect(() => - { - if(!items || items.length < 2) return; - const id = window.setTimeout(() => - { - setIndex(i => (i + 1) % items.length); - }, NEWS_AUTO_ADVANCE_MS); - return () => window.clearTimeout(id); - }, [ items, index, autoTick ]); - - if(failed) return null; - if(!items || !items.length) return null; - - const current = items[Math.min(index, items.length - 1)]; - const hasMany = items.length > 1; - const bumpAuto = () => setAutoTick(t => t + 1); - const prev = () => { setIndex(i => (i - 1 + items.length) % items.length); bumpAuto(); }; - const next = () => { setIndex(i => (i + 1) % items.length); bumpAuto(); }; - - const safeLinkUrl = resolveNewsLink(current.linkUrl); - const safeImageSrc = resolveNewsImage(current.image); - const openLink = () => - { - if(!safeLinkUrl) return; - window.open(safeLinkUrl, '_blank', 'noopener,noreferrer'); - }; - - return ( -
-
- - - - - - -
-
- { t('nitro.login.news.title', 'Hotel News') } -
-
- { safeImageSrc && -
- { { (e.currentTarget as HTMLImageElement).style.display = 'none'; } } - /> -
- } -
{ current.title }
- { current.body && -
{ current.body }
} - -
- { current.linkText && safeLinkUrl - ? - : } - - { hasMany && -
- - { index + 1 }/{ items.length } - -
- } -
-
-
-
-
- ); -}; diff --git a/src/components/login/components/ForgotDialog.tsx b/src/components/login/components/ForgotDialog.tsx new file mode 100644 index 0000000..65cffa8 --- /dev/null +++ b/src/components/login/components/ForgotDialog.tsx @@ -0,0 +1,72 @@ +import { FC, FormEvent, useCallback, useState } from 'react'; +import { TurnstileWidget } from '../TurnstileWidget'; +import { t } from '../utils/i18n'; +import { DialogSharedProps } from './shared'; + +export interface ForgotDialogProps extends DialogSharedProps +{ + onSubmit: (body: { email: string; turnstileToken: string; }, onDialogReset: () => void) => void; +} + +export const ForgotDialog: FC = props => +{ + const { onCancel, onSubmit, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props; + const [ email, setEmail ] = useState(''); + const [ localError, setLocalError ] = useState(null); + const [ turnstileToken, setTurnstileToken ] = useState(''); + const [ resetSignal, setResetSignal ] = useState(0); + + const resetWidget = useCallback(() => + { + setTurnstileToken(''); + setResetSignal(prev => prev + 1); + }, []); + + const handle = (event: FormEvent) => + { + event.preventDefault(); + setLocalError(null); + + if(!email.trim()) + { + setLocalError(t('nitro.login.error.missing_email', 'Please enter your email address.')); + return; + } + + onSubmit({ email: email.trim(), turnstileToken }, resetWidget); + }; + + return ( +
+
+
+
+ { t('nitro.login.forgot.title', 'Reset password') } + +
+
+
+ + setEmail(e.target.value) } /> +
+ { turnstileEnabled && + setTurnstileToken('') } + onError={ () => setTurnstileToken('') } + resetSignal={ resetSignal } + /> } + { (localError || error) &&
{ localError || error }
} + { info &&
{ info }
} +
+ +
+ +
+
+
+ ); +}; diff --git a/src/components/login/components/NewsWindow.tsx b/src/components/login/components/NewsWindow.tsx new file mode 100644 index 0000000..a7c05d3 --- /dev/null +++ b/src/components/login/components/NewsWindow.tsx @@ -0,0 +1,122 @@ +import { FC, useEffect, useState } from 'react'; +import { t } from '../utils/i18n'; +import { resolveNewsImage, resolveNewsLink } from '../utils/news'; + +interface NewsItem +{ + id: number; + title: string; + body: string; + image: string | null; + linkText: string; + linkUrl: string; +} + +interface NewsWindowProps { newsUrl: string; } + +const NEWS_AUTO_ADVANCE_MS = 10000; + +export const NewsWindow: FC = ({ newsUrl }) => +{ + const [ items, setItems ] = useState(null); + const [ failed, setFailed ] = useState(false); + const [ index, setIndex ] = useState(0); + const [ autoTick, setAutoTick ] = useState(0); + + useEffect(() => + { + if(!newsUrl) { setFailed(true); return; } + let cancelled = false; + fetch(newsUrl, { credentials: 'omit' }) + .then(async r => + { + if(!r.ok) throw new Error('status ' + r.status); + return r.json(); + }) + .then((json: unknown) => + { + if(cancelled) return; + const list = Array.isArray((json as { news?: unknown })?.news) + ? (json as { news: NewsItem[] }).news + : []; + setItems(list); + }) + .catch(() => { if(!cancelled) setFailed(true); }); + return () => { cancelled = true; }; + }, [ newsUrl ]); + + useEffect(() => + { + if(!items || items.length < 2) return; + const id = window.setTimeout(() => + { + setIndex(i => (i + 1) % items.length); + }, NEWS_AUTO_ADVANCE_MS); + return () => window.clearTimeout(id); + }, [ items, index, autoTick ]); + + if(failed) return null; + if(!items || !items.length) return null; + + const current = items[Math.min(index, items.length - 1)]; + const hasMany = items.length > 1; + const bumpAuto = () => setAutoTick(t => t + 1); + const prev = () => { setIndex(i => (i - 1 + items.length) % items.length); bumpAuto(); }; + const next = () => { setIndex(i => (i + 1) % items.length); bumpAuto(); }; + + const safeLinkUrl = resolveNewsLink(current.linkUrl); + const safeImageSrc = resolveNewsImage(current.image); + const openLink = () => + { + if(!safeLinkUrl) return; + window.open(safeLinkUrl, '_blank', 'noopener,noreferrer'); + }; + + return ( +
+
+ + + + + + +
+
+ { t('nitro.login.news.title', 'Hotel News') } +
+
+ { safeImageSrc && +
+ { { (e.currentTarget as HTMLImageElement).style.display = 'none'; } } + /> +
+ } +
{ current.title }
+ { current.body && +
{ current.body }
} + +
+ { current.linkText && safeLinkUrl + ? + : } + + { hasMany && +
+ + { index + 1 }/{ items.length } + +
+ } +
+
+
+
+
+ ); +}; diff --git a/src/components/login/components/RegisterDialog.tsx b/src/components/login/components/RegisterDialog.tsx new file mode 100644 index 0000000..b952c56 --- /dev/null +++ b/src/components/login/components/RegisterDialog.tsx @@ -0,0 +1,633 @@ +import { FC, FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; +import { GetConfiguration } from '@nitrots/nitro-renderer'; +import { GetConfigurationValue } from '../../../api'; +import { TurnstileWidget } from '../TurnstileWidget'; +import { t } from '../utils/i18n'; +import { + buildFigureString, + buildImagingUrl, + buildPartPreviewUrl, + EMAIL_REGEX, + FALLBACK_DEFAULTS, + FALLBACK_HEX, + FigureData, + FigureSelection, + GenderKey, + PART_ROWS +} from '../utils/figure'; +import { DialogSharedProps } from './shared'; + +export interface RegisterDialogProps extends DialogSharedProps +{ + onSubmit: (body: { username: string; email: string; password: string; figure: string; gender: string; turnstileToken: string; templateId: number | null; }, onDialogReset: () => void) => void; + onCheckEmail: (email: string) => Promise<{ available: boolean; error?: string }>; + onCheckUsername: (username: string) => Promise<{ available: boolean; error?: string }>; + onCheckServer: () => Promise; + imagingUrl: string; + roomTemplatesUrl: string; +} + +type RegisterStep = 'credentials' | 'avatar' | 'room'; + +interface RoomTemplate { templateId: number; title: string; description: string; thumbnail: string; } + +export const RegisterDialog: FC = props => +{ + const { onCancel, onSubmit, onCheckEmail, onCheckUsername, onCheckServer, imagingUrl, roomTemplatesUrl, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props; + + const [ step, setStep ] = useState('credentials'); + const [ email, setEmail ] = useState(''); + const [ password, setPassword ] = useState(''); + const [ confirm, setConfirm ] = useState(''); + const [ username, setUsername ] = useState(''); + const [ gender, setGender ] = useState('F'); + const [ selection, setSelection ] = useState(() => ({ ...FALLBACK_DEFAULTS.F })); + const [ localError, setLocalError ] = useState(null); + const [ checking, setChecking ] = useState(false); + const [ turnstileToken, setTurnstileToken ] = useState(''); + const [ resetSignal, setResetSignal ] = useState(0); + const [ serverReachable, setServerReachable ] = useState(null); + const [ pingingServer, setPingingServer ] = useState(false); + + const pingServer = useCallback(async () => + { + setPingingServer(true); + try + { + const ok = await onCheckServer(); + setServerReachable(ok); + return ok; + } + finally + { + setPingingServer(false); + } + }, [ onCheckServer ]); + + useEffect(() => + { + let cancelled = false; + (async () => + { + const ok = await onCheckServer(); + if(!cancelled) setServerReachable(ok); + })(); + return () => { cancelled = true; }; + }, [ onCheckServer ]); + + const resetWidget = useCallback(() => + { + setTurnstileToken(''); + setResetSignal(prev => prev + 1); + }, []); + + useEffect(() => { setLocalError(null); }, [ step ]); + + const [ roomTemplates, setRoomTemplates ] = useState(null); + const [ roomTemplatesError, setRoomTemplatesError ] = useState(null); + const [ selectedTemplateId, setSelectedTemplateId ] = useState(null); + + const [ figureData, setFigureData ] = useState(null); + const figureDataUrlRaw = GetConfigurationValue('avatar.figuredata.url', ''); + const figureDataUrl = useMemo(() => + { + if(!figureDataUrlRaw) return ''; + try { return GetConfiguration().interpolate(figureDataUrlRaw); } + catch { return figureDataUrlRaw; } + }, [ figureDataUrlRaw ]); + + useEffect(() => + { + if(step !== 'avatar' || figureData || !figureDataUrl) return; + let cancelled = false; + fetch(figureDataUrl, { credentials: 'omit' }) + .then(r => r.ok ? r.json() : null) + .then(json => { if(!cancelled && json) setFigureData(json as FigureData); }) + .catch(() => { }); + return () => { cancelled = true; }; + }, [ step, figureData, figureDataUrl ]); + + useEffect(() => + { + if(step !== 'room' || roomTemplates !== null || !roomTemplatesUrl) return; + let cancelled = false; + setRoomTemplatesError(null); + fetch(roomTemplatesUrl, { credentials: 'include' }) + .then(async r => { + if(!r.ok) throw new Error(`status ${ r.status }`); + return r.json(); + }) + .then(json => { + if(cancelled) return; + const list = Array.isArray((json as { templates?: unknown })?.templates) + ? (json as { templates: RoomTemplate[] }).templates + : []; + setRoomTemplates(list); + }) + .catch(() => { + if(cancelled) return; + setRoomTemplates([]); + setRoomTemplatesError(t('nitro.login.register.room.error', 'Could not load room options. You can still skip this step.')); + }); + return () => { cancelled = true; }; + }, [ step, roomTemplates, roomTemplatesUrl ]); + + const partOptions = useMemo(() => + { + const result: Record> = {}; + if(!figureData) return result; + for(const st of figureData.setTypes) + { + if(!PART_ROWS.includes(st.type)) continue; + const forGender = (g: GenderKey) => st.sets + .filter(s => s.selectable && s.club === 0 && (s.gender === g || s.gender === 'U')) + .map(s => s.id); + result[st.type] = { M: forGender('M'), F: forGender('F') }; + } + return result; + }, [ figureData ]); + + const paletteOptions = useMemo(() => + { + const result: Record = {}; + if(!figureData) return result; + for(const st of figureData.setTypes) + { + if(!PART_ROWS.includes(st.type)) continue; + const palette = figureData.palettes.find(p => p.id === st.paletteId); + if(!palette) { result[st.type] = []; continue; } + result[st.type] = palette.colors + .filter(c => c.selectable && c.club === 0) + .map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() })); + } + return result; + }, [ figureData ]); + + const hexFor = useCallback((setType: string, colorId: number): string => + { + const list = paletteOptions[setType]; + if(list) + { + const found = list.find(c => c.id === colorId); + if(found) return found.hex; + } + return FALLBACK_HEX[colorId] || '#c9c9c9'; + }, [ paletteOptions ]); + + const [ hotLooks, setHotLooks ] = useState<{ gender: GenderKey; figure: string }[]>([]); + const [ hotLookIndex, setHotLookIndex ] = useState(-1); + + useEffect(() => + { + if(step !== 'avatar' || hotLooks.length) return; + let cancelled = false; + fetch('hotlooks.json', { credentials: 'omit' }) + .then(r => r.ok ? r.json() : null) + .then((json: unknown) => + { + if(cancelled || !Array.isArray(json)) return; + const parsed: { gender: GenderKey; figure: string }[] = []; + for(const entry of json as Record[]) + { + const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : ''; + const figure = typeof entry._figure === 'string' ? entry._figure : ''; + if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue; + parsed.push({ gender: rawGender as GenderKey, figure }); + } + if(parsed.length) setHotLooks(parsed); + }) + .catch(() => { }); + return () => { cancelled = true; }; + }, [ step, hotLooks.length ]); + + const applyLook = useCallback((figure: string, lookGender: GenderKey) => + { + const next: FigureSelection = {}; + for(const setPart of figure.split('.')) + { + const bits = setPart.split('-'); + if(bits.length < 2) continue; + const setType = bits[0]; + const partId = parseInt(bits[1], 10); + if(!setType || Number.isNaN(partId)) continue; + const colors: number[] = []; + for(let i = 2; i < bits.length; i++) + { + const c = parseInt(bits[i], 10); + if(!Number.isNaN(c)) colors.push(c); + } + next[setType] = { partId, colors }; + } + + for(const setType of PART_ROWS) + { + if(!next[setType]) next[setType] = { ...FALLBACK_DEFAULTS[lookGender][setType] }; + } + setGender(lookGender); + setSelection(next); + }, []); + + const cycleHotLook = useCallback(() => + { + if(!hotLooks.length) return; + const nextIdx = (hotLookIndex + 1) % hotLooks.length; + setHotLookIndex(nextIdx); + const look = hotLooks[nextIdx]; + applyLook(look.figure, look.gender); + }, [ hotLooks, hotLookIndex, applyLook ]); + + const credentialsValid = + EMAIL_REGEX.test(email.trim()) && + password.length >= 8 && + password === confirm; + + const handleCredentialsNext = async (event: FormEvent) => + { + event.preventDefault(); + setLocalError(null); + + if(!email.trim() || !password || !confirm) + { + setLocalError(t('nitro.login.error.missing_fields', 'Please fill in every field.')); + return; + } + if(!EMAIL_REGEX.test(email.trim())) + { + setLocalError(t('nitro.login.error.invalid_email', 'Please enter a valid email address.')); + return; + } + if(password.length < 8) + { + setLocalError(t('nitro.login.error.password_too_short', 'Your password must be at least 8 characters.')); + return; + } + if(password !== confirm) + { + setLocalError(t('nitro.login.error.password_mismatch', 'Passwords do not match.')); + return; + } + + setChecking(true); + try + { + const serverOk = await pingServer(); + if(!serverOk) + { + setLocalError(t('nitro.login.error.server_offline', 'The gameserver is not running. Please try again later.')); + return; + } + const result = await onCheckEmail(email.trim()); + if(!result.available) + { + setLocalError(result.error || t('nitro.login.error.email_taken', 'This email is already in use.')); + return; + } + setStep('avatar'); + } + finally + { + setChecking(false); + } + }; + + const applyGender = (newGender: GenderKey) => + { + setGender(newGender); + setSelection({ ...FALLBACK_DEFAULTS[newGender] }); + setHotLookIndex(-1); + }; + + const getPartList = useCallback((setType: string): number[] => + { + const loaded = partOptions[setType]?.[gender]; + if(loaded && loaded.length) return loaded; + const fallback = FALLBACK_DEFAULTS[gender][setType]?.partId; + return fallback !== undefined ? [ fallback ] : []; + }, [ partOptions, gender ]); + + const getColorList = useCallback((setType: string): number[] => + { + const loaded = paletteOptions[setType]; + if(loaded && loaded.length) return loaded.map(c => c.id); + const fallback = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0]; + return fallback !== undefined ? [ fallback ] : []; + }, [ paletteOptions, gender ]); + + const cyclePart = (setType: string, direction: 1 | -1) => + { + const options = getPartList(setType); + if(!options.length) return; + const current = selection[setType]?.partId ?? options[0]; + const idx = options.indexOf(current); + const nextIdx = ((idx === -1 ? 0 : idx) + direction + options.length) % options.length; + const colors = getColorList(setType); + setSelection(prev => ({ + ...prev, + [setType]: { + partId: options[nextIdx], + colors: prev[setType]?.colors ?? [ colors[0] ?? 0 ] + } + })); + }; + + const cycleColor = (setType: string, direction: 1 | -1) => + { + const colors = getColorList(setType); + if(!colors.length) return; + const currentColor = selection[setType]?.colors?.[0] ?? colors[0]; + const idx = colors.indexOf(currentColor); + const nextIdx = ((idx === -1 ? 0 : idx) + direction + colors.length) % colors.length; + const parts = getPartList(setType); + setSelection(prev => ({ + ...prev, + [setType]: { + partId: prev[setType]?.partId ?? parts[0], + colors: [ colors[nextIdx] ] + } + })); + }; + + const figure = buildFigureString(selection); + const previewSrc = buildImagingUrl(imagingUrl, figure, gender); + + const handleAvatarSubmit = async (event: FormEvent) => + { + event.preventDefault(); + setLocalError(null); + + const trimmed = username.trim(); + if(!trimmed) + { + setLocalError(t('nitro.login.error.missing_username', 'Please choose a Habbo name.')); + return; + } + if(trimmed.length < 3 || trimmed.length > 16) + { + setLocalError(t('nitro.login.error.username_length', 'Habbo name must be 3–16 characters.')); + return; + } + + if(turnstileEnabled && !turnstileToken) + { + setLocalError(t('nitro.login.error.turnstile', 'Please complete the security check.')); + return; + } + + setChecking(true); + try + { + const serverOk = await pingServer(); + if(!serverOk) + { + setLocalError(t('nitro.login.error.server_offline', 'The gameserver is not running. Please try again later.')); + return; + } + const result = await onCheckUsername(trimmed); + if(!result.available) + { + setLocalError(result.error || t('nitro.login.error.username_taken', 'This Habbo name is already taken.')); + return; + } + } + finally + { + setChecking(false); + } + + setStep('room'); + }; + + const submitRegistration = (templateId: number | null) => + { + onSubmit({ + username: username.trim(), + email: email.trim(), + password, + figure, + gender, + turnstileToken, + templateId + }, resetWidget); + }; + + const handleRoomSubmit = (event: FormEvent) => + { + event.preventDefault(); + setLocalError(null); + submitRegistration(selectedTemplateId); + }; + + const busy = submitting || checking || pingingServer; + const serverOffline = serverReachable === false; + + return ( +
+
+
+
+ { t('nitro.login.register.title', 'Habbo Details') } + +
+ + { step === 'credentials' && +
+
+ { t('nitro.login.register.intro.credentials', 'Let\'s create your account. Enter your email and pick a password — we\'ll check that email isn\'t already in use.') } +
+ { serverOffline && +
+ { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') } + +
+ } +
+ + setEmail(e.target.value) } /> +
+
+ + setPassword(e.target.value) } /> +
+
+ + setConfirm(e.target.value) } /> +
+ { (localError || error) &&
{ localError || error }
} + { info &&
{ info }
} +
+ 1/3 + +
+
+ } + + { step === 'avatar' && +
+
+ { t('nitro.login.register.intro.avatar', 'Now it\'s time to make your own Habbo character! To make your own Habbo, please start by choosing your Habbo Name.') } +
+ { serverOffline && +
+ { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') } + +
+ } +
+ setUsername(e.target.value) } /> +
+ +
+ + +
+ +
+
+ { PART_ROWS.map(setType => { + const partPreviewSrc = buildPartPreviewUrl(imagingUrl, setType, selection, gender); + return ( +
+ +
+ { { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> +
+ +
+ ); + }) } +
+ +
+ Habbo preview { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> +
+ +
+ { PART_ROWS.map(setType => { + const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0; + const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor; + const swatchHex = hexFor(setType, currentColor); + return ( +
+ +
+ +
+ ); + }) } +
+
+ +
+ +
+ + { turnstileEnabled && + setTurnstileToken('') } + onError={ () => setTurnstileToken('') } + resetSignal={ resetSignal } + /> } + { (localError || error) &&
{ localError || error }
} + { info &&
{ info }
} + +
+ + 2/3 + +
+ + } + + { step === 'room' && +
+
+ { t('nitro.login.register.intro.room', 'Last step — pick a starter room, or skip and create your own later.') } +
+ { serverOffline && +
+ { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') } + +
+ } + +
+ + + { roomTemplates === null &&
{ t('nitro.login.register.room.loading', 'Loading rooms…') }
} + + { roomTemplates !== null && roomTemplates.map(template => ( + + )) } +
+ + { roomTemplatesError &&
{ roomTemplatesError }
} + { (localError || error) &&
{ localError || error }
} + { info &&
{ info }
} + +
+ + 3/3 + +
+
+ } +
+
+
+ ); +}; diff --git a/src/components/login/components/shared.ts b/src/components/login/components/shared.ts new file mode 100644 index 0000000..53e30ed --- /dev/null +++ b/src/components/login/components/shared.ts @@ -0,0 +1,9 @@ +export interface DialogSharedProps +{ + onCancel: () => void; + submitting: boolean; + error: string | null; + info: string | null; + turnstileEnabled: boolean; + turnstileSiteKey: string; +} diff --git a/src/components/login/utils/ban.ts b/src/components/login/utils/ban.ts new file mode 100644 index 0000000..7c9b904 --- /dev/null +++ b/src/components/login/utils/ban.ts @@ -0,0 +1,32 @@ +export interface BanInfo +{ + type: 'account' | 'ip' | 'machine' | 'super' | string; + reason: string; + permanent: boolean; + expiresAt?: number; +} + +export const parseBan = (payload: Record): BanInfo | null => +{ + const raw = payload?.ban; + if(!raw || typeof raw !== 'object') return null; + const ban = raw as Record; + const type = typeof ban.type === 'string' ? ban.type : 'account'; + const reason = typeof ban.reason === 'string' ? ban.reason : ''; + const permanent = ban.permanent === true || ban.permanent === 'true'; + const expiresAt = typeof ban.expiresAt === 'number' ? ban.expiresAt : undefined; + return { type, reason, permanent, expiresAt }; +}; + +export const formatRemaining = (epochSeconds: number): string => +{ + const totalSeconds = Math.max(0, epochSeconds - Math.floor(Date.now() / 1000)); + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + if(days > 0) return `${ days }d ${ hours }h ${ minutes }m`; + if(hours > 0) return `${ hours }h ${ minutes }m`; + if(minutes > 0) return `${ minutes }m ${ seconds }s`; + return `${ seconds }s`; +}; diff --git a/src/components/login/utils/figure.ts b/src/components/login/utils/figure.ts new file mode 100644 index 0000000..2743662 --- /dev/null +++ b/src/components/login/utils/figure.ts @@ -0,0 +1,106 @@ +export type GenderKey = 'M' | 'F'; + +export const PART_ROWS: string[] = [ 'hr', 'hd', 'ch', 'lg', 'sh' ]; + +export const FALLBACK_DEFAULTS: Record> = { + M: { + hr: { partId: 180, colors: [ 45 ] }, + hd: { partId: 180, colors: [ 1 ] }, + ch: { partId: 215, colors: [ 66 ] }, + lg: { partId: 270, colors: [ 82 ] }, + sh: { partId: 290, colors: [ 80 ] } + }, + F: { + hr: { partId: 515, colors: [ 45 ] }, + hd: { partId: 600, colors: [ 1 ] }, + ch: { partId: 660, colors: [ 100 ] }, + lg: { partId: 716, colors: [ 82 ] }, + sh: { partId: 725, colors: [ 61 ] } + } +}; + +export const FALLBACK_HEX: Record = { + 1: '#ffcb98', 8: '#f4ac54', 14: '#f5da88', 19: '#b87560', 20: '#9c543f', + 45: '#e8c498', 61: '#f1ece3', 66: '#96743d', 80: '#4f4d4d', 82: '#7f4f30', + 92: '#ececec', 100: '#c7ddff', 106: '#c6e6bd', 110: '#91a7c8', 143: '#ffffff' +}; + +export interface FigureColor { id: number; hexCode: string; club: number; selectable: boolean; } +export interface FigurePalette { id: number; colors: FigureColor[]; } +export interface FigureSet { id: number; gender: 'M' | 'F' | 'U'; club: number; selectable: boolean; } +export interface FigureSetType { type: string; paletteId: number; sets: FigureSet[]; } +export interface FigureData { palettes: FigurePalette[]; setTypes: FigureSetType[]; } + +export interface PartSelection { partId: number; colors: number[]; } +export type FigureSelection = Record; + +export const buildFigureString = (selection: FigureSelection): string => +{ + const seen = new Set(); + const parts: string[] = []; + const push = (setType: string) => + { + if(seen.has(setType)) return; + seen.add(setType); + const sel = selection[setType]; + if(!sel || sel.partId < 0) return; + const tail = (sel.colors && sel.colors.length) ? `-${ sel.colors.join('-') }` : ''; + parts.push(`${ setType }-${ sel.partId }${ tail }`); + }; + for(const setType of PART_ROWS) push(setType); + for(const setType of Object.keys(selection)) push(setType); + return parts.join('.'); +}; + +export const buildImagingUrl = (template: string, figure: string, gender: GenderKey): string => + template + .replace(/\{figure\}/g, encodeURIComponent(figure)) + .replace(/\{gender\}/g, gender) + .replace(/\{direction\}/g, '2'); + +const HEAD_ONLY_PARTS = new Set([ 'hr', 'hd' ]); + +export const buildPartPreviewUrl = ( + template: string, + setType: string, + selection: FigureSelection, + gender: GenderKey +): string => +{ + const defaults = FALLBACK_DEFAULTS[gender]; + const partSel = selection[setType] ?? defaults[setType]; + const tail = (partSel.colors && partSel.colors.length) ? `-${ partSel.colors.join('-') }` : ''; + const isHeadOnly = HEAD_ONLY_PARTS.has(setType); + + let parts: string[]; + if(isHeadOnly) + { + const hd = defaults.hd; + const pieces = new Map(); + pieces.set('hd', `hd-${ hd.partId }-${ hd.colors.join('-') }`); + pieces.set(setType, `${ setType }-${ partSel.partId }${ tail }`); + parts = Array.from(pieces.values()); + } + else + { + const hd = defaults.hd; + parts = [ + `hd-${ hd.partId }-${ hd.colors.join('-') }`, + `${ setType }-${ partSel.partId }${ tail }` + ]; + } + + const figure = parts.join('.'); + let url = template + .replace(/\{figure\}/g, encodeURIComponent(figure)) + .replace(/\{gender\}/g, gender) + .replace(/\{direction\}/g, '2'); + + url = url.replace(/size=l/, 'size=s').replace(/size=m/, 'size=s'); + if(!/size=/.test(url)) url += (url.includes('?') ? '&' : '?') + 'size=s'; + if(isHeadOnly && !/headonly=/.test(url)) url += '&headonly=1'; + + return url; +}; + +export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; diff --git a/src/components/login/utils/i18n.ts b/src/components/login/utils/i18n.ts new file mode 100644 index 0000000..58f88f8 --- /dev/null +++ b/src/components/login/utils/i18n.ts @@ -0,0 +1,27 @@ +import { GetConfiguration } from '@nitrots/nitro-renderer'; +import { LocalizeText } from '../../../api'; + +export const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string => +{ + try + { + const value = LocalizeText(key, params ?? null, replacements ?? null); + if(value && value !== key) return value; + } + catch {} + + if(!params || !replacements) return fallback; + let out = fallback; + for(let i = 0; i < params.length; i++) + { + if(replacements[i] !== undefined) out = out.replace('%' + params[i] + '%', replacements[i]); + } + return out; +}; + +export const interpolate = (value: string | null | undefined): string => +{ + if(!value) return ''; + try { return GetConfiguration().interpolate(value); } + catch { return value; } +}; diff --git a/src/components/login/utils/lockState.ts b/src/components/login/utils/lockState.ts new file mode 100644 index 0000000..ef5a946 --- /dev/null +++ b/src/components/login/utils/lockState.ts @@ -0,0 +1,23 @@ +export const LOCK_KEY = 'nitro.login.lock'; +export const MAX_ATTEMPTS = 5; +export const LOCK_WINDOW_MS = 60_000; +export const LOCK_DURATION_MS = 2 * 60_000; + +export type AttemptState = { attempts: number; firstAt: number; lockedUntil: number }; + +export const readLock = (): AttemptState => +{ + try + { + const raw = sessionStorage.getItem(LOCK_KEY); + if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 }; + return JSON.parse(raw); + } + catch { return { attempts: 0, firstAt: 0, lockedUntil: 0 }; } +}; + +export const writeLock = (state: AttemptState) => +{ + try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); } + catch { } +}; diff --git a/src/components/login/utils/news.ts b/src/components/login/utils/news.ts new file mode 100644 index 0000000..510655c --- /dev/null +++ b/src/components/login/utils/news.ts @@ -0,0 +1,46 @@ +/** + * Accepts a URL (http/https, protocol-relative, or site-relative), + * a data URL with an image mime type, or a raw base64 image payload. + * Anything else (including data:text/html, javascript:, etc.) is rejected + * to keep an admin-set DB value from becoming an XSS / phishing vector. + */ +export const resolveNewsImage = (raw: string | null | undefined): string => +{ + const value = (raw ?? '').trim(); + if(!value) return ''; + if(/^https?:\/\//i.test(value)) return value; + if(value.startsWith('//')) return value; + if(value.startsWith('/') && !value.startsWith('//')) return value; + if(value.startsWith('data:')) + { + return /^data:image\/[a-z0-9.+-]+[,;]/i.test(value) ? value : ''; + } + + const stripped = value.replace(/\s+/g, ''); + if(!/^[A-Za-z0-9+/=]+$/.test(stripped)) return ''; + let mime = 'image/png'; + if(stripped.startsWith('/9j/')) mime = 'image/jpeg'; + else if(stripped.startsWith('R0lGOD')) mime = 'image/gif'; + else if(stripped.startsWith('UklGR')) mime = 'image/webp'; + else if(stripped.startsWith('PHN2Zy') || stripped.startsWith('PD94bWw')) mime = 'image/svg+xml'; + else if(stripped.startsWith('iVBORw0KGgo')) mime = 'image/png'; + return `data:${ mime };base64,${ stripped }`; +}; + +/** + * Rejects anything that isn't an http(s) URL or a same-origin path so a + * malicious DB value can't be a `javascript:` / `data:` / `file:` link. + */ +export const resolveNewsLink = (raw: string | null | undefined): string => +{ + const value = (raw ?? '').trim(); + if(!value) return ''; + try + { + const url = new URL(value, window.location.href); + const proto = url.protocol.toLowerCase(); + if(proto !== 'http:' && proto !== 'https:') return ''; + return url.href; + } + catch { return ''; } +}; From a0a4848ecb325c5c00c12dc05806367429c4fc97 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 1 May 2026 08:24:16 +0200 Subject: [PATCH 10/20] =?UTF-8?q?=F0=9F=86=99=20Small=20update=20news=20im?= =?UTF-8?q?ages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/login/utils/news.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/login/utils/news.ts b/src/components/login/utils/news.ts index 510655c..0fa0c82 100644 --- a/src/components/login/utils/news.ts +++ b/src/components/login/utils/news.ts @@ -9,8 +9,12 @@ export const resolveNewsImage = (raw: string | null | undefined): string => const value = (raw ?? '').trim(); if(!value) return ''; if(/^https?:\/\//i.test(value)) return value; - if(value.startsWith('//')) return value; - if(value.startsWith('/') && !value.startsWith('//')) return value; + if(value.startsWith('//')) return window.location.protocol + value; + if(value.startsWith('/')) + { + try { return new URL(value, window.location.origin).href; } + catch { return window.location.origin + value; } + } if(value.startsWith('data:')) { return /^data:image\/[a-z0-9.+-]+[,;]/i.test(value) ? value : ''; From 506a29c9a0fd903d3d79a23af400c5a2f00fb071 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 1 May 2026 16:02:56 +0200 Subject: [PATCH 11/20] =?UTF-8?q?=F0=9F=86=95=20Create=20Custom=20Bage=20&?= =?UTF-8?q?=20Security=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/renderer-config.example | 7 + src/App.tsx | 38 +- src/api/auth/accessToken.ts | 52 ++ src/api/auth/index.ts | 1 + .../avatar/AvatarEditorThumbnailsHelper.ts | 4 - src/api/badges/CustomBadgeApi.ts | 172 +++++ src/api/badges/index.ts | 1 + src/api/index.ts | 2 + src/components/MainView.tsx | 2 + .../badge-creator/BadgeCreatorView.tsx | 629 ++++++++++++++++++ src/components/badge-creator/index.ts | 1 + .../views/badge/InventoryBadgeView.tsx | 89 ++- src/components/login/LoginView.tsx | 3 +- src/components/toolbar/ToolbarMeView.tsx | 1 + src/css/icons/icons.css | 13 +- 15 files changed, 1000 insertions(+), 15 deletions(-) create mode 100644 src/api/auth/accessToken.ts create mode 100644 src/api/auth/index.ts create mode 100644 src/api/badges/CustomBadgeApi.ts create mode 100644 src/api/badges/index.ts create mode 100644 src/components/badge-creator/BadgeCreatorView.tsx create mode 100644 src/components/badge-creator/index.ts diff --git a/public/renderer-config.example b/public/renderer-config.example index b45973e..eccba80 100644 --- a/public/renderer-config.example +++ b/public/renderer-config.example @@ -55,6 +55,13 @@ "login.remember.endpoint": "${api.url}/api/auth/remember", "login.server_key.endpoint": "${api.url}/api/auth/server-key", "login.news.endpoint": "${api.url}/api/auth/news", + "login.sso-token.endpoint": "${api.url}/api/auth/sso-token", + "login.refresh.endpoint": "${api.url}/api/auth/refresh", + "badges.custom.list.endpoint": "${api.url}/api/badges/custom", + "badges.custom.create.endpoint": "${api.url}/api/badges/custom", + "badges.custom.update.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.delete.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.texts.endpoint": "${api.url}/api/badges/custom/texts", "login.turnstile.enabled": true, "login.turnstile.sitekey": "", "avatar.mandatory.libraries": [ diff --git a/src/App.tsx b/src/App.tsx index f08efdc..b59c167 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroEventType, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useState } from 'react'; -import { GetUIVersion } from './api'; +import { clearAccessToken, getAccessToken, getAccessTokenExpiresAt, GetUIVersion, persistAccessTokenFromPayload } from './api'; import { Base } from './common'; import { LoadingView } from './components/loading/LoadingView'; import { LoginView } from './components/login/LoginView'; @@ -106,11 +106,13 @@ export const App: FC<{}> = props => window.localStorage.setItem('nitro.remember.token', payload.rememberToken); } catch {} + persistAccessTokenFromPayload(payload); } } else if(response.status === 401) { try { window.localStorage.removeItem('nitro.remember.token'); } catch {} + clearAccessToken(); } } catch {} @@ -118,6 +120,38 @@ export const App: FC<{}> = props => } } + if(ssoTicket) + { + const expiresAt = getAccessTokenExpiresAt(); + const nowSec = Math.floor(Date.now() / 1000); + const accessNeedsRefresh = !getAccessToken() || (expiresAt > 0 && expiresAt - nowSec < 60); + + if(accessNeedsRefresh) + { + const ssoTokenUrlTemplate = GetConfiguration().getValue('login.sso-token.endpoint', '/api/auth/sso-token'); + const ssoTokenUrl = GetConfiguration().interpolate(ssoTokenUrlTemplate); + try + { + const response = await fetch(ssoTokenUrl, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Requested-With': 'NitroSsoExchange' + }, + body: JSON.stringify({ ssoTicket }) + }); + if(response.ok) + { + const payload = await response.json(); + persistAccessTokenFromPayload(payload); + } + } + catch {} + } + } + if(!ssoTicket || ssoTicket === '') { const rawLoginEnabled = GetConfiguration().getValue('login.screen.enabled', false); @@ -219,10 +253,12 @@ export const App: FC<{}> = props => { try { window.localStorage.setItem('nitro.remember.token', payload.rememberToken); } catch {} } + persistAccessTokenFromPayload(payload); } else if(resp.status === 401) { try { window.localStorage.removeItem('nitro.remember.token'); } catch {} + clearAccessToken(); } } catch {} diff --git a/src/api/auth/accessToken.ts b/src/api/auth/accessToken.ts new file mode 100644 index 0000000..1d53575 --- /dev/null +++ b/src/api/auth/accessToken.ts @@ -0,0 +1,52 @@ +const STORAGE_KEY = 'nitro.access.token'; +const EXPIRES_KEY = 'nitro.access.token.exp'; + +export const setAccessToken = (token: string | null | undefined, expiresAt?: number | null): void => +{ + try + { + if(token && typeof token === 'string') + { + window.localStorage.setItem(STORAGE_KEY, token); + if(typeof expiresAt === 'number' && expiresAt > 0) window.localStorage.setItem(EXPIRES_KEY, String(expiresAt)); + else window.localStorage.removeItem(EXPIRES_KEY); + } + else + { + window.localStorage.removeItem(STORAGE_KEY); + window.localStorage.removeItem(EXPIRES_KEY); + } + } + catch {} +}; + +export const getAccessToken = (): string => +{ + try { return window.localStorage.getItem(STORAGE_KEY) ?? ''; } + catch { return ''; } +}; + +export const getAccessTokenExpiresAt = (): number => +{ + try + { + const raw = window.localStorage.getItem(EXPIRES_KEY); + if(!raw) return 0; + const value = parseInt(raw, 10); + return Number.isFinite(value) ? value : 0; + } + catch { return 0; } +}; + +export const clearAccessToken = (): void => +{ + setAccessToken(null); +}; + +export const persistAccessTokenFromPayload = (payload: Record | null | undefined): void => +{ + if(!payload) return; + const token = typeof payload.accessToken === 'string' ? payload.accessToken : ''; + const expiresAt = typeof payload.accessTokenExpiresAt === 'number' ? payload.accessTokenExpiresAt : null; + if(token) setAccessToken(token, expiresAt); +}; diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts new file mode 100644 index 0000000..865e7c0 --- /dev/null +++ b/src/api/auth/index.ts @@ -0,0 +1 @@ +export * from './accessToken'; diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index 9cb58b6..5176436 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -224,11 +224,8 @@ export class AvatarEditorThumbnailsHelper const texture = avatarImage.processAsTexture(AvatarSetType.HEAD, false); const sprite = new NitroSprite(texture); - if(isDisabled) sprite.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ]; - const frame = AvatarEditorThumbnailsHelper.findOpaqueBoundsFrame(sprite, texture.width, texture.height); - const imageUrl = await TextureUtils.generateImageUrl({ target: sprite, frame @@ -257,7 +254,6 @@ export class AvatarEditorThumbnailsHelper const width = data.width; const height = data.height; if(!pixels || width <= 0 || height <= 0) return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight); - const ALPHA_THRESHOLD = 8; let minX = width; diff --git a/src/api/badges/CustomBadgeApi.ts b/src/api/badges/CustomBadgeApi.ts new file mode 100644 index 0000000..9e6eeca --- /dev/null +++ b/src/api/badges/CustomBadgeApi.ts @@ -0,0 +1,172 @@ +import { GetConfiguration, GetLocalizationManager } from '@nitrots/nitro-renderer'; +import { getAccessToken } from '../auth'; + +export interface CustomBadgeRecord +{ + badgeId: string; + badgeCode: string; + name: string; + description: string; + dateCreated: number; + dateEdit: number; + url: string; +} + +export interface CustomBadgeListResponse +{ + badges: CustomBadgeRecord[]; + max: number; + badgeWidth: number; + badgeHeight: number; + maxBadgeSizeBytes: number; + priceBadge?: number; + currencyType?: number; +} + +export interface CustomBadgeError +{ + error: string; + code?: string; +} + +const interpolate = (value: string): string => +{ + try { return GetConfiguration().interpolate(value); } + catch { return value; } +}; + +const getConfigUrl = (key: string, fallback: string): string => + interpolate(GetConfiguration().getValue(key, fallback)); + +const buildUrl = (key: string, fallback: string, badgeId?: string): string => +{ + const template = getConfigUrl(key, fallback); + if(!badgeId) return template; + if(template.includes('%badgeId%')) return template.replace(/%badgeId%/g, encodeURIComponent(badgeId)); + return template + (template.endsWith('/') ? '' : '/') + encodeURIComponent(badgeId); +}; + +const authHeaders = (): Record => +{ + const headers: Record = { + 'Accept': 'application/json', + 'X-Requested-With': 'NitroCustomBadges' + }; + const token = getAccessToken(); + if(token) headers['Authorization'] = `Bearer ${ token }`; + return headers; +}; + +const parseJson = async (response: Response): Promise => +{ + const text = await response.text(); + if(!text) return {} as T; + try { return JSON.parse(text) as T; } + catch { throw new Error('Invalid response from server.'); } +}; + +const throwOnError = async (response: Response): Promise => +{ + if(response.ok) return; + const payload = await parseJson(response); + const message = payload?.error || `Request failed (${ response.status }).`; + const err = new Error(message) as Error & { status: number; code?: string }; + err.status = response.status; + if(payload?.code) err.code = payload.code; + throw err; +}; + +export const fetchCustomBadges = async (): Promise => +{ + const url = buildUrl('badges.custom.list.endpoint', '/api/badges/custom'); + const response = await fetch(url, { method: 'GET', credentials: 'include', headers: authHeaders() }); + await throwOnError(response); + return parseJson(response); +}; + +export const createCustomBadge = async (body: { name: string; description: string; image: string }): Promise => +{ + const url = buildUrl('badges.custom.create.endpoint', '/api/badges/custom'); + const response = await fetch(url, { + method: 'POST', + credentials: 'include', + headers: { ...authHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + await throwOnError(response); + return parseJson(response); +}; + +export const updateCustomBadge = async (badgeId: string, body: { name: string; description: string; image: string }): Promise => +{ + const url = buildUrl('badges.custom.update.endpoint', '/api/badges/custom/%badgeId%', badgeId); + const response = await fetch(url, { + method: 'PUT', + credentials: 'include', + headers: { ...authHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + await throwOnError(response); + return parseJson(response); +}; + +export const deleteCustomBadge = async (badgeId: string): Promise => +{ + const url = buildUrl('badges.custom.delete.endpoint', '/api/badges/custom/%badgeId%', badgeId); + const response = await fetch(url, { method: 'DELETE', credentials: 'include', headers: authHeaders() }); + await throwOnError(response); +}; + +export const isCustomBadgeCode = (code: string | null | undefined): boolean => +{ + if(!code) return false; + return /^CUST[A-Z0-9]{5}-\d+$/.test(code); +}; + +let customBadgeTextsLoadPromise: Promise | null = null; + +const injectTextsIntoLocalization = (texts: Record | null | undefined): void => +{ + if(!texts) return; + let manager: ReturnType | null = null; + try { manager = GetLocalizationManager(); } + catch { return; } + if(!manager || typeof manager.setValue !== 'function') return; + for(const key of Object.keys(texts)) + { + const value = texts[key]; + if(typeof value === 'string') manager.setValue(key, value); + } +}; + +export const ensureCustomBadgeTexts = (): Promise => +{ + if(customBadgeTextsLoadPromise) return customBadgeTextsLoadPromise; + customBadgeTextsLoadPromise = (async () => + { + try + { + const url = buildUrl('badges.custom.texts.endpoint', '/api/badges/custom/texts'); + const response = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Accept': 'application/json' } }); + if(!response.ok) return; + const payload = await parseJson<{ texts: Record }>(response); + injectTextsIntoLocalization(payload.texts); + } + catch {} + })(); + return customBadgeTextsLoadPromise; +}; + +export const refreshCustomBadgeTexts = (): Promise => +{ + customBadgeTextsLoadPromise = null; + return ensureCustomBadgeTexts(); +}; + +export const setCustomBadgeText = (badgeId: string, name: string, description: string): void => +{ + injectTextsIntoLocalization({ + [`badge_name_${ badgeId }`]: name || badgeId, + [`badge_desc_${ badgeId }`]: description || '' + }); +}; diff --git a/src/api/badges/index.ts b/src/api/badges/index.ts new file mode 100644 index 0000000..75e2cd1 --- /dev/null +++ b/src/api/badges/index.ts @@ -0,0 +1 @@ +export * from './CustomBadgeApi'; diff --git a/src/api/index.ts b/src/api/index.ts index 0f11ac4..6bb1536 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,9 @@ export * from './GetRendererVersion'; export * from './GetUIVersion'; export * from './achievements'; +export * from './auth'; export * from './avatar'; +export * from './badges'; export * from './camera'; export * from './campaign'; export * from './catalog'; diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 70f2fd3..41a7322 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -4,6 +4,7 @@ import { FC, useEffect, useState } from 'react'; import { useNitroEvent } from '../hooks'; import { AchievementsView } from './achievements/AchievementsView'; import { AvatarEditorView } from './avatar-editor'; +import { BadgeCreatorView } from './badge-creator'; import { AvatarEffectsView } from './avatar-effects'; import { CameraWidgetView } from './camera/CameraWidgetView'; import { CampaignView } from './campaign/CampaignView'; @@ -106,6 +107,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/badge-creator/BadgeCreatorView.tsx b/src/components/badge-creator/BadgeCreatorView.tsx new file mode 100644 index 0000000..e8d9db1 --- /dev/null +++ b/src/components/badge-creator/BadgeCreatorView.tsx @@ -0,0 +1,629 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, MouseEvent as ReactMouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { LocalizeText } from '../../api'; +import { createCustomBadge, CustomBadgeRecord, deleteCustomBadge, ensureCustomBadgeTexts, fetchCustomBadges, refreshCustomBadgeTexts, setCustomBadgeText, updateCustomBadge } from '../../api/badges'; +import { Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { useNotification } from '../../hooks'; + +const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string => +{ + try + { + const value = LocalizeText(key, params ?? null, replacements ?? null); + if(value && value !== key) return value; + } + catch {} + + if(!params || !replacements) return fallback; + let out = fallback; + for(let i = 0; i < params.length; i++) + { + if(replacements[i] !== undefined) out = out.replace('%' + params[i] + '%', replacements[i]); + } + return out; +}; + +const GRID_WIDTH = 40; +const GRID_HEIGHT = 40; +const PIXEL_DISPLAY_SIZE = 12; +const TRANSPARENT = 0; + +const PALETTE: number[] = [ + 0xFF000000, 0xFF4F4F4F, 0xFF808080, 0xFFB0B0B0, 0xFFD8D8D8, 0xFFFFFFFF, TRANSPARENT, 0xFF7B0000, + 0xFFBF0000, 0xFFFF0000, 0xFFFF7777, 0xFFFF7700, 0xFFFFAA00, 0xFFFFD700, 0xFFFFEB3B, 0xFF003E1F, + 0xFF006837, 0xFF00A653, 0xFF2BC93C, 0xFF00C8A0, 0xFF00BCFF, 0xFF2962FF, 0xFF1A237E, 0xFF4A0072, + 0xFF9C00B5, 0xFFE91E63, 0xFFFF80AB, 0xFF5D2E1A, 0xFF8B5A2B, 0xFFC28E5E, 0xFFF1D7B6, 0xFFE8C3A0 +]; + +const currencyName = (type: number): string => +{ + if(type === -1) return 'credits'; + if(type === 0) return 'duckets'; + if(type === 5) return 'diamonds'; + return `currency #${ type }`; +}; + +type Tool = 'paint' | 'erase' | 'picker' | 'fill'; + +const floodFill = (grid: Uint32Array, w: number, h: number, startX: number, startY: number, replacement: number): Uint32Array => +{ + if(startX < 0 || startY < 0 || startX >= w || startY >= h) return grid; + const startIdx = startY * w + startX; + const target = grid[startIdx]; + if(target === replacement) return grid; + + const next = new Uint32Array(grid.length); + next.set(grid); + + const stack: number[] = [ startIdx ]; + while(stack.length) + { + const idx = stack.pop() as number; + if(next[idx] !== target) continue; + next[idx] = replacement; + const x = idx % w; + const y = (idx - x) / w; + if(x > 0) stack.push(idx - 1); + if(x < w - 1) stack.push(idx + 1); + if(y > 0) stack.push(idx - w); + if(y < h - 1) stack.push(idx + w); + } + return next; +}; + +const argbToCss = (argb: number): string => +{ + if(argb === TRANSPARENT) return 'transparent'; + const a = ((argb >>> 24) & 0xff) / 255; + const r = (argb >>> 16) & 0xff; + const g = (argb >>> 8) & 0xff; + const b = argb & 0xff; + return `rgba(${ r }, ${ g }, ${ b }, ${ a })`; +}; + +const argbToHex = (argb: number): string => +{ + if(argb === TRANSPARENT) return '#000000'; + const r = (argb >>> 16) & 0xff; + const g = (argb >>> 8) & 0xff; + const b = argb & 0xff; + return '#' + [ r, g, b ].map(c => c.toString(16).padStart(2, '0')).join(''); +}; + +const hexToArgb = (hex: string): number => +{ + const match = /^#?([0-9a-f]{6})$/i.exec(hex || ''); + if(!match) return 0xFF000000; + return (0xFF000000 | parseInt(match[1], 16)) >>> 0; +}; + +const emptyGrid = (): Uint32Array => new Uint32Array(GRID_WIDTH * GRID_HEIGHT); + +const cloneGrid = (src: Uint32Array): Uint32Array => +{ + const copy = new Uint32Array(src.length); + copy.set(src); + return copy; +}; + +const gridToPngBase64 = async (grid: Uint32Array): Promise<{ b64: string; bytes: number }> => +{ + const canvas = document.createElement('canvas'); + canvas.width = GRID_WIDTH; + canvas.height = GRID_HEIGHT; + const ctx = canvas.getContext('2d'); + if(!ctx) throw new Error('Canvas not supported.'); + + const image = ctx.createImageData(GRID_WIDTH, GRID_HEIGHT); + for(let i = 0; i < grid.length; i++) + { + const argb = grid[i]; + const o = i * 4; + image.data[o] = (argb >>> 16) & 0xff; + image.data[o + 1] = (argb >>> 8) & 0xff; + image.data[o + 2] = argb & 0xff; + image.data[o + 3] = (argb >>> 24) & 0xff; + } + ctx.putImageData(image, 0, 0); + + const blob: Blob = await new Promise((resolve, reject) => canvas.toBlob(b => b ? resolve(b) : reject(new Error('PNG encode failed.')), 'image/png')); + const arrayBuffer = await blob.arrayBuffer(); + const bytes = arrayBuffer.byteLength; + let binary = ''; + const u8 = new Uint8Array(arrayBuffer); + for(let i = 0; i < u8.length; i++) binary += String.fromCharCode(u8[i]); + return { b64: window.btoa(binary), bytes }; +}; + +const loadGridFromUrl = (url: string): Promise => + new Promise((resolve, reject) => + { + const image = new Image(); + image.crossOrigin = 'anonymous'; + image.onload = () => + { + try + { + const canvas = document.createElement('canvas'); + canvas.width = GRID_WIDTH; + canvas.height = GRID_HEIGHT; + const ctx = canvas.getContext('2d'); + if(!ctx) return reject(new Error('Canvas not supported.')); + ctx.clearRect(0, 0, GRID_WIDTH, GRID_HEIGHT); + ctx.drawImage(image, 0, 0, GRID_WIDTH, GRID_HEIGHT); + const data = ctx.getImageData(0, 0, GRID_WIDTH, GRID_HEIGHT).data; + const grid = emptyGrid(); + for(let i = 0; i < grid.length; i++) + { + const o = i * 4; + const a = data[o + 3]; + if(a === 0) { grid[i] = 0; continue; } + grid[i] = ((a & 0xff) << 24) | ((data[o] & 0xff) << 16) | ((data[o + 1] & 0xff) << 8) | (data[o + 2] & 0xff); + } + resolve(grid); + } + catch(err) { reject(err); } + }; + image.onerror = () => reject(new Error('Could not load badge image (CORS?).')); + image.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now(); + }); + +export const BadgeCreatorView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ grid, setGrid ] = useState(() => emptyGrid()); + const [ selectedColor, setSelectedColor ] = useState(PALETTE[0]); + const [ tool, setTool ] = useState('paint'); + const [ showGrid, setShowGrid ] = useState(true); + const [ name, setName ] = useState(''); + const [ description, setDescription ] = useState(''); + const [ editingBadgeId, setEditingBadgeId ] = useState(null); + const [ badges, setBadges ] = useState(null); + const [ pendingEditBadgeId, setPendingEditBadgeId ] = useState(null); + const [ maxBadges, setMaxBadges ] = useState(5); + const [ maxBytes, setMaxBytes ] = useState(40960); + const [ priceBadge, setPriceBadge ] = useState(0); + const [ currencyType, setCurrencyType ] = useState(-1); + const [ submitting, setSubmitting ] = useState(false); + const [ error, setError ] = useState(null); + + const { showConfirm } = useNotification(); + + const refresh = useCallback(async () => + { + try + { + const data = await fetchCustomBadges(); + setBadges(data.badges ?? []); + if(typeof data.max === 'number') setMaxBadges(data.max); + if(typeof data.maxBadgeSizeBytes === 'number') setMaxBytes(data.maxBadgeSizeBytes); + if(typeof data.priceBadge === 'number') setPriceBadge(data.priceBadge); + if(typeof data.currencyType === 'number') setCurrencyType(data.currencyType); + } + catch(err) + { + setBadges([]); + setError((err as Error)?.message || 'Could not load badges.'); + } + }, []); + + useEffect(() => + { + const tracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + if(parts.length < 2) return; + switch(parts[1]) + { + case 'show': setIsVisible(true); return; + case 'hide': setIsVisible(false); return; + case 'toggle': setIsVisible(v => !v); return; + case 'edit': + if(!parts[2]) return; + setPendingEditBadgeId(parts[2]); + setIsVisible(true); + return; + } + }, + eventUrlPrefix: 'badge-creator/' + }; + AddLinkEventTracker(tracker); + return () => RemoveLinkEventTracker(tracker); + }, []); + + useEffect(() => { if(isVisible) { refresh(); ensureCustomBadgeTexts(); } }, [ isVisible, refresh ]); + + const resetEditor = useCallback(() => + { + setGrid(emptyGrid()); + setName(''); + setDescription(''); + setEditingBadgeId(null); + setError(null); + }, []); + + const startEdit = useCallback(async (badge: CustomBadgeRecord) => + { + setError(null); + setEditingBadgeId(badge.badgeId); + setName(badge.name || ''); + setDescription(badge.description || ''); + try + { + const loaded = await loadGridFromUrl(badge.url); + setGrid(loaded); + } + catch(err) + { + setError((err as Error)?.message || 'Could not load that badge.'); + setGrid(emptyGrid()); + } + }, []); + + useEffect(() => + { + if(!pendingEditBadgeId || !badges) return; + const target = badges.find(b => b.badgeId === pendingEditBadgeId); + if(!target) return; + setPendingEditBadgeId(null); + startEdit(target); + }, [ pendingEditBadgeId, badges, startEdit ]); + + const paintAt = useCallback((x: number, y: number, isClick: boolean) => + { + if(x < 0 || y < 0 || x >= GRID_WIDTH || y >= GRID_HEIGHT) return; + const idx = y * GRID_WIDTH + x; + + if(tool === 'picker') + { + const cell = grid[idx]; + if(cell !== TRANSPARENT) setSelectedColor(cell); + setTool('paint'); + return; + } + + if(tool === 'fill') + { + if(!isClick) return; + setGrid(floodFill(grid, GRID_WIDTH, GRID_HEIGHT, x, y, selectedColor)); + return; + } + + const value = (tool === 'erase') ? TRANSPARENT : selectedColor; + if(grid[idx] === value) return; + const next = cloneGrid(grid); + next[idx] = value; + setGrid(next); + }, [ grid, selectedColor, tool ]); + + const isDraggingRef = useRef(false); + const colorInputRef = useRef(null); + const mainCanvasRef = useRef(null); + const previewCanvasRef = useRef(null); + + useEffect(() => + { + const targets = [ mainCanvasRef.current, previewCanvasRef.current ]; + for(const canvas of targets) + { + if(!canvas) continue; + const ctx = canvas.getContext('2d'); + if(!ctx) continue; + const image = ctx.createImageData(GRID_WIDTH, GRID_HEIGHT); + const buffer = image.data; + for(let i = 0; i < grid.length; i++) + { + const v = grid[i]; + const o = i * 4; + buffer[o] = (v >>> 16) & 0xff; + buffer[o + 1] = (v >>> 8) & 0xff; + buffer[o + 2] = v & 0xff; + buffer[o + 3] = (v >>> 24) & 0xff; + } + ctx.putImageData(image, 0, 0); + } + }, [ grid, isVisible ]); + + const openColorPicker = useCallback(() => + { + const input = colorInputRef.current; + if(!input) return; + input.value = argbToHex(selectedColor); + input.click(); + }, [ selectedColor ]); + + const handleColorPicked = useCallback((event: React.ChangeEvent) => + { + setSelectedColor(hexToArgb(event.target.value)); + setTool('paint'); + }, []); + + const cellFromEvent = useCallback((event: ReactMouseEvent): { x: number; y: number } => + { + const rect = event.currentTarget.getBoundingClientRect(); + const x = Math.floor(((event.clientX - rect.left) / rect.width) * GRID_WIDTH); + const y = Math.floor(((event.clientY - rect.top) / rect.height) * GRID_HEIGHT); + return { x, y }; + }, []); + + const handleMouseDown = useCallback((event: ReactMouseEvent) => + { + if(event.button !== 0) return; + event.preventDefault(); + isDraggingRef.current = true; + const { x, y } = cellFromEvent(event); + paintAt(x, y, true); + }, [ cellFromEvent, paintAt ]); + + const handleMouseMove = useCallback((event: ReactMouseEvent) => + { + if(!isDraggingRef.current) return; + const { x, y } = cellFromEvent(event); + paintAt(x, y, false); + }, [ cellFromEvent, paintAt ]); + + useEffect(() => + { + const stopDrag = () => { isDraggingRef.current = false; }; + window.addEventListener('mouseup', stopDrag); + return () => window.removeEventListener('mouseup', stopDrag); + }, []); + + const clearCanvas = useCallback(() => setGrid(emptyGrid()), []); + + const copyColor = useCallback(() => setTool('picker'), []); + + const isEmpty = useMemo(() => + { + for(let i = 0; i < grid.length; i++) if(grid[i] !== 0) return false; + return true; + }, [ grid ]); + + const canCreateMore = (badges?.length ?? 0) < maxBadges; + + const handleSave = useCallback(async () => + { + if(submitting) return; + if(isEmpty) { setError(t('badgecreator.error.empty', 'Draw something first.')); return; } + if(!editingBadgeId && !canCreateMore) + { + setError(t('badgecreator.error.limit', 'You already have %max% custom badges.', [ 'max' ], [ String(maxBadges) ])); + return; + } + + setSubmitting(true); + setError(null); + try + { + const { b64, bytes } = await gridToPngBase64(grid); + if(bytes > maxBytes) + { + setError(t('badgecreator.error.too_large', `Image is too large (${ bytes } / %max% bytes).`, [ 'max' ], [ String(maxBytes) ])); + return; + } + const body = { name: name.trim(), description: description.trim(), image: b64 }; + const saved = editingBadgeId + ? await updateCustomBadge(editingBadgeId, body) + : await createCustomBadge(body); + if(saved && saved.badgeId) setCustomBadgeText(saved.badgeId, saved.name, saved.description); + await refresh(); + refreshCustomBadgeTexts(); + resetEditor(); + } + catch(err) + { + setError((err as Error)?.message || 'Could not save the badge.'); + } + finally + { + setSubmitting(false); + } + }, [ submitting, isEmpty, editingBadgeId, canCreateMore, maxBadges, grid, maxBytes, name, description, refresh, resetEditor ]); + + const handleDelete = useCallback((badge: CustomBadgeRecord) => + { + showConfirm( + t('badgecreator.delete.confirm', 'Delete "%name%"?', [ 'name' ], [ badge.name || badge.badgeId ]), + async () => + { + try + { + await deleteCustomBadge(badge.badgeId); + if(editingBadgeId === badge.badgeId) resetEditor(); + await refresh(); + refreshCustomBadgeTexts(); + } + catch(err) + { + setError((err as Error)?.message || 'Could not delete the badge.'); + } + }, + null, null, null, + t('badgecreator.delete.title', 'Delete badge') + ); + }, [ showConfirm, editingBadgeId, refresh, resetEditor ]); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + + +
+ +
+ + + + + + + + +
+ +
+ { t('badgecreator.palette', 'Palette') } +
+ { PALETTE.map((color, idx) => + { + const isTransparent = color === TRANSPARENT; + const isSelected = color === selectedColor; + return ( +
+
+ +
+ { argbToHex(selectedColor).toUpperCase() } + +
+
+
+ { t('badgecreator.preview', 'Preview') } +
+ +
+
+
+ { t('badgecreator.name', 'Name') } + setName(e.target.value) } /> +
+
+ { t('badgecreator.description', 'Description') } + setDescription(e.target.value) } /> +
+ { error && { error } } + { !editingBadgeId && priceBadge > 0 && + + { t('badgecreator.price', 'Cost: %price% %currency%', [ 'price', 'currency' ], [ String(priceBadge), currencyName(currencyType) ]) } + } + + + { editingBadgeId && + } + + + + + + { t('badgecreator.list.title', 'Your custom badges (%count%/%max%)', [ 'count', 'max' ], [ String(badges?.length ?? 0), String(maxBadges) ]) } + + { badges === null && { t('badgecreator.list.loading', 'Loading…') } } + { badges !== null && !badges.length && { t('badgecreator.list.empty', 'You haven\'t made any badges yet.') } } + { badges !== null && badges.map(badge => ( + + { + + { badge.name || badge.badgeId } + { badge.description && { badge.description } } + + + + + )) } + + + + ); +}; diff --git a/src/components/badge-creator/index.ts b/src/components/badge-creator/index.ts new file mode 100644 index 0000000..d12515e --- /dev/null +++ b/src/components/badge-creator/index.ts @@ -0,0 +1 @@ +export * from './BadgeCreatorView'; diff --git a/src/components/inventory/views/badge/InventoryBadgeView.tsx b/src/components/inventory/views/badge/InventoryBadgeView.tsx index f1fad00..1d39b6d 100644 --- a/src/components/inventory/views/badge/InventoryBadgeView.tsx +++ b/src/components/inventory/views/badge/InventoryBadgeView.tsx @@ -1,7 +1,7 @@ -import { DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { FaTrashAlt } from 'react-icons/fa'; -import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, SendMessageComposer, UnseenItemCategory } from '../../../../api'; +import { FaPaintBrush, FaPencilAlt, FaTrashAlt } from 'react-icons/fa'; +import { deleteCustomBadge, ensureCustomBadgeTexts, fetchCustomBadges, GetConfigurationValue, isCustomBadgeCode, LocalizeBadgeName, LocalizeText, refreshCustomBadgeTexts, SendMessageComposer, UnseenItemCategory } from '../../../../api'; import { LayoutBadgeImageView } from '../../../../common'; import { useInventoryBadges, useInventoryUnseenTracker, useNotification } from '../../../../hooks'; import { InfiniteGrid, NitroButton } from '../../../../layout'; @@ -90,7 +90,60 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = const [ isDraggingFromActive, setIsDraggingFromActive ] = useState(false); const maxSlots = useMemo(() => GetConfigurationValue('user.badges.max.slots', 5), []); - const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); + + const [ ownCustomBadgeIds, setOwnCustomBadgeIds ] = useState>(() => new Set()); + const [ filter, setFilter ] = useState<'all' | 'custom'>('all'); + + const refreshOwnCustomBadges = useCallback(async () => + { + try + { + const data = await fetchCustomBadges(); + setOwnCustomBadgeIds(new Set((data.badges ?? []).map(b => b.badgeId))); + } + catch + { + setOwnCustomBadgeIds(new Set()); + } + }, []); + + useEffect(() => { refreshOwnCustomBadges(); }, [ refreshOwnCustomBadges ]); + useEffect(() => { ensureCustomBadgeTexts(); }, []); + + const baseCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); + const customCount = useMemo(() => baseCodes.filter(c => isCustomBadgeCode(c)).length, [ baseCodes ]); + const displayCodes = useMemo(() => + filter === 'custom' ? baseCodes.filter(c => isCustomBadgeCode(c)) : baseCodes, + [ baseCodes, filter ]); + + const isOwnCustomBadge = (code: string | null) => !!code && isCustomBadgeCode(code) && ownCustomBadgeIds.has(code); + + const handleEditCustom = useCallback(() => + { + if(!selectedBadgeCode) return; + CreateLinkEvent(`badge-creator/edit/${ selectedBadgeCode }`); + }, [ selectedBadgeCode ]); + + const handleDeleteCustom = useCallback(() => + { + if(!selectedBadgeCode) return; + const target = selectedBadgeCode; + showConfirm( + LocalizeText('inventory.delete.confirm_delete.info', [ 'furniname', 'amount' ], [ LocalizeBadgeName(target), '1' ]), + async () => + { + try + { + await deleteCustomBadge(target); + await refreshOwnCustomBadges(); + refreshCustomBadgeTexts(); + } + catch { /* error already surfaced server-side */ } + }, + null, null, null, + LocalizeText('inventory.delete.confirm_delete.title') + ); + }, [ selectedBadgeCode, showConfirm, refreshOwnCustomBadges ]); const attemptDeleteBadge = () => { @@ -205,6 +258,28 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = { LocalizeText('inventory.badges.clearbadge') }
) } +
+ + + +
columnCount={ 5 } estimateSize={ 50 } @@ -242,8 +317,12 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = onClick={ event => toggleBadge(selectedBadgeCode) }> { LocalizeText(isWearingBadge(selectedBadgeCode) ? 'inventory.badges.clearbadge' : 'inventory.badges.wearbadge') } + { isOwnCustomBadge(selectedBadgeCode) && + + + } { !isWearingBadge(selectedBadgeCode) && - + }
diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index e4a8f61..83a8c31 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -1,5 +1,5 @@ import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { GetConfigurationValue } from '../../api'; +import { GetConfigurationValue, persistAccessTokenFromPayload } from '../../api'; import { ForgotDialog } from './components/ForgotDialog'; import { NewsWindow } from './components/NewsWindow'; import { RegisterDialog } from './components/RegisterDialog'; @@ -244,6 +244,7 @@ export const LoginView: FC = ({ onAuthenticated }) => const rememberToken = typeof payload.rememberToken === 'string' ? payload.rememberToken : ''; if(rememberMe && rememberToken) window.localStorage.setItem('nitro.remember.token', rememberToken); else window.localStorage.removeItem('nitro.remember.token'); + persistAccessTokenFromPayload(payload); } catch {} diff --git a/src/components/toolbar/ToolbarMeView.tsx b/src/components/toolbar/ToolbarMeView.tsx index 642e519..741b66a 100644 --- a/src/components/toolbar/ToolbarMeView.tsx +++ b/src/components/toolbar/ToolbarMeView.tsx @@ -42,6 +42,7 @@ export const ToolbarMeView: FC GetUserProfile(GetSessionDataManager().userId) } />
CreateLinkEvent('navigator/search/myworld_view') } />
CreateLinkEvent('avatar-editor/toggle') } /> +
CreateLinkEvent('badge-creator/toggle') } title={ LocalizeText('toolbar.icon.label.badge_creator') } />
CreateLinkEvent('user-settings/toggle') } />
CreateLinkEvent('groupforum/toggle') } title={ LocalizeText('toolbar.icon.label.forums') } /> { children } diff --git a/src/css/icons/icons.css b/src/css/icons/icons.css index 62120ea..54cced7 100644 --- a/src/css/icons/icons.css +++ b/src/css/icons/icons.css @@ -4,10 +4,6 @@ background-position: center; background-repeat: no-repeat; outline: 0; - image-rendering: -webkit-optimize-contrast !important; - image-rendering: -moz-crisp-edges !important; - image-rendering: crisp-edges !important; - image-rendering: pixelated !important; } .nitro-icon:hover { @@ -147,6 +143,15 @@ height: 30px; } +.nitro-icon.icon-me-badge-creator { + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: center; + background-size: 30px 30px; + width: 32px; + height: 32px; +} + .nitro-icon.icon-me-settings { background-image: url("@/assets/images/toolbar/icons/me-menu/cog.png"); width: 28px; From 99aceefb9e1276464746cc204b0b9a3b67d95927 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sun, 3 May 2026 16:05:23 +0200 Subject: [PATCH 12/20] =?UTF-8?q?=F0=9F=86=99=20Updates=20thanks=20to=20Li?= =?UTF-8?q?fe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit react-bootstrap → migrate to shadcn/ui + Tailwind (or HeroUI) Legacy, ~250KB bundle, dated API, inconsistent with CMS stack react-transition-group → use framer-motion (already installed!) De-facto deprecated, duplicate animation lib --- package.json | 2 - .../transitions/TransitionAnimation.tsx | 50 ++---- .../transitions/TransitionAnimationStyles.ts | 170 ++++++------------ src/components/purse/views/CurrencyView.tsx | 20 +-- 4 files changed, 76 insertions(+), 166 deletions(-) diff --git a/package.json b/package.json index f43f8b0..b504e77 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,11 @@ "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@tanstack/react-virtual": "3.13.24", - "@types/react-transition-group": "^4.4.12", "dompurify": "^3.4.1", "emoji-mart": "^5.6.0", "emoji-toolkit": "10.0.0", "framer-motion": "^12.38.0", "react": "^19.2.5", - "react-bootstrap": "^2.10.10", "react-dom": "^19.2.5", "react-icons": "^5.5.0", "react-slider": "^2.0.6", diff --git a/src/common/transitions/TransitionAnimation.tsx b/src/common/transitions/TransitionAnimation.tsx index 8e34849..db90938 100644 --- a/src/common/transitions/TransitionAnimation.tsx +++ b/src/common/transitions/TransitionAnimation.tsx @@ -1,6 +1,6 @@ -import { FC, ReactNode, useEffect, useState } from 'react'; -import { Transition } from 'react-transition-group'; -import { getTransitionAnimationStyle } from './TransitionAnimationStyles'; +import { AnimatePresence, motion, Variants } from 'framer-motion'; +import { FC, ReactNode } from 'react'; +import { getTransitionVariants } from './TransitionAnimationStyles'; interface TransitionAnimationProps { @@ -15,38 +15,22 @@ export const TransitionAnimation: FC = props => { const { type = null, inProp = false, timeout = 300, className = null, children = null } = props; - const [ isChildrenVisible, setChildrenVisible ] = useState(false); - - useEffect(() => - { - let timeoutData: ReturnType = null; - - if(inProp) - { - setChildrenVisible(true); - } - else - { - timeoutData = setTimeout(() => - { - setChildrenVisible(false); - clearTimeout(timeout); - }, timeout); - } - - return () => - { - if(timeoutData) clearTimeout(timeoutData); - }; - }, [ inProp, timeout ]); + const variants: Variants = getTransitionVariants(type); + const duration = timeout / 1000; return ( - - { state => ( -
- { isChildrenVisible && children } -
+ + { inProp && ( + + { children } + ) } -
+ ); }; diff --git a/src/common/transitions/TransitionAnimationStyles.ts b/src/common/transitions/TransitionAnimationStyles.ts index feebdcc..ab7315e 100644 --- a/src/common/transitions/TransitionAnimationStyles.ts +++ b/src/common/transitions/TransitionAnimationStyles.ts @@ -1,136 +1,66 @@ -import { CSSProperties } from 'react'; -import { TransitionStatus } from 'react-transition-group'; -import { ENTERING, EXITING } from 'react-transition-group/Transition'; +import { Variants } from 'framer-motion'; import { TransitionAnimationTypes } from './TransitionAnimationTypes'; -export function getTransitionAnimationStyle(type: string, transition: TransitionStatus, timeout: number = 300): Partial +export function getTransitionVariants(type: string): Variants { switch(type) { case TransitionAnimationTypes.BOUNCE: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'bounceIn', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'bounceOut', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, scale: 0.3 }, + visible: { opacity: 1, scale: 1, transition: { type: 'spring', stiffness: 260, damping: 12 } }, + exit: { opacity: 0, scale: 0.3 } + }; case TransitionAnimationTypes.SLIDE_LEFT: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'slideInLeft', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'slideOutLeft', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, x: '-100%' }, + visible: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: '-100%' } + }; case TransitionAnimationTypes.SLIDE_RIGHT: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'slideInRight', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'slideOutRight', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, x: '100%' }, + visible: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: '100%' } + }; case TransitionAnimationTypes.FLIP_X: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'flipInX', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'flipOutX', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, rotateX: 90 }, + visible: { opacity: 1, rotateX: 0 }, + exit: { opacity: 0, rotateX: 90 } + }; case TransitionAnimationTypes.FADE_UP: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'fadeInUp', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'fadeOutDown', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: 20 } + }; case TransitionAnimationTypes.FADE_IN: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'fadeIn', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'fadeOut', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + exit: { opacity: 0 } + }; case TransitionAnimationTypes.FADE_DOWN: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'fadeInDown', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'fadeOutUp', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, y: -20 }, + visible: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 } + }; case TransitionAnimationTypes.HEAD_SHAKE: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'headShake', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { x: 0 }, + visible: { + x: [ 0, -6, 5, -3, 2, 0 ], + transition: { duration: 0.5 } + }, + exit: { x: 0 } + }; } - return null; + return { + hidden: {}, + visible: {}, + exit: {} + }; } diff --git a/src/components/purse/views/CurrencyView.tsx b/src/components/purse/views/CurrencyView.tsx index 9f72158..47c07ab 100644 --- a/src/components/purse/views/CurrencyView.tsx +++ b/src/components/purse/views/CurrencyView.tsx @@ -1,5 +1,4 @@ import { FC, useMemo } from 'react'; -import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import { LocalizeFormattedNumber, LocalizeShortNumber } from '../../../api'; import { Flex, LayoutCurrencyIcon, Text } from '../../../common'; @@ -17,23 +16,22 @@ export const CurrencyView: FC = props => const element = useMemo(() => { return ( - + { short ? LocalizeShortNumber(amount) : LocalizeFormattedNumber(amount) } ); }, [ amount, short, type ]); if(!short) return element; - + return ( - - { LocalizeFormattedNumber(amount) } - - }> +
{ element } - +
+ { LocalizeFormattedNumber(amount) } +
+
); } From 92e9bb19cd9dd814062f8ecf4b49a7b97122e2fd Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sun, 3 May 2026 17:54:10 +0200 Subject: [PATCH 13/20] =?UTF-8?q?=F0=9F=86=99=20Updates=20thanks=20to=20Li?= =?UTF-8?q?fe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit react-slider → @radix-ui/react-slider react-tiny-popover → @radix-ui/react-popover react-youtube → react-player (more formats, better maintained) --- package.json | 7 +- src/common/Slider.tsx | 141 ++++++++++++++++-- .../NavigatorSearchResultItemInfoView.tsx | 58 ++++--- .../chat-input/ChatInputEmojiSelectorView.tsx | 25 ++-- .../chat-input/ChatInputStyleSelectorView.tsx | 43 +++--- .../furniture/FurnitureYoutubeDisplayView.tsx | 91 ++++------- src/components/toolbar/YouTubePlayerView.tsx | 26 ++-- 7 files changed, 229 insertions(+), 162 deletions(-) diff --git a/package.json b/package.json index b504e77..6619dea 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@babel/runtime": "^7.29.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-slider": "^1.2.4", "@tanstack/react-virtual": "3.13.24", "dompurify": "^3.4.1", "emoji-mart": "^5.6.0", @@ -21,9 +23,7 @@ "react": "^19.2.5", "react-dom": "^19.2.5", "react-icons": "^5.5.0", - "react-slider": "^2.0.6", - "react-tiny-popover": "^8.1.6", - "react-youtube": "^10.1.0", + "react-player": "^2.16.0", "use-between": "^1.4.0" }, "devDependencies": { @@ -32,7 +32,6 @@ "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@types/react-slider": "^1.3.6", "@typescript-eslint/eslint-plugin": "^8.59.1", "@typescript-eslint/parser": "^8.59.1", "@vitejs/plugin-react": "^6.0.1", diff --git a/src/common/Slider.tsx b/src/common/Slider.tsx index 72fb74c..8cd7490 100644 --- a/src/common/Slider.tsx +++ b/src/common/Slider.tsx @@ -1,35 +1,148 @@ -import { FC } from 'react'; -import ReactSlider, { ReactSliderProps } from 'react-slider'; +import * as RadixSlider from '@radix-ui/react-slider'; +import { CSSProperties, FC, HTMLProps, ReactElement } from 'react'; +import { FaAngleLeft, FaAngleRight } from 'react-icons/fa'; import { Button } from './Button'; import { Flex } from './Flex'; -import { FaAngleLeft, FaAngleRight } from 'react-icons/fa'; -export interface SliderProps extends ReactSliderProps +export interface SliderThumbState { - disabledButton?: boolean; + index: number; + value: number | number[]; + valueNow: number; } +export interface SliderProps +{ + min?: number; + max?: number; + step?: number; + value?: number | number[]; + defaultValue?: number | number[]; + onChange?: (value: any, thumbIndex: number) => void; + disabled?: boolean; + disabledButton?: boolean; + invert?: boolean; + className?: string; + style?: CSSProperties; + trackClassName?: string; + thumbClassName?: string; + renderThumb?: (props: HTMLProps, state: SliderThumbState) => ReactElement; +} + +const toArray = (value: number | number[] | undefined): number[] => +{ + if(Array.isArray(value)) return value; + if(typeof value === 'number') return [ value ]; + + return [ 0 ]; +}; + +const cn = (...parts: (string | undefined | false)[]) => parts.filter(Boolean).join(' '); + export const Slider: FC = props => { - const { disabledButton, max, min, step, value, onChange, ...rest } = props; - const currentValue = Array.isArray(value) ? value[0] : ((typeof value === 'number') ? value : 0); + const { + disabledButton, + disabled, + max = 100, + min = 0, + step = 1, + value, + defaultValue, + onChange, + invert, + className, + style, + trackClassName, + thumbClassName, + renderThumb + } = props; + + const valueArr = toArray(value); + const currentValue = valueArr[0] ?? 0; const minimum = (typeof min === 'number') ? min : 0; const maximum = (typeof max === 'number') ? max : 0; const buttonStep = ((typeof step === 'number') && (step > 0)) ? step : 1; + const isRange = valueArr.length > 1; const roundToStep = (nextValue: number) => { - if(typeof buttonStep !== 'number') return nextValue; - const decimalStep = buttonStep.toString(); const precision = decimalStep.includes('.') ? (decimalStep.length - decimalStep.indexOf('.') - 1) : 0; return parseFloat(nextValue.toFixed(precision)); }; - return - { !disabledButton && } - - { !disabledButton && } - ; + const emit = (next: number[]) => + { + if(!onChange) return; + + if(isRange) onChange(next, 0); + else onChange(next[0], 0); + }; + + const stepDown = () => + { + const next = roundToStep(minimum < currentValue ? currentValue - buttonStep : minimum); + + emit([ next, ...valueArr.slice(1) ]); + }; + + const stepUp = () => + { + const next = roundToStep(maximum > currentValue ? currentValue + buttonStep : maximum); + + emit([ next, ...valueArr.slice(1) ]); + }; + + const renderThumbElement = (i: number) => + { + const baseProps: HTMLProps = { + key: i, + className: cn('thumb', `thumb-${ i }`, thumbClassName) + }; + + const state: SliderThumbState = { + index: i, + value: isRange ? valueArr : currentValue, + valueNow: valueArr[i] ?? 0 + }; + + return ( + + { renderThumb ? renderThumb(baseProps, state) :
} + + ); + }; + + return ( + + { !disabledButton && ( + + ) } + + + + + { valueArr.map((_, i) => renderThumbElement(i)) } + + { !disabledButton && ( + + ) } + + ); } diff --git a/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx b/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx index 645e5c9..40cc451 100644 --- a/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx +++ b/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx @@ -1,7 +1,7 @@ import { RoomDataParser, RoomSettingsComposer, UpdateHomeRoomMessageComposer } from '@nitrots/nitro-renderer'; +import * as Popover from '@radix-ui/react-popover'; import React, { FC, useRef, useState } from 'react'; import { FaUser } from 'react-icons/fa'; -import { ArrowContainer, Popover } from 'react-tiny-popover'; import { GetGroupInformation, GetSessionDataManager, GetUserProfile, LocalizeText, ReportType, SendMessageComposer, ToggleFavoriteRoom } from '../../../../api'; import { Column, Flex, LayoutBadgeImageView, LayoutRoomThumbnailView, NitroCardContentView, Text, UserProfileIconView } from '../../../../common'; import { useHelp, useNavigator } from '../../../../hooks'; @@ -26,6 +26,12 @@ export const NavigatorSearchResultItemInfoView: FC + { + if(!isControlled) setInternalVisible(open); + if(!open && setIsPopoverActive) setIsPopoverActive(false); + }; + const getUserCounterColor = () => { const num: number = (100 * (roomData.userCount / roomData.maxUserCount)); @@ -88,17 +94,22 @@ export const NavigatorSearchResultItemInfoView: FC ( - + + +
{ if(!isControlled) setInternalVisible(true); } } + onMouseLeave={ () => { if(!isControlled) setInternalVisible(false); } } + /> + + + e.stopPropagation() }> @@ -173,24 +184,9 @@ export const NavigatorSearchResultItemInfoView: FC } - - ) } - isOpen={ popoverOpen } - onClickOutside={ () => - { - if(!isControlled) setInternalVisible(false); - if(setIsPopoverActive) setIsPopoverActive(false); - } } - padding={ 10 } - positions={ [ 'right', 'left', 'top', 'bottom' ] } - > -
{ if(!isControlled) setInternalVisible(true); } } - onMouseLeave={ () => { if(!isControlled) setInternalVisible(false); } } - /> - + + + + ); }; diff --git a/src/components/room/widgets/chat-input/ChatInputEmojiSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputEmojiSelectorView.tsx index f547e46..bd2dcae 100644 --- a/src/components/room/widgets/chat-input/ChatInputEmojiSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputEmojiSelectorView.tsx @@ -1,7 +1,7 @@ import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; +import * as Popover from '@radix-ui/react-popover'; import { FC, useState } from 'react'; -import { Popover } from 'react-tiny-popover'; interface ChatInputEmojiSelectorViewProps { @@ -19,19 +19,16 @@ export const ChatInputEmojiSelectorView: FC = p setSelectorVisible(false); }; - const toggleSelector = () => setSelectorVisible(prev => !prev); - return ( -
- } - isOpen={ selectorVisible } - positions={ [ 'top' ] } - onClickOutside={ () => setSelectorVisible(false) } - > -
🙂
-
-
+ + +
🙂
+
+ + + + + +
); }; diff --git a/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx index 2a6d165..ec089c3 100644 --- a/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx @@ -1,5 +1,5 @@ +import * as Popover from '@radix-ui/react-popover'; import { FC, useState } from 'react'; -import { ArrowContainer, Popover } from 'react-tiny-popover'; import { Flex, Grid, NitroCardContentView } from '../../../../common'; interface ChatInputStyleSelectorViewProps @@ -21,20 +21,17 @@ export const ChatInputStyleSelectorView: FC = p }; return ( - ( - + +
+
+
+ + + @@ -47,15 +44,9 @@ export const ChatInputStyleSelectorView: FC = p ))} - - )} - > -
setSelectorVisible(v => !v)} - > -
-
- + + + + ); -}; \ No newline at end of file +}; diff --git a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx index f2375ca..82a4b8c 100644 --- a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx +++ b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx @@ -1,6 +1,5 @@ -import { FC, useEffect, useState } from 'react'; -import YouTube, { Options } from 'react-youtube'; -import { YouTubePlayer } from 'youtube-player/dist/types'; +import { FC, useRef } from 'react'; +import ReactPlayer from 'react-player/youtube'; import { LocalizeText, YoutubeVideoPlaybackStateEnum } from '../../../../api'; import { AutoGrid, AutoGridProps, LayoutGridItem, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useFurnitureYoutubeWidget } from '../../../../hooks'; @@ -12,71 +11,24 @@ interface FurnitureYoutubeDisplayViewProps extends AutoGridProps export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewProps => { - const [ player, setPlayer ] = useState(null); const { objectId = -1, videoId = null, videoStart = 0, videoEnd = 0, currentVideoState = null, selectedVideo = null, playlists = [], onClose = null, previous = null, next = null, pause = null, play = null, selectVideo = null } = useFurnitureYoutubeWidget(); + const playerRef = useRef(null); - const onStateChange = (event: { target: YouTubePlayer; data: number }) => + const handlePlay = () => { - try - { - setPlayer(event.target); - - if(objectId === -1) return; - - switch(event.target.getPlayerState()) - { - case -1: - case 1: - if(currentVideoState !== 1) play(); - return; - case 2: - if(currentVideoState !== 2) pause(); - } - } - catch(err) {} + if(objectId === -1) return; + if(currentVideoState !== YoutubeVideoPlaybackStateEnum.PLAYING) play(); }; - useEffect(() => + const handlePause = () => { - if((currentVideoState === null) || !player) return; - - try - { - if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING)) - { - player.playVideo(); - - return; - } - - if((currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED)) - { - player.pauseVideo(); - - return; - } - } - catch(err) - { - setPlayer(null); - } - }, [ currentVideoState, player ]); + if(objectId === -1) return; + if(currentVideoState !== YoutubeVideoPlaybackStateEnum.PAUSED) pause(); + }; if(objectId === -1) return null; - const youtubeOptions: Options = { - height: '375', - width: '500', - playerVars: { - autoplay: 1, - disablekb: 1, - controls: 0, - origin: window.origin, - modestbranding: 1, - start: videoStart, - end: videoEnd - } - }; + const playing = (currentVideoState === null) ? true : (currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING); return ( @@ -85,7 +37,26 @@ export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewPr
{ (videoId && videoId.length > 0) && - setPlayer(event.target) } onStateChange={ onStateChange } /> + } { (!videoId || videoId.length === 0) &&
{ LocalizeText('widget.furni.video_viewer.no_videos') }
diff --git a/src/components/toolbar/YouTubePlayerView.tsx b/src/components/toolbar/YouTubePlayerView.tsx index 39e8e76..5f1c89c 100644 --- a/src/components/toolbar/YouTubePlayerView.tsx +++ b/src/components/toolbar/YouTubePlayerView.tsx @@ -1,6 +1,6 @@ import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomSettingsEvent, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from "@nitrots/nitro-renderer"; import { FC, useEffect, useRef, useState } from "react"; -import YouTube from "react-youtube"; +import ReactPlayer from "react-player/youtube"; import { GetRoomSession, getYoutubeRoomEnabled, GetSessionDataManager, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from "../../api"; import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from "../../common"; import { useFurnitureYoutubeWidget, useMessageEvent } from "../../hooks"; @@ -35,7 +35,7 @@ export const YouTubePlayerView: FC<{}> = () => { const [playlist, setPlaylist] = useState([]); const [history, setHistory] = useState([]); const [showVolumeSlider, setShowVolumeSlider] = useState(true); - const playerRef = useRef(null); + const playerRef = useRef(null); const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget(); const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]); const [broadcastVideo, setBroadcastVideo] = useState(""); @@ -310,22 +310,22 @@ export const YouTubePlayerView: FC<{}> = () => { )} {videoId ? ( - { playerRef.current = ref; }} + url={`https://www.youtube.com/watch?v=${videoId}`} + width="100%" + height={isFullscreen ? "100%" : 280} + playing + muted={isMuted} + loop={isLooping} + volume={Math.max(0, Math.min(1, volume / 100))} + config={{ playerVars: { autoplay: 1, - volume: volume, - muted: isMuted ? 1 : 0, loop: isLooping ? 1 : 0, }, }} - onReady={(e) => { - playerRef.current = e.target; - addToHistory(videoId); - }} + onReady={() => addToHistory(videoId)} /> ) : (
From d9b6a3eb0c3d2f91b692eb2551a0c9e3f9bb79df Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 3 May 2026 19:53:57 +0200 Subject: [PATCH 14/20] feat(infostand): gate Edit Furni button behind moderator permission Mirrors the isModerator check already used by the toolbar furni-editor icon, so users without the moderator rank no longer see the button. --- .../infostand/InfoStandWidgetFurniView.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index d415a1b..f396c28 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -1,4 +1,4 @@ -import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer'; +import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSessionDataManager, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FaCrosshairs, FaRulerVertical, FaTimes } from 'react-icons/fa'; import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr'; @@ -585,19 +585,20 @@ export const InfoStandWidgetFurniView: FC = props onClick={ () => setDropdownOpen(!dropdownOpen) }> { dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` } - + if(typeId) window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: typeId } })); + } }> + Edit Furni + } { dropdownOpen &&
{ /* Left panel: position + rotation */ } From 0f6bf7e9eb71a8f72c0cb70a81fdbf9a784d5c65 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 3 May 2026 20:40:30 +0200 Subject: [PATCH 15/20] fix(toolbar): anchor Me menu popup to face button instead of fixed offset Removed `absolute bottom-[60px] left-[33px]` from the inner Flex of ToolbarMeView. The outer wrapper in ToolbarView already anchors the popup above the face button (bottom-[calc(100%+8px)] left-1/2 -translate-x-1/2), so the inner pixel-perfect override was detaching it and making it float mid-screen. --- src/components/toolbar/ToolbarMeView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/toolbar/ToolbarMeView.tsx b/src/components/toolbar/ToolbarMeView.tsx index 741b66a..79fa576 100644 --- a/src/components/toolbar/ToolbarMeView.tsx +++ b/src/components/toolbar/ToolbarMeView.tsx @@ -32,7 +32,7 @@ export const ToolbarMeView: FC + { (GetConfigurationValue('guides.enabled') && useGuideTool) &&
DispatchUiEvent(new GuideToolEvent(GuideToolEvent.TOGGLE_GUIDE_TOOL)) } /> }
CreateLinkEvent('achievements/toggle') }> From 017e780e7451b4aa60452a63f9cf47a338a37326 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 3 May 2026 20:49:15 +0200 Subject: [PATCH 16/20] fix(toolbar): align face button with other toolbar icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The face avatar (headOnly LayoutAvatarImageView) sits in a 63px-tall box (44px on mobile) while sibling toolbar icons are smaller, so its head sprite rendered visually higher than the other icons. Bumped marginTop from 2px → 12px (desktop) and 4px → 9px (mobile) so the head sits on the same horizontal axis as the rest of the toolbar. --- src/components/toolbar/ToolbarView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 38758cd..c919d7f 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -184,7 +184,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } { setMeExpanded(value => !value); event.stopPropagation(); } }> - + { (getTotalUnseen > 0) && } @@ -279,7 +279,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } { setMeExpanded(value => !value); event.stopPropagation(); } }> - + { (getTotalUnseen > 0) && } From 72bc4da3c0aad95412cde78d7f05ee43487f37f9 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 3 May 2026 22:00:33 +0200 Subject: [PATCH 17/20] feat(profile): add full-box card background tab and rendering Adds a "Cards" tab to the Profile Background picker (BackgroundsView) that selects a pattern applied to the entire user info card and the extended profile container, in addition to the existing avatar-pad background/stand/overlay layers. - AvatarInfoUser/Utilities: propagate cardBackgroundId from RoomUserData. - InfoStandWidgetUserView: stateful cardBackgroundId, applied as .profile-card-background.card-background-{id} on the outer Column with bg-color suppressed when active. - UserContainerView: same class on the wrapper of the extended profile. - BackgroundsView: 4th tab "cards" backed by cards.data config (falls back to backgrounds.data); sends 4-id message via the extended sendBackgroundMessage signature. - ui-config.example: cards.data dataset (15 entries). - BackgroundsView.css: 188 .card-background-{N} rules cloned from background-{N} (repeat-tiled) plus 15 CSS-pattern overrides for the provisional dataset (gradients, stripes, dots, grid, checker). --- public/ui-config.example | 17 + src/api/room/widgets/AvatarInfoUser.ts | 1 + src/api/room/widgets/AvatarInfoUtilities.ts | 1 + .../backgrounds/BackgroundsView.tsx | 28 +- .../infostand/InfoStandWidgetUserView.tsx | 9 +- .../user-profile/UserContainerView.tsx | 3 +- src/css/backgrounds/BackgroundsView.css | 650 ++++++++++++++++++ 7 files changed, 697 insertions(+), 12 deletions(-) diff --git a/public/ui-config.example b/public/ui-config.example index 946e5e0..996976c 100644 --- a/public/ui-config.example +++ b/public/ui-config.example @@ -1346,6 +1346,23 @@ "isAmbassadorOnly": false } ], + "cards.data": [ + { "backgroundId": 1, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 2, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 3, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 4, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 5, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 6, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 7, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 8, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 9, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 10, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 11, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 12, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 13, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 14, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 15, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false } + ], "stands.data": [ { "standId": 0, diff --git a/src/api/room/widgets/AvatarInfoUser.ts b/src/api/room/widgets/AvatarInfoUser.ts index fa3fc1a..ad728f3 100644 --- a/src/api/room/widgets/AvatarInfoUser.ts +++ b/src/api/room/widgets/AvatarInfoUser.ts @@ -16,6 +16,7 @@ export class AvatarInfoUser implements IAvatarInfo public backgroundId: number = 0; public standId: number = 0; public overlayId: number = 0; + public cardBackgroundId: number = 0; public webID: number = 0; public xp: number = 0; public userType: number = -1; diff --git a/src/api/room/widgets/AvatarInfoUtilities.ts b/src/api/room/widgets/AvatarInfoUtilities.ts index 0def77a..f9b29ac 100644 --- a/src/api/room/widgets/AvatarInfoUtilities.ts +++ b/src/api/room/widgets/AvatarInfoUtilities.ts @@ -186,6 +186,7 @@ export class AvatarInfoUtilities userInfo.backgroundId = userData.background; userInfo.standId = userData.stand; userInfo.overlayId = userData.overlay; + userInfo.cardBackgroundId = userData.cardBackground ?? 0; userInfo.achievementScore = userData.activityPoints; userInfo.webID = userData.webID; userInfo.roomIndex = userData.roomIndex; diff --git a/src/components/backgrounds/BackgroundsView.tsx b/src/components/backgrounds/BackgroundsView.tsx index 9782dd6..ecd08df 100644 --- a/src/components/backgrounds/BackgroundsView.tsx +++ b/src/components/backgrounds/BackgroundsView.tsx @@ -20,9 +20,11 @@ interface BackgroundsViewProps { setSelectedStand: Dispatch>; selectedOverlay: number; setSelectedOverlay: Dispatch>; + selectedCardBackground: number; + setSelectedCardBackground: Dispatch>; } -const TABS = ['backgrounds', 'stands', 'overlays'] as const; +const TABS = ['backgrounds', 'stands', 'overlays', 'cards'] as const; type TabType = typeof TABS[number]; export const BackgroundsView: FC = ({ @@ -32,7 +34,9 @@ export const BackgroundsView: FC = ({ selectedStand, setSelectedStand, selectedOverlay, - setSelectedOverlay + setSelectedOverlay, + selectedCardBackground, + setSelectedCardBackground }) => { const [activeTab, setActiveTab] = useState('backgrounds'); const { roomSession } = useRoom(); @@ -58,20 +62,21 @@ export const BackgroundsView: FC = ({ const allData = useMemo(() => ({ backgrounds: processData(GetConfigurationValue('backgrounds.data'), 'background'), stands: processData(GetConfigurationValue('stands.data'), 'stand'), - overlays: processData(GetConfigurationValue('overlays.data'), 'overlay') + overlays: processData(GetConfigurationValue('overlays.data'), 'overlay'), + cards: processData(GetConfigurationValue('cards.data') || GetConfigurationValue('backgrounds.data'), 'background') }), [processData]); const handleSelection = useCallback((id: number) => { if (!roomSession) return; - const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay }; - - const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay }; + const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay, cards: setSelectedCardBackground }; + + const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay, cards: selectedCardBackground }; setters[activeTab](id); const newValues = { ...currentValues, [activeTab]: id }; - roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays ); - }, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, setSelectedBackground, setSelectedStand, setSelectedOverlay]); + roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays, newValues.cards ); + }, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, selectedCardBackground, setSelectedBackground, setSelectedStand, setSelectedOverlay, setSelectedCardBackground]); const renderItem = useCallback((item: ItemData, type: string) => ( = ({ onClick={() => item.selectable && handleSelection(item.id)} className={item.selectable ? '' : 'non-selectable'} > - + {item.isHcOnly && } ), [handleSelection]); @@ -103,7 +111,7 @@ export const BackgroundsView: FC = ({ Select an Option - {allData[activeTab].map(item => renderItem(item, activeTab.slice(0, -1)))} + {allData[activeTab].map(item => renderItem(item, activeTab === 'cards' ? 'card-background' : activeTab.slice(0, -1)))} diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 91ced43..2e0797b 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -24,12 +24,14 @@ export const InfoStandWidgetUserView: FC = props = const [backgroundId, setBackgroundId] = useState(null); const [standId, setStandId] = useState(null); const [overlayId, setOverlayId] = useState(null); + const [cardBackgroundId, setCardBackgroundId] = useState(null); const [isVisible, setIsVisible] = useState(false); const { roomSession = null } = useRoom(); const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`; const infostandStandClass = `stand-${standId ?? 'default'}`; const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`; + const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : ''; const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]); @@ -91,6 +93,7 @@ export const InfoStandWidgetUserView: FC = props = newValue.backgroundId = event.backgroundId; newValue.standId = event.standId; newValue.overlayId = event.overlayId; + newValue.cardBackgroundId = event.cardBackgroundId ?? 0; return newValue; }); }); @@ -125,6 +128,7 @@ export const InfoStandWidgetUserView: FC = props = setBackgroundId(avatarInfo.backgroundId); setStandId(avatarInfo.standId); setOverlayId(avatarInfo.overlayId); + setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0); SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID)); @@ -135,6 +139,7 @@ export const InfoStandWidgetUserView: FC = props = setBackgroundId(null); setStandId(null); setOverlayId(null); + setCardBackgroundId(null); }; }, [avatarInfo]); @@ -142,7 +147,7 @@ export const InfoStandWidgetUserView: FC = props = return ( <> - +
@@ -277,6 +282,8 @@ export const InfoStandWidgetUserView: FC = props = setSelectedStand={setStandId} selectedOverlay={overlayId} setSelectedOverlay={setOverlayId} + selectedCardBackground={cardBackgroundId} + setSelectedCardBackground={setCardBackgroundId} />
)} diff --git a/src/components/user-profile/UserContainerView.tsx b/src/components/user-profile/UserContainerView.tsx index 88d3150..1262bd4 100644 --- a/src/components/user-profile/UserContainerView.tsx +++ b/src/components/user-profile/UserContainerView.tsx @@ -18,6 +18,7 @@ export const UserContainerView: FC<{ const infostandBackgroundClass = `background-${userProfile.backgroundId ?? 'default'}`; const infostandStandClass = `stand-${userProfile.standId ?? 'default'}`; const infostandOverlayClass = `overlay-${userProfile.overlayId ?? 'default'}`; + const profileCardBgClass = userProfile.cardBackgroundId ? `card-background-${userProfile.cardBackgroundId}` : ''; const addFriend = () => { @@ -32,7 +33,7 @@ export const UserContainerView: FC<{ }, [ userProfile ]); return ( -
+
diff --git a/src/css/backgrounds/BackgroundsView.css b/src/css/backgrounds/BackgroundsView.css index 5b67a48..1aa6114 100644 --- a/src/css/backgrounds/BackgroundsView.css +++ b/src/css/backgrounds/BackgroundsView.css @@ -78,6 +78,656 @@ background: none; } +.profile-card-background { + background-repeat: repeat; + background-position: top left; + background-size: auto; + &.card-background-0 { + background-image: url('@/assets/images/backgrounds/background/bg_0.png'); + } + &.card-background-1 { + background-image: url('@/assets/images/backgrounds/background/bg_1.png'); + } + &.card-background-2 { + background-image: url('@/assets/images/backgrounds/background/bg_2.png'); + } + &.card-background-3 { + background-image: url('@/assets/images/backgrounds/background/bg_3.png'); + } + &.card-background-4 { + background-image: url('@/assets/images/backgrounds/background/bg_4.png'); + } + &.card-background-5 { + background-image: url('@/assets/images/backgrounds/background/bg_5.png'); + } + &.card-background-6 { + background-image: url('@/assets/images/backgrounds/background/bg_6.png'); + } + &.card-background-7 { + background-image: url('@/assets/images/backgrounds/background/bg_7.png'); + } + &.card-background-8 { + background-image: url('@/assets/images/backgrounds/background/bg_8.png'); + } + &.card-background-9 { + background-image: url('@/assets/images/backgrounds/background/bg_9.png'); + } + &.card-background-10 { + background-image: url('@/assets/images/backgrounds/background/bg_10.png'); + } + &.card-background-11 { + background-image: url('@/assets/images/backgrounds/background/bg_11.png'); + } + &.card-background-12 { + background-image: url('@/assets/images/backgrounds/background/bg_12.png'); + } + &.card-background-13 { + background-image: url('@/assets/images/backgrounds/background/bg_13.png'); + } + &.card-background-14 { + background-image: url('@/assets/images/backgrounds/background/bg_14.png'); + } + &.card-background-15 { + background-image: url('@/assets/images/backgrounds/background/bg_15.png'); + } + &.card-background-16 { + background-image: url('@/assets/images/backgrounds/background/bg_16.png'); + } + &.card-background-17 { + background-image: url('@/assets/images/backgrounds/background/bg_17.png'); + } + &.card-background-18 { + background-image: url('@/assets/images/backgrounds/background/bg_18.png'); + } + &.card-background-19 { + background-image: url('@/assets/images/backgrounds/background/bg_19.png'); + } + &.card-background-20 { + background-image: url('@/assets/images/backgrounds/background/bg_20.png'); + } + &.card-background-21 { + background-image: url('@/assets/images/backgrounds/background/bg_21.png'); + } + &.card-background-22 { + background-image: url('@/assets/images/backgrounds/background/bg_22.png'); + } + &.card-background-23 { + background-image: url('@/assets/images/backgrounds/background/bg_23.png'); + } + &.card-background-24 { + background-image: url('@/assets/images/backgrounds/background/bg_24.png'); + } + &.card-background-25 { + background-image: url('@/assets/images/backgrounds/background/bg_25.png'); + } + &.card-background-26 { + background-image: url('@/assets/images/backgrounds/background/bg_26.png'); + } + &.card-background-27 { + background-image: url('@/assets/images/backgrounds/background/bg_27.png'); + } + &.card-background-28 { + background-image: url('@/assets/images/backgrounds/background/bg_28.png'); + } + &.card-background-29 { + background-image: url('@/assets/images/backgrounds/background/bg_29.png'); + } + &.card-background-30 { + background-image: url('@/assets/images/backgrounds/background/bg_30.png'); + } + &.card-background-31 { + background-image: url('@/assets/images/backgrounds/background/bg_31.png'); + } + &.card-background-32 { + background-image: url('@/assets/images/backgrounds/background/bg_32.png'); + } + &.card-background-33 { + background-image: url('@/assets/images/backgrounds/background/bg_33.png'); + } + &.card-background-34 { + background-image: url('@/assets/images/backgrounds/background/bg_34.png'); + } + &.card-background-35 { + background-image: url('@/assets/images/backgrounds/background/bg_35.png'); + } + &.card-background-36 { + background-image: url('@/assets/images/backgrounds/background/bg_36.gif'); + } + &.card-background-37 { + background-image: url('@/assets/images/backgrounds/background/bg_37.png'); + } + &.card-background-38 { + background-image: url('@/assets/images/backgrounds/background/bg_38.png'); + } + &.card-background-39 { + background-image: url('@/assets/images/backgrounds/background/bg_39.png'); + } + &.card-background-40 { + background-image: url('@/assets/images/backgrounds/background/bg_40.png'); + } + &.card-background-41 { + background-image: url('@/assets/images/backgrounds/background/bg_41.png'); + } + &.card-background-42 { + background-image: url('@/assets/images/backgrounds/background/bg_42.png'); + } + &.card-background-43 { + background-image: url('@/assets/images/backgrounds/background/bg_43.png'); + } + &.card-background-44 { + background-image: url('@/assets/images/backgrounds/background/bg_44.png'); + } + &.card-background-45 { + background-image: url('@/assets/images/backgrounds/background/bg_45.png'); + } + &.card-background-46 { + background-image: url('@/assets/images/backgrounds/background/bg_46.png'); + } + &.card-background-47 { + background-image: url('@/assets/images/backgrounds/background/bg_47.png'); + } + &.card-background-48 { + background-image: url('@/assets/images/backgrounds/background/bg_48.png'); + } + &.card-background-49 { + background-image: url('@/assets/images/backgrounds/background/bg_49.png'); + } + &.card-background-50 { + background-image: url('@/assets/images/backgrounds/background/bg_50.png'); + } + &.card-background-51 { + background-image: url('@/assets/images/backgrounds/background/bg_51.gif'); + } + &.card-background-52 { + background-image: url('@/assets/images/backgrounds/background/bg_52.gif'); + } + &.card-background-53 { + background-image: url('@/assets/images/backgrounds/background/bg_53.gif'); + } + &.card-background-54 { + background-image: url('@/assets/images/backgrounds/background/bg_54.gif'); + } + &.card-background-55 { + background-image: url('@/assets/images/backgrounds/background/bg_55.gif'); + } + &.card-background-56 { + background-image: url('@/assets/images/backgrounds/background/bg_56.gif'); + } + &.card-background-57 { + background-image: url('@/assets/images/backgrounds/background/bg_57.gif'); + } + &.card-background-58 { + background-image: url('@/assets/images/backgrounds/background/bg_58.gif'); + } + &.card-background-59 { + background-image: url('@/assets/images/backgrounds/background/bg_59.gif'); + } + &.card-background-60 { + background-image: url('@/assets/images/backgrounds/background/bg_60.gif'); + } + &.card-background-61 { + background-image: url('@/assets/images/backgrounds/background/bg_61.gif'); + } + &.card-background-62 { + background-image: url('@/assets/images/backgrounds/background/bg_62.gif'); + } + &.card-background-63 { + background-image: url('@/assets/images/backgrounds/background/bg_63.gif'); + } + &.card-background-64 { + background-image: url('@/assets/images/backgrounds/background/bg_64.gif'); + } + &.card-background-65 { + background-image: url('@/assets/images/backgrounds/background/bg_65.gif'); + } + &.card-background-66 { + background-image: url('@/assets/images/backgrounds/background/bg_66.gif'); + } + &.card-background-67 { + background-image: url('@/assets/images/backgrounds/background/bg_67.gif'); + } + &.card-background-68 { + background-image: url('@/assets/images/backgrounds/background/bg_68.gif'); + } + &.card-background-69 { + background-image: url('@/assets/images/backgrounds/background/bg_69.gif'); + } + &.card-background-70 { + background-image: url('@/assets/images/backgrounds/background/bg_70.gif'); + } + &.card-background-71 { + background-image: url('@/assets/images/backgrounds/background/bg_71.gif'); + } + &.card-background-72 { + background-image: url('@/assets/images/backgrounds/background/bg_72.gif'); + } + &.card-background-73 { + background-image: url('@/assets/images/backgrounds/background/bg_73.gif'); + } + &.card-background-74 { + background-image: url('@/assets/images/backgrounds/background/bg_74.gif'); + } + &.card-background-75 { + background-image: url('@/assets/images/backgrounds/background/bg_75.gif'); + } + &.card-background-76 { + background-image: url('@/assets/images/backgrounds/background/bg_76.gif'); + } + &.card-background-77 { + background-image: url('@/assets/images/backgrounds/background/bg_77.gif'); + } + &.card-background-78 { + background-image: url('@/assets/images/backgrounds/background/bg_78.gif'); + } + &.card-background-79 { + background-image: url('@/assets/images/backgrounds/background/bg_79.gif'); + } + &.card-background-80 { + background-image: url('@/assets/images/backgrounds/background/bg_80.gif'); + } + &.card-background-81 { + background-image: url('@/assets/images/backgrounds/background/bg_81.gif'); + } + &.card-background-82 { + background-image: url('@/assets/images/backgrounds/background/bg_82.gif'); + } + &.card-background-83 { + background-image: url('@/assets/images/backgrounds/background/bg_83.gif'); + } + &.card-background-84 { + background-image: url('@/assets/images/backgrounds/background/bg_84.gif'); + } + &.card-background-85 { + background-image: url('@/assets/images/backgrounds/background/bg_85.gif'); + } + &.card-background-86 { + background-image: url('@/assets/images/backgrounds/background/bg_86.png'); + } + &.card-background-87 { + background-image: url('@/assets/images/backgrounds/background/bg_87.gif'); + } + &.card-background-88 { + background-image: url('@/assets/images/backgrounds/background/bg_88.gif'); + } + &.card-background-89 { + background-image: url('@/assets/images/backgrounds/background/bg_89.gif'); + } + &.card-background-90 { + background-image: url('@/assets/images/backgrounds/background/bg_90.gif'); + } + &.card-background-91 { + background-image: url('@/assets/images/backgrounds/background/bg_91.gif'); + } + &.card-background-92 { + background-image: url('@/assets/images/backgrounds/background/bg_92.gif'); + } + &.card-background-93 { + background-image: url('@/assets/images/backgrounds/background/bg_93.gif'); + } + &.card-background-94 { + background-image: url('@/assets/images/backgrounds/background/bg_94.gif'); + } + &.card-background-95 { + background-image: url('@/assets/images/backgrounds/background/bg_95.gif'); + } + &.card-background-96 { + background-image: url('@/assets/images/backgrounds/background/bg_96.gif'); + } + &.card-background-97 { + background-image: url('@/assets/images/backgrounds/background/bg_97.gif'); + } + &.card-background-98 { + background-image: url('@/assets/images/backgrounds/background/bg_98.gif'); + } + &.card-background-99 { + background-image: url('@/assets/images/backgrounds/background/bg_99.gif'); + } + &.card-background-100 { + background-image: url('@/assets/images/backgrounds/background/bg_100.gif'); + } + &.card-background-101 { + background-image: url('@/assets/images/backgrounds/background/bg_101.png'); + } + &.card-background-102 { + background-image: url('@/assets/images/backgrounds/background/bg_102.gif'); + } + &.card-background-103 { + background-image: url('@/assets/images/backgrounds/background/bg_103.gif'); + } + &.card-background-104 { + background-image: url('@/assets/images/backgrounds/background/bg_104.gif'); + } + &.card-background-105 { + background-image: url('@/assets/images/backgrounds/background/bg_105.gif'); + } + &.card-background-106 { + background-image: url('@/assets/images/backgrounds/background/bg_106.gif'); + } + &.card-background-107 { + background-image: url('@/assets/images/backgrounds/background/bg_107.gif'); + } + &.card-background-108 { + background-image: url('@/assets/images/backgrounds/background/bg_108.gif'); + } + &.card-background-109 { + background-image: url('@/assets/images/backgrounds/background/bg_109.gif'); + } + &.card-background-110 { + background-image: url('@/assets/images/backgrounds/background/bg_110.gif'); + } + &.card-background-111 { + background-image: url('@/assets/images/backgrounds/background/bg_111.gif'); + } + &.card-background-112 { + background-image: url('@/assets/images/backgrounds/background/bg_112.gif'); + } + &.card-background-113 { + background-image: url('@/assets/images/backgrounds/background/bg_113.gif'); + } + &.card-background-114 { + background-image: url('@/assets/images/backgrounds/background/bg_114.gif'); + } + &.card-background-115 { + background-image: url('@/assets/images/backgrounds/background/bg_115.gif'); + } + &.card-background-116 { + background-image: url('@/assets/images/backgrounds/background/bg_116.gif'); + } + &.card-background-117 { + background-image: url('@/assets/images/backgrounds/background/bg_117.gif'); + } + &.card-background-118 { + background-image: url('@/assets/images/backgrounds/background/bg_118.gif'); + } + &.card-background-119 { + background-image: url('@/assets/images/backgrounds/background/bg_119.gif'); + } + &.card-background-120 { + background-image: url('@/assets/images/backgrounds/background/bg_120.gif'); + } + &.card-background-121 { + background-image: url('@/assets/images/backgrounds/background/bg_121.gif'); + } + &.card-background-122 { + background-image: url('@/assets/images/backgrounds/background/bg_122.gif'); + } + &.card-background-123 { + background-image: url('@/assets/images/backgrounds/background/bg_123.gif'); + } + &.card-background-124 { + background-image: url('@/assets/images/backgrounds/background/bg_124.gif'); + } + &.card-background-125 { + background-image: url('@/assets/images/backgrounds/background/bg_125.gif'); + } + &.card-background-126 { + background-image: url('@/assets/images/backgrounds/background/bg_126.gif'); + } + &.card-background-127 { + background-image: url('@/assets/images/backgrounds/background/bg_127.gif'); + } + &.card-background-128 { + background-image: url('@/assets/images/backgrounds/background/bg_128.gif'); + } + &.card-background-129 { + background-image: url('@/assets/images/backgrounds/background/bg_129.gif'); + } + &.card-background-130 { + background-image: url('@/assets/images/backgrounds/background/bg_130.gif'); + } + &.card-background-131 { + background-image: url('@/assets/images/backgrounds/background/bg_131.gif'); + } + &.card-background-132 { + background-image: url('@/assets/images/backgrounds/background/bg_132.gif'); + } + &.card-background-133 { + background-image: url('@/assets/images/backgrounds/background/bg_133.gif'); + } + &.card-background-134 { + background-image: url('@/assets/images/backgrounds/background/bg_134.gif'); + } + &.card-background-135 { + background-image: url('@/assets/images/backgrounds/background/bg_135.gif'); + } + &.card-background-136 { + background-image: url('@/assets/images/backgrounds/background/bg_136.gif'); + } + &.card-background-137 { + background-image: url('@/assets/images/backgrounds/background/bg_137.gif'); + } + &.card-background-138 { + background-image: url('@/assets/images/backgrounds/background/bg_138.gif'); + } + &.card-background-139 { + background-image: url('@/assets/images/backgrounds/background/bg_139.gif'); + } + &.card-background-140 { + background-image: url('@/assets/images/backgrounds/background/bg_140.gif'); + } + &.card-background-141 { + background-image: url('@/assets/images/backgrounds/background/bg_141.gif'); + } + &.card-background-142 { + background-image: url('@/assets/images/backgrounds/background/bg_142.gif'); + } + &.card-background-143 { + background-image: url('@/assets/images/backgrounds/background/bg_143.gif'); + } + &.card-background-144 { + background-image: url('@/assets/images/backgrounds/background/bg_144.gif'); + } + &.card-background-145 { + background-image: url('@/assets/images/backgrounds/background/bg_145.gif'); + } + &.card-background-146 { + background-image: url('@/assets/images/backgrounds/background/bg_146.gif'); + } + &.card-background-147 { + background-image: url('@/assets/images/backgrounds/background/bg_147.gif'); + } + &.card-background-148 { + background-image: url('@/assets/images/backgrounds/background/bg_148.gif'); + } + &.card-background-149 { + background-image: url('@/assets/images/backgrounds/background/bg_149.gif'); + } + &.card-background-150 { + background-image: url('@/assets/images/backgrounds/background/bg_150.gif'); + } + &.card-background-151 { + background-image: url('@/assets/images/backgrounds/background/bg_151.gif'); + } + &.card-background-152 { + background-image: url('@/assets/images/backgrounds/background/bg_152.gif'); + } + &.card-background-153 { + background-image: url('@/assets/images/backgrounds/background/bg_153.gif'); + } + &.card-background-154 { + background-image: url('@/assets/images/backgrounds/background/bg_154.gif'); + } + &.card-background-155 { + background-image: url('@/assets/images/backgrounds/background/bg_155.gif'); + } + &.card-background-156 { + background-image: url('@/assets/images/backgrounds/background/bg_156.gif'); + } + &.card-background-157 { + background-image: url('@/assets/images/backgrounds/background/bg_157.gif'); + } + &.card-background-158 { + background-image: url('@/assets/images/backgrounds/background/bg_158.gif'); + } + &.card-background-159 { + background-image: url('@/assets/images/backgrounds/background/bg_159.gif'); + } + &.card-background-160 { + background-image: url('@/assets/images/backgrounds/background/bg_160.gif'); + } + &.card-background-161 { + background-image: url('@/assets/images/backgrounds/background/bg_161.gif'); + } + &.card-background-162 { + background-image: url('@/assets/images/backgrounds/background/bg_162.gif'); + } + &.card-background-163 { + background-image: url('@/assets/images/backgrounds/background/bg_163.gif'); + } + &.card-background-164 { + background-image: url('@/assets/images/backgrounds/background/bg_164.gif'); + } + &.card-background-165 { + background-image: url('@/assets/images/backgrounds/background/bg_165.gif'); + } + &.card-background-166 { + background-image: url('@/assets/images/backgrounds/background/bg_166.gif'); + } + &.card-background-167 { + background-image: url('@/assets/images/backgrounds/background/bg_167.gif'); + } + &.card-background-168 { + background-image: url('@/assets/images/backgrounds/background/bg_168.gif'); + } + &.card-background-169 { + background-image: url('@/assets/images/backgrounds/background/bg_169.gif'); + } + &.card-background-170 { + background-image: url('@/assets/images/backgrounds/background/bg_170.png'); + } + &.card-background-171 { + background-image: url('@/assets/images/backgrounds/background/bg_171.png'); + } + &.card-background-172 { + background-image: url('@/assets/images/backgrounds/background/bg_172.png'); + } + &.card-background-173 { + background-image: url('@/assets/images/backgrounds/background/bg_173.png'); + } + &.card-background-174 { + background-image: url('@/assets/images/backgrounds/background/bg_174.png'); + } + &.card-background-175 { + background-image: url('@/assets/images/backgrounds/background/bg_175.png'); + } + &.card-background-176 { + background-image: url('@/assets/images/backgrounds/background/bg_176.png'); + } + &.card-background-177 { + background-image: url('@/assets/images/backgrounds/background/bg_177.gif'); + } + &.card-background-178 { + background-image: url('@/assets/images/backgrounds/background/bg_178.png'); + } + &.card-background-179 { + background-image: url('@/assets/images/backgrounds/background/bg_179.png'); + } + &.card-background-180 { + background-image: url('@/assets/images/backgrounds/background/bg_180.png'); + } + &.card-background-181 { + background-image: url('@/assets/images/backgrounds/background/bg_181.png'); + } + &.card-background-182 { + background-image: url('@/assets/images/backgrounds/background/bg_182.png'); + } + &.card-background-183 { + background-image: url('@/assets/images/backgrounds/background/bg_183.png'); + } + &.card-background-184 { + background-image: url('@/assets/images/backgrounds/background/bg_184.png'); + } + &.card-background-185 { + background-image: url('@/assets/images/backgrounds/background/bg_185.png'); + } + &.card-background-186 { + background-image: url('@/assets/images/backgrounds/background/bg_186.png'); + } + &.card-background-187 { + background-image: url('@/assets/images/backgrounds/background/bg_187.gif'); + } +} + +.profile-card-background.card-background-1 { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-2 { + background: linear-gradient(135deg, #4ecdc4 0%, #44a8a3 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-3 { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-4 { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-5 { + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-6 { + background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-7 { + background: linear-gradient(135deg, #5ee7df 0%, #b490ca 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-8 { + background: linear-gradient(135deg, #243949 0%, #517fa4 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-9 { + background-image: repeating-linear-gradient(45deg, #ff6b9d 0 10px, #c06c84 10px 20px); + background-color: #c06c84; + background-size: auto; +} +.profile-card-background.card-background-10 { + background-image: repeating-linear-gradient(90deg, #2b5876 0 8px, #4e4376 8px 16px); + background-color: #2b5876; + background-size: auto; +} +.profile-card-background.card-background-11 { + background-image: radial-gradient(circle, #ffd54f 1.5px, transparent 2px); + background-color: #2c3e50; + background-size: 12px 12px; + background-repeat: repeat; +} +.profile-card-background.card-background-12 { + background-image: linear-gradient(45deg, #1a1a2e 25%, transparent 25%), linear-gradient(-45deg, #1a1a2e 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #1a1a2e 75%), linear-gradient(-45deg, transparent 75%, #1a1a2e 75%); + background-color: #16213e; + background-size: 16px 16px; + background-position: 0 0, 0 8px, 8px -8px, -8px 0; + background-repeat: repeat; +} +.profile-card-background.card-background-13 { + background: linear-gradient(135deg, #232526 0%, #414345 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-14 { + background: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-15 { + background-image: linear-gradient(0deg, transparent 49%, rgba(255,255,255,0.08) 49% 51%, transparent 51%), linear-gradient(90deg, transparent 49%, rgba(255,255,255,0.08) 49% 51%, transparent 51%); + background-color: #1a1a2e; + background-size: 24px 24px; + background-repeat: repeat; +} + .profile-background { background-repeat: no-repeat; background-position: center; From da9e39490132eb12c35a4a87c533e4493e260b9b Mon Sep 17 00:00:00 2001 From: duckietm Date: Mon, 4 May 2026 10:24:02 +0200 Subject: [PATCH 18/20] =?UTF-8?q?=F0=9F=86=99=20Small=20fix=20Badge=20tool?= =?UTF-8?q?tip=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/LayoutBadgeImageView.tsx | 41 ++++++++++++++++--- .../infostand/InfoStandWidgetUserView.tsx | 6 --- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/common/layout/LayoutBadgeImageView.tsx b/src/common/layout/LayoutBadgeImageView.tsx index 75a6533..7162627 100644 --- a/src/common/layout/LayoutBadgeImageView.tsx +++ b/src/common/layout/LayoutBadgeImageView.tsx @@ -1,5 +1,6 @@ import { BadgeImageReadyEvent, GetEventDispatcher, GetSessionDataManager, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer'; -import { CSSProperties, FC, useEffect, useMemo, useState } from 'react'; +import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { GetConfigurationValue, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../api'; import { Base, BaseProps } from '../Base'; @@ -17,6 +18,26 @@ export const LayoutBadgeImageView: FC = props => { const { badgeCode = null, isGroup = false, showInfo = false, customTitle = null, isGrayscale = false, scale = 1, classNames = [], style = {}, children = null, ...rest } = props; const [ imageElement, setImageElement ] = useState(null); + const [ tooltipPosition, setTooltipPosition ] = useState<{ top: number; left: number } | null>(null); + const badgeRef = useRef(null); + + const tooltipsEnabled = showInfo && GetConfigurationValue('badge.descriptions.enabled', true); + + const showTooltip = () => + { + if(!tooltipsEnabled || !badgeRef.current) return; + + const rect = badgeRef.current.getBoundingClientRect(); + const tooltipWidth = 210; + const gap = 10; + let left = rect.left - tooltipWidth - gap; + + if(left < gap) left = rect.right + gap; + + setTooltipPosition({ top: rect.top, left }); + }; + + const hideTooltip = () => setTooltipPosition(null); const getClassNames = useMemo(() => { @@ -116,12 +137,22 @@ export const LayoutBadgeImageView: FC = props => }, [ badgeCode, isGroup ]); return ( - - { (showInfo && GetConfigurationValue('badge.descriptions.enabled', true)) && - + + { tooltipsEnabled && tooltipPosition && createPortal( +
{ isGroup ? customTitle : LocalizeBadgeName(badgeCode) }
{ isGroup ? LocalizeText('group.badgepopup.body') : LocalizeBadgeDescription(badgeCode) }
- } +
, + document.body + ) } { children } ); diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 2e0797b..518bf7a 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -133,13 +133,7 @@ export const InfoStandWidgetUserView: FC = props = SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID)); return () => { - setIsEditingMotto(false); - setMotto(null); setRelationships(null); - setBackgroundId(null); - setStandId(null); - setOverlayId(null); - setCardBackgroundId(null); }; }, [avatarInfo]); From 6fce62fb479e68ee146398cf2854d382752639ed Mon Sep 17 00:00:00 2001 From: duckietm Date: Mon, 4 May 2026 15:26:29 +0200 Subject: [PATCH 19/20] =?UTF-8?q?=F0=9F=86=99=20Updated=20Background=20pro?= =?UTF-8?q?files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Please make sure you change your UI-Config !!! --- index.html | 2 +- public/infostand_backgrounds.json | 712 +++++++ public/ui-config.example | 1730 ++--------------- .../backgrounds/BackgroundsView.tsx | 43 +- 4 files changed, 857 insertions(+), 1630 deletions(-) create mode 100644 public/infostand_backgrounds.json diff --git a/index.html b/index.html index 4e6a87e..e735f16 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@