From 6698c6d5749a6ab5264de5b1a77e28c8cb534180 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 25 Jul 2026 14:01:25 +0200 Subject: [PATCH 01/56] build(deps): update dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @tailwindcss/vite 4.3.2 → 4.3.3 @tanstack/eslint-plugin-query 5.101.2 → 5.101.4 @tanstack/react-query 5.101.2 → 5.101.4 @vitejs/plugin-react 6.0.3 → 6.0.4 lucide-react 1.24.0 → 1.26.0 react 19.2.7 → 19.2.8 react-dom 19.2.7 → 19.2.8 react-hook-form 7.81.0 → 7.83.0 react-i18next 17.0.9 → 17.0.11 tailwindcss 4.3.2 → 4.3.3 vite 8.1.4 → 8.1.5 --- package-lock.json | 271 ++++++++++++++++++++++------------------------ package.json | 22 ++-- 2 files changed, 142 insertions(+), 151 deletions(-) diff --git a/package-lock.json b/package-lock.json index a093c158..bc12304d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,28 +29,28 @@ "@radix-ui/react-tooltip": "1.2.12", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", - "@tailwindcss/vite": "4.3.2", + "@tailwindcss/vite": "4.3.3", "@tanstack/match-sorter-utils": "8.19.4", - "@tanstack/react-query": "5.101.2", + "@tanstack/react-query": "5.101.4", "@tanstack/react-table": "8.21.3", "bitcoin-address-validation": "3.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "html5-qrcode": "2.3.8", "i18next-browser-languagedetector": "8.2.1", - "lucide-react": "1.24.0", + "lucide-react": "1.26.0", "next-themes": "0.4.6", "qrcode": "1.5.4", "radix-ui": "1.6.2", - "react": "19.2.7", - "react-dom": "19.2.7", - "react-hook-form": "7.81.0", - "react-i18next": "17.0.9", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-hook-form": "7.83.0", + "react-i18next": "17.0.11", "react-router-dom": "7.18.1", "react-use-websocket": "4.13.0", "sonner": "2.0.7", "tailwind-merge": "3.6.0", - "tailwindcss": "4.3.2", + "tailwindcss": "4.3.3", "tw-animate-css": "1.4.0", "yup": "1.7.1", "zustand": "5.0.14" @@ -63,7 +63,7 @@ "@storybook/addon-docs": "10.5.0", "@storybook/addon-vitest": "10.5.0", "@storybook/react-vite": "10.5.0", - "@tanstack/eslint-plugin-query": "5.101.2", + "@tanstack/eslint-plugin-query": "5.101.4", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", @@ -72,7 +72,7 @@ "@types/qrcode": "1.5.6", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "6.0.3", + "@vitejs/plugin-react": "6.0.4", "@vitest/browser": "4.1.10", "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "4.1.10", @@ -94,7 +94,7 @@ "storybook": "10.5.0", "typescript": "6.0.3", "typescript-eslint": "8.63.0", - "vite": "8.1.4", + "vite": "8.1.5", "vitest": "4.1.10" }, "engines": { @@ -4624,47 +4624,47 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", + "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" + "tailwindcss": "4.3.3" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", - "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ "arm64" ], @@ -4678,9 +4678,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], @@ -4694,9 +4694,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ "x64" ], @@ -4710,9 +4710,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ "x64" ], @@ -4726,9 +4726,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", "cpu": [ "arm" ], @@ -4742,9 +4742,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ "arm64" ], @@ -4761,9 +4761,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ "arm64" ], @@ -4780,9 +4780,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], @@ -4799,9 +4799,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], @@ -4818,9 +4818,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -4847,9 +4847,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], @@ -4863,9 +4863,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", - "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], @@ -4879,23 +4879,23 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "tailwindcss": "4.3.2" + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "node_modules/@tanstack/eslint-plugin-query": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.101.2.tgz", - "integrity": "sha512-cPE99s3XZwlObfn8lCezT4j4JLj2CVzpIEywx0H4hzfPsX/o9QhdwaOwcDXxrQAqx2ds7TbvTinxhB8B/ywb6w==", + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.101.4.tgz", + "integrity": "sha512-jqAZJWXGd5PthJYH9wmC2O5Ry5xAN2/7zxi9qcANQ+Xix8V5JkqNRjZ8Fh+rYzqzVYGiHqbrHekt+uh38OZVpw==", "dev": true, "license": "MIT", "dependencies": { @@ -4932,9 +4932,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", - "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", "license": "MIT", "funding": { "type": "github", @@ -4942,12 +4942,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", - "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.101.2" + "@tanstack/query-core": "5.101.4" }, "funding": { "type": "github", @@ -5491,16 +5491,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -5575,9 +5575,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", "dev": true, "license": "MIT", "dependencies": { @@ -6840,9 +6840,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7562,16 +7562,16 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { @@ -7691,12 +7691,12 @@ "license": "MIT" }, "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz", + "integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==", "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" + "funding": { + "url": "https://locize.com" } }, "node_modules/html5-qrcode": { @@ -8630,9 +8630,9 @@ } }, "node_modules/lucide-react": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz", - "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.26.0.tgz", + "integrity": "sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -9256,9 +9256,9 @@ } }, "node_modules/postcss": { - "version": "8.5.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz", - "integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -9275,7 +9275,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9689,9 +9689,9 @@ } }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9730,21 +9730,21 @@ } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-hook-form": { - "version": "7.81.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.81.0.tgz", - "integrity": "sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==", + "version": "7.83.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.83.0.tgz", + "integrity": "sha512-AXt8cMCmx5a7u4uvpb2uRFVrWQhllI4pV+LSykxIac/hjt44TnQkmX9BKuQi2i+LDC62esmiLpilkav+kjVf/A==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -9758,13 +9758,13 @@ } }, "node_modules/react-i18next": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.9.tgz", - "integrity": "sha512-buLzOSqHtXxjf+qgSrLWNTXVZ1jSwO6kUv3uJqSP1roGBPgNnbhFm7OmdVwWcgf2gIbUyP0J333uPyx+Btsi3w==", + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.11.tgz", + "integrity": "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "html-parse-stringify": "^3.0.1", + "html-parse-stringify": "^4.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { @@ -10536,9 +10536,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "license": "MIT" }, "node_modules/tapable": { @@ -10949,15 +10949,15 @@ } }, "node_modules/vite": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", - "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", - "postcss": "^8.5.16", - "rolldown": "~1.1.4", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { @@ -11157,15 +11157,6 @@ "node": ">=18" } }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index af04c0e5..4545f695 100644 --- a/package.json +++ b/package.json @@ -91,28 +91,28 @@ "@radix-ui/react-tooltip": "1.2.12", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", - "@tailwindcss/vite": "4.3.2", + "@tailwindcss/vite": "4.3.3", "@tanstack/match-sorter-utils": "8.19.4", - "@tanstack/react-query": "5.101.2", + "@tanstack/react-query": "5.101.4", "@tanstack/react-table": "8.21.3", "bitcoin-address-validation": "3.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "html5-qrcode": "2.3.8", "i18next-browser-languagedetector": "8.2.1", - "lucide-react": "1.24.0", + "lucide-react": "1.26.0", "next-themes": "0.4.6", "qrcode": "1.5.4", "radix-ui": "1.6.2", - "react": "19.2.7", - "react-dom": "19.2.7", - "react-hook-form": "7.81.0", - "react-i18next": "17.0.9", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-hook-form": "7.83.0", + "react-i18next": "17.0.11", "react-router-dom": "7.18.1", "react-use-websocket": "4.13.0", "sonner": "2.0.7", "tailwind-merge": "3.6.0", - "tailwindcss": "4.3.2", + "tailwindcss": "4.3.3", "tw-animate-css": "1.4.0", "yup": "1.7.1", "zustand": "5.0.14" @@ -125,7 +125,7 @@ "@storybook/addon-docs": "10.5.0", "@storybook/addon-vitest": "10.5.0", "@storybook/react-vite": "10.5.0", - "@tanstack/eslint-plugin-query": "5.101.2", + "@tanstack/eslint-plugin-query": "5.101.4", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", @@ -134,7 +134,7 @@ "@types/qrcode": "1.5.6", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "6.0.3", + "@vitejs/plugin-react": "6.0.4", "@vitest/browser": "4.1.10", "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "4.1.10", @@ -156,7 +156,7 @@ "storybook": "10.5.0", "typescript": "6.0.3", "typescript-eslint": "8.63.0", - "vite": "8.1.4", + "vite": "8.1.5", "vitest": "4.1.10" }, "overrides": { From 5f0818d7346325d1dbdd1bcdd6b45d28d8f1eca0 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 25 Jul 2026 14:04:24 +0200 Subject: [PATCH 02/56] build(deps): update dev dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lint-staged 17.0.8 → 17.2.0 prettier 3.9.5 → 3.9.6 prettier-plugin-tailwindcss 0.8.0 → 0.8.1 typescript-eslint 8.63.0 → 8.65.0 --- package-lock.json | 490 +++++++--------------------------------------- package.json | 8 +- 2 files changed, 79 insertions(+), 419 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc12304d..6b9bb868 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,14 +86,14 @@ "globals": "17.7.0", "husky": "9.1.7", "jsdom": "29.1.1", - "lint-staged": "17.0.8", + "lint-staged": "17.2.0", "msw": "2.15.0", "msw-storybook-addon": "2.0.7", - "prettier": "3.9.5", - "prettier-plugin-tailwindcss": "0.8.0", + "prettier": "3.9.6", + "prettier-plugin-tailwindcss": "0.8.1", "storybook": "10.5.0", "typescript": "6.0.3", - "typescript-eslint": "8.63.0", + "typescript-eslint": "8.65.0", "vite": "8.1.5", "vitest": "4.1.10" }, @@ -5293,17 +5293,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", - "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/type-utils": "8.63.0", - "@typescript-eslint/utils": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -5316,7 +5316,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.63.0", + "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -5332,16 +5332,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", - "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "engines": { @@ -5357,14 +5357,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", - "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.63.0", - "@typescript-eslint/types": "^8.63.0", + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "engines": { @@ -5379,14 +5379,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", - "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5397,9 +5397,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", - "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", "dev": true, "license": "MIT", "engines": { @@ -5414,15 +5414,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", - "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -5439,9 +5439,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", - "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, "license": "MIT", "engines": { @@ -5453,16 +5453,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", - "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.63.0", - "@typescript-eslint/tsconfig-utils": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5520,16 +5520,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", - "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5544,13 +5544,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", - "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5898,22 +5898,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6339,39 +6323,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -6865,19 +6816,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -7298,13 +7236,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -7498,19 +7429,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -7867,22 +7785,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8441,14 +8343,13 @@ } }, "node_modules/lint-staged": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.8.tgz", - "integrity": "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.2.0.tgz", + "integrity": "sha512-FchGnFe4i4B1C/a35SPU9bNGPEHSC1+1iV0plLjzBmKVe9klZrlRfSgK6Cw4VeHyqOXbJUXP0vON61uRftNQ0A==", "dev": true, "license": "MIT", "dependencies": { - "listr2": "^10.2.1", - "picomatch": "^4.0.4", + "picomatch": "^4.0.5", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, @@ -8465,23 +8366,6 @@ "yaml": "^2.9.0" } }, - "node_modules/listr2": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.2.tgz", - "integrity": "sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.2.0", - "eventemitter3": "^5.0.4", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^10.0.0" - }, - "engines": { - "node": ">=22.13.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8519,99 +8403,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -8692,19 +8483,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8895,22 +8673,6 @@ "node": ">=12.20.0" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -9294,9 +9056,9 @@ } }, "node_modules/prettier": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", - "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -9310,9 +9072,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.8.0.tgz", - "integrity": "sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.8.1.tgz", + "integrity": "sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==", "dev": true, "license": "MIT", "engines": { @@ -10040,23 +9802,6 @@ "node": ">=4" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/rettime": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", @@ -10064,13 +9809,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -10235,36 +9973,6 @@ "node": ">=18" } }, - "node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -10397,23 +10105,6 @@ "node": ">=0.6.19" } }, - "node_modules/string-width": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", - "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -10779,16 +10470,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", - "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.63.0", - "@typescript-eslint/parser": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0" + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -11271,37 +10962,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", diff --git a/package.json b/package.json index 4545f695..feea3698 100644 --- a/package.json +++ b/package.json @@ -148,14 +148,14 @@ "globals": "17.7.0", "husky": "9.1.7", "jsdom": "29.1.1", - "lint-staged": "17.0.8", + "lint-staged": "17.2.0", "msw": "2.15.0", "msw-storybook-addon": "2.0.7", - "prettier": "3.9.5", - "prettier-plugin-tailwindcss": "0.8.0", + "prettier": "3.9.6", + "prettier-plugin-tailwindcss": "0.8.1", "storybook": "10.5.0", "typescript": "6.0.3", - "typescript-eslint": "8.63.0", + "typescript-eslint": "8.65.0", "vite": "8.1.5", "vitest": "4.1.10" }, From 3f0d3bbc40dcfdfcf3fc70c5e580ea4ed31646b9 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 25 Jul 2026 14:06:01 +0200 Subject: [PATCH 03/56] build(deps): update storybook and eslint --- package-lock.json | 126 +++++++++++++++++++++++----------------------- package.json | 16 +++--- 2 files changed, 71 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b9bb868..07635e64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,12 +57,12 @@ }, "devDependencies": { "@chromatic-com/storybook": "5.2.1", - "@eslint/js": "9.39.4", + "@eslint/js": "9.39.5", "@playwright/test": "1.61.1", - "@storybook/addon-a11y": "10.5.0", - "@storybook/addon-docs": "10.5.0", - "@storybook/addon-vitest": "10.5.0", - "@storybook/react-vite": "10.5.0", + "@storybook/addon-a11y": "10.5.4", + "@storybook/addon-docs": "10.5.4", + "@storybook/addon-vitest": "10.5.4", + "@storybook/react-vite": "10.5.4", "@tanstack/eslint-plugin-query": "5.101.4", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", @@ -77,11 +77,11 @@ "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "4.1.10", "conventional-changelog": "8.1.0", - "eslint": "9.39.4", + "eslint": "9.39.5", "eslint-plugin-compat": "7.0.2", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.3", - "eslint-plugin-storybook": "10.5.0", + "eslint-plugin-storybook": "10.5.4", "eslint-plugin-unicorn": "65.0.1", "globals": "17.7.0", "husky": "9.1.7", @@ -91,7 +91,7 @@ "msw-storybook-addon": "2.0.7", "prettier": "3.9.6", "prettier-plugin-tailwindcss": "0.8.1", - "storybook": "10.5.0", + "storybook": "10.5.4", "typescript": "6.0.3", "typescript-eslint": "8.65.0", "vite": "8.1.5", @@ -1259,9 +1259,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { @@ -4374,9 +4374,9 @@ "license": "MIT" }, "node_modules/@storybook/addon-a11y": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.0.tgz", - "integrity": "sha512-AguqTsYS2NX20AF1vWt6IonbkpMXur30/dAOOYltbEW7uKmPxBNtjuiBmYB2WI4aFN4r8jje4bbDGk/Yfq58hQ==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.4.tgz", + "integrity": "sha512-UWdZtB7Dh1GjqxTPOrisUNDhDGF5pKGzZdzW9DSSHMBcAOx2dsTmXGzLSFprAz4LTT0OHCtKhxNqKK0JZJ0Y8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4388,20 +4388,20 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.0" + "storybook": "^10.5.4" } }, "node_modules/@storybook/addon-docs": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.0.tgz", - "integrity": "sha512-kbZGdX+mysiY4xezhpcFEwzQ1zSXcMhSS3u09Qt8+w5XetCAMMSv/yAxzpi6z+YoH+EdQ53e1q3MFtPUkzkfUA==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.4.tgz", + "integrity": "sha512-2Z/x2pKEmXOCQjmttYzPuQBu9aWeMly8uEs3msrCTBLiHs/F7IlBFnMu0Z+T2Qvk0LEy8O93AlcPSP76aCcKjw==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.5.0", + "@storybook/csf-plugin": "10.5.4", "@storybook/icons": "^2.0.2", - "@storybook/react-dom-shim": "10.5.0", + "@storybook/react-dom-shim": "10.5.4", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -4412,7 +4412,7 @@ }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.0" + "storybook": "^10.5.4" }, "peerDependenciesMeta": { "@types/react": { @@ -4421,9 +4421,9 @@ } }, "node_modules/@storybook/addon-vitest": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.0.tgz", - "integrity": "sha512-o/H+ii8o7bu4r1M4pgyFt+DjVaIIccSeaAikpspNHKkKdiJ2GXfkgNwtib8Jru28HaBRfcP5u+m0QE3KBYIQkw==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.4.tgz", + "integrity": "sha512-5lr81sgrbK3Tjjbstieg/yYj3jdtQYP70ihpOiN1TwogvFATY2/9eMT46FE5ioKtEFcfJRDcgJ2wSlmJKE+kZw==", "dev": true, "license": "MIT", "dependencies": { @@ -4438,7 +4438,7 @@ "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", - "storybook": "^10.5.0", + "storybook": "^10.5.4", "vitest": "^3.0.0 || ^4.0.0" }, "peerDependenciesMeta": { @@ -4457,13 +4457,13 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.0.tgz", - "integrity": "sha512-KXlifNIThDgS84KqVAJXyilool8OLTWp6DGoO9h5bHM2IPLe7UcdKfOzMUBkQ807mWBk4aW1yGEekj7kC2dvmg==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.4.tgz", + "integrity": "sha512-eXdgow+brSzZrG3CnG18YwxZwEYNAZIh2G5qKwNoZf0uk2ZCPgLRQwtVAcd7BiiEDGnXUHm9pycAS+UiF4F4Mg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.5.0", + "@storybook/csf-plugin": "10.5.4", "ts-dedent": "^2.0.0" }, "funding": { @@ -4471,14 +4471,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.0", + "storybook": "^10.5.4", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.0.tgz", - "integrity": "sha512-R7VFw6FnDZTWct0ekOcFnTfDgdHnnAuQW+AbGG9A9IZPvyH9uLJivK7L86+r9MITRrO2+v81HaFDsT/OFk6ZkQ==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.4.tgz", + "integrity": "sha512-DSp5Z/eZlRnKq0KrKLJE6uoYf/Ysc+FP0Z5DVTGnOrie+z3tC0lNi9I4RB++EXkJeUDS9/4dxvJZVSWjhLlXxw==", "dev": true, "license": "MIT", "dependencies": { @@ -4491,7 +4491,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.5.0", + "storybook": "^10.5.4", "vite": "*", "webpack": "*" }, @@ -4528,14 +4528,14 @@ } }, "node_modules/@storybook/react": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.0.tgz", - "integrity": "sha512-2IhddiREy7NqAqnFsEOfW6HBG7azIcLxhRQYploSsaemOncR29xhzvAZsG+xlWgrtvxv4h3fYQTTR4TNn8CgPQ==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.4.tgz", + "integrity": "sha512-tOxfVgbYcaVsArN8XTDkJfdsnsnHh1LxjRHVpJ/N+VEkz4FveK/XH3jOLV0YqgrG8yXza7+CteDP4FfPVQY/mw==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.5.0", + "@storybook/react-dom-shim": "10.5.4", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, @@ -4548,7 +4548,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.0", + "storybook": "^10.5.4", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -4564,9 +4564,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.0.tgz", - "integrity": "sha512-GwGA6zDj4Cfw6vGsdfPKfF39L0W6solNbbnjdjGa57m2ooASkidLhrXo2wgC9wh3o/jcfonmYPDRGY6sEhGDtg==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.4.tgz", + "integrity": "sha512-YdlppEOReg8MvTECRNuf79gu2zL83JqKDHIR/65eS0M6y+ue9pkpfjYo7hZVIcyOcRd9npBDXMdt2kC92bCuaA==", "dev": true, "license": "MIT", "funding": { @@ -4578,7 +4578,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.0" + "storybook": "^10.5.4" }, "peerDependenciesMeta": { "@types/react": { @@ -4590,16 +4590,16 @@ } }, "node_modules/@storybook/react-vite": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.0.tgz", - "integrity": "sha512-d9n/I3pViscXpJYT9JHxcpxVSam14HsHOr2wBqTQTiRtaiH4xSsT00HTEGm6CEZTyq/npTJyV0Qday9XhrtiDQ==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.4.tgz", + "integrity": "sha512-iw7EAA98n30vpf+ZSy2Ll4Ne7oyZ/lS6W22u5Sp5UOI2oAkYuklxIWjRIKGqpY0BHP+GqaGYtUGMZQnBqgul2g==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.5.0", - "@storybook/react": "10.5.0", + "@storybook/builder-vite": "10.5.4", + "@storybook/react": "10.5.4", "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.2", @@ -4613,7 +4613,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.0", + "storybook": "^10.5.4", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, @@ -6899,9 +6899,9 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", "dependencies": { @@ -6910,8 +6910,8 @@ "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7024,18 +7024,18 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.0.tgz", - "integrity": "sha512-UyhbK11baKAYqzyDUHlwDlm1aP/wxPMR0vFmsxA5984BGaPwarhoXUWng1Xa0cd9BdPbQ+zr/y8ADk6XxtwkYg==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.4.tgz", + "integrity": "sha512-IEG6GK9iMKH45WxiBUoDu3AajUyz+AcRXsLRkwnsNz9TwzZI6tbyHgdZ1Tkvs2RXOovYUYYksNpbtnmY746LAw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "^8.48.0", - "@typescript-eslint/utils": "^8.48.0" + "@typescript-eslint/types": "^8.60.0", + "@typescript-eslint/utils": "^8.60.0" }, "peerDependencies": { "eslint": ">=8", - "storybook": "^10.5.0" + "storybook": "^10.5.4" } }, "node_modules/eslint-plugin-unicorn": { @@ -10027,9 +10027,9 @@ "license": "MIT" }, "node_modules/storybook": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.0.tgz", - "integrity": "sha512-dRhM/kSSvHQR8DmZO41v5sJuz9U6zDjjR2gRBTgZN2RBSXbmF0Brvgszrvvxyx2VfxuYKzhB+xumKwWkwlBtig==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.4.tgz", + "integrity": "sha512-bmLxPsxVSPnbeiZqYQpozyNOiJXfk+pf7WfHZflvPkwT6Y+rvYz3Cj/D6H4Kf2jHpuDNiMXBKO3yawLN2OWirg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index feea3698..cdc1d4ee 100644 --- a/package.json +++ b/package.json @@ -119,12 +119,12 @@ }, "devDependencies": { "@chromatic-com/storybook": "5.2.1", - "@eslint/js": "9.39.4", + "@eslint/js": "9.39.5", "@playwright/test": "1.61.1", - "@storybook/addon-a11y": "10.5.0", - "@storybook/addon-docs": "10.5.0", - "@storybook/addon-vitest": "10.5.0", - "@storybook/react-vite": "10.5.0", + "@storybook/addon-a11y": "10.5.4", + "@storybook/addon-docs": "10.5.4", + "@storybook/addon-vitest": "10.5.4", + "@storybook/react-vite": "10.5.4", "@tanstack/eslint-plugin-query": "5.101.4", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", @@ -139,11 +139,11 @@ "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "4.1.10", "conventional-changelog": "8.1.0", - "eslint": "9.39.4", + "eslint": "9.39.5", "eslint-plugin-compat": "7.0.2", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.3", - "eslint-plugin-storybook": "10.5.0", + "eslint-plugin-storybook": "10.5.4", "eslint-plugin-unicorn": "65.0.1", "globals": "17.7.0", "husky": "9.1.7", @@ -153,7 +153,7 @@ "msw-storybook-addon": "2.0.7", "prettier": "3.9.6", "prettier-plugin-tailwindcss": "0.8.1", - "storybook": "10.5.0", + "storybook": "10.5.4", "typescript": "6.0.3", "typescript-eslint": "8.65.0", "vite": "8.1.5", From fe43741a37f3d75bbb862b714d70b5c4c0dc9516 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 25 Jul 2026 14:44:49 +0200 Subject: [PATCH 04/56] build(deps): update radix-ui components --- package-lock.json | 1119 +++++++++++++++++++++++---------------------- package.json | 32 +- 2 files changed, 576 insertions(+), 575 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07635e64..4d4070f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,21 +12,21 @@ "@hookform/resolvers": "5.4.0", "@joinmarket-webui/joinmarket-ng-api-ts": "1.0.0", "@noble/hashes": "2.2.0", - "@radix-ui/react-accordion": "1.2.16", - "@radix-ui/react-avatar": "1.2.2", - "@radix-ui/react-checkbox": "1.3.7", - "@radix-ui/react-dialog": "1.1.19", - "@radix-ui/react-dropdown-menu": "2.1.20", - "@radix-ui/react-label": "2.1.11", - "@radix-ui/react-popover": "1.1.19", - "@radix-ui/react-radio-group": "1.4.3", - "@radix-ui/react-select": "2.3.3", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-slider": "1.4.3", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-switch": "1.3.3", - "@radix-ui/react-tabs": "1.1.17", - "@radix-ui/react-tooltip": "1.2.12", + "@radix-ui/react-accordion": "1.2.20", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-checkbox": "1.3.11", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-popover": "1.1.23", + "@radix-ui/react-radio-group": "1.4.7", + "@radix-ui/react-select": "2.3.7", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slider": "1.4.7", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-switch": "1.3.7", + "@radix-ui/react-tabs": "1.1.21", + "@radix-ui/react-tooltip": "1.2.16", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "@tailwindcss/vite": "4.3.3", @@ -41,7 +41,7 @@ "lucide-react": "1.26.0", "next-themes": "0.4.6", "qrcode": "1.5.4", - "radix-ui": "1.6.2", + "radix-ui": "1.6.7", "react": "19.2.8", "react-dom": "19.2.8", "react-hook-form": "7.83.0", @@ -2451,24 +2451,24 @@ "license": "MIT" }, "node_modules/@radix-ui/number": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", - "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.5.tgz", - "integrity": "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", "license": "MIT" }, "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.11.tgz", - "integrity": "sha512-HQDOFTKwSnmUij6l54wYJJtxTAnxI71+YJLOrjm2ladFB8HAV5Jt7hwaZPhWTGBkYoW4+ZAOfNZrLDh/qvxSYA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.15.tgz", + "integrity": "sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==", "license": "MIT", "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -2486,20 +2486,20 @@ } }, "node_modules/@radix-ui/react-accordion": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.16.tgz", - "integrity": "sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ==", + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.20.tgz", + "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collapsible": "1.1.16", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -2517,16 +2517,16 @@ } }, "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.19.tgz", - "integrity": "sha512-FA7n1f6D/DwGE0+AWxiY5LacNbbExQuEgMubeG06idEaH+mSLuf9dp/qBNqOnvbTQ+4gZ2ue1RATF1Ub91Mg5g==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.23.tgz", + "integrity": "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dialog": "1.1.19", - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -2544,12 +2544,12 @@ } }, "node_modules/@radix-ui/react-arrow": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.11.tgz", - "integrity": "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -2567,12 +2567,12 @@ } }, "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.11.tgz", - "integrity": "sha512-IUAhIVpBUvP5NNICjlaB1OFmtRLGqQqTF3ZOSGPoq3XeLXRFtHiWTRxSVEULgOd9GQR2c7tsYqDnhUennapZnw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.15.tgz", + "integrity": "sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -2590,16 +2590,17 @@ } }, "node_modules/@radix-ui/react-avatar": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.2.tgz", - "integrity": "sha512-sST0qh8GzOB7besQ3tMLWLyngnRuSk0gc/Hm+667KYKQFCt6Y6ZXv25WlqM7dIDK54ULCh5+CHmk4LIolzfz+A==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -2617,19 +2618,18 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.7.tgz", - "integrity": "sha512-JroKHfQBfh+fDuzpPsBC+pESkhuq8ql4hljTguz8MWnS35cISr3d/Jhl9kYrB44FlDtxCArYdDvTx+BSsJ64rQ==", + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", + "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -2647,19 +2647,19 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.16.tgz", - "integrity": "sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -2677,15 +2677,15 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.12.tgz", - "integrity": "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -2703,9 +2703,9 @@ } }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", - "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2718,9 +2718,9 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz", - "integrity": "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2733,16 +2733,16 @@ } }, "node_modules/@radix-ui/react-context-menu": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.3.tgz", - "integrity": "sha512-PS+gKE0z2prJ74Y0sM+brAGK4mYOHIR7TlcV5EJgUQ6E0xMvyswkK2X4yRqyganrzsRL+WCSKAPu0NQITICRWg==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.7.tgz", + "integrity": "sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -2760,23 +2760,24 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.19.tgz", - "integrity": "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -2796,9 +2797,9 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", - "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2811,16 +2812,16 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.15.tgz", - "integrity": "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-effect-event": "0.0.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", @@ -2838,18 +2839,18 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.20.tgz", - "integrity": "sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -2867,9 +2868,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", - "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2882,14 +2883,14 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.12.tgz", - "integrity": "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -2907,17 +2908,17 @@ } }, "node_modules/@radix-ui/react-form": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.12.tgz", - "integrity": "sha512-JTX94E4LDL91rzLg7X0mHPdxr0A8JEdVwZEmeOwZJSMDHCGW5DFtSlTSJozUyUs807IQmnvbfzKZFVCK5DmkqQ==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.16.tgz", + "integrity": "sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-label": "2.1.11", - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -2935,20 +2936,20 @@ } }, "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.19.tgz", - "integrity": "sha512-2KTgMLQtKvicznQgbindEI2RZ3QbDIwU5gabjUPwFJsormjGDz+rUvO4NANmYwzEEpTcTONUt33vBHIfTIVSfw==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.23.tgz", + "integrity": "sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -2966,12 +2967,12 @@ } }, "node_modules/@radix-ui/react-id": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", - "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -2984,12 +2985,12 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.11.tgz", - "integrity": "sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ==", + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -3007,27 +3008,27 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.20.tgz", - "integrity": "sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -3047,21 +3048,21 @@ } }, "node_modules/@radix-ui/react-menubar": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.20.tgz", - "integrity": "sha512-gzFZvybgmwYsFBWDqanycIoEYnhyk8MMnuLamdFVHUZYGp4COM+sqXiwbnn0VMWqGLeeU7GV7jm+dXRa+Wufag==", + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.24.tgz", + "integrity": "sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -3079,25 +3080,25 @@ } }, "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.18.tgz", - "integrity": "sha512-K9HiuxZ6xCwSaHcIuUpxyhy4w5gpwzWjh9dHTSbMN3Ix4qAyVObS9RlU3zMycb0PO3v9Tpk0BXMwWvXOUbVXew==", + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.22.tgz", + "integrity": "sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -3115,23 +3116,23 @@ } }, "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.12.tgz", - "integrity": "sha512-nQLu5OAcORDQp1EHAv6k3mJGV1hjMTw2NTGVAsGE1g/mWeNqAd1R5jyaAs3U+A8ZD/W8XNPY2yKT0ZdQnqo3NA==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.16.tgz", + "integrity": "sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3149,19 +3150,19 @@ } }, "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.7.tgz", - "integrity": "sha512-gB1Mr8vzdv1XzDjrtJTXmL0JORRs1B4g7ngUs0F+H2VvMOwXTZMTmLCl0wZZ3m7ylX8TssI7NCvgiSHmLuTm/A==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.11.tgz", + "integrity": "sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-is-hydrated": "0.1.1" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3" }, "peerDependencies": { "@types/react": "*", @@ -3179,24 +3180,24 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.19.tgz", - "integrity": "sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", + "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -3216,21 +3217,21 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.3.tgz", - "integrity": "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-rect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/rect": "1.1.2" + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", @@ -3248,13 +3249,13 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.13.tgz", - "integrity": "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==", + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3272,12 +3273,12 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.7.tgz", - "integrity": "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3295,12 +3296,12 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", - "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -3318,13 +3319,13 @@ } }, "node_modules/@radix-ui/react-progress": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.12.tgz", - "integrity": "sha512-ZPHyI0JyzoH/rP0tq2uRaIZTj/4s8+kAbqPz+e2N8+ejHvwPJ889dHhqn+vh7PNvNeq+boAoH9yzqeoShzwF2w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", + "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -3342,21 +3343,20 @@ } }, "node_modules/@radix-ui/react-radio-group": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.3.tgz", - "integrity": "sha512-WwZFjWV4s3aC1QtR3k04R+oANHtX2q6fgKlc7MCEiDNlnTxCZ3H8k3mHtEgVlOejystwk1WQgarQhNOQZ2bK1g==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.7.tgz", + "integrity": "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3374,22 +3374,22 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.15.tgz", - "integrity": "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3407,20 +3407,20 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.14.tgz", - "integrity": "sha512-bBODCWZK7JTbQLHs0uIP4f73wIWatakK4OS33UzkR1x897wu0PuO658a3f+6P2GEGyDzGYMuHRatMVoAk9WZTw==", + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3438,31 +3438,31 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.3.tgz", - "integrity": "sha512-L5RQTXz6Anxsf9CCv+pTgiAsUpyVj7rJxsGtmhFaEOJ++cVfXucv4qWfsIO0AIB4NAhi3yovWGVMKKS1Xf1Wrg==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7", + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -3482,12 +3482,12 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.11.tgz", - "integrity": "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -3505,22 +3505,22 @@ } }, "node_modules/@radix-ui/react-slider": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.3.tgz", - "integrity": "sha512-CWVVj+XaTom0SKCqw1EUgb0NuiLwS+N3OFG73mVEezKEjgNIvZiu0EevMelSSU+CbX3owbqJweG2gPU31WGC5A==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.7.tgz", + "integrity": "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3538,12 +3538,12 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", - "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3" + "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", @@ -3556,18 +3556,17 @@ } }, "node_modules/@radix-ui/react-switch": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.3.tgz", - "integrity": "sha512-1+mlB4/lxJfk5tgJ4g+R5mUCbRpPE1T9+UsEyeLYbGgMtwiMgmuTnfKz4Mw1nHALHjuwyxw4MLd4cSHn6pNSlQ==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", + "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3585,19 +3584,19 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.17.tgz", - "integrity": "sha512-nRyXnrAVCwjeXcHbvEbLS6ndbTeKHG1RqCP4A8Gw5L4cemDzPXdD8rAmr6wet0v57R69wGvuIIsFjHSVkZiMzQ==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -3615,23 +3614,23 @@ } }, "node_modules/@radix-ui/react-toast": { - "version": "1.2.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.19.tgz", - "integrity": "sha512-SxfVZfVOibWKWdkf0Xx1awW2d09fQu4V4PXDY1j5hi4MVf7MWdJZqTBJMa1KWtOr1S6GGtCk02nniZ0Iia+dHw==", + "version": "1.2.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.23.tgz", + "integrity": "sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -3649,14 +3648,14 @@ } }, "node_modules/@radix-ui/react-toggle": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.14.tgz", - "integrity": "sha512-QI/hB65XKWACA66P64A+aHxtLUgHJeJLkaQa+awUNXT6T3swndtY5DojeHA+vldrTspMTtFBd7HfZ9QGbM1Qrw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.18.tgz", + "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -3674,18 +3673,18 @@ } }, "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.15.tgz", - "integrity": "sha512-gIC5Q+Xljg7lmUdzSuDoy0t97yZn1sZl00Ra37ZvKrYdWnQLU6sWLd09yG8cIB9jUAlQfHgJ2ACAG00MFwsqSQ==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.19.tgz", + "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-toggle": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -3703,18 +3702,18 @@ } }, "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.15.tgz", - "integrity": "sha512-t/iEuVjUnXXtrsGK40AA43uIx37sn3AqZ7oAVnPICK6lFJP6dzMzWR3U9b6eCfFjb6wtSEqkJ9Rn9xDjiOx20g==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.19.tgz", + "integrity": "sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-toggle-group": "1.1.15" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-toggle-group": "1.1.19" }, "peerDependencies": { "@types/react": "*", @@ -3732,23 +3731,24 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.12.tgz", - "integrity": "sha512-U3HoftgWnmla78vzQbLvKKb7bUYJxoiiqYFzp1wu/TBMyDqMZSuCl3aRICsD6EfVEwcJD2mumGDGUXLFVqQHKA==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -3766,9 +3766,9 @@ } }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", - "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3781,13 +3781,14 @@ } }, "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", - "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3800,12 +3801,12 @@ } }, "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", - "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3818,12 +3819,12 @@ } }, "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.3.tgz", - "integrity": "sha512-3wEkMiPHXha/2VadZ68rYBcmYnPINVGl4Y3gtcM7fKRjANk0OscK+cdqBgUWdozb7YJxsh0vefM7vgAMHXOjqg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.5.tgz", + "integrity": "sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.2" + "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3836,9 +3837,9 @@ } }, "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", - "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3851,9 +3852,9 @@ } }, "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3866,9 +3867,9 @@ } }, "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", - "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3881,12 +3882,12 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", - "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.2" + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", @@ -3899,12 +3900,12 @@ } }, "node_modules/@radix-ui/react-use-size": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", - "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3917,12 +3918,12 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.7.tgz", - "integrity": "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -3940,9 +3941,9 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", - "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { @@ -9374,66 +9375,66 @@ } }, "node_modules/radix-ui": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.2.tgz", - "integrity": "sha512-OwYUjzMwiInCUxgAWpPsavXC3Kh4iyi/49uU1/qZTG3RQDlvegyk1GOMiGvSkjua1RDb3JD3fo3eroL9FV4GQw==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.7.tgz", + "integrity": "sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-accessible-icon": "1.1.11", - "@radix-ui/react-accordion": "1.2.16", - "@radix-ui/react-alert-dialog": "1.1.19", - "@radix-ui/react-arrow": "1.1.11", - "@radix-ui/react-aspect-ratio": "1.1.11", - "@radix-ui/react-avatar": "1.2.2", - "@radix-ui/react-checkbox": "1.3.7", - "@radix-ui/react-collapsible": "1.1.16", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-context-menu": "2.3.3", - "@radix-ui/react-dialog": "1.1.19", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-dropdown-menu": "2.1.20", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-form": "0.1.12", - "@radix-ui/react-hover-card": "1.1.19", - "@radix-ui/react-label": "2.1.11", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-menubar": "1.1.20", - "@radix-ui/react-navigation-menu": "1.2.18", - "@radix-ui/react-one-time-password-field": "0.1.12", - "@radix-ui/react-password-toggle-field": "0.1.7", - "@radix-ui/react-popover": "1.1.19", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-progress": "1.1.12", - "@radix-ui/react-radio-group": "1.4.3", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-scroll-area": "1.2.14", - "@radix-ui/react-select": "2.3.3", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-slider": "1.4.3", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-switch": "1.3.3", - "@radix-ui/react-tabs": "1.1.17", - "@radix-ui/react-toast": "1.2.19", - "@radix-ui/react-toggle": "1.1.14", - "@radix-ui/react-toggle-group": "1.1.15", - "@radix-ui/react-toolbar": "1.1.15", - "@radix-ui/react-tooltip": "1.2.12", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-escape-keydown": "1.1.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-accessible-icon": "1.1.15", + "@radix-ui/react-accordion": "1.2.20", + "@radix-ui/react-alert-dialog": "1.1.23", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-aspect-ratio": "1.1.15", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-checkbox": "1.3.11", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-context-menu": "2.3.7", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-form": "0.1.16", + "@radix-ui/react-hover-card": "1.1.23", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-menubar": "1.1.24", + "@radix-ui/react-navigation-menu": "1.2.22", + "@radix-ui/react-one-time-password-field": "0.1.16", + "@radix-ui/react-password-toggle-field": "0.1.11", + "@radix-ui/react-popover": "1.1.23", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-progress": "1.1.16", + "@radix-ui/react-radio-group": "1.4.7", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-scroll-area": "1.2.18", + "@radix-ui/react-select": "2.3.7", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slider": "1.4.7", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-switch": "1.3.7", + "@radix-ui/react-tabs": "1.1.21", + "@radix-ui/react-toast": "1.2.23", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-toggle-group": "1.1.19", + "@radix-ui/react-toolbar": "1.1.19", + "@radix-ui/react-tooltip": "1.2.16", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-escape-keydown": "1.1.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", diff --git a/package.json b/package.json index cdc1d4ee..d6bdccc1 100644 --- a/package.json +++ b/package.json @@ -74,21 +74,21 @@ "@hookform/resolvers": "5.4.0", "@joinmarket-webui/joinmarket-ng-api-ts": "1.0.0", "@noble/hashes": "2.2.0", - "@radix-ui/react-accordion": "1.2.16", - "@radix-ui/react-avatar": "1.2.2", - "@radix-ui/react-checkbox": "1.3.7", - "@radix-ui/react-dialog": "1.1.19", - "@radix-ui/react-dropdown-menu": "2.1.20", - "@radix-ui/react-label": "2.1.11", - "@radix-ui/react-popover": "1.1.19", - "@radix-ui/react-radio-group": "1.4.3", - "@radix-ui/react-select": "2.3.3", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-slider": "1.4.3", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-switch": "1.3.3", - "@radix-ui/react-tabs": "1.1.17", - "@radix-ui/react-tooltip": "1.2.12", + "@radix-ui/react-accordion": "1.2.20", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-checkbox": "1.3.11", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-popover": "1.1.23", + "@radix-ui/react-radio-group": "1.4.7", + "@radix-ui/react-select": "2.3.7", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slider": "1.4.7", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-switch": "1.3.7", + "@radix-ui/react-tabs": "1.1.21", + "@radix-ui/react-tooltip": "1.2.16", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "@tailwindcss/vite": "4.3.3", @@ -103,7 +103,7 @@ "lucide-react": "1.26.0", "next-themes": "0.4.6", "qrcode": "1.5.4", - "radix-ui": "1.6.2", + "radix-ui": "1.6.7", "react": "19.2.8", "react-dom": "19.2.8", "react-hook-form": "7.83.0", From c41b40bec9e9d458270cf67f8492375bd3360c36 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sun, 26 Jul 2026 15:32:15 +0200 Subject: [PATCH 05/56] dev(regtest): fund jar 0, 1 and 4 of dev wallet --- docker/regtest/init-setup.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/regtest/init-setup.sh b/docker/regtest/init-setup.sh index a5cd53e1..b1f7e03e 100755 --- a/docker/regtest/init-setup.sh +++ b/docker/regtest/init-setup.sh @@ -16,7 +16,9 @@ set -Eeuo pipefail script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) # fund wallet in primary JoinMarket NG container -. "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket --unmatured --blocks 3 +. "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket --mixdepth 0 --unmatured --blocks 1 +. "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket --mixdepth 1 --unmatured --blocks 2 # outputs should be flagged as "reused" +. "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket --mixdepth 4 --unmatured --blocks 1 # fund wallet in ng standalone secondary container . "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket2 --unmatured --blocks 50 @@ -37,7 +39,7 @@ dummy_wallet_address2='bcrt1qt5yxk3xzrx66q9wd5sdyynklqynqcyf7uh74j3' # 8th addre dummy_wallet_address3='bcrt1qn8804dw5fahuc5cwqteuq5j4xlhk2cnkq7a8kw' # 21st change address of jar E (m/84'/1'/4'/1/21) dummy_wallet_address_fb0='bcrt1q7war3lusq3ez633rzzfkp75unfcgrqqcu80sgp0ellhrdvkzxh5swspygk' # 1st fb address (m/84'/1'/0'/2/0) dummy_wallet_address_fb959='bcrt1q6rlcknptmqhf4w20928tpz6px4y70960gyupd029c7vx7kdjwt0q46nk9c' # 960th fb address (m/84'/1'/0'/2/959) -# make block rewards spendable: 100 + 5 (default of `taker_utxo_age`) + 1 = 106 +# make block rewards spendable: 100 + 5 (default of `taker_utxo_age`) + 1 = at least 106 blocks . "$script_dir/mine-block.sh" 2 "$dummy_wallet_address1" &>/dev/null . "$script_dir/mine-block.sh" 2 "$dummy_wallet_address2" &>/dev/null . "$script_dir/mine-block.sh" 2 "$dummy_wallet_address3" &>/dev/null From 3e7f5ea41fa01131acadfa2d6ef440eb54de290a Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Sun, 26 Jul 2026 16:32:42 +0200 Subject: [PATCH 06/56] build(deps): update node to v26.5.0 (#1353) --- .github/workflows/build.yml | 2 +- .nvmrc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01fa7889..1d8095b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: ['v26.3.0'] + node-version: ['v26.5.0'] steps: - name: Checkout diff --git a/.nvmrc b/.nvmrc index 84a2f4a2..eacd4817 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v26.3.0 +v26.5.0 From 8789e637985de5f1bbb5683cbb804b450815debe Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Sun, 26 Jul 2026 17:44:44 +0200 Subject: [PATCH 07/56] chore: raise coverage thresholds (#1356) --- vitest.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index edb3a027..01666cb5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -29,10 +29,10 @@ export default defineConfig((args: ConfigEnv): ViteUserConfig => { 'src/main.tsx', ], thresholds: { - lines: 80, - functions: 80, + lines: 85, + functions: 85, branches: 80, - statements: 80, + statements: 85, }, }, projects: [ From 82cc4762ff84a689f4ac71f4afd801b10bb4a4e7 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 27 Jul 2026 00:33:31 +0530 Subject: [PATCH 08/56] fix(sweep): validate destination addresses against wallet network (#1355) --- src/components/sweep/SweepForm.tsx | 6 ++++- src/components/sweep/SweepFormSchema.test.ts | 25 +++++++++++++++++++- src/components/sweep/SweepFormSchema.ts | 22 ++++++++++------- src/components/sweep/SweepPage.test.tsx | 1 + src/components/sweep/SweepPage.tsx | 4 +++- src/i18n/locales/en/translation.json | 1 + src/lib/formValidation.test.ts | 15 ++++++++++++ src/lib/formValidation.ts | 12 ++++++++-- 8 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/components/sweep/SweepForm.tsx b/src/components/sweep/SweepForm.tsx index 6a763fff..3a86088b 100644 --- a/src/components/sweep/SweepForm.tsx +++ b/src/components/sweep/SweepForm.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react' import { yupResolver } from '@hookform/resolvers/yup' +import type { Network } from 'bitcoin-address-validation' import { AlertTriangleIcon } from 'lucide-react' import { useFieldArray, useForm, useWatch, type SubmitHandler } from 'react-hook-form' import { useTranslation } from 'react-i18next' @@ -53,6 +54,7 @@ interface SweepFormProps { initialValues?: Partial jars: Jar[] addressSummary: AddressSummary + network: Network disabled?: boolean debug?: boolean } @@ -62,6 +64,7 @@ export const SweepForm = ({ onSubmit, jars, addressSummary, + network, initialValues, disabled, debug, @@ -82,10 +85,11 @@ export const SweepForm = ({ minNumberOfDestinations, maxNumberOfDestinations, addressSummary, + network, }, t, ), - [minNumberOfDestinations, maxNumberOfDestinations, addressSummary, t], + [minNumberOfDestinations, maxNumberOfDestinations, addressSummary, network, t], ) const { formState, reset, register, control, setValue, handleSubmit, trigger } = useForm< SweepFormValues, diff --git a/src/components/sweep/SweepFormSchema.test.ts b/src/components/sweep/SweepFormSchema.test.ts index a3094edc..214f6974 100644 --- a/src/components/sweep/SweepFormSchema.test.ts +++ b/src/components/sweep/SweepFormSchema.test.ts @@ -1,3 +1,4 @@ +import { Network } from 'bitcoin-address-validation' import type { TFunction } from 'i18next' import { describe, expect, it } from 'vitest' import type { AddressSummary } from '@/context/JamWalletInfoContext' @@ -12,13 +13,19 @@ import { const t = ((key: string) => key) as unknown as TFunction<'translation', undefined> const validRegtestAddress = 'bcrt1qrnz0thqslhxu86th069r9j6y7ldkgs2tzgf5wx' +const validMainnetAddress = '1BoatSLRHtKNngkdXEeobR76b53LETtpyT' -const validate = async (values: SweepFormValues, addressSummary = {} as AddressSummary) => { +const validate = async ( + values: SweepFormValues, + addressSummary = {} as AddressSummary, + network: Network = Network.regtest, +) => { return await sweepFormSchema( { minNumberOfDestinations: 1, maxNumberOfDestinations: 5, addressSummary, + network, }, t, ).validate(values, { abortEarly: false }) @@ -49,6 +56,22 @@ describe('sweepFormSchema', () => { }) }) + it('rejects an address from the wrong network', async () => { + await expect( + validate({ + ...buildSweepFormValuesDefaultValues(), + destinations: [{ address: validMainnetAddress }], + }), + ).rejects.toMatchObject({ + inner: [ + expect.objectContaining({ + path: 'destinations[0].address', + message: 'scheduler.feedback_destination_network_mismatch', + }), + ], + }) + }) + it('rejects duplicate destination addresses', async () => { await expect( validate({ diff --git a/src/components/sweep/SweepFormSchema.ts b/src/components/sweep/SweepFormSchema.ts index e2ff18e0..380ccdb4 100644 --- a/src/components/sweep/SweepFormSchema.ts +++ b/src/components/sweep/SweepFormSchema.ts @@ -1,3 +1,4 @@ +import type { Network } from 'bitcoin-address-validation' import type { TFunction } from 'i18next' import * as yup from 'yup' import { @@ -15,7 +16,7 @@ import { } from '@/constants/jam' import { JM_NG_DEFAULT_TUMBLER_PARAMS, type TumblerParameters } from '@/constants/jm' import type { AddressSummary } from '@/context/JamWalletInfoContext' -import { isValidAddress } from '@/lib/formValidation' +import { isAddressOnNetwork, isValidAddress } from '@/lib/formValidation' import { factorToPercentage, isValidNumber, percentageToFactor, pseudoRandomInteger } from '@/lib/utils' import type { Seconds } from '@/types/global' import { buildDestinationErrors, normalizeDestinationAddresses } from './destinationValidation' @@ -115,14 +116,17 @@ export const sweepFormSchema = ( minNumberOfDestinations, maxNumberOfDestinations, addressSummary, + network, }: { minNumberOfDestinations: number maxNumberOfDestinations: number addressSummary: AddressSummary + network: Network }, t: TFunction<'translation', undefined>, ): yup.ObjectSchema => { const invalidDestinationAddressMessage = t('scheduler.feedback_invalid_destination_address') + const networkMismatchDestinationAddressMessage = t('scheduler.feedback_destination_network_mismatch') const invalidNumberOfDestinationsMessage = t('send.feedback_invalid_number_of_destination_addresses', { // TODO: i18n defaultValue: 'Please provide between {{ min }} and {{ max }} destination addresses.', @@ -137,20 +141,20 @@ export const sweepFormSchema = ( .of( yup .object({ - // TODO: use formValidation#destinationAddressField ? address: yup .string() .transform((_, originalValue: unknown) => typeof originalValue === 'string' ? normalizeDestinationAddresses([originalValue])[0] : '', ) .defined() - .test('valid-sweep-destination', invalidDestinationAddressMessage, function (value) { - if (!isValidAddress(value)) { - return false - } - - return true - }), + .test('valid-sweep-destination', invalidDestinationAddressMessage, (value) => isValidAddress(value)) + // Only run once the address itself is valid, so an invalid address surfaces a + // single, correct error instead of also reporting a network mismatch. + .test( + 'sweep-destination-network-mismatch', + networkMismatchDestinationAddressMessage, + (value) => !isValidAddress(value) || isAddressOnNetwork(value, network), + ), }) .required(), ) diff --git a/src/components/sweep/SweepPage.test.tsx b/src/components/sweep/SweepPage.test.tsx index 7383cedf..b290d6ea 100644 --- a/src/components/sweep/SweepPage.test.tsx +++ b/src/components/sweep/SweepPage.test.tsx @@ -264,6 +264,7 @@ vi.mock('@/components/ui/jam/PageLoading', () => ({ vi.mock('@/context/JamWalletInfoContext', () => ({ useJamWalletInfoContext: () => mocks.walletInfo, + useDetectNetwork: () => ({ network: 'regtest' }), })) vi.mock('@/hooks/useApiClient', () => ({ diff --git a/src/components/sweep/SweepPage.tsx b/src/components/sweep/SweepPage.tsx index 3e8ba2a1..d12bfa81 100644 --- a/src/components/sweep/SweepPage.tsx +++ b/src/components/sweep/SweepPage.tsx @@ -28,7 +28,7 @@ import PageTitle from '@/components/ui/jam/PageTitle' import { isDevMode } from '@/constants/debugFeatures' import type { TumblerParameters } from '@/constants/jm' import { useJamSessionInfoContext } from '@/context/JamSessionInfoContext' -import { useJamWalletInfoContext } from '@/context/JamWalletInfoContext' +import { useDetectNetwork, useJamWalletInfoContext } from '@/context/JamWalletInfoContext' import { useApiClient } from '@/hooks/useApiClient' import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation' import { useRefreshSession } from '@/hooks/useRefreshSession' @@ -65,6 +65,7 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { const { rescanInfo, takerInfo, makerInfo } = useJamSessionInfoContext() const jmSession = useStore(jmSessionStore, (state) => state.state) const walletInfo = useJamWalletInfoContext() + const { network } = useDetectNetwork() const { enabled: isDeveloperMode } = useDeveloperMode() const [showFeeConfigDialog, setShowFeeConfigDialog] = useState(false) @@ -452,6 +453,7 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 1a2d3716..d9bf7c88 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -783,6 +783,7 @@ "complete_wallet_subtitle": "This will use all of your non-frozen funds.", "description_destination_addresses": "A Scheduled Sweep will send all available funds to multiple destinations, splitting them up in random chunks.", "feedback_invalid_destination_address": "Please enter a valid destination address.", + "feedback_destination_network_mismatch": "$t(send.feedback_destination_network_mismatch)", "feedback_reused_destination_address": "This address is already used. To preserve your privacy please choose another one.", "label_destination_input": "Destination {{ destination }}", "placeholder_destination_input": "Enter destination address...", diff --git a/src/lib/formValidation.test.ts b/src/lib/formValidation.test.ts index 32f80763..40369411 100644 --- a/src/lib/formValidation.test.ts +++ b/src/lib/formValidation.test.ts @@ -11,6 +11,8 @@ import { const mainnetAddress = '1BoatSLRHtKNngkdXEeobR76b53LETtpyT' const testnetAddress = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn' +const regtestBech32Address = 'bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk' +const regtestLegacyAddressLabeledTestnet = 'mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV' const addressSummary = { [mainnetAddress]: { address: mainnetAddress, used: false }, @@ -36,6 +38,19 @@ describe('isAddressOnNetwork', () => { it('returns false for unparseable input instead of throwing', () => { expect(isAddressOnNetwork('not-an-address', Network.mainnet)).toBe(false) }) + + it('treats testnet and regtest as interchangeable for base58 addresses ambiguous between the two', () => { + // bech32 regtest addresses are unambiguous and match regtest directly... + expect(isAddressOnNetwork(regtestBech32Address, Network.regtest)).toBe(true) + expect(isAddressOnNetwork(regtestBech32Address, Network.testnet)).toBe(true) // TODO: can be detected and differentiated for `p2wpkh` + // ...but a legacy address on a regtest wallet is labeled "testnet" by the library, so it + // must still be accepted when the wallet's detected network is regtest. + expect(isAddressOnNetwork(regtestLegacyAddressLabeledTestnet, Network.regtest)).toBe(true) + expect(isAddressOnNetwork(regtestLegacyAddressLabeledTestnet, Network.testnet)).toBe(true) + // mainnet is never ambiguous with testnet/regtest. + expect(isAddressOnNetwork(mainnetAddress, Network.regtest)).toBe(false) + expect(isAddressOnNetwork(regtestBech32Address, Network.mainnet)).toBe(false) + }) }) describe('isReusedAddress', () => { diff --git a/src/lib/formValidation.ts b/src/lib/formValidation.ts index 2da65b3e..591e68f1 100644 --- a/src/lib/formValidation.ts +++ b/src/lib/formValidation.ts @@ -1,4 +1,4 @@ -import { getAddressInfo, validate as isValidBitcoinAddress, type Network } from 'bitcoin-address-validation' +import { getAddressInfo, Network, validate as isValidBitcoinAddress } from 'bitcoin-address-validation' import * as yup from 'yup' import type { AddressSummary } from '@/context/JamWalletInfoContext' import type { BitcoinAddress, BlockHeight, JarIndex } from '@/types/global' @@ -11,9 +11,17 @@ import { isValidInteger } from './utils' export const isValidAddress = (value: unknown): value is BitcoinAddress => typeof value === 'string' && isValidBitcoinAddress(value) +// Legacy (base58) addresses share the same version bytes on testnet and regtest, so +// bitcoin-address-validation can't tell them apart and always labels them "testnet" - +// only bech32 addresses carry a distinct "bcrt1" prefix. Treat the two as interchangeable +// so a regtest wallet doesn't reject its own legacy-style addresses as "wrong network". +const AMBIGUOUS_TESTNET_REGTEST_NETWORKS: ReadonlySet = new Set([Network.testnet, Network.regtest]) + export const isAddressOnNetwork = (value: string, network: Network): boolean => { try { - return getAddressInfo(value).network === network + const addressNetwork = getAddressInfo(value).network + if (addressNetwork === network) return true + return AMBIGUOUS_TESTNET_REGTEST_NETWORKS.has(addressNetwork) && AMBIGUOUS_TESTNET_REGTEST_NETWORKS.has(network) } catch (_ignoredOnPurpose) { return false } From fc1a02cead8c2dc63a4a0b7f0d7e4ce29041fccf Mon Sep 17 00:00:00 2001 From: Parth <143504541+parrth20@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:17:04 +0530 Subject: [PATCH 09/56] refactor(send): standardize jar selector form handling (#1357) * refactor(send): add jar selector form schema * refactor(send): use react-hook-form for jar selection --- .../send/JarSelectorDialog.schema.test.ts | 14 ++ .../send/JarSelectorDialog.schema.ts | 15 ++ .../send/JarSelectorDialog.test.tsx | 13 +- src/components/send/JarSelectorDialog.tsx | 142 +++++++++++------- 4 files changed, 123 insertions(+), 61 deletions(-) create mode 100644 src/components/send/JarSelectorDialog.schema.test.ts create mode 100644 src/components/send/JarSelectorDialog.schema.ts diff --git a/src/components/send/JarSelectorDialog.schema.test.ts b/src/components/send/JarSelectorDialog.schema.test.ts new file mode 100644 index 00000000..245a483c --- /dev/null +++ b/src/components/send/JarSelectorDialog.schema.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { createJarSelectorDialogFormSchema } from './JarSelectorDialog.schema' + +describe('createJarSelectorDialogFormSchema', () => { + const schema = createJarSelectorDialogFormSchema([0, 2]) + + it('accepts a selectable jar index', async () => { + await expect(schema.validate({ jarIndex: 2 })).resolves.toEqual({ jarIndex: 2 }) + }) + + it.each([{ jarIndex: 1 }, { jarIndex: undefined }])('rejects an unavailable jar index', async (values) => { + await expect(schema.isValid(values)).resolves.toBe(false) + }) +}) diff --git a/src/components/send/JarSelectorDialog.schema.ts b/src/components/send/JarSelectorDialog.schema.ts new file mode 100644 index 00000000..65acf39c --- /dev/null +++ b/src/components/send/JarSelectorDialog.schema.ts @@ -0,0 +1,15 @@ +import * as yup from 'yup' +import type { JarIndex } from '@/types/global' + +export type JarSelectorDialogFormValues = { + jarIndex: JarIndex +} + +export const createJarSelectorDialogFormSchema = ( + selectableJarIndexes: JarIndex[], +): yup.ObjectSchema => + yup + .object({ + jarIndex: yup.number().oneOf(selectableJarIndexes).required(), + }) + .required() diff --git a/src/components/send/JarSelectorDialog.test.tsx b/src/components/send/JarSelectorDialog.test.tsx index d0527787..ca199a44 100644 --- a/src/components/send/JarSelectorDialog.test.tsx +++ b/src/components/send/JarSelectorDialog.test.tsx @@ -20,7 +20,7 @@ vi.mock('../ui/jam/SelectableJar', () => ({ disabled?: boolean isSelected?: boolean }) => ( - ), @@ -94,20 +94,23 @@ describe('JarSelectorDialog', () => { expect(screen.queryByText('Pick a jar')).not.toBeInTheDocument() }) - it('disables the confirm button until a jar is selected', () => { + it('disables the confirm button until a jar is selected', async () => { renderDialog() - const confirm = screen.getByText('modal.confirm_button_accept').closest('button') + const confirm = screen.getByRole('button', { name: 'modal.confirm_button_accept' }) expect(confirm).toBeDisabled() fireEvent.click(screen.getByText('Jar 0')) - expect(confirm).not.toBeDisabled() + await waitFor(() => expect(confirm).not.toBeDisabled()) }) it('confirms the selected jar', async () => { const onConfirm = vi.fn().mockResolvedValue(undefined) renderDialog({ onConfirm }) fireEvent.click(screen.getByText('Jar 0')) - fireEvent.click(screen.getByText('modal.confirm_button_accept')) + const confirm = screen.getByRole('button', { name: 'modal.confirm_button_accept' }) + await waitFor(() => expect(confirm).not.toBeDisabled()) + fireEvent.click(confirm) await waitFor(() => expect(onConfirm).toHaveBeenCalledWith(0)) + await waitFor(() => expect(confirm).toBeDisabled()) }) it('closes and resets via the reject button', () => { diff --git a/src/components/send/JarSelectorDialog.tsx b/src/components/send/JarSelectorDialog.tsx index 1ae72b40..27b15d64 100644 --- a/src/components/send/JarSelectorDialog.tsx +++ b/src/components/send/JarSelectorDialog.tsx @@ -1,4 +1,6 @@ -import { useState, type ComponentProps } from 'react' +import { useMemo, type ComponentProps } from 'react' +import { yupResolver } from '@hookform/resolvers/yup' +import { useForm, useWatch } from 'react-hook-form' import { useTranslation } from 'react-i18next' import type { Jar } from '@/context/JamWalletInfoContext' import type { BalanceSummary } from '@/lib/balanceSummary' @@ -7,6 +9,7 @@ import { Button } from '../ui/button' import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogFooter, DialogHeader } from '../ui/dialog' import { SelectableJar } from '../ui/jam/SelectableJar' import { Spinner } from '../ui/spinner' +import { createJarSelectorDialogFormSchema, type JarSelectorDialogFormValues } from './JarSelectorDialog.schema' type JarSelectorDialogProps = WithRequiredProperty< Omit, 'children'>, @@ -33,73 +36,100 @@ export default function JarSelectorDialog({ }: JarSelectorDialogProps) { const { t } = useTranslation() - const [isConfirming, setIsConfirming] = useState(false) - const [selectedJar, setSelectedJar] = useState() + const selectableJarIndexes = useMemo( + () => jars.filter((jar) => !disabledJars.includes(jar)).map((jar) => jar.jarIndex), + [disabledJars, jars], + ) + const schema = useMemo(() => createJarSelectorDialogFormSchema(selectableJarIndexes), [selectableJarIndexes]) + const { + control, + handleSubmit, + reset, + setValue, + formState: { isSubmitting }, + } = useForm({ + mode: 'onChange', + defaultValues: { + jarIndex: undefined, + }, + resolver: yupResolver(schema), + }) + const selectedJarIndex = useWatch({ control, name: 'jarIndex' }) + const selectedJar = jars.find((jar) => jar.jarIndex === selectedJarIndex) + const canConfirm = + selectedJarIndex !== undefined && selectedJar !== undefined && selectableJarIndexes.includes(selectedJarIndex) const handleClose = () => { - setSelectedJar(undefined) + reset() onOpenChange(false) } - const confirm = async () => { - if (selectedJar === undefined) return - - setIsConfirming(true) - try { - await onConfirm(selectedJar.jarIndex) - setSelectedJar(undefined) - } finally { - setIsConfirming(false) - } - } + const doOnSubmit = handleSubmit(async ({ jarIndex }) => { + await onConfirm(jarIndex) + reset() + }) return ( - - {title} - {subtitle && {subtitle}} - +
void doOnSubmit(event)} className="grid gap-4" noValidate> + + {title} + {subtitle && {subtitle}} + -
-
- {jars.map((jar, index) => ( - setSelectedJar(jar)} - disabled={disabledJars.includes(jar)} - /> - ))} +
+
+ {jars.map((jar) => ( + + setValue('jarIndex', jar.jarIndex, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + } + disabled={disabledJars.includes(jar)} + /> + ))} +
-
- - - - + + + + +
) From 01a952ff63598c8fab00c851bdb1e1ebcf9141aa Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Mon, 27 Jul 2026 11:18:12 +0200 Subject: [PATCH 10/56] refactor: improve balance summary calculation (#1350) * chore: store bonded balance in BalanceSummary * refactor: improve balance summary calculation --- src/context/JamWalletInfoContextProvider.tsx | 9 +-- src/lib/balanceSummary.test.ts | 81 +++++++++++++++++--- src/lib/balanceSummary.ts | 45 +++++++---- 3 files changed, 102 insertions(+), 33 deletions(-) diff --git a/src/context/JamWalletInfoContextProvider.tsx b/src/context/JamWalletInfoContextProvider.tsx index 1ae1d21a..430bfa21 100644 --- a/src/context/JamWalletInfoContextProvider.tsx +++ b/src/context/JamWalletInfoContextProvider.tsx @@ -5,7 +5,7 @@ import { CancelledError, useMutation, useQuery } from '@tanstack/react-query' import { getAddressInfo } from 'bitcoin-address-validation' import { useQueryDisplayWallet, type WalletInfoApiObject } from '@/hooks/useQueryDisplayWallet' import { useQueryUtxos, type Utxo, type UtxoId } from '@/hooks/useQueryUtxos' -import { toBalanceSummary } from '@/lib/balanceSummary' +import { BALANCE_SUMMARY_EMPTY, toBalanceSummary } from '@/lib/balanceSummary' import * as fb from '@/lib/fidelityBondUtils' import { delayedPromise, walletDisplayName, type WalletFileName } from '@/lib/utils' import type { JarIndex, Milliseconds } from '@/types/global' @@ -107,7 +107,6 @@ type UtxosByJarIndex = ByJarIndex type JarTemplateByJarIndex = ByJarIndex const EMPTY_UTXOS: Utxo[] = [] -const EMPTY_BALANCE_SUMMARY = toBalanceSummary(EMPTY_UTXOS) type JarTemplate = Pick const jarTemplates: JarTemplate[] = [ @@ -184,7 +183,7 @@ export const JamWalletInfoContextProvider = ({ if (!jarPresent) { jars.push({ ...jarTemplate, - balanceSummary: EMPTY_BALANCE_SUMMARY, + balanceSummary: BALANCE_SUMMARY_EMPTY, utxos: EMPTY_UTXOS, }) } @@ -271,13 +270,13 @@ export const JamWalletInfoContextProvider = ({ // Delaying the poll requests gives the wallet some time to synchronize // the utxo set and reduces amount of http requests -const WAIT_FOR_UTXOS_TO_BE_SPENT_DEFAUL_DELAY: Milliseconds = 1_000 +const WAIT_FOR_UTXOS_TO_BE_SPENT_DEFAULT_DELAY: Milliseconds = 1_000 const useWaitForUtxosToBeSpent = ({ refetch, walletFileName, utxos, - delay = WAIT_FOR_UTXOS_TO_BE_SPENT_DEFAUL_DELAY, + delay = WAIT_FOR_UTXOS_TO_BE_SPENT_DEFAULT_DELAY, resetOnErrors = true, }: { refetch: (options?: WalletRefetchOptions) => Promise diff --git a/src/lib/balanceSummary.test.ts b/src/lib/balanceSummary.test.ts index b7e1f620..3c2449c1 100644 --- a/src/lib/balanceSummary.test.ts +++ b/src/lib/balanceSummary.test.ts @@ -5,7 +5,7 @@ import { toBalanceSummary } from './balanceSummary' const now = Date.UTC(2009, 0, 3) describe('BalanceSummary', () => { - it('should populate balance properties calculated from utxo data', () => { + it('should calculate balances from utxo data', () => { const balanceSummary = toBalanceSummary( [ { @@ -40,14 +40,15 @@ describe('BalanceSummary', () => { ], now, ) - - expect(balanceSummary.calculatedTotalBalanceInSats).toBe(11) - expect(balanceSummary.calculatedAvailableBalanceInSats).toBe(6) - expect(balanceSummary.calculatedConfirmedAvailableBalanceInSats).toBe(1) - expect(balanceSummary.calculatedFrozenOrLockedBalanceInSats).toBe(5) + expect(balanceSummary).toEqual({ + calculatedTotalBalanceInSats: 11, + calculatedAvailableBalanceInSats: 6, + calculatedConfirmedAvailableBalanceInSats: 1, + calculatedFrozenOrLockedBalanceInSats: 5, + }) }) - it('should populate account balance data', () => { + it('should calculate balances from utxo data (bondless)', () => { const balanceSummary = toBalanceSummary( [ { @@ -71,13 +72,71 @@ describe('BalanceSummary', () => { confirmations: 0, frozen: true, } as Utxo, + { + value: 0, + mixdepth: 4, + confirmations: 1, + frozen: true, + } as Utxo, ], now, ) - expect(balanceSummary.calculatedTotalBalanceInSats).toBe(677777777) - expect(balanceSummary.calculatedAvailableBalanceInSats).toBe(333333333) - expect(balanceSummary.calculatedConfirmedAvailableBalanceInSats).toBe(111111111) - expect(balanceSummary.calculatedFrozenOrLockedBalanceInSats).toBe(344444444) + expect(balanceSummary).toEqual({ + calculatedTotalBalanceInSats: 677777777, + calculatedAvailableBalanceInSats: 333333333, + calculatedConfirmedAvailableBalanceInSats: 111111111, + calculatedFrozenOrLockedBalanceInSats: 344444444, + }) + }) + + it('should calculate balances from utxo data (bonds only)', () => { + const balanceSummary = toBalanceSummary( + [ + { + value: 1, + mixdepth: 0, + confirmations: 1, + frozen: true, + // frozen and not yet expired + locktime: '2999-12-01 00:00:00', + path: `m/84'/1'/0'/0/2:${now / 1_000 + 1}`, + } as Utxo, + { + value: 2, + mixdepth: 0, + frozen: false, + // unfrozen but not yet expired + locktime: '2999-12-01 00:00:00', + path: `m/84'/1'/0'/0/2:${now / 1_000 + 1}`, + } as Utxo, + { + value: 3, + mixdepth: 0, + confirmations: 1, + // frozen and expired + frozen: true, + locktime: '2009-01-01 00:00:00', + path: `m/84'/1'/0'/0/2:${now / 1_000 - 1}`, + } as Utxo, + { + value: 5, + mixdepth: 0, + confirmations: 0, + // unfrozen and expired + frozen: false, + locktime: '2009-01-01 00:00:00', + path: `m/84'/1'/0'/0/2:${now / 1_000 - 1}`, + } as Utxo, + ], + now, + ) + + expect(balanceSummary).toEqual({ + calculatedTotalBalanceInSats: 11, + calculatedAvailableBalanceInSats: 5, + calculatedConfirmedAvailableBalanceInSats: 0, + calculatedFrozenOrLockedBalanceInSats: 6, + }) }) }) diff --git a/src/lib/balanceSummary.ts b/src/lib/balanceSummary.ts index 84bf3e95..b0e65111 100644 --- a/src/lib/balanceSummary.ts +++ b/src/lib/balanceSummary.ts @@ -22,25 +22,36 @@ export type BalanceSummary = { calculatedFrozenOrLockedBalanceInSats: AmountSats } -const calculateFrozenOrLockedBalance = (utxos: Utxo[], refTime: Milliseconds = Date.now()) => { - const frozenOrLockedUtxos = utxos.filter((utxo) => utxo.frozen || fb.utxo.isLocked(utxo, refTime)) - return frozenOrLockedUtxos.reduce((acc, utxo) => acc + utxo.value, 0) +export const BALANCE_SUMMARY_EMPTY: BalanceSummary = { + calculatedTotalBalanceInSats: 0, + calculatedAvailableBalanceInSats: 0, + calculatedConfirmedAvailableBalanceInSats: 0, + calculatedFrozenOrLockedBalanceInSats: 0, } export const toBalanceSummary = (utxos: Utxo[], now?: Milliseconds): BalanceSummary => { const refTime = now !== undefined ? now : Date.now() - - const walletTotalCalculated: AmountSats = utxos.reduce((acc, utxo) => acc + utxo.value, 0) - const walletFrozenOrLockedCalculated: AmountSats = calculateFrozenOrLockedBalance(utxos, refTime) - const walletAvailableCalculated = walletTotalCalculated - walletFrozenOrLockedCalculated - const walletConfirmedAvailableCalculated = utxos - .filter((utxo) => !utxo.frozen && !fb.utxo.isLocked(utxo, refTime) && utxo.confirmations > 0) - .reduce((acc, utxo) => acc + utxo.value, 0) - - return { - calculatedTotalBalanceInSats: walletTotalCalculated, - calculatedFrozenOrLockedBalanceInSats: walletFrozenOrLockedCalculated, - calculatedAvailableBalanceInSats: walletAvailableCalculated, - calculatedConfirmedAvailableBalanceInSats: walletConfirmedAvailableCalculated, - } + return utxos + .map((utxo) => { + const isFidelityBond = fb.utxo.isFidelityBond(utxo) + const frozenOrLocked = utxo.frozen || (isFidelityBond && fb.utxo.isLocked(utxo, refTime)) + const available = !frozenOrLocked + return { + total: utxo.value, + available: available ? utxo.value : 0, + confirmedAvailable: available && utxo.confirmations > 0 ? utxo.value : 0, + frozenOrLocked: frozenOrLocked ? utxo.value : 0, + bond: fb.utxo.isFidelityBond(utxo) ? utxo.value : 0, + } + }) + .reduce( + (acc, info) => ({ + calculatedTotalBalanceInSats: acc.calculatedTotalBalanceInSats + info.total, + calculatedAvailableBalanceInSats: acc.calculatedAvailableBalanceInSats + info.available, + calculatedConfirmedAvailableBalanceInSats: + acc.calculatedConfirmedAvailableBalanceInSats + info.confirmedAvailable, + calculatedFrozenOrLockedBalanceInSats: acc.calculatedFrozenOrLockedBalanceInSats + info.frozenOrLocked, + }), + BALANCE_SUMMARY_EMPTY, + ) } From 0e24250e3d8fc1ab3389f540c146228ddf8cba23 Mon Sep 17 00:00:00 2001 From: Parth <143504541+parrth20@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:27:08 +0530 Subject: [PATCH 11/56] refactor: standardize remaining dialog forms (#1358) * refactor(earn): standardize move-to-jar form * refactor(earn): standardize bond renewal form * refactor(send): standardize utxo selection form * chore(settings): remove stale form migration todo * refactor(earn): rely on form confirmation default --- .../earn/MoveToJarDialog.schema.test.ts | 17 +++++ src/components/earn/MoveToJarDialog.schema.ts | 13 ++++ src/components/earn/MoveToJarDialog.tsx | 44 ++++++++--- .../earn/RenewBondDialog.schema.test.ts | 16 ++++ src/components/earn/RenewBondDialog.schema.ts | 19 +++++ src/components/earn/RenewBondDialog.tsx | 53 +++++++++++--- .../send/UtxoSelectionDialog.schema.test.ts | 22 ++++++ .../send/UtxoSelectionDialog.schema.ts | 32 ++++++++ src/components/send/UtxoSelectionDialog.tsx | 71 ++++++++++-------- src/components/settings/SeedPhraseDialog.tsx | 1 - src/hooks/useUtxoSelectionDialog.ts | 73 +++++++++++++++---- 11 files changed, 295 insertions(+), 66 deletions(-) create mode 100644 src/components/earn/MoveToJarDialog.schema.test.ts create mode 100644 src/components/earn/MoveToJarDialog.schema.ts create mode 100644 src/components/earn/RenewBondDialog.schema.test.ts create mode 100644 src/components/earn/RenewBondDialog.schema.ts create mode 100644 src/components/send/UtxoSelectionDialog.schema.test.ts create mode 100644 src/components/send/UtxoSelectionDialog.schema.ts diff --git a/src/components/earn/MoveToJarDialog.schema.test.ts b/src/components/earn/MoveToJarDialog.schema.test.ts new file mode 100644 index 00000000..efa65a6a --- /dev/null +++ b/src/components/earn/MoveToJarDialog.schema.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' +import { createMoveToJarFormSchema } from './MoveToJarDialog.schema' + +describe('createMoveToJarFormSchema', () => { + const schema = createMoveToJarFormSchema([0, 2]) + + it('accepts an available destination jar', async () => { + await expect(schema.validate({ destinationJarIndex: 2 })).resolves.toEqual({ destinationJarIndex: 2 }) + }) + + it.each([{ destinationJarIndex: 1 }, { destinationJarIndex: undefined }])( + 'rejects an unavailable destination jar', + async (values) => { + await expect(schema.isValid(values)).resolves.toBe(false) + }, + ) +}) diff --git a/src/components/earn/MoveToJarDialog.schema.ts b/src/components/earn/MoveToJarDialog.schema.ts new file mode 100644 index 00000000..a14aa72a --- /dev/null +++ b/src/components/earn/MoveToJarDialog.schema.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup' +import type { JarIndex } from '@/types/global' + +export type MoveToJarFormValues = { + destinationJarIndex: JarIndex +} + +export const createMoveToJarFormSchema = (availableJarIndexes: JarIndex[]): yup.ObjectSchema => + yup + .object({ + destinationJarIndex: yup.number().oneOf(availableJarIndexes).required(), + }) + .required() diff --git a/src/components/earn/MoveToJarDialog.tsx b/src/components/earn/MoveToJarDialog.tsx index 7b7234c0..2175f706 100644 --- a/src/components/earn/MoveToJarDialog.tsx +++ b/src/components/earn/MoveToJarDialog.tsx @@ -1,8 +1,10 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' +import { yupResolver } from '@hookform/resolvers/yup' import { getaddressOptions } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query' import type { DirectSendResponse } from '@joinmarket-webui/joinmarket-ng-api-ts/jm' import { useQuery } from '@tanstack/react-query' import { AlertTriangleIcon, UnlockIcon } from 'lucide-react' +import { useForm, useWatch } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Alert, AlertTitle } from '@/components/ui/alert' @@ -11,7 +13,7 @@ import { useJamWalletInfoContext } from '@/context/JamWalletInfoContext' import { useApiClient } from '@/hooks/useApiClient' import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos' import type { WalletFileName } from '@/lib/utils' -import type { JarIndex } from '@/types/global' +import { createMoveToJarFormSchema, type MoveToJarFormValues } from './MoveToJarDialog.schema' import { FidelityBondDialogLayout } from './fidelity-bond/FidelityBondDialogLayout' import { AddressPreview, @@ -45,9 +47,25 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo const walletInfo = useJamWalletInfoContext() const [step, setStep] = useState('select_jar') - const [selectedJarIndex, setSelectedJarIndex] = useState() const [txResult, setTxResult] = useState() + const availableJarIndexes = useMemo(() => walletInfo.jars.map((jar) => jar.jarIndex), [walletInfo.jars]) + const formSchema = useMemo(() => createMoveToJarFormSchema(availableJarIndexes), [availableJarIndexes]) + const { + control, + handleSubmit, + reset, + setValue, + formState: { isSubmitting }, + } = useForm({ + mode: 'onChange', + defaultValues: { + destinationJarIndex: undefined, + }, + resolver: yupResolver(formSchema), + }) + const selectedJarIndex = useWatch({ control, name: 'destinationJarIndex' }) + const { sweep, isLoading, error, setError, sourceJar } = useFidelityBondSweep({ walletFileName, utxo, @@ -77,7 +95,7 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo const handleReset = () => { setStep('select_jar') - setSelectedJarIndex(undefined) + reset() setTxResult(undefined) setError(undefined) } @@ -89,8 +107,8 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo onOpenChange(newOpen) } - const handleSubmit = async () => { - if (selectedJarIndex === undefined || !destinationAddress) return + const submitMove = handleSubmit(async () => { + if (!destinationAddress) return setStep('sending') const swept = await sweep(destinationAddress, (result) => { @@ -99,7 +117,7 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo toast.success(t('earn.fidelity_bond.move.success_text')) }) if (!swept) setStep('confirm') - } + }) const renderFooter = () => { switch (step) { @@ -117,9 +135,9 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo setStep('select_jar')} onCancel={() => handleOpenChange(false)} - onPrimary={() => void handleSubmit()} + onPrimary={() => void submitMove()} primaryDisabled={!destinationAddress} - isLoading={isLoading} + isLoading={isLoading || isSubmitting} primaryLabel={t('earn.fidelity_bond.move.text_button_submit')} /> ) @@ -150,7 +168,13 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo + setValue('destinationJarIndex', jarIndex, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + } isJarDisabled={() => false} /> diff --git a/src/components/earn/RenewBondDialog.schema.test.ts b/src/components/earn/RenewBondDialog.schema.test.ts new file mode 100644 index 00000000..2053bb0a --- /dev/null +++ b/src/components/earn/RenewBondDialog.schema.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest' +import { renewBondFormSchema } from './RenewBondDialog.schema' + +describe('renewBondFormSchema', () => { + it('accepts a lockdate after confirmation', async () => { + const values = { lockdate: '2030-01' as const, confirmationAccepted: true } + await expect(renewBondFormSchema.validate(values)).resolves.toEqual(values) + }) + + it.each([ + { lockdate: undefined, confirmationAccepted: true }, + { lockdate: '2030-01' as const, confirmationAccepted: false }, + ])('rejects incomplete renewal values', async (values) => { + await expect(renewBondFormSchema.isValid(values)).resolves.toBe(false) + }) +}) diff --git a/src/components/earn/RenewBondDialog.schema.ts b/src/components/earn/RenewBondDialog.schema.ts new file mode 100644 index 00000000..a36649f9 --- /dev/null +++ b/src/components/earn/RenewBondDialog.schema.ts @@ -0,0 +1,19 @@ +import * as yup from 'yup' +import * as fb from '@/lib/fidelityBondUtils' + +export type RenewBondFormValues = { + lockdate?: fb.Lockdate + confirmationAccepted: boolean +} + +export const RENEW_BOND_FORM_DEFAULT_VALUES: RenewBondFormValues = { + lockdate: undefined, + confirmationAccepted: false, +} + +export const renewBondFormSchema: yup.ObjectSchema = yup + .object({ + lockdate: yup.string().required(), + confirmationAccepted: yup.boolean().oneOf([true]).required(), + }) + .required() diff --git a/src/components/earn/RenewBondDialog.tsx b/src/components/earn/RenewBondDialog.tsx index db02845b..68819346 100644 --- a/src/components/earn/RenewBondDialog.tsx +++ b/src/components/earn/RenewBondDialog.tsx @@ -1,8 +1,10 @@ import { useState } from 'react' +import { yupResolver } from '@hookform/resolvers/yup' import { gettimelockaddressOptions } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query' import type { DirectSendResponse } from '@joinmarket-webui/joinmarket-ng-api-ts/jm' import { useQuery } from '@tanstack/react-query' import { AlertTriangleIcon, CalendarIcon, RefreshCwIcon } from 'lucide-react' +import { useForm, useWatch } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' @@ -11,6 +13,7 @@ import { useApiClient } from '@/hooks/useApiClient' import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos' import * as fb from '@/lib/fidelityBondUtils' import type { WalletFileName } from '@/lib/utils' +import { RENEW_BOND_FORM_DEFAULT_VALUES, renewBondFormSchema, type RenewBondFormValues } from './RenewBondDialog.schema' import { FidelityBondDialogLayout } from './fidelity-bond/FidelityBondDialogLayout' import { AddressPreview, @@ -44,10 +47,25 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re const client = useApiClient() const [step, setStep] = useState('select_date') - const [selectedLockdate, setSelectedLockdate] = useState('') - const [confirmationChecked, setConfirmationChecked] = useState(false) const [txResult, setTxResult] = useState() + const { + control, + handleSubmit, + reset, + setValue, + formState: { isSubmitting }, + } = useForm({ + mode: 'onChange', + defaultValues: RENEW_BOND_FORM_DEFAULT_VALUES, + resolver: yupResolver(renewBondFormSchema), + }) + const selectedLockdate = useWatch({ control, name: 'lockdate' }) + const confirmationChecked = useWatch({ + control, + name: 'confirmationAccepted', + }) + const { sweep, isLoading, error, setError, sourceJar } = useFidelityBondSweep({ walletFileName, utxo, @@ -76,8 +94,7 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re const handleReset = () => { setStep('select_date') - setSelectedLockdate('') - setConfirmationChecked(false) + reset(RENEW_BOND_FORM_DEFAULT_VALUES) setTxResult(undefined) setError(undefined) } @@ -89,7 +106,7 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re onOpenChange(newOpen) } - const handleSubmit = async () => { + const submitRenewal = handleSubmit(async () => { if (!destinationAddress) return setStep('sending') @@ -99,7 +116,7 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re toast.success(t('earn.fidelity_bond.renew.success_text')) }) if (!swept) setStep('confirm') - } + }) const renderFooter = () => { switch (step) { @@ -117,9 +134,9 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re setStep('select_date')} onCancel={() => handleOpenChange(false)} - onPrimary={() => void handleSubmit()} + onPrimary={() => void submitRenewal()} primaryDisabled={!confirmationChecked || !destinationAddress} - isLoading={isLoading} + isLoading={isLoading || isSubmitting} primaryLabel={t('earn.fidelity_bond.renew.text_button_submit')} /> ) @@ -148,7 +165,17 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re - + + setValue('lockdate', lockdate || undefined, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + } + /> )} @@ -193,7 +220,13 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re + setValue('confirmationAccepted', checked, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + } /> )} diff --git a/src/components/send/UtxoSelectionDialog.schema.test.ts b/src/components/send/UtxoSelectionDialog.schema.test.ts new file mode 100644 index 00000000..e8b680ce --- /dev/null +++ b/src/components/send/UtxoSelectionDialog.schema.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { createUtxoSelectionFormSchema } from './UtxoSelectionDialog.schema' + +describe('createUtxoSelectionFormSchema', () => { + const schema = createUtxoSelectionFormSchema(['tx-a:0', 'tx-b:1']) + + it('accepts selectable UTXO rows and a filter', async () => { + const values = { + filter: 'confirmed', + rowSelection: { 'tx-a:0': true, 'tx-b:1': false }, + } + await expect(schema.validate(values)).resolves.toEqual(values) + }) + + it('allows clearing the selection', async () => { + await expect(schema.isValid({ filter: '', rowSelection: {} })).resolves.toBe(true) + }) + + it('rejects selected rows that are not available', async () => { + await expect(schema.isValid({ filter: '', rowSelection: { 'unknown:0': true } })).resolves.toBe(false) + }) +}) diff --git a/src/components/send/UtxoSelectionDialog.schema.ts b/src/components/send/UtxoSelectionDialog.schema.ts new file mode 100644 index 00000000..6ed25f30 --- /dev/null +++ b/src/components/send/UtxoSelectionDialog.schema.ts @@ -0,0 +1,32 @@ +import type { RowSelectionState } from '@tanstack/react-table' +import * as yup from 'yup' + +export type UtxoSelectionFormValues = { + filter: string + rowSelection: RowSelectionState +} + +export const UTXO_SELECTION_FORM_DEFAULT_VALUES: UtxoSelectionFormValues = { + filter: '', + rowSelection: {}, +} + +export const createUtxoSelectionFormSchema = ( + selectableUtxoIds: string[], +): yup.ObjectSchema => { + const selectableUtxoIdSet = new Set(selectableUtxoIds) + + return yup + .object({ + filter: yup.string().defined(), + rowSelection: yup + .mixed() + .defined() + .test('selectable-utxos', (value) => + Object.entries(value).every( + ([utxoId, selected]) => typeof selected === 'boolean' && (!selected || selectableUtxoIdSet.has(utxoId)), + ), + ), + }) + .required() +} diff --git a/src/components/send/UtxoSelectionDialog.tsx b/src/components/send/UtxoSelectionDialog.tsx index 4b3e2684..b28f1027 100644 --- a/src/components/send/UtxoSelectionDialog.tsx +++ b/src/components/send/UtxoSelectionDialog.tsx @@ -37,40 +37,49 @@ export const UtxoSelectionDialog = ({ return ( - - {t('show_utxos.title')} - - {t('show_utxos.subtitle', { count: selectedCount })} {t('show_utxos.text_subtitle_addon')} - - +
{ + event.preventDefault() + void onSubmit() + }} + noValidate + > + + {t('show_utxos.title')} + + {t('show_utxos.subtitle', { count: selectedCount })} {t('show_utxos.text_subtitle_addon')} + + - onFilterChange(event.target.value)} - placeholder={t('jar_details.utxo_list.placeholder_search')} - disabled={isSubmitting} - /> -
- onFilterChange(event.target.value)} + placeholder={t('jar_details.utxo_list.placeholder_search')} + disabled={isSubmitting} /> -
+
+ +
- - - - + + + + +
) diff --git a/src/components/settings/SeedPhraseDialog.tsx b/src/components/settings/SeedPhraseDialog.tsx index d4e91e92..146ade70 100644 --- a/src/components/settings/SeedPhraseDialog.tsx +++ b/src/components/settings/SeedPhraseDialog.tsx @@ -33,7 +33,6 @@ type SeedPhraseDialogProps = WithRequiredProperty< autoCloseTimeout: Milliseconds } -// TODO: use react-hook-form and yup schema export const SeedPhraseDialog = ({ open, onOpenChange, diff --git a/src/hooks/useUtxoSelectionDialog.ts b/src/hooks/useUtxoSelectionDialog.ts index 369837c3..1682b289 100644 --- a/src/hooks/useUtxoSelectionDialog.ts +++ b/src/hooks/useUtxoSelectionDialog.ts @@ -1,10 +1,17 @@ import { useMemo, useState } from 'react' +import { yupResolver } from '@hookform/resolvers/yup' import { freezeMutation } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query' import { useMutation } from '@tanstack/react-query' import type { RowSelectionState } from '@tanstack/react-table' +import { useForm, useWatch } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import type { UtxoSelectionDialogProps } from '@/components/send/UtxoSelectionDialog' +import { + createUtxoSelectionFormSchema, + UTXO_SELECTION_FORM_DEFAULT_VALUES, + type UtxoSelectionFormValues, +} from '@/components/send/UtxoSelectionDialog.schema' import { useJamWalletInfoContext, type AddressSummary, type Jar } from '@/context/JamWalletInfoContext' import { useApiClient } from '@/hooks/useApiClient' import type { Utxo } from '@/hooks/useQueryUtxos' @@ -26,8 +33,6 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma const { refetch: walletInfoRefetch } = useJamWalletInfoContext() const [open, setOpen] = useState(false) - const [filter, setFilter] = useState('') - const [rowSelection, setRowSelection] = useState({}) const tableEntries = useMemo(() => { return (sourceJar?.utxos || []).map((utxo) => ({ @@ -45,6 +50,26 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma }, {} as RowSelectionState) }, [sourceJar?.utxos]) + const selectableUtxoIds = useMemo( + () => (sourceJar?.utxos || []).filter((utxo) => !fb.utxo.isFidelityBond(utxo)).map((utxo) => utxo.utxo), + [sourceJar?.utxos], + ) + const formSchema = useMemo(() => createUtxoSelectionFormSchema(selectableUtxoIds), [selectableUtxoIds]) + const { + control, + getValues, + handleSubmit, + reset, + setValue, + formState: { isSubmitting: isFormSubmitting }, + } = useForm({ + mode: 'onChange', + defaultValues: UTXO_SELECTION_FORM_DEFAULT_VALUES, + resolver: yupResolver(formSchema), + }) + const filter = useWatch({ control, name: 'filter', defaultValue: '' }) + const rowSelection = useWatch({ control, name: 'rowSelection', defaultValue: {} }) + const selectedUtxos = useMemo(() => { return (sourceJar?.utxos || []).filter((utxo) => rowSelection[utxo.utxo] === true) }, [sourceJar?.utxos, rowSelection]) @@ -56,7 +81,7 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma retry: false, }) - const { mutateAsync: applyUtxoSelectionMutateAsync, isPending: isSubmitting } = useMutation({ + const { mutateAsync: applyUtxoSelectionMutateAsync, isPending: isMutationPending } = useMutation({ mutationFn: async ({ utxosToFreeze, utxosToUnfreeze }: { utxosToFreeze: Utxo[]; utxosToUnfreeze: Utxo[] }) => { const [freezeResult, unfreezeResult] = await Promise.all([ Promise.allSettled( @@ -94,25 +119,28 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma const onOpenUtxoSelector = () => { if (!sourceJar) return toast.dismiss(SEND_AUTO_SELECTION_TOAST_ID) - setFilter('') - setRowSelection(initialRowSelection) + reset({ + filter: '', + rowSelection: initialRowSelection, + }) setOpen(true) } - const onSubmit = async () => { + const onSubmit = handleSubmit(async ({ rowSelection: submittedRowSelection }) => { if (!sourceJar) return - const selectedUtxoIds = new Set(selectedUtxos.map((it) => it.utxo)) - const selectedAddresses = new Set(selectedUtxos.map((it) => it.address)) + const submittedUtxos = sourceJar.utxos.filter((utxo) => submittedRowSelection[utxo.utxo] === true) + const selectedUtxoIds = new Set(submittedUtxos.map((it) => it.utxo)) + const selectedAddresses = new Set(submittedUtxos.map((it) => it.address)) const mutableUtxos = sourceJar.utxos.filter((it) => !fb.utxo.isFidelityBond(it)) const groupedSelectedUtxos = mutableUtxos.filter((it) => selectedAddresses.has(it.address)) const groupedDeselectedUtxos = mutableUtxos.filter((it) => !selectedAddresses.has(it.address)) const userDeselectedUtxos = mutableUtxos.filter((it) => !selectedUtxoIds.has(it.utxo)) - if (groupedSelectedUtxos.length > selectedUtxos.length) { + if (groupedSelectedUtxos.length > submittedUtxos.length) { toast.warning(t('jar_details.utxo_list.toast_auto_selection_title'), { description: t('jar_details.utxo_list.toast_auto_selected_matching', { - count: groupedSelectedUtxos.length - selectedUtxos.length, + count: groupedSelectedUtxos.length - submittedUtxos.length, }), id: SEND_AUTO_SELECTION_TOAST_ID, }) @@ -178,7 +206,9 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma toast.error(t('jar_details.utxo_list.toast_unfreeze_error', { count: utxosToUnfreeze.length })) } } - } + }) + + const isSubmitting = isFormSubmitting || isMutationPending const dialogProps: UtxoSelectionDialogProps = { open, @@ -190,11 +220,26 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma enableRowSelection: isSubmitting ? false : undefined, onOpenChange: (nextOpen: boolean) => { if (isSubmitting) return + if (!nextOpen) { + reset(UTXO_SELECTION_FORM_DEFAULT_VALUES) + } setOpen(nextOpen) }, - onFilterChange: setFilter, - onRowSelectionChange: setRowSelection, - onSubmit, + onFilterChange: (nextFilter) => + setValue('filter', nextFilter, { + shouldDirty: true, + shouldValidate: true, + }), + onRowSelectionChange: (updater) => { + const currentSelection = getValues('rowSelection') + const nextSelection = typeof updater === 'function' ? updater(currentSelection) : updater + setValue('rowSelection', nextSelection, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + }, + onSubmit: async () => onSubmit(), } return { From 6490a967146dd239f92e364d75bd04f9d1d4da5a Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Wed, 29 Jul 2026 09:18:20 +0200 Subject: [PATCH 12/56] fix(receive): save sourceJarIndex before async operation (#1369) * fix(receive): save sourceJarIndex before async operation * chore: flag if address from parsed from bip21 uri * chore: scan applied alert only when parsed from uri --- src/components/receive/ReceivePage.tsx | 5 +- src/components/send/SendForm.test.tsx | 16 +++++- src/components/send/SendForm.tsx | 4 +- .../sweep/SweepDestinationInputs.test.tsx | 38 ++++++++++++- .../sweep/SweepDestinationInputs.tsx | 4 +- src/lib/bip21.test.ts | 57 ++++++++++++------- src/lib/bip21.ts | 18 +++--- 7 files changed, 105 insertions(+), 37 deletions(-) diff --git a/src/components/receive/ReceivePage.tsx b/src/components/receive/ReceivePage.tsx index 9975902e..2afa461a 100644 --- a/src/components/receive/ReceivePage.tsx +++ b/src/components/receive/ReceivePage.tsx @@ -67,17 +67,18 @@ export const ReceivePage = ({ walletFileName }: ReceivePageProps) => { mutationKey: ['receive', ...getaddressQueryKey(getAddressOptions)], mutationFn: withMutationDelay( async () => { + const sourceJarIndex = Number.parseInt(getAddressOptions.path.mixdepth, 10) const { data } = await getaddress({ ...getAddressOptions, throwOnError: true, }) return { address: data.address, - sourceJarIndex: Number.parseInt(getAddressOptions.path.mixdepth, 10), + sourceJarIndex, } }, { - delayAfter: 21, + throttle: 210, }, ), retry: false, diff --git a/src/components/send/SendForm.test.tsx b/src/components/send/SendForm.test.tsx index bda8c888..78597dd9 100644 --- a/src/components/send/SendForm.test.tsx +++ b/src/components/send/SendForm.test.tsx @@ -286,7 +286,7 @@ describe('SendForm', () => { await waitFor(() => expect(h.toastError).toHaveBeenCalledWith('global.errors.error_loading_address_failed')) }) - it('applies a pasted bip21 uri', async () => { + it('applies a pasted bip21 uri and shows success toast', async () => { renderForm() const inputBefore = document.querySelector('#send-destination') as HTMLInputElement @@ -303,7 +303,7 @@ describe('SendForm', () => { await flushActUpdates() }) - it('ignores a non-bitcoin paste', () => { + it('ignores a non-bitcoin-uri paste', () => { renderForm() const input = document.querySelector('#send-destination') as HTMLInputElement @@ -312,6 +312,18 @@ describe('SendForm', () => { expect(h.toastSuccess).not.toHaveBeenCalled() }) + it('applies pasted address and does show success toast', () => { + renderForm() + const inputBefore = document.querySelector('#send-destination') as HTMLInputElement + + fireEvent.paste(inputBefore, { clipboardData: { getData: () => h.DEFAULT_SCAN_DUMMY_ADDRESS_1 } }) + + const inputAfter = document.querySelector('#send-destination') as HTMLInputElement + expect(inputAfter).toHaveValue(h.DEFAULT_SCAN_DUMMY_ADDRESS_1) + + expect(h.toastSuccess).not.toHaveBeenCalled() + }) + it('renders the debug panel when debug is enabled', () => { renderForm({ debug: true }) expect(screen.getByText('isValid:')).toBeInTheDocument() diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx index ba69dbc7..78dce756 100644 --- a/src/components/send/SendForm.tsx +++ b/src/components/send/SendForm.tsx @@ -259,7 +259,9 @@ export function SendForm({ event.preventDefault() applyBip21Result(parsed) - toast.success(t('send.qr_scan_bip21_applied')) + if (parsed.fromUri) { + toast.success(t('send.qr_scan_bip21_applied')) + } }, [applyBip21Result, t], ) diff --git a/src/components/sweep/SweepDestinationInputs.test.tsx b/src/components/sweep/SweepDestinationInputs.test.tsx index cfe7d400..f621e149 100644 --- a/src/components/sweep/SweepDestinationInputs.test.tsx +++ b/src/components/sweep/SweepDestinationInputs.test.tsx @@ -1,19 +1,35 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import type { FieldArrayWithId, UseFormReturn } from 'react-hook-form' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { SweepDestinationInputs } from './SweepDestinationInputs' import type { SweepFormValues } from './SweepFormSchema' type SweepForm = UseFormReturn type SweepFields = Array> +const h = vi.hoisted(() => { + const DEFAULT_NEW_DUMMY_ADDRESS_0 = 'bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk' + return { + DEFAULT_NEW_DUMMY_ADDRESS_0, + toastSuccess: vi.fn<(message: string) => void>(), + } +}) + vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, options?: Record) => (options ? `${key} ${JSON.stringify(options)}` : key), }), })) +vi.mock('sonner', () => ({ + toast: { + success: (message: string) => { + h.toastSuccess(message) + }, + }, +})) + describe('SweepDestinationInputs', () => { const defaultFormMock = { formState: { @@ -30,6 +46,11 @@ describe('SweepDestinationInputs', () => { { id: '2', address: '' }, ] as unknown as SweepFields + beforeEach(() => { + vi.clearAllMocks() + h.toastSuccess = vi.fn<(message: string) => void>() + }) + it('renders input fields correctly', async () => { render( { ) const inputs = screen.getAllByRole('textbox') + expect(inputs).toHaveLength(fields.length) await act(async () => { await userEvent.click(inputs[0]) await userEvent.paste('anything') @@ -128,19 +150,33 @@ describe('SweepDestinationInputs', () => { await userEvent.click(inputs[0]) await userEvent.paste('bitcoin:') }) + expect(defaultFormMock.setValue).not.toHaveBeenCalled() + await act(async () => { + await userEvent.click(inputs[0]) + await userEvent.paste(h.DEFAULT_NEW_DUMMY_ADDRESS_0) + }) + + expect(defaultFormMock.setValue).toHaveBeenCalledWith(`destinations.0.address`, h.DEFAULT_NEW_DUMMY_ADDRESS_0, { + shouldValidate: true, + }) + + expect(h.toastSuccess).not.toHaveBeenCalled() + await act(async () => { await userEvent.click(inputs[1]) await userEvent.paste( 'bitcoin:bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk?amount=0.0021&label=order%20123&message=regtest', ) }) + expect(defaultFormMock.setValue).toHaveBeenCalledWith( `destinations.1.address`, 'bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk', { shouldValidate: true }, ) + expect(h.toastSuccess).toHaveBeenCalledWith('send.qr_scan_bip21_applied') }) it('adds additional destination inputs', () => { diff --git a/src/components/sweep/SweepDestinationInputs.tsx b/src/components/sweep/SweepDestinationInputs.tsx index 821e08c4..d0b2afbe 100644 --- a/src/components/sweep/SweepDestinationInputs.tsx +++ b/src/components/sweep/SweepDestinationInputs.tsx @@ -55,7 +55,9 @@ export const SweepDestinationInputs = ({ event.preventDefault() applyBip21Result(parsed, index) - toast.success(t('send.qr_scan_bip21_applied')) + if (parsed.fromUri) { + toast.success(t('send.qr_scan_bip21_applied')) + } }, [applyBip21Result, t], ) diff --git a/src/lib/bip21.test.ts b/src/lib/bip21.test.ts index 5d36cb2f..2b906e7a 100644 --- a/src/lib/bip21.test.ts +++ b/src/lib/bip21.test.ts @@ -10,17 +10,17 @@ describe('parseBip21Uri', () => { describe('raw addresses', () => { it('parses a valid mainnet address', () => { const result = parseBip21Uri(VALID_ADDRESS) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: false }) }) it('parses a valid testnet address', () => { const result = parseBip21Uri(VALID_TESTNET_ADDRESS) - expect(result).toEqual({ address: VALID_TESTNET_ADDRESS }) + expect(result).toEqual({ address: VALID_TESTNET_ADDRESS, fromUri: false }) }) it('trims whitespace around address', () => { const result = parseBip21Uri(` ${VALID_ADDRESS} `) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: false }) }) it('returns undefined for invalid address', () => { @@ -35,32 +35,38 @@ describe('parseBip21Uri', () => { describe('BIP21 URIs', () => { it('parses bitcoin: URI with address only', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}`) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true }) }) it('parses bitcoin: URI with amount', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=0.5`) - expect(result).toEqual({ address: VALID_ADDRESS, amount: 50_000_000 }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true, amount: 50_000_000 }) }) it('parses bitcoin: URI with 1 BTC amount', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=1`) - expect(result).toEqual({ address: VALID_ADDRESS, amount: 100_000_000 }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true, amount: 100_000_000 }) }) it('parses bitcoin: URI with small amount (1 sat)', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=0.00000001`) - expect(result).toEqual({ address: VALID_ADDRESS, amount: 1 }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true, amount: 1 }) }) it('handles case-insensitive scheme', () => { - const result = parseBip21Uri(`BITCOIN:${VALID_ADDRESS}?amount=0.1`) - expect(result).toEqual({ address: VALID_ADDRESS, amount: 10_000_000 }) + const result = parseBip21Uri(`BiTcOiN:${VALID_ADDRESS}?amount=0.1`) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true, amount: 10_000_000 }) }) it('ignores extra query parameters', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=0.01&label=test&message=hello`) - expect(result).toEqual({ address: VALID_ADDRESS, amount: 1_000_000, label: 'test', message: 'hello' }) + expect(result).toEqual({ + address: VALID_ADDRESS, + fromUri: true, + amount: 1_000_000, + label: 'test', + message: 'hello', + }) }) it('returns undefined for invalid address in URI', () => { @@ -73,48 +79,48 @@ describe('parseBip21Uri', () => { it('ignores zero amount', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=0`) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true }) }) it('ignores negative amount', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=-1`) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true }) }) it('ignores non-numeric amount', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=abc`) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true }) }) it('returns undefined for non-bitcoin URI scheme', () => { - expect(parseBip21Uri(`ethereum:${VALID_ADDRESS}`)).toBeUndefined() + expect(parseBip21Uri(`lnurl:${VALID_ADDRESS}`)).toBeUndefined() }) }) describe('label parameter', () => { it('parses label from BIP21 URI', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?label=Donation`) - expect(result).toEqual({ address: VALID_ADDRESS, label: 'Donation' }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true, label: 'Donation' }) }) it('parses label with amount', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=0.01&label=Payment`) - expect(result).toEqual({ address: VALID_ADDRESS, amount: 1_000_000, label: 'Payment' }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true, amount: 1_000_000, label: 'Payment' }) }) it('ignores empty label', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?label=`) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true }) }) it('decodes URL-encoded label', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?label=My%20Wallet`) - expect(result).toEqual({ address: VALID_ADDRESS, label: 'My Wallet' }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true, label: 'My Wallet' }) }) it('does not include label for raw address', () => { const result = parseBip21Uri(VALID_ADDRESS) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: false }) expect(result).not.toHaveProperty('label') }) }) @@ -122,21 +128,28 @@ describe('parseBip21Uri', () => { describe('message parameter', () => { it('parses message from BIP21 URI', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?message=Invoice%20%231234`) - expect(result).toEqual({ address: VALID_ADDRESS, message: 'Invoice #1234' }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true, message: 'Invoice #1234' }) }) it('parses message with amount and label', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?amount=0.05&label=Shop&message=Order%20789`) - expect(result).toEqual({ address: VALID_ADDRESS, amount: 5_000_000, label: 'Shop', message: 'Order 789' }) + expect(result).toEqual({ + address: VALID_ADDRESS, + fromUri: true, + amount: 5_000_000, + label: 'Shop', + message: 'Order 789', + }) }) it('ignores empty message', () => { const result = parseBip21Uri(`bitcoin:${VALID_ADDRESS}?message=`) - expect(result).toEqual({ address: VALID_ADDRESS }) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: true }) }) it('does not include message for raw address', () => { const result = parseBip21Uri(VALID_ADDRESS) + expect(result).toEqual({ address: VALID_ADDRESS, fromUri: false }) expect(result).not.toHaveProperty('message') }) }) diff --git a/src/lib/bip21.ts b/src/lib/bip21.ts index 4bb34f43..ea14b054 100644 --- a/src/lib/bip21.ts +++ b/src/lib/bip21.ts @@ -2,34 +2,36 @@ import { validate as isValidBitcoinAddress } from 'bitcoin-address-validation' import { tryBtcToSat } from '@/lib/utils' import type { AmountSats, BitcoinAddress } from '@/types/global' +const BITCOIN_URI_SCHEME = 'bitcoin:' + export type Bip21ParseResult = { address: BitcoinAddress amount?: AmountSats label?: string message?: string + fromUri: boolean } export const parseBip21Uri = (raw: string): Bip21ParseResult | undefined => { const trimmed = raw.trim() - // raw address (no URI scheme) - if (isValidBitcoinAddress(trimmed)) { - return { address: trimmed } - } - // BIP21: bitcoin:
?amount=&... - if (!trimmed.toLowerCase().startsWith('bitcoin:')) { + if (!trimmed.toLowerCase().startsWith(BITCOIN_URI_SCHEME)) { + // raw address (no URI scheme) + if (isValidBitcoinAddress(trimmed)) { + return { address: trimmed, fromUri: false } + } return undefined } - const withoutScheme = trimmed.slice('bitcoin:'.length) + const withoutScheme = trimmed.slice(BITCOIN_URI_SCHEME.length) const [addressPart, queryString] = withoutScheme.split('?', 2) if (!addressPart || !isValidBitcoinAddress(addressPart)) { return undefined } - const result: Bip21ParseResult = { address: addressPart } + const result: Bip21ParseResult = { address: addressPart, fromUri: true } if (queryString) { const parameters = new URLSearchParams(queryString) From ff0a9f229efc6d31180567008d0302e484fa8fcc Mon Sep 17 00:00:00 2001 From: Parth <143504541+parrth20@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:50:38 +0530 Subject: [PATCH 13/56] fix(receive): block new addresses during rescan (#1367) * fix(receive): block new addresses during rescan * test(receive): click disabled address button --- src/components/receive/ReceivePage.test.tsx | 19 +++++++++++++++++++ src/components/receive/ReceivePage.tsx | 11 +++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/receive/ReceivePage.test.tsx b/src/components/receive/ReceivePage.test.tsx index b355513e..1d637b4d 100644 --- a/src/components/receive/ReceivePage.test.tsx +++ b/src/components/receive/ReceivePage.test.tsx @@ -34,6 +34,7 @@ const mocks = vi.hoisted(() => { return { developerMode: false, + rescanning: false, getAddress: vi.fn(), defaultJars, jars: defaultJars, @@ -107,6 +108,12 @@ vi.mock('@/context/JamWalletInfoContext', () => ({ }), })) +vi.mock('@/context/JamSessionInfoContext', () => ({ + useRescanStatus: () => ({ + rescanInfo: { rescanning: mocks.rescanning }, + }), +})) + vi.mock('@/hooks/useApiClient', () => ({ useApiClient: () => ({}), })) @@ -148,6 +155,7 @@ vi.mock('./ReceiveForm', () => ({ describe('ReceivePage', () => { beforeEach(() => { mocks.developerMode = false + mocks.rescanning = false mocks.jars = mocks.defaultJars mocks.getAddress.mockReset() mocks.getAddress.mockResolvedValue({ data: { address: 'bc1qexample' } }) @@ -175,6 +183,17 @@ describe('ReceivePage', () => { await flushActUpdates() }) + it('prevents loading addresses while rescanning', () => { + mocks.rescanning = true + + render() + + expect(screen.getByRole('button', { name: 'receive.button_reveal_address' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'receive.button_new_address' })).toBeDisabled() + fireEvent.click(screen.getByRole('button', { name: 'receive.button_new_address' })) + expect(mocks.getAddress).not.toHaveBeenCalled() + }) + it('uses a secondary jar badge when no jar is available', () => { mocks.jars = [] diff --git a/src/components/receive/ReceivePage.tsx b/src/components/receive/ReceivePage.tsx index 2afa461a..9b378a58 100644 --- a/src/components/receive/ReceivePage.tsx +++ b/src/components/receive/ReceivePage.tsx @@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import PageTitle from '@/components/ui/jam/PageTitle' import { Skeleton } from '@/components/ui/skeleton' +import { useRescanStatus } from '@/context/JamSessionInfoContext' import { useJars } from '@/context/JamWalletInfoContext' import { useApiClient } from '@/hooks/useApiClient' import { withMutationDelay } from '@/lib/queryClient' @@ -34,6 +35,7 @@ interface ReceivePageProps { export const ReceivePage = ({ walletFileName }: ReceivePageProps) => { const { t } = useTranslation() const { jars } = useJars() + const { rescanInfo } = useRescanStatus() const [selectedSourceJarIndex, setSelectedSourceJarIndex] = useState(jars.length > 0 ? jars[0].jarIndex : undefined) const [amount, setAmount] = useState() @@ -108,6 +110,7 @@ export const ReceivePage = ({ walletFileName }: ReceivePageProps) => { } const fetchNewAddress = async () => { + if (rescanInfo.rescanning) return await getAddressMutation.mutateAsync() } @@ -133,7 +136,7 @@ export const ReceivePage = ({ walletFileName }: ReceivePageProps) => { variant={jarButtonVariant(selectedSourceJarIndex)} size="lg" onClick={() => void fetchNewAddress()} - disabled={getAddressMutation.isPending} + disabled={getAddressMutation.isPending || rescanInfo.rescanning} > {t('receive.button_reveal_address')} @@ -190,7 +193,11 @@ export const ReceivePage = ({ walletFileName }: ReceivePageProps) => {
- ), diff --git a/src/components/earn/FidelityBondCard.test.tsx b/src/components/earn/FidelityBondCard.test.tsx index 60b17a50..b7cffbfc 100644 --- a/src/components/earn/FidelityBondCard.test.tsx +++ b/src/components/earn/FidelityBondCard.test.tsx @@ -34,7 +34,7 @@ vi.mock('../ui/jam/Address', () => ({ Address: ({ value }: { value?: string }) => {value}, })) -vi.mock('../ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString }: { valueString?: string }) => {valueString}, })) diff --git a/src/components/earn/FidelityBondCard.tsx b/src/components/earn/FidelityBondCard.tsx index 7cb649bf..7b6e10f0 100644 --- a/src/components/earn/FidelityBondCard.tsx +++ b/src/components/earn/FidelityBondCard.tsx @@ -2,12 +2,12 @@ import { useMemo, type PropsWithChildren } from 'react' import { ClockIcon, CoinsIcon, CopyIcon } from 'lucide-react' import { Trans, useTranslation } from 'react-i18next' import { Card, CardContent, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' +import { Balance } from '@/components/ui/jam/Balance' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos' import * as fb from '@/lib/fidelityBondUtils' import { time } from '@/lib/utils' import { Address } from '../ui/jam/Address' -import { Balance } from '../ui/jam/Balance' interface FidelityBondCardProps { value: FidelityBondUtxo diff --git a/src/components/earn/MoveToJarDialog.test.tsx b/src/components/earn/MoveToJarDialog.test.tsx index 9215d192..98210f2a 100644 --- a/src/components/earn/MoveToJarDialog.test.tsx +++ b/src/components/earn/MoveToJarDialog.test.tsx @@ -72,16 +72,16 @@ vi.mock('@/context/JamWalletInfoContext', () => ({ vi.mock('@/components/ui/jam/SelectableJar', () => ({ SelectableJar: ({ name, - onClick, + onSelect, disabled, isSelected, }: { name?: string - onClick?: () => void + onSelect?: () => void disabled?: boolean isSelected?: boolean }) => ( - ), diff --git a/src/components/earn/OfferCard.test.tsx b/src/components/earn/OfferCard.test.tsx index 62821467..2f8e48f2 100644 --- a/src/components/earn/OfferCard.test.tsx +++ b/src/components/earn/OfferCard.test.tsx @@ -18,7 +18,7 @@ vi.mock('@/lib/utils', () => ({ isRelativeOffer: (type: string) => type === 'sw0relo', })) -vi.mock('../ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString }: { valueString?: string }) => {valueString}, })) diff --git a/src/components/earn/fidelity-bond/FidelityBondJarSelector.tsx b/src/components/earn/fidelity-bond/FidelityBondJarSelector.tsx index 68f6a4e4..db4db5f7 100644 --- a/src/components/earn/fidelity-bond/FidelityBondJarSelector.tsx +++ b/src/components/earn/fidelity-bond/FidelityBondJarSelector.tsx @@ -24,7 +24,7 @@ export function FidelityBondJarSelector({ selectedJarIndex, onSelect, isJarDisab frozenOrLockedBalance={jar.balanceSummary.calculatedFrozenOrLockedBalanceInSats} totalWalletBalance={walletBalanceSummary.calculatedTotalBalanceInSats} isSelected={selectedJarIndex === jar.jarIndex} - onClick={() => onSelect(jar.jarIndex)} + onSelect={() => onSelect(jar.jarIndex)} disabled={isJarDisabled(jar)} /> ))} diff --git a/src/components/layout/AppNavbar.tsx b/src/components/layout/AppNavbar.tsx index c4a883ff..4255c19a 100644 --- a/src/components/layout/AppNavbar.tsx +++ b/src/components/layout/AppNavbar.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' import { Link, useNavigate, type NavigateFunction } from 'react-router-dom' import { DevBadge } from '@/components/dev/DevBadge' import { Button } from '@/components/ui/button' +import { Balance } from '@/components/ui/jam/Balance' import { ThemeToggleButton } from '@/components/ui/jam/ThemeToggleButton' import { Skeleton } from '@/components/ui/skeleton' import type { SidebarContextProps } from '@/components/ui/use-sidebar' @@ -15,7 +16,6 @@ import type { RescanInfo } from '@/context/JamSessionInfoContext' import { cn, shortenStringMiddle } from '@/lib/utils' import type { AmountSats } from '@/types/global' import { WithActivityIndicator } from '../ui/jam/ActivityIndicator' -import { Balance } from '../ui/jam/Balance' import { Spinner } from '../ui/spinner' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' diff --git a/src/components/orderbook/OrderbookContent.tsx b/src/components/orderbook/OrderbookContent.tsx index e6c9893b..ae616ade 100644 --- a/src/components/orderbook/OrderbookContent.tsx +++ b/src/components/orderbook/OrderbookContent.tsx @@ -18,6 +18,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' +import { Balance } from '@/components/ui/jam/Balance' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' @@ -36,7 +37,6 @@ import { } from '@/lib/utils' import { useDeveloperMode } from '@/store/jamSettingsStore' import { jmSessionStore } from '@/store/jmSessionStore' -import { Balance } from '../ui/jam/Balance' import { Spinner } from '../ui/spinner' import { OrderbookChart } from './OrderbookChart' import { OrderbookTable, type OrderTableEntry } from './OrderbookTable' diff --git a/src/components/receive/ReceiveForm.test.tsx b/src/components/receive/ReceiveForm.test.tsx index 5eba191d..9e04c219 100644 --- a/src/components/receive/ReceiveForm.test.tsx +++ b/src/components/receive/ReceiveForm.test.tsx @@ -19,8 +19,8 @@ vi.mock('@/context/JamWalletInfoContext', () => ({ })) vi.mock('@/components/ui/jam/SelectableJar', () => ({ - SelectableJar: ({ name, isSelected, onClick }: { name: string; isSelected: boolean; onClick: () => void }) => ( - ), diff --git a/src/components/receive/ReceiveForm.tsx b/src/components/receive/ReceiveForm.tsx index f3be268f..2ed14ade 100644 --- a/src/components/receive/ReceiveForm.tsx +++ b/src/components/receive/ReceiveForm.tsx @@ -91,7 +91,7 @@ export const ReceiveForm = ({ className, defaultValues, onSubmit, jars, disabled > {t('receive.label_source_jar')} -
+
{jars.map((jar, index) => ( { + onSelect={() => { setValue('source.fromJar', jar.jarIndex, { shouldValidate: true }) }} disabled={disabled || isSubmitting} diff --git a/src/components/send/JarSelectorDialog.test.tsx b/src/components/send/JarSelectorDialog.test.tsx index ca199a44..6d86673a 100644 --- a/src/components/send/JarSelectorDialog.test.tsx +++ b/src/components/send/JarSelectorDialog.test.tsx @@ -11,16 +11,16 @@ vi.mock('react-i18next', () => ({ vi.mock('../ui/jam/SelectableJar', () => ({ SelectableJar: ({ name, - onClick, + onSelect, disabled, isSelected, }: { name?: string - onClick?: () => void + onSelect?: () => void disabled?: boolean isSelected?: boolean }) => ( - ), diff --git a/src/components/send/JarSelectorDialog.tsx b/src/components/send/JarSelectorDialog.tsx index 27b15d64..fd8e8206 100644 --- a/src/components/send/JarSelectorDialog.tsx +++ b/src/components/send/JarSelectorDialog.tsx @@ -90,7 +90,7 @@ export default function JarSelectorDialog({ frozenOrLockedBalance={jar.balanceSummary.calculatedFrozenOrLockedBalanceInSats} totalWalletBalance={walletBalanceSummary.calculatedTotalBalanceInSats} isSelected={selectedJarIndex === jar.jarIndex} - onClick={() => + onSelect={() => setValue('jarIndex', jar.jarIndex, { shouldDirty: true, shouldTouch: true, diff --git a/src/components/send/PaymentConfirmDialog.test.tsx b/src/components/send/PaymentConfirmDialog.test.tsx index f3b2962f..cf6c04ce 100644 --- a/src/components/send/PaymentConfirmDialog.test.tsx +++ b/src/components/send/PaymentConfirmDialog.test.tsx @@ -27,7 +27,7 @@ vi.mock('../ui/jam/Address', () => ({ Address: ({ value }: { value: string }) => address:{value}, })) -vi.mock('../ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString }: { valueString: string }) => balance:{valueString}, })) @@ -50,6 +50,7 @@ const sourceJar: Jar = { calculatedAvailableBalanceInSats: 50_000, calculatedTotalBalanceInSats: 50_000, calculatedConfirmedAvailableBalanceInSats: 50_000, + calculatedAvailableFrozenBalanceInSats: 0, calculatedFrozenOrLockedBalanceInSats: 0, }, color: '#e2b86a', diff --git a/src/components/send/PaymentConfirmDialog.tsx b/src/components/send/PaymentConfirmDialog.tsx index 5e455a8d..462beaba 100644 --- a/src/components/send/PaymentConfirmDialog.tsx +++ b/src/components/send/PaymentConfirmDialog.tsx @@ -3,6 +3,7 @@ import { InfoIcon } from 'lucide-react' import { Trans, useTranslation } from 'react-i18next' import { Badge } from '@/components/ui/badge' import { jarBadgeVariant } from '@/components/ui/badge-variants' +import { Balance } from '@/components/ui/jam/Balance' import type { Jar } from '@/context/JamWalletInfoContext' import type { Utxo } from '@/hooks/useQueryUtxos' import type { JamFeeConfigValues } from '@/lib/feeConfig' @@ -13,7 +14,6 @@ import { Button } from '../ui/button' import { Card, CardContent, CardHeader } from '../ui/card' import { Dialog, DialogTitle, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '../ui/dialog' import { Address } from '../ui/jam/Address' -import { Balance } from '../ui/jam/Balance' import { Spinner } from '../ui/spinner' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' import { estimateMaxCollaboratorFee, useMiningFeeText, type EstimateMaxCollaboratorFeeResult } from './feeEstimate' diff --git a/src/components/send/SendForm.schema.ts b/src/components/send/SendForm.schema.ts index 9b353799..73589821 100644 --- a/src/components/send/SendForm.schema.ts +++ b/src/components/send/SendForm.schema.ts @@ -64,11 +64,28 @@ export const createSendFormSchema = ( .object({ source: yup .object({ - fromJar: sourceJarField( - t('send.feedback_invalid_source_jar'), - (jarIndex) => - (jars.find((it) => it.jarIndex === jarIndex)?.balanceSummary.calculatedAvailableBalanceInSats || 0) > 0, - ), + fromJar: sourceJarField(t('send.feedback_invalid_source_jar'), (jarIndex) => + jars.some((it) => it.jarIndex === jarIndex), + ) + .test( + 'valid-source-jar-must-unfreeze-utxos-test', + t('send.feedback_invalid_source_jar_must_unfreeze_utxos'), + (jarIndex) => { + const jar = jars.find((it) => it.jarIndex === jarIndex) + if (!jar) return true + if (jar.balanceSummary.calculatedAvailableBalanceInSats > 0) return true + return jar.balanceSummary.calculatedAvailableFrozenBalanceInSats <= 0 + }, + ) + .test( + 'valid-source-jar-has-funds-test', + t('send.feedback_invalid_source_jar_no_available_utxos'), + (jarIndex) => { + const jar = jars.find((it) => it.jarIndex === jarIndex) + if (!jar) return true + return jar.balanceSummary.calculatedAvailableBalanceInSats > 0 + }, + ), }) .required(), destination: yup diff --git a/src/components/send/SendForm.test.tsx b/src/components/send/SendForm.test.tsx index 78597dd9..10f4a0a3 100644 --- a/src/components/send/SendForm.test.tsx +++ b/src/components/send/SendForm.test.tsx @@ -55,13 +55,13 @@ vi.mock('../ui/jam/Address', () => ({ Address: () =>
, })) -vi.mock('../ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString }: { valueString?: string }) =>
{valueString}
, })) vi.mock('../ui/jam/SelectableJar', () => ({ - SelectableJar: ({ name, onClick, disabled }: { name?: string; onClick?: () => void; disabled?: boolean }) => ( - ), diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx index 78dce756..bad1016f 100644 --- a/src/components/send/SendForm.tsx +++ b/src/components/send/SendForm.tsx @@ -5,11 +5,11 @@ import { getAddressInfo, Network } from 'bitcoin-address-validation' import type { AddressInfo } from 'bitcoin-address-validation' import { AlertTriangleIcon, BrushCleaningIcon, MilkIcon, ScanQrCodeIcon, XIcon } from 'lucide-react' import { FormProvider, useForm, useWatch } from 'react-hook-form' -import type { Resolver, SubmitHandler } from 'react-hook-form' +import type { FieldErrors, Resolver, SubmitHandler } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import QrScannerDialog from '@/components/ui/QrScannerDialog' -import { isDevMode } from '@/constants/debugFeatures' +import { Balance } from '@/components/ui/jam/Balance' import { MAX_NUM_COLLABORATORS } from '@/constants/jam' import { JM_MINIMUM_MAKERS_DEFAULT, JM_TAKER_UTXO_AGE } from '@/constants/jm' import { useDetectNetwork, type AddressSummary, type Jar } from '@/context/JamWalletInfoContext' @@ -31,7 +31,6 @@ import { Field, FieldDescription, FieldError, FieldLabel } from '../ui/field' import { Input } from '../ui/input' import { inputVariants } from '../ui/input-variants' import { Address } from '../ui/jam/Address' -import { Balance } from '../ui/jam/Balance' import { SatSymbol } from '../ui/jam/CurrencySymbol' import { SelectableJar } from '../ui/jam/SelectableJar' import { Slider } from '../ui/slider' @@ -94,12 +93,6 @@ const AddressFromJarSelectorDialog = ({ ) } -// set the default to one collaborator in dev mode -const DEV_INITIAL_NUM_COLLABORATORS_INPUT = 1 - -// TODO: this value should be dynamic via jm backend settings -const MIN_NUM_COLLABORATORS = isDevMode() ? DEV_INITIAL_NUM_COLLABORATORS_INPUT : JM_MINIMUM_MAKERS_DEFAULT - const FieldPrefixSatSymbol = ( onSourceJarChange?: (jarIndex: JarIndex | undefined) => void - sourceJarLabelButton?: React.ReactElement + onSourceJarClicked?: (jarIndex: JarIndex | undefined) => void + sourceJarLabelButton?: (errors?: FieldErrors<{ fromJar: number }>) => React.ReactElement minNumberOfCollaborators?: number feeConfigValues: JamFeeConfigValues walletFileName: WalletFileName @@ -129,11 +123,12 @@ export function SendForm({ className, onSubmit, onSourceJarChange, + onSourceJarClicked, sourceJarLabelButton, disabled, feeConfigValues, walletFileName, - minNumberOfCollaborators = MIN_NUM_COLLABORATORS, + minNumberOfCollaborators = JM_MINIMUM_MAKERS_DEFAULT, jars, walletBalanceSummary, addressSummary, @@ -204,6 +199,15 @@ export function SendForm({ onSourceJarChange?.(sourceJarIndex) }, [onSourceJarChange, sourceJarIndex]) + useEffect(() => { + if (formState.dirtyFields.source?.fromJar) { + console.debug( + 'Validating `source.fromJar` as wallet balance summary changed (e.g. frozen/unfrozen utxos, new transaction, etc.)', + ) + void trigger('source.fromJar') + } + }, [walletBalanceSummary, trigger, formState.dirtyFields.source?.fromJar]) + const destinationJar = useMemo(() => { if (destinationJarIndex === undefined) return return jars.find((it) => it.jarIndex === destinationJarIndex) @@ -300,41 +304,58 @@ export function SendForm({ {...sendFormMethods} >
void doOnSubmit(event)} className={cn('flex flex-col gap-4', className)} noValidate> -
- +
+
{t('send.label_source_jar')} - {sourceJarLabelButton && <>{sourceJarLabelButton}} + {sourceJarLabelButton?.(errors.source)}
-
- {jars.map((jar, index) => ( - { - setValue('source.fromJar', jar.jarIndex, { shouldValidate: true }) +
+ {jars.map((jar, index) => { + const noFunds = + jar.balanceSummary.calculatedAvailableBalanceInSats <= 0 && + jar.balanceSummary.calculatedAvailableFrozenBalanceInSats <= 0 - if (isSweep === true) { - setValue('amount.isSweep', false, { shouldValidate: true }) - setValue('amount.sweepAmount', undefined, { shouldValidate: true }) - setValue('amount.amount', undefined, { shouldValidate: true }) - } - if (destinationJarIndex === jar.jarIndex) { - setValue('destination.address', '', { shouldValidate: true }) - setValue('destination.fromJar', undefined, { shouldValidate: true }) - } else if (destinationAddress !== undefined) { - void trigger('destination.address') - } - }} - disabled={disabled || jar.balanceSummary.calculatedAvailableBalanceInSats <= 0} - /> - ))} + const isSelected = sourceJarIndex === jar.jarIndex + + return ( + { + if (isSelected) { + onSourceJarClicked?.(sourceJarIndex) + } + }} + onSelect={() => { + setValue('source.fromJar', jar.jarIndex, { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }) + + if (isSweep === true) { + setValue('amount.isSweep', false, { shouldValidate: true }) + setValue('amount.sweepAmount', undefined, { shouldValidate: true }) + setValue('amount.amount', undefined, { shouldValidate: true }) + } + if (destinationJarIndex === jar.jarIndex) { + setValue('destination.address', '', { shouldValidate: true }) + setValue('destination.fromJar', undefined, { shouldValidate: true }) + } else if (destinationAddress !== undefined) { + void trigger('destination.address') + } + }} + disabled={disabled || noFunds} + /> + ) + })}
{errors.source?.fromJar?.message ? {errors.source?.fromJar.message} : null} diff --git a/src/components/send/SendPage.test.tsx b/src/components/send/SendPage.test.tsx index 5aa3bd87..d7ec0b85 100644 --- a/src/components/send/SendPage.test.tsx +++ b/src/components/send/SendPage.test.tsx @@ -209,11 +209,11 @@ vi.mock('./SendForm', () => ({ disabled?: boolean onSourceJarChange?: (jarIndex: number | undefined) => void onSubmit: (values: SendFormValues) => void - sourceJarLabelButton?: React.ReactElement + sourceJarLabelButton?: () => React.ReactElement }) => (
send-form:{String(disabled)} -
{sourceJarLabelButton}
+
{sourceJarLabelButton?.()}
- } + onSourceJarClicked={onOpenUtxoSelector} + sourceJarLabelButton={(errors) => { + const errorSolvedByUtxoSelection = errors?.fromJar?.type === 'valid-source-jar-must-unfreeze-utxos-test' + const animate = utxoSelectionDialog.dialogProps.open === false && errorSolvedByUtxoSelection + return ( + + ) + }} /> diff --git a/src/components/sweep/SweepPage.test.tsx b/src/components/sweep/SweepPage.test.tsx index b290d6ea..c46ef3e3 100644 --- a/src/components/sweep/SweepPage.test.tsx +++ b/src/components/sweep/SweepPage.test.tsx @@ -303,6 +303,7 @@ const makeWalletInfo = (overrides: Partial = {}): WalletInfo => { calculatedAvailableBalanceInSats: 100_000, calculatedTotalBalanceInSats: 100_000, calculatedConfirmedAvailableBalanceInSats: 100_000, + calculatedAvailableFrozenBalanceInSats: 0, calculatedFrozenOrLockedBalanceInSats: 0, }, color: '#e2b86a', @@ -315,6 +316,7 @@ const makeWalletInfo = (overrides: Partial = {}): WalletInfo => { calculatedAvailableBalanceInSats: 0, calculatedTotalBalanceInSats: 0, calculatedConfirmedAvailableBalanceInSats: 0, + calculatedAvailableFrozenBalanceInSats: 0, calculatedFrozenOrLockedBalanceInSats: 0, }, color: '#ccc', @@ -341,6 +343,7 @@ const makeWalletInfo = (overrides: Partial = {}): WalletInfo => { calculatedAvailableBalanceInSats: 100_000, calculatedTotalBalanceInSats: 100_000, calculatedConfirmedAvailableBalanceInSats: 100_000, + calculatedAvailableFrozenBalanceInSats: 0, calculatedFrozenOrLockedBalanceInSats: 0, }, walletName: 'wallet.jmdat', diff --git a/src/components/ui/jam/Balance.test.tsx b/src/components/ui/jam/Balance.test.tsx index 25489f7e..b5c8ce40 100644 --- a/src/components/ui/jam/Balance.test.tsx +++ b/src/components/ui/jam/Balance.test.tsx @@ -3,8 +3,8 @@ import '@testing-library/jest-dom/vitest' import { render as reactRender, screen, type RenderOptions } from '@testing-library/react' import user from '@testing-library/user-event' import { describe, it, expect, vi } from 'vitest' +import { Balance } from '@/components/ui/jam/Balance' import { JamDisplayContextProvider } from '@/context/JamDisplayContextProvider' -import { Balance } from './Balance' const render = (ui: React.ReactNode, options?: Omit) => { const providers = ({ children }: { children: React.ReactNode }) => { diff --git a/src/components/ui/jam/Jar.test.tsx b/src/components/ui/jam/Jar.test.tsx new file mode 100644 index 00000000..d9c82b42 --- /dev/null +++ b/src/components/ui/jam/Jar.test.tsx @@ -0,0 +1,70 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { type Jar as JarType } from '@/context/JamWalletInfoContext' +import type { AmountSats } from '@/types/global' +import { Jar } from './Jar' + +vi.mock('@/components/ui/jam/Balance', () => ({ + Balance: ({ valueString, onClick }: { valueString: string; onClick?: () => void }) => ( + balance:{valueString} + ), +})) + +const DUMMY_JAR: JarType = { + balanceSummary: { + calculatedTotalBalanceInSats: 5, + calculatedAvailableBalanceInSats: 4, + calculatedConfirmedAvailableBalanceInSats: 3, + calculatedAvailableFrozenBalanceInSats: 2, + calculatedFrozenOrLockedBalanceInSats: 1, + }, + color: '#666', + jarIndex: 0, + name: 'Test jar', + utxos: [], +} + +describe('Jar', () => { + it('renders correctly', () => { + render( + , + ) + + expect(screen.getByText(DUMMY_JAR.name)).toBeInTheDocument() + expect(screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedAvailableBalanceInSats)).toBeInTheDocument() + expect( + screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedFrozenOrLockedBalanceInSats), + ).toBeInTheDocument() + expect( + screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedFrozenOrLockedBalanceInSats).parentNode, + ).not.toHaveClass('invisible') + }) + it('does not render zero frozen balance', () => { + const frozenBalance: AmountSats = 0 + + render( + , + ) + + expect(screen.getByText(DUMMY_JAR.name)).toBeInTheDocument() + expect(screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedAvailableBalanceInSats)).toBeInTheDocument() + expect(screen.getByText('balance:' + frozenBalance)).toBeInTheDocument() + expect(screen.getByText('balance:' + frozenBalance).parentNode).toHaveClass('invisible') + }) +}) diff --git a/src/components/ui/jam/Jar.tsx b/src/components/ui/jam/Jar.tsx index 451ea510..8cb6250f 100644 --- a/src/components/ui/jam/Jar.tsx +++ b/src/components/ui/jam/Jar.tsx @@ -1,7 +1,7 @@ +import { Balance } from '@/components/ui/jam/Balance' import type { JarColor } from '@/context/JamWalletInfoContext' import { cn } from '@/lib/utils' import type { AmountSats } from '@/types/global' -import { Balance } from './Balance' import { JarIcon } from './JarIcon' interface JarProps { @@ -53,7 +53,7 @@ export function Jar({
diff --git a/src/components/ui/jam/SelectableJar.test.tsx b/src/components/ui/jam/SelectableJar.test.tsx new file mode 100644 index 00000000..cb7c7a6e --- /dev/null +++ b/src/components/ui/jam/SelectableJar.test.tsx @@ -0,0 +1,131 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import { beforeEach } from 'vitest' +import type { Jar } from '@/context/JamWalletInfoContext' +import { SelectableJar } from './SelectableJar' + +const mocks = vi.hoisted(() => ({ + onClick: vi.fn(), + onSelect: vi.fn(), +})) + +vi.mock('@/components/ui/jam/Balance', () => ({ + Balance: ({ valueString, onClick }: { valueString: string; onClick?: () => void }) => ( + balance:{valueString} + ), +})) + +const DUMMY_JAR: Jar = { + balanceSummary: { + calculatedTotalBalanceInSats: 5, + calculatedAvailableBalanceInSats: 4, + calculatedConfirmedAvailableBalanceInSats: 3, + calculatedAvailableFrozenBalanceInSats: 2, + calculatedFrozenOrLockedBalanceInSats: 1, + }, + color: '#666', + jarIndex: 0, + name: 'Test jar', + utxos: [], +} + +describe('SelectableJar', () => { + beforeEach(() => { + mocks.onClick.mockReset() + mocks.onSelect.mockReset() + }) + + it('renders correctly', () => { + render( + , + ) + + expect(screen.getByText(DUMMY_JAR.name)).toBeInTheDocument() + expect(screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedAvailableBalanceInSats)).toBeInTheDocument() + expect( + screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedFrozenOrLockedBalanceInSats), + ).toBeInTheDocument() + }) + + it('can be selected', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.click(screen.getByRole('button')) + + expect(mocks.onClick).toHaveBeenCalled() + expect(mocks.onSelect).toHaveBeenCalled() + }) + + it('can not be selected if disabled', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.click(screen.getByRole('button')) + + expect(mocks.onClick).not.toHaveBeenCalled() + expect(mocks.onSelect).not.toHaveBeenCalled() + }) + + it('does not call onSelect if already selected', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.click(screen.getByRole('button')) + + expect(mocks.onClick).toHaveBeenCalled() + expect(mocks.onSelect).not.toHaveBeenCalled() + }) +}) diff --git a/src/components/ui/jam/SelectableJar.tsx b/src/components/ui/jam/SelectableJar.tsx index 60e76f9b..a526b7cf 100644 --- a/src/components/ui/jam/SelectableJar.tsx +++ b/src/components/ui/jam/SelectableJar.tsx @@ -4,7 +4,8 @@ import { Jar } from './Jar' interface SelectableJarProps extends ComponentProps { isSelected: NonNullable['checked']> - onClick: NonNullable['onChange']> + onClick?: NonNullable['onClick']> + onSelect: NonNullable['onChange']> } export const SelectableJar = ({ @@ -16,20 +17,19 @@ export const SelectableJar = ({ totalWalletBalance, isSelected, disabled = false, + onSelect, onClick, }: SelectableJarProps) => { const radioRef = useRef(null) return (
@@ -235,6 +241,7 @@ export const Interactive: Story = { frozenOrLockedBalance: 100_000, totalWalletBalance: 500_000, isSelected: false, + onSelect: () => alert('Selected'), onClick: () => alert('Interactive jar clicked'), }, parameters: { From 59e959da976dba58185f6130d496249a35c76b23 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Thu, 30 Jul 2026 16:52:26 +0200 Subject: [PATCH 17/56] fix(earn): disable fidelity bond actions while rescanning (#1373) --- src/components/earn/EarnPage.test.tsx | 96 +++++++++++++++++++++++++-- src/components/earn/EarnPage.tsx | 43 ++++++++---- 2 files changed, 121 insertions(+), 18 deletions(-) diff --git a/src/components/earn/EarnPage.test.tsx b/src/components/earn/EarnPage.test.tsx index e767f53d..49557b78 100644 --- a/src/components/earn/EarnPage.test.tsx +++ b/src/components/earn/EarnPage.test.tsx @@ -328,23 +328,109 @@ describe('EarnPage', () => { expect(screen.getByText('earn.alert_waiting_start_title')).toBeInTheDocument() }) - it('opens fidelity bond actions', async () => { - const user = userEvent.setup() + it('display debug output in developer mode', () => { mocks.developerMode = true mocks.walletInfo.fidelityBondSummary = { fbOutputs: [expiredBond] } render() - expect(screen.getByText('fidelity-bond:bond-tx:0')).toBeInTheDocument() expect(screen.getByText(/walletInfo\.fidelityBondSummary\.fbOutputs/u)).toBeInTheDocument() + }) - await user.click(screen.getByRole('button', { name: /earn\.fidelity_bond\.existing\.button_spend/u })) + it('enables creating a fidelity bond', async () => { + const user = userEvent.setup() + + expect(mocks.walletInfo.fidelityBondSummary.fbOutputs, 'sanity check').toHaveLength(0) + + render() + + expect(screen.getByText('create-bond-dialog:false')).toBeInTheDocument() + + const createFidelityBondButton = screen.getByRole('button', { + name: 'earn.fidelity_bond.create_form.button_create', + }) + expect(createFidelityBondButton).toBeEnabled() + + await user.click(createFidelityBondButton) + expect(screen.getByText('create-bond-dialog:true')).toBeInTheDocument() + }) + + it('disables creating a fidelity bond while rescanning', async () => { + const user = userEvent.setup() + setSession({ + rescanning: true, + }) + + expect(mocks.walletInfo.fidelityBondSummary.fbOutputs, 'sanity check').toHaveLength(0) + + render() + + expect(screen.getByText('create-bond-dialog:false')).toBeInTheDocument() + + const createFidelityBondButton = screen.getByRole('button', { + name: 'earn.fidelity_bond.create_form.button_create', + }) + expect(createFidelityBondButton).toBeDisabled() + + await user.click(createFidelityBondButton) + expect(screen.getByText('create-bond-dialog:false')).toBeInTheDocument() + }) + + it('enables fidelity bond actions on existing fidelity bond', async () => { + const user = userEvent.setup() + mocks.walletInfo.fidelityBondSummary = { fbOutputs: [expiredBond] } + + render() + + expect(screen.getByText('fidelity-bond:bond-tx:0')).toBeInTheDocument() + + expect(screen.queryByText('move-to-jar-dialog:false')).not.toBeInTheDocument() + expect(screen.queryByText('move-to-jar-dialog:true')).not.toBeInTheDocument() + expect(screen.queryByText('renew-bond-dialog:false')).not.toBeInTheDocument() + expect(screen.queryByText('renew-bond-dialog:true')).not.toBeInTheDocument() + + const moveToJarButton = screen.getByRole('button', { name: /earn\.fidelity_bond\.existing\.button_spend/u }) + expect(moveToJarButton).toBeEnabled() + + await user.click(moveToJarButton) expect(screen.getByText('move-to-jar-dialog:true')).toBeInTheDocument() - await user.click(screen.getByRole('button', { name: /earn\.fidelity_bond\.existing\.button_renew/u })) + const renewButton = screen.getByRole('button', { name: /earn\.fidelity_bond\.existing\.button_renew/u }) + expect(renewButton).toBeEnabled() + + await user.click(renewButton) expect(screen.getByText('renew-bond-dialog:true')).toBeInTheDocument() }) + it('disables fidelity bond actions on existing fidelity bond when rescanning is active', async () => { + const user = userEvent.setup() + mocks.walletInfo.fidelityBondSummary = { fbOutputs: [expiredBond] } + setSession({ + rescanning: true, + }) + + render() + + expect(screen.queryByText('move-to-jar-dialog:false')).not.toBeInTheDocument() + expect(screen.queryByText('move-to-jar-dialog:true')).not.toBeInTheDocument() + expect(screen.queryByText('renew-bond-dialog:false')).not.toBeInTheDocument() + expect(screen.queryByText('renew-bond-dialog:true')).not.toBeInTheDocument() + + const moveToJarButton = screen.getByRole('button', { name: /earn\.fidelity_bond\.existing\.button_spend/u }) + expect(moveToJarButton).toBeDisabled() + + await user.click(moveToJarButton) + expect(screen.queryByText('move-to-jar-dialog:false')).not.toBeInTheDocument() + expect(screen.queryByText('move-to-jar-dialog:true')).not.toBeInTheDocument() + + const renewButton = screen.getByRole('button', { name: /earn\.fidelity_bond\.existing\.button_renew/u }) + expect(renewButton).toBeDisabled() + + await user.click(renewButton) + expect(screen.queryByText('renew-bond-dialog:false')).not.toBeInTheDocument() + expect(screen.queryByText('renew-bond-dialog:true')).not.toBeInTheDocument() + }) + it('shows the coinjoin-in-progress alert', () => { setSession({ coinjoin_in_process: true }) render() diff --git a/src/components/earn/EarnPage.tsx b/src/components/earn/EarnPage.tsx index 932ff220..b440e7b2 100644 --- a/src/components/earn/EarnPage.tsx +++ b/src/components/earn/EarnPage.tsx @@ -24,6 +24,7 @@ import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, import { FeeConfigErrorAlert } from '@/components/ui/jam/FeeConfigErrorAlert' import { PageLoading } from '@/components/ui/jam/PageLoading' import PageTitle from '@/components/ui/jam/PageTitle' +import { isDevMode } from '@/constants/debugFeatures' import * as JAM from '@/constants/jam' import { routes } from '@/constants/routes' import { useJamWalletInfoContext } from '@/context/JamWalletInfoContext' @@ -106,6 +107,8 @@ const NoSpendableBalanceAlert = () => { ) } +const JAM_EARN_CREATE_MULTIPLE_FIDELITY_BONDS_ENABLED = isDevMode() + export const EarnPage = ({ walletFileName }: EarnPageProps) => { const { t } = useTranslation() const client = useApiClient() @@ -182,6 +185,19 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { const waitingForMakerUpdate = isWaitingMakerStart || isWaitingMakerStop const waitingForOfferUpdate = makerRunning && !isCurrentOfferAvailable + const hasFidelityBond = walletInfo.fidelityBondSummary.fbOutputs.length > 0 + const isFidelityBondActionsEnabled = + jmSession?.rescanning === false && + jmSession.maker_running === false && + jmSession.coinjoin_in_process === false && + !waitingForMakerUpdate && + !waitingForOfferUpdate && + !walletInfo.isFetching + + const isCreateFidelityBondEnabled = + isFidelityBondActionsEnabled && (!hasFidelityBond || JAM_EARN_CREATE_MULTIPLE_FIDELITY_BONDS_ENABLED) + const showCreateAdditionalFidelityBond = JAM_EARN_CREATE_MULTIPLE_FIDELITY_BONDS_ENABLED + useRefreshSession({ enabled: waitingForMakerUpdate || waitingForOfferUpdate, refetchInterval: WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL, @@ -322,7 +338,7 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { {/* Fidelity Bonds */} - {walletInfo.fidelityBondSummary.fbOutputs.length === 0 ? ( + {!hasFidelityBond ? (
{ - @@ -354,14 +374,7 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => {
{walletInfo.fidelityBondSummary.fbOutputs.map((it) => { const isExpired = !fb.utxo.isLocked(it) - const actionsEnabled = - isExpired && - !jmSession.rescanning && - !jmSession.maker_running && - !jmSession.coinjoin_in_process && - !waitingForMakerUpdate && - !waitingForOfferUpdate && - !walletInfo.isFetching + const actionsEnabled = isExpired && isFidelityBondActionsEnabled return ( {isExpired && ( @@ -390,10 +403,14 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { ) })}
- {/* Existing bonds hide the create button — expose it in developer mode to test multiple bonds */} - {isDeveloperMode && ( + + {showCreateAdditionalFidelityBond && (
- From 306e6b83d1dc7eaad5b853928bf5418903e882e1 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Thu, 30 Jul 2026 16:52:44 +0200 Subject: [PATCH 18/56] chore(orderbook): improve bond and relative fee display value (#1384) * chore(orderbook): improve bond and relative fee display value * ui(earn): hover effects on fidelity bond and offer cards * fix: use correct repo url base * chore(jar-detials): improve confirmations display value --- .env | 2 +- src/components/earn/FidelityBondCard.tsx | 7 ++- src/components/earn/OfferCard.tsx | 2 +- src/components/orderbook/OrderbookContent.tsx | 2 +- src/components/orderbook/OrderbookTable.tsx | 58 +++++++++++-------- src/components/wallet/JarUtxosTable.tsx | 34 +++++------ 6 files changed, 59 insertions(+), 46 deletions(-) diff --git a/.env b/.env index 207d5a1c..3a29ce53 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ VITE_JAM_DEFAULT_THEME=dark -VITE_JAM_REPO_URL=https://github.com/joinmarket-webui +VITE_JAM_REPO_URL=https://github.com/joinmarket-webui/jam VITE_JAM_DOCS_URL=https://jamdocs.org VITE_JAM_MATRIX_URL=https://matrix.to/#/%23jam:bitcoin.kyoto VITE_JAM_TELEGRAM_URL=https://t.me/JoinMarketWebUI diff --git a/src/components/earn/FidelityBondCard.tsx b/src/components/earn/FidelityBondCard.tsx index 7b6e10f0..ef254522 100644 --- a/src/components/earn/FidelityBondCard.tsx +++ b/src/components/earn/FidelityBondCard.tsx @@ -6,14 +6,15 @@ import { Balance } from '@/components/ui/jam/Balance' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos' import * as fb from '@/lib/fidelityBondUtils' -import { time } from '@/lib/utils' +import { cn, time } from '@/lib/utils' import { Address } from '../ui/jam/Address' interface FidelityBondCardProps { value: FidelityBondUtxo + className?: string } -export function FidelityBondCard({ value, children }: PropsWithChildren) { +export function FidelityBondCard({ value, className, children }: PropsWithChildren) { const { t, i18n } = useTranslation() const isExpired = !fb.utxo.isLocked(value) @@ -31,7 +32,7 @@ export function FidelityBondCard({ value, children }: PropsWithChildren + {isExpired ? ( diff --git a/src/components/earn/OfferCard.tsx b/src/components/earn/OfferCard.tsx index 33eaae1e..10077199 100644 --- a/src/components/earn/OfferCard.tsx +++ b/src/components/earn/OfferCard.tsx @@ -42,7 +42,7 @@ export function OfferCard({ className, value, nickname, children }: PropsWithChi const { t } = useTranslation() return ( - + {t('earn.current.text_offer')} diff --git a/src/components/orderbook/OrderbookContent.tsx b/src/components/orderbook/OrderbookContent.tsx index ae616ade..cd36028d 100644 --- a/src/components/orderbook/OrderbookContent.tsx +++ b/src/components/orderbook/OrderbookContent.tsx @@ -87,7 +87,7 @@ const offerToTableEntry = ( maximumSize: String(offer.maxsize || 0), bondValue: { value: offer.fidelity_bond_value || 0, - displayValue: String((offer.fidelity_bond_value || 0).toFixed(0)), + displayValue: Math.floor(offer.fidelity_bond_value || 0).toLocaleString(), locktime: fidelityBond?.locktime, displayLocktime: fidelityBond?.locktime ? new Date(fidelityBond.locktime * 1_000).toDateString() : undefined, displayExpiresIn: fidelityBond?.locktime diff --git a/src/components/orderbook/OrderbookTable.tsx b/src/components/orderbook/OrderbookTable.tsx index f4e1289c..41203c41 100644 --- a/src/components/orderbook/OrderbookTable.tsx +++ b/src/components/orderbook/OrderbookTable.tsx @@ -116,16 +116,19 @@ export const OrderbookTable = ({ columnHelper.accessor<'type', OrderTableEntry['type']>('type', { header: () =>
{t('orderbook.table.heading_type')}
, sortingFn: (a, b) => a.original.type.displayValue.localeCompare(b.original.type.displayValue), - cell: (info) => ( - - - {info.getValue().displayValue} - - -

{info.getValue().tooltip}

-
-
- ), + cell: (info) => { + const value = info.getValue() + return ( + + + {value.displayValue} + + +

{value.tooltip}

+
+
+ ) + }, meta: { align: 'center', alphabetic: true, @@ -140,12 +143,18 @@ export const OrderbookTable = ({ } return a.original.fee.value - b.original.fee.value }, - cell: (info) => { - const entry = info.row.original - return entry.fee.displayValue.includes('%') ? ( - {entry.fee.displayValue} + cell: ({ + row: { + original: { + type: { isAbsolute }, + fee, + }, + }, + }) => { + return isAbsolute ? ( + {fee.displayValue} ) : ( - + ) }, meta: { @@ -184,26 +193,29 @@ export const OrderbookTable = ({ columnHelper.accessor('bondValue', { header: () =>
{t('orderbook.table.heading_bond_value')}
, sortingFn: (a, b) => a.original.bondValue.value - b.original.bondValue.value, - cell: (info) => { - const entry = info.row.original - return entry.bondValue.value > 0 ? ( + cell: ({ + row: { + original: { bondValue }, + }, + }) => { + return bondValue.value > 0 ? ( - {entry.bondValue.displayValue} + {bondValue.displayValue}
- - {entry.bondValue.displayLocktime && ( + + {bondValue.displayLocktime && (
- {entry.bondValue.displayLocktime} ({entry.bondValue.displayExpiresIn}) + {bondValue.displayLocktime} ({bondValue.displayExpiresIn})
)}
) : ( - <>{entry.bondValue.displayValue} + <>{bondValue.displayValue} ) }, meta: { diff --git a/src/components/wallet/JarUtxosTable.tsx b/src/components/wallet/JarUtxosTable.tsx index f6ac6e5a..f5fb6be7 100644 --- a/src/components/wallet/JarUtxosTable.tsx +++ b/src/components/wallet/JarUtxosTable.tsx @@ -147,14 +147,12 @@ const utxoTableColumns = (enableSelectAllToggle: boolean, t: TFunction): ColumnD ), }, columnHelper.accessor('utxo.frozen', { - header: () =>
, - sortingFn: (a, b) => { - const val = (a.original.utxo.frozen ? 1 : 0) - (b.original.utxo.frozen ? 1 : 0) + header: () => <>, + sortingFn: ({ original: { utxo: a } }, { original: { utxo: b } }) => { + const val = (a.frozen ? 1 : 0) - (b.frozen ? 1 : 0) if (val !== 0) return val // tie-break using confirmations - const aid = Number(a.original.utxo.confirmations) - const bid = Number(b.original.utxo.confirmations) - return aid - bid + return a.confirmations - b.confirmations }, cell: (info) => { return ( @@ -172,14 +170,12 @@ const utxoTableColumns = (enableSelectAllToggle: boolean, t: TFunction): ColumnD }, }), columnHelper.accessor('utxo.value', { - header: () =>
{t('jar_details.utxo_list.column_title_balance')}
, - sortingFn: (a, b) => { - const val = a.original.utxo.value - b.original.utxo.value + header: () => t('jar_details.utxo_list.column_title_balance'), + sortingFn: ({ original: { utxo: a } }, { original: { utxo: b } }) => { + const val = a.value - b.value if (val !== 0) return val // tie-break using confirmations - const aid = Number(a.original.utxo.confirmations) - const bid = Number(b.original.utxo.confirmations) - return aid - bid + return a.confirmations - b.confirmations }, cell: (info) => , meta: { @@ -189,17 +185,21 @@ const utxoTableColumns = (enableSelectAllToggle: boolean, t: TFunction): ColumnD }), columnHelper.accessor('utxo.confirmations', { header: () => t('jar_details.utxo_list.column_title_confirmations'), - cell: (info) => <>{info.getValue()}, + cell: (info) => {info.getValue()}, meta: { numeric: true, - align: 'center', + align: 'right', }, }), columnHelper.accessor('tags', { header: () => t('jar_details.utxo_list.column_title_label_and_status'), - cell: (info) => ( + cell: ({ + row: { + original: { tags }, + }, + }) => (
- {info.row.original.tags.map((it, index) => { + {tags.map((it, index) => { const tooltipKey = `jar_details.utxo_list.utxo_tag_tooltip_${it.value.replaceAll(':', '_')}` const tooltip = t(tooltipKey) const hasTooltip = tooltip !== tooltipKey @@ -214,7 +214,7 @@ const utxoTableColumns = (enableSelectAllToggle: boolean, t: TFunction): ColumnD enableSorting: false, }), columnHelper.accessor<'utxo.address', UtxoTableEntry['utxo']['address']>('utxo.address', { - header: () =>
{t('jar_details.utxo_list.column_title_address')}
, + header: () => t('jar_details.utxo_list.column_title_address'), sortingFn: (a, b) => { const val = a.original.utxo.address.localeCompare(b.original.utxo.address) if (val !== 0) return val From 9346804bf85e58e595fbf67e7cd50d767458927a Mon Sep 17 00:00:00 2001 From: Brian Soltani Date: Thu, 30 Jul 2026 13:43:42 -0400 Subject: [PATCH 19/56] fix(wallet): keep jars visible and clickable during background refetch (#1392) * fix(wallet): keep jars visible and clickable during background refetch Gate the jars grid on isLoading instead of isFetching, matching the balance display above it. isFetching is true on every background refetch, so the jars (and the Jar Details overlay they open) were being replaced by a spinner any time wallet data refreshed, even though the previous jars were still valid to show. * test(wallet): assert jars stay visible during background refetch Update the jars spinner test to match the new isLoading-based gate. The test now asserts that a jar name stays on screen and the background refetch still calls refetch on click, instead of asserting a spinner appears. * test(wallet): assert jar overlay opens on click during background refetch --- src/components/MainWalletPage.test.tsx | 8 ++++++-- src/components/MainWalletPage.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/MainWalletPage.test.tsx b/src/components/MainWalletPage.test.tsx index 5d456415..a2362747 100644 --- a/src/components/MainWalletPage.test.tsx +++ b/src/components/MainWalletPage.test.tsx @@ -125,10 +125,14 @@ describe('MainWalletPage', () => { expect(refetch).toHaveBeenCalled() }) - it('shows a jars spinner while fetching and refreshes on click', () => { + it('keeps jars visible and clickable while fetching in the background', () => { walletInfo = { ...walletInfo, isFetching: true } render() - expect(screen.getByTestId('spinner')).toBeInTheDocument() + expect(screen.getByText(jars[0].name)).toBeInTheDocument() + + fireEvent.click(screen.getByText(jars[0].name)) + expect(screen.getByTestId('WalletJarsDetailsOverlay#open')).toHaveTextContent('open') + expect(screen.getByTestId('WalletJarsDetailsOverlay#selectedJarIndex')).toHaveTextContent(String(jars[0].jarIndex)) fireEvent.click(screen.getByText('global.refresh')) expect(refetch).toHaveBeenCalled() }) diff --git a/src/components/MainWalletPage.tsx b/src/components/MainWalletPage.tsx index 3dbcdc4a..a239142f 100644 --- a/src/components/MainWalletPage.tsx +++ b/src/components/MainWalletPage.tsx @@ -116,7 +116,7 @@ export default function MainWalletPage({ walletFileName }: MainWalletPageProps)
- {isFetching ? ( + {isLoading ? (
{t('global.loading')} From 02115e808a360d5bdb47037b7b383587c6e50819 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Thu, 30 Jul 2026 21:23:08 +0200 Subject: [PATCH 20/56] chore(ci): pin ci actions (#1389) --- .github/workflows/build.yml | 8 ++++---- .github/workflows/codeql-analysis.yml | 4 +++- .github/workflows/storybook-build.yml | 6 ++++-- .github/workflows/storybook-pages.yml | 10 +++++----- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d8095b7..db7f7bd4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup (Node.js ${{ matrix.node-version }}) - uses: actions/setup-node@v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node-version }} @@ -30,7 +30,7 @@ jobs: run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package-lock.json').packages['node_modules/@playwright/test'].version)")" >> $GITHUB_ENV - name: Cache Playwright - uses: actions/cache@v5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 id: playwright-cache with: path: | @@ -59,4 +59,4 @@ jobs: # Set if: always() to also generate the report if tests are failing # Only works if you set `reportOnFailure: true` in vite config if: ${{ always() && hashFiles('coverage/coverage-summary.json') != '' }} - uses: davelosert/vitest-coverage-report-action@v2 + uses: davelosert/vitest-coverage-report-action@8b157684c6a6b259b97d45e72b44242865c0f6a5 # v2.12.2 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e89f19fa..ae329291 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -41,7 +41,9 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/storybook-build.yml b/.github/workflows/storybook-build.yml index 35594961..01d50de6 100644 --- a/.github/workflows/storybook-build.yml +++ b/.github/workflows/storybook-build.yml @@ -12,10 +12,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc diff --git a/.github/workflows/storybook-pages.yml b/.github/workflows/storybook-pages.yml index a0cdacb5..e23a3876 100644 --- a/.github/workflows/storybook-pages.yml +++ b/.github/workflows/storybook-pages.yml @@ -24,10 +24,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc @@ -38,13 +38,13 @@ jobs: run: npm run storybook:build-gh-pages - name: Configure Pages - uses: actions/configure-pages@v6 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v5 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: storybook-static - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 From e0d747971190a984bf20b9338c9f2c139aeda9b7 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Thu, 30 Jul 2026 21:25:10 +0200 Subject: [PATCH 21/56] build(deps): update dependencies (#1390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build(deps): update dependencies @vitejs/plugin-react 6.0.4 → 6.0.5 lucide-react 1.26.0 → 1.28.0 react-router-dom 7.18.1 → 7.18.2 vite 8.1.5 → 8.2.0 @storybook/addon-a11y 10.5.4 → 10.5.5 @storybook/addon-docs 10.5.4 → 10.5.5 @storybook/addon-vitest 10.5.4 → 10.5.5 @storybook/react-vite 10.5.4 → 10.5.5 @types/node 26.1.1 → 26.1.2 eslint-plugin-storybook 10.5.4 → 10.5.5 storybook 10.5.4 → 10.5.5 @playwright/test 1.61.1 → 1.62.0 @testing-library/jest-dom 6.9.1 → 6.10.0 globals 17.7.0 → 17.8.0 * build: fix vitest warnings after update * build(deps): update @hookform/resolvers to v5.5.7 * build(deps): update msw-storybook-addon to v3.0.0 note: still uses a deprecated feature as the migration path did not work properly * build(dev-deps): update jsdom to v30.0.1 --- .storybook/preview.tsx | 30 +- package-lock.json | 1043 +++++++++++++++++++++--------- package.json | 34 +- src/components/earn/EarnForm.tsx | 5 +- vite.config.ts | 2 +- vitest.config.ts | 6 +- 6 files changed, 788 insertions(+), 332 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index d4363bf0..b6009ab1 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,7 +2,8 @@ import { Suspense, useEffect } from 'react' import type { Preview } from '@storybook/react-vite' import { QueryClientProvider, QueryClient } from '@tanstack/react-query' -import { initialize, mswLoader } from 'msw-storybook-addon' +import { mswLoader } from 'msw-storybook-addon/csf3' +import { setupWorker } from 'msw/browser' import { ThemeProvider } from 'next-themes' import { I18nextProvider } from 'react-i18next' import { MemoryRouter } from 'react-router-dom' @@ -17,15 +18,6 @@ import mswHandlers from './msw-handlers' // needed if you want to use msw on a subpath (e.g. github pages /) const mswServiceWorkerUrl = import.meta.env.STORYBOOK_MSW_SERVICE_WORKER_URL ?? '/mockServiceWorker.js' -// Initialize MSW (https://github.com/mswjs/msw-storybook-addon) -initialize({ - onUnhandledRequest: 'bypass', - serviceWorker: { - url: mswServiceWorkerUrl, - }, - quiet: true, -}) - const locales = [ { value: 'en', title: 'en' }, { value: 'fr', title: 'fr' }, @@ -85,12 +77,28 @@ const preview: Preview = { viewport: { options: { ...MINIMAL_VIEWPORTS, ...INITIAL_VIEWPORTS }, }, + // this approach is deprecated as of v3.0.0 + // but it seems the new approach does not work as expected (404 response for our handlers) + // revisit after next update (last checked 2026-07-30) + // see: https://github.com/mswjs/msw-storybook-addon/blob/v3.0.0/MIGRATION.md#parametersmsw-is-deprecated-in-favor-of-beforeeach msw: { handlers: mswHandlers, }, }, // Provide the MSW addon loader globally - loaders: [mswLoader], + loaders: [ + mswLoader(async () => { + const worker = setupWorker() + await worker.start({ + onUnhandledRequest: 'bypass', + serviceWorker: { + url: mswServiceWorkerUrl, + }, + quiet: true, + }) + return worker + }), + ], } const withTheme = (Story: React.ComponentType, context: GlobalContext) => { diff --git a/package-lock.json b/package-lock.json index 4d4070f3..50e50ebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.0.0-beta.1", "license": "MIT", "dependencies": { - "@hookform/resolvers": "5.4.0", + "@hookform/resolvers": "5.5.7", "@joinmarket-webui/joinmarket-ng-api-ts": "1.0.0", "@noble/hashes": "2.2.0", "@radix-ui/react-accordion": "1.2.20", @@ -38,7 +38,7 @@ "clsx": "2.1.1", "html5-qrcode": "2.3.8", "i18next-browser-languagedetector": "8.2.1", - "lucide-react": "1.26.0", + "lucide-react": "1.28.0", "next-themes": "0.4.6", "qrcode": "1.5.4", "radix-ui": "1.6.7", @@ -46,7 +46,7 @@ "react-dom": "19.2.8", "react-hook-form": "7.83.0", "react-i18next": "17.0.11", - "react-router-dom": "7.18.1", + "react-router-dom": "7.18.2", "react-use-websocket": "4.13.0", "sonner": "2.0.7", "tailwind-merge": "3.6.0", @@ -58,21 +58,21 @@ "devDependencies": { "@chromatic-com/storybook": "5.2.1", "@eslint/js": "9.39.5", - "@playwright/test": "1.61.1", - "@storybook/addon-a11y": "10.5.4", - "@storybook/addon-docs": "10.5.4", - "@storybook/addon-vitest": "10.5.4", - "@storybook/react-vite": "10.5.4", + "@playwright/test": "1.62.0", + "@storybook/addon-a11y": "10.5.5", + "@storybook/addon-docs": "10.5.5", + "@storybook/addon-vitest": "10.5.5", + "@storybook/react-vite": "10.5.5", "@tanstack/eslint-plugin-query": "5.101.4", - "@testing-library/jest-dom": "6.9.1", + "@testing-library/jest-dom": "7.0.0", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", "@trivago/prettier-plugin-sort-imports": "6.0.2", - "@types/node": "26.1.1", + "@types/node": "26.1.2", "@types/qrcode": "1.5.6", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "6.0.4", + "@vitejs/plugin-react": "6.0.5", "@vitest/browser": "4.1.10", "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "4.1.10", @@ -81,20 +81,20 @@ "eslint-plugin-compat": "7.0.2", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.3", - "eslint-plugin-storybook": "10.5.4", + "eslint-plugin-storybook": "10.5.5", "eslint-plugin-unicorn": "65.0.1", - "globals": "17.7.0", + "globals": "17.8.0", "husky": "9.1.7", - "jsdom": "29.1.1", + "jsdom": "30.0.1", "lint-staged": "17.2.0", "msw": "2.15.0", - "msw-storybook-addon": "2.0.7", + "msw-storybook-addon": "3.0.0", "prettier": "3.9.6", "prettier-plugin-tailwindcss": "0.8.1", - "storybook": "10.5.4", + "storybook": "10.5.5", "typescript": "6.0.3", "typescript-eslint": "8.65.0", - "vite": "8.1.5", + "vite": "8.2.0", "vitest": "4.1.10" }, "engines": { @@ -110,56 +110,58 @@ "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz", + "integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-calc": "^3.2.1", + "@csstools/css-color-parser": "^4.1.9", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.0.tgz", + "integrity": "sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "20 || >=22" } }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -537,9 +539,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -561,9 +563,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", - "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", "dev": true, "funding": [ { @@ -578,7 +580,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.2.1" + "@csstools/css-calc": "^3.3.0" }, "engines": { "node": ">=20.19.0" @@ -612,9 +614,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", - "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", "dev": true, "funding": [ { @@ -1148,9 +1150,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1221,10 +1223,27 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1245,6 +1264,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -1352,15 +1378,113 @@ "license": "MIT" }, "node_modules/@hookform/resolvers": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", - "integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==", + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.5.7.tgz", + "integrity": "sha512-CyPCYV8/KlXfEXLWj8HHHhVsR/IZ6Ckm3b/a4fWtO/lRnRK1huqncb8LlAWrmpPRsse5glF5aVuDRMHEr3UGag==", "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { - "react-hook-form": "^7.55.0" + "@sinclair/typebox": ">=0.25.24", + "@standard-schema/spec": "^1.0.0", + "@typeschema/main": ">=0.13.7", + "@vinejs/vine": "^2.0.0 || ^3.0.0", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "arktype": "^2.0.0", + "ata-validator": "^0.7.0", + "class-transformer": ">=0.4.0", + "class-validator": ">=0.12.0", + "computed-types": "^1.0.0", + "effect": "^3.10.3", + "fluentvalidation-ts": "^3.0.0", + "fp-ts": "^2.7.0", + "io-ts": "^2.0.0", + "joi": "^17.0.0", + "nope-validator": ">=0.12.0", + "react-hook-form": "^7.55.0", + "superstruct": ">=0.12.0", + "typanion": "^3.3.2", + "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", + "vest": ">=3.0.0", + "yup": "^1.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + }, + "@standard-schema/spec": { + "optional": true + }, + "@typeschema/main": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "ajv": { + "optional": true + }, + "ajv-errors": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "arktype": { + "optional": true + }, + "ata-validator": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "computed-types": { + "optional": true + }, + "effect": { + "optional": true + }, + "fluentvalidation-ts": { + "optional": true + }, + "fp-ts": { + "optional": true + }, + "io-ts": { + "optional": true + }, + "joi": { + "optional": true + }, + "nope-validator": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "typanion": { + "optional": true + }, + "valibot": { + "optional": true + }, + "vest": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } } }, "node_modules/@humanfs/core": { @@ -2428,19 +2552,19 @@ ] }, "node_modules/@playwright/test": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", - "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", + "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.61.1" + "playwright": "1.62.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@polka/url": { @@ -3947,9 +4071,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", "cpu": [ "arm64" ], @@ -3963,9 +4087,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", "cpu": [ "arm64" ], @@ -3979,9 +4103,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", "cpu": [ "x64" ], @@ -3995,9 +4119,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", "cpu": [ "x64" ], @@ -4011,9 +4135,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", "cpu": [ "arm" ], @@ -4027,9 +4151,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", "cpu": [ "arm64" ], @@ -4046,9 +4170,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", "cpu": [ "arm64" ], @@ -4065,9 +4189,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", "cpu": [ "ppc64" ], @@ -4084,9 +4208,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", "cpu": [ "s390x" ], @@ -4103,9 +4227,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", "cpu": [ "x64" ], @@ -4122,9 +4246,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", "cpu": [ "x64" ], @@ -4141,9 +4265,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", "cpu": [ "arm64" ], @@ -4157,38 +4281,35 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" } }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.2", + "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" } }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", "license": "MIT", "optional": true, "dependencies": { @@ -4196,19 +4317,40 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz", + "integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", "cpu": [ "arm64" ], @@ -4222,9 +4364,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", "cpu": [ "x64" ], @@ -4365,7 +4507,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@standard-schema/utils": { @@ -4375,9 +4517,9 @@ "license": "MIT" }, "node_modules/@storybook/addon-a11y": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.4.tgz", - "integrity": "sha512-UWdZtB7Dh1GjqxTPOrisUNDhDGF5pKGzZdzW9DSSHMBcAOx2dsTmXGzLSFprAz4LTT0OHCtKhxNqKK0JZJ0Y8g==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.5.tgz", + "integrity": "sha512-nsMnSRe7pzepXIkUUqI/rL7sp8juXOluyypU4Dz0UuYHhw/cKaxfzuO+3WJN5EtEr/gcnKYb4awxH/duPGavUw==", "dev": true, "license": "MIT", "dependencies": { @@ -4389,20 +4531,20 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.4" + "storybook": "^10.5.5" } }, "node_modules/@storybook/addon-docs": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.4.tgz", - "integrity": "sha512-2Z/x2pKEmXOCQjmttYzPuQBu9aWeMly8uEs3msrCTBLiHs/F7IlBFnMu0Z+T2Qvk0LEy8O93AlcPSP76aCcKjw==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.5.tgz", + "integrity": "sha512-0YpKlimS4XE0kQ8Maa5coeefQxdyDrBHg1wOP3WTPuBe4FolFSCDveR0ge2+vuUBk+fZfn2+l+3Q2jmAWaRGDg==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.5.4", + "@storybook/csf-plugin": "10.5.5", "@storybook/icons": "^2.0.2", - "@storybook/react-dom-shim": "10.5.4", + "@storybook/react-dom-shim": "10.5.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -4413,7 +4555,7 @@ }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.4" + "storybook": "^10.5.5" }, "peerDependenciesMeta": { "@types/react": { @@ -4422,9 +4564,9 @@ } }, "node_modules/@storybook/addon-vitest": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.4.tgz", - "integrity": "sha512-5lr81sgrbK3Tjjbstieg/yYj3jdtQYP70ihpOiN1TwogvFATY2/9eMT46FE5ioKtEFcfJRDcgJ2wSlmJKE+kZw==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.5.tgz", + "integrity": "sha512-Ymq9ErkSkYiIDuqpJ2+hE5GCQ5J6TCLOWhutqArvwaeAO+HAibM82XNExpJ1/kvPqk9y961GDPkv2W15I88JIw==", "dev": true, "license": "MIT", "dependencies": { @@ -4439,7 +4581,7 @@ "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", - "storybook": "^10.5.4", + "storybook": "^10.5.5", "vitest": "^3.0.0 || ^4.0.0" }, "peerDependenciesMeta": { @@ -4458,13 +4600,13 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.4.tgz", - "integrity": "sha512-eXdgow+brSzZrG3CnG18YwxZwEYNAZIh2G5qKwNoZf0uk2ZCPgLRQwtVAcd7BiiEDGnXUHm9pycAS+UiF4F4Mg==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.5.tgz", + "integrity": "sha512-dQoJ7gUl8y0z5rV9cE0mz6qTBNmN9R4GOLIZk98rJ8CwduNJOb9eGZXusDzzvnYcp8TnNkqDtyx4tXQSUDInPQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.5.4", + "@storybook/csf-plugin": "10.5.5", "ts-dedent": "^2.0.0" }, "funding": { @@ -4472,14 +4614,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.4", + "storybook": "^10.5.5", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.4.tgz", - "integrity": "sha512-DSp5Z/eZlRnKq0KrKLJE6uoYf/Ysc+FP0Z5DVTGnOrie+z3tC0lNi9I4RB++EXkJeUDS9/4dxvJZVSWjhLlXxw==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.5.tgz", + "integrity": "sha512-/euibhRFqklYCZqUseokojmfYcQpXshVY2QmA1qCuxMz9SzVFD3iSTw+aFLTxpsJGGdcZJk8fnm/rEthLzZ9jA==", "dev": true, "license": "MIT", "dependencies": { @@ -4492,7 +4634,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.5.4", + "storybook": "^10.5.5", "vite": "*", "webpack": "*" }, @@ -4529,14 +4671,14 @@ } }, "node_modules/@storybook/react": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.4.tgz", - "integrity": "sha512-tOxfVgbYcaVsArN8XTDkJfdsnsnHh1LxjRHVpJ/N+VEkz4FveK/XH3jOLV0YqgrG8yXza7+CteDP4FfPVQY/mw==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.5.tgz", + "integrity": "sha512-T2Xj0ey7a9RHU6coYLC0L5lhjcdyhLCs9wNv15FvHvgmrRobkynEV72kq5vGW8tFkahNWI1X9+GZPQ6r8Nm38w==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.5.4", + "@storybook/react-dom-shim": "10.5.5", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, @@ -4549,7 +4691,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.4", + "storybook": "^10.5.5", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -4565,9 +4707,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.4.tgz", - "integrity": "sha512-YdlppEOReg8MvTECRNuf79gu2zL83JqKDHIR/65eS0M6y+ue9pkpfjYo7hZVIcyOcRd9npBDXMdt2kC92bCuaA==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.5.tgz", + "integrity": "sha512-PIk7N3LLrZIxfNxmkvmQN1d5UQ70XEedT8n0GhBiXnM6XL09xPGB8n8TZXeJBRYluKhDQcAyQeT0/OZmcDVQJg==", "dev": true, "license": "MIT", "funding": { @@ -4579,7 +4721,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.4" + "storybook": "^10.5.5" }, "peerDependenciesMeta": { "@types/react": { @@ -4591,16 +4733,16 @@ } }, "node_modules/@storybook/react-vite": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.4.tgz", - "integrity": "sha512-iw7EAA98n30vpf+ZSy2Ll4Ne7oyZ/lS6W22u5Sp5UOI2oAkYuklxIWjRIKGqpY0BHP+GqaGYtUGMZQnBqgul2g==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.5.tgz", + "integrity": "sha512-Uy7VV72kVSkw6aDTAPQupXUeZX5LF6e4zqNvTZ+36qxsXAkaFgw7HPEm7L1tsaRfiV+s9anU7UvX47tfJpYGuQ==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.5.4", - "@storybook/react": "10.5.4", + "@storybook/builder-vite": "10.5.5", + "@storybook/react": "10.5.5", "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.2", @@ -4614,7 +4756,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.4", + "storybook": "^10.5.5", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, @@ -5012,9 +5154,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", + "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", "dev": true, "license": "MIT", "dependencies": { @@ -5026,9 +5168,12 @@ "redent": "^3.0.0" }, "engines": { - "node": ">=14", + "node": ">=22", "npm": ">=6", "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" } }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { @@ -5230,9 +5375,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5576,9 +5721,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", - "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", "dev": true, "license": "MIT", "dependencies": { @@ -5882,23 +6027,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "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" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6102,9 +6230,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -7025,9 +7153,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.4.tgz", - "integrity": "sha512-IEG6GK9iMKH45WxiBUoDu3AajUyz+AcRXsLRkwnsNz9TwzZI6tbyHgdZ1Tkvs2RXOovYUYYksNpbtnmY746LAw==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.5.tgz", + "integrity": "sha512-xGCrddoZ8pMmwc7M5mp+habWbvqBjRuyGVTJ6RpavpoAP6NBUfGSRXHuBs7cHRDpDSuErYeDiEo4XHTonRp68g==", "dev": true, "license": "MIT", "dependencies": { @@ -7036,7 +7164,7 @@ }, "peerDependencies": { "eslint": ">=8", - "storybook": "^10.5.4" + "storybook": "^10.5.5" } }, "node_modules/eslint-plugin-unicorn": { @@ -7102,10 +7230,27 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -7126,6 +7271,13 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -7510,9 +7662,9 @@ } }, "node_modules/globals": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", - "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "version": "17.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz", + "integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==", "dev": true, "license": "MIT", "engines": { @@ -7941,39 +8093,39 @@ } }, "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", + "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", + "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "canvas": "^3.0.0" + "canvas": "^3.2.3" }, "peerDependenciesMeta": { "canvas": { @@ -7991,6 +8143,21 @@ "node": "20 || >=22" } }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8011,13 +8178,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -8422,9 +8582,9 @@ } }, "node_modules/lucide-react": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.26.0.tgz", - "integrity": "sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.28.0.tgz", + "integrity": "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8593,16 +8753,17 @@ } }, "node_modules/msw-storybook-addon": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.7.tgz", - "integrity": "sha512-TGmlxXy2TsaB6QcClVKRxqvay5f93xoLguHOihRFQ+gIEIyiyvcoQjkEeuOe7Y9qvddzGB1LyFomzPo9/EpnuQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-3.0.0.tgz", + "integrity": "sha512-tyYmYAwSGcfFSHhrE5yzun/OXuVIkBGOt4YTsH5IR94N38F+hzZhvGqe5scu1ENJh/hOApI/8ZRQkWdFNFsKcw==", "dev": true, "license": "MIT", - "dependencies": { - "is-node-process": "^1.0.1" + "bin": { + "msw-storybook-migrate": "build/migrate.mjs" }, "peerDependencies": { - "msw": "^2.0.0" + "msw": ">=2", + "storybook": ">=9" } }, "node_modules/mute-stream": { @@ -8967,35 +9128,35 @@ } }, "node_modules/playwright": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", - "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.61.1" + "playwright-core": "1.62.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", - "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/pluralize": { @@ -9602,9 +9763,9 @@ } }, "node_modules/react-router": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", - "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -9624,12 +9785,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", - "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "react-router": "7.18.1" + "react-router": "7.18.2" }, "engines": { "node": ">=20.0.0" @@ -9811,12 +9972,12 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", + "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -9826,27 +9987,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "@rolldown/binding-android-arm64": "1.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" } }, "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -10028,9 +10189,9 @@ "license": "MIT" }, "node_modules/storybook": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.4.tgz", - "integrity": "sha512-bmLxPsxVSPnbeiZqYQpozyNOiJXfk+pf7WfHZflvPkwT6Y+rvYz3Cj/D6H4Kf2jHpuDNiMXBKO3yawLN2OWirg==", + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.5.tgz", + "integrity": "sha512-UscBIBJDloUeqntukHOhP1a5W/vouePDJbzPSxj466WK801FZtzQiMffMtkjzJiWSuj20wfaYlB2QQKh9aOYAg==", "dev": true, "license": "MIT", "dependencies": { @@ -10050,7 +10211,7 @@ "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", - "ws": "^8.18.0" + "ws": "^8.21.1" }, "bin": { "storybook": "dist/bin/dispatcher.js" @@ -10076,6 +10237,26 @@ } } }, + "node_modules/storybook/node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/storybook/node_modules/@vitest/spy": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", @@ -10089,6 +10270,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/storybook/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -10495,13 +10683,13 @@ } }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", + "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", "dev": true, "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/undici-types": { @@ -10641,15 +10829,15 @@ } }, "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", + "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "bin": { @@ -10666,7 +10854,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -10731,6 +10919,267 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", @@ -10964,9 +11413,9 @@ } }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "dev": true, "license": "MIT", "engines": { @@ -11159,7 +11608,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index d6bdccc1..8b92dadc 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ ] }, "dependencies": { - "@hookform/resolvers": "5.4.0", + "@hookform/resolvers": "5.5.7", "@joinmarket-webui/joinmarket-ng-api-ts": "1.0.0", "@noble/hashes": "2.2.0", "@radix-ui/react-accordion": "1.2.20", @@ -100,7 +100,7 @@ "clsx": "2.1.1", "html5-qrcode": "2.3.8", "i18next-browser-languagedetector": "8.2.1", - "lucide-react": "1.26.0", + "lucide-react": "1.28.0", "next-themes": "0.4.6", "qrcode": "1.5.4", "radix-ui": "1.6.7", @@ -108,7 +108,7 @@ "react-dom": "19.2.8", "react-hook-form": "7.83.0", "react-i18next": "17.0.11", - "react-router-dom": "7.18.1", + "react-router-dom": "7.18.2", "react-use-websocket": "4.13.0", "sonner": "2.0.7", "tailwind-merge": "3.6.0", @@ -120,21 +120,21 @@ "devDependencies": { "@chromatic-com/storybook": "5.2.1", "@eslint/js": "9.39.5", - "@playwright/test": "1.61.1", - "@storybook/addon-a11y": "10.5.4", - "@storybook/addon-docs": "10.5.4", - "@storybook/addon-vitest": "10.5.4", - "@storybook/react-vite": "10.5.4", + "@playwright/test": "1.62.0", + "@storybook/addon-a11y": "10.5.5", + "@storybook/addon-docs": "10.5.5", + "@storybook/addon-vitest": "10.5.5", + "@storybook/react-vite": "10.5.5", "@tanstack/eslint-plugin-query": "5.101.4", - "@testing-library/jest-dom": "6.9.1", + "@testing-library/jest-dom": "7.0.0", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", "@trivago/prettier-plugin-sort-imports": "6.0.2", - "@types/node": "26.1.1", + "@types/node": "26.1.2", "@types/qrcode": "1.5.6", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "6.0.4", + "@vitejs/plugin-react": "6.0.5", "@vitest/browser": "4.1.10", "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "4.1.10", @@ -143,20 +143,20 @@ "eslint-plugin-compat": "7.0.2", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.3", - "eslint-plugin-storybook": "10.5.4", + "eslint-plugin-storybook": "10.5.5", "eslint-plugin-unicorn": "65.0.1", - "globals": "17.7.0", + "globals": "17.8.0", "husky": "9.1.7", - "jsdom": "29.1.1", + "jsdom": "30.0.1", "lint-staged": "17.2.0", "msw": "2.15.0", - "msw-storybook-addon": "2.0.7", + "msw-storybook-addon": "3.0.0", "prettier": "3.9.6", "prettier-plugin-tailwindcss": "0.8.1", - "storybook": "10.5.4", + "storybook": "10.5.5", "typescript": "6.0.3", "typescript-eslint": "8.65.0", - "vite": "8.1.5", + "vite": "8.2.0", "vitest": "4.1.10" }, "overrides": { diff --git a/src/components/earn/EarnForm.tsx b/src/components/earn/EarnForm.tsx index f45b166a..9a4d7243 100644 --- a/src/components/earn/EarnForm.tsx +++ b/src/components/earn/EarnForm.tsx @@ -3,7 +3,7 @@ import { yupResolver } from '@hookform/resolvers/yup' import type { TFunction } from 'i18next' import { HandshakeIcon, PercentIcon } from 'lucide-react' import { useForm, useWatch } from 'react-hook-form' -import type { Resolver, SubmitHandler } from 'react-hook-form' +import type { SubmitHandler } from 'react-hook-form' import { useTranslation } from 'react-i18next' import * as yup from 'yup' import { Button } from '@/components/ui/button' @@ -179,8 +179,7 @@ export function EarnForm({ } = useForm({ mode: 'onSubmit', defaultValues: FORM_INPUT_DEFAULT_VALUES, - // force type (see https://github.com/react-hook-form/resolvers/issues/807) - resolver: yupResolver(schema) as Resolver, + resolver: yupResolver(schema), }) const values = useWatch({ control }) diff --git a/vite.config.ts b/vite.config.ts index ba35e702..0dad02fe 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -75,7 +75,7 @@ export default defineConfig((config): UserConfig => { ], resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': path.resolve(import.meta.dirname, './src'), }, }, server: { diff --git a/vitest.config.ts b/vitest.config.ts index 01666cb5..4c06ecea 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ import { playwright } from '@vitest/browser-playwright' import path from 'node:path' import { mergeConfig } from 'vite' import { ConfigEnv, coverageConfigDefaults, defineConfig, type ViteUserConfig } from 'vitest/config' -import viteConfig from './vite.config' +import viteConfig from './vite.config.ts' export default defineConfig((args: ConfigEnv): ViteUserConfig => { return mergeConfig(viteConfig(args), { @@ -41,7 +41,7 @@ export default defineConfig((args: ConfigEnv): ViteUserConfig => { plugins: [ // The plugin will run tests for the stories defined in your Storybook config // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - storybookTest({ configDir: path.resolve(__dirname, './.storybook') }), + storybookTest({ configDir: path.resolve(import.meta.dirname, './.storybook') }), ], test: { name: 'storybook', @@ -69,7 +69,7 @@ export default defineConfig((args: ConfigEnv): ViteUserConfig => { }, resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': path.resolve(import.meta.dirname, './src'), }, }, }, From d8e6d6aa15deb08634047abf946b60581bc77fde Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Fri, 31 Jul 2026 11:02:02 +0200 Subject: [PATCH 22/56] chore: define error type for manual coin control needed --- src/components/send/SendForm.schema.ts | 4 +++- src/components/send/SendPage.tsx | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/send/SendForm.schema.ts b/src/components/send/SendForm.schema.ts index 73589821..084c232a 100644 --- a/src/components/send/SendForm.schema.ts +++ b/src/components/send/SendForm.schema.ts @@ -21,6 +21,8 @@ export const initialNumberOfCollaborators = (minValue: number): number => { export const MIN_SEND_AMOUNT: AmountSats = 292 export const MAX_SEND_AMOUNT: AmountSats = TOTAL_COIN_SUPPLY +export const ERROR_SOURCE_JAR_NEEDS_MANUAL_COIN_CONTROL = 'valid-source-jar-needs-manual-coin-control-test' + const FORM_INPUT_DEFAULT_VALUES: Partial = { source: undefined, destination: undefined, @@ -68,7 +70,7 @@ export const createSendFormSchema = ( jars.some((it) => it.jarIndex === jarIndex), ) .test( - 'valid-source-jar-must-unfreeze-utxos-test', + ERROR_SOURCE_JAR_NEEDS_MANUAL_COIN_CONTROL, t('send.feedback_invalid_source_jar_must_unfreeze_utxos'), (jarIndex) => { const jar = jars.find((it) => it.jarIndex === jarIndex) diff --git a/src/components/send/SendPage.tsx b/src/components/send/SendPage.tsx index 21c8096a..60c49ad4 100644 --- a/src/components/send/SendPage.tsx +++ b/src/components/send/SendPage.tsx @@ -47,6 +47,7 @@ import { Spinner } from '../ui/spinner' import { PaymentAbortDialog } from './PaymentAbortDialog' import PaymentConfirmDialog from './PaymentConfirmDialog' import { SendForm } from './SendForm' +import { ERROR_SOURCE_JAR_NEEDS_MANUAL_COIN_CONTROL } from './SendForm.schema' import { UtxoSelectionDialog } from './UtxoSelectionDialog' import { buildCollaborativeSendRequest, buildNonCollaborativeSendRequest } from './collaborativeSend' import type { SendFormValues } from './types' @@ -612,7 +613,7 @@ export const SendPage = ({ walletFileName }: SendPageProps) => { onSourceJarChange={setSourceJarIndex} onSourceJarClicked={onOpenUtxoSelector} sourceJarLabelButton={(errors) => { - const errorSolvedByUtxoSelection = errors?.fromJar?.type === 'valid-source-jar-must-unfreeze-utxos-test' + const errorSolvedByUtxoSelection = errors?.fromJar?.type === ERROR_SOURCE_JAR_NEEDS_MANUAL_COIN_CONTROL const animate = utxoSelectionDialog.dialogProps.open === false && errorSolvedByUtxoSelection return ( + + + + +
- - {globalFilter === '' @@ -236,7 +259,7 @@ export const EarnReportContent = ({ className, enabled }: EarnReportContentProps : t('earn.report.text_report_summary_filtered', { count: table.getFilteredRowModel().rows.length })} {isDeveloperMode ? ( - - )} -
+ + + -
{/* Search match count */} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 40b2d072..fc0ec977 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -585,7 +585,6 @@ "heading_earned": "Earned", "heading_notes": "Notes", "text_button_generate_demo_report": "Generate demo entry", - "text_button_download_csv": "Download", "stats": { "earned_total": "Earned (total)", "earned_90days": "Earned (90 days)", From 28888dbb6d1167727c680c313a558387358c2dc0 Mon Sep 17 00:00:00 2001 From: Kishore B Date: Wed, 5 Aug 2026 18:18:52 +0530 Subject: [PATCH 40/56] refactor: externalize hardcoded polling intervals and delays (#1406) * refactor: externalize hardcoded polling intervals and delays Signed-off-by: kishore08-07 * refactor(constants): move schedule polling interval dev config to env.dev Signed-off-by: kishore08-07 --------- Signed-off-by: kishore08-07 --- .env.development | 2 + src/components/earn/EarnPage.tsx | 16 ++------ .../logging/useJmwalletdStdoutLog.ts | 3 +- src/components/send/SendPage.tsx | 9 +++-- src/components/sweep/SweepPage.tsx | 14 ++----- src/constants/jam.ts | 39 +++++++++++++++++++ 6 files changed, 55 insertions(+), 28 deletions(-) diff --git a/.env.development b/.env.development index 3a37e0c6..9b4b3df4 100644 --- a/.env.development +++ b/.env.development @@ -13,3 +13,5 @@ VITE_JAM_SWEEP_DESTINATION_ADDRESSES_MIN_COUNT=1 VITE_JAM_SWEEP_DESTINATION_ADDRESSES_DEFAULT_COUNT=3 VITE_JAM_SWEEP_MIN_MIN_NUMBER_OF_COLLABORATORS=1 + +VITE_JAM_RUNNING_SCHEDULE_POLLING_INTERVAL=5000 diff --git a/src/components/earn/EarnPage.tsx b/src/components/earn/EarnPage.tsx index 3bc0173b..96fd75a3 100644 --- a/src/components/earn/EarnPage.tsx +++ b/src/components/earn/EarnPage.tsx @@ -29,7 +29,6 @@ import { cn, isAbsoluteOffer, isRelativeOffer, percentageToFactor, scrollToTop } import type { WalletFileName } from '@/lib/utils' import { useDeveloperMode } from '@/store/jamSettingsStore' import { jmSessionStore } from '@/store/jmSessionStore' -import type { Milliseconds } from '@/types/global' import { Spinner } from '../ui/spinner' import { CreateFidelityBondDialog } from './CreateFidelityBondDialog' import { EarnForm, type EarnFormValues } from './EarnForm' @@ -40,15 +39,6 @@ import { OfferCard } from './OfferCard' import { RenewBondDialog } from './RenewBondDialog' import { EarnReportOverlay } from './report/EarnReportOverlay' -// In order to prevent state mismatch, the 'maker stop' response is delayed shortly. -// Even though the API response suggests that the maker has started or stopped immediately, it seems that this is not always the case. -// There is currently no way to know for sure - adding a delay at least mitigates the problem. -// 2022-04-26: With value of 2_000ms, no state corruption could be provoked in a local dev setup. -const MAKER_STOP_RESPONSE_DELAY: Milliseconds = 2_000 - -const WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL: Milliseconds = 3_000 -const WAIT_FOR_UPDATE_SESSION_POLLING_DELAY: Milliseconds = 1_000 - const toStartMakerRequest = (values: EarnFormValues): StartMakerRequest => { // both fee properties need to be provided. // prevent providing an invalid value by setting the ignored prop to zero @@ -99,7 +89,7 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { const stopMakerQuery = useQuery({ ...stopMakerQueryOptions, queryFn: withQueryDelay(stopMakerQueryOptions.queryFn, { - delayAfter: MAKER_STOP_RESPONSE_DELAY, + delayAfter: JAM.MAKER_STOP_RESPONSE_DELAY, }), enabled: false, retry: false, @@ -167,8 +157,8 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { useRefreshSession({ enabled: waitingForMakerUpdate || waitingForOfferUpdate, - refetchInterval: WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL, - refetchDelay: WAIT_FOR_UPDATE_SESSION_POLLING_DELAY, + refetchInterval: JAM.WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL, + refetchDelay: JAM.WAIT_FOR_UPDATE_SESSION_POLLING_DELAY, }) const onStop = async () => { diff --git a/src/components/logging/useJmwalletdStdoutLog.ts b/src/components/logging/useJmwalletdStdoutLog.ts index 692cc041..ba49de08 100644 --- a/src/components/logging/useJmwalletdStdoutLog.ts +++ b/src/components/logging/useJmwalletdStdoutLog.ts @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { useStore } from 'zustand' import { Alert } from '@/components/ui/alert' +import * as JAM from '@/constants/jam' import { fetchLog } from '@/lib/api/jam' import { getErrorReason } from '@/lib/errorReason' import { authStore } from '@/store/authStore' @@ -38,7 +39,7 @@ export function useJmwalletdStdoutLog({ enabled = true }: UseJmwalletdStdoutLogP refetchInterval: (query) => { if (!enabled || token === undefined) return false if (query.state.error) return false - return 2_500 + return JAM.JMWALLETD_LOGS_POLLING_INTERVAL }, // Keep previous data during refetch to prevent content flicker on slower networks. placeholderData: (previousData) => previousData, diff --git a/src/components/send/SendPage.tsx b/src/components/send/SendPage.tsx index 60c49ad4..9ec72481 100644 --- a/src/components/send/SendPage.tsx +++ b/src/components/send/SendPage.tsx @@ -20,6 +20,7 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { FeeConfigErrorAlert } from '@/components/ui/jam/FeeConfigErrorAlert' import { PageLoading } from '@/components/ui/jam/PageLoading' import PageTitle from '@/components/ui/jam/PageTitle' +import * as JAM from '@/constants/jam' import { useJamSessionInfoContext } from '@/context/JamSessionInfoContext' import { useAddressSummary, @@ -204,14 +205,14 @@ export const SendPage = ({ walletFileName }: SendPageProps) => { useRefreshSession({ enabled: isWaitingCoinjoinStart || isWaitingCoinjoinStop, - refetchInterval: 3_000, - refetchDelay: 1_000, + refetchInterval: JAM.WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL, + refetchDelay: JAM.WAIT_FOR_UPDATE_SESSION_POLLING_DELAY, }) useRefreshSession({ enabled: takerRunning, - refetchInterval: 5_000, - refetchDelay: 1_000, + refetchInterval: JAM.RUNNING_COINJOIN_POLLING_INTERVAL, + refetchDelay: JAM.RUNNING_COINJOIN_POLLING_DELAY, }) useEffect(() => { diff --git a/src/components/sweep/SweepPage.tsx b/src/components/sweep/SweepPage.tsx index d12bfa81..26d50aa6 100644 --- a/src/components/sweep/SweepPage.tsx +++ b/src/components/sweep/SweepPage.tsx @@ -25,7 +25,7 @@ import { Balance } from '@/components/ui/jam/Balance' import { FeeConfigErrorAlert } from '@/components/ui/jam/FeeConfigErrorAlert' import { PageLoading } from '@/components/ui/jam/PageLoading' import PageTitle from '@/components/ui/jam/PageTitle' -import { isDevMode } from '@/constants/debugFeatures' +import * as JAM from '@/constants/jam' import type { TumblerParameters } from '@/constants/jm' import { useJamSessionInfoContext } from '@/context/JamSessionInfoContext' import { useDetectNetwork, useJamWalletInfoContext } from '@/context/JamWalletInfoContext' @@ -36,7 +36,6 @@ import { getErrorReason } from '@/lib/errorReason' import { scrollToTop, type WalletFileName } from '@/lib/utils' import { useDeveloperMode } from '@/store/jamSettingsStore' import { jmSessionStore } from '@/store/jmSessionStore' -import type { Milliseconds } from '@/types/global' import { Button } from '../ui/button' import { Spinner } from '../ui/spinner' import { SweepForm } from './SweepForm' @@ -46,11 +45,6 @@ interface SweepPageProps { walletFileName: WalletFileName } -const WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL: Milliseconds = 3_000 -const WAIT_FOR_UPDATE_SESSION_POLLING_DELAY: Milliseconds = 1_000 - -const RUNNING_SCHEDULE_POLLING_INTERVAL: Milliseconds = isDevMode() ? 5_000 : 10_000 - const INSECURE_SCHEDULE_TUMBLER_OPTIONS: Partial = { time_lambda_seconds: 10, stage1_wait_multiplier: 1.5, @@ -87,7 +81,7 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { refetchInterval: (query) => { const schedulerRunning = takerInfo.running && takerInfo.scheduler.running const currentScheduleStillActive = query.state.data && !isPlanTerminated(query.state.data) - return schedulerRunning || currentScheduleStillActive ? RUNNING_SCHEDULE_POLLING_INTERVAL : false + return schedulerRunning || currentScheduleStillActive ? JAM.RUNNING_SCHEDULE_POLLING_INTERVAL : false }, refetchIntervalInBackground: true, retry: false, @@ -208,8 +202,8 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { useRefreshSession({ enabled: isWaitingSchedulerStart || isWaitingSchedulerStop, - refetchInterval: WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL, - refetchDelay: WAIT_FOR_UPDATE_SESSION_POLLING_DELAY, + refetchInterval: JAM.WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL, + refetchDelay: JAM.WAIT_FOR_UPDATE_SESSION_POLLING_DELAY, }) useEffect(() => { diff --git a/src/constants/jam.ts b/src/constants/jam.ts index 11c91024..b8b86242 100644 --- a/src/constants/jam.ts +++ b/src/constants/jam.ts @@ -78,6 +78,45 @@ export const JAM_RESCAN_PROGRESS_INTERVAL: Milliseconds = Math.max( JAM_RESCAN_PROGRESS_MIN_INTERVAL, ) +export const WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_WAIT_FOR_UPDATE_SESSION_POLLING_INTERVAL, 3_000), + 1_000, +) +export const WAIT_FOR_UPDATE_SESSION_POLLING_DELAY: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_WAIT_FOR_UPDATE_SESSION_POLLING_DELAY, 1_000), + 1, +) + +export const RUNNING_COINJOIN_POLLING_INTERVAL: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_RUNNING_COINJOIN_POLLING_INTERVAL, 5_000), + 1_000, +) +export const RUNNING_COINJOIN_POLLING_DELAY: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_RUNNING_COINJOIN_POLLING_DELAY, 1_000), + 1, +) + +export const RUNNING_SCHEDULE_POLLING_INTERVAL: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_RUNNING_SCHEDULE_POLLING_INTERVAL, 10_000), + 1_000, +) + +/** + * In order to prevent state mismatch, the 'maker stop' response is delayed shortly. + * Even though the API response suggests that the maker has started or stopped immediately, it seems that this is not always the case. + * There is currently no way to know for sure - adding a delay at least mitigates the problem. + * 2022-04-26: With value of 2_000ms, no state corruption could be provoked in a local dev setup. + */ +export const MAKER_STOP_RESPONSE_DELAY: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_MAKER_STOP_RESPONSE_DELAY, 2_000), + 1, +) + +export const JMWALLETD_LOGS_POLLING_INTERVAL: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_JMWALLETD_LOGS_POLLING_INTERVAL, 2_500), + 500, +) + const JAM_SEED_MODAL_MIN_TIMEOUT: Milliseconds = 5_000 const JAM_SEED_MODAL_DEFAULT_TIMEOUT: Milliseconds = 30_000 export const JAM_SEED_MODAL_TIMEOUT: Milliseconds = Math.max( From 8eef1958a5d6b7259d69c94c8a0ee6bc8470f075 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Fri, 7 Aug 2026 19:56:18 +0200 Subject: [PATCH 41/56] build(deps): update dependencies (#1416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @hookform/resolvers 5.5.7 → 5.5.8 @storybook/addon-a11y 10.5.5 → 10.5.7 @storybook/addon-docs 10.5.5 → 10.5.7 @storybook/addon-vitest 10.5.5 → 10.5.7 @storybook/react-vite 10.5.5 → 10.5.7 @testing-library/user-event 14.6.1 → 14.6.3 @types/react 19.2.17 → 19.2.18 @types/react-dom 19.2.3 → 19.2.4 conventional-changelog 8.1.0 → 8.1.1 eslint-plugin-storybook 10.5.5 → 10.5.7 storybook 10.5.5 → 10.5.7 vite 8.2.0 → 8.2.1 @chromatic-com/storybook 5.2.1 → 5.3.0 @hookform/resolvers 5.5.8 → 5.7.1 @noble/hashes 2.2.0 → 2.3.0 globals 17.8.0 → 17.9.0 lint-staged 17.2.0 → 17.3.0 lucide-react 1.28.0 → 1.30.0 react-hook-form 7.83.0 → 7.84.0 typescript-eslint 8.65.0 → 8.66.0 --- package-lock.json | 429 +++++++++++++++++++++++++--------------------- package.json | 38 ++-- 2 files changed, 253 insertions(+), 214 deletions(-) diff --git a/package-lock.json b/package-lock.json index 38a9fe80..e386f3e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "2.0.0-beta.2", "license": "MIT", "dependencies": { - "@hookform/resolvers": "5.5.7", + "@hookform/resolvers": "5.7.1", "@joinmarket-webui/joinmarket-ng-api-ts": "1.0.0", - "@noble/hashes": "2.2.0", + "@noble/hashes": "2.3.0", "@radix-ui/react-accordion": "1.2.20", "@radix-ui/react-avatar": "1.2.6", "@radix-ui/react-checkbox": "1.3.11", @@ -38,13 +38,13 @@ "clsx": "2.1.1", "html5-qrcode": "2.3.8", "i18next-browser-languagedetector": "8.2.1", - "lucide-react": "1.28.0", + "lucide-react": "1.30.0", "next-themes": "0.4.6", "qrcode": "1.5.4", "radix-ui": "1.6.7", "react": "19.2.8", "react-dom": "19.2.8", - "react-hook-form": "7.83.0", + "react-hook-form": "7.84.0", "react-i18next": "17.0.11", "react-router-dom": "7.18.2", "react-use-websocket": "4.13.0", @@ -56,45 +56,45 @@ "zustand": "5.0.14" }, "devDependencies": { - "@chromatic-com/storybook": "5.2.1", + "@chromatic-com/storybook": "5.3.0", "@eslint/js": "9.39.5", "@playwright/test": "1.62.0", - "@storybook/addon-a11y": "10.5.5", - "@storybook/addon-docs": "10.5.5", - "@storybook/addon-vitest": "10.5.5", - "@storybook/react-vite": "10.5.5", + "@storybook/addon-a11y": "10.5.7", + "@storybook/addon-docs": "10.5.7", + "@storybook/addon-vitest": "10.5.7", + "@storybook/react-vite": "10.5.7", "@tanstack/eslint-plugin-query": "5.101.4", "@testing-library/jest-dom": "7.0.0", "@testing-library/react": "16.3.2", - "@testing-library/user-event": "14.6.1", + "@testing-library/user-event": "14.6.3", "@trivago/prettier-plugin-sort-imports": "6.0.2", "@types/node": "26.1.2", "@types/qrcode": "1.5.6", - "@types/react": "19.2.17", - "@types/react-dom": "19.2.3", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", "@vitejs/plugin-react": "6.0.5", "@vitest/browser": "4.1.10", "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "4.1.10", - "conventional-changelog": "8.1.0", + "conventional-changelog": "8.1.1", "eslint": "9.39.5", "eslint-plugin-compat": "7.0.2", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.3", - "eslint-plugin-storybook": "10.5.5", + "eslint-plugin-storybook": "10.5.7", "eslint-plugin-unicorn": "65.0.1", - "globals": "17.8.0", + "globals": "17.9.0", "husky": "9.1.7", "jsdom": "30.0.1", - "lint-staged": "17.2.0", + "lint-staged": "17.3.0", "msw": "2.15.0", "msw-storybook-addon": "3.0.0", "prettier": "3.9.6", "prettier-plugin-tailwindcss": "0.8.1", - "storybook": "10.5.5", + "storybook": "10.5.7", "typescript": "6.0.3", - "typescript-eslint": "8.65.0", - "vite": "8.2.0", + "typescript-eslint": "8.66.0", + "vite": "8.2.1", "vitest": "4.1.10" }, "engines": { @@ -462,14 +462,14 @@ } }, "node_modules/@chromatic-com/storybook": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-5.2.1.tgz", - "integrity": "sha512-z6I7NJk/0VngA64y5TNYaB4Hc2X8+90n4op6lBt9PvWk5TmIlFLDqdX33rlrwbNRkkYijVgA/wO04rVYXi5Mlg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-5.3.0.tgz", + "integrity": "sha512-gMAfVYJnF/tjdY8C3Q54KYz/low2NFjPIZMpCqSFuMUiTJ2DNWCTb7mFhuuzZ1iduWFDhewlJyilFmvKcEDYRA==", "dev": true, "license": "MIT", "dependencies": { "@neoconfetti/react": "^1.0.0", - "chromatic": "16.10.0", + "chromatic": "^18.0.1", "jsonfile": "^6.1.0", "strip-ansi": "^7.1.0" }, @@ -1378,9 +1378,9 @@ "license": "MIT" }, "node_modules/@hookform/resolvers": { - "version": "5.5.7", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.5.7.tgz", - "integrity": "sha512-CyPCYV8/KlXfEXLWj8HHHhVsR/IZ6Ckm3b/a4fWtO/lRnRK1huqncb8LlAWrmpPRsse5glF5aVuDRMHEr3UGag==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.7.1.tgz", + "integrity": "sha512-8wS/P4UDr5sQDe4nFaV51TVyfDPrWgNIXweqG0Bs9Z5LSuzKLb+RQNPvkN2oHM5SRrJyWrVH/F+LOUcFjUyvwQ==", "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" @@ -1389,12 +1389,12 @@ "@sinclair/typebox": ">=0.25.24", "@standard-schema/spec": "^1.0.0", "@typeschema/main": ">=0.13.7", - "@vinejs/vine": "^2.0.0 || ^3.0.0", + "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", "ajv": "^8.12.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "arktype": "^2.0.0", - "ata-validator": "^0.7.0", + "ata-validator": "^1.2.0", "class-transformer": ">=0.4.0", "class-validator": ">=0.12.0", "computed-types": "^1.0.0", @@ -1809,7 +1809,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/hashes": { + "node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", @@ -1821,6 +1821,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@open-draft/deferred-promise": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", @@ -4431,6 +4443,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", @@ -4444,6 +4468,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@simple-libs/child-process-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-2.0.0.tgz", @@ -4517,9 +4553,9 @@ "license": "MIT" }, "node_modules/@storybook/addon-a11y": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.5.tgz", - "integrity": "sha512-nsMnSRe7pzepXIkUUqI/rL7sp8juXOluyypU4Dz0UuYHhw/cKaxfzuO+3WJN5EtEr/gcnKYb4awxH/duPGavUw==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.7.tgz", + "integrity": "sha512-I30rsNz6aA3xg3811MEry40uJDHP3l5SOkfqtmNkp7y4NdqTDdKhmbhhDAZQX1WWEk6GeMBGr3AJ3TCz7r7JmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4531,20 +4567,20 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.5" + "storybook": "^10.5.7" } }, "node_modules/@storybook/addon-docs": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.5.tgz", - "integrity": "sha512-0YpKlimS4XE0kQ8Maa5coeefQxdyDrBHg1wOP3WTPuBe4FolFSCDveR0ge2+vuUBk+fZfn2+l+3Q2jmAWaRGDg==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.7.tgz", + "integrity": "sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.5.5", + "@storybook/csf-plugin": "10.5.7", "@storybook/icons": "^2.0.2", - "@storybook/react-dom-shim": "10.5.5", + "@storybook/react-dom-shim": "10.5.7", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -4555,7 +4591,7 @@ }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.5" + "storybook": "^10.5.7" }, "peerDependenciesMeta": { "@types/react": { @@ -4564,9 +4600,9 @@ } }, "node_modules/@storybook/addon-vitest": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.5.tgz", - "integrity": "sha512-Ymq9ErkSkYiIDuqpJ2+hE5GCQ5J6TCLOWhutqArvwaeAO+HAibM82XNExpJ1/kvPqk9y961GDPkv2W15I88JIw==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.7.tgz", + "integrity": "sha512-7NK7Kzazc2vb2h8nGlH15QlUn5J6/LV0xF70VziAK0bxu3r1JupLCoBvckX39vHzFXiAZljXZtt1Wq/OidBxow==", "dev": true, "license": "MIT", "dependencies": { @@ -4581,7 +4617,7 @@ "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", - "storybook": "^10.5.5", + "storybook": "^10.5.7", "vitest": "^3.0.0 || ^4.0.0" }, "peerDependenciesMeta": { @@ -4600,13 +4636,13 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.5.tgz", - "integrity": "sha512-dQoJ7gUl8y0z5rV9cE0mz6qTBNmN9R4GOLIZk98rJ8CwduNJOb9eGZXusDzzvnYcp8TnNkqDtyx4tXQSUDInPQ==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.7.tgz", + "integrity": "sha512-fShF/aQaITqcJuMCLr42BGNUAbhDi4IboqvlbZqXAwgrrTslnZEUnY8GcEcvpZmjl11VwlmazhMJdH50fIgBPg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.5.5", + "@storybook/csf-plugin": "10.5.7", "ts-dedent": "^2.0.0" }, "funding": { @@ -4614,14 +4650,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.5", + "storybook": "^10.5.7", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.5.tgz", - "integrity": "sha512-/euibhRFqklYCZqUseokojmfYcQpXshVY2QmA1qCuxMz9SzVFD3iSTw+aFLTxpsJGGdcZJk8fnm/rEthLzZ9jA==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.7.tgz", + "integrity": "sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag==", "dev": true, "license": "MIT", "dependencies": { @@ -4634,7 +4670,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.5.5", + "storybook": "^10.5.7", "vite": "*", "webpack": "*" }, @@ -4671,14 +4707,14 @@ } }, "node_modules/@storybook/react": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.5.tgz", - "integrity": "sha512-T2Xj0ey7a9RHU6coYLC0L5lhjcdyhLCs9wNv15FvHvgmrRobkynEV72kq5vGW8tFkahNWI1X9+GZPQ6r8Nm38w==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.7.tgz", + "integrity": "sha512-uFvty2MMdFXzW5PcQe1JqDAZkz6cQq7q/9G/cbGVnBEvP6zsOVeL+bmrQ0/WBlFQN0Ko9+ZoCTvaQ9s65zBa5g==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.5.5", + "@storybook/react-dom-shim": "10.5.7", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, @@ -4691,7 +4727,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.5", + "storybook": "^10.5.7", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -4707,9 +4743,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.5.tgz", - "integrity": "sha512-PIk7N3LLrZIxfNxmkvmQN1d5UQ70XEedT8n0GhBiXnM6XL09xPGB8n8TZXeJBRYluKhDQcAyQeT0/OZmcDVQJg==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz", + "integrity": "sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==", "dev": true, "license": "MIT", "funding": { @@ -4721,7 +4757,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.5" + "storybook": "^10.5.7" }, "peerDependenciesMeta": { "@types/react": { @@ -4733,16 +4769,16 @@ } }, "node_modules/@storybook/react-vite": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.5.tgz", - "integrity": "sha512-Uy7VV72kVSkw6aDTAPQupXUeZX5LF6e4zqNvTZ+36qxsXAkaFgw7HPEm7L1tsaRfiV+s9anU7UvX47tfJpYGuQ==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.7.tgz", + "integrity": "sha512-eEo3eVa2pvqrzQukKxAzx7YvswDAA1s6k/y+tdMxmRvWyHX6QEOsb9Tda6wcVaa7c8BeJM7Ggq+289cRMTH6Iw==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.5.5", - "@storybook/react": "10.5.5", + "@storybook/builder-vite": "10.5.7", + "@storybook/react": "10.5.7", "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.2", @@ -4756,7 +4792,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.5", + "storybook": "^10.5.7", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, @@ -5212,9 +5248,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "version": "14.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz", + "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==", "dev": true, "license": "MIT", "engines": { @@ -5395,9 +5431,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5405,9 +5441,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "devOptional": true, "license": "MIT", "peerDependencies": { @@ -5439,17 +5475,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", - "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/type-utils": "8.65.0", - "@typescript-eslint/utils": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -5462,7 +5498,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.65.0", + "@typescript-eslint/parser": "^8.66.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -5478,16 +5514,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", - "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "engines": { @@ -5503,14 +5539,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "engines": { @@ -5525,14 +5561,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5543,9 +5579,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", "engines": { @@ -5560,15 +5596,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", - "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -5585,9 +5621,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", "engines": { @@ -5599,16 +5635,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5637,9 +5673,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -5650,13 +5686,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -5666,16 +5702,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", - "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0" + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5690,13 +5726,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/types": "8.66.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -6394,9 +6430,9 @@ } }, "node_modules/chromatic": { - "version": "16.10.0", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-16.10.0.tgz", - "integrity": "sha512-nFsztmnu7rFiGafUJgXvLUNpqmRylz92eNvzBoJNTKKQj4EQUyxznwnfpf1dTs7hXtWD8JwcH92jADydaHA1sw==", + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-18.1.0.tgz", + "integrity": "sha512-I9av8lUc5CQRlYzwKpmOG9cwYvZ4AFmTvtHeNrzOMC1353F9xYw45CHpSNIsNKuOiqqmzci9esXQd5akl0fi6w==", "dev": true, "license": "MIT", "dependencies": { @@ -6407,6 +6443,9 @@ "chromatic": "dist/bin.cjs", "chromatic-cli": "dist/bin.cjs" }, + "engines": { + "node": ">=22.0.0" + }, "peerDependencies": { "@chromatic-com/cypress": "^0.*.* || ^1.0.0", "@chromatic-com/playwright": "^0.*.* || ^1.0.0", @@ -6568,9 +6607,9 @@ "license": "MIT" }, "node_modules/conventional-changelog": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-8.1.0.tgz", - "integrity": "sha512-idt1JK+3+Nt7gqH/d/sEYWdDeR+5XPov/jssmFN6XxmYSRlonTWSDWhWUPkmm0zbwYjtVdt+4My5aiPkKyvocw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-8.1.1.tgz", + "integrity": "sha512-my0arqBpzr8mg+7RKDGnw8OiDvBr8/QVJIxazYJlIDeKx/2GCBYxZELaDYpbRhM5gNKfZB8h9SHuetrUFxd5HQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6580,7 +6619,7 @@ "argue-cli": "^3.1.0", "conventional-changelog-preset-loader": "^6.0.1", "conventional-changelog-writer": "^9.2.0", - "conventional-commits-parser": "^7.1.0", + "conventional-commits-parser": "^7.1.2", "fd-package-json": "^2.0.0" }, "bin": { @@ -6631,9 +6670,9 @@ } }, "node_modules/conventional-commits-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.0.tgz", - "integrity": "sha512-DPp6hkUjvwIivxbkrTiLXeRswNv1A/4GFA2X6scXma0AMa9632V3TwxmrlkUIEtUktiM3Ln+RrSH2xlP3/jUTw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz", + "integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7153,9 +7192,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.5.tgz", - "integrity": "sha512-xGCrddoZ8pMmwc7M5mp+habWbvqBjRuyGVTJ6RpavpoAP6NBUfGSRXHuBs7cHRDpDSuErYeDiEo4XHTonRp68g==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.7.tgz", + "integrity": "sha512-mLpamG1Rsica2jYbUzIZOEuy7Fm1IMtVLMvvxGTpjTVKUMxTXJsANx3MBpH2VSbGQB8Yzlt5399WL/O07K97Ig==", "dev": true, "license": "MIT", "dependencies": { @@ -7164,7 +7203,7 @@ }, "peerDependencies": { "eslint": ">=8", - "storybook": "^10.5.5" + "storybook": "^10.5.7" } }, "node_modules/eslint-plugin-unicorn": { @@ -7633,9 +7672,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -7662,9 +7701,9 @@ } }, "node_modules/globals": { - "version": "17.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz", - "integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==", + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", "dev": true, "license": "MIT", "engines": { @@ -8070,9 +8109,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -8504,9 +8543,9 @@ } }, "node_modules/lint-staged": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.2.0.tgz", - "integrity": "sha512-FchGnFe4i4B1C/a35SPU9bNGPEHSC1+1iV0plLjzBmKVe9klZrlRfSgK6Cw4VeHyqOXbJUXP0vON61uRftNQ0A==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.3.0.tgz", + "integrity": "sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==", "dev": true, "license": "MIT", "dependencies": { @@ -8582,9 +8621,9 @@ } }, "node_modules/lucide-react": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.28.0.tgz", - "integrity": "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.30.0.tgz", + "integrity": "sha512-tUIr2jXLbWpCkdtH8XP7P7YppM9ueWgTky99lpWDY6z5REs6B+O6ZQ3U5tHkUUY59ANyOv/PBcs8E4Fe3KO3eA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8777,9 +8816,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "funding": [ { "type": "github", @@ -9180,9 +9219,9 @@ } }, "node_modules/postcss": { - "version": "8.5.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", - "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -9199,7 +9238,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9666,9 +9705,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.83.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.83.0.tgz", - "integrity": "sha512-AXt8cMCmx5a7u4uvpb2uRFVrWQhllI4pV+LSykxIac/hjt44TnQkmX9BKuQi2i+LDC62esmiLpilkav+kjVf/A==", + "version": "7.84.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.84.0.tgz", + "integrity": "sha512-+hWvQP6GLco56mDwrbU4XnHix8t1z90ltZsDIrREl+jnQFQxYLX8oAzqe/Xn8nHpmoXTY5M6oEXrAhbP1qevNQ==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -9835,9 +9874,9 @@ "license": "MIT" }, "node_modules/recast": { - "version": "0.23.12", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz", - "integrity": "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==", + "version": "0.23.19", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.19.tgz", + "integrity": "sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w==", "dev": true, "license": "MIT", "dependencies": { @@ -10189,16 +10228,16 @@ "license": "MIT" }, "node_modules/storybook": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.5.tgz", - "integrity": "sha512-UscBIBJDloUeqntukHOhP1a5W/vouePDJbzPSxj466WK801FZtzQiMffMtkjzJiWSuj20wfaYlB2QQKh9aOYAg==", + "version": "10.5.7", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.7.tgz", + "integrity": "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", @@ -10659,16 +10698,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", - "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.65.0", - "@typescript-eslint/parser": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0" + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10829,15 +10868,15 @@ } }, "node_modules/vite": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", - "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.23", - "rolldown": "~1.2.0", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "bin": { diff --git a/package.json b/package.json index 0bb798fa..5953be25 100644 --- a/package.json +++ b/package.json @@ -71,9 +71,9 @@ ] }, "dependencies": { - "@hookform/resolvers": "5.5.7", + "@hookform/resolvers": "5.7.1", "@joinmarket-webui/joinmarket-ng-api-ts": "1.0.0", - "@noble/hashes": "2.2.0", + "@noble/hashes": "2.3.0", "@radix-ui/react-accordion": "1.2.20", "@radix-ui/react-avatar": "1.2.6", "@radix-ui/react-checkbox": "1.3.11", @@ -100,13 +100,13 @@ "clsx": "2.1.1", "html5-qrcode": "2.3.8", "i18next-browser-languagedetector": "8.2.1", - "lucide-react": "1.28.0", + "lucide-react": "1.30.0", "next-themes": "0.4.6", "qrcode": "1.5.4", "radix-ui": "1.6.7", "react": "19.2.8", "react-dom": "19.2.8", - "react-hook-form": "7.83.0", + "react-hook-form": "7.84.0", "react-i18next": "17.0.11", "react-router-dom": "7.18.2", "react-use-websocket": "4.13.0", @@ -118,45 +118,45 @@ "zustand": "5.0.14" }, "devDependencies": { - "@chromatic-com/storybook": "5.2.1", + "@chromatic-com/storybook": "5.3.0", "@eslint/js": "9.39.5", "@playwright/test": "1.62.0", - "@storybook/addon-a11y": "10.5.5", - "@storybook/addon-docs": "10.5.5", - "@storybook/addon-vitest": "10.5.5", - "@storybook/react-vite": "10.5.5", + "@storybook/addon-a11y": "10.5.7", + "@storybook/addon-docs": "10.5.7", + "@storybook/addon-vitest": "10.5.7", + "@storybook/react-vite": "10.5.7", "@tanstack/eslint-plugin-query": "5.101.4", "@testing-library/jest-dom": "7.0.0", "@testing-library/react": "16.3.2", - "@testing-library/user-event": "14.6.1", + "@testing-library/user-event": "14.6.3", "@trivago/prettier-plugin-sort-imports": "6.0.2", "@types/node": "26.1.2", "@types/qrcode": "1.5.6", - "@types/react": "19.2.17", - "@types/react-dom": "19.2.3", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", "@vitejs/plugin-react": "6.0.5", "@vitest/browser": "4.1.10", "@vitest/browser-playwright": "4.1.10", "@vitest/coverage-v8": "4.1.10", - "conventional-changelog": "8.1.0", + "conventional-changelog": "8.1.1", "eslint": "9.39.5", "eslint-plugin-compat": "7.0.2", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.3", - "eslint-plugin-storybook": "10.5.5", + "eslint-plugin-storybook": "10.5.7", "eslint-plugin-unicorn": "65.0.1", - "globals": "17.8.0", + "globals": "17.9.0", "husky": "9.1.7", "jsdom": "30.0.1", - "lint-staged": "17.2.0", + "lint-staged": "17.3.0", "msw": "2.15.0", "msw-storybook-addon": "3.0.0", "prettier": "3.9.6", "prettier-plugin-tailwindcss": "0.8.1", - "storybook": "10.5.5", + "storybook": "10.5.7", "typescript": "6.0.3", - "typescript-eslint": "8.65.0", - "vite": "8.2.0", + "typescript-eslint": "8.66.0", + "vite": "8.2.1", "vitest": "4.1.10" }, "overrides": { From 152793a11bae174f39a594457687b19a9839a602 Mon Sep 17 00:00:00 2001 From: Kishore B Date: Sun, 9 Aug 2026 21:24:34 +0530 Subject: [PATCH 42/56] ui(send): improve active collaborative send info (#1413) * refactor(send): improve active collaborative send alert Signed-off-by: kishore08-07 * chore: remove schedulerRunning flag from send alert * chore(storybook): add ActiveCollaborativeSendAlert story * chore(ui): ActiveCollaborativeSendAlert is an Item component * chore(ui): blur forms when maker/taker operation is running --------- Signed-off-by: kishore08-07 Co-authored-by: theborakompanioni --- src/components/earn/EarnPage.tsx | 7 +- .../ActiveCollaborativeSendAlert.test.tsx | 135 ++++++++++++++ .../send/ActiveCollaborativeSendAlert.tsx | 144 +++++++++++++++ src/components/send/SendPage.test.tsx | 18 +- src/components/send/SendPage.tsx | 169 ++++++++---------- src/components/sweep/SweepPage.tsx | 33 ++-- src/components/ui/jam/CookingPotIcon.tsx | 163 +++++++++++++++++ .../ActiveCollaborativeSendAlert.stories.tsx | 74 ++++++++ src/stories/jam/ScheduleEntryItem.stories.tsx | 59 ++++-- 9 files changed, 672 insertions(+), 130 deletions(-) create mode 100644 src/components/send/ActiveCollaborativeSendAlert.test.tsx create mode 100644 src/components/send/ActiveCollaborativeSendAlert.tsx create mode 100644 src/components/ui/jam/CookingPotIcon.tsx create mode 100644 src/stories/jam/ActiveCollaborativeSendAlert.stories.tsx diff --git a/src/components/earn/EarnPage.tsx b/src/components/earn/EarnPage.tsx index 96fd75a3..27815ec4 100644 --- a/src/components/earn/EarnPage.tsx +++ b/src/components/earn/EarnPage.tsx @@ -279,7 +279,12 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { diff --git a/src/components/send/ActiveCollaborativeSendAlert.test.tsx b/src/components/send/ActiveCollaborativeSendAlert.test.tsx new file mode 100644 index 00000000..251750e6 --- /dev/null +++ b/src/components/send/ActiveCollaborativeSendAlert.test.tsx @@ -0,0 +1,135 @@ +import { sha256 } from '@noble/hashes/sha2.js' +import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import type { PaymentAttempt } from '@/context/JamSessionInfoContext' +import type { Jar } from '@/context/JamWalletInfoContext' +import { TX_FEE_UNITS } from '@/lib/feeConfig' +import { ActiveCollaborativeSendAlert } from './ActiveCollaborativeSendAlert' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: Record) => (options ? `${key}:${JSON.stringify(options)}` : key), + }), + Trans: ({ i18nKey, children }: { i18nKey: string; children?: React.ReactNode }) => {i18nKey || children}, +})) + +vi.mock('@/context/JamDisplayContext', () => ({ + useJamDisplayContext: () => ({ addressChunkingEnabled: false }), +})) + +const sampleJars: Jar[] = [ + { + balanceSummary: { + calculatedAvailableBalanceInSats: 100_000, + calculatedConfirmedAvailableBalanceInSats: 100_000, + calculatedAvailableFrozenBalanceInSats: 0, + calculatedFrozenOrLockedBalanceInSats: 0, + calculatedTotalBalanceInSats: 100_000, + }, + color: '#e2b86a', + jarIndex: 0, + name: 'Jar Zero', + utxos: [], + }, + { + balanceSummary: { + calculatedAvailableBalanceInSats: 50_000, + calculatedConfirmedAvailableBalanceInSats: 50_000, + calculatedAvailableFrozenBalanceInSats: 0, + calculatedFrozenOrLockedBalanceInSats: 0, + calculatedTotalBalanceInSats: 50_000, + }, + color: '#3b5ba9', + jarIndex: 1, + name: 'Jar One', + utxos: [], + }, +] + +const sampleAttempt: PaymentAttempt = { + createdAt: 1_000, + utxosHashHex: bytesToHex(sha256(hexToBytes('00'))), + walletFileName: 'test.jmdat', + data: { + amount: { amount: 21_000, isSweep: false as const, sweepAmount: undefined }, + destination: { address: 'bcrt1qdestinationaddress123456789', fromJar: undefined }, + isCoinJoin: true, + numCollaborators: 5, + source: { fromJar: 0 as const }, + txFee: { txFeeInBlocks: 3, txFeeInSatsPerVbyte: undefined, txFeeUnit: TX_FEE_UNITS.BLOCKS }, + }, +} + +describe('ActiveCollaborativeSendAlert', () => { + it('renders title and details for active collaborative send to external address', () => { + render( + , + ) + + expect(screen.getByText('send.text_coinjoin_already_running')).toBeInTheDocument() + expect(screen.getByText('Jar Zero')).toBeInTheDocument() + expect(screen.getByText('#0')).toBeInTheDocument() + expect(screen.getByText(/bcrt1qdestinationaddress123456789/u)).toBeInTheDocument() + expect(screen.getAllByText('5').length).toBeGreaterThan(0) + expect(screen.getByRole('button', { name: 'global.abort' })).toBeInTheDocument() + }) + + it('renders destination jar badge when destination jar is present', () => { + const internalAttempt = { + ...sampleAttempt, + data: { + ...sampleAttempt.data, + destination: { address: 'bc1qinternal', fromJar: 1 as const }, + }, + } + + render( + , + ) + + expect(screen.getByText('Jar One')).toBeInTheDocument() + expect(screen.getByText('#1')).toBeInTheDocument() + }) + + it('calls onAbort when abort button is clicked', async () => { + const user = userEvent.setup() + const onAbort = vi.fn() + + render( + , + ) + + await user.click(screen.getByRole('button', { name: 'global.abort' })) + expect(onAbort).toHaveBeenCalledTimes(1) + }) + + it('disables abort button when stopping coinjoin', () => { + render( + , + ) + + expect(screen.getByRole('button', { name: 'global.abort' })).toBeDisabled() + }) +}) diff --git a/src/components/send/ActiveCollaborativeSendAlert.tsx b/src/components/send/ActiveCollaborativeSendAlert.tsx new file mode 100644 index 00000000..ba71683c --- /dev/null +++ b/src/components/send/ActiveCollaborativeSendAlert.tsx @@ -0,0 +1,144 @@ +import { useMemo } from 'react' +import { MilkIcon, UsersIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { Badge } from '@/components/ui/badge' +import { jarBadgeVariant } from '@/components/ui/badge-variants' +import { Button } from '@/components/ui/button' +import { Address } from '@/components/ui/jam/Address' +import { Balance } from '@/components/ui/jam/Balance' +import { CookingPotIcon } from '@/components/ui/jam/CookingPotIcon' +import type { PaymentAttempt } from '@/context/JamSessionInfoContext' +import type { Jar } from '@/context/JamWalletInfoContext' +import { cn } from '@/lib/utils' +import { Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemHeader, ItemTitle } from '../ui/item' +import { Label } from '../ui/label' + +export interface ActiveCollaborativeSendAlertProps { + paymentAttempt?: PaymentAttempt + jars: Jar[] + isAborting: boolean + onAbort: () => void +} + +export const ActiveCollaborativeSendAlert = ({ + paymentAttempt, + jars, + isAborting, + onAbort, +}: ActiveCollaborativeSendAlertProps) => { + const { t } = useTranslation() + + const data = paymentAttempt?.data + + const sourceJar = useMemo(() => { + if (data?.source?.fromJar === undefined) return undefined + return jars.find((index) => index.jarIndex === data.source.fromJar) + }, [jars, data]) + + const destinationJar = useMemo(() => { + if (data?.destination?.fromJar === undefined) return undefined + return jars.find((index) => index.jarIndex === data.destination.fromJar) + }, [jars, data]) + + return ( + <> + + +
+ {t('send.text_coinjoin_already_running')} + +
+
+ + +
+ +
+ {data && ( + <> +
+ {sourceJar ? ( +
+ +
+ + + {sourceJar.name} #{sourceJar.jarIndex.toLocaleString()} + +
+
+ ) : null} + +
+ +
+ + {data.amount.isSweep ? ( + + ) : ( + + )} +
+
+ + {data.numCollaborators !== undefined ? ( +
+ +
+ + {data.numCollaborators.toLocaleString()} +
+
+ ) : null} + +
+ +
+ + {destinationJar && ( + + {destinationJar.name}{' '} + #{destinationJar.jarIndex.toLocaleString()} + + )} +
+
+
+
+ + )} +
+ + + + + + +
+ + ) +} diff --git a/src/components/send/SendPage.test.tsx b/src/components/send/SendPage.test.tsx index d7ec0b85..f1441067 100644 --- a/src/components/send/SendPage.test.tsx +++ b/src/components/send/SendPage.test.tsx @@ -26,6 +26,7 @@ const mocks = vi.hoisted(() => ({ stopCoinjoinRefetch: vi.fn(), takerRunning: false, schedulerRunning: false, + makerRunning: false, toastError: vi.fn(), toastInfo: vi.fn(), toastSuccess: vi.fn(), @@ -145,6 +146,10 @@ vi.mock('@/store/jamSettingsStore', () => ({ useDeveloperMode: () => ({ enabled: true }), })) +vi.mock('@/context/JamDisplayContext', () => ({ + useJamDisplayContext: () => ({ addressChunkingEnabled: false }), +})) + vi.mock('@/context/JamSessionInfoContext', () => ({ useJamSessionInfoContext: () => ({ clearCurrentPaymentAttempt: mocks.clearCurrentPaymentAttempt, @@ -157,6 +162,9 @@ vi.mock('@/context/JamSessionInfoContext', () => ({ running: mocks.schedulerRunning, }, }, + makerInfo: { + running: mocks.makerRunning, + }, }), })) @@ -510,15 +518,7 @@ describe('SendPage', () => { }) it('shows the maker running warning', async () => { - jmSessionStore.setState({ - state: { - coinjoin_in_process: false, - maker_running: true, - session: true, - wallet_name: 'wallet.jmdat', - rescanning: false, - }, - }) + mocks.makerRunning = true render() diff --git a/src/components/send/SendPage.tsx b/src/components/send/SendPage.tsx index 9ec72481..a6e01cd4 100644 --- a/src/components/send/SendPage.tsx +++ b/src/components/send/SendPage.tsx @@ -37,7 +37,7 @@ import { useUtxoSelectionDialog } from '@/hooks/useUtxoSelectionDialog' import { getErrorReason } from '@/lib/errorReason' import * as fb from '@/lib/fidelityBondUtils' import { withMutationDelay } from '@/lib/queryClient' -import { scrollToTop, type WalletFileName } from '@/lib/utils' +import { cn, scrollToTop, type WalletFileName } from '@/lib/utils' import { useDeveloperMode } from '@/store/jamSettingsStore' import { jmSessionStore } from '@/store/jmSessionStore' import { jmTxStore, type JmTxInfo } from '@/store/jmTxStore' @@ -45,6 +45,7 @@ import type { JarIndex } from '@/types/global' import { Button } from '../ui/button' import { Card, CardContent } from '../ui/card' import { Spinner } from '../ui/spinner' +import { ActiveCollaborativeSendAlert } from './ActiveCollaborativeSendAlert' import { PaymentAbortDialog } from './PaymentAbortDialog' import PaymentConfirmDialog from './PaymentConfirmDialog' import { SendForm } from './SendForm' @@ -84,8 +85,9 @@ export const SendPage = ({ walletFileName }: SendPageProps) => { waitForUtxosToBeSpent, setWaitForUtxosToBeSpent, } = useJamWalletInfoContext() - const jmSession = useStore(jmSessionStore, (state) => state.state) + const jmSessionActive = useStore(jmSessionStore, (state) => state.state?.session) const { + makerInfo: { running: makerRunning }, takerInfo: { running: takerRunning, currentPaymentAttempt, @@ -369,7 +371,7 @@ export const SendPage = ({ walletFileName }: SendPageProps) => { await stopCoinjoinMutationMutateAsync() } - if (!jmSession || walletInfoIsLoading) { + if (!jmSessionActive || walletInfoIsLoading) { return } @@ -411,7 +413,7 @@ export const SendPage = ({ walletFileName }: SendPageProps) => { {feeConfigValidation.maxFeesConfigMissing && ( setShowFeeConfigDialog(true)} className="mb-4" /> )} - {jmSession?.maker_running === true && ( + {makerRunning === true && ( {t('send.text_maker_running')} @@ -447,102 +449,77 @@ export const SendPage = ({ walletFileName }: SendPageProps) => { * If data `takerCurrentAttempt` is not present, no message is shown * when the taker service stops - this is not ideal, but okay. */ - currentPaymentAttempt !== undefined && - currentPaymentAttempt.data.isCoinJoin && - !isWaitingCoinjoinStart && - takerRunning === false && ( - <> - {walletInfoIsFetching ? ( - <> - - - {t('send.alert_collaborative_awaiting_completion')} + currentPaymentAttempt?.data.isCoinJoin === true && !isWaitingCoinjoinStart && takerRunning === false && ( + <> + {walletInfoIsFetching ? ( + <> + + + {t('send.alert_collaborative_awaiting_completion')} + + + ) : ( + <> + {currentPaymentAttempt.utxosHashHex === utxosHashHex ? ( + + + {t('send.alert_collaborative_ended_title')} + +
{t('send.alert_collaborative_ended_description')}
+
+ +
+
- - ) : ( - <> - {currentPaymentAttempt.utxosHashHex === utxosHashHex ? ( - - - {t('send.alert_collaborative_ended_title')} - -
{t('send.alert_collaborative_ended_description')}
-
- -
-
-
- ) : ( - - - {t('send.alert_collaborative_completed_title')} - -
{t('send.alert_collaborative_completed_description')}
-
- -
-
-
- )} - - )} - - ) + ) : ( + + + {t('send.alert_collaborative_completed_title')} + +
{t('send.alert_collaborative_completed_description')}
+
+ +
+
+
+ )} + + )} + + ) } - {takerRunning && !isWaitingCoinjoinStop && ( + + {schedulerRunning && ( {t('send.text_coinjoin_already_running')} - - {currentPaymentAttempt && ( -
-                  {JSON.stringify(
-                    {
-                      sourceJar: currentPaymentAttempt.data.source.fromJar,
-                      destinationJar: currentPaymentAttempt.data.destination.fromJar,
-                      destinationAddress: currentPaymentAttempt.data.destination.address,
-                      isSweep: currentPaymentAttempt.data.amount.isSweep === true,
-                      amount:
-                        currentPaymentAttempt.data.amount.isSweep === true
-                          ? currentPaymentAttempt.data.amount.sweepAmount
-                          : currentPaymentAttempt.data.amount.amount,
-                      numCollaborators: currentPaymentAttempt.data.numCollaborators,
-                    },
-                    null,
-                    2,
-                  )}
-                
- )} - {takerRunning && !schedulerRunning ? ( -
- -
- ) : null} -
)} + {takerRunning && !schedulerRunning && ( + setShowAbortCoinjoinDialog(true)} + /> + )} + {triggerNonCollaborativeTransaction.error ? ( @@ -587,7 +564,11 @@ export const SendPage = ({ walletFileName }: SendPageProps) => { )} - + { addressSummary={addressSummary} walletBalanceSummary={walletBalanceSummary} disabled={ - jmSession?.maker_running === true || + makerRunning || takerRunning || rescanInfo.rescanning || isWaitingCoinjoinStart || diff --git a/src/components/sweep/SweepPage.tsx b/src/components/sweep/SweepPage.tsx index 26d50aa6..303d4542 100644 --- a/src/components/sweep/SweepPage.tsx +++ b/src/components/sweep/SweepPage.tsx @@ -33,7 +33,7 @@ import { useApiClient } from '@/hooks/useApiClient' import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation' import { useRefreshSession } from '@/hooks/useRefreshSession' import { getErrorReason } from '@/lib/errorReason' -import { scrollToTop, type WalletFileName } from '@/lib/utils' +import { cn, scrollToTop, type WalletFileName } from '@/lib/utils' import { useDeveloperMode } from '@/store/jamSettingsStore' import { jmSessionStore } from '@/store/jmSessionStore' import { Button } from '../ui/button' @@ -56,7 +56,14 @@ const INSECURE_SCHEDULE_TUMBLER_OPTIONS: Partial = { export const SweepPage = ({ walletFileName }: SweepPageProps) => { const { t } = useTranslation() const client = useApiClient() - const { rescanInfo, takerInfo, makerInfo } = useJamSessionInfoContext() + const { + rescanInfo, + makerInfo: { running: makerRunning }, + takerInfo: { + running: takerRunning, + scheduler: { running: schedulerRunning }, + }, + } = useJamSessionInfoContext() const jmSession = useStore(jmSessionStore, (state) => state.state) const walletInfo = useJamWalletInfoContext() const { network } = useDetectNetwork() @@ -79,7 +86,6 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { path: { walletname: walletFileName }, }), refetchInterval: (query) => { - const schedulerRunning = takerInfo.running && takerInfo.scheduler.running const currentScheduleStillActive = query.state.data && !isPlanTerminated(query.state.data) return schedulerRunning || currentScheduleStillActive ? JAM.RUNNING_SCHEDULE_POLLING_INTERVAL : false }, @@ -189,14 +195,11 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { return toSchedule(getScheduleQuery.data, walletInfo.jars) }, [getScheduleQuery.error, getScheduleQuery.data, walletInfo.jars]) - const schedulerRunning = takerInfo.scheduler.running const isWaitingSchedulerStart = startScheduleMutationIsPending || (startScheduleMutationIsSuccess && !schedulerRunning) - const waitingForTumblerStatus = takerInfo.running && getScheduleQuery.isPending + const waitingForTumblerStatus = takerRunning && getScheduleQuery.isPending const singleCoinJoinRunning = - takerInfo.running && !schedulerRunning && !isWaitingSchedulerStart && !waitingForTumblerStatus - const makerRunning = makerInfo.running === true - const collaborativeOperationRunning = makerRunning || takerInfo.running + takerRunning && !schedulerRunning && !isWaitingSchedulerStart && !waitingForTumblerStatus const isWaitingSchedulerStop = stopScheduleMutationIsPending || (stopScheduleMutationIsSuccess && schedulerRunning) @@ -220,8 +223,10 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { const isOperationDisabled = feeConfigValidation.maxFeesConfigMissing || - collaborativeOperationRunning || + makerRunning || + takerRunning || rescanInfo.rescanning || + makerRunning || !preconditionSummary.isFulfilled const isStartDisabled = @@ -289,14 +294,14 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { {singleCoinJoinRunning && ( - {t('send.text_coinjoin_already_running')} + {t('send.text_coinjoin_already_running')} )} {makerRunning && ( - {t('send.text_maker_running')} + {t('send.text_maker_running')} )} @@ -430,7 +435,11 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { {!currentSchedule && !schedulerRunning && ( <> - +
diff --git a/src/components/ui/jam/CookingPotIcon.tsx b/src/components/ui/jam/CookingPotIcon.tsx new file mode 100644 index 00000000..1dd2d288 --- /dev/null +++ b/src/components/ui/jam/CookingPotIcon.tsx @@ -0,0 +1,163 @@ +import { jarBadgeVariant } from '@/components/ui/badge-variants' +import { cn } from '@/lib/utils' + +export interface CookingPotIconProps { + sourceJarIndex?: number + destinationJarIndex?: number + className?: string +} + +const STROKE_CLASSES: Record = { + jar0: 'stroke-jar0', + jar1: 'stroke-jar1', + jar2: 'stroke-jar2', + jar3: 'stroke-jar3', + jar4: 'stroke-jar4', + jarUnknown: 'stroke-jar-unknown', +} + +export const CookingPotIcon = ({ sourceJarIndex, destinationJarIndex, className }: CookingPotIconProps) => { + const sourceVariant = jarBadgeVariant(sourceJarIndex) + const destinationVariant = jarBadgeVariant(destinationJarIndex) ?? sourceVariant + + const sourceStrokeClass = (sourceVariant && STROKE_CLASSES[sourceVariant]) || 'stroke-jar-unknown' + const destinationStrokeClass = (destinationVariant && STROKE_CLASSES[destinationVariant]) || sourceStrokeClass + + return ( +
+ + +
+ +
+
+ ) +} diff --git a/src/stories/jam/ActiveCollaborativeSendAlert.stories.tsx b/src/stories/jam/ActiveCollaborativeSendAlert.stories.tsx new file mode 100644 index 00000000..4299292f --- /dev/null +++ b/src/stories/jam/ActiveCollaborativeSendAlert.stories.tsx @@ -0,0 +1,74 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { ActiveCollaborativeSendAlert } from '@/components/send/ActiveCollaborativeSendAlert' +import type { PaymentAttempt } from '@/context/JamSessionInfoContext' +import type { Jar } from '@/context/JamWalletInfoContext' +import { TX_FEE_UNITS } from '@/lib/feeConfig' + +const meta: Meta = { + title: 'Jam/ActiveCollaborativeSendAlert', + component: ActiveCollaborativeSendAlert, + tags: ['autodocs'], + args: { + jars: [], + isAborting: false, + onAbort: () => alert('onAbort clicked'), + }, +} +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: {}, +} + +const jars: Jar[] = [ + { + balanceSummary: { + calculatedAvailableBalanceInSats: 100_000, + calculatedConfirmedAvailableBalanceInSats: 100_000, + calculatedAvailableFrozenBalanceInSats: 0, + calculatedFrozenOrLockedBalanceInSats: 0, + calculatedTotalBalanceInSats: 100_000, + }, + color: '#e2b86a', + jarIndex: 0, + name: 'Jar Zero', + utxos: [], + }, +] + +const paymentAttempt: PaymentAttempt = { + createdAt: 1_000, + utxosHashHex: 'abc123hash', + walletFileName: 'Satoshi.jmdat', + data: { + amount: { amount: 21_000, isSweep: false as const, sweepAmount: undefined }, + destination: { address: 'bcrt1qdestinationaddress123456789', fromJar: undefined }, + isCoinJoin: true, + numCollaborators: 5, + source: { fromJar: 0 as const }, + txFee: { txFeeInBlocks: 3, txFeeInSatsPerVbyte: undefined, txFeeUnit: TX_FEE_UNITS.BLOCKS }, + }, +} + +export const WithPaymentAttempt: Story = { + args: { + paymentAttempt, + jars, + }, +} + +export const WithSweepPaymentAttempt: Story = { + args: { + paymentAttempt: { + ...paymentAttempt, + data: { + ...paymentAttempt.data, + amount: { amount: undefined, isSweep: true, sweepAmount: 21_000 }, + destination: { address: 'bcrt1qdestinationaddress123456789', fromJar: paymentAttempt.data.source.fromJar }, + }, + }, + jars, + }, +} diff --git a/src/stories/jam/ScheduleEntryItem.stories.tsx b/src/stories/jam/ScheduleEntryItem.stories.tsx index da69cace..e303631f 100644 --- a/src/stories/jam/ScheduleEntryItem.stories.tsx +++ b/src/stories/jam/ScheduleEntryItem.stories.tsx @@ -2,6 +2,7 @@ import type { TumblerPhaseResponse } from '@joinmarket-webui/joinmarket-ng-api-t import type { Meta, StoryObj } from '@storybook/react-vite' import { ScheduleEntryItem } from '@/components/sweep/ScheduleEntryItem' import { toScheduleEntry } from '@/components/sweep/scheduleUtils' +import type { Jar } from '@/context/JamWalletInfoContext' const meta: Meta = { title: 'Jam/ScheduleEntryItem', @@ -12,6 +13,22 @@ export default meta type Story = StoryObj +const jars: Jar[] = [ + { + balanceSummary: { + calculatedAvailableBalanceInSats: 100_000, + calculatedConfirmedAvailableBalanceInSats: 100_000, + calculatedAvailableFrozenBalanceInSats: 0, + calculatedFrozenOrLockedBalanceInSats: 0, + calculatedTotalBalanceInSats: 100_000, + }, + color: '#e2b86a', + jarIndex: 0, + name: 'Jar Zero', + utxos: [], + }, +] + const takerPhase: TumblerPhaseResponse = { kind: 'taker_coinjoin', index: 0, @@ -20,7 +37,7 @@ const takerPhase: TumblerPhaseResponse = { started_at: '2026-07-19T10:35:52.775747+00:00', finished_at: null, error: null, - mixdepth: 0, + mixdepth: jars[0].jarIndex, amount: 0, amount_fraction: null, counterparty_count: 21, @@ -54,9 +71,28 @@ const makerPhase: TumblerPhaseResponse = { attempt_count: 0, } +export const Maker: Story = { + args: { + value: toScheduleEntry(makerPhase, jars), + }, +} + export const Active: Story = { args: { - value: toScheduleEntry(takerPhase, []), + value: toScheduleEntry(takerPhase, jars), + active: true, + }, +} + +export const ActiveExternal: Story = { + args: { + value: toScheduleEntry( + { + ...takerPhase, + destination: 'bcrt1qdestinationaddress123456789', + }, + jars, + ), active: true, }, } @@ -68,7 +104,7 @@ export const Running: Story = { ...takerPhase, status: 'running', }, - [], + jars, ), active: false, }, @@ -81,7 +117,7 @@ export const Pending: Story = { ...takerPhase, status: 'pending', }, - [], + jars, ), active: false, }, @@ -93,8 +129,9 @@ export const Completed: Story = { { ...takerPhase, status: 'completed', + finished_at: '2026-07-19T10:35:52.775747+00:00', }, - [], + jars, ), active: false, }, @@ -107,7 +144,7 @@ export const Skipped: Story = { ...takerPhase, status: 'skipped', }, - [], + jars, ), active: false, }, @@ -120,7 +157,7 @@ export const Cancelled: Story = { ...takerPhase, status: 'cancelled', }, - [], + jars, ), active: false, }, @@ -134,14 +171,8 @@ export const Failed: Story = { status: 'failed', error: 'Error description', }, - [], + jars, ), active: false, }, } - -export const Maker: Story = { - args: { - value: toScheduleEntry(makerPhase, []), - }, -} From 037bf95784c18682bbb34bb8ce80c77b99db4ec0 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sun, 9 Aug 2026 20:37:53 +0200 Subject: [PATCH 43/56] chore(dev): expose orderbook watcher for dev container --- docker/regtest/docker-compose-common.yml | 4 ++++ docker/regtest/docker-compose.yml | 2 ++ docs/developing.md | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docker/regtest/docker-compose-common.yml b/docker/regtest/docker-compose-common.yml index 1ed04619..0bd00c63 100644 --- a/docker/regtest/docker-compose-common.yml +++ b/docker/regtest/docker-compose-common.yml @@ -86,6 +86,8 @@ services: NETWORK_CONFIG__NETWORK: testnet NETWORK_CONFIG__BITCOIN_NETWORK: regtest NETWORK_CONFIG__DIRECTORY_SERVERS: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + ORDERBOOK_WATCHER__HTTP_HOST: joinmarket_ng_orderbook_watcher + ORDERBOOK_WATCHER__HTTP_PORT: 8000 OBWATCH_URL: http://joinmarket_ng_orderbook_watcher:8000 TOR__SOCKS_HOST: tor TOR__SOCKS_PORT: 9050 @@ -124,6 +126,8 @@ services: NETWORK_CONFIG__NETWORK: testnet NETWORK_CONFIG__BITCOIN_NETWORK: regtest NETWORK_CONFIG__DIRECTORY_SERVERS: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + ORDERBOOK_WATCHER__HTTP_HOST: joinmarket_ng_orderbook_watcher + ORDERBOOK_WATCHER__HTTP_PORT: 8000 OBWATCH_URL: http://joinmarket_ng_orderbook_watcher:8000 TOR__SOCKS_HOST: tor TOR__SOCKS_PORT: 9050 diff --git a/docker/regtest/docker-compose.yml b/docker/regtest/docker-compose.yml index 319892e0..9adaeeca 100644 --- a/docker/regtest/docker-compose.yml +++ b/docker/regtest/docker-compose.yml @@ -156,6 +156,8 @@ services: TOR__SOCKS_HOST: tor TOR__SOCKS_PORT: 9050 LOGGING__LEVEL: DEBUG + ORDERBOOK_WATCHER__HTTP_HOST: 0.0.0.0 + ORDERBOOK_WATCHER__HTTP_PORT: 8000 expose: - 8000 ports: diff --git a/docs/developing.md b/docs/developing.md index e796c92c..1355b4c7 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -26,7 +26,7 @@ If your separately running joinmarket-ng services use different ports, you can o JAM_BACKEND=joinmarket-ng \ JMWALLETD_API_PORT=28183 \ JMWALLETD_WEBSOCKET_PORT=28283 \ -JMOBWATCH_PORT=8080 \ +JMOBWATCH_PORT=8000 \ npm run dev ``` From b9f2730a72e2d34270a33f4c5077548ba1ef0da9 Mon Sep 17 00:00:00 2001 From: Kishore B Date: Mon, 10 Aug 2026 00:14:07 +0530 Subject: [PATCH 44/56] feat(wallet): implement wallet transaction history (#1368) * feat(wallet): implement transaction history feature - Add useQueryWalletHistory hook to fetch paginated transaction history from daemon - Add TxHistoryTable component to render sortable and expandable transaction rows - Add TxHistoryContent container for handling loading states and batch pagination - Add TxHistoryOverlay full-screen dialog for viewing complete history - Integrate inline Recent Activity section into MainWalletPage - Add translation keys - Add comprehensive unit tests Signed-off-by: kishore08-07 * Update src/i18n/locales/en/translation.json Co-authored-by: Thebora Kompanioni * feat(wallet): migrate transaction history to dedicated page and update dashboard view - Move transaction history to top-level /wallet/history route and add sidebar link - Refactor dashboard transaction history into a collapsible accordion section - Auto-refetch transaction history on UTXO set hash () updates - Fallback to raw string for unrecognized roles in - Standardize i18n keys Signed-off-by: kishore08-07 * style(wallet): update taker role badge variant to cj-change in TxHistoryTable Signed-off-by: kishore08-07 * chore: hide tx history behind feature toggle --------- Signed-off-by: kishore08-07 Co-authored-by: Thebora Kompanioni Co-authored-by: theborakompanioni --- src/App.tsx | 2 + src/components/MainWalletPage.test.tsx | 23 ++ src/components/MainWalletPage.tsx | 41 ++- src/components/layout/AppSidebar.test.tsx | 3 + src/components/layout/AppSidebar.tsx | 18 +- src/components/settings/SettingsPage.tsx | 31 ++ .../wallet/TxHistoryContent.test.tsx | 113 +++++++ src/components/wallet/TxHistoryContent.tsx | 132 ++++++++ .../wallet/TxHistoryOverlay.test.tsx | 66 ++++ src/components/wallet/TxHistoryOverlay.tsx | 36 ++ src/components/wallet/TxHistoryPage.test.tsx | 26 ++ src/components/wallet/TxHistoryPage.tsx | 19 ++ src/components/wallet/TxHistoryTable.test.tsx | 108 ++++++ src/components/wallet/TxHistoryTable.tsx | 317 ++++++++++++++++++ src/constants/routes.ts | 1 + src/hooks/useQueryWalletHistory.test.ts | 72 ++++ src/hooks/useQueryWalletHistory.ts | 67 ++++ src/i18n/locales/en/translation.json | 28 ++ src/store/jamSettingsStore.ts | 12 + 19 files changed, 1112 insertions(+), 3 deletions(-) create mode 100644 src/components/wallet/TxHistoryContent.test.tsx create mode 100644 src/components/wallet/TxHistoryContent.tsx create mode 100644 src/components/wallet/TxHistoryOverlay.test.tsx create mode 100644 src/components/wallet/TxHistoryOverlay.tsx create mode 100644 src/components/wallet/TxHistoryPage.test.tsx create mode 100644 src/components/wallet/TxHistoryPage.tsx create mode 100644 src/components/wallet/TxHistoryTable.test.tsx create mode 100644 src/components/wallet/TxHistoryTable.tsx create mode 100644 src/hooks/useQueryWalletHistory.test.ts create mode 100644 src/hooks/useQueryWalletHistory.ts diff --git a/src/App.tsx b/src/App.tsx index e1fb0c4e..d3887a78 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,7 @@ import { EarnReportPage } from './components/earn/report/EarnReportPage' import { RootLayout } from './components/layout/RootLayout' import { LockWalletConfirmDialog } from './components/ui/jam/LockWalletConfirmDialog' import { Spinner } from './components/ui/spinner' +import { TxHistoryPage } from './components/wallet/TxHistoryPage' import { WalletJarsDetailsPage } from './components/wallet/WalletJarsDetailsPage' import { useJamSessionInfoContext } from './context/JamSessionInfoContext' import { JamSessionInfoContextProvider } from './context/JamSessionInfoContextProvider' @@ -240,6 +241,7 @@ function App() { path={routes.walletJarsDetails} element={} /> + } /> {isDeveloperMode && isDebugFeatureEnabled('devPage') && ( ({ useJars: () => ({ jars }), })) +vi.mock('@/store/jamSettingsStore', () => ({ + usePreviewFeatures: () => ({ + 'tx-history': true, + }), +})) + vi.mock('./wallet/WalletJarsDetailsOverlay', () => ({ WalletJarsDetailsOverlay: ({ open, @@ -53,6 +59,10 @@ vi.mock('./wallet/WalletJarsDetailsOverlay', () => ({ ), })) +vi.mock('./wallet/TxHistoryContent', () => ({ + TxHistoryContent: () =>
, +})) + vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString, onClick }: { valueString: string; onClick?: () => void }) => ( {valueString} @@ -142,4 +152,17 @@ describe('MainWalletPage', () => { fireEvent.click(screen.getByText('5000')) expect(toggleDisplayMode).toHaveBeenCalled() }) + + it('expands and collapses transaction history via accordion toggle button', () => { + render() + expect(screen.queryByTestId('TxHistoryContent')).not.toBeInTheDocument() + + const toggleButton = screen.getByTitle('tx_history.expand_history') + fireEvent.click(toggleButton) + expect(screen.getByTestId('TxHistoryContent')).toBeInTheDocument() + + const collapseButton = screen.getByTitle('tx_history.collapse_history') + fireEvent.click(collapseButton) + expect(screen.queryByTestId('TxHistoryContent')).not.toBeInTheDocument() + }) }) diff --git a/src/components/MainWalletPage.tsx b/src/components/MainWalletPage.tsx index a239142f..68b8aa9b 100644 --- a/src/components/MainWalletPage.tsx +++ b/src/components/MainWalletPage.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { DownloadIcon, InfoIcon, RefreshCwIcon, UploadIcon } from 'lucide-react' +import { ChevronDownIcon, DownloadIcon, InfoIcon, RefreshCwIcon, UploadIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { Alert, AlertDescription } from '@/components/ui/alert' @@ -17,8 +17,11 @@ import { import { getErrorReason } from '@/lib/errorReason' import type { WalletFileName } from '@/lib/utils' import { cn, shortenStringMiddle, walletDisplayName } from '@/lib/utils' +import { usePreviewFeatures } from '@/store/jamSettingsStore' import { Balance } from './ui/jam/Balance' import { Spinner } from './ui/spinner' +import { TxHistoryContent } from './wallet/TxHistoryContent' +import { TxHistoryOverlay } from './wallet/TxHistoryOverlay' import { WalletJarsDetailsOverlay } from './wallet/WalletJarsDetailsOverlay' interface MainWalletPageProps { @@ -28,8 +31,11 @@ interface MainWalletPageProps { export default function MainWalletPage({ walletFileName }: MainWalletPageProps) { const { t } = useTranslation() const navigate = useNavigate() + const previewFeatures = usePreviewFeatures() const [selectedJar, setSelectedJar] = useState() const [isWalletJarsDetailsOpen, setIsWalletJarsDetailsOpen] = useState(false) + const [isTxHistoryOpen, setIsTxHistoryOpen] = useState(false) + const [isHistoryExpanded, setIsHistoryExpanded] = useState(false) const { toggleDisplayMode } = useJamDisplayContext() const { isLoading, isFetching, error, refetch: refetchWalletData } = useJamWalletInfoContext() @@ -55,6 +61,10 @@ export default function MainWalletPage({ walletFileName }: MainWalletPageProps) walletFileName={walletFileName} selectedJarIndex={selectedJar?.jarIndex} /> + {previewFeatures?.['tx-history'] === true ? ( + + ) : null} +

@@ -87,8 +97,37 @@ export default function MainWalletPage({ walletFileName }: MainWalletPageProps) {t('current_wallet.button_withdraw')}

+ + {previewFeatures?.['tx-history'] === true ? ( +
+
+ +
+
+ ) : null}
+ {isHistoryExpanded && ( + setIsTxHistoryOpen(true)} + className="w-full max-w-6xl" + /> + )} + {error ? ( diff --git a/src/components/layout/AppSidebar.test.tsx b/src/components/layout/AppSidebar.test.tsx index f9808e55..c9cccf07 100644 --- a/src/components/layout/AppSidebar.test.tsx +++ b/src/components/layout/AppSidebar.test.tsx @@ -77,6 +77,9 @@ vi.mock('@/hooks/useFeatures', () => ({ vi.mock('@/store/jamSettingsStore', () => ({ useDeveloperMode: () => ({ enabled: mocks.developerMode }), + usePreviewFeatures: () => ({ + 'tx-history': true, + }), })) vi.mock('../dev/DevBadge', () => ({ diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index 404b80d0..40f1cacd 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -6,6 +6,7 @@ import { CurlyBracesIcon, DownloadIcon, HandCoinsIcon, + HistoryIcon, LogsIcon, MilkIcon, NotebookTabsIcon, @@ -40,14 +41,16 @@ import { isDebugFeatureEnabled, isDevMode } from '@/constants/debugFeatures' import { POST_LOGIN_TOUR_EVENT } from '@/constants/onboarding' import { routes } from '@/constants/routes' import { useFeatures } from '@/hooks/useFeatures' -import { useDeveloperMode } from '@/store/jamSettingsStore' +import { useDeveloperMode, usePreviewFeatures } from '@/store/jamSettingsStore' import { DevBadge } from '../dev/DevBadge' +import { Badge } from '../ui/badge' export function AppSidebar({ side }: Pick, 'side'>) { const { t } = useTranslation() const { toggleSidebar } = useSidebar() const { enabled: isDeveloperMode } = useDeveloperMode() + const previewFeatures = usePreviewFeatures() const { isFeatureEnabled } = useFeatures() const mainItems = useMemo( @@ -94,8 +97,18 @@ export function AppSidebar({ side }: Pick, url: routes.walletJarsDetails, icon: MilkIcon, }, + ...(previewFeatures?.['tx-history'] !== true + ? [] + : [ + { + title: t('sidebar.item_history.label'), + url: routes.txHistory, + icon: HistoryIcon, + preview: true, + }, + ]), ], - [t], + [t, previewFeatures], ) const settingsItems = useMemo( @@ -180,6 +193,7 @@ export function AppSidebar({ side }: Pick, {item.title} + {item.preview ? {/* TODO: i18n */ 'Preview'} : null} {item.subitems?.length && ( diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 93190ee7..d0f1337a 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -16,6 +16,8 @@ import { UnfoldHorizontalIcon, KeyRoundIcon, HandCoinsIcon, + SparklesIcon, + HistoryIcon, } from 'lucide-react' import { useTheme } from 'next-themes' import { useTranslation } from 'react-i18next' @@ -214,6 +216,35 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) to={JAM_REPO_URL} external={true} /> + + { + jamSettings.update({ previewFeatures: checked ? {} : undefined }) + }} + /> + {!!jamSettings.state.previewFeatures && ( + <> + + { + jamSettings.update({ + previewFeatures: { + ...jamSettings.state.previewFeatures, + 'tx-history': checked, + }, + }) + }} + /> + + )} {isDevMode() && ( <> diff --git a/src/components/wallet/TxHistoryContent.test.tsx b/src/components/wallet/TxHistoryContent.test.tsx new file mode 100644 index 00000000..b22a61a9 --- /dev/null +++ b/src/components/wallet/TxHistoryContent.test.tsx @@ -0,0 +1,113 @@ +import type { WalletHistoryResponse } from '@joinmarket-webui/joinmarket-ng-api-ts/jm' +import type { UseQueryResult } from '@tanstack/react-query' +import { render, screen, fireEvent } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { useQueryWalletHistory } from '@/hooks/useQueryWalletHistory' +import { TxHistoryContent } from './TxHistoryContent' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/hooks/useQueryWalletHistory', () => ({ + useQueryWalletHistory: vi.fn(), +})) + +vi.mock('./TxHistoryTable', () => ({ + TxHistoryTable: ({ history, compact }: { history: unknown[]; compact: boolean }) => ( +
+ Mock Table (Rows: {history.length}) +
+ ), +})) + +vi.mock('@/components/ui/alert', () => ({ + Alert: ({ children, variant }: { children: React.ReactNode; variant?: string }) => ( +
+ {children} +
+ ), + AlertDescription: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +vi.mock('@/components/ui/spinner', () => ({ + Spinner: () =>
, +})) + +const mockUseQuery = useQueryWalletHistory as unknown as ReturnType + +const mockQueryResult = (overrides: Partial>) => + ({ + isLoading: false, + error: null, + isFetching: false, + refetch: vi.fn(), + ...overrides, + }) as UseQueryResult + +describe('TxHistoryContent', () => { + it('renders loading state', () => { + mockUseQuery.mockReturnValue({ + history: [], + queryResult: mockQueryResult({ isLoading: true }), + }) + + render() + expect(screen.getByTestId('mock-loading')).toBeInTheDocument() + }) + + it('renders error state', () => { + mockUseQuery.mockReturnValue({ + history: [], + queryResult: mockQueryResult({ error: new Error('failed') }), + }) + + render() + expect(screen.getByTestId('mock-alert')).toBeInTheDocument() + expect(screen.getByText('tx_history.error_loading')).toBeInTheDocument() + }) + + it('renders the table with data', () => { + const mockHistory = [{ txid: '1' }, { txid: '2' }] + mockUseQuery.mockReturnValue({ + history: mockHistory, + queryResult: mockQueryResult({}), + }) + + render() + expect(screen.getByTestId('mock-table')).toHaveAttribute('data-compact', 'true') + expect(screen.getByText('Mock Table (Rows: 2)')).toBeInTheDocument() + }) + + it('renders Load More button when not compact and handles clicks', () => { + const mockHistory = Array.from({ length: 10 }, () => ({ txid: 'mock' })) + const refetch = vi.fn() + + mockUseQuery.mockReturnValue({ + history: mockHistory, + queryResult: mockQueryResult({ refetch, isFetching: false }), + }) + + render() + + const loadMoreButton = screen.getByRole('button', { name: 'tx_history.button_load_more' }) + expect(loadMoreButton).toBeInTheDocument() + + fireEvent.click(loadMoreButton) + expect(mockUseQuery).toHaveBeenLastCalledWith(expect.objectContaining({ limit: 20 })) + }) + + it('hides Load More button if history length is less than limit', () => { + const mockHistory = Array.from({ length: 5 }, () => ({ txid: 'mock' })) + + mockUseQuery.mockReturnValue({ + history: mockHistory, + queryResult: mockQueryResult({}), + }) + + render() + expect(screen.queryByRole('button', { name: 'tx_history.button_load_more' })).not.toBeInTheDocument() + }) +}) diff --git a/src/components/wallet/TxHistoryContent.tsx b/src/components/wallet/TxHistoryContent.tsx new file mode 100644 index 00000000..f73118e4 --- /dev/null +++ b/src/components/wallet/TxHistoryContent.tsx @@ -0,0 +1,132 @@ +import { useMemo, useState } from 'react' +import { AlertTriangleIcon, ListIcon, PlusIcon, RefreshCwIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { DevBadge } from '@/components/dev/DevBadge' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Button } from '@/components/ui/button' +import { Spinner } from '@/components/ui/spinner' +import { useJamWalletInfoContext } from '@/context/JamWalletInfoContext' +import { useQueryWalletHistory, type HistoryEntry } from '@/hooks/useQueryWalletHistory' +import { cn, type WalletFileName } from '@/lib/utils' +import { useDeveloperMode } from '@/store/jamSettingsStore' +import { TxHistoryTable } from './TxHistoryTable' + +interface TxHistoryContentProps { + walletFileName: WalletFileName + className?: string + initialLimit?: number + compact?: boolean + enabled?: boolean + onViewAll?: () => void +} + +const useSafeUtxosHashHex = () => { + try { + const context = useJamWalletInfoContext() + return context?.utxosHashHex + } catch { + return undefined + } +} + +export const TxHistoryContent = ({ + walletFileName, + className, + initialLimit = 10, + compact = false, + enabled = true, + onViewAll, +}: TxHistoryContentProps) => { + const { t } = useTranslation() + const [limit, setLimit] = useState(initialLimit) + const { enabled: isDeveloperMode } = useDeveloperMode() + const [demoEntries, setDemoEntries] = useState([]) + const utxosHashHex = useSafeUtxosHashHex() + + const { history, queryResult } = useQueryWalletHistory({ + walletFileName, + limit, + enabled, + utxosHashHex, + }) + + const combinedHistory = useMemo(() => [...demoEntries, ...history], [demoEntries, history]) + + return ( +
+
+

{t('tx_history.section_title')}

+
+ {isDeveloperMode && ( + + )} + + + + {compact && onViewAll ? ( + + ) : null} +
+
+ + {queryResult.isLoading ? ( +
+ + {t('global.loading')} +
+ ) : queryResult.error ? ( + + + {t('tx_history.error_loading')} + + ) : ( + + )} + + {!compact && combinedHistory.length >= limit ? ( +
+ +
+ ) : null} +
+ ) +} diff --git a/src/components/wallet/TxHistoryOverlay.test.tsx b/src/components/wallet/TxHistoryOverlay.test.tsx new file mode 100644 index 00000000..f2f44194 --- /dev/null +++ b/src/components/wallet/TxHistoryOverlay.test.tsx @@ -0,0 +1,66 @@ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { TxHistoryOverlay } from './TxHistoryOverlay' + +type ChildrenProps = { children: ReactNode } +type DialogProps = ChildrenProps & { open?: boolean; onOpenChange?: (open: boolean) => void } + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('./TxHistoryContent', () => ({ + TxHistoryContent: ({ walletFileName }: { walletFileName: string }) => ( +
+ Content +
+ ), +})) + +vi.mock('../ui/jam/PageTitle', () => ({ + default: ({ title }: { title: string }) =>

{title}

, +})) + +// Mock Dialog to avoid dealing with portals and radix UI internals +vi.mock('@/components/ui/dialog', () => ({ + Dialog: ({ children, open, onOpenChange }: DialogProps) => + open ? ( +
+ + {children} +
+ ) : null, + DialogContent: ({ children }: ChildrenProps) =>
{children}
, + DialogHeader: ({ children }: ChildrenProps) =>
{children}
, + DialogTitle: ({ children }: ChildrenProps) =>
{children}
, +})) + +describe('TxHistoryOverlay', () => { + it('renders dialog content when open', () => { + render() + + expect(screen.getByTestId('page-title')).toHaveTextContent('tx_history.overlay_title') + + const content = screen.getByTestId('tx-history-content') + expect(content).toBeInTheDocument() + expect(content).toHaveAttribute('data-wallet', 'test-wallet.jmdat') + }) + + it('calls onOpenChange when closed', () => { + const onOpenChange = vi.fn() + render() + + const closeButton = screen.getByText('Close') + fireEvent.click(closeButton) + + expect(onOpenChange).toHaveBeenCalledWith(false) + }) + + it('does not render when closed', () => { + render() + expect(screen.queryByTestId('dialog')).not.toBeInTheDocument() + }) +}) diff --git a/src/components/wallet/TxHistoryOverlay.tsx b/src/components/wallet/TxHistoryOverlay.tsx new file mode 100644 index 00000000..6b77b12d --- /dev/null +++ b/src/components/wallet/TxHistoryOverlay.tsx @@ -0,0 +1,36 @@ +import type { ComponentProps } from 'react' +import { useTranslation } from 'react-i18next' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import type { WithRequiredProperty } from '@/types/global' +import PageTitle from '../ui/jam/PageTitle' +import { TxHistoryContent } from './TxHistoryContent' + +type TxHistoryOverlayProps = WithRequiredProperty< + Omit, 'children'>, + 'open' | 'onOpenChange' +> & + Pick, 'walletFileName'> + +export function TxHistoryOverlay({ open, onOpenChange, walletFileName, ...dialogProps }: TxHistoryOverlayProps) { + const { t } = useTranslation() + + return ( + onOpenChange(false)} {...dialogProps}> + + + + + + + +
+ +
+
+
+ ) +} diff --git a/src/components/wallet/TxHistoryPage.test.tsx b/src/components/wallet/TxHistoryPage.test.tsx new file mode 100644 index 00000000..c96405d0 --- /dev/null +++ b/src/components/wallet/TxHistoryPage.test.tsx @@ -0,0 +1,26 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { TxHistoryPage } from './TxHistoryPage' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('./TxHistoryContent', () => ({ + TxHistoryContent: ({ compact }: { compact: boolean }) => ( +
+ ), +})) + +describe('TxHistoryPage', () => { + it('renders TxHistoryContent in non-compact page mode with title', () => { + render() + + expect(screen.getByText('tx_history.title')).toBeInTheDocument() + const content = screen.getByTestId('tx-history-content') + expect(content).toBeInTheDocument() + expect(content).toHaveAttribute('data-compact', 'false') + }) +}) diff --git a/src/components/wallet/TxHistoryPage.tsx b/src/components/wallet/TxHistoryPage.tsx new file mode 100644 index 00000000..b7c2576f --- /dev/null +++ b/src/components/wallet/TxHistoryPage.tsx @@ -0,0 +1,19 @@ +import { useTranslation } from 'react-i18next' +import PageTitle from '@/components/ui/jam/PageTitle' +import type { WalletFileName } from '@/lib/utils' +import { TxHistoryContent } from './TxHistoryContent' + +interface TxHistoryPageProps { + walletFileName: WalletFileName +} + +export function TxHistoryPage({ walletFileName }: TxHistoryPageProps) { + const { t } = useTranslation() + + return ( +
+ + +
+ ) +} diff --git a/src/components/wallet/TxHistoryTable.test.tsx b/src/components/wallet/TxHistoryTable.test.tsx new file mode 100644 index 00000000..f9dc23ac --- /dev/null +++ b/src/components/wallet/TxHistoryTable.test.tsx @@ -0,0 +1,108 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { HistoryEntry } from '@/hooks/useQueryWalletHistory' +import { TxHistoryTable } from './TxHistoryTable' + +const mocks = vi.hoisted(() => ({ + toastSuccess: vi.fn(), + toastError: vi.fn(), +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('sonner', () => ({ + toast: { + success: mocks.toastSuccess, + error: mocks.toastError, + }, +})) + +vi.mock('../ui/jam/Address', () => ({ + Address: ({ value }: { value: string }) => {value}, +})) + +vi.mock('../ui/jam/Balance', () => ({ + Balance: ({ valueString }: { valueString: string }) => {valueString}, +})) + +const mockDeposit: HistoryEntry = { + timestamp: '2026-07-28T12:00:00Z', + role: 'deposit', + txid: 'tx1', + cj_amount: 5000, + net_fee: 0, + confirmations: 10, + success: true, + source_mixdepth: 0, +} + +const mockSend: HistoryEntry = { + timestamp: '2026-07-28T13:00:00Z', + role: 'send', + txid: 'tx2', + cj_amount: 10000, + net_fee: -100, + confirmations: 0, + success: true, + source_mixdepth: 0, +} + +const mockFailed: HistoryEntry = { + timestamp: '2026-07-28T14:00:00Z', + role: 'taker', + txid: 'tx3', + cj_amount: 0, + net_fee: 0, + confirmations: 0, + success: false, + failure_reason: 'aborted', + source_mixdepth: 1, +} + +describe('TxHistoryTable', () => { + beforeEach(() => { + mocks.toastSuccess.mockReset() + mocks.toastError.mockReset() + }) + + it('renders history rows and sorts by date', () => { + render() + + expect(screen.getByText('tx_history.label_role_deposit')).toBeInTheDocument() + expect(screen.getByText('tx_history.label_role_send')).toBeInTheDocument() + + const dateHeader = screen.getByText('tx_history.column_title_date') + fireEvent.click(dateHeader) + expect(dateHeader).toBeInTheDocument() + }) + + it('renders empty history alert', () => { + render() + expect(screen.getByText('tx_history.empty_history')).toBeInTheDocument() + }) + + it('expands row details to show raw JSON', () => { + render() + + const detailsButton = screen.getByRole('button', { name: 'jar_details.utxo_list.row_button_details' }) + fireEvent.click(detailsButton) + + // Check if the JSON is rendered + expect(screen.getByText(/"txid": "tx1"/u)).toBeInTheDocument() + }) + + it('sorts columns correctly', () => { + render() + + fireEvent.click(screen.getByText('tx_history.column_title_amount')) + fireEvent.click(screen.getByText('tx_history.column_title_net_fee')) + fireEvent.click(screen.getByText('tx_history.column_title_confirmations')) + fireEvent.click(screen.getByText('tx_history.column_title_role')) + + expect(screen.getByText('tx_history.label_role_taker')).toBeInTheDocument() + }) +}) diff --git a/src/components/wallet/TxHistoryTable.tsx b/src/components/wallet/TxHistoryTable.tsx new file mode 100644 index 00000000..8abb3ba5 --- /dev/null +++ b/src/components/wallet/TxHistoryTable.tsx @@ -0,0 +1,317 @@ +import { useEffect, useMemo, useState } from 'react' +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getExpandedRowModel, + getPaginationRowModel, + getSortedRowModel, + type CellContext, + type PaginationState, + type Row, + type SortingState, + useReactTable, +} from '@tanstack/react-table' +import type { TFunction } from 'i18next' +import { CheckIcon, ChevronDownIcon, CopyIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' +import { TablePagination } from '@/components/ui/jam/TablePagination' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import type { HistoryEntry } from '@/hooks/useQueryWalletHistory' +import { cn, shortenStringMiddle } from '@/lib/utils' +import { Alert, AlertDescription } from '../ui/alert' +import { Button } from '../ui/button' +import { buttonVariants } from '../ui/button-variants' +import { Card, CardContent } from '../ui/card' +import { Balance } from '../ui/jam/Balance' +import { CopyButton } from '../ui/jam/CopyButton' +import { SortIcon } from '../ui/jam/SortIcon' +import { StatusBadge } from '../ui/jam/StatusBadge' + +const ITEMS_PER_PAGE = 25 + +type KnownHistoryRole = 'maker' | 'taker' | 'send' | 'deposit' +type StatusBadgeVariant = NonNullable[0]['variant']> + +const ROLE_VARIANT: Record = { + maker: 'cj-out', + taker: 'cj-change', + send: 'used-empty', + deposit: 'deposit', +} + +const columnHelper = createColumnHelper() + +const historyRole = (entry: HistoryEntry): KnownHistoryRole | undefined => { + return ['maker', 'taker', 'send', 'deposit'].includes(entry.role ?? '') ? (entry.role as KnownHistoryRole) : undefined +} + +const roleLabel = (entry: HistoryEntry, t: TFunction) => { + const role = historyRole(entry) + return role ? t(`tx_history.label_role_${role}`) : entry.role || '' +} + +const dateTimeValue = (value: string) => { + const date = new Date(value) + return Number.isNaN(date.getTime()) ? 0 : date.getTime() +} + +const formatDateTime = (value: string) => { + const date = new Date(value) + return Number.isNaN(date.getTime()) ? value : date.toLocaleString() +} + +const TxHistoryDetails = ({ entry }: { entry: HistoryEntry }) => { + return ( + + +
{JSON.stringify(entry, null, 2)}
+
+
+ ) +} + +const TxHistoryTableRow = ({ row }: { row: Row }) => { + return ( + <> + + {row.getVisibleCells().map((cell) => { + const alignCenter = cell.column.columnDef.meta?.align === 'center' + const alignRight = cell.column.columnDef.meta?.align === 'right' + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) + })} + + {row.getIsExpanded() && ( + + + + + + )} + + ) +} + +const txHistoryTableColumns = (t: TFunction) => { + return [ + columnHelper.accessor('timestamp', { + header: () => t('tx_history.column_title_date'), + sortingFn: (a, b) => dateTimeValue(a.original.timestamp) - dateTimeValue(b.original.timestamp), + cell: (info) => {formatDateTime(info.getValue())}, + meta: { + alphabetic: true, + }, + }), + columnHelper.accessor('role', { + header: () => t('tx_history.column_title_role'), + sortingFn: (a, b) => roleLabel(a.original, t).localeCompare(roleLabel(b.original, t)), + cell: (info) => { + const role = historyRole(info.row.original) + return ( + {roleLabel(info.row.original, t)} + ) + }, + }), + columnHelper.accessor('cj_amount', { + header: () => t('tx_history.column_title_amount'), + sortingFn: (a, b) => (a.original.cj_amount ?? 0) - (b.original.cj_amount ?? 0), + cell: (info) => , + meta: { + numeric: true, + align: 'right', + }, + }), + columnHelper.accessor('net_fee', { + header: () => t('tx_history.column_title_net_fee'), + sortingFn: (a, b) => (a.original.net_fee ?? 0) - (b.original.net_fee ?? 0), + cell: (info) => , + meta: { + numeric: true, + align: 'right', + }, + }), + columnHelper.accessor('confirmations', { + header: () => t('tx_history.column_title_confirmations'), + sortingFn: (a, b) => (a.original.confirmations ?? 0) - (b.original.confirmations ?? 0), + cell: (info) => <>{info.getValue() ?? 0}, + meta: { + numeric: true, + align: 'center', + }, + }), + columnHelper.accessor('txid', { + header: () => t('tx_history.column_title_txid'), + cell: (info) => { + const txid = info.getValue() ?? '' + return txid ? ( +
+ {shortenStringMiddle(txid, 16)} + } + successText={} + className={cn(buttonVariants({ variant: 'outline', size: 'icon-xs' }), 'shrink-0')} + onSuccess={() => toast.success(t('tx_history.copy_txid_success'))} + onError={() => toast.error(t('tx_history.copy_txid_error'))} + /> +
+ ) : ( + '' + ) + }, + enableSorting: false, + }), + { + id: 'expand-col', + cell: ({ row }: CellContext) => { + return row.getCanExpand() ? ( + + ) : ( + '' + ) + }, + enableSorting: false, + }, + ] +} + +interface TxHistoryTableProps { + history: HistoryEntry[] + compact?: boolean +} + +export const TxHistoryTable = ({ history, compact = false }: TxHistoryTableProps) => { + const { t } = useTranslation() + const [sorting, setSorting] = useState([{ id: 'timestamp', desc: true }]) + const [isShowAll, setIsShowAll] = useState(false) + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: compact ? 5 : ITEMS_PER_PAGE, + }) + + useEffect(() => { + if (isShowAll) { + setPagination((previous) => ({ + ...previous, + pageSize: Math.max(1, history.length), + pageIndex: 0, + })) + } + }, [history.length, isShowAll]) + + const columns = useMemo(() => txHistoryTableColumns(t), [t]) + + const table = useReactTable({ + data: history, + columns, + state: { + sorting, + pagination, + }, + autoResetPageIndex: true, + getRowId: (row, index) => `${row.txid || 'notxid'}-${row.source_mixdepth ?? 0}-${row.timestamp}-${index}`, + onSortingChange: setSorting, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getRowCanExpand: () => true, + getExpandedRowModel: getExpandedRowModel(), + paginateExpandedRows: false, + }) + + if (history.length === 0) { + return ( + + {t('tx_history.empty_history')} + + ) + } + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const canSort = header.column.getCanSort() + const alignCenter = header.column.columnDef.meta?.align === 'center' + const alignRight = header.column.columnDef.meta?.align === 'right' + return ( + header.column.toggleSorting() : undefined} + > +
0 && !header.column.getIsSorted(), + })} + > + {flexRender(header.column.columnDef.header, header.getContext())} + {canSort ? : undefined} +
+
+ ) + })} +
+ ))} +
+ + {table.getRowModel().rows.map((row) => ( + + ))} + +
+
+ + {!compact && ( + table.setPageIndex(page - 1)} + onItemsPerPageChange={(newItemsPerPage) => { + if (newItemsPerPage === -1) { + setIsShowAll(true) + table.setPageSize(history.length || 1) + } else { + setIsShowAll(false) + table.setPageSize(newItemsPerPage) + } + table.setPageIndex(0) + }} + /> + )} +
+ ) +} diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 150fb238..d307413d 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -13,6 +13,7 @@ export const routes = { orderbook: '/orderbook', logs: '/logs', walletJarsDetails: '/wallet/jars', + txHistory: '/wallet/history', /* walletList: '/', wallet: '/wallet' */ __dev: '/dev', __devErrorExample: '/dev/error-example', diff --git a/src/hooks/useQueryWalletHistory.test.ts b/src/hooks/useQueryWalletHistory.test.ts new file mode 100644 index 00000000..440e5fa8 --- /dev/null +++ b/src/hooks/useQueryWalletHistory.test.ts @@ -0,0 +1,72 @@ +import { renderHook } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import type { WalletFileName } from '@/lib/utils' +import { useQueryWalletHistory } from './useQueryWalletHistory' + +const queryMock = vi.fn<(options: unknown) => unknown>() + +vi.mock('@tanstack/react-query', () => ({ + useQuery: (options: unknown) => queryMock(options), +})) + +vi.mock('zustand', () => ({ + useStore: () => ({ session: 'mock-session' }), +})) + +vi.mock('@/hooks/useApiClient', () => ({ + useApiClient: () => ({}), +})) + +vi.mock('@/constants/debugFeatures', () => ({ + isDevMode: () => false, +})) + +vi.mock('@/lib/queryClient', () => ({ + withQueryDelay: (function_: unknown) => function_, +})) + +vi.mock('@/store/jmSessionStore', () => ({ + jmSessionStore: {}, +})) + +vi.mock('@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query', () => ({ + wallethistoryOptions: () => ({ queryKey: ['history'], queryFn: vi.fn() }), +})) + +describe('useQueryWalletHistory', () => { + it('returns history and queryResult when data exists', () => { + const mockHistory = [{ txid: 'tx1', timestamp: '2026-07-28' }] + queryMock.mockReturnValue({ data: { history: mockHistory } }) + + const { result } = renderHook(() => useQueryWalletHistory({ walletFileName: 'wallet.jmdat' })) + + expect(result.current.history).toEqual(mockHistory) + expect(result.current.queryResult.data?.history).toEqual(mockHistory) + }) + + it('returns empty array when data does not exist', () => { + queryMock.mockReturnValue({ data: null }) + + const { result } = renderHook(() => useQueryWalletHistory({ walletFileName: 'wallet.jmdat' })) + + expect(result.current.history).toEqual([]) + }) + + it('is not enabled if walletFileName is empty', () => { + queryMock.mockImplementation((options) => options) + + const { result } = renderHook(() => + useQueryWalletHistory({ walletFileName: undefined as unknown as WalletFileName }), + ) + + expect((result.current.queryResult as { enabled?: boolean }).enabled).toBe(false) + }) + + it('is not enabled if explicitly disabled', () => { + queryMock.mockImplementation((options) => options) + + const { result } = renderHook(() => useQueryWalletHistory({ walletFileName: 'wallet.jmdat', enabled: false })) + + expect((result.current.queryResult as { enabled?: boolean }).enabled).toBe(false) + }) +}) diff --git a/src/hooks/useQueryWalletHistory.ts b/src/hooks/useQueryWalletHistory.ts new file mode 100644 index 00000000..978030cb --- /dev/null +++ b/src/hooks/useQueryWalletHistory.ts @@ -0,0 +1,67 @@ +import { useEffect } from 'react' +import { wallethistoryOptions } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query' +import type { HistoryEntry, WalletHistoryResponse } from '@joinmarket-webui/joinmarket-ng-api-ts/jm' +import { useQuery, type UseQueryResult } from '@tanstack/react-query' +import { useStore } from 'zustand' +import { isDevMode } from '@/constants/debugFeatures' +import { useApiClient } from '@/hooks/useApiClient' +import { withQueryDelay } from '@/lib/queryClient' +import type { WalletFileName } from '@/lib/utils' +import { jmSessionStore } from '@/store/jmSessionStore' + +export type { HistoryEntry } from '@joinmarket-webui/joinmarket-ng-api-ts/jm' + +export type UseQueryWalletHistoryResult = { + history: HistoryEntry[] + queryResult: UseQueryResult +} + +interface UseQueryWalletHistoryProps { + walletFileName: WalletFileName + limit?: number + enabled?: boolean + utxosHashHex?: string +} + +const EMPTY_HISTORY: HistoryEntry[] = [] + +export function useQueryWalletHistory({ + walletFileName, + limit, + enabled = true, + utxosHashHex = '', +}: UseQueryWalletHistoryProps): UseQueryWalletHistoryResult { + const client = useApiClient() + const jmSession = useStore(jmSessionStore, (state) => state.state?.session) + + const queryOptions = wallethistoryOptions({ + client, + path: { walletname: walletFileName || '' }, + query: { limit }, + meta: { + cacheBuster: utxosHashHex, + }, + }) + + const queryResult = useQuery({ + ...queryOptions, + queryFn: withQueryDelay(queryOptions.queryFn, { + // simulate slow mainnet responses in dev mode + throttle: isDevMode() ? 210 : 0, + }), + enabled: enabled && !!walletFileName && !!jmSession, + }) + + const { refetch } = queryResult + + useEffect(() => { + if (utxosHashHex && enabled) { + void refetch() + } + }, [utxosHashHex, enabled, refetch]) + + return { + history: queryResult.data?.history ?? EMPTY_HISTORY, + queryResult, + } +} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index fc0ec977..4f954a33 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -80,6 +80,9 @@ "item_jars": { "label": "Jars" }, + "item_history": { + "label": "History" + }, "item_tour": { "label": "Tour" }, @@ -898,5 +901,30 @@ "subtitle_one": "The selected UTXO is eligible for inclusion in the transaction.", "subtitle_other": "The selected UTXOs are eligible for inclusion in the transaction.", "text_subtitle_addon": "Unselected UTXOs will be frozen and can be unfrozen later anytime." + }, + "tx_history": { + "title": "Transaction History (Experimental)", + "section_title": "Recent Activity", + "overlay_title": "Transaction History (Experimental)", + "button_view_all": "View All", + "button_load_more": "Load More", + "button_generate_demo_entry": "Add Demo Entry", + "button_reload_title": "Reload", + "expand_history": "Show transaction history", + "collapse_history": "Hide transaction history", + "empty_history": "No transactions found.", + "error_loading": "Failed to load transaction history.", + "copy_txid_success": "Transaction ID copied.", + "copy_txid_error": "Failed to copy transaction ID.", + "column_title_date": "Date", + "column_title_role": "Type", + "column_title_amount": "Amount", + "column_title_net_fee": "Net Fee", + "column_title_confirmations": "Confs.", + "column_title_txid": "Transaction", + "label_role_maker": "Maker", + "label_role_taker": "Taker", + "label_role_send": "Send", + "label_role_deposit": "Deposit" } } diff --git a/src/store/jamSettingsStore.ts b/src/store/jamSettingsStore.ts index 1ba6fd65..3363386d 100644 --- a/src/store/jamSettingsStore.ts +++ b/src/store/jamSettingsStore.ts @@ -3,8 +3,15 @@ import { persist, createJSONStorage } from 'zustand/middleware' import { isDevMode } from '@/constants/debugFeatures' import type { Currency } from '@/types/global' +type PreviewFeatures = { + 'tx-history'?: boolean + // add more entries on demand + // 'myCoolNewFeature'?: boolean +} + export type JamSettings = { developerMode: boolean + previewFeatures: PreviewFeatures | undefined privateMode: boolean addressChunking: boolean currencyUnit: Currency @@ -19,6 +26,7 @@ interface JamSettingsStoreState { const initial: JamSettings = { developerMode: isDevMode(), + previewFeatures: isDevMode() ? {} : undefined, privateMode: false, addressChunking: true, currencyUnit: 'sats', @@ -43,3 +51,7 @@ export const useDeveloperMode = () => { const isDeveloperMode = useStore(jamSettingsStore, (state) => state.state.developerMode) return { enabled: isDeveloperMode } } + +export const usePreviewFeatures = () => { + return useStore(jamSettingsStore, (state) => state.state.previewFeatures) +} From 56541c5c2912f4e6a1e702325524005de3116485 Mon Sep 17 00:00:00 2001 From: Anirudh Patwal <161865581+CapThunder19@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:17:48 +0530 Subject: [PATCH 45/56] fix(fb): re-freeze fidelity bond utxo when renew/move sweep fails (#1399) --- .../useFidelityBondSweep.test.ts | 208 ++++++++++++++++++ .../fidelity-bond/useFidelityBondSweep.ts | 31 ++- 2 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 src/components/earn/fidelity-bond/useFidelityBondSweep.test.ts diff --git a/src/components/earn/fidelity-bond/useFidelityBondSweep.test.ts b/src/components/earn/fidelity-bond/useFidelityBondSweep.test.ts new file mode 100644 index 00000000..ca985da0 --- /dev/null +++ b/src/components/earn/fidelity-bond/useFidelityBondSweep.test.ts @@ -0,0 +1,208 @@ +import { renderHook } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Jar } from '@/context/JamWalletInfoContext' +import type { FidelityBondUtxo, Utxo } from '@/hooks/useQueryUtxos' +import { BALANCE_SUMMARY_EMPTY } from '@/lib/balanceSummary' +import { useFidelityBondSweep } from './useFidelityBondSweep' + +const mocks = vi.hoisted(() => ({ + freezeMutateAsync: vi.fn(), + unfreezeMutateAsync: vi.fn(), + directSendMutateAsync: vi.fn(), + setError: vi.fn(), + walletInfoRefetch: vi.fn(), + walletInfo: { + jars: [] as Jar[], + refetch: vi.fn(), + }, +})) + +// `useFidelityBondSweep` only talks to the network through these two hooks — +// mock at that boundary and let the hook's own orchestration logic run for real. +vi.mock('@tanstack/react-query', () => ({ + useMutation: vi.fn((options: { mutationFn: (variables: unknown) => Promise }) => ({ + mutateAsync: options.mutationFn, + isPending: false, + })), +})) + +vi.mock('./useFidelityBondMutations', () => ({ + useFidelityBondMutations: () => ({ + freezeUtxo: { mutateAsync: mocks.freezeMutateAsync }, + unfreezeUtxo: { mutateAsync: mocks.unfreezeMutateAsync }, + directSend: { mutateAsync: mocks.directSendMutateAsync }, + error: undefined, + setError: mocks.setError, + }), +})) + +vi.mock('@/context/JamWalletInfoContext', async (importOriginal) => ({ + ...(await importOriginal()), + useJamWalletInfoContext: () => mocks.walletInfo, +})) + +vi.mock('@/lib/utils', async (importOriginal) => ({ + ...(await importOriginal()), + delayedPromise: vi.fn(() => Promise.resolve()), +})) + +const utxo = (overrides: Partial): Utxo => ({ + utxo: 'tx:0', + address: 'bcrt1qsource', + path: "m/84'/1'/0'/0/0", + label: '', + value: 100_000, + tries: 0, + tries_remaining: 3, + external: false, + mixdepth: 0, + confirmations: 6, + frozen: false, + locktime: undefined, + ...overrides, +}) + +const bondUtxo = (overrides: Partial): FidelityBondUtxo => ({ + ...utxo({ utxo: 'bond:0', frozen: true, ...overrides }), + locktime: '2999-01-01 00:00:00', +}) + +const jar = (jarIndex: number, utxos: Utxo[]): Jar => + ({ + jarIndex, + name: `Jar ${jarIndex}`, + color: '#808080', + balanceSummary: BALANCE_SUMMARY_EMPTY, + utxos, + }) as unknown as Jar + +describe('useFidelityBondSweep', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.freezeMutateAsync.mockResolvedValue(undefined) + mocks.unfreezeMutateAsync.mockResolvedValue(undefined) + mocks.walletInfoRefetch.mockResolvedValue(undefined) + mocks.walletInfo.refetch = mocks.walletInfoRefetch + }) + + it('re-freezes the fidelity bond after a failed sweep that had unfrozen it', async () => { + const bond = bondUtxo({ mixdepth: 0 }) + const other = utxo({ utxo: 'other:0', mixdepth: 0, frozen: false }) + mocks.walletInfo.jars = [jar(0, [bond, other])] + mocks.directSendMutateAsync.mockRejectedValue(new Error('broadcast failed')) + + const { result } = renderHook(() => + useFidelityBondSweep({ + walletFileName: 'wallet.jmdat', + utxo: bond, + unfreezeErrorKey: 'earn.fidelity_bond.error_unfreezing_utxos', + sendErrorKey: 'earn.fidelity_bond.renew.error_renewing_fidelity_bond', + }), + ) + + const txResult = await result.current.sweep({ + destination: 'bcrt1qdestination', + tryFreezeAfterBroadcast: false, + }) + + expect(txResult).toBeUndefined() + + // the other jar utxo was frozen then correctly rolled back + expect(mocks.freezeMutateAsync).toHaveBeenCalledWith({ + path: { walletname: 'wallet.jmdat' }, + body: { 'utxo-string': 'other:0', freeze: true }, + }) + expect(mocks.unfreezeMutateAsync).toHaveBeenCalledWith( + expect.objectContaining({ body: { 'utxo-string': 'other:0', freeze: false } }), + ) + + // the bond itself was unfrozen before the failed send, and MUST be re-frozen in rollback + expect(mocks.unfreezeMutateAsync).toHaveBeenCalledWith({ + path: { walletname: 'wallet.jmdat' }, + body: { 'utxo-string': 'bond:0', freeze: false }, + }) + expect(mocks.freezeMutateAsync).toHaveBeenCalledWith( + expect.objectContaining({ body: { 'utxo-string': 'bond:0', freeze: true } }), + ) + }) + + it('does not attempt to re-freeze the bond on failure when it was never unfrozen', async () => { + const bond = bondUtxo({ mixdepth: 0, frozen: false }) + mocks.walletInfo.jars = [jar(0, [bond])] + mocks.directSendMutateAsync.mockRejectedValue(new Error('broadcast failed')) + + const { result } = renderHook(() => + useFidelityBondSweep({ + walletFileName: 'wallet.jmdat', + utxo: bond, + unfreezeErrorKey: 'earn.fidelity_bond.error_unfreezing_utxos', + sendErrorKey: 'earn.fidelity_bond.renew.error_renewing_fidelity_bond', + }), + ) + + await result.current.sweep({ destination: 'bcrt1qdestination', tryFreezeAfterBroadcast: false }) + + expect(mocks.unfreezeMutateAsync).not.toHaveBeenCalled() + expect(mocks.freezeMutateAsync).not.toHaveBeenCalledWith( + expect.objectContaining({ body: { 'utxo-string': 'bond:0', freeze: true } }), + ) + }) + + it('sweeps successfully: freezes other utxos, unfreezes the bond, then restores the others', async () => { + const bond = bondUtxo({ mixdepth: 0 }) + const other = utxo({ utxo: 'other:0', mixdepth: 0, frozen: false }) + mocks.walletInfo.jars = [jar(0, [bond, other])] + mocks.directSendMutateAsync.mockResolvedValue({ txinfo: { txid: 'renewed-tx' } }) + + const { result } = renderHook(() => + useFidelityBondSweep({ + walletFileName: 'wallet.jmdat', + utxo: bond, + unfreezeErrorKey: 'earn.fidelity_bond.error_unfreezing_utxos', + sendErrorKey: 'earn.fidelity_bond.renew.error_renewing_fidelity_bond', + }), + ) + + const txResult = await result.current.sweep({ + destination: 'bcrt1qdestination', + tryFreezeAfterBroadcast: false, + }) + + expect(txResult).toEqual({ txinfo: { txid: 'renewed-tx' } }) + expect(mocks.directSendMutateAsync).toHaveBeenCalledWith({ + path: { walletname: 'wallet.jmdat' }, + body: { mixdepth: 0, amount_sats: 0, destination: 'bcrt1qdestination' }, + }) + // the other utxo ends up unfrozen again post-broadcast + expect(mocks.unfreezeMutateAsync).toHaveBeenCalledWith( + expect.objectContaining({ body: { 'utxo-string': 'other:0', freeze: false } }), + ) + expect(mocks.walletInfoRefetch).toHaveBeenCalled() + }) + + it('does not re-freeze the bond if the sweep broadcast but the refetch after it fails', async () => { + const bond = bondUtxo({ mixdepth: 0 }) + mocks.walletInfo.jars = [jar(0, [bond])] + mocks.directSendMutateAsync.mockResolvedValue({ txinfo: { txid: 'renewed-tx' } }) + mocks.walletInfoRefetch.mockRejectedValue(new Error('refetch failed')) + + const { result } = renderHook(() => + useFidelityBondSweep({ + walletFileName: 'wallet.jmdat', + utxo: bond, + unfreezeErrorKey: 'earn.fidelity_bond.error_unfreezing_utxos', + sendErrorKey: 'earn.fidelity_bond.renew.error_renewing_fidelity_bond', + }), + ) + + const txResult = await result.current.sweep({ + destination: 'bcrt1qdestination', + tryFreezeAfterBroadcast: false, + }) + + expect(txResult).toEqual({ txinfo: { txid: 'renewed-tx' } }) + expect(mocks.freezeMutateAsync).not.toHaveBeenCalledWith( + expect.objectContaining({ body: { 'utxo-string': 'bond:0', freeze: true } }), + ) + }) +}) diff --git a/src/components/earn/fidelity-bond/useFidelityBondSweep.ts b/src/components/earn/fidelity-bond/useFidelityBondSweep.ts index 340f889f..80c877e8 100644 --- a/src/components/earn/fidelity-bond/useFidelityBondSweep.ts +++ b/src/components/earn/fidelity-bond/useFidelityBondSweep.ts @@ -56,6 +56,9 @@ export function useFidelityBondSweep({ setError(undefined) const frozen: Utxo[] = [] + let bondWasUnfrozen = false + let sweepBroadcasted = false + try { // Freeze other UTXOs in the source jar so only the FB gets swept for (const u of utxosToFreeze) { @@ -71,6 +74,7 @@ export function useFidelityBondSweep({ path: { walletname: walletFileName }, body: { 'utxo-string': utxo.utxo, freeze: false }, }) + bondWasUnfrozen = true } const result = await directSend.mutateAsync({ @@ -81,6 +85,7 @@ export function useFidelityBondSweep({ destination, }, }) + sweepBroadcasted = true // Best-effort cleanup — tx already broadcast, don't throw on unfreeze failure for (const u of frozen) { @@ -117,7 +122,6 @@ export function useFidelityBondSweep({ console.warn('onBroadcastSuccess failed', error) } - await walletInfoRefetch() return result } catch (_ignoredOnPurpose: unknown) { // Best-effort rollback — unfreeze UTXOs that were frozen before the error @@ -133,7 +137,32 @@ export function useFidelityBondSweep({ console.debug('Error while unfreezing previously frozen UTXO in error handling.') } } + + // re-freeze the bond if it was unfrozen before the error. skip once the + // sweep already broadcast, the bond utxo is spent by then + if (bondWasUnfrozen && !sweepBroadcasted) { + try { + await freezeUtxo.mutateAsync({ + path: { walletname: walletFileName }, + body: { 'utxo-string': utxo.utxo, freeze: true }, + throwOnError: true, + }) + } catch (_ignoredOnPurpose: unknown) { + console.debug('Error while re-freezing fidelity bond UTXO in error handling.') + } + } + return undefined + } finally { + // refetch only if the sweep broadcast, and don't let a refetch error + // get treated as a sweep failure + if (sweepBroadcasted) { + try { + await walletInfoRefetch() + } catch (error: unknown) { + console.warn('Error while refetching wallet info after sweep.', error) + } + } } }, retry: false, From 8f9d8d59170586eb216d73fdf50c12c01f100b54 Mon Sep 17 00:00:00 2001 From: Aman Yadav Date: Mon, 10 Aug 2026 00:27:31 +0530 Subject: [PATCH 46/56] fix(validation): correctly handle zero block height in blockHeightField (#1414) --- src/lib/formValidation.test.ts | 42 ++++++++++++++++++++++++++++++++++ src/lib/formValidation.ts | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/lib/formValidation.test.ts b/src/lib/formValidation.test.ts index d77f283a..bb37dfd0 100644 --- a/src/lib/formValidation.test.ts +++ b/src/lib/formValidation.test.ts @@ -7,6 +7,8 @@ import { isReusedAddress, isValidAddress, sourceJarField, + blockHeightField, + INPUT_BLOCK_HEIGHT_MIN, } from './formValidation' const mainnetAddress = '1BitcoinEaterAddressDontSend8MUo1T' @@ -164,3 +166,43 @@ describe('destinationAddressField', () => { expect(() => reusedSchema.validateSync(mainnetAddress)).toThrow('reused') }) }) + +describe('blockHeightField', () => { + const invalidMsg = ({ min, max }: { min: number; max: number }) => `Invalid blockheight ${min}-${max}` + + it('handles currentBlockHeight = 0', () => { + const schema = blockHeightField({ currentBlockHeight: 0, messages: { invalid: invalidMsg } }) + // min should be 0, max should be 0 + expect(schema.isValidSync(0)).toBe(true) + expect(schema.isValidSync(1)).toBe(false) + }) + + it('handles currentBlockHeight = undefined', () => { + const schema = blockHeightField({ currentBlockHeight: undefined, messages: { invalid: invalidMsg } }) + // min should be 0, max should be Number.MAX_SAFE_INTEGER + expect(schema.isValidSync(0)).toBe(true) + expect(schema.isValidSync(100)).toBe(true) + expect(schema.isValidSync(Number.MAX_SAFE_INTEGER + 1)).toBe(false) + }) + + it('handles currentBlockHeight = null', () => { + // @ts-expect-error Intentionally pass null to test JS boundary cases + const schema = blockHeightField({ currentBlockHeight: null, messages: { invalid: invalidMsg } }) + expect(schema.isValidSync(0)).toBe(true) + expect(schema.isValidSync(100)).toBe(true) + }) + + it('handles currentBlockHeight > minBlockHeight', () => { + const schema = blockHeightField({ currentBlockHeight: 100, messages: { invalid: invalidMsg } }) + // min should be 0, max should be 100 + expect(schema.isValidSync(50)).toBe(true) + expect(schema.isValidSync(100)).toBe(true) + expect(schema.isValidSync(101)).toBe(false) + }) + + it('handles currentBlockHeight == minBlockHeight (0)', () => { + const schema = blockHeightField({ currentBlockHeight: INPUT_BLOCK_HEIGHT_MIN, messages: { invalid: invalidMsg } }) + expect(schema.isValidSync(INPUT_BLOCK_HEIGHT_MIN)).toBe(true) + expect(schema.isValidSync(INPUT_BLOCK_HEIGHT_MIN + 1)).toBe(false) + }) +}) diff --git a/src/lib/formValidation.ts b/src/lib/formValidation.ts index 5ec2e3a3..fee03563 100644 --- a/src/lib/formValidation.ts +++ b/src/lib/formValidation.ts @@ -121,7 +121,7 @@ export const blockHeightField = ({ } }) => { const minBlockHeight = Math.min(INPUT_BLOCK_HEIGHT_MIN, currentBlockHeight || INPUT_BLOCK_HEIGHT_MIN) - const maxBlockheight = Math.max(minBlockHeight, currentBlockHeight || INPUT_BLOCK_HEIGHT_MAX) + const maxBlockheight = Math.max(minBlockHeight, currentBlockHeight ?? INPUT_BLOCK_HEIGHT_MAX) const invalidBlockheightMessage = invalid({ min: minBlockHeight, max: maxBlockheight }) return yup From 6022ed3d41c20466afa9bccb1e27414ef2cb4e6e Mon Sep 17 00:00:00 2001 From: GuTS805 Date: Mon, 10 Aug 2026 00:35:49 +0530 Subject: [PATCH 47/56] fix(a11y): make balance visibility toggle keyboard-accessible (#1412) --- src/components/ui/jam/Balance.test.tsx | 48 +++++++++++++++++++ src/components/ui/jam/Balance.tsx | 64 ++++++++++++++++++-------- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/components/ui/jam/Balance.test.tsx b/src/components/ui/jam/Balance.test.tsx index 0915d358..49d0355a 100644 --- a/src/components/ui/jam/Balance.test.tsx +++ b/src/components/ui/jam/Balance.test.tsx @@ -5,6 +5,7 @@ import user from '@testing-library/user-event' import { describe, it, expect, vi } from 'vitest' import { Balance } from '@/components/ui/jam/Balance' import { JamDisplayContextProvider } from '@/context/JamDisplayContextProvider' +import '@/i18n/config' import { withRuntimeLocale } from '@/test/withRuntimeLocale' const render = (ui: React.ReactNode, options?: Omit) => { @@ -221,4 +222,51 @@ describe('', () => { expect(screen.getByTestId(`sats-amount`)).toBeInTheDocument() expect(screen.queryByText(`*****`)).not.toBeInTheDocument() }) + + it('should render the visibility toggle as a keyboard-focusable button with an accessible name', () => { + render() + + const toggleButton = screen.getByRole('button', { name: 'Show balance' }) + expect(toggleButton).toBeInTheDocument() + expect(toggleButton).toHaveAttribute('aria-pressed', 'true') + }) + + it('should update the accessible name and aria-pressed after toggling', async () => { + render() + + await user.click(screen.getByRole('button', { name: 'Show balance' })) + + const toggleButton = screen.getByRole('button', { name: 'Hide balance' }) + expect(toggleButton).toBeInTheDocument() + expect(toggleButton).toHaveAttribute('aria-pressed', 'false') + }) + + it('should toggle visibility via keyboard (Enter)', async () => { + render() + + await user.tab() + expect(screen.getByRole('button', { name: 'Show balance' })).toHaveFocus() + + await user.keyboard('{Enter}') + + expect(screen.getByTestId(`sats-amount`)).toBeInTheDocument() + expect(screen.queryByText(`*****`)).not.toBeInTheDocument() + }) + + it('should toggle visibility via keyboard (Space)', async () => { + render() + + await user.tab() + expect(screen.getByRole('button', { name: 'Show balance' })).toHaveFocus() + + await user.keyboard(' ') + + expect(screen.getByTestId(`sats-amount`)).toBeInTheDocument() + expect(screen.queryByText(`*****`)).not.toBeInTheDocument() + }) + + it('should not render a focusable button when the visibility toggle is disabled', () => { + render() + expect(screen.queryByRole('button')).not.toBeInTheDocument() + }) }) diff --git a/src/components/ui/jam/Balance.tsx b/src/components/ui/jam/Balance.tsx index 11740b9a..c61215cd 100644 --- a/src/components/ui/jam/Balance.tsx +++ b/src/components/ui/jam/Balance.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState, type MouseEvent, type MouseEventHandler, type PropsWithChildren } from 'react' import { SnowflakeIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' import { CurrencySymbol } from '@/components/ui/jam/CurrencySymbol' import { useJamDisplayContext } from '@/context/JamDisplayContext' import { cn, satsToBtc, tryBtcToSat, isValidNumber, getBtcParts, formatSats } from '@/lib/utils' @@ -23,6 +24,8 @@ type ElementWithSymbolsProps = PropsWithChildren<{ frozenSymbol?: boolean className?: string onClick?: MouseEventHandler + 'aria-label'?: string + 'aria-pressed'?: boolean }> const ElementWithSymbols = ({ @@ -33,23 +36,40 @@ const ElementWithSymbols = ({ className, children, onClick, + 'aria-label': ariaLabel, + 'aria-pressed': ariaPressed, }: ElementWithSymbolsProps) => { - return ( - + const sharedClassName = cn( + 'balance-hook inline-flex items-center', + { + 'text-brand-info': frozen, + }, + className, + ) + + const content = ( + <> {children} {showSymbol && symbol} {frozen && frozenSymbol && FROZEN_SYMBOL} - + ) + + if (onClick) { + return ( + + ) + } + + return {content} } const DECIMAL_POINT_CHAR = '.' @@ -156,6 +176,7 @@ export const BalanceComponent = ({ enableVisibilityToggle, ...props }: BalanceComponentProps) => { + const { t } = useTranslation() const [isBalanceVisible, setIsBalanceVisible] = useState(showBalance) const displayMode = useMemo(() => { return isBalanceVisible ? (convertToUnit ?? 'default') : 'hidden' @@ -173,18 +194,25 @@ export const BalanceComponent = ({ setIsBalanceVisible((current) => !current) } const onClickHandler = enableVisibilityToggle === false ? undefined : toggleVisibility + const isInteractive = Boolean(onClickHandler || props.onClick) return { ...props, className: cn(props.className, { - 'cursor-pointer': onClickHandler || props.onClick, + 'cursor-pointer': isInteractive, }), - onClick: (event: MouseEvent) => { - onClickHandler?.(event) - props.onClick?.(event) - }, + onClick: isInteractive + ? (event: MouseEvent) => { + onClickHandler?.(event) + props.onClick?.(event) + } + : undefined, + 'aria-label': onClickHandler + ? t(isBalanceVisible ? 'settings.hide_balance' : 'settings.show_balance') + : undefined, + 'aria-pressed': onClickHandler ? !isBalanceVisible : undefined, } - }, [props, enableVisibilityToggle]) + }, [props, enableVisibilityToggle, isBalanceVisible, t]) const element = useMemo(() => { if (displayMode === 'hidden') { From 1535a8148ef7b5897565f6f25019ac534bd9c2f1 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sun, 9 Aug 2026 22:58:11 +0200 Subject: [PATCH 48/56] chore(build): fix lint warnings in form validation test --- src/lib/formValidation.test.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lib/formValidation.test.ts b/src/lib/formValidation.test.ts index bb37dfd0..1c87f48f 100644 --- a/src/lib/formValidation.test.ts +++ b/src/lib/formValidation.test.ts @@ -168,18 +168,21 @@ describe('destinationAddressField', () => { }) describe('blockHeightField', () => { - const invalidMsg = ({ min, max }: { min: number; max: number }) => `Invalid blockheight ${min}-${max}` + // eslint-disable-next-line unicorn/consistent-function-scoping -- okay in tests + const invalidMessage = ({ min, max }: { min: number; max: number }) => `Invalid blockheight ${min}-${max}` it('handles currentBlockHeight = 0', () => { - const schema = blockHeightField({ currentBlockHeight: 0, messages: { invalid: invalidMsg } }) + const schema = blockHeightField({ currentBlockHeight: 0, messages: { invalid: invalidMessage } }) // min should be 0, max should be 0 + expect(schema.isValidSync(-1)).toBe(false) expect(schema.isValidSync(0)).toBe(true) expect(schema.isValidSync(1)).toBe(false) }) it('handles currentBlockHeight = undefined', () => { - const schema = blockHeightField({ currentBlockHeight: undefined, messages: { invalid: invalidMsg } }) + const schema = blockHeightField({ currentBlockHeight: undefined, messages: { invalid: invalidMessage } }) // min should be 0, max should be Number.MAX_SAFE_INTEGER + expect(schema.isValidSync(-1)).toBe(false) expect(schema.isValidSync(0)).toBe(true) expect(schema.isValidSync(100)).toBe(true) expect(schema.isValidSync(Number.MAX_SAFE_INTEGER + 1)).toBe(false) @@ -187,21 +190,28 @@ describe('blockHeightField', () => { it('handles currentBlockHeight = null', () => { // @ts-expect-error Intentionally pass null to test JS boundary cases - const schema = blockHeightField({ currentBlockHeight: null, messages: { invalid: invalidMsg } }) + const schema = blockHeightField({ currentBlockHeight: null, messages: { invalid: invalidMessage } }) + expect(schema.isValidSync(-1)).toBe(false) expect(schema.isValidSync(0)).toBe(true) expect(schema.isValidSync(100)).toBe(true) }) it('handles currentBlockHeight > minBlockHeight', () => { - const schema = blockHeightField({ currentBlockHeight: 100, messages: { invalid: invalidMsg } }) + const schema = blockHeightField({ currentBlockHeight: 100, messages: { invalid: invalidMessage } }) // min should be 0, max should be 100 + expect(schema.isValidSync(-1)).toBe(false) + expect(schema.isValidSync(0)).toBe(true) expect(schema.isValidSync(50)).toBe(true) expect(schema.isValidSync(100)).toBe(true) expect(schema.isValidSync(101)).toBe(false) }) it('handles currentBlockHeight == minBlockHeight (0)', () => { - const schema = blockHeightField({ currentBlockHeight: INPUT_BLOCK_HEIGHT_MIN, messages: { invalid: invalidMsg } }) + const schema = blockHeightField({ + currentBlockHeight: INPUT_BLOCK_HEIGHT_MIN, + messages: { invalid: invalidMessage }, + }) + expect(schema.isValidSync(INPUT_BLOCK_HEIGHT_MIN - 1)).toBe(false) expect(schema.isValidSync(INPUT_BLOCK_HEIGHT_MIN)).toBe(true) expect(schema.isValidSync(INPUT_BLOCK_HEIGHT_MIN + 1)).toBe(false) }) From c832b80a2fe22cb7ebfb48bcb89bef25ffb410c8 Mon Sep 17 00:00:00 2001 From: Parth <143504541+parrth20@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:34:39 +0530 Subject: [PATCH 49/56] feat(earn): show own offer status in local orderbook (#1420) --- src/components/earn/EarnPage.test.tsx | 84 +++++++++++++- src/components/earn/EarnPage.tsx | 38 +++++++ src/components/earn/OfferCard.test.tsx | 38 +++++++ src/components/earn/OfferCard.tsx | 95 +++++++++++++++- src/constants/jam.ts | 9 ++ src/i18n/locales/en/translation.json | 9 +- src/lib/api/orderbook.ts | 17 ++- src/stories/jam/OfferCard.stories.tsx | 151 +++++++++++++++++++++++++ 8 files changed, 426 insertions(+), 15 deletions(-) create mode 100644 src/stories/jam/OfferCard.stories.tsx diff --git a/src/components/earn/EarnPage.test.tsx b/src/components/earn/EarnPage.test.tsx index 2c4dcaa7..fe6c0954 100644 --- a/src/components/earn/EarnPage.test.tsx +++ b/src/components/earn/EarnPage.test.tsx @@ -2,6 +2,7 @@ import type React from 'react' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' +import * as JAM from '@/constants/jam' import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos' import { jmSessionStore } from '@/store/jmSessionStore' import type { EarnFormValues } from './EarnForm' @@ -10,6 +11,12 @@ import { EarnPage } from './EarnPage' const mocks = vi.hoisted(() => ({ developerMode: false, feeConfigMissing: false, + orderbookData: vi.fn<() => unknown>(), + orderbookQueryOptions: vi.fn(), + orderbookQueryState: { + isError: false, + isLoading: false, + }, scrollToTop: vi.fn(), startMaker: vi.fn(), startMutationState: { @@ -76,9 +83,13 @@ vi.mock('@tanstack/react-query', () => ({ reset: vi.fn(), } }), - useQuery: vi.fn(() => ({ - refetch: mocks.stopMakerRefetch, - })), + useQuery: vi.fn((options: { queryKey?: unknown[] }) => { + if (options.queryKey?.[0] === 'orderbook') { + mocks.orderbookQueryOptions(options) + return { ...mocks.orderbookQueryState, data: mocks.orderbookData() } + } + return { refetch: mocks.stopMakerRefetch } + }), })) vi.mock('react-i18next', () => ({ @@ -194,9 +205,24 @@ vi.mock('./MoveToJarDialog', () => ({ })) vi.mock('./OfferCard', () => ({ - OfferCard: ({ children, nickname }: { children?: React.ReactNode; nickname?: string }) => ( + OfferCard: ({ + children, + nickname, + orderbookStatus, + orderbookOffer, + fidelityBond, + }: { + children?: React.ReactNode + nickname?: string + orderbookStatus?: string + orderbookOffer?: { fidelity_bond_value?: number } + fidelityBond?: { amount?: number } + }) => (
offer-card:{nickname} + orderbook-status:{orderbookStatus} + bond-value:{orderbookOffer?.fidelity_bond_value} + bond-amount:{fidelityBond?.amount} {children}
), @@ -251,6 +277,11 @@ describe('EarnPage', () => { beforeEach(() => { mocks.developerMode = false mocks.feeConfigMissing = false + mocks.orderbookData.mockReset() + mocks.orderbookData.mockReturnValue(undefined) + mocks.orderbookQueryOptions.mockReset() + mocks.orderbookQueryState.isError = false + mocks.orderbookQueryState.isLoading = false mocks.scrollToTop.mockReset() mocks.startMaker.mockReset() mocks.startMaker.mockResolvedValue({}) @@ -342,6 +373,51 @@ describe('EarnPage', () => { expect(mocks.stopMakerRefetch).toHaveBeenCalledWith({ throwOnError: true }) }) + it('shows the current offer and fidelity bond from the local orderbook', () => { + setSession({ + maker_running: true, + offer_list: [{ oid: 7, cjfee: '250', minsize: '5000', ordertype: 'sw0absoffer' }], + }) + mocks.orderbookData.mockReturnValue({ + offers: [{ counterparty: 'maker-a', oid: 7, fidelity_bond_value: 42_000 }], + fidelitybonds: [ + { counterparty: 'maker-a', amount: 50_000, locktime: 1_700_000_000 }, + { counterparty: 'maker-a', amount: 100_000, locktime: 1_800_000_000 }, + ], + }) + + render() + + expect(screen.getByText('orderbook-status:visible')).toBeInTheDocument() + expect(screen.getByText('bond-value:42000')).toBeInTheDocument() + expect(screen.getByText('bond-amount:100000')).toBeInTheDocument() + }) + + it('polls until the current offer is visible, then slows down', () => { + setSession({ + maker_running: true, + offer_list: [{ oid: 7, cjfee: '250', minsize: '5000', ordertype: 'sw0absoffer' }], + }) + + render() + + const { refetchInterval } = mocks.orderbookQueryOptions.mock.calls[0][0] as { + refetchInterval: (query: { + state: { + data?: { offers: Array<{ counterparty: string; oid: number }> } + error?: Error | null + } + }) => number + } + const visibleData = { offers: [{ counterparty: 'maker-a', oid: 7 }] } + + expect(refetchInterval({ state: {} })).toBe(JAM.WAIT_FOR_UPDATE_ORDERBOOK_POLLING_INTERVAL) + expect(refetchInterval({ state: { data: visibleData } })).toBe(JAM.VISIBLE_ORDERBOOK_POLLING_INTERVAL) + expect(refetchInterval({ state: { data: visibleData, error: new Error('offline') } })).toBe( + JAM.WAIT_FOR_UPDATE_ORDERBOOK_POLLING_INTERVAL, + ) + }) + it('shows waiting states while maker updates', () => { mocks.startMutationState.isSuccess = true diff --git a/src/components/earn/EarnPage.tsx b/src/components/earn/EarnPage.tsx index 27815ec4..6f9b2ab9 100644 --- a/src/components/earn/EarnPage.tsx +++ b/src/components/earn/EarnPage.tsx @@ -22,6 +22,7 @@ import { useApiClient } from '@/hooks/useApiClient' import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation' import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos' import { useRefreshSession } from '@/hooks/useRefreshSession' +import * as OrderbookApi from '@/lib/api/orderbook' import { getErrorReason } from '@/lib/errorReason' import * as fb from '@/lib/fidelityBondUtils' import { withQueryDelay } from '@/lib/queryClient' @@ -80,6 +81,40 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { const [showFeeConfigDialog, setShowFeeConfigDialog] = useState(false) const isCurrentOfferAvailable = jmSession?.offer_list && jmSession.offer_list.length > 0 + const currentOffer = jmSession?.offer_list?.[0] + const isCurrentOrderbookOffer = (offer: OrderbookApi.OrderbookOffer) => + offer.counterparty === jmSession?.nickname && String(offer.oid) === String(currentOffer?.oid) + + const { + data: orderbookData, + isFetching: orderbookIsFetching, + isError: orderbookIsError, + } = useQuery({ + queryKey: ['orderbook'], + queryFn: withQueryDelay(OrderbookApi.fetchOrderbook, { + // avoid flickering and let user briefly know that something is happening in the background + delayBefore: 1_000, + }), + enabled: makerRunning && !!currentOffer && !!jmSession?.nickname, + staleTime: Number.POSITIVE_INFINITY, + refetchInterval: (query) => + query.state.error || !query.state.data?.offers.some((offer) => isCurrentOrderbookOffer(offer)) + ? JAM.WAIT_FOR_UPDATE_ORDERBOOK_POLLING_INTERVAL + : JAM.VISIBLE_ORDERBOOK_POLLING_INTERVAL, + }) + const currentOrderbookOffer = orderbookData?.offers.find((offer) => isCurrentOrderbookOffer(offer)) + const currentOrderbookFidelityBond = + Number(currentOrderbookOffer?.fidelity_bond_value) > 0 + ? orderbookData?.fidelitybonds?.findLast((bond) => bond.counterparty === jmSession?.nickname) + : undefined + const orderbookStatus = + !jmSession?.nickname || orderbookIsFetching + ? 'checking' + : orderbookIsError + ? 'error' + : currentOrderbookOffer + ? 'visible' + : 'missing' const stopMakerQueryOptions = stopmakerOptions({ client, @@ -261,6 +296,9 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { className="motion-safe:animate-in blur-in" value={jmSession.offer_list[0]} nickname={jmSession.nickname} + orderbookStatus={orderbookStatus} + orderbookOffer={currentOrderbookOffer} + fidelityBond={currentOrderbookFidelityBond} >
{!!value?.txfee && ( -
-
+
+ +
@@ -109,6 +155,45 @@ export function OfferCard({ className, value, nickname, children }: PropsWithChi
)} + {fidelityBond !== undefined && ( +
+ 0, + 'text-brand-warning': bondWarning, + })} + /> +
+ +
+
+ + {t('earn.current.text_bond_value')}: {Math.floor(bondValue).toLocaleString()} + +
+
+ + + {t('earn.current.text_bond_locktime', { + date: new Date(fidelityBond.locktime * 1_000).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + }), + })} + +
+
+
+
+ )} {children} diff --git a/src/constants/jam.ts b/src/constants/jam.ts index b8b86242..13d62497 100644 --- a/src/constants/jam.ts +++ b/src/constants/jam.ts @@ -87,6 +87,15 @@ export const WAIT_FOR_UPDATE_SESSION_POLLING_DELAY: Milliseconds = Math.max( 1, ) +export const WAIT_FOR_UPDATE_ORDERBOOK_POLLING_INTERVAL: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_WAIT_FOR_UPDATE_ORDERBOOK_POLLING_INTERVAL, 15_000), + 1_000, +) +export const VISIBLE_ORDERBOOK_POLLING_INTERVAL: Milliseconds = Math.max( + parseAsIntOrDefault(import.meta.env.VITE_JAM_VISIBLE_ORDERBOOK_POLLING_INTERVAL, 120_000), + 1_000, +) + export const RUNNING_COINJOIN_POLLING_INTERVAL: Milliseconds = Math.max( parseAsIntOrDefault(import.meta.env.VITE_JAM_RUNNING_COINJOIN_POLLING_INTERVAL, 5_000), 1_000, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 4f954a33..caa6a61b 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -572,7 +572,14 @@ "text_txfee": "Transaction Fee", "text_offer_id": "Offer ID", "text_offer_type_absolute": "absolute", - "text_offer_type_relative": "relative" + "text_offer_type_relative": "relative", + "text_orderbook_checking": "Checking local orderbook...", + "text_orderbook_visible": "Visible in local orderbook", + "text_orderbook_missing": "Not found in local orderbook yet", + "text_orderbook_error": "Could not check local orderbook", + "text_fidelity_bond": "Fidelity Bond", + "text_bond_value": "Bond value", + "text_bond_locktime": "locked until {{date}}" }, "report": { "title": "Earnings Report", diff --git a/src/lib/api/orderbook.ts b/src/lib/api/orderbook.ts index 5fe18b8d..388f6108 100644 --- a/src/lib/api/orderbook.ts +++ b/src/lib/api/orderbook.ts @@ -5,11 +5,18 @@ export interface OrderbookOffer { counterparty: string oid: number ordertype: OfferType - minsize: AmountSats | null | undefined - maxsize: AmountSats | null | undefined - txfee: number | null | undefined - cjfee: number | null | undefined - fidelity_bond_value: number | null | undefined + minsize?: AmountSats | null + maxsize?: AmountSats | null + txfee?: number | null + cjfee?: number | null + fidelity_bond_value?: number | null + fidelity_bond_verification_stale?: boolean | null + directory_nodes?: string[] | null + directly_reachable?: boolean | null + // TODO: many more fields, e.g.: + // fidelity_bond_value + // fidelity_bond_verified + // features { neutrino_compat: true, nick_auth: true, peerlist_features: true, … } } export interface OrderbookFidelityBond { diff --git a/src/stories/jam/OfferCard.stories.tsx b/src/stories/jam/OfferCard.stories.tsx new file mode 100644 index 00000000..b89f48c5 --- /dev/null +++ b/src/stories/jam/OfferCard.stories.tsx @@ -0,0 +1,151 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { OfferCard } from '@/components/earn/OfferCard' + +const meta: Meta = { + title: 'Jam/OfferCard', + component: OfferCard, + tags: ['autodocs'], +} +export default meta + +type Story = StoryObj + +export const Absolute: Story = { + args: { + value: { + oid: 0, + ordertype: 'sw0absoffer', + minsize: 90_463, + maxsize: 9_008_744_958, + txfee: 0, + cjfee: '21', + }, + nickname: 'TEST7i74jfC9M8MV', + }, +} + +export const Relative: Story = { + args: { + value: { + oid: 0, + ordertype: 'sw0reloffer', + minsize: 90_463, + maxsize: 9_008_744_958, + txfee: 0, + cjfee: '0.000021', + }, + nickname: 'TESTCKqXnDjxnFHN', + }, +} + +export const Checking: Story = { + args: { + value: { + oid: 0, + ordertype: 'sw0reloffer', + minsize: 90_463, + maxsize: 9_008_744_958, + txfee: 0, + cjfee: '0.000021', + }, + nickname: 'TESTCKqXnDjxnFHN', + orderbookStatus: 'checking', + }, +} + +export const Visible: Story = { + args: { + value: { + oid: 0, + ordertype: 'sw0reloffer', + minsize: 90_463, + maxsize: 9_008_744_958, + txfee: 0, + cjfee: '0.000021', + }, + nickname: 'TESTCKqXnDjxnFHN', + orderbookStatus: 'visible', + }, +} + +export const Missing: Story = { + args: { + value: { + oid: 0, + ordertype: 'sw0reloffer', + minsize: 90_463, + maxsize: 9_008_744_958, + txfee: 0, + cjfee: '0.000021', + }, + nickname: 'TESTCKqXnDjxnFHN', + orderbookStatus: 'missing', + }, +} + +export const Error: Story = { + args: { + value: { + oid: 0, + ordertype: 'sw0reloffer', + minsize: 90_463, + maxsize: 9_008_744_958, + txfee: 0, + cjfee: '0.000021', + }, + nickname: 'TESTCKqXnDjxnFHN', + orderbookStatus: 'error', + }, +} + +export const RelativeWithFidelityBondNotYetInLocalOrderbook: Story = { + args: { + value: { + oid: 0, + ordertype: 'sw0reloffer', + minsize: 90_463, + maxsize: 9_008_744_958, + txfee: 0, + cjfee: '0.000021', + }, + nickname: 'TESTCKqXnDjxnFHN', + orderbookStatus: 'visible', + orderbookOffer: { + counterparty: 'TESTCKqXnDjxnFHN', + oid: 0, + ordertype: 'sw0reloffer', + fidelity_bond_value: 0, + }, + fidelityBond: { + counterparty: 'TESTCKqXnDjxnFHN', + amount: 123_456_789, + locktime: 1_000_000, + }, + }, +} + +export const RelativeWithFidelityBondInLocalOrderbook: Story = { + args: { + value: { + oid: 0, + ordertype: 'sw0reloffer', + minsize: 90_463, + maxsize: 9_008_744_958, + txfee: 0, + cjfee: '0.000021', + }, + nickname: 'TESTCKqXnDjxnFHN', + orderbookStatus: 'visible', + orderbookOffer: { + counterparty: 'TESTCKqXnDjxnFHN', + oid: 0, + ordertype: 'sw0reloffer', + fidelity_bond_value: 123_456_789.1337, + }, + fidelityBond: { + counterparty: 'TESTCKqXnDjxnFHN', + amount: 123_456_789, + locktime: 0, + }, + }, +} From 04c62c12fd71b0c915e38a1bb9efc4335ac8f523 Mon Sep 17 00:00:00 2001 From: Parth <143504541+parrth20@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:23:42 +0530 Subject: [PATCH 50/56] fix(fb): explain missing eligible UTXOs (#1408) --- .../useCreateFidelityBondWizard.ts | 20 ++---- src/components/earn/EarnPage.test.tsx | 66 +++++++++++++++---- src/components/earn/EarnPage.tsx | 29 ++++++-- src/components/sweep/SweepPage.test.tsx | 1 + src/context/JamWalletInfoContext.ts | 1 + .../JamWalletInfoContextProvider.test.tsx | 18 ++++- src/context/JamWalletInfoContextProvider.tsx | 4 ++ src/i18n/locales/en/translation.json | 2 + src/lib/fidelityBondUtils.test.ts | 13 ++++ src/lib/fidelityBondUtils.ts | 15 ++++- 10 files changed, 135 insertions(+), 34 deletions(-) diff --git a/src/components/earn/CreateFidelityBondDialog/useCreateFidelityBondWizard.ts b/src/components/earn/CreateFidelityBondDialog/useCreateFidelityBondWizard.ts index 8170b344..b19b0f49 100644 --- a/src/components/earn/CreateFidelityBondDialog/useCreateFidelityBondWizard.ts +++ b/src/components/earn/CreateFidelityBondDialog/useCreateFidelityBondWizard.ts @@ -110,10 +110,8 @@ export function useCreateFidelityBondWizard( const jarsWithUtxos = useMemo(() => { return walletInfo.jars.filter((jar) => { - // TODO: let's allow selecting frozen utxos - unfreeze them before sending the transaction - const availableUtxos = jar.utxos.filter( - (utxo) => - !utxo.frozen && !fb.utxo.isFidelityBond(utxo) && walletInfo.addressSummary[utxo.address]?.status === 'cj-out', + const availableUtxos = jar.utxos.filter((utxo) => + fb.utxo.isEligibleForCreation(utxo, walletInfo.addressSummary[utxo.address]?.status), ) return availableUtxos.length > 0 }) @@ -121,17 +119,9 @@ export function useCreateFidelityBondWizard( const availableUtxos = useMemo(() => { if (selectedJar === undefined) return [] - return ( - selectedJar.utxos - // TODO: let's allow selecting frozen utxos - unfreeze them before sending the transaction - .filter( - (utxo) => - !utxo.frozen && - !fb.utxo.isFidelityBond(utxo) && - walletInfo.addressSummary[utxo.address]?.status === 'cj-out', - ) - .toSorted((a, b) => b.value - a.value) - ) + return selectedJar.utxos + .filter((utxo) => fb.utxo.isEligibleForCreation(utxo, walletInfo.addressSummary[utxo.address]?.status)) + .toSorted((a, b) => b.value - a.value) }, [selectedJar, walletInfo.addressSummary]) const selectedUtxos = useMemo(() => { diff --git a/src/components/earn/EarnPage.test.tsx b/src/components/earn/EarnPage.test.tsx index fe6c0954..fc554c94 100644 --- a/src/components/earn/EarnPage.test.tsx +++ b/src/components/earn/EarnPage.test.tsx @@ -3,7 +3,8 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import * as JAM from '@/constants/jam' -import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos' +import type { Jar } from '@/context/JamWalletInfoContext' +import type { FidelityBondUtxo, Utxo } from '@/hooks/useQueryUtxos' import { jmSessionStore } from '@/store/jmSessionStore' import type { EarnFormValues } from './EarnForm' import { EarnPage } from './EarnPage' @@ -33,17 +34,12 @@ const mocks = vi.hoisted(() => ({ toastInfo: vi.fn(), toastSuccess: vi.fn(), walletInfo: { + addressSummary: {}, fidelityBondSummary: { fbOutputs: [] as FidelityBondUtxo[] }, + hasEligibleFidelityBondUtxo: true, isFetching: false, isLoading: false, - jars: [] as Array<{ - balanceSummary: { - calculatedAvailableBalanceInSats: number - calculatedConfirmedAvailableBalanceInSats: number - calculatedFrozenOrLockedBalanceInSats: number - calculatedTotalBalanceInSats: number - } - }>, + jars: [] as Jar[], maxJarAvailableBalance: 100_000_000, }, })) @@ -238,11 +234,35 @@ vi.mock('./report/EarnReportOverlay', () => ({ const balanceSummary = { calculatedAvailableBalanceInSats: 100_000_000, + calculatedAvailableFrozenBalanceInSats: 0, calculatedConfirmedAvailableBalanceInSats: 100_000_000, calculatedFrozenOrLockedBalanceInSats: 0, calculatedTotalBalanceInSats: 100_000_000, } +const eligibleUtxo: Utxo = { + address: 'bcrt1qcj', + confirmations: 12, + external: false, + frozen: false, + label: '', + locktime: undefined, + mixdepth: 0, + path: "m/84'/1'/0'/0/0", + tries: 3, + tries_remaining: 3, + utxo: 'eligible:0', + value: 100_000, +} + +const makeJar = (balanceOverrides = {}): Jar => ({ + balanceSummary: { ...balanceSummary, ...balanceOverrides }, + color: '#e2b86a', + jarIndex: 0, + name: 'Jar 0', + utxos: [eligibleUtxo], +}) + const expiredBond: FidelityBondUtxo = { address: 'bc1qbond', confirmations: 12, @@ -295,10 +315,14 @@ describe('EarnPage', () => { mocks.toastError.mockReset() mocks.toastInfo.mockReset() mocks.toastSuccess.mockReset() + mocks.walletInfo.addressSummary = { + [eligibleUtxo.address]: { status: 'cj-out' }, + } mocks.walletInfo.fidelityBondSummary = { fbOutputs: [] } + mocks.walletInfo.hasEligibleFidelityBondUtxo = true mocks.walletInfo.isFetching = false mocks.walletInfo.isLoading = false - mocks.walletInfo.jars = [{ balanceSummary }] + mocks.walletInfo.jars = [makeJar()] mocks.walletInfo.maxJarAvailableBalance = 100_000_000 setSession() }) @@ -453,6 +477,24 @@ describe('EarnPage', () => { expect(screen.getByText('create-bond-dialog:true')).toBeInTheDocument() }) + it('explains when no UTXO is eligible for fidelity-bond creation', async () => { + const user = userEvent.setup() + mocks.walletInfo.hasEligibleFidelityBondUtxo = false + + render() + + expect(screen.getByText('earn.fidelity_bond.create_form.alert_no_eligible_utxos_title')).toBeInTheDocument() + expect(screen.getByText('earn.fidelity_bond.create_form.alert_no_eligible_utxos_description')).toBeInTheDocument() + + const createFidelityBondButton = screen.getByRole('button', { + name: 'earn.fidelity_bond.create_form.button_create', + }) + expect(createFidelityBondButton).toBeDisabled() + + await user.click(createFidelityBondButton) + expect(screen.getByText('create-bond-dialog:false')).toBeInTheDocument() + }) + it('disables creating a fidelity bond while rescanning', async () => { const user = userEvent.setup() setSession({ @@ -561,7 +603,7 @@ describe('EarnPage', () => { }) it('warns when the spendable balance is only unconfirmed', () => { - mocks.walletInfo.jars = [{ balanceSummary: { ...balanceSummary, calculatedConfirmedAvailableBalanceInSats: 0 } }] + mocks.walletInfo.jars = [makeJar({ calculatedConfirmedAvailableBalanceInSats: 0 })] mocks.walletInfo.maxJarAvailableBalance = 100_000_000 render() @@ -573,7 +615,7 @@ describe('EarnPage', () => { }) it('warns when there is no spendable balance at all', () => { - mocks.walletInfo.jars = [{ balanceSummary: { ...balanceSummary, calculatedConfirmedAvailableBalanceInSats: 0 } }] + mocks.walletInfo.jars = [makeJar({ calculatedConfirmedAvailableBalanceInSats: 0 })] mocks.walletInfo.maxJarAvailableBalance = 0 render() diff --git a/src/components/earn/EarnPage.tsx b/src/components/earn/EarnPage.tsx index 6f9b2ab9..40dc876b 100644 --- a/src/components/earn/EarnPage.tsx +++ b/src/components/earn/EarnPage.tsx @@ -2,14 +2,15 @@ import { useEffect, useMemo, useState } from 'react' import { startmakerMutation, stopmakerOptions } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query' import type { StartMakerRequest } from '@joinmarket-webui/joinmarket-ng-api-ts/jm' import { useMutation, useQuery } from '@tanstack/react-query' -import { FileTextIcon, HourglassIcon, PlusIcon, RefreshCwIcon, ShuffleIcon, UnlockIcon } from 'lucide-react' +import { FileTextIcon, HourglassIcon, InfoIcon, PlusIcon, RefreshCwIcon, ShuffleIcon, UnlockIcon } from 'lucide-react' import type { SubmitHandler } from 'react-hook-form' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' import { toast } from 'sonner' import { useStore } from 'zustand' import { DevBadge } from '@/components/dev/DevBadge' import { FeeConfigDialog } from '@/components/settings/fees/FeeConfigDialog' -import { Alert, AlertTitle } from '@/components/ui/alert' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { FeeConfigErrorAlert } from '@/components/ui/jam/FeeConfigErrorAlert' @@ -17,6 +18,7 @@ import { PageLoading } from '@/components/ui/jam/PageLoading' import PageTitle from '@/components/ui/jam/PageTitle' import { isDevMode } from '@/constants/debugFeatures' import * as JAM from '@/constants/jam' +import { routes } from '@/constants/routes' import { useJamWalletInfoContext } from '@/context/JamWalletInfoContext' import { useApiClient } from '@/hooks/useApiClient' import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation' @@ -174,7 +176,9 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { !walletInfo.isFetching const isCreateFidelityBondEnabled = - isFidelityBondActionsEnabled && (!hasFidelityBond || JAM_EARN_CREATE_MULTIPLE_FIDELITY_BONDS_ENABLED) + isFidelityBondActionsEnabled && + walletInfo.hasEligibleFidelityBondUtxo && + (!hasFidelityBond || JAM_EARN_CREATE_MULTIPLE_FIDELITY_BONDS_ENABLED) const showCreateAdditionalFidelityBond = JAM_EARN_CREATE_MULTIPLE_FIDELITY_BONDS_ENABLED const numberOfNonFrozenFidelityBondOutputs = !hasFidelityBond @@ -362,6 +366,23 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { {t('earn.fidelity_bond.subtitle')} + {!walletInfo.hasEligibleFidelityBondUtxo && ( + + + + {t('earn.fidelity_bond.create_form.alert_no_eligible_utxos_title')} + + , + '3': , + }} + /> + + + + )}
diff --git a/src/components/send/SendPage.test.tsx b/src/components/send/SendPage.test.tsx index f1441067..10cc9b8e 100644 --- a/src/components/send/SendPage.test.tsx +++ b/src/components/send/SendPage.test.tsx @@ -34,6 +34,9 @@ const mocks = vi.hoisted(() => ({ walletInfoIsFetching: false, walletInfoIsLoading: false, waitForUtxosToBeSpent: [] as string[], + hasOrders: true, + orderbookIsLoading: false, + orderbookError: null as Error | null, })) vi.mock('@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query', () => ({ @@ -73,11 +76,23 @@ vi.mock('@tanstack/react-query', () => ({ })) vi.mock('react-i18next', () => ({ + Trans: ({ i18nKey, values }: { i18nKey: string; values?: unknown }) => + values ? `${i18nKey}:${JSON.stringify(values)}` : i18nKey, useTranslation: () => ({ t: (key: string, options?: Record) => (options ? `${key}:${JSON.stringify(options)}` : key), }), })) +vi.mock('@/hooks/useQueryOrderbook', () => ({ + useQueryOrderbook: () => ({ + hasOrders: mocks.hasOrders, + queryResult: { + isLoading: mocks.orderbookIsLoading, + error: mocks.orderbookError, + }, + }), +})) + vi.mock('sonner', () => ({ toast: { error: mocks.toastError, @@ -331,6 +346,9 @@ describe('SendPage', () => { mocks.walletInfoIsFetching = false mocks.walletInfoIsLoading = false mocks.waitForUtxosToBeSpent = [] + mocks.hasOrders = true + mocks.orderbookIsLoading = false + mocks.orderbookError = null jmSessionStore.setState({ state: { coinjoin_in_process: false, diff --git a/src/components/sweep/SweepPage.test.tsx b/src/components/sweep/SweepPage.test.tsx index ceb074d3..16700047 100644 --- a/src/components/sweep/SweepPage.test.tsx +++ b/src/components/sweep/SweepPage.test.tsx @@ -107,6 +107,19 @@ const mocks = vi.hoisted(() => ({ stopState: { isPending: false, isSuccess: false }, toastError: vi.fn(), walletInfo: undefined as WalletInfo | undefined, + hasOrders: true, + orderbookIsLoading: false, + orderbookError: false, +})) + +vi.mock('@/hooks/useQueryOrderbook', () => ({ + useQueryOrderbook: () => ({ + hasOrders: mocks.hasOrders, + queryResult: { + isLoading: mocks.orderbookIsLoading, + isError: mocks.orderbookError, + }, + }), })) vi.mock('@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query', () => ({ @@ -404,6 +417,9 @@ describe('SweepPage', async () => { mocks.stopState = { isPending: false, isSuccess: false } mocks.toastError.mockReset() mocks.walletInfo = makeWalletInfo() + mocks.hasOrders = true + mocks.orderbookIsLoading = false + mocks.orderbookError = false setSession() vi.clearAllMocks() @@ -722,4 +738,27 @@ describe('SweepPage', async () => { await flushActUpdates() }) }) + + describe('Orderbook Precheck', () => { + it('shows empty orderbook warning when orderbook has no offers', () => { + mocks.hasOrders = false + + render() + + expect(screen.getByText('orderbook.alert_precheck_empty_title')).toBeInTheDocument() + }) + + it('does not show empty orderbook warning when orderbook is loading or has error', () => { + mocks.hasOrders = false + mocks.orderbookIsLoading = true + + const { rerender } = render() + expect(screen.queryByText('orderbook.alert_precheck_empty_title')).not.toBeInTheDocument() + + mocks.orderbookIsLoading = false + mocks.orderbookError = true + rerender() + expect(screen.queryByText('orderbook.alert_precheck_empty_title')).not.toBeInTheDocument() + }) + }) }) diff --git a/src/components/sweep/SweepPage.tsx b/src/components/sweep/SweepPage.tsx index 303d4542..bbc95e4c 100644 --- a/src/components/sweep/SweepPage.tsx +++ b/src/components/sweep/SweepPage.tsx @@ -23,6 +23,7 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { Balance } from '@/components/ui/jam/Balance' import { FeeConfigErrorAlert } from '@/components/ui/jam/FeeConfigErrorAlert' +import { OrderbookEmptyAlert } from '@/components/ui/jam/OrderbookEmptyAlert' import { PageLoading } from '@/components/ui/jam/PageLoading' import PageTitle from '@/components/ui/jam/PageTitle' import * as JAM from '@/constants/jam' @@ -31,6 +32,7 @@ import { useJamSessionInfoContext } from '@/context/JamSessionInfoContext' import { useDetectNetwork, useJamWalletInfoContext } from '@/context/JamWalletInfoContext' import { useApiClient } from '@/hooks/useApiClient' import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation' +import { useQueryOrderbook } from '@/hooks/useQueryOrderbook' import { useRefreshSession } from '@/hooks/useRefreshSession' import { getErrorReason } from '@/lib/errorReason' import { cn, scrollToTop, type WalletFileName } from '@/lib/utils' @@ -72,6 +74,15 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { const [showFeeConfigDialog, setShowFeeConfigDialog] = useState(false) const [showScheduleConfirmDialog, setShowScheduleConfirmDialog] = useState() const [alertMessage, setAlertMessage] = useState() + const { + hasOrders, + queryResult: { + isLoading: orderbookCheckIsLoading, + isError: orderbookCheckIsError, + isFetching: orderbookCheckIsFetching, + refetch: orderbookRefetch, + }, + } = useQueryOrderbook() const feeConfigValidation = useFeeConfigValidation({ walletFileName }) @@ -284,6 +295,10 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => { setShowFeeConfigDialog(true)} className="mb-4" /> )} + {!orderbookCheckIsLoading && !orderbookCheckIsError && !hasOrders && ( + + )} + {alertMessage && ( {t('global.error')} diff --git a/src/components/ui/jam/OrderbookEmptyAlert.test.tsx b/src/components/ui/jam/OrderbookEmptyAlert.test.tsx new file mode 100644 index 00000000..aae12d42 --- /dev/null +++ b/src/components/ui/jam/OrderbookEmptyAlert.test.tsx @@ -0,0 +1,46 @@ +import type { ReactNode } from 'react' +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { OrderbookEmptyAlert } from './OrderbookEmptyAlert' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + Trans: ({ i18nKey, components }: { i18nKey: string; components?: Record }) => ( + + {i18nKey} + {components?.['1']} + + ), +})) + +vi.mock('react-router-dom', () => ({ + Link: ({ children, to }: { children?: ReactNode; to: string }) => {children}, +})) + +describe('OrderbookEmptyAlert', () => { + it('renders title, description, button and link to orderbook page', () => { + render( {}} />) + + expect(screen.getByText('orderbook.alert_precheck_empty_title')).toBeInTheDocument() + expect(screen.getByText(/orderbook.alert_precheck_empty_description/u)).toBeInTheDocument() + expect(screen.getByRole('link')).toHaveAttribute('href', '/orderbook') + + const actionCheck = screen.getByRole('button', { name: 'orderbook.alert_precheck_empty_text_button_check' }) + expect(actionCheck).toBeEnabled() + + const actionChecking = screen.queryByRole('button', { name: 'orderbook.alert_precheck_empty_text_button_checking' }) + expect(actionChecking).not.toBeInTheDocument() + }) + + it('disables button while checking', () => { + render( {}} />) + + const actionChecking = screen.getByRole('button', { name: 'orderbook.alert_precheck_empty_text_button_checking' }) + expect(actionChecking).toBeDisabled() + + const actionCheck = screen.queryByRole('button', { name: 'orderbook.alert_precheck_empty_text_button_check' }) + expect(actionCheck).not.toBeInTheDocument() + }) +}) diff --git a/src/components/ui/jam/OrderbookEmptyAlert.tsx b/src/components/ui/jam/OrderbookEmptyAlert.tsx new file mode 100644 index 00000000..95de8d79 --- /dev/null +++ b/src/components/ui/jam/OrderbookEmptyAlert.tsx @@ -0,0 +1,43 @@ +import { AlertTriangleIcon, RefreshCwIcon } from 'lucide-react' +import { Trans, useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { routes } from '@/constants/routes' +import { Button } from '../button' + +interface OrderbookEmptyAlertProps { + className?: string + isChecking?: boolean + onCheckClick?: () => Promise +} + +export const OrderbookEmptyAlert = ({ className, isChecking, onCheckClick }: OrderbookEmptyAlertProps) => { + const { t } = useTranslation() + + return ( + + + {t('orderbook.alert_precheck_empty_title')} + +

+ , + }} + /> +

+
+ {onCheckClick && ( + + )} +
+
+
+ ) +} diff --git a/src/hooks/useQueryOrderbook.test.ts b/src/hooks/useQueryOrderbook.test.ts new file mode 100644 index 00000000..3314324b --- /dev/null +++ b/src/hooks/useQueryOrderbook.test.ts @@ -0,0 +1,47 @@ +import { renderHook } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { useQueryOrderbook } from './useQueryOrderbook' + +const queryMock = vi.fn<(options: unknown) => unknown>() + +vi.mock('@/lib/queryClient', () => ({ + withQueryDelay: (queryFn: unknown) => queryFn, +})) + +vi.mock('@tanstack/react-query', () => ({ + useQuery: (options: unknown): unknown => queryMock(options), +})) + +vi.mock('@/lib/api/orderbook', () => ({ + fetchOrderbook: vi.fn(), +})) + +describe('useQueryOrderbook', () => { + it('returns hasOrders: true when orderbook offers exist', () => { + const mockOrderbook = { + offers: [{ counterparty: 'maker1' }], + } + queryMock.mockReturnValue({ data: mockOrderbook, isLoading: false, isError: false }) + + const { result } = renderHook(() => useQueryOrderbook()) + + expect(result.current.hasOrders).toBe(true) + expect(result.current.queryResult.data).toEqual(mockOrderbook) + }) + + it('returns hasOrders: false when orderbook offers array is empty', () => { + queryMock.mockReturnValue({ data: { offers: [] }, isLoading: false, isError: false }) + + const { result } = renderHook(() => useQueryOrderbook()) + + expect(result.current.hasOrders).toBe(false) + }) + + it('returns hasOrders: false when data is undefined', () => { + queryMock.mockReturnValue({ data: undefined, isLoading: false, isError: false }) + + const { result } = renderHook(() => useQueryOrderbook()) + + expect(result.current.hasOrders).toBe(false) + }) +}) diff --git a/src/hooks/useQueryOrderbook.ts b/src/hooks/useQueryOrderbook.ts new file mode 100644 index 00000000..1249d4f6 --- /dev/null +++ b/src/hooks/useQueryOrderbook.ts @@ -0,0 +1,28 @@ +import { useQuery, type UseQueryResult } from '@tanstack/react-query' +import { fetchOrderbook, type OrderbookResponse } from '@/lib/api/orderbook' +import { withQueryDelay } from '@/lib/queryClient' + +export type UseQueryOrderbookResult = { + hasOrders: boolean + queryResult: UseQueryResult +} + +export function useQueryOrderbook( + options: Omit>[0], 'queryFn' | 'queryKey'> = {}, +): UseQueryOrderbookResult { + const queryResult = useQuery({ + queryKey: ['orderbook-precheck'], + queryFn: withQueryDelay(fetchOrderbook, { + // avoid flickering and let user briefly know that something is happening in the background + delayBefore: 1_000, + }), + retry: false, + staleTime: 30 * 1_000, + ...options, + }) + + return { + hasOrders: (queryResult.data?.offers.length ?? 0) > 0, + queryResult, + } +} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index adbb1d0c..13bac7c6 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -762,6 +762,10 @@ "text_orderbook_summary_filtered_one": "found {{ count }} offer by {{ counterpartyCount }} counterparty", "text_orderbook_summary_filtered_other": "found {{ count }} offers by {{ counterpartyCount }} counterparties", "alert_empty_orderbook": "Orderbook is empty", + "alert_precheck_empty_title": "No active makers found", + "alert_precheck_empty_description": "Your local Orderbook appears to be empty right now. Collaborative transactions might fail. Please check <1>your local Orderbook.", + "alert_precheck_empty_text_button_check": "Check again", + "alert_precheck_empty_text_button_checking": "Checking...", "error_loading_orderbook_failed": "Error while loading the orderbook. Your current local setup might not support fetching the orderbook. Reason: {{ reason }}", "text_offer_type_absolute": "absolute", "text_offer_type_relative": "relative", diff --git a/src/stories/jam/OrderbookEmptyAlert.stories.tsx b/src/stories/jam/OrderbookEmptyAlert.stories.tsx new file mode 100644 index 00000000..c7a39a0f --- /dev/null +++ b/src/stories/jam/OrderbookEmptyAlert.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { OrderbookEmptyAlert } from '@/components/ui/jam/OrderbookEmptyAlert' + +const meta: Meta = { + title: 'Jam/OrderbookEmptyAlert', + component: OrderbookEmptyAlert, + tags: ['autodocs'], +} +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + isChecking: false, + onCheckClick: async () => alert('onCheck clicked'), + }, +} + +export const Checking: Story = { + args: { + isChecking: true, + onCheckClick: async () => alert('onCheck clicked'), + }, +} From 69423b6b1dc141e371c9cba261f75ac5869eb4bb Mon Sep 17 00:00:00 2001 From: Aman Yadav Date: Wed, 12 Aug 2026 00:48:00 +0530 Subject: [PATCH 54/56] perf(context): optimize combinedUtxosHash from O(n^2) to O(n) (#1424) --- .../JamWalletInfoContextProvider.test.tsx | 91 +++++++++++++++++++ src/context/JamWalletInfoContextProvider.tsx | 4 +- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/context/JamWalletInfoContextProvider.test.tsx b/src/context/JamWalletInfoContextProvider.test.tsx index 7fb95c29..984aed9f 100644 --- a/src/context/JamWalletInfoContextProvider.test.tsx +++ b/src/context/JamWalletInfoContextProvider.test.tsx @@ -1,3 +1,5 @@ +import { sha256 } from '@noble/hashes/sha2.js' +import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js' import { render, screen, waitFor } from '@testing-library/react' import { Network } from 'bitcoin-address-validation' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -283,3 +285,92 @@ describe('', () => { expect(balanceSummary?.calculatedTotalBalanceInSats).toBe(25_000) }) }) + +/** + * Reference implementation of the old O(n²) combinedUtxosHash logic. + * Used only in tests to verify the optimized implementation is a drop-in replacement. + */ +const combinedUtxosHashReference = (utxoList: Utxo[]): string => { + const utxoIds = utxoList.map((it) => hexToBytes(it.utxo.split(':', 1)[0])) + const combinedUtxoIds = new Uint8Array(utxoIds.reduce((acc, current) => [...acc, ...current], [] as number[])) + return bytesToHex(sha256(combinedUtxoIds)) +} + +describe('combinedUtxosHash — optimized vs reference (regression)', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.waitQueryError = null + mocks.walletInfo = undefined + mocks.utxosRefetch.mockResolvedValue({ data: { utxos: [] } }) + mocks.displayWalletRefetch.mockResolvedValue({ data: { walletinfo: undefined } }) + }) + + /** Render the provider with the given UTXO list and capture the computed utxosHashHex. */ + const captureHash = (utxoList: Utxo[]): string => { + mocks.utxos = utxoList + let captured = '' + render( + + (captured = context.utxosHashHex)} /> + , + ) + return captured + } + + it('produces the same hash for an empty UTXO list', () => { + const utxoList: Utxo[] = [] + const actual = captureHash(utxoList) + expect(actual).toBe(combinedUtxosHashReference(utxoList)) + expect(actual).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + }) + + it('produces the same hash for a single UTXO', () => { + const utxoList = [utxo({ utxo: `${txid('a')}:0` })] + const actual = captureHash(utxoList) + expect(actual).toBe(combinedUtxosHashReference(utxoList)) + expect(actual).toBe('e0e77a507412b120f6ede61f62295b1a7b2ff19d3dcc8f7253e51663470c888e') + }) + + it('is okay for it to produce the same hash for different output in same txid', () => { + const utxoList0 = [utxo({ utxo: `${txid('a')}:0` })] + const actual0 = captureHash(utxoList0) + expect(actual0).toBe(combinedUtxosHashReference(utxoList0)) + expect(actual0).toBe('e0e77a507412b120f6ede61f62295b1a7b2ff19d3dcc8f7253e51663470c888e') + + const utxoList1 = [utxo({ utxo: `${txid('a')}:1` })] + const actual1 = captureHash(utxoList1) + expect(actual1).toBe(combinedUtxosHashReference(utxoList1)) + expect(actual1).toBe('e0e77a507412b120f6ede61f62295b1a7b2ff19d3dcc8f7253e51663470c888e') + expect(actual1).toBe(actual0) + }) + + it('produces the same hash for multiple UTXOs', () => { + const utxoList = [ + utxo({ utxo: `${txid('a')}:0` }), + utxo({ utxo: `${txid('b')}:1` }), + utxo({ utxo: `${txid('c')}:2` }), + ] + + const actual = captureHash(utxoList) + expect(actual).toBe(combinedUtxosHashReference(utxoList)) + expect(actual).toBe('6bdaf8651d78f983ab7ce864414ace095b2942cbe5ff43deade48d51aa34b2cd') + }) + + it('produces the same hash for UTXOs whose txid bytes vary across the full byte range', () => { + const utxoList = [ + utxo({ utxo: `${txid('1')}:0` }), + utxo({ utxo: `${'ab'.repeat(32)}:3` }), + utxo({ utxo: `${'ff'.repeat(32)}:7` }), + ] + expect(captureHash(utxoList)).toBe(combinedUtxosHashReference(utxoList)) + }) + + it('produces the same hash for a large UTXO set (50 entries)', () => { + const chars = '0123456789abcdef' + const utxoList = Array.from({ length: 50 }, (_, i) => { + const ch = chars[i % chars.length] + return utxo({ utxo: `${ch.repeat(64)}:${i}`, mixdepth: i % 5 }) + }) + expect(captureHash(utxoList)).toBe(combinedUtxosHashReference(utxoList)) + }) +}) diff --git a/src/context/JamWalletInfoContextProvider.tsx b/src/context/JamWalletInfoContextProvider.tsx index 721a8c8f..7962f4f2 100644 --- a/src/context/JamWalletInfoContextProvider.tsx +++ b/src/context/JamWalletInfoContextProvider.tsx @@ -1,6 +1,6 @@ import { useMemo, useState, type Dispatch, type PropsWithChildren, type SetStateAction } from 'react' import { sha256 } from '@noble/hashes/sha2.js' -import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js' +import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils.js' import { CancelledError, useMutation, useQuery } from '@tanstack/react-query' import { getAddressInfo } from 'bitcoin-address-validation' import { useQueryDisplayWallet, type WalletInfoApiObject } from '@/hooks/useQueryDisplayWallet' @@ -131,7 +131,7 @@ interface JamWalletInfoContextProviderProps { const combinedUtxosHash = (utxos: Utxo[]) => { const utxoIds = utxos.map((it) => hexToBytes(it.utxo.split(':', 1)[0])) - const combinedUtxoIds = new Uint8Array(utxoIds.reduce((acc, current) => [...acc, ...current], [] as number[])) + const combinedUtxoIds = concatBytes(...utxoIds) return sha256(combinedUtxoIds) } From 0b707f44fc07793ec2fbfcf4d5252f5cf6b8536d Mon Sep 17 00:00:00 2001 From: Anirudh Patwal <161865581+CapThunder19@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:25:54 +0530 Subject: [PATCH 55/56] fix(a11y): make settings rows keyboard-accessible (#1430) --- src/components/settings/SeedPhraseDialog.tsx | 7 +- src/components/settings/SettingsItem.test.tsx | 183 ++++++++++++++++++ src/components/settings/SettingsItem.tsx | 62 ++++-- 3 files changed, 235 insertions(+), 17 deletions(-) create mode 100644 src/components/settings/SettingsItem.test.tsx diff --git a/src/components/settings/SeedPhraseDialog.tsx b/src/components/settings/SeedPhraseDialog.tsx index 146ade70..bddd616d 100644 --- a/src/components/settings/SeedPhraseDialog.tsx +++ b/src/components/settings/SeedPhraseDialog.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, useMemo, type ComponentProps } from 'react' import { getseedOptions } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { cx } from 'class-variance-authority' import { AlertTriangleIcon, ClockIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' @@ -17,7 +16,7 @@ import { import { Label } from '@/components/ui/label' import { useApiClient } from '@/hooks/useApiClient' import { getErrorReason } from '@/lib/errorReason' -import type { WalletFileName } from '@/lib/utils' +import { cn, type WalletFileName } from '@/lib/utils' import type { Milliseconds, WithRequiredProperty } from '@/types/global' import { SeedPhraseGrid } from '../ui/jam/SeedPhraseGrid' import { Spinner } from '../ui/spinner' @@ -189,13 +188,13 @@ export const SeedPhraseDialog = ({
diff --git a/src/components/settings/SettingsItem.test.tsx b/src/components/settings/SettingsItem.test.tsx new file mode 100644 index 00000000..1eb8cd2e --- /dev/null +++ b/src/components/settings/SettingsItem.test.tsx @@ -0,0 +1,183 @@ +import { render, screen } from '@testing-library/react' +import user from '@testing-library/user-event' +import { KeyRoundIcon } from 'lucide-react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { SettingItem, SettingsLink, SettingSwitch } from './SettingsItem' + +const mocks = vi.hoisted(() => ({ + navigate: vi.fn(), + open: vi.fn(), +})) + +vi.mock('react-router-dom', () => ({ + useNavigate: () => mocks.navigate, +})) + +vi.mock('@/components/ui/switch', () => ({ + Switch: ({ + checked, + disabled, + onCheckedChange, + }: { + checked?: boolean + disabled?: boolean + onCheckedChange?: (checked: boolean) => void + }) => ( + + ), +})) + +describe('SettingItem', () => { + beforeEach(() => { + mocks.navigate.mockReset() + mocks.open.mockReset() + }) + + it('renders title and icon', () => { + render() + + expect(screen.getByText('settings.show_seed')).toBeInTheDocument() + }) + + it('exposes an actionable row as a button', () => { + render() + + expect(screen.getByRole('button', { name: 'settings.show_seed' })).toBeInTheDocument() + }) + + it('does not render a button without an action', () => { + render() + + expect(screen.queryByRole('button')).not.toBeInTheDocument() + }) + + it('is reachable by keyboard', async () => { + render() + + await user.tab() + + expect(screen.getByRole('button', { name: 'settings.show_seed' })).toHaveFocus() + }) + + it('triggers the action on Enter', async () => { + const action = vi.fn() + render() + + await user.tab() + await user.keyboard('{Enter}') + + expect(action).toHaveBeenCalledOnce() + }) + + it('triggers the action on Space', async () => { + const action = vi.fn() + render() + + await user.tab() + await user.keyboard(' ') + + expect(action).toHaveBeenCalledOnce() + }) + + it('triggers the action on click', async () => { + const action = vi.fn() + render() + + await user.click(screen.getByRole('button', { name: 'settings.show_seed' })) + + expect(action).toHaveBeenCalledOnce() + }) + + it('does not trigger the action while disabled', async () => { + const action = vi.fn() + render() + + const item = screen.getByRole('button', { name: 'settings.show_seed' }) + expect(item).toBeDisabled() + + await user.click(item) + await user.tab() + await user.keyboard('{Enter}') + + expect(action).not.toHaveBeenCalled() + }) + + it('stays a wrapper when children provide their own control', () => { + render( + + + , + ) + + // no button is nested inside another button + expect(screen.queryByRole('button', { name: 'settings.show_seed' })).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'child-control' })).toBeInTheDocument() + }) +}) + +describe('SettingsLink', () => { + beforeEach(() => { + mocks.navigate.mockReset() + }) + + it('navigates internally on Enter', async () => { + render() + + await user.tab() + await user.keyboard('{Enter}') + + expect(mocks.navigate).toHaveBeenCalledWith('/rescan') + }) + + it('opens external links in a new tab on Enter', async () => { + vi.stubGlobal('open', mocks.open) + render() + + await user.tab() + await user.keyboard('{Enter}') + + expect(mocks.open).toHaveBeenCalledWith('https://example.org', '_blank', 'noreferrer,noopener') + expect(mocks.navigate).not.toHaveBeenCalled() + }) +}) + +describe('SettingSwitch', () => { + it('is keyboard operable via the row when no toggle is displayed', async () => { + const onCheckedChange = vi.fn() + render( + , + ) + + await user.tab() + await user.keyboard('{Enter}') + + expect(onCheckedChange).toHaveBeenCalledWith(true) + }) + + it('is keyboard operable via the toggle when one is displayed', async () => { + const onCheckedChange = vi.fn() + render( + , + ) + + await user.tab() + expect(screen.getByRole('button', { name: 'switch:false' })).toHaveFocus() + + await user.keyboard('{Enter}') + expect(onCheckedChange).toHaveBeenCalledWith(true) + }) +}) diff --git a/src/components/settings/SettingsItem.tsx b/src/components/settings/SettingsItem.tsx index e92e70be..499d5fe7 100644 --- a/src/components/settings/SettingsItem.tsx +++ b/src/components/settings/SettingsItem.tsx @@ -1,9 +1,9 @@ import type { PropsWithChildren, ReactNode } from 'react' -import { cx } from 'class-variance-authority' import { ExternalLinkIcon } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { Switch } from '@/components/ui/switch' +import { cn } from '@/lib/utils' type SettingsItemProps = PropsWithChildren<{ icon?: LucideIcon @@ -11,6 +11,12 @@ type SettingsItemProps = PropsWithChildren<{ title: string disabled?: boolean action?: () => void | Promise + /** + * Set when {@link children} already renders its own focusable control (e.g. a `Switch`). + * The row then stays a plain wrapper instead of becoming a button, so a button is never + * nested inside a button - keyboard users operate the child control directly. + */ + hasInteractiveChild?: boolean }> export const SettingItem = ({ @@ -19,16 +25,22 @@ export const SettingItem = ({ title, action, disabled = false, + hasInteractiveChild = false, children, }: SettingsItemProps) => { + const rowClassName = cn('flex min-w-0 items-center justify-between gap-2 py-2', { + 'hover:bg-muted/50 cursor-pointer rounded-md px-2': !disabled, + 'cursor-not-allowed opacity-60': disabled, + }) + // The row bleeds into the card padding so the hover background spans the full width. + // A block `div` with `width: auto` gets this from the negative margin alone, but a + // `button` does not stretch to its parent, and once `w-full` pins the width the + // negative margin no longer widens it. Keeping the bleed on a wrapper makes `w-full` + // resolve against the widened box, so both variants end up the same size. + const bleedClassName = !disabled ? '-mx-2' : undefined + const content = ( -
void action() : undefined} - > + <>
{Icon && } @@ -39,13 +51,34 @@ export const SettingItem = ({
{children !== undefined &&
{children}
} -
+ ) - return content + // A row that is itself the control must be a real button: the browser then provides + // focus, Enter/Space activation and the correct role for free. + if (action && !hasInteractiveChild) { + return ( +
+ +
+ ) + } + + return ( +
void action() : undefined}> + {content} +
+ ) } -type SettingsLinkProps = Omit & { +type SettingsLinkProps = Omit & { to: string external?: boolean } @@ -69,14 +102,17 @@ export const SettingsLink = ({ to, external = false, ...props }: SettingsLinkPro ) } -type SettingsSwitchProps = Omit & { +type SettingsSwitchProps = Omit & { checked?: boolean onCheckedChange?: (checked: boolean) => void displayToggle?: boolean } export const SettingSwitch = ({ checked, onCheckedChange, displayToggle = true, ...props }: SettingsSwitchProps) => { return ( - onCheckedChange?.(!checked)}> + // With a visible toggle the `Switch` is the focusable control and the row stays a + // wrapper. Without one the row itself has to be the button, or the setting cannot be + // reached by keyboard at all. + onCheckedChange?.(!checked)} hasInteractiveChild={displayToggle}> {displayToggle && } ) From a1abd6c7e67439f40108dd2d7dcf6cd3d8a5f7a3 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Thu, 13 Aug 2026 08:23:27 +0200 Subject: [PATCH 56/56] feat: add language selector to login page (#1431) * chore: remove wrapper from language selector and move to ui module * chore: rename SettingItem -> SettingsItem * chore(settings): hover and pointer only for items with action * feat: add language selector to login page --- src/components/login/LoginCard.tsx | 39 ++++-- src/components/settings/LanguageSelector.tsx | 43 ------- src/components/settings/SettingsItem.test.tsx | 28 ++--- src/components/settings/SettingsItem.tsx | 15 +-- src/components/settings/SettingsPage.test.tsx | 5 +- src/components/settings/SettingsPage.tsx | 33 ++--- src/components/ui/button-variants.ts | 1 + .../jam}/LanguageSelector.test.tsx | 2 + src/components/ui/jam/LanguageSelector.tsx | 116 ++++++++++++++++++ src/i18n/locales/en/translation.json | 2 + 10 files changed, 192 insertions(+), 92 deletions(-) delete mode 100644 src/components/settings/LanguageSelector.tsx rename src/components/{settings => ui/jam}/LanguageSelector.test.tsx (93%) create mode 100644 src/components/ui/jam/LanguageSelector.tsx diff --git a/src/components/login/LoginCard.tsx b/src/components/login/LoginCard.tsx index 934ddeca..410f8096 100644 --- a/src/components/login/LoginCard.tsx +++ b/src/components/login/LoginCard.tsx @@ -1,9 +1,10 @@ import { useState, type ComponentProps } from 'react' -import { RefreshCwIcon, WalletIcon } from 'lucide-react' +import { LanguagesIcon, RefreshCwIcon, WalletIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { LanguageSelectorDropdownMenu } from '@/components/ui/jam/LanguageSelector' import { WalletLoadErrorAlert } from '@/components/ui/jam/WalletLoadErrorAlert' import { Skeleton } from '@/components/ui/skeleton' import { Spinner } from '@/components/ui/spinner' @@ -71,22 +72,36 @@ export const LoginCard = ({ <> -
- {listWalletsFetching ? ( - - ) : ( +
+
+ + + + - )} +
{t('login.title')} {listWalletsLoading ? ( @@ -103,7 +118,7 @@ export const LoginCard = ({ ) : null} - + {listWalletsError ? ( <> diff --git a/src/components/settings/LanguageSelector.tsx b/src/components/settings/LanguageSelector.tsx deleted file mode 100644 index f65dd5d2..00000000 --- a/src/components/settings/LanguageSelector.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { GlobeIcon } from 'lucide-react' -import { useTranslation } from 'react-i18next' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import languages from '@/i18n/languages' - -export const LanguageSelector = () => { - const { i18n, t } = useTranslation() - - const handleLanguageChange = async (languageKey: string) => { - await i18n.changeLanguage(languageKey) - } - - const getCurrentLanguageDescription = () => { - const currentLanguage = languages.find((lang) => lang.key === i18n.resolvedLanguage) - return currentLanguage?.description || 'English' - } - - return ( -
-
-
- -
-
-

{t('settings.label_select_language')}

-
-
- - -
- ) -} diff --git a/src/components/settings/SettingsItem.test.tsx b/src/components/settings/SettingsItem.test.tsx index 1eb8cd2e..f2edd7e3 100644 --- a/src/components/settings/SettingsItem.test.tsx +++ b/src/components/settings/SettingsItem.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react' import user from '@testing-library/user-event' import { KeyRoundIcon } from 'lucide-react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { SettingItem, SettingsLink, SettingSwitch } from './SettingsItem' +import { SettingsItem, SettingsLink, SettingsSwitch } from './SettingsItem' const mocks = vi.hoisted(() => ({ navigate: vi.fn(), @@ -29,32 +29,32 @@ vi.mock('@/components/ui/switch', () => ({ ), })) -describe('SettingItem', () => { +describe('SettingsItem', () => { beforeEach(() => { mocks.navigate.mockReset() mocks.open.mockReset() }) it('renders title and icon', () => { - render() + render() expect(screen.getByText('settings.show_seed')).toBeInTheDocument() }) it('exposes an actionable row as a button', () => { - render() + render() expect(screen.getByRole('button', { name: 'settings.show_seed' })).toBeInTheDocument() }) it('does not render a button without an action', () => { - render() + render() expect(screen.queryByRole('button')).not.toBeInTheDocument() }) it('is reachable by keyboard', async () => { - render() + render() await user.tab() @@ -63,7 +63,7 @@ describe('SettingItem', () => { it('triggers the action on Enter', async () => { const action = vi.fn() - render() + render() await user.tab() await user.keyboard('{Enter}') @@ -73,7 +73,7 @@ describe('SettingItem', () => { it('triggers the action on Space', async () => { const action = vi.fn() - render() + render() await user.tab() await user.keyboard(' ') @@ -83,7 +83,7 @@ describe('SettingItem', () => { it('triggers the action on click', async () => { const action = vi.fn() - render() + render() await user.click(screen.getByRole('button', { name: 'settings.show_seed' })) @@ -92,7 +92,7 @@ describe('SettingItem', () => { it('does not trigger the action while disabled', async () => { const action = vi.fn() - render() + render() const item = screen.getByRole('button', { name: 'settings.show_seed' }) expect(item).toBeDisabled() @@ -106,9 +106,9 @@ describe('SettingItem', () => { it('stays a wrapper when children provide their own control', () => { render( - + - , + , ) // no button is nested inside another button @@ -147,7 +147,7 @@ describe('SettingSwitch', () => { it('is keyboard operable via the row when no toggle is displayed', async () => { const onCheckedChange = vi.fn() render( - { it('is keyboard operable via the toggle when one is displayed', async () => { const onCheckedChange = vi.fn() render( - -export const SettingItem = ({ +export const SettingsItem = ({ icon: Icon, renderIcon, title, @@ -29,7 +29,8 @@ export const SettingItem = ({ children, }: SettingsItemProps) => { const rowClassName = cn('flex min-w-0 items-center justify-between gap-2 py-2', { - 'hover:bg-muted/50 cursor-pointer rounded-md px-2': !disabled, + 'hover:bg-muted/50 cursor-pointer': !disabled && action, + 'rounded-md px-2': !disabled, 'cursor-not-allowed opacity-60': disabled, }) // The row bleeds into the card padding so the hover background spans the full width. @@ -87,7 +88,7 @@ export const SettingsLink = ({ to, external = false, ...props }: SettingsLinkPro const navigate = useNavigate() return ( - { if (external) { @@ -98,7 +99,7 @@ export const SettingsLink = ({ to, external = false, ...props }: SettingsLinkPro }} > {external && } - + ) } @@ -107,13 +108,13 @@ type SettingsSwitchProps = Omit void displayToggle?: boolean } -export const SettingSwitch = ({ checked, onCheckedChange, displayToggle = true, ...props }: SettingsSwitchProps) => { +export const SettingsSwitch = ({ checked, onCheckedChange, displayToggle = true, ...props }: SettingsSwitchProps) => { return ( // With a visible toggle the `Switch` is the focusable control and the row stays a // wrapper. Without one the row itself has to be the button, or the setting cannot be // reached by keyboard at all. - onCheckedChange?.(!checked)} hasInteractiveChild={displayToggle}> + onCheckedChange?.(!checked)} hasInteractiveChild={displayToggle}> {displayToggle && } - + ) } diff --git a/src/components/settings/SettingsPage.test.tsx b/src/components/settings/SettingsPage.test.tsx index 91c4684a..20e58cb3 100644 --- a/src/components/settings/SettingsPage.test.tsx +++ b/src/components/settings/SettingsPage.test.tsx @@ -59,6 +59,9 @@ vi.mock('next-themes', () => ({ vi.mock('react-i18next', () => ({ useTranslation: () => ({ + i18n: { + resolvedLanguage: 'en', + }, t: (key: string) => key, }), })) @@ -123,7 +126,7 @@ vi.mock('@/components/settings/AccountXpubsDialog', () => ({ ) : null, })) -vi.mock('@/components/settings/LanguageSelector', () => ({ +vi.mock('@/components/ui/jam/LanguageSelector', () => ({ LanguageSelector: () =>
language-selector
, })) diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index d0f1337a..3fba6fd4 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -18,6 +18,7 @@ import { HandCoinsIcon, SparklesIcon, HistoryIcon, + LanguagesIcon, } from 'lucide-react' import { useTheme } from 'next-themes' import { useTranslation } from 'react-i18next' @@ -25,8 +26,10 @@ import { useNavigate, type NavigateFunction } from 'react-router-dom' import { useStore } from 'zustand' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { CurrencySymbol } from '@/components/ui/jam/CurrencySymbol' +import { LanguageSelector } from '@/components/ui/jam/LanguageSelector' import PageTitle from '@/components/ui/jam/PageTitle' import { Separator } from '@/components/ui/separator' +import { Spinner } from '@/components/ui/spinner' import { isDebugFeatureEnabled, isDevMode } from '@/constants/debugFeatures' import { JAM_DOCS_URL, JAM_MATRIX_URL, JAM_REPO_URL, JAM_SEED_MODAL_TIMEOUT, JAM_TELEGRAM_URL } from '@/constants/jam' import { routes } from '@/constants/routes' @@ -36,11 +39,9 @@ import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation' import { cn, type WalletFileName } from '@/lib/utils' import { authStore } from '@/store/authStore' import { jamSettingsStore } from '@/store/jamSettingsStore' -import { Spinner } from '../ui/spinner' import { AccountXpubsDialog } from './AccountXpubsDialog' -import { LanguageSelector } from './LanguageSelector' import { SeedPhraseDialog } from './SeedPhraseDialog' -import { SettingItem, SettingsLink, SettingSwitch } from './SettingsItem' +import { SettingsItem, SettingsLink, SettingsSwitch } from './SettingsItem' import { FeeConfigDialog } from './fees/FeeConfigDialog' interface SettingPageProps { @@ -81,7 +82,7 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) {t('settings.section_title_display')} - - } title={t(currency === 'btc' ? 'settings.use_btc' : 'settings.use_sats')} checked={currency === 'btc'} @@ -97,7 +98,7 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) displayToggle={false} /> - - - + + + @@ -127,7 +130,7 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) {t('settings.section_title_market')} - setShowFeeConfigDialog(true)} @@ -141,21 +144,21 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) {t('settings.section_title_wallet')} - setShowSeedDialog(true)} disabled={hashedPassword === undefined} /> - setShowXpubsDialog(true)} disabled={hashedPassword === undefined} /> - lockWalletMutation.isPending ? ( @@ -217,7 +220,7 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) external={true} /> - - - ({ SelectTrigger: ({ children }: ChildrenProps) =>
{children}
, SelectValue: ({ placeholder }: { placeholder?: string }) =>
{placeholder}
, SelectContent: ({ children }: ChildrenProps) =>
{children}
, + SelectGroup: ({ children }: ChildrenProps) =>
{children}
, + SelectLabel: ({ children }: ChildrenProps) =>
{children}
, SelectItem: ({ children, value }: ChildrenProps & { value: string }) => (
{children}
), diff --git a/src/components/ui/jam/LanguageSelector.tsx b/src/components/ui/jam/LanguageSelector.tsx new file mode 100644 index 00000000..d0553a45 --- /dev/null +++ b/src/components/ui/jam/LanguageSelector.tsx @@ -0,0 +1,116 @@ +import type { ComponentProps, PropsWithChildren } from 'react' +import { LanguagesIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + SelectGroup, + SelectLabel, +} from '@/components/ui/select' +import languages from '@/i18n/languages' +import { cn } from '@/lib/utils' +import { Button } from '../button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from '../dropdown-menu' + +export const LanguageSelector = ({ + className, + ...props +}: ComponentProps & { className?: string } = {}) => { + const { i18n, t } = useTranslation() + const currentLanguage = languages.find((lang) => lang.key === i18n.resolvedLanguage) + const currentLanguageDescription = currentLanguage?.description || 'English' + + return ( + + ) +} + +export const LanguageSelectorDropdownMenu = ({ + className, + align, + loop = true, + ...props +}: PropsWithChildren< + ComponentProps & { + className?: string + align?: ComponentProps['align'] + loop?: ComponentProps['loop'] + } +> = {}) => { + const { i18n, t } = useTranslation() + + return ( + + + {props.children ?? ( + + )} + + + + + + {t('settings.label_select_language')} + + { + void i18n.changeLanguage(value) + }} + > + {languages.map((language) => ( + + {language.description} + + ))} + + + + + ) +} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 13bac7c6..b5bb57a2 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -293,6 +293,8 @@ "use_address_chunking_enabled": "Address display chunking enabled", "use_address_chunking_disabled": "Address display chunking disabled", "label_select_language": "Language", + "label_select_language_title": "Select language", + "label_select_language_aria_label": "$t(settings.label_select_language_title)", "show_seed": "Show seed phrase", "show_xpubs": "Show account xpubs", "reveal_seed": "Reveal seed phrase",