From 3520f485e8427075a62bc8532d980bc2dc320e6c Mon Sep 17 00:00:00 2001 From: Alexander Khrushkov Date: Thu, 26 Feb 2026 17:38:16 +0400 Subject: [PATCH 01/17] init --- package-lock.json | 1945 +++++++++-------- package.json | 46 +- postcss.config.js | 5 - public/popup.html | 2 +- scripts/build.js | 27 +- src/background/nostr-service.ts | 19 - src/background/providers/index.ts | 5 - src/background/token-transfer-service.ts | 12 - src/components/ui/AlertMessage.tsx | 107 + src/components/ui/BaseModal.tsx | 79 + src/components/ui/Button.tsx | 99 + src/components/ui/EmptyState.tsx | 42 + src/components/ui/MenuButton.tsx | 77 + src/components/ui/ModalHeader.tsx | 72 + src/components/ui/index.ts | 11 + src/components/wallet/L3WalletView.tsx | 467 ++++ src/components/wallet/UnlockWallet.tsx | 96 + src/components/wallet/WalletPanel.tsx | 183 ++ src/components/wallet/modals/LookupModal.tsx | 191 ++ .../wallet/modals/PaymentRequestModal.tsx | 221 ++ .../wallet/modals/SeedPhraseModal.tsx | 97 + src/components/wallet/modals/SendModal.tsx | 449 ++++ .../wallet/modals/SettingsModal.tsx | 68 + src/components/wallet/modals/SwapModal.tsx | 304 +++ src/components/wallet/modals/TopUpModal.tsx | 102 + .../wallet/modals/TransactionHistoryModal.tsx | 303 +++ src/components/wallet/modals/index.ts | 11 + .../wallet/onboarding/CreateWalletFlow.tsx | 154 ++ .../onboarding/MnemonicBackupScreen.tsx | 117 + .../wallet/onboarding/NametagScreen.tsx | 154 ++ .../wallet/onboarding/RestoreScreen.tsx | 199 ++ .../wallet/onboarding/StartScreen.tsx | 147 ++ src/components/wallet/onboarding/index.ts | 8 + .../wallet/onboarding/useOnboardingFlow.ts | 329 +++ .../wallet/shared/AddressSelector.tsx | 98 + src/components/wallet/shared/AssetRow.tsx | 136 ++ .../wallet/shared/BackupWalletModal.tsx | 57 + .../wallet/shared/LogoutConfirmModal.tsx | 80 + .../wallet/shared/RegisterNametagModal.tsx | 216 ++ .../wallet/shared/SaveWalletModal.tsx | 116 + src/components/wallet/shared/TokenRow.tsx | 136 ++ src/components/wallet/shared/index.ts | 6 + src/platform/extension/SphereProvider.tsx | 263 +++ .../extension}/background/index.ts | 0 .../extension}/background/message-handler.ts | 24 + .../background/nametag-mint-service.ts | 0 .../extension}/background/nostr-keys.ts | 0 .../providers/chrome-storage-provider.ts | 0 .../extension/background/providers/index.ts | 2 + .../extension}/background/storage.ts | 0 .../extension}/background/wallet-manager.ts | 71 +- src/{ => platform/extension}/content/index.ts | 0 src/{ => platform/extension}/inject/index.ts | 0 src/platform/extension/popup/PopupApp.tsx | 27 + src/platform/extension/popup/main.tsx | 25 + src/platform/extension/popup/styles.css | 38 + src/popup/App.tsx | 81 - src/popup/components/CreateWallet.tsx | 132 -- src/popup/components/Dashboard.tsx | 174 -- src/popup/components/ImportWallet.tsx | 127 -- src/popup/components/PendingTransactions.tsx | 163 -- src/popup/components/Receive.tsx | 144 -- src/popup/components/RegisterNametag.tsx | 241 -- src/popup/components/Send.tsx | 256 --- src/popup/components/Settings.tsx | 399 ---- src/popup/components/UnlockWallet.tsx | 71 - src/popup/hooks/useWallet.ts | 569 ----- src/popup/main.tsx | 14 - src/popup/store/index.ts | 81 - src/popup/styles/index.css | 14 - src/sdk/context.ts | 60 + src/sdk/hooks/index.ts | 7 + src/sdk/hooks/useAssets.ts | 22 + src/sdk/hooks/useIdentity.ts | 32 + src/sdk/hooks/useTokens.ts | 29 + src/sdk/hooks/useTransactionHistory.ts | 16 + src/sdk/hooks/useTransfer.ts | 39 + src/sdk/hooks/useWalletStatus.ts | 6 + src/sdk/index.ts | 6 + src/sdk/queryKeys.ts | 30 + src/sdk/types.ts | 11 + src/sdk/utils/currency.ts | 72 + src/shared/sphere-sdk-browser.d.ts | 1 + src/shared/types.ts | 7 +- src/types/sphere-sdk-browser.d.ts | 24 - tsconfig.json | 18 +- vite.config.ts | 11 +- 87 files changed, 6805 insertions(+), 3495 deletions(-) delete mode 100644 postcss.config.js delete mode 100644 src/background/nostr-service.ts delete mode 100644 src/background/providers/index.ts delete mode 100644 src/background/token-transfer-service.ts create mode 100644 src/components/ui/AlertMessage.tsx create mode 100644 src/components/ui/BaseModal.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/EmptyState.tsx create mode 100644 src/components/ui/MenuButton.tsx create mode 100644 src/components/ui/ModalHeader.tsx create mode 100644 src/components/ui/index.ts create mode 100644 src/components/wallet/L3WalletView.tsx create mode 100644 src/components/wallet/UnlockWallet.tsx create mode 100644 src/components/wallet/WalletPanel.tsx create mode 100644 src/components/wallet/modals/LookupModal.tsx create mode 100644 src/components/wallet/modals/PaymentRequestModal.tsx create mode 100644 src/components/wallet/modals/SeedPhraseModal.tsx create mode 100644 src/components/wallet/modals/SendModal.tsx create mode 100644 src/components/wallet/modals/SettingsModal.tsx create mode 100644 src/components/wallet/modals/SwapModal.tsx create mode 100644 src/components/wallet/modals/TopUpModal.tsx create mode 100644 src/components/wallet/modals/TransactionHistoryModal.tsx create mode 100644 src/components/wallet/modals/index.ts create mode 100644 src/components/wallet/onboarding/CreateWalletFlow.tsx create mode 100644 src/components/wallet/onboarding/MnemonicBackupScreen.tsx create mode 100644 src/components/wallet/onboarding/NametagScreen.tsx create mode 100644 src/components/wallet/onboarding/RestoreScreen.tsx create mode 100644 src/components/wallet/onboarding/StartScreen.tsx create mode 100644 src/components/wallet/onboarding/index.ts create mode 100644 src/components/wallet/onboarding/useOnboardingFlow.ts create mode 100644 src/components/wallet/shared/AddressSelector.tsx create mode 100644 src/components/wallet/shared/AssetRow.tsx create mode 100644 src/components/wallet/shared/BackupWalletModal.tsx create mode 100644 src/components/wallet/shared/LogoutConfirmModal.tsx create mode 100644 src/components/wallet/shared/RegisterNametagModal.tsx create mode 100644 src/components/wallet/shared/SaveWalletModal.tsx create mode 100644 src/components/wallet/shared/TokenRow.tsx create mode 100644 src/components/wallet/shared/index.ts create mode 100644 src/platform/extension/SphereProvider.tsx rename src/{ => platform/extension}/background/index.ts (100%) rename src/{ => platform/extension}/background/message-handler.ts (97%) rename src/{ => platform/extension}/background/nametag-mint-service.ts (100%) rename src/{ => platform/extension}/background/nostr-keys.ts (100%) rename src/{ => platform/extension}/background/providers/chrome-storage-provider.ts (100%) create mode 100644 src/platform/extension/background/providers/index.ts rename src/{ => platform/extension}/background/storage.ts (100%) rename src/{ => platform/extension}/background/wallet-manager.ts (95%) rename src/{ => platform/extension}/content/index.ts (100%) rename src/{ => platform/extension}/inject/index.ts (100%) create mode 100644 src/platform/extension/popup/PopupApp.tsx create mode 100644 src/platform/extension/popup/main.tsx create mode 100644 src/platform/extension/popup/styles.css delete mode 100644 src/popup/App.tsx delete mode 100644 src/popup/components/CreateWallet.tsx delete mode 100644 src/popup/components/Dashboard.tsx delete mode 100644 src/popup/components/ImportWallet.tsx delete mode 100644 src/popup/components/PendingTransactions.tsx delete mode 100644 src/popup/components/Receive.tsx delete mode 100644 src/popup/components/RegisterNametag.tsx delete mode 100644 src/popup/components/Send.tsx delete mode 100644 src/popup/components/Settings.tsx delete mode 100644 src/popup/components/UnlockWallet.tsx delete mode 100644 src/popup/hooks/useWallet.ts delete mode 100644 src/popup/main.tsx delete mode 100644 src/popup/store/index.ts delete mode 100644 src/popup/styles/index.css create mode 100644 src/sdk/context.ts create mode 100644 src/sdk/hooks/index.ts create mode 100644 src/sdk/hooks/useAssets.ts create mode 100644 src/sdk/hooks/useIdentity.ts create mode 100644 src/sdk/hooks/useTokens.ts create mode 100644 src/sdk/hooks/useTransactionHistory.ts create mode 100644 src/sdk/hooks/useTransfer.ts create mode 100644 src/sdk/hooks/useWalletStatus.ts create mode 100644 src/sdk/index.ts create mode 100644 src/sdk/queryKeys.ts create mode 100644 src/sdk/types.ts create mode 100644 src/sdk/utils/currency.ts create mode 100644 src/shared/sphere-sdk-browser.d.ts delete mode 100644 src/types/sphere-sdk-browser.d.ts diff --git a/package-lock.json b/package-lock.json index 1350f05..c5f34dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,35 +1,34 @@ { - "name": "sphere-extension", - "version": "0.1.5", + "name": "sphere-wallet", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "sphere-extension", - "version": "0.1.5", + "name": "sphere-wallet", + "version": "0.2.0", "dependencies": { - "@noble/curves": "^1.4.0", - "@noble/hashes": "^1.4.0", - "@scure/base": "^1.1.0", - "@tailwindcss/postcss": "^4.1.18", + "@noble/curves": "^1.8.2", + "@noble/hashes": "^1.7.2", + "@scure/base": "^1.2.4", + "@tanstack/react-query": "^5.90.0", "@unicitylabs/nostr-js-sdk": "^0.3.2", - "@unicitylabs/sphere-sdk": "^0.4.7", - "@unicitylabs/state-transition-sdk": "^1.6.1-rc.f37cb85", + "@unicitylabs/sphere-sdk": "^0.5.3", + "@unicitylabs/state-transition-sdk": "^1.6.1-rc", + "lucide-react": "^0.552.0", "react": "^18.3.1", - "react-dom": "^18.3.1", - "zustand": "^5.0.0" + "react-dom": "^18.3.1" }, "devDependencies": { - "@types/chrome": "^0.0.268", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.0", + "@tailwindcss/vite": "^4.1.0", + "@types/chrome": "^0.0.287", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", "archiver": "^7.0.1", - "autoprefixer": "^10.4.18", - "postcss": "^8.4.35", - "tailwindcss": "^4.0.0", - "typescript": "^5.5.0", - "vite": "^5.4.0", + "tailwindcss": "^4.1.0", + "typescript": "^5.7.3", + "vite": "^6.3.5", "vite-plugin-node-polyfills": "^0.25.0" } }, @@ -74,18 +73,6 @@ "xml2js": "^0.6.2" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -572,26 +559,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse--for-generate-function-map": { - "name": "@babel/traverse", - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", @@ -729,9 +696,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -742,13 +709,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -759,13 +726,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -776,13 +743,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -793,13 +760,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -810,13 +777,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -827,13 +794,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -844,13 +811,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -861,13 +828,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -878,13 +845,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -895,13 +862,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -912,13 +879,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -929,13 +896,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -946,13 +913,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -963,13 +930,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -980,13 +947,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -997,13 +964,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -1014,13 +981,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -1031,13 +1015,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -1048,13 +1049,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -1065,13 +1083,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -1082,13 +1100,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -1099,13 +1117,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -1116,7 +1134,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@helia/bitswap": { @@ -1608,6 +1626,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1618,6 +1637,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1628,6 +1648,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1648,12 +1669,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3385,192 +3408,218 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.2.1" } }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -3582,129 +3631,413 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "postcss": "^8.4.41", - "tailwindcss": "4.1.18" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" + "node": ">= 20" } }, - "node_modules/@types/chrome": { - "version": "0.0.268", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.268.tgz", - "integrity": "sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==", + "node_modules/@tailwindcss/vite/node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "@types/filesystem": "*", - "@types/har-format": "*" + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@types/dns-packet": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/@types/dns-packet/-/dns-packet-5.6.5.tgz", - "integrity": "sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==", - "license": "MIT", + "node_modules/@tailwindcss/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", "optional": true, - "dependencies": { - "@types/node": "*" + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.287", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.287.tgz", + "integrity": "sha512-wWhBNPNXZHwycHKNYnexUcpSbrihVZu++0rdp6GEk5ZgAglenLx+RwdEouh6FrHS0XQiOxSd62yaujM1OoQlZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/dns-packet": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/@types/dns-packet/-/dns-packet-5.6.5.tgz", + "integrity": "sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" } }, "node_modules/@types/estree": { @@ -3800,14 +4133,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "devOptional": true, + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -3883,9 +4216,9 @@ } }, "node_modules/@unicitylabs/sphere-sdk": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/@unicitylabs/sphere-sdk/-/sphere-sdk-0.4.7.tgz", - "integrity": "sha512-JIRR8nAuPY5HYiqtrgN9xkNcqddSCHK9Kyq4rtJnoFWwIZhFdAi2lAWEQ4bBLtO+MWhOoTux2PTsL2oZuIPASA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@unicitylabs/sphere-sdk/-/sphere-sdk-0.5.3.tgz", + "integrity": "sha512-Bgy+J8CC3ci4ZMxb1hFYfdDgirF3fkwSGggjPfaec0EYTNUc6DOeHp9Ayq5rHH30OOxWPaYjezUoMk9u4yWW0g==", "license": "MIT", "dependencies": { "@noble/curves": "^2.0.1", @@ -4053,19 +4386,46 @@ "optional": true }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "optional": true, "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/acme-client": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/acme-client/-/acme-client-5.4.0.tgz", @@ -4084,9 +4444,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "optional": true, "bin": { @@ -4264,66 +4624,29 @@ "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT", - "optional": true - }, - "node_modules/autoprefixer": { - "version": "10.4.24", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", - "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001766", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT", + "optional": true + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5660,6 +5983,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5785,6 +6109,7 @@ "version": "5.19.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -5861,9 +6186,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5871,32 +6196,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -6066,8 +6394,8 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=12.0.0" }, @@ -6224,20 +6552,6 @@ "node": ">= 6" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/freeport-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/freeport-promise/-/freeport-promise-2.0.0.tgz", @@ -6455,6 +6769,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, "license": "ISC" }, "node_modules/has-flag": { @@ -7601,6 +7916,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -7680,367 +7996,118 @@ "license": "MIT" }, "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/libp2p": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/libp2p/-/libp2p-3.1.3.tgz", - "integrity": "sha512-Jgl6Km1PfFTKR7krDNDxuuxQ6ya3D6VHFOi/XYJA539F62PmbxOQLd+nqbqozwB9BgJVTxaXRVmGTKo7dyrdQw==", - "license": "Apache-2.0 OR MIT", - "optional": true, - "dependencies": { - "@chainsafe/is-ip": "^2.1.0", - "@chainsafe/netmask": "^2.0.0", - "@libp2p/crypto": "^5.1.13", - "@libp2p/interface": "^3.1.0", - "@libp2p/interface-internal": "^3.0.10", - "@libp2p/logger": "^6.2.2", - "@libp2p/multistream-select": "^7.0.10", - "@libp2p/peer-collections": "^7.0.10", - "@libp2p/peer-id": "^6.0.4", - "@libp2p/peer-store": "^12.0.10", - "@libp2p/utils": "^7.0.10", - "@multiformats/dns": "^1.0.6", - "@multiformats/multiaddr": "^13.0.1", - "@multiformats/multiaddr-matcher": "^3.0.1", - "any-signal": "^4.1.1", - "datastore-core": "^11.0.1", - "interface-datastore": "^9.0.1", - "it-merge": "^3.0.12", - "it-parallel": "^3.0.13", - "main-event": "^1.0.1", - "multiformats": "^13.4.0", - "p-defer": "^4.0.1", - "p-event": "^7.0.0", - "p-retry": "^7.0.0", - "progress-events": "^1.0.1", - "race-signal": "^2.0.0", - "uint8arrays": "^5.1.0" - } - }, - "node_modules/libphonenumber-js": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.36.tgz", - "integrity": "sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==", - "license": "MIT" - }, - "node_modules/lighthouse-logger": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", - "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" - } - }, - "node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/lighthouse-logger/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "optional": true - }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "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.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "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/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "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/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "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/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "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/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "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/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=6" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", + "node_modules/libp2p": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/libp2p/-/libp2p-3.1.3.tgz", + "integrity": "sha512-Jgl6Km1PfFTKR7krDNDxuuxQ6ya3D6VHFOi/XYJA539F62PmbxOQLd+nqbqozwB9BgJVTxaXRVmGTKo7dyrdQw==", + "license": "Apache-2.0 OR MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "dependencies": { + "@chainsafe/is-ip": "^2.1.0", + "@chainsafe/netmask": "^2.0.0", + "@libp2p/crypto": "^5.1.13", + "@libp2p/interface": "^3.1.0", + "@libp2p/interface-internal": "^3.0.10", + "@libp2p/logger": "^6.2.2", + "@libp2p/multistream-select": "^7.0.10", + "@libp2p/peer-collections": "^7.0.10", + "@libp2p/peer-id": "^6.0.4", + "@libp2p/peer-store": "^12.0.10", + "@libp2p/utils": "^7.0.10", + "@multiformats/dns": "^1.0.6", + "@multiformats/multiaddr": "^13.0.1", + "@multiformats/multiaddr-matcher": "^3.0.1", + "any-signal": "^4.1.1", + "datastore-core": "^11.0.1", + "interface-datastore": "^9.0.1", + "it-merge": "^3.0.12", + "it-parallel": "^3.0.13", + "main-event": "^1.0.1", + "multiformats": "^13.4.0", + "p-defer": "^4.0.1", + "p-event": "^7.0.0", + "p-retry": "^7.0.0", + "progress-events": "^1.0.1", + "race-signal": "^2.0.0", + "uint8arrays": "^5.1.0" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", + "node_modules/libphonenumber-js": { + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.36.tgz", + "integrity": "sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==", + "license": "MIT" + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "dependencies": { + "ms": "2.0.0" } }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "optional": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8093,10 +8160,20 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.552.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz", + "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -8176,20 +8253,20 @@ "optional": true }, "node_modules/metro": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", - "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.4.tgz", + "integrity": "sha512-eBkAtcob+YmvSLL+/rsFiK8dHNfDbQA2/pi0lnxg3E6LLtUpwDfdGJ9WBWXkj0PVeOhoWQyj9Rt7s/+6k/GXuA==", "license": "MIT", "optional": true, "dependencies": { - "@babel/code-frame": "^7.24.7", + "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.3", - "@babel/types": "^7.25.2", - "accepts": "^1.3.7", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "accepts": "^2.0.0", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", @@ -8197,25 +8274,25 @@ "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", - "hermes-parser": "0.32.0", + "hermes-parser": "0.33.3", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.83.3", - "metro-cache": "0.83.3", - "metro-cache-key": "0.83.3", - "metro-config": "0.83.3", - "metro-core": "0.83.3", - "metro-file-map": "0.83.3", - "metro-resolver": "0.83.3", - "metro-runtime": "0.83.3", - "metro-source-map": "0.83.3", - "metro-symbolicate": "0.83.3", - "metro-transform-plugins": "0.83.3", - "metro-transform-worker": "0.83.3", - "mime-types": "^2.1.27", + "metro-babel-transformer": "0.83.4", + "metro-cache": "0.83.4", + "metro-cache-key": "0.83.4", + "metro-config": "0.83.4", + "metro-core": "0.83.4", + "metro-file-map": "0.83.4", + "metro-resolver": "0.83.4", + "metro-runtime": "0.83.4", + "metro-source-map": "0.83.4", + "metro-symbolicate": "0.83.4", + "metro-transform-plugins": "0.83.4", + "metro-transform-worker": "0.83.4", + "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", @@ -8231,41 +8308,58 @@ } }, "node_modules/metro-babel-transformer": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz", - "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.4.tgz", + "integrity": "sha512-xfNtsYIigybqm9xVL3ygTYYNFyYTMf2lGg/Wt+znVGtwcjXoRPG80WlL5SS09ZjYVei3MoE920i7MNr7ukSULA==", "license": "MIT", "optional": true, "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.32.0", + "hermes-parser": "0.33.3", "nullthrows": "^1.1.1" }, "engines": { "node": ">=20.19.4" } }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", + "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", + "license": "MIT", + "optional": true + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", + "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", + "license": "MIT", + "optional": true, + "dependencies": { + "hermes-estree": "0.33.3" + } + }, "node_modules/metro-cache": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", - "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.4.tgz", + "integrity": "sha512-Pm6CiksVms0cZNDDe/nFzYr1xpXzJLOSwvOjl4b3cYtXxEFllEjD6EeBgoQK5C8yk7U54PcuRaUAFSvJ+eCKbg==", "license": "MIT", "optional": true, "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", - "metro-core": "0.83.3" + "metro-core": "0.83.4" }, "engines": { "node": ">=20.19.4" } }, "node_modules/metro-cache-key": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz", - "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.4.tgz", + "integrity": "sha512-Y8E6mm1alkYIRzmfkOdrwXMzJ4HKANYiZE7J2d3iYTwmnLIQG+aoIpvla+bo6LRxH1Gm3qjEiOl+LbxvPCzIug==", "license": "MIT", "optional": true, "dependencies": { @@ -8276,19 +8370,19 @@ } }, "node_modules/metro-config": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz", - "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.4.tgz", + "integrity": "sha512-ydOgMNI9aT8l2LOTOugt1FvC7getPKG9uJo9Vclg9/RWJxbwkBF/FMBm6w5gH8NwJokSmQrbNkojXPn7nm0kGw==", "license": "MIT", "optional": true, "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", - "metro": "0.83.3", - "metro-cache": "0.83.3", - "metro-core": "0.83.3", - "metro-runtime": "0.83.3", + "metro": "0.83.4", + "metro-cache": "0.83.4", + "metro-core": "0.83.4", + "metro-runtime": "0.83.4", "yaml": "^2.6.1" }, "engines": { @@ -8296,24 +8390,24 @@ } }, "node_modules/metro-core": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz", - "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.4.tgz", + "integrity": "sha512-EE+j/imryd3og/6Ly9usku9vcTLQr2o4IDax/izsr6b0HRqZK9k6f5SZkGkOPqnsACLq6csPCx+2JsgF9DkVbw==", "license": "MIT", "optional": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", - "metro-resolver": "0.83.3" + "metro-resolver": "0.83.4" }, "engines": { "node": ">=20.19.4" } }, "node_modules/metro-file-map": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz", - "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.4.tgz", + "integrity": "sha512-RSZLpGQhW9topefjJ9dp77Ff7BP88b17sb/YjxLHC1/H0lJVYYC9Cgqua21Vxe4RUJK2z64hw72g+ySLGTCawA==", "license": "MIT", "optional": true, "dependencies": { @@ -8332,9 +8426,9 @@ } }, "node_modules/metro-minify-terser": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz", - "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.4.tgz", + "integrity": "sha512-KmZnpxfj0nPIRkbBNTc6xul5f5GPvWL5kQ1UkisB7qFkgh6+UiJG+L4ukJ2sK7St6+8Za/Cb68MUEYkUouIYcQ==", "license": "MIT", "optional": true, "dependencies": { @@ -8346,9 +8440,9 @@ } }, "node_modules/metro-resolver": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz", - "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.4.tgz", + "integrity": "sha512-drWdylyNqgdaJufz0GjU/ielv2hjcc6piegjjJwKn8l7A/72aLQpUpOHtP+GMR+kOqhSsD4MchhJ6PSANvlSEw==", "license": "MIT", "optional": true, "dependencies": { @@ -8359,9 +8453,9 @@ } }, "node_modules/metro-runtime": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz", - "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.4.tgz", + "integrity": "sha512-sWj9KN311yG22Zv0kVbAp9dorB9HtTThvQKsAn6PLxrVrz+1UBsLrQSxjE/s4PtzDi1HABC648jo4K9Euz/5jw==", "license": "MIT", "optional": true, "dependencies": { @@ -8373,20 +8467,19 @@ } }, "node_modules/metro-source-map": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz", - "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.4.tgz", + "integrity": "sha512-pPbmQwS0zgU+/0u5KPkuvlsQP0V+WYQ9qNshqupIL720QRH0vS3QR25IVVtbunofEDJchI11Q4QtIbmUyhpOBw==", "license": "MIT", "optional": true, "dependencies": { - "@babel/traverse": "^7.25.3", - "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", - "@babel/types": "^7.25.2", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-symbolicate": "0.83.3", + "metro-symbolicate": "0.83.4", "nullthrows": "^1.1.1", - "ob1": "0.83.3", + "ob1": "0.83.4", "source-map": "^0.5.6", "vlq": "^1.0.0" }, @@ -8395,15 +8488,15 @@ } }, "node_modules/metro-symbolicate": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", - "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.4.tgz", + "integrity": "sha512-clyWAXDgkDHPwvldl95pcLTrJIqUj9GbZayL8tfeUs69ilsIUBpVym2lRd/8l3/8PIHCInxL868NvD2Y7OqKXg==", "license": "MIT", "optional": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-source-map": "0.83.3", + "metro-source-map": "0.83.4", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" @@ -8416,16 +8509,16 @@ } }, "node_modules/metro-transform-plugins": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", - "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.4.tgz", + "integrity": "sha512-c0ROVcyvdaGPUFIg2N5nEQF4xbsqB2p1PPPhVvK1d/Y7ZhBAFiwQ75so0SJok32q+I++lc/hq7IdPCp2frPGQg==", "license": "MIT", "optional": true, "dependencies": { "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.3", + "@babel/generator": "^7.29.1", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" }, @@ -8434,24 +8527,24 @@ } }, "node_modules/metro-transform-worker": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz", - "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.4.tgz", + "integrity": "sha512-6I81IZLeU/0ww7OBgCPALFl0OE0FQwvIuKCtuViSiKufmislF7kVr7IHH9GYtQuZcnualQ82gYeQ11KzZQTouw==", "license": "MIT", "optional": true, "dependencies": { "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/types": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", - "metro": "0.83.3", - "metro-babel-transformer": "0.83.3", - "metro-cache": "0.83.3", - "metro-cache-key": "0.83.3", - "metro-minify-terser": "0.83.3", - "metro-source-map": "0.83.3", - "metro-transform-plugins": "0.83.3", + "metro": "0.83.4", + "metro-babel-transformer": "0.83.4", + "metro-cache": "0.83.4", + "metro-cache-key": "0.83.4", + "metro-minify-terser": "0.83.4", + "metro-source-map": "0.83.4", + "metro-transform-plugins": "0.83.4", "nullthrows": "^1.1.1" }, "engines": { @@ -8465,6 +8558,50 @@ "license": "MIT", "optional": true }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", + "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", + "license": "MIT", + "optional": true + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", + "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", + "license": "MIT", + "optional": true, + "dependencies": { + "hermes-estree": "0.33.3" + } + }, + "node_modules/metro/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/metro/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/metro/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -8712,9 +8849,9 @@ "optional": true }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "optional": true, "engines": { @@ -8892,9 +9029,9 @@ "optional": true }, "node_modules/ob1": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz", - "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==", + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.4.tgz", + "integrity": "sha512-9JiflaRKCkxKzH8uuZlax72cHzZ8iFLsNIORFOAKDgZUOfvfwYWOVS0ezGLzPp/yEhVktD+PTTImC0AAehSOBw==", "license": "MIT", "optional": true, "dependencies": { @@ -9273,6 +9410,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { @@ -9326,6 +9464,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -9341,7 +9480,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9351,17 +9489,11 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/postcss/node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -9878,9 +10010,9 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "optional": true, "dependencies": { @@ -10450,6 +10582,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -10774,12 +10907,14 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10936,9 +11071,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "optional": true, "dependencies": { @@ -11030,8 +11165,8 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" @@ -11369,22 +11504,25 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -11393,19 +11531,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -11426,6 +11570,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -11880,35 +12030,6 @@ "engines": { "node": ">= 14" } - }, - "node_modules/zustand": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", - "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index f4274d2..0c8ccd1 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,44 @@ { - "name": "sphere-extension", - "version": "0.1.6", + "name": "sphere-wallet", + "version": "0.2.0", "private": true, "type": "module", "scripts": { "dev": "node scripts/build.js && vite build --watch", "build": "tsc --noEmit && node scripts/build.js", "build:fast": "node scripts/build.js", - "lint": "eslint src --ext .ts,.tsx", + "lint": "tsc --noEmit", "package": "node scripts/build.js && node scripts/package.js" }, "dependencies": { - "@noble/curves": "^1.4.0", - "@noble/hashes": "^1.4.0", - "@scure/base": "^1.1.0", - "@tailwindcss/postcss": "^4.1.18", + "@noble/curves": "^1.8.2", + "@noble/hashes": "^1.7.2", + "@scure/base": "^1.2.4", + "@tanstack/react-query": "^5.90.0", "@unicitylabs/nostr-js-sdk": "^0.3.2", - "@unicitylabs/sphere-sdk": "^0.4.7", - "@unicitylabs/state-transition-sdk": "^1.6.1-rc.f37cb85", + "@unicitylabs/sphere-sdk": "^0.5.3", + "@unicitylabs/state-transition-sdk": "^1.6.1-rc", + "lucide-react": "^0.552.0", "react": "^18.3.1", - "react-dom": "^18.3.1", - "zustand": "^5.0.0" + "react-dom": "^18.3.1" }, "devDependencies": { - "@types/chrome": "^0.0.268", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.0", + "@types/chrome": "^0.0.287", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "@tailwindcss/vite": "^4.1.0", "archiver": "^7.0.1", - "autoprefixer": "^10.4.18", - "postcss": "^8.4.35", - "tailwindcss": "^4.0.0", - "typescript": "^5.5.0", - "vite": "^5.4.0", + "tailwindcss": "^4.1.0", + "typescript": "^5.7.3", + "vite": "^6.3.5", "vite-plugin-node-polyfills": "^0.25.0" + }, + "overrides": { + "@unicitylabs/sphere-sdk": { + "helia": "-", + "@helia/ipns": "-", + "@helia/json": "-" + } } } diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index a34a3d5..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - plugins: { - '@tailwindcss/postcss': {}, - }, -}; diff --git a/public/popup.html b/public/popup.html index 067a864..d325f00 100644 --- a/public/popup.html +++ b/public/popup.html @@ -15,6 +15,6 @@
- + diff --git a/scripts/build.js b/scripts/build.js index 0cef60a..94132f7 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -87,18 +87,17 @@ async function buildExtension() { resolve: { preserveSymlinks: true, alias: { - '@/background': resolve(root, 'src/background'), - '@/content': resolve(root, 'src/content'), - '@/inject': resolve(root, 'src/inject'), - '@/popup': resolve(root, 'src/popup'), '@/shared': resolve(root, 'src/shared'), + '@/sdk': resolve(root, 'src/sdk'), + '@/components': resolve(root, 'src/components'), + '@/platform': resolve(root, 'src/platform'), }, }, build: { outDir: resolve(root, 'dist'), emptyOutDir: false, lib: { - entry: resolve(root, 'src/background/index.ts'), + entry: resolve(root, 'src/platform/extension/background/index.ts'), name: 'background', formats: ['es'], fileName: () => 'background.js', @@ -122,18 +121,17 @@ async function buildExtension() { resolve: { preserveSymlinks: true, alias: { - '@/background': resolve(root, 'src/background'), - '@/content': resolve(root, 'src/content'), - '@/inject': resolve(root, 'src/inject'), - '@/popup': resolve(root, 'src/popup'), '@/shared': resolve(root, 'src/shared'), + '@/sdk': resolve(root, 'src/sdk'), + '@/components': resolve(root, 'src/components'), + '@/platform': resolve(root, 'src/platform'), }, }, build: { outDir: resolve(root, 'dist'), emptyOutDir: false, lib: { - entry: resolve(root, 'src/content/index.ts'), + entry: resolve(root, 'src/platform/extension/content/index.ts'), name: 'content', formats: ['iife'], fileName: () => 'content.js', @@ -157,18 +155,17 @@ async function buildExtension() { resolve: { preserveSymlinks: true, alias: { - '@/background': resolve(root, 'src/background'), - '@/content': resolve(root, 'src/content'), - '@/inject': resolve(root, 'src/inject'), - '@/popup': resolve(root, 'src/popup'), '@/shared': resolve(root, 'src/shared'), + '@/sdk': resolve(root, 'src/sdk'), + '@/components': resolve(root, 'src/components'), + '@/platform': resolve(root, 'src/platform'), }, }, build: { outDir: resolve(root, 'dist'), emptyOutDir: false, lib: { - entry: resolve(root, 'src/inject/index.ts'), + entry: resolve(root, 'src/platform/extension/inject/index.ts'), name: 'inject', formats: ['iife'], fileName: () => 'inject.js', diff --git a/src/background/nostr-service.ts b/src/background/nostr-service.ts deleted file mode 100644 index c5bb993..0000000 --- a/src/background/nostr-service.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * NostrService - DEPRECATED - * - * NOSTR operations are now handled by Sphere SDK's TransportProvider. - * This file is kept as a thin compatibility shim for any remaining callers. - * It delegates to the WalletManager's Sphere instance. - */ - -// This module is intentionally empty. -// All NOSTR operations are now handled by sphere-sdk's NostrTransportProvider -// via WalletManager.createSphereFromMnemonic(). -// -// If you need NOSTR functionality, use walletManager methods: -// - walletManager.resolveNametag() for nametag resolution -// - walletManager.registerNametag() for nametag registration -// - sphere.payments.send() for token transfers (SDK handles NOSTR delivery) -// - sphere.on('transfer:incoming', handler) for incoming transfers - -export {}; diff --git a/src/background/providers/index.ts b/src/background/providers/index.ts deleted file mode 100644 index f901875..0000000 --- a/src/background/providers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - ChromeStorageProvider, - createChromeStorageProvider, -} from './chrome-storage-provider'; -export type { ChromeStorageProviderConfig } from './chrome-storage-provider'; diff --git a/src/background/token-transfer-service.ts b/src/background/token-transfer-service.ts deleted file mode 100644 index 24af4c8..0000000 --- a/src/background/token-transfer-service.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * TokenTransferService - DEPRECATED - * - * Incoming token transfers are now handled by Sphere SDK's PaymentsModule. - * The SDK automatically accepts incoming transfers via the TransportProvider - * and updates balances. The WalletManager listens for 'transfer:incoming' - * events to update UI (badge, notifications). - * - * This file is kept empty for reference. - */ - -export {}; diff --git a/src/components/ui/AlertMessage.tsx b/src/components/ui/AlertMessage.tsx new file mode 100644 index 0000000..f11b77e --- /dev/null +++ b/src/components/ui/AlertMessage.tsx @@ -0,0 +1,107 @@ +import { XCircle, AlertTriangle, CheckCircle, Info, X } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import type { ReactNode } from 'react'; + +type AlertVariant = 'error' | 'warning' | 'success' | 'info'; + +interface AlertMessageProps { + variant: AlertVariant; + children: ReactNode; + /** Optional title for multi-line alerts */ + title?: string; + /** Show dismiss button */ + onDismiss?: () => void; + /** Custom icon override */ + icon?: LucideIcon; +} + +const variantConfig: Record = { + error: { + icon: XCircle, + bgClass: 'bg-red-500/10', + borderClass: 'border-red-500/20', + textClass: 'text-red-600 dark:text-red-400', + iconClass: 'text-red-500 dark:text-red-400', + }, + warning: { + icon: AlertTriangle, + bgClass: 'bg-amber-500/10', + borderClass: 'border-amber-500/20', + textClass: 'text-amber-600 dark:text-amber-400', + iconClass: 'text-amber-500 dark:text-amber-400', + }, + success: { + icon: CheckCircle, + bgClass: 'bg-green-500/10', + borderClass: 'border-green-500/20', + textClass: 'text-green-600 dark:text-green-400', + iconClass: 'text-green-500 dark:text-green-400', + }, + info: { + icon: Info, + bgClass: 'bg-blue-500/10', + borderClass: 'border-blue-500/20', + textClass: 'text-blue-600 dark:text-blue-400', + iconClass: 'text-blue-500 dark:text-blue-400', + }, +}; + +export function AlertMessage({ + variant, + children, + title, + onDismiss, + icon: CustomIcon, +}: AlertMessageProps) { + const config = variantConfig[variant]; + const Icon = CustomIcon || config.icon; + + return ( +
+ {title ? ( + // Layout with title: icon centered with title, description below +
+
+ +

{title}

+ {onDismiss && ( + + )} +
+
+ {children} +
+
+ ) : ( + // Simple layout without title +
+ +
+ {children} +
+ {onDismiss && ( + + )} +
+ )} +
+ ); +} diff --git a/src/components/ui/BaseModal.tsx b/src/components/ui/BaseModal.tsx new file mode 100644 index 0000000..1e9129b --- /dev/null +++ b/src/components/ui/BaseModal.tsx @@ -0,0 +1,79 @@ +import { useEffect, useRef } from 'react'; +import type { ReactNode } from 'react'; + +type ModalSize = 'sm' | 'md' | 'lg'; + +interface BaseModalProps { + isOpen: boolean; + onClose: () => void; + children: ReactNode; + /** Modal max-width: sm (384px), md (448px), lg (512px) */ + size?: ModalSize; + /** Show decorative background orbs */ + showOrbs?: boolean; + /** Additional className for the modal container */ + className?: string; +} + +const sizeClasses: Record = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', +}; + +export function BaseModal({ + isOpen, + onClose, + children, + size = 'md', + showOrbs = true, + className = '', +}: BaseModalProps) { + const backdropRef = useRef(null); + const panelRef = useRef(null); + + // Handle enter/exit transitions via CSS classes + useEffect(() => { + if (isOpen) { + // Trigger enter transition on next frame so the initial state is rendered first + requestAnimationFrame(() => { + backdropRef.current?.classList.add('opacity-100'); + backdropRef.current?.classList.remove('opacity-0'); + panelRef.current?.classList.add('opacity-100', 'scale-100', 'translate-y-0'); + panelRef.current?.classList.remove('opacity-0', 'scale-95', 'translate-y-4'); + }); + } + }, [isOpen]); + + if (!isOpen) return null; + + return ( + <> + {/* Backdrop */} +
+ + {/* Modal Container */} +
+
e.stopPropagation()} + className={`relative w-full ${sizeClasses[size]} max-h-[70dvh] sm:max-h-[600px] bg-white dark:bg-[#111] border border-neutral-200 dark:border-white/10 rounded-3xl shadow-2xl pointer-events-auto flex flex-col overflow-hidden opacity-0 scale-95 translate-y-4 transition-all duration-300 ease-out ${className}`} + > + {/* Background Orbs */} + {showOrbs && ( + <> +
+
+ + )} + + {children} +
+
+ + ); +} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..99975d0 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,99 @@ +import { Loader2 } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import type { ButtonHTMLAttributes } from 'react'; + +type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success'; +type ButtonSize = 'sm' | 'md' | 'lg'; +type IconPosition = 'left' | 'right'; + +interface ButtonProps extends Omit, 'children'> { + variant?: ButtonVariant; + size?: ButtonSize; + /** Icon */ + icon?: LucideIcon; + /** Icon position */ + iconPosition?: IconPosition; + /** Show loading spinner */ + loading?: boolean; + /** Loading text (defaults to "Loading...") */ + loadingText?: string; + /** Button content */ + children: React.ReactNode; + /** Full width */ + fullWidth?: boolean; +} + +const variantClasses: Record = { + primary: 'bg-orange-500 hover:bg-orange-600 text-white shadow-lg shadow-orange-500/25', + secondary: 'bg-neutral-100 dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700 text-neutral-700 dark:text-white', + danger: 'bg-red-500 hover:bg-red-600 text-white shadow-lg shadow-red-500/25', + success: 'bg-emerald-500 hover:bg-emerald-600 text-white shadow-lg shadow-emerald-500/25', +}; + +const sizeClasses: Record = { + sm: 'py-2 px-4 text-sm rounded-lg', + md: 'py-3 px-6 text-sm rounded-xl', + lg: 'py-4 px-8 text-base rounded-xl', +}; + +export function Button({ + variant = 'primary', + size = 'md', + icon: Icon, + iconPosition = 'left', + loading = false, + loadingText = 'Loading...', + children, + fullWidth = false, + disabled, + className = '', + ...props +}: ButtonProps) { + const isDisabled = disabled || loading; + + return ( + + ); +} + +// Convenience exports for common variants +export function PrimaryButton(props: Omit) { + return + ); +} diff --git a/src/components/ui/ModalHeader.tsx b/src/components/ui/ModalHeader.tsx new file mode 100644 index 0000000..6b14e05 --- /dev/null +++ b/src/components/ui/ModalHeader.tsx @@ -0,0 +1,72 @@ +import { X } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import type { ReactNode } from 'react'; + +type IconVariant = 'gradient' | 'neutral'; + +interface ModalHeaderProps { + title: string; + onClose: () => void; + /** Icon to display in badge */ + icon?: LucideIcon; + /** Icon badge style: 'gradient' (orange) or 'neutral' (grey) */ + iconVariant?: IconVariant; + /** Subtitle text or ReactNode below title */ + subtitle?: ReactNode; + /** Disable close button */ + closeDisabled?: boolean; +} + +const iconVariantClasses: Record = { + gradient: { + badge: 'bg-linear-to-br from-orange-500 to-orange-600 shadow-lg shadow-orange-500/30', + icon: 'text-white', + }, + neutral: { + badge: 'bg-neutral-100 dark:bg-neutral-800', + icon: 'text-neutral-600 dark:text-neutral-400', + }, +}; + +export function ModalHeader({ + title, + onClose, + icon: Icon, + iconVariant = 'gradient', + subtitle, + closeDisabled = false, +}: ModalHeaderProps) { + const iconStyles = iconVariantClasses[iconVariant]; + + return ( +
+
+ {Icon && ( +
+ +
+ )} +
+

{title}

+ {subtitle && ( +
{subtitle}
+ )} +
+
+ + +
+ ); +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 0000000..39df4fc --- /dev/null +++ b/src/components/ui/index.ts @@ -0,0 +1,11 @@ +// Modal components +export { BaseModal } from './BaseModal'; +export { ModalHeader } from './ModalHeader'; + +// Feedback components +export { AlertMessage } from './AlertMessage'; +export { EmptyState } from './EmptyState'; + +// Button components +export { Button, PrimaryButton, SecondaryButton, DangerButton, SuccessButton } from './Button'; +export { MenuButton } from './MenuButton'; diff --git a/src/components/wallet/L3WalletView.tsx b/src/components/wallet/L3WalletView.tsx new file mode 100644 index 0000000..3b567f6 --- /dev/null +++ b/src/components/wallet/L3WalletView.tsx @@ -0,0 +1,467 @@ +import { Plus, ArrowUpRight, ArrowDownUp, Sparkles, Loader2, Coins, Layers, Eye, EyeOff, Wifi } from 'lucide-react'; +import { AssetRow } from '@/components/wallet/shared/AssetRow'; +import { TokenRow } from '@/components/wallet/shared/TokenRow'; +import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { useIdentity, useAssets, useTokens, useSphereContext } from '@/sdk'; +import { SendModal } from './modals/SendModal'; +import { SwapModal } from './modals/SwapModal'; +import { PaymentRequestsModal } from './modals/PaymentRequestModal'; +import type { IncomingPaymentRequest } from './modals/PaymentRequestModal'; +import { TopUpModal } from './modals/TopUpModal'; +import { SeedPhraseModal } from './modals/SeedPhraseModal'; +import { TransactionHistoryModal } from './modals/TransactionHistoryModal'; +import { SettingsModal } from './modals/SettingsModal'; +import { BackupWalletModal, LogoutConfirmModal } from '@/components/wallet/shared'; +import { SaveWalletModal } from '@/components/wallet/shared/SaveWalletModal'; + +type Tab = 'assets' | 'tokens'; + +// Static balance display (replaces Framer Motion animated numbers) +function BalanceDisplay({ + totalValue, + showBalances, + onToggle, + isLoading, +}: { + totalValue: number; + showBalances: boolean; + onToggle: () => void; + isLoading?: boolean; +}) { + const formatted = `$${totalValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + + return ( +
+

+ {isLoading ? ( + + + + ) : showBalances ? ( + {formatted} + ) : ( + '••••••' + )} +

+ +
+ ); +} + +// Inline status line showing current wallet activity +function WalletStatusLine({ + isLoadingAssets, + pendingCount, +}: { + isLoadingAssets: boolean; + pendingCount: number; +}) { + const items: { label: string; spinning?: boolean }[] = []; + + if (isLoadingAssets) items.push({ label: 'Loading assets', spinning: true }); + if (pendingCount > 0) items.push({ label: `${pendingCount} pending transfer${pendingCount > 1 ? 's' : ''}` }); + + if (items.length === 0) return null; + + // Show the first (most relevant) status item + const current = items[0]; + + return ( +
+ {current.spinning ? ( + + ) : ( + + )} + {current.label}... +
+ ); +} + +interface L3WalletViewProps { + showBalances: boolean; + setShowBalances: (value: boolean) => void; + isHistoryOpen: boolean; + setIsHistoryOpen: (value: boolean) => void; + isRequestsOpen: boolean; + setIsRequestsOpen: (value: boolean) => void; + isSettingsOpen: boolean; + setIsSettingsOpen: (value: boolean) => void; + isL1WalletOpen: boolean; + setIsL1WalletOpen: (value: boolean) => void; +} + +export function L3WalletView({ + showBalances, + setShowBalances, + isHistoryOpen, + setIsHistoryOpen, + isRequestsOpen, + setIsRequestsOpen, + isSettingsOpen, + setIsSettingsOpen, + setIsL1WalletOpen, +}: L3WalletViewProps) { + // SDK hooks + const { identity, isLoading: isLoadingIdentity } = useIdentity(); + const { assets: sdkAssets, isLoading: isLoadingAssets } = useAssets(); + const { tokens: sdkTokens, pendingTokens } = useTokens(); + const { deleteWallet, getMnemonic, exportWallet } = useSphereContext(); + + const assets = sdkAssets; + const tokens = sdkTokens; + const sendableTokens = useMemo(() => tokens.filter((t: any) => t.coinId !== 'NAMETAG'), [tokens]); + + const [activeTab, setActiveTab] = useState('assets'); + const [isSendModalOpen, setIsSendModalOpen] = useState(false); + const [isSwapModalOpen, setIsSwapModalOpen] = useState(false); + const [isSeedPhraseOpen, setIsSeedPhraseOpen] = useState(false); + const [seedPhrase, setSeedPhrase] = useState([]); + const [isTopUpModalOpen, setIsTopUpModalOpen] = useState(false); + + // Track previous token/asset IDs to detect truly new items + const prevTokenIdsRef = useRef>(new Set()); + const prevAssetCoinIdsRef = useRef>(new Set()); + const isFirstLoadRef = useRef(true); + + // Compute new token IDs by comparing with previous snapshot + const newTokenIds = useMemo(() => { + if (isFirstLoadRef.current) { + return new Set(); // First load - no highlights + } + const newIds = new Set(); + tokens.filter((t: any) => t.coinId !== 'NAMETAG').forEach((token: any) => { + if (!prevTokenIdsRef.current.has(token.id)) { + newIds.add(token.id); + } + }); + return newIds; + }, [tokens]); + + // Compute new asset IDs by comparing with previous snapshot + const newAssetCoinIds = useMemo(() => { + if (isFirstLoadRef.current) { + return new Set(); // First load - no highlights + } + const newIds = new Set(); + assets.forEach((asset: any) => { + if (!prevAssetCoinIdsRef.current.has(asset.coinId)) { + newIds.add(asset.coinId); + } + }); + return newIds; + }, [assets]); + + // Update previous snapshots after render (for next comparison) + useEffect(() => { + const currentIds = new Set(tokens.filter((t: any) => t.coinId !== 'NAMETAG').map((t: any) => t.id)); + prevTokenIdsRef.current = currentIds; + isFirstLoadRef.current = false; + }, [tokens]); + + useEffect(() => { + const currentIds = new Set(assets.map((a: any) => a.coinId)); + prevAssetCoinIdsRef.current = currentIds; + }, [assets]); + + // New modal states + const [isBackupOpen, setIsBackupOpen] = useState(false); + const [isLogoutConfirmOpen, setIsLogoutConfirmOpen] = useState(false); + const [isSaveWalletOpen, setIsSaveWalletOpen] = useState(false); + + // Payment requests (populated via wallet update events) + const [paymentRequests, setPaymentRequests] = useState([]); + + // Stable callback for toggling balance visibility + const handleToggleBalances = useCallback(() => { + setShowBalances(!showBalances); + }, [showBalances, setShowBalances]); + + const totalValue = useMemo(() => { + // Sum up L3 asset values (using SDK-provided fiat values for accuracy) + const l3Value = sdkAssets.reduce((sum: number, asset: any) => sum + (asset.fiatValueUsd ?? 0), 0); + return l3Value; + }, [sdkAssets]); + + const handleShowSeedPhrase = async () => { + try { + const mnemonic = await getMnemonic(); + if (mnemonic) { + setSeedPhrase(mnemonic.split(' ')); + setIsSeedPhraseOpen(true); + } else { + alert("Recovery phrase not available.\n\nThis wallet was imported from a file that doesn't contain a mnemonic phrase."); + } + } catch (err) { + console.error('Failed to get mnemonic:', err); + } + }; + + // Handle export wallet file + const handleExportWalletFile = () => { + setIsSaveWalletOpen(true); + }; + + // Handle save wallet + const handleSaveWallet = async (filename: string) => { + try { + const jsonData = await exportWallet(); + const blob = new Blob([jsonData], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename.endsWith('.json') ? filename : `${filename}.json`; + a.click(); + URL.revokeObjectURL(url); + setIsSaveWalletOpen(false); + } catch (err) { + console.error('Failed to save wallet:', err); + } + }; + + // Handle logout + const [isLoggingOut, setIsLoggingOut] = useState(false); + const handleLogout = async () => { + try { + setIsLoggingOut(true); + await deleteWallet(); + // In extension context, this will trigger state change via SphereProvider + } catch (err) { + console.error('Failed to logout:', err); + setIsLoggingOut(false); + } + }; + + // Handle backup and logout + const handleBackupAndLogout = () => { + setIsLogoutConfirmOpen(false); + setIsBackupOpen(true); + }; + + if (isLoadingIdentity) { + return ( +
+ + +
+ ); + } + + if (!identity) { + return ( +
+

No identity found. Please create a wallet.

+
+ ); + } + + return ( +
+ {/* Main Balance - Centered with Eye Toggle */} +
+
+ + +
+ + {/* Actions - Speed focused */} +
+ + + + + +
+ +
+ +
+
+ + +
+
+ + {/* Assets List */} +
+
+
+ +

Network Assets

+
+
+ +
+ {isLoadingAssets ? ( +
+ +
+ ) : ( + <> + {/* ASSETS VIEW */} + {activeTab === 'assets' && ( +
+ {assets.length === 0 ? ( + + ) : ( + <> + {/* L3 Assets */} + {assets.map((asset: any, index: number) => ( + + ))} + + )} +
+ )} + + {/* TOKENS VIEW */} + {activeTab === 'tokens' && ( +
+ {tokens.filter((t: any) => t.coinId !== 'NAMETAG').length === 0 ? ( + + ) : ( + tokens + .filter((t: any) => t.coinId !== 'NAMETAG') + .sort((a: any, b: any) => b.createdAt - a.createdAt) + .map((token: any, index: number) => ( + + )) + )} +
+ )} + + )} +
+
+ + {/* Modals */} + setIsTopUpModalOpen(false)} /> + setIsSendModalOpen(false)} /> + setIsSwapModalOpen(false)} /> + setIsRequestsOpen(false)} + requests={paymentRequests} + pendingCount={paymentRequests.filter(r => r.status === 'pending').length} + reject={async (req) => { setPaymentRequests(prev => prev.filter(r => r.id !== req.id)); }} + paid={async (req) => { setPaymentRequests(prev => prev.map(r => r.id === req.id ? { ...r, status: 'paid' as any } : r)); }} + clearProcessed={() => { setPaymentRequests(prev => prev.filter(r => r.status === 'pending')); }} + /> + setIsSeedPhraseOpen(false)} + seedPhrase={seedPhrase} + /> + setIsHistoryOpen(false)} /> + setIsSettingsOpen(false)} + onBackupWallet={() => setIsBackupOpen(true)} + onLogout={() => setIsLogoutConfirmOpen(true)} + /> + setIsBackupOpen(false)} + onExportWalletFile={handleExportWalletFile} + onShowRecoveryPhrase={handleShowSeedPhrase} + hasMnemonic={true} + /> + setIsLogoutConfirmOpen(false)} + onBackupAndLogout={handleBackupAndLogout} + onLogoutWithoutBackup={handleLogout} + isLoggingOut={isLoggingOut} + /> + setIsSaveWalletOpen(false)} + hasMnemonic={true} + /> + +
+ ); +} + +// Helper Component +function EmptyState({ text }: { text?: string }) { + return ( +
+
+ +
+
+ {text || <>Wallet is empty.
Mint some tokens to start!} +
+
+ ); +} diff --git a/src/components/wallet/UnlockWallet.tsx b/src/components/wallet/UnlockWallet.tsx new file mode 100644 index 0000000..b14fcd1 --- /dev/null +++ b/src/components/wallet/UnlockWallet.tsx @@ -0,0 +1,96 @@ +import { useState } from 'react'; +import { Loader2, Lock, AlertCircle } from 'lucide-react'; + +interface UnlockWalletProps { + onUnlock: (password: string) => Promise; +} + +export function UnlockWallet({ onUnlock }: UnlockWalletProps) { + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!password.trim() || isLoading) return; + + setError(''); + setIsLoading(true); + + try { + await onUnlock(password); + } catch (err) { + setError((err as Error).message || 'Failed to unlock wallet'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Background Gradients */} +
+
+ +
+ {/* Lock Icon */} +
+
+
+ +
+
+ + {/* Title */} +
+

Unlock Wallet

+

+ Enter your password to continue +

+
+ + {/* Form */} +
+
+ { + setPassword(e.target.value); + if (error) setError(''); + }} + placeholder="Password" + required + autoFocus + disabled={isLoading} + className="w-full px-4 py-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-orange-500 disabled:opacity-50 transition-colors" + /> +
+ + {/* Error Display */} + {error && ( +
+ +

{error}

+
+ )} + + +
+
+
+ ); +} diff --git a/src/components/wallet/WalletPanel.tsx b/src/components/wallet/WalletPanel.tsx new file mode 100644 index 0000000..cd042fe --- /dev/null +++ b/src/components/wallet/WalletPanel.tsx @@ -0,0 +1,183 @@ +import { Wallet, Clock, Bell, MoreVertical, Tag, Loader2, RefreshCw } from 'lucide-react'; +import { useState } from 'react'; +import { L3WalletView } from './L3WalletView'; +import { useIdentity, useWalletStatus, useSphereContext } from '@/sdk'; +import { AddressSelector, RegisterNametagModal } from '@/components/wallet/shared'; +import { CreateWalletFlow } from './onboarding/CreateWalletFlow'; + +const PANEL_SHELL = "bg-white dark:bg-neutral-900 backdrop-blur-xl rounded-none border-0 overflow-hidden h-full relative flex flex-col transition-all duration-500"; + +export function WalletPanel() { + const [showBalances, setShowBalances] = useState(true); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [isRequestsOpen, setIsRequestsOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isL1WalletOpen, setIsL1WalletOpen] = useState(false); + const [isNametagModalOpen, setIsNametagModalOpen] = useState(false); + const { isLoading: isWalletLoading, walletExists, error: walletError } = useWalletStatus(); + const { identity, nametag, isLoading: isLoadingIdentity } = useIdentity(); + const { isLoading: _contextLoading } = useSphereContext(); + + // Initialization error (e.g. IndexedDB timeout after retry) + if (walletError) { + return ( +
+
+
+ +
+
+

Initialization error

+

Please reload the extension

+
+ +
+
+ ); + } + + // Wallet system still initializing + if (isWalletLoading) { + return ( +
+
+
+
+
+
+
+ +
+
+

+ Initializing wallet... +

+
+
+ ); + } + + // No wallet — show onboarding flow inside the panel + if (!walletExists) { + return ( +
+
+
+
+ +
+
+ ); + } + + // Wallet exists but identity still loading + if (isLoadingIdentity || !identity) { + return ( +
+
+
+
+
+
+
+ +
+
+

+ Loading identity... +

+
+
+ ); + } + + return ( +
+ + {/* Background Gradients - Orange theme */} +
+
+ + {/* TOP BAR: Title & Actions */} +
+
+
+
+
+
+ +
+
+ +
+
+ Wallet + {!nametag && ( + + )} +
+ +
+
+ +
+ + + +
+
+
+ + {/* CONTENT AREA - L3 Only */} +
+ +
+ + setIsNametagModalOpen(false)} + /> +
+ ); +} diff --git a/src/components/wallet/modals/LookupModal.tsx b/src/components/wallet/modals/LookupModal.tsx new file mode 100644 index 0000000..281aacf --- /dev/null +++ b/src/components/wallet/modals/LookupModal.tsx @@ -0,0 +1,191 @@ +import { useState, useCallback, useEffect } from 'react'; +import { Key, Search, Loader2, Copy, Check } from 'lucide-react'; +import { BaseModal, ModalHeader } from '@/components/ui'; +import { useSphereContext, useIdentity } from '@/sdk'; + +interface ResolvedInfo { + nametag?: string; + transportPubkey?: string; + chainPubkey?: string; + l1Address?: string; + directAddress?: string; + proxyAddress?: string; +} + +interface LookupModalProps { + isOpen: boolean; + onClose: () => void; +} + +function CopyableField({ label, value, prefix, copied, onCopy }: { + label: string; + value: string; + prefix?: string; + copied: boolean; + onCopy: () => void; +}) { + const display = prefix ? `${prefix}${value}` : value; + return ( +
+ {label} + {display} + +
+ ); +} + +export function LookupModal({ isOpen, onClose }: LookupModalProps) { + const ctx = useSphereContext(); + const { nametag, directAddress, l1Address } = useIdentity(); + const [query, setQuery] = useState(''); + const [result, setResult] = useState(null); + const [myInfo, setMyInfo] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [copied, setCopied] = useState(false); + + // Auto-resolve own keys when modal opens + useEffect(() => { + if (isOpen && ctx.resolve && directAddress) { + ctx.resolve(directAddress).then((info: ResolvedInfo | null) => { + if (info) setMyInfo(info); + }).catch(() => { /* ignore */ }); + setQuery(''); + setResult(null); + setError(null); + } + }, [isOpen, ctx, directAddress]); + + const handleLookup = useCallback(async () => { + const input = query.trim(); + if (!input || !ctx.resolve) return; + + setIsLoading(true); + setError(null); + setResult(null); + + try { + const info = await ctx.resolve(input); + if (!info) { + setError(`Not found: "${input}"`); + } else { + setResult(info as ResolvedInfo); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Lookup failed'); + } finally { + setIsLoading(false); + } + }, [query, ctx]); + + const handleCopy = useCallback(async (value: string, field: string) => { + try { + await navigator.clipboard.writeText(value); + setCopied(field); + setTimeout(() => setCopied(false), 1500); + } catch { /* ignore */ } + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleLookup(); + } + }; + + const toFields = (info: ResolvedInfo, prefix: string) => [ + { label: 'Nametag', value: info.nametag, key: `${prefix}-nametag`, displayPrefix: '@' }, + { label: 'Direct Address', value: info.directAddress, key: `${prefix}-direct` }, + { label: 'Proxy Address', value: info.proxyAddress, key: `${prefix}-proxy` }, + { label: 'L1 Address', value: info.l1Address, key: `${prefix}-l1` }, + { label: 'Chain Pubkey', value: info.chainPubkey, key: `${prefix}-chain` }, + { label: 'Transport Pubkey', value: info.transportPubkey, key: `${prefix}-transport` }, + ].filter((f): f is { label: string; value: string; key: string; displayPrefix?: string } => !!f.value); + + // Fall back to identity data if resolve didn't return full info + const myFields = myInfo ? toFields(myInfo, 'my') : [ + nametag && { label: 'Nametag', value: nametag, key: 'my-nametag', displayPrefix: '@' }, + directAddress && { label: 'Direct Address', value: directAddress, key: 'my-direct' }, + l1Address && { label: 'L1 Address', value: l1Address, key: 'my-l1' }, + ].filter((f): f is { label: string; value: string; key: string; displayPrefix?: string } => !!f); + + const lookupFields = result ? toFields(result, 'lookup') : []; + + return ( + + + +
+ {/* My Keys */} + {myFields.length > 0 && ( +
+
+ {myFields.map(({ label, value, key, displayPrefix }) => ( + handleCopy(displayPrefix ? `${displayPrefix}${value}` : value, key)} + /> + ))} +
+
+ )} + + {/* Lookup */} +
+

+ Lookup +

+ +
+
+ + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="@nametag, DIRECT://..., alpha1..." + className="w-full pl-8 pr-3 py-2.5 text-sm bg-neutral-100 dark:bg-neutral-800/50 text-neutral-900 dark:text-white placeholder-neutral-400 rounded-xl border border-neutral-200 dark:border-neutral-700/50 focus:outline-none focus:border-orange-500 transition-colors" + /> +
+ +
+ + {error && ( +

{error}

+ )} + + {lookupFields.length > 0 && ( +
+ {lookupFields.map(({ label, value, key, displayPrefix }) => ( + handleCopy(displayPrefix ? `${displayPrefix}${value}` : value, key)} + /> + ))} +
+ )} +
+
+
+ ); +} diff --git a/src/components/wallet/modals/PaymentRequestModal.tsx b/src/components/wallet/modals/PaymentRequestModal.tsx new file mode 100644 index 0000000..b472428 --- /dev/null +++ b/src/components/wallet/modals/PaymentRequestModal.tsx @@ -0,0 +1,221 @@ +import { Check, Sparkles, Trash2, Loader2, XIcon, ArrowRight, Clock, Receipt, AlertCircle } from 'lucide-react'; +import { useTransfer } from '@/sdk'; +import { useState } from 'react'; +import { BaseModal, ModalHeader, EmptyState } from '@/components/ui'; + +export enum PaymentRequestStatus { + PENDING = 'pending', + ACCEPTED = 'accepted', + PAID = 'paid', + REJECTED = 'rejected', +} + +export interface IncomingPaymentRequest { + id: string; + requestId: string; + senderPubkey: string; + recipientNametag?: string; + amount: number; + coinId: string; + symbol: string; + message?: string; + timestamp: number; + status: PaymentRequestStatus; +} + +interface PaymentRequestsModalProps { + isOpen: boolean; + onClose: () => void; + requests: IncomingPaymentRequest[]; + pendingCount: number; + reject: (request: IncomingPaymentRequest) => Promise; + paid: (request: IncomingPaymentRequest) => Promise; + clearProcessed: () => void; +} + +export function PaymentRequestsModal({ isOpen, onClose, requests, pendingCount, reject, clearProcessed, paid }: PaymentRequestsModalProps) { + const { transfer } = useTransfer(); + const [processingId, setProcessingId] = useState(null); + const [errors, setErrors] = useState>({}); + + const hasProcessed = requests.some(r => r.status !== PaymentRequestStatus.PENDING); + const isGlobalProcessing = !!processingId; + + const handleSafeClose = () => { + if (!isGlobalProcessing) { + setErrors({}); + onClose(); + } + }; + + const handlePay = async (req: IncomingPaymentRequest) => { + setProcessingId(req.id); + setErrors(prev => ({ ...prev, [req.id]: '' })); + try { + const recipient = req.recipientNametag ? `@${req.recipientNametag}` : req.senderPubkey; + await transfer({ recipient, amount: req.amount.toString(), coinId: req.coinId }); + paid(req); + } catch (error: unknown) { + let errorMessage = 'Transaction failed'; + if (error instanceof Error) { + errorMessage = error.message.includes('Insufficient') ? 'Insufficient funds' : error.message; + } + setErrors(prev => ({ ...prev, [req.id]: errorMessage })); + } finally { + setProcessingId(null); + } + }; + + const subtitle = pendingCount > 0 ? ( +
+ + + + + {pendingCount} pending +
+ ) : undefined; + + return ( + + + +
+ {requests.length === 0 ? ( + + ) : ( + requests.map((req) => ( + handlePay(req)} + onReject={() => reject(req)} + isProcessing={processingId === req.id} + isGlobalDisabled={isGlobalProcessing} + /> + )) + )} +
+ + {hasProcessed && ( +
+ +
+ )} +
+ ); +} + +interface RequestCardProps { + req: IncomingPaymentRequest; + error?: string; + onPay: () => void; + onReject: () => void; + isProcessing: boolean; + isGlobalDisabled: boolean; +} + +function RequestCard({ req, error, onPay, onReject, isProcessing, isGlobalDisabled }: RequestCardProps) { + const isPending = req.status === PaymentRequestStatus.PENDING; + const timeAgo = getTimeAgo(req.timestamp); + + const statusConfig = { + [PaymentRequestStatus.ACCEPTED]: { color: 'text-emerald-500', bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', icon: Check, label: 'Payment Sent' }, + [PaymentRequestStatus.PAID]: { color: 'text-emerald-500', bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', icon: Check, label: 'Paid Successfully' }, + [PaymentRequestStatus.REJECTED]: { color: 'text-red-500', bg: 'bg-red-500/10', border: 'border-red-500/20', icon: XIcon, label: 'Request Declined' }, + [PaymentRequestStatus.PENDING]: { color: 'text-orange-500', bg: 'bg-orange-500/10', border: 'border-orange-500/20', icon: Clock, label: 'Awaiting Payment' }, + }; + + const currentStatus = statusConfig[req.status]; + const StatusIcon = currentStatus.icon; + const isDisabled = isGlobalDisabled && !isProcessing; + + return ( +
+ {isPending &&
} + +
+
+
+ From + + {req.recipientNametag ? `@${req.recipientNametag}` : `${req.senderPubkey.slice(0, 12)}...`} + +
+
+ {timeAgo} +
+
+ +
+
+ {req.amount} {req.symbol} +
+ {req.message && ( +
+ "{req.message}" +
+ )} +
+
+ +
+ {isPending ? ( +
+ {error && ( +
+ + {error} +
+ )} +
+ + +
+
+ ) : ( +
+ + {currentStatus.label} +
+ )} +
+
+ ); +} + +const getTimeAgo = (timestamp: number) => { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 60) return 'Just now'; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + return new Date(timestamp).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +}; diff --git a/src/components/wallet/modals/SeedPhraseModal.tsx b/src/components/wallet/modals/SeedPhraseModal.tsx new file mode 100644 index 0000000..db267bb --- /dev/null +++ b/src/components/wallet/modals/SeedPhraseModal.tsx @@ -0,0 +1,97 @@ +import { Eye, EyeOff, Copy, Check, ShieldAlert } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { BaseModal, ModalHeader, AlertMessage, Button, SecondaryButton } from '@/components/ui'; + +interface SeedPhraseModalProps { + isOpen: boolean; + onClose: () => void; + seedPhrase: string[]; +} + +export function SeedPhraseModal({ isOpen, onClose, seedPhrase }: SeedPhraseModalProps) { + const [isRevealed, setIsRevealed] = useState(false); + const [copied, setCopied] = useState(false); + + // Reset revealed state when modal closes + useEffect(() => { + if (!isOpen) { + setIsRevealed(false); + setCopied(false); + } + }, [isOpen]); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(seedPhrase.join(' ')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy seed phrase:', err); + } + }; + + return ( + + + + {/* Content */} +
+ {/* Warning */} +
+ + Anyone with these words can access your wallet and steal your funds. + +
+ + {/* Seed phrase grid */} +
+ {!isRevealed ? ( +
+ setIsRevealed(true)}> + Reveal Recovery Phrase + +
+ ) : ( + <> +
+ {seedPhrase.map((word, index) => ( +
+ + {index + 1}. + +
+ {word} +
+
+ ))} +
+ + {/* Action buttons */} +
+ setIsRevealed(false)} className="flex-1"> + Hide + + + +
+ + )} +
+ + {/* Info */} +
+ Write down these 12 words in order and store them safely. You'll need them to recover your wallet. +
+
+
+ ); +} diff --git a/src/components/wallet/modals/SendModal.tsx b/src/components/wallet/modals/SendModal.tsx new file mode 100644 index 0000000..d01f6fd --- /dev/null +++ b/src/components/wallet/modals/SendModal.tsx @@ -0,0 +1,449 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { ArrowRight, Loader2, User, CheckCircle, Coins, Hash, Copy, Check } from 'lucide-react'; +import { useAssets, useTransfer, useSphereContext, CurrencyUtils } from '@/sdk'; +import { BaseModal, ModalHeader, Button } from '@/components/ui'; + +type Step = 'recipient' | 'asset' | 'amount' | 'confirm' | 'processing' | 'success'; + +export interface SendPrefill { + to: string; + amount: string; + coinId: string; + memo?: string; +} + +interface SendModalProps { + isOpen: boolean; + onClose: (result?: { success: boolean }) => void; + prefill?: SendPrefill; +} + +export function SendModal({ isOpen, onClose, prefill }: SendModalProps) { + const { assets } = useAssets(); + const { transfer, isLoading: isTransferring } = useTransfer(); + const ctx = useSphereContext(); + + const [copiedKey, setCopiedKey] = useState(null); + const copyToClipboard = useCallback((text: string, key: string) => { + navigator.clipboard.writeText(text).then(() => { + setCopiedKey(key); + setTimeout(() => setCopiedKey(null), 2000); + }).catch(() => {}); + }, []); + + // State + const [step, setStep] = useState('recipient'); + const [recipientMode, setRecipientMode] = useState<'nametag' | 'direct'>('nametag'); + const [recipient, setRecipient] = useState(''); + const [isCheckingRecipient, setIsCheckingRecipient] = useState(false); + const [recipientError, setRecipientError] = useState(null); + + const [resolvedAddress, setResolvedAddress] = useState(null); + + const [selectedAsset, setSelectedAsset] = useState(null); + const [amountInput, setAmountInput] = useState(''); + const [memoInput, setMemoInput] = useState(''); + + // Pre-fill from connect intent (dApp request) + const prefillApplied = useRef(false); + useEffect(() => { + if (!prefill || !isOpen || prefillApplied.current) return; + if (assets.length === 0) return; // wait for assets to load + + const { to, amount, coinId } = prefill; + + if (to.startsWith('DIRECT://')) { + setRecipientMode('direct'); + setRecipient(to); + } else { + setRecipientMode('nametag'); + setRecipient(to.replace(/^@/, '')); + } + + setAmountInput(amount); + if (prefill.memo) setMemoInput(prefill.memo); + + const asset = assets.find((a: any) => a.coinId === coinId); + if (asset) { + setSelectedAsset(asset); + setStep('confirm'); + prefillApplied.current = true; + } + }, [prefill, isOpen, assets]); + + const handleRecipientChange = (e: React.ChangeEvent) => { + if (recipientMode === 'nametag') { + const value = e.target.value.toLowerCase(); + if (/^@?[a-z0-9_\-+.]*$/.test(value)) { + setRecipient(value); + setRecipientError(null); + } + } else { + setRecipient(e.target.value); + setRecipientError(null); + } + }; + + const reset = () => { + setStep('recipient'); + setRecipientMode('nametag'); + setRecipient(''); + setResolvedAddress(null); + setSelectedAsset(null); + setAmountInput(''); + setMemoInput(''); + setRecipientError(null); + prefillApplied.current = false; + }; + + const handleClose = () => { + reset(); + onClose(); + }; + + // STEP 1: Validate Recipient via SDK transport + const handleRecipientNext = async () => { + if (!recipient.trim()) return; + setIsCheckingRecipient(true); + setRecipientError(null); + + try { + if (recipientMode === 'direct') { + const addr = recipient.trim(); + if (!addr.startsWith('DIRECT://')) { + setRecipientError('Direct address must start with DIRECT://'); + return; + } + setRecipient(addr); + setResolvedAddress(addr); + setStep('asset'); + } else { + const cleanTag = recipient.replace('@', '').replace('@unicity', '').trim(); + + if (ctx.resolve) { + const peerInfo = await ctx.resolve(`@${cleanTag}`); + if (peerInfo) { + setRecipient(cleanTag); + setResolvedAddress(peerInfo.directAddress || null); + setStep('asset'); + } else { + setRecipientError(`User @${cleanTag} not found`); + } + } else { + setRecipient(cleanTag); + setStep('asset'); + } + } + } catch { + setRecipientError("Network error"); + } finally { + setIsCheckingRecipient(false); + } + }; + + // STEP 3: Go to confirm + const handleAmountNext = () => { + if (!selectedAsset || !amountInput) return; + const targetAmount = CurrencyUtils.toSmallestUnit(amountInput, selectedAsset.decimals); + if (targetAmount === '0') return; + setStep('confirm'); + }; + + // STEP 4: Execute transfer via SDK + const handleSend = async () => { + if (!selectedAsset || !amountInput || !recipient) return; + + setStep('processing'); + setRecipientError(null); + + try { + const amount = CurrencyUtils.toSmallestUnit(amountInput, selectedAsset.decimals); + await transfer({ + coinId: selectedAsset.coinId, + amount, + recipient, + ...(memoInput ? { memo: memoInput } : {}), + }); + + setStep('success'); + } catch (e: unknown) { + console.error(e); + setRecipientError(e instanceof Error ? e.message : "Transfer failed"); + setStep('confirm'); + } + }; + + const handleSuccessClose = () => { + reset(); + onClose({ success: true }); + }; + + const getTitle = () => { + switch (step) { + case 'recipient': return 'Send To'; + case 'asset': return 'Select Asset'; + case 'amount': return 'Enter Amount'; + case 'confirm': return 'Confirm Transfer'; + case 'processing': return 'Processing...'; + case 'success': return 'Sent!'; + } + }; + + const formatAmount = (rawAmount: string, decimals: number) => + CurrencyUtils.toHumanReadable(rawAmount, decimals); + + return ( + + + +
+ + {/* 1. RECIPIENT */} + {step === 'recipient' && ( +
+
+ +
+ {recipientMode === 'nametag' && ( + @ + )} + e.key === 'Enter' && handleRecipientNext()} + className={`w-full bg-neutral-100 dark:bg-neutral-900 border border-neutral-200 dark:border-white/10 rounded-xl py-3 pr-4 text-neutral-900 dark:text-white focus:border-orange-500 outline-none ${recipientMode === 'nametag' ? 'pl-8' : 'pl-4 font-mono text-sm'}`} + placeholder={recipientMode === 'nametag' ? 'Unicity ID' : 'DIRECT://...'} + /> +
+ {recipientError &&

{recipientError}

} + +
+ + +
+ )} + + {/* 2. ASSET */} + {step === 'asset' && ( +
+ {assets.map((asset: any) => ( + + ))} +
+ )} + + {/* 3. AMOUNT */} + {step === 'amount' && selectedAsset && (() => { + const smallestUnit = CurrencyUtils.toSmallestUnit(amountInput || '0', selectedAsset.decimals); + const insufficientBalance = amountInput !== '' && BigInt(smallestUnit) > BigInt(selectedAsset.totalAmount); + return ( +
+
+
+ Amount + + Available: {formatAmount(selectedAsset.totalAmount, selectedAsset.decimals)} + +
+
+ { + const v = e.target.value; + if (v === '' || /^\d*\.?\d*$/.test(v)) setAmountInput(v); + }} + className={`w-full bg-neutral-100 dark:bg-neutral-900 border rounded-xl py-3 px-4 text-neutral-900 dark:text-white text-2xl font-mono outline-none ${insufficientBalance ? 'border-red-500 focus:border-red-500' : 'border-neutral-200 dark:border-white/10 focus:border-orange-500'}`} + placeholder="0.00" + /> + +
+ {insufficientBalance &&

Insufficient balance

} + {recipientError &&

{recipientError}

} +
+
+ + setMemoInput(e.target.value)} + className="w-full bg-neutral-100 dark:bg-neutral-900 border border-neutral-200 dark:border-white/10 rounded-xl py-3 px-4 text-neutral-900 dark:text-white outline-none focus:border-orange-500 text-sm" + placeholder="Add a note to this transfer" + /> +
+ +
+ ); + })()} + + {/* 4. CONFIRM */} + {step === 'confirm' && selectedAsset && ( +
+ + {/* Summary Card */} +
+
You are sending
+
+ {amountInput} {selectedAsset.symbol} +
+ {selectedAsset.priceUsd != null && ( +
+ ≈ ${(parseFloat(amountInput) * selectedAsset.priceUsd).toFixed(2)} USD +
+ )} + {selectedAsset.priceUsd == null &&
} + +
+
+
+ {recipientMode === 'direct' ? ( + + ) : ( + + )} + + {recipientMode === 'direct' ? recipient : `@${recipient}`} + +
+ +
+ {recipientMode === 'nametag' && resolvedAddress && ( +
+ + {resolvedAddress.length > 30 + ? `${resolvedAddress.slice(0, 18)}...${resolvedAddress.slice(-8)}` + : resolvedAddress} + + +
+ )} +
+ {memoInput && ( +
+ “{memoInput}” +
+ )} +
+ + {/* Strategy Info */} +
+
+ +
+
Smart Transfer
+
+ Token splitting and transfer optimization is handled automatically. +
+
+
+
+ + {recipientError &&

{recipientError}

} + + +
+ )} + + {/* 5. PROCESSING */} + {step === 'processing' && ( +
+ +

Sending Transaction...

+

Processing proofs and broadcasting via Nostr

+
+ )} + + {/* 6. SUCCESS */} + {step === 'success' && ( +
+
+ +
+

Success!

+

+ Successfully sent {amountInput} {selectedAsset?.symbol} to {recipientMode === 'direct' ? recipient : `@${recipient}`} +

+ +
+ )} + +
+ + ); +} diff --git a/src/components/wallet/modals/SettingsModal.tsx b/src/components/wallet/modals/SettingsModal.tsx new file mode 100644 index 0000000..c563f36 --- /dev/null +++ b/src/components/wallet/modals/SettingsModal.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { Settings, Download, LogOut, Key } from 'lucide-react'; +import { BaseModal, ModalHeader, MenuButton } from '@/components/ui'; +import { LookupModal } from './LookupModal'; + +interface SettingsModalProps { + isOpen: boolean; + onClose: () => void; + onBackupWallet: () => void; + onLogout: () => void; +} + +export function SettingsModal({ + isOpen, + onClose, + onBackupWallet, + onLogout, +}: SettingsModalProps) { + const [isLookupOpen, setIsLookupOpen] = useState(false); + + return ( + <> + + + +
+ { + onClose(); + setIsLookupOpen(true); + }} + /> + + { + onClose(); + onBackupWallet(); + }} + /> + + { + onClose(); + onLogout(); + }} + /> +
+
+ + setIsLookupOpen(false)} + /> + + ); +} diff --git a/src/components/wallet/modals/SwapModal.tsx b/src/components/wallet/modals/SwapModal.tsx new file mode 100644 index 0000000..341ef3d --- /dev/null +++ b/src/components/wallet/modals/SwapModal.tsx @@ -0,0 +1,304 @@ +import { useState, useMemo } from 'react'; +import { ArrowDownUp, Loader2, TrendingUp, CheckCircle, ArrowDown } from 'lucide-react'; +import { useIdentity, useAssets, useTransfer } from '@/sdk'; +import { CurrencyUtils } from '@/sdk'; +import { BaseModal, ModalHeader, Button } from '@/components/ui'; + +type Step = 'swap' | 'processing' | 'success'; + +interface SwapModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function SwapModal({ isOpen, onClose }: SwapModalProps) { + const { nametag } = useIdentity(); + const { assets } = useAssets(); + const { transfer } = useTransfer(); + + const [step, setStep] = useState('swap'); + const [fromAsset, setFromAsset] = useState(null); + const [toAsset, setToAsset] = useState(null); + const [fromAmount, setFromAmount] = useState(''); + const [showFromDropdown, setShowFromDropdown] = useState(false); + const [showToDropdown, setShowToDropdown] = useState(false); + const [error, setError] = useState(null); + + // Use user's assets as available swap options + const swappableAssets = useMemo(() => assets, [assets]); + + // Format asset amount from smallest unit to human-readable + const formatAssetAmount = (asset: any): string => { + try { + return CurrencyUtils.toHumanReadable(asset.totalAmount, asset.decimals); + } catch { + return '0'; + } + }; + + const resolvePrice = (asset: any): number => { + return asset.priceUsd && asset.priceUsd > 0 ? asset.priceUsd : 1.0; + }; + + const exchangeInfo = useMemo(() => { + if (!fromAsset || !toAsset || !fromAmount || parseFloat(fromAmount) <= 0) return null; + const fromAmountNum = parseFloat(fromAmount); + const fromPrice = resolvePrice(fromAsset); + const toPrice = resolvePrice(toAsset); + if (fromPrice === 0 || toPrice === 0) return null; + const rate = fromPrice / toPrice; + const toAmount = fromAmountNum * rate; + return { rate, fromValueUSD: fromAmountNum * fromPrice, toAmount, toValueUSD: toAmount * toPrice }; + }, [fromAsset, toAsset, fromAmount]); + + const isValidAmount = useMemo(() => { + if (!fromAsset || !fromAmount) return false; + const amount = parseFloat(fromAmount); + if (isNaN(amount) || amount <= 0) return false; + const maxAmount = parseFloat(formatAssetAmount(fromAsset)); + return amount <= maxAmount; + }, [fromAsset, fromAmount]); + + const reset = () => { + setStep('swap'); + setFromAsset(null); + setToAsset(null); + setFromAmount(''); + setError(null); + setShowFromDropdown(false); + setShowToDropdown(false); + }; + + const handleClose = () => { reset(); onClose(); }; + + const handleSwap = async () => { + if (!fromAsset || !toAsset || !fromAmount || !exchangeInfo || !nametag) return; + setStep('processing'); + setError(null); + try { + const fromAmountSmallest = CurrencyUtils.toSmallestUnit(fromAmount, fromAsset.decimals); + await transfer({ recipient: 'swap', amount: fromAmountSmallest.toString(), coinId: fromAsset.coinId }); + // Request swapped tokens from faucet + const coinName = (toAsset.name || toAsset.symbol || '').toLowerCase(); + await fetch(`https://faucet.unicity.network/api/faucet/request?nametag=${encodeURIComponent(nametag)}&coin=${encodeURIComponent(coinName)}&amount=${exchangeInfo.toAmount}`); + setStep('success'); + } catch (e: unknown) { + console.error('Swap failed:', e); + setError(e instanceof Error ? e.message : 'Swap failed'); + setStep('swap'); + } + }; + + const handleFlipAssets = () => { + if (!fromAsset || !toAsset) return; + const newFrom = assets.find((a: any) => a.coinId === toAsset.coinId); + if (!newFrom) { + setError(`You don't have any ${toAsset.symbol} to swap from`); + return; + } + const newTo = swappableAssets.find((a: any) => a.coinId === fromAsset.coinId); + setFromAsset(newFrom); + setToAsset(newTo || fromAsset); + setError(null); + if (exchangeInfo && exchangeInfo.toAmount > 0) { + setFromAmount(parseFloat(exchangeInfo.toAmount.toFixed(6)).toString()); + } else { + setFromAmount(''); + } + }; + + const getTitle = () => { + switch (step) { + case 'swap': return 'Swap Tokens'; + case 'processing': return 'Processing Swap...'; + case 'success': return 'Swap Complete!'; + } + }; + + return ( + + + +
+ {/* SWAP INTERFACE */} + {step === 'swap' && ( +
+ {/* FROM */} +
+
+ From + {fromAsset && ( + + Balance: {formatAssetAmount(fromAsset)} + + )} +
+
+
+
+ + {showFromDropdown && ( +
+ {assets.map((asset: any) => ( + + ))} +
+ )} +
+ setFromAmount(e.target.value)} + placeholder="0.00" + disabled={!fromAsset} + className="flex-1 bg-transparent text-right text-xl font-mono text-neutral-900 dark:text-white outline-none disabled:opacity-50 min-w-0" + /> +
+ {fromAsset && fromAmount && ( +
+ ≈ ${(parseFloat(fromAmount) * resolvePrice(fromAsset)).toFixed(2)} +
+ )} +
+
+ + {/* Flip */} +
+ +
+ + {/* TO */} +
+
+ To +
+
+
+
+ + {showToDropdown && ( +
+ {swappableAssets.filter((a: any) => a.coinId !== fromAsset?.coinId).map((asset: any) => ( + + ))} +
+ )} +
+
+ {exchangeInfo ? exchangeInfo.toAmount.toFixed(6) : '0.00'} +
+
+ {exchangeInfo && ( +
+ ≈ ${exchangeInfo.toValueUSD.toFixed(2)} +
+ )} +
+
+ + {/* Exchange Rate */} + {exchangeInfo && fromAsset && toAsset && ( +
+
+ + Exchange Rate +
+
+ 1 {fromAsset.symbol} = {exchangeInfo.rate.toFixed(6)} {toAsset.symbol} +
+
+ )} + + {error && ( +
+

{error}

+
+ )} + + +
+ )} + + {/* PROCESSING */} + {step === 'processing' && ( +
+ +

Processing Swap...

+

Sending tokens and requesting swap

+
+ )} + + {/* SUCCESS */} + {step === 'success' && fromAsset && toAsset && exchangeInfo && ( +
+
+ +
+

Swap Complete!

+

+ Swapped {fromAmount} {fromAsset.symbol} +

+

+ for {exchangeInfo.toAmount.toFixed(6)} {toAsset.symbol} +

+ +
+ )} +
+
+ ); +} diff --git a/src/components/wallet/modals/TopUpModal.tsx b/src/components/wallet/modals/TopUpModal.tsx new file mode 100644 index 0000000..8f6add7 --- /dev/null +++ b/src/components/wallet/modals/TopUpModal.tsx @@ -0,0 +1,102 @@ +import { useState } from 'react'; +import { Plus, Sparkles, CheckCircle, XCircle, Loader2 } from 'lucide-react'; +import { useIdentity } from '@/sdk'; +import { BaseModal, ModalHeader, Button } from '@/components/ui'; + +interface TopUpModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function TopUpModal({ isOpen, onClose }: TopUpModalProps) { + const { nametag } = useIdentity(); + + const [isFaucetLoading, setIsFaucetLoading] = useState(false); + const [faucetSuccess, setFaucetSuccess] = useState(false); + const [faucetError, setFaucetError] = useState(null); + + const handleFaucetRequest = async () => { + if (!nametag) return; + + setIsFaucetLoading(true); + setFaucetError(null); + setFaucetSuccess(false); + + try { + // Request tokens via faucet API + const response = await fetch(`https://faucet.unicity.network/api/faucet/request-all?nametag=${encodeURIComponent(nametag)}`); + if (!response.ok) { + throw new Error(`Faucet request failed: ${response.statusText}`); + } + const results = await response.json(); + const failedRequests = Array.isArray(results) ? results.filter((r: any) => !r.success) : []; + + if (failedRequests.length > 0) { + const failedCoins = failedRequests.map((r: any) => r.coin).join(', '); + setFaucetError(`Failed to request: ${failedCoins}`); + } else { + setFaucetSuccess(true); + setTimeout(() => setFaucetSuccess(false), 3000); + } + } catch (error) { + setFaucetError(error instanceof Error ? error.message : 'Failed to request tokens'); + } finally { + setIsFaucetLoading(false); + } + }; + + const handleClose = () => { + setFaucetError(null); + setFaucetSuccess(false); + onClose(); + }; + + return ( + + + +
+
+
+ +
+

+ Request test tokens from the Unicity faucet +

+ + {!nametag ? ( +

+ Nametag is required to request tokens +

+ ) : ( + <> + + + {faucetError && ( +
+ +

{faucetError}

+
+ )} + + )} +
+
+
+ ); +} diff --git a/src/components/wallet/modals/TransactionHistoryModal.tsx b/src/components/wallet/modals/TransactionHistoryModal.tsx new file mode 100644 index 0000000..4f85127 --- /dev/null +++ b/src/components/wallet/modals/TransactionHistoryModal.tsx @@ -0,0 +1,303 @@ +import { useMemo, useState, useCallback } from 'react'; +import { ArrowUpRight, ArrowDownLeft, Loader2, Clock, ChevronDown, Copy, Check } from 'lucide-react'; +import { useTransactionHistory } from '@/sdk'; +import { BaseModal, ModalHeader, EmptyState } from '@/components/ui'; + +/** Copy text to clipboard, return true on success */ +function useCopyToClipboard() { + const [copiedKey, setCopiedKey] = useState(null); + + const copy = useCallback(async (text: string, key: string) => { + try { + await navigator.clipboard.writeText(text); + setCopiedKey(key); + setTimeout(() => setCopiedKey(null), 2000); + } catch { + // Ignore + } + }, []); + + return { copiedKey, copy }; +} + +/** Truncate middle of string: "DIRECT://abc...xyz" */ +function truncateMiddle(str: string, startLen = 14, endLen = 6): string { + if (str.length <= startLen + endLen + 3) return str; + return `${str.slice(0, startLen)}...${str.slice(-endLen)}`; +} + +/** Format raw amount (smallest units) to human-readable with given decimals */ +function formatRawAmount(raw: string, decimals: number): string { + const val = BigInt(raw || '0'); + if (decimals === 0) return val.toString(); + const divisor = BigInt(10 ** decimals); + const intPart = val / divisor; + const fracPart = val % divisor; + const fracStr = fracPart.toString().padStart(decimals, '0'); + return `${intPart}.${fracStr}`.replace(/\.?0+$/, ''); +} + +/** Single detail row with copy button */ +function DetailRow({ label, value, copyKey, copiedKey, onCopy }: { + label: string; + value: string; + copyKey: string; + copiedKey: string | null; + onCopy: (text: string, key: string) => void; +}) { + return ( +
+ {label} +
+ + {truncateMiddle(value)} + + +
+
+ ); +} + +interface TransactionHistoryModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function TransactionHistoryModal({ isOpen, onClose }: TransactionHistoryModalProps) { + const { history, isLoading } = useTransactionHistory(); + const [expandedId, setExpandedId] = useState(null); + const { copiedKey, copy } = useCopyToClipboard(); + + const formattedHistory = useMemo(() => { + return history.map((entry: any) => { + const decimals = entry.decimals || 0; + + return { + ...entry, + formattedAmount: formatRawAmount(entry.amount, decimals), + formattedTokenIds: entry.tokenIds?.map((t: any) => ({ + ...t, + formattedAmount: formatRawAmount(t.amount, decimals), + })), + date: new Date(entry.timestamp).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }), + time: new Date(entry.timestamp).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + }), + }; + }); + }, [history]); + + const toggleExpand = (id: string) => { + setExpandedId(prev => prev === id ? null : id); + }; + + return ( + + + + {/* Content - Scrollable */} +
+ {isLoading ? ( +
+ +
+ ) : history.length === 0 ? ( + + ) : ( +
+ {formattedHistory.map((entry: any) => { + const isExpanded = expandedId === entry.id; + const peerLabel = entry.type === 'RECEIVED' + ? (entry.senderNametag ? `@${entry.senderNametag}` : entry.senderAddress ? truncateMiddle(entry.senderAddress) : entry.senderPubkey ? `${entry.senderPubkey.slice(0, 4)}...${entry.senderPubkey.slice(-4)}` : null) + : (entry.recipientNametag ? `@${entry.recipientNametag}` : entry.recipientAddress ? truncateMiddle(entry.recipientAddress) : null); + + return ( +
toggleExpand(entry.id)} + > + {/* Main row */} +
+ {/* Icon with badge */} +
+ {entry.iconUrl ? ( + + ) : ( +
+ + {entry.symbol?.slice(0, 2) || '??'} + +
+ )} +
+ {entry.type === 'RECEIVED' ? ( + + ) : ( + + )} +
+
+ + {/* Title & Subtitle */} +
+
+ {entry.type === 'RECEIVED' ? 'Received' : 'Sent'} + {peerLabel && ( + + {entry.type === 'RECEIVED' ? 'from' : 'to'} {peerLabel} + + )} +
+
+ {entry.date} • {entry.time} +
+ {entry.memo && ( +
+ “{entry.memo}” +
+ )} +
+ + {/* Amount + chevron */} +
+
+ {entry.type === 'RECEIVED' ? '+' : '-'}{entry.formattedAmount} {entry.symbol} +
+ +
+
+ + {/* Expandable detail panel */} + {isExpanded && ( +
+
+
+ {/* Peer info */} + {entry.type === 'RECEIVED' && ( + <> + {entry.senderNametag && ( + + )} + {entry.senderAddress && ( + + )} + {entry.senderPubkey && ( + + )} + + )} + {entry.type === 'SENT' && ( + <> + {entry.recipientNametag && ( + + )} + {entry.recipientAddress && ( + + )} + {entry.recipientPubkey && ( + + )} + + )} + + {/* Memo */} + {entry.memo && ( +
+ Memo +
+ “{entry.memo}” +
+
+ )} + + {/* Token breakdown (V6 combined transfers) */} + {entry.formattedTokenIds && entry.formattedTokenIds.length > 1 && ( +
+ + Tokens ({entry.formattedTokenIds.length}) + +
+ {entry.formattedTokenIds.map((t: any, idx: number) => ( +
+
+ + {t.source} + + + {truncateMiddle(t.id, 8, 6)} + + +
+ + {t.formattedAmount} {entry.symbol} + +
+ ))} +
+
+ )} + + {/* Common fields */} + {entry.tokenId && !entry.formattedTokenIds?.length && ( + + )} + {entry.transferId && ( + + )} +
+ Amount (raw) + {entry.amount} +
+
+
+
+ )} +
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/src/components/wallet/modals/index.ts b/src/components/wallet/modals/index.ts new file mode 100644 index 0000000..0684bd0 --- /dev/null +++ b/src/components/wallet/modals/index.ts @@ -0,0 +1,11 @@ +export { SendModal } from './SendModal'; +export type { SendPrefill } from './SendModal'; +export { TransactionHistoryModal } from './TransactionHistoryModal'; +export { SettingsModal } from './SettingsModal'; +export { SeedPhraseModal } from './SeedPhraseModal'; +export { LookupModal } from './LookupModal'; +export { TopUpModal } from './TopUpModal'; +export { SwapModal } from './SwapModal'; +export { PaymentRequestsModal } from './PaymentRequestModal'; +export { PaymentRequestStatus } from './PaymentRequestModal'; +export type { IncomingPaymentRequest } from './PaymentRequestModal'; diff --git a/src/components/wallet/onboarding/CreateWalletFlow.tsx b/src/components/wallet/onboarding/CreateWalletFlow.tsx new file mode 100644 index 0000000..a48691e --- /dev/null +++ b/src/components/wallet/onboarding/CreateWalletFlow.tsx @@ -0,0 +1,154 @@ +/** + * CreateWalletFlow - Main onboarding flow component + * Extension-adapted: no framer-motion, password-based wallet creation + * Uses state-based conditional rendering instead of AnimatePresence + */ +import { useOnboardingFlow } from "./useOnboardingFlow"; +import { StartScreen } from "./StartScreen"; +import { RestoreScreen } from "./RestoreScreen"; +import { MnemonicBackupScreen } from "./MnemonicBackupScreen"; +import { NametagScreen } from "./NametagScreen"; + +export type { OnboardingStep } from "./useOnboardingFlow"; + +export function CreateWalletFlow() { + const { + // Step management + step, + setStep, + goToStart, + + // State + isBusy, + error, + + // Password state + password, + setPassword, + confirmPassword, + setConfirmPassword, + + // Mnemonic restore state + seedWords, + setSeedWords, + + // Generated mnemonic + generatedMnemonic, + + // Nametag state + nametagInput, + setNametagInput, + nametagAvailability, + + // Processing state + processingStatus, + processingTitle, + processingCompleteTitle, + isProcessingComplete, + + // Actions + handleCreateWallet, + handleRestoreWallet, + handleMnemonicBackupConfirm, + handleMintNametag, + handleSkipNametag, + handleCompleteOnboarding, + } = useOnboardingFlow(); + + return ( +
+ {step === "start" && ( + setStep("restore")} + /> + )} + + {step === "restore" && ( + + )} + + {step === "mnemonicBackup" && generatedMnemonic && ( + + )} + + {step === "nametag" && ( + + )} + + {step === "processing" && ( +
+
+ {isProcessingComplete ? ( + <> +
+
+ + + +
+ + ) : ( + <> +
+
+ + + + +
+ + )} +
+ +

+ {isProcessingComplete ? processingCompleteTitle : processingTitle} +

+ +

+ {processingStatus} +

+ + {isProcessingComplete && ( + + )} +
+ )} +
+ ); +} diff --git a/src/components/wallet/onboarding/MnemonicBackupScreen.tsx b/src/components/wallet/onboarding/MnemonicBackupScreen.tsx new file mode 100644 index 0000000..c3c60a6 --- /dev/null +++ b/src/components/wallet/onboarding/MnemonicBackupScreen.tsx @@ -0,0 +1,117 @@ +/** + * MnemonicBackupScreen - Extension-specific screen for mnemonic backup + * Shows the generated mnemonic words in a grid for user backup + * Uses same styling patterns as the rest of the onboarding flow + */ +import { ShieldAlert, Copy, Check } from "lucide-react"; +import { useState, useCallback } from "react"; + +interface MnemonicBackupScreenProps { + mnemonic: string; + onConfirm: () => void; +} + +export function MnemonicBackupScreen({ + mnemonic, + onConfirm, +}: MnemonicBackupScreenProps) { + const [copied, setCopied] = useState(false); + const words = mnemonic.split(" "); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(mnemonic); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback for environments where clipboard API is not available + const textarea = document.createElement("textarea"); + textarea.value = mnemonic; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }, [mnemonic]); + + return ( +
+ {/* Warning Icon */} +
+
+
+ +
+
+ +

+ Back Up Recovery Phrase +

+ +

+ Write down these 12 words in order and keep them safe.{" "} + + This is the only way to recover your wallet. + +

+ + {/* Mnemonic word grid */} +
+ {words.map((word, index) => ( +
+ + {index + 1}. + + + {word} + +
+ ))} +
+ + {/* Copy button */} + + + {/* Warning notice */} +
+

+ Never share your recovery phrase with anyone. Anyone with these words + can access your wallet and funds. +

+
+ + {/* Confirm button */} + +
+ ); +} diff --git a/src/components/wallet/onboarding/NametagScreen.tsx b/src/components/wallet/onboarding/NametagScreen.tsx new file mode 100644 index 0000000..369e33e --- /dev/null +++ b/src/components/wallet/onboarding/NametagScreen.tsx @@ -0,0 +1,154 @@ +/** + * NametagScreen - Unicity ID creation screen + * Extension-adapted: removed framer-motion animations + * Keeps real-time availability checking UI + */ +import { ShieldCheck, ArrowRight, ArrowLeft, Loader2, CheckCircle2, AlertCircle } from "lucide-react"; + +export type NametagAvailability = "idle" | "checking" | "available" | "taken"; + +interface NametagScreenProps { + nametagInput: string; + isBusy: boolean; + error: string | null; + availability: NametagAvailability; + onNametagChange: (value: string) => void; + onSubmit: () => void; + onSkip?: () => void; + onBack?: () => void; +} + +export function NametagScreen({ + nametagInput, + isBusy, + error, + availability, + onNametagChange, + onSubmit, + onSkip, + onBack, +}: NametagScreenProps) { + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value.toLowerCase(); + // Allow only valid nametag characters + if (/^[a-z0-9_\-+.]*$/.test(value)) { + onNametagChange(value); + } + }; + + const canSubmit = nametagInput && !isBusy && availability !== "taken" && availability !== "checking"; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && canSubmit) { + onSubmit(); + } + }; + + return ( +
+ {/* Success Icon */} +
+
+
+ +
+
+ +

+ Choose Unicity ID +

+ +

+ Choose a unique{" "} + + Unicity ID + {" "} + to receive tokens easily without long addresses. +

+ + {/* Input Field */} +
+
+ {availability === "checking" && } + {availability === "available" && } + {availability === "taken" && } + + @unicity + +
+ +
+
+ + {/* Availability status -- fixed height to prevent layout shift */} +
+ {availability === "taken" && !error && ( +

+ @{nametagInput} is already taken +

+ )} + {availability === "available" && ( +

+ @{nametagInput} is available +

+ )} +
+ + {/* Continue Button */} + + + {/* Skip Button */} + {onSkip && ( + + )} + + {/* Back Button */} + {onBack && ( + + )} + + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/src/components/wallet/onboarding/RestoreScreen.tsx b/src/components/wallet/onboarding/RestoreScreen.tsx new file mode 100644 index 0000000..99f23da --- /dev/null +++ b/src/components/wallet/onboarding/RestoreScreen.tsx @@ -0,0 +1,199 @@ +/** + * RestoreScreen - Mnemonic recovery phrase input screen + * Extension-adapted: includes password fields for encrypted storage + * Removed: framer-motion animations + */ +import { KeyRound, ArrowLeft, ArrowRight, Loader2, Eye, EyeOff } from "lucide-react"; +import { useState } from "react"; + +interface RestoreScreenProps { + seedWords: string[]; + isBusy: boolean; + error: string | null; + password: string; + confirmPassword: string; + onPasswordChange: (value: string) => void; + onConfirmPasswordChange: (value: string) => void; + onSeedWordsChange: (words: string[]) => void; + onRestore: () => void; + onBack: () => void; +} + +export function RestoreScreen({ + seedWords, + isBusy, + error, + password, + confirmPassword, + onPasswordChange, + onConfirmPasswordChange, + onSeedWordsChange, + onRestore, + onBack, +}: RestoreScreenProps) { + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + + const handleWordChange = (index: number, value: string) => { + const newWords = [...seedWords]; + newWords[index] = value; + onSeedWordsChange(newWords); + }; + + const handlePaste = (e: React.ClipboardEvent) => { + const pastedText = e.clipboardData.getData("text").trim(); + const words = pastedText.split(/\s+/).filter((w) => w.length > 0); + // If pasted text contains multiple words, fill all fields + if (words.length > 1) { + e.preventDefault(); + const newWords = Array(12).fill(""); + words.slice(0, 12).forEach((word, i) => { + newWords[i] = word.toLowerCase(); + }); + onSeedWordsChange(newWords); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent, index: number) => { + if (e.key === "Enter" && index < 11) { + const nextInput = (e.currentTarget as HTMLElement).parentElement + ?.nextElementSibling?.querySelector("input"); + nextInput?.focus(); + } else if (e.key === "Enter" && index === 11) { + // Focus the password field + const passwordInput = document.getElementById("restore-password"); + passwordInput?.focus(); + } + }; + + const handlePasswordKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && isComplete && password && confirmPassword && !isBusy) { + onRestore(); + } + }; + + const isComplete = seedWords.every((w) => w.trim()); + + return ( +
+ {/* Icon */} +
+
+
+ +
+
+ +

+ Restore Wallet +

+

+ Enter your 12-word recovery phrase to restore your wallet +

+ + {/* 12-word grid */} +
+ {Array.from({ length: 12 }).map((_, index) => ( +
+ + {index + 1}. + + handleWordChange(index, e.target.value)} + onPaste={handlePaste} + onKeyDown={(e) => handleKeyDown(e, index)} + placeholder="word" + className="w-full bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700/50 rounded-lg py-2.5 pl-8 pr-2 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-600 focus:outline-none focus:border-blue-500 focus:bg-white dark:focus:bg-neutral-800 transition-all" + autoFocus={index === 0} + /> +
+ ))} +
+ + {/* Password inputs for encrypted storage */} +
+
+ onPasswordChange(e.target.value)} + onKeyDown={handlePasswordKeyDown} + placeholder="Create password (min 8 characters)" + disabled={isBusy} + className="w-full bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700/50 rounded-xl py-3 pl-3 pr-10 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-600 focus:outline-none focus:border-blue-500 focus:bg-white dark:focus:bg-neutral-800 transition-all disabled:opacity-50" + /> + +
+ +
+ onConfirmPasswordChange(e.target.value)} + onKeyDown={handlePasswordKeyDown} + placeholder="Confirm password" + disabled={isBusy} + className="w-full bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700/50 rounded-xl py-3 pl-3 pr-10 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-600 focus:outline-none focus:border-blue-500 focus:bg-white dark:focus:bg-neutral-800 transition-all disabled:opacity-50" + /> + +
+
+ + {/* Buttons */} +
+ + + +
+ + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/src/components/wallet/onboarding/StartScreen.tsx b/src/components/wallet/onboarding/StartScreen.tsx new file mode 100644 index 0000000..bab6370 --- /dev/null +++ b/src/components/wallet/onboarding/StartScreen.tsx @@ -0,0 +1,147 @@ +/** + * StartScreen - Initial onboarding screen + * Extension-adapted: includes password input for wallet creation (encrypted storage) + * Removed: "Continue Setup", IPNS checking, framer-motion + */ +import { + Wallet, + ArrowRight, + Loader2, + KeyRound, + Eye, + EyeOff, +} from "lucide-react"; +import { useState } from "react"; + +interface StartScreenProps { + isBusy: boolean; + error: string | null; + password: string; + confirmPassword: string; + onPasswordChange: (value: string) => void; + onConfirmPasswordChange: (value: string) => void; + onCreateWallet: () => void; + onRestore: () => void; +} + +export function StartScreen({ + isBusy, + error, + password, + confirmPassword, + onPasswordChange, + onConfirmPasswordChange, + onCreateWallet, + onRestore, +}: StartScreenProps) { + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && password && confirmPassword && !isBusy) { + onCreateWallet(); + } + }; + + return ( +
+ {/* Icon with glow effect */} +
+
+
+ +
+
+ +

+ No Wallet Found +

+

+ Create a new secure wallet to start using{" "} + + the Unicity Network + +

+ + {/* Password inputs for wallet creation */} +
+
+ onPasswordChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Create password (min 8 characters)" + disabled={isBusy} + className="w-full bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700/50 rounded-xl py-3 pl-3 pr-10 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-600 focus:outline-none focus:border-orange-500 focus:bg-white dark:focus:bg-neutral-800 transition-all disabled:opacity-50" + autoFocus + /> + +
+ +
+ onConfirmPasswordChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Confirm password" + disabled={isBusy} + className="w-full bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700/50 rounded-xl py-3 pl-3 pr-10 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-600 focus:outline-none focus:border-orange-500 focus:bg-white dark:focus:bg-neutral-800 transition-all disabled:opacity-50" + /> + +
+
+ + + + + + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/src/components/wallet/onboarding/index.ts b/src/components/wallet/onboarding/index.ts new file mode 100644 index 0000000..6c98e6e --- /dev/null +++ b/src/components/wallet/onboarding/index.ts @@ -0,0 +1,8 @@ +export { CreateWalletFlow } from "./CreateWalletFlow"; +export type { OnboardingStep } from "./CreateWalletFlow"; +export { StartScreen } from "./StartScreen"; +export { RestoreScreen } from "./RestoreScreen"; +export { MnemonicBackupScreen } from "./MnemonicBackupScreen"; +export { NametagScreen } from "./NametagScreen"; +export type { NametagAvailability } from "./NametagScreen"; +export { useOnboardingFlow } from "./useOnboardingFlow"; diff --git a/src/components/wallet/onboarding/useOnboardingFlow.ts b/src/components/wallet/onboarding/useOnboardingFlow.ts new file mode 100644 index 0000000..a94bc8c --- /dev/null +++ b/src/components/wallet/onboarding/useOnboardingFlow.ts @@ -0,0 +1,329 @@ +/** + * useOnboardingFlow - Manages onboarding flow state and navigation + * Extension-specific: requires password for wallet creation/import (encrypted storage) + * Simplified from sphere web app: no file import, no address selection, no IPNS + */ +import { useState, useCallback, useEffect } from "react"; +import { useSphereContext } from "@/sdk/context"; + +export type NametagAvailability = "idle" | "checking" | "available" | "taken"; + +export type OnboardingStep = + | "start" + | "restore" + | "mnemonicBackup" + | "nametag" + | "processing"; + +export interface UseOnboardingFlowReturn { + // Step management + step: OnboardingStep; + setStep: (step: OnboardingStep) => void; + goToStart: () => void; + + // State + isBusy: boolean; + error: string | null; + + // Password state (extension-specific) + password: string; + setPassword: (value: string) => void; + confirmPassword: string; + setConfirmPassword: (value: string) => void; + + // Mnemonic restore state + seedWords: string[]; + setSeedWords: (words: string[]) => void; + + // Generated mnemonic (from create flow) + generatedMnemonic: string | null; + + // Nametag state + nametagInput: string; + setNametagInput: (value: string) => void; + nametagAvailability: NametagAvailability; + + // Processing state + processingStatus: string; + processingStep: number; + processingTotalSteps: number; + processingTitle: string; + processingCompleteTitle: string; + isProcessingComplete: boolean; + + // Actions + handleCreateWallet: () => Promise; + handleRestoreWallet: () => Promise; + handleMnemonicBackupConfirm: () => void; + handleMintNametag: () => Promise; + handleSkipNametag: () => void; + handleCompleteOnboarding: () => void; +} + +export function useOnboardingFlow(): UseOnboardingFlowReturn { + const { + createWallet, + importWallet, + isNametagAvailable, + registerNametag, + } = useSphereContext(); + + // Step management + const [step, setStep] = useState("start"); + + // Common state + const [isBusy, setIsBusy] = useState(false); + const [error, setError] = useState(null); + + // Password state (extension-specific: encrypted mnemonic storage) + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + // Mnemonic restore state + const [seedWords, setSeedWords] = useState(Array(12).fill("")); + + // Generated mnemonic (from create flow) + const [generatedMnemonic, setGeneratedMnemonic] = useState(null); + + // Nametag state + const [nametagInput, setNametagInput] = useState(""); + const [nametagAvailability, setNametagAvailability] = useState("idle"); + + // Processing state + const [processingStatus, setProcessingStatus] = useState(""); + const [processingStep, setProcessingStep] = useState(0); + const [processingTotalSteps, setProcessingTotalSteps] = useState(3); + const [processingTitle, setProcessingTitle] = useState("Setting up Profile..."); + const [processingCompleteTitle, setProcessingCompleteTitle] = useState("Profile Ready!"); + const [isProcessingComplete, setIsProcessingComplete] = useState(false); + + // Debounced nametag availability check + useEffect(() => { + const cleanTag = nametagInput.trim().replace(/^@/, ""); + if (!cleanTag || cleanTag.length < 2) { + setNametagAvailability("idle"); + return; + } + + let cancelled = false; + setNametagAvailability("checking"); + + const timer = setTimeout(async () => { + const maxAttempts = 2; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (cancelled) return; + try { + const available = await isNametagAvailable(cleanTag); + if (!cancelled) { + setNametagAvailability(available ? "available" : "taken"); + } + return; + } catch { + if (attempt < maxAttempts) { + await new Promise((r) => setTimeout(r, 1500)); + } + } + } + // All attempts failed + if (!cancelled) { + setNametagAvailability("idle"); + } + }, 500); + + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [nametagInput, isNametagAvailable]); + + // Go back to start screen + const goToStart = useCallback(() => { + setStep("start"); + setSeedWords(Array(12).fill("")); + setPassword(""); + setConfirmPassword(""); + setGeneratedMnemonic(null); + setError(null); + }, []); + + // Action: Create wallet with password + const handleCreateWallet = useCallback(async () => { + if (!password) { + setError("Please enter a password"); + return; + } + if (password.length < 8) { + setError("Password must be at least 8 characters"); + return; + } + if (password !== confirmPassword) { + setError("Passwords do not match"); + return; + } + + setIsBusy(true); + setError(null); + + try { + const result = await createWallet(password); + setGeneratedMnemonic(result.mnemonic); + setStep("mnemonicBackup"); + } catch (e) { + const message = e instanceof Error ? e.message : "Failed to create wallet"; + setError(message); + } finally { + setIsBusy(false); + } + }, [password, confirmPassword, createWallet]); + + // Action: Confirm mnemonic backup and proceed to nametag + const handleMnemonicBackupConfirm = useCallback(() => { + setStep("nametag"); + }, []); + + // Action: Restore wallet from mnemonic + password + const handleRestoreWallet = useCallback(async () => { + const words = seedWords.map((w) => w.trim().toLowerCase()); + const missingIndex = words.findIndex((w) => w === ""); + + if (missingIndex !== -1) { + setError(`Please fill in word ${missingIndex + 1}`); + return; + } + + if (!password) { + setError("Please enter a password"); + return; + } + if (password.length < 8) { + setError("Password must be at least 8 characters"); + return; + } + if (password !== confirmPassword) { + setError("Passwords do not match"); + return; + } + + setIsBusy(true); + setError(null); + + try { + const mnemonic = words.join(" "); + await importWallet(mnemonic, password); + setStep("nametag"); + } catch (e) { + const message = e instanceof Error ? e.message : "Invalid recovery phrase"; + setError(message); + } finally { + setIsBusy(false); + } + }, [seedWords, password, confirmPassword, importWallet]); + + // Action: Register nametag + const handleMintNametag = useCallback(async () => { + if (!nametagInput.trim()) return; + + setIsBusy(true); + setError(null); + + const cleanTag = nametagInput.trim().replace("@", ""); + + setStep("processing"); + setProcessingTitle("Setting up Profile..."); + setProcessingCompleteTitle("Profile Ready!"); + setProcessingStep(0); + setProcessingTotalSteps(3); + setProcessingStatus("Checking Unicity ID availability..."); + setIsProcessingComplete(false); + + try { + // Step 1: Check availability + const available = await isNametagAvailable(cleanTag); + if (!available) { + setError(`@${cleanTag} is already taken`); + setStep("nametag"); + setIsBusy(false); + return; + } + + setProcessingStep(1); + setProcessingStatus("Registering Unicity ID..."); + + // Step 2: Register nametag + await registerNametag(cleanTag); + + setProcessingStep(2); + setProcessingStatus("Setup complete!"); + setIsProcessingComplete(true); + } catch (e) { + const message = e instanceof Error ? e.message : "Failed to register Unicity ID"; + console.error("Nametag registration failed:", e); + setError(message); + setStep("nametag"); + } finally { + setIsBusy(false); + } + }, [nametagInput, isNametagAvailable, registerNametag]); + + // Action: Skip nametag + const handleSkipNametag = useCallback(() => { + setStep("processing"); + setProcessingTitle("Setting up Profile..."); + setProcessingCompleteTitle("Profile Ready!"); + setProcessingTotalSteps(1); + setProcessingStep(1); + setProcessingStatus("Setup complete!"); + setIsProcessingComplete(true); + }, []); + + // Action: Complete onboarding + const handleCompleteOnboarding = useCallback(() => { + // Reload to let the app detect the wallet and show the main UI + window.location.reload(); + }, []); + + return { + // Step management + step, + setStep, + goToStart, + + // State + isBusy, + error, + + // Password state + password, + setPassword, + confirmPassword, + setConfirmPassword, + + // Mnemonic restore state + seedWords, + setSeedWords, + + // Generated mnemonic + generatedMnemonic, + + // Nametag state + nametagInput, + setNametagInput, + nametagAvailability, + + // Processing state + processingStatus, + processingStep, + processingTotalSteps, + processingTitle, + processingCompleteTitle, + isProcessingComplete, + + // Actions + handleCreateWallet, + handleRestoreWallet, + handleMnemonicBackupConfirm, + handleMintNametag, + handleSkipNametag, + handleCompleteOnboarding, + }; +} diff --git a/src/components/wallet/shared/AddressSelector.tsx b/src/components/wallet/shared/AddressSelector.tsx new file mode 100644 index 0000000..be031e3 --- /dev/null +++ b/src/components/wallet/shared/AddressSelector.tsx @@ -0,0 +1,98 @@ +import { useState, useCallback } from 'react'; +import { Copy, Check } from 'lucide-react'; +import { useIdentity } from '@/sdk'; + +/** Truncate long nametags: show first 6 chars + ... + last 3 chars */ +function truncateNametag(nametag: string, maxLength: number = 20): string { + if (nametag.length <= maxLength) return nametag; + return `${nametag.slice(0, 6)}...${nametag.slice(-3)}`; +} + +interface AddressSelectorProps { + /** Compact mode - just show nametag with copy button */ + compact?: boolean; +} + +export function AddressSelector({ compact = true }: AddressSelectorProps) { + const [copied, setCopied] = useState<'nametag' | 'address' | false>(false); + const { nametag, directAddress } = useIdentity(); + + const handleCopyNametag = useCallback(async () => { + if (!nametag) return; + try { + await navigator.clipboard.writeText(`@${nametag}`); + setCopied('nametag'); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy nametag:', err); + } + }, [nametag]); + + const handleCopyDirectAddress = useCallback(async () => { + if (!directAddress) return; + try { + await navigator.clipboard.writeText(directAddress); + setCopied('address'); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy direct address:', err); + } + }, [directAddress]); + + if (compact) { + return ( +
+ {nametag ? ( + <> + + @{truncateNametag(nametag)} + + + + ) : directAddress ? ( + <> + + {directAddress.slice(0, 8)}...{directAddress.slice(-4)} + + + + ) : null} +
+ ); + } + + // Full mode (placeholder for future use) + return ( +
+ {nametag ? ( + @{nametag} + ) : directAddress ? ( + + {directAddress.slice(0, 8)}...{directAddress.slice(-6)} + + ) : ( + ... + )} +
+ ); +} diff --git a/src/components/wallet/shared/AssetRow.tsx b/src/components/wallet/shared/AssetRow.tsx new file mode 100644 index 0000000..1437872 --- /dev/null +++ b/src/components/wallet/shared/AssetRow.tsx @@ -0,0 +1,136 @@ +import { type Asset, TokenRegistry } from '@unicitylabs/sphere-sdk'; +import { Box, Loader2 } from 'lucide-react'; +import { memo } from 'react'; + +interface AssetRowProps { + asset: Asset; + showBalances: boolean; + delay: number; + onClick?: () => void; + layer?: 'L1' | 'L3'; + /** If true, animate entrance. If false, render without animation (asset was already shown) */ + isNew?: boolean; +} + +// Custom comparison: allow re-render when amount or price changes +function areAssetPropsEqual(prev: AssetRowProps, next: AssetRowProps): boolean { + return ( + prev.asset.coinId === next.asset.coinId && + prev.asset.symbol === next.asset.symbol && + prev.asset.totalAmount === next.asset.totalAmount && + prev.asset.tokenCount === next.asset.tokenCount && + prev.asset.unconfirmedTokenCount === next.asset.unconfirmedTokenCount && + prev.asset.transferringTokenCount === next.asset.transferringTokenCount && + prev.asset.priceUsd === next.asset.priceUsd && + prev.asset.change24h === next.asset.change24h && + prev.asset.iconUrl === next.asset.iconUrl && + prev.showBalances === next.showBalances && + prev.layer === next.layer && + prev.isNew === next.isNew && + prev.delay === next.delay + ); +} + +// Static fiat value display (replaces AnimatedFiatValue) +function FiatValue({ value, showBalances }: { value: number; showBalances: boolean }) { + if (!showBalances) return ••••••; + const formatted = `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + return {formatted}; +} + +// Static amount display (replaces AnimatedAmount) +function AmountDisplay({ value, symbol, decimals, showBalances }: { + value: number; + symbol: string; + decimals: number; + showBalances: boolean; +}) { + if (!showBalances) return ••••; + const formatted = value.toLocaleString('en-US', { + minimumFractionDigits: Math.min(decimals, 4), + maximumFractionDigits: Math.min(decimals, 4) + }); + return {formatted} {symbol}; +} + +export const AssetRow = memo(function AssetRow({ asset, showBalances, delay, onClick, layer, isNew = true }: AssetRowProps) { + const change24h = asset.change24h ?? 0; + const changeColor = change24h >= 0 ? 'text-emerald-500 dark:text-emerald-400' : 'text-red-500 dark:text-red-400'; + const changeSign = change24h >= 0 ? '+' : ''; + + const fiatValue = asset.fiatValueUsd ?? 0; + const numericAmount = Number(asset.totalAmount) / Math.pow(10, asset.decimals); + + const className = `p-3 rounded-xl transition-all group border border-transparent hover:border-neutral-200/50 dark:hover:border-white/5 ${onClick ? 'cursor-pointer hover:translate-x-1' : ''}`; + + const content = ( +
+
+
+ {(asset.iconUrl || TokenRegistry.getInstance().getIconUrl(asset.coinId)) ? ( + {asset.symbol} + ) : ( + + )} +
+ +
+
+
{asset.symbol}
+ {layer && ( + + {layer} + + )} +
+ {asset.name} +
+ {asset.transferringTokenCount > 0 && ( + + + {asset.transferringTokenCount} sending + + )} + {asset.unconfirmedTokenCount - asset.transferringTokenCount > 0 && ( + + + {asset.unconfirmedTokenCount - asset.transferringTokenCount} pending + + )} +
+
+ +
+
+
+ +
+
+ +
+
+ {changeSign}{change24h.toFixed(2)}% +
+
+
+ ); + + return ( +
+ {content} +
+ ); +}, areAssetPropsEqual); diff --git a/src/components/wallet/shared/BackupWalletModal.tsx b/src/components/wallet/shared/BackupWalletModal.tsx new file mode 100644 index 0000000..5f609fa --- /dev/null +++ b/src/components/wallet/shared/BackupWalletModal.tsx @@ -0,0 +1,57 @@ +import { Download, Key, ShieldCheck } from 'lucide-react'; +import { BaseModal, MenuButton } from '@/components/ui'; + +interface BackupWalletModalProps { + isOpen: boolean; + onClose: () => void; + onExportWalletFile: () => void; + onShowRecoveryPhrase: () => void; + hasMnemonic?: boolean; +} + +export function BackupWalletModal({ + isOpen, + onClose, + onExportWalletFile, + onShowRecoveryPhrase, + hasMnemonic = true, +}: BackupWalletModalProps) { + return ( + +
+
+ +
+

Backup Wallet

+

+ Choose how you want to backup your wallet +

+
+
+ { onClose(); onExportWalletFile(); }} + /> + { onClose(); onShowRecoveryPhrase(); }} + /> + +
+
+ ); +} diff --git a/src/components/wallet/shared/LogoutConfirmModal.tsx b/src/components/wallet/shared/LogoutConfirmModal.tsx new file mode 100644 index 0000000..470ab98 --- /dev/null +++ b/src/components/wallet/shared/LogoutConfirmModal.tsx @@ -0,0 +1,80 @@ +import { useState, useEffect } from 'react'; +import { AlertTriangle, Download, LogOut, Loader2 } from 'lucide-react'; +import { BaseModal, Button } from '@/components/ui'; + +interface LogoutConfirmModalProps { + isOpen: boolean; + onClose: () => void; + onBackupAndLogout: () => void; + onLogoutWithoutBackup: () => void; + isLoggingOut?: boolean; +} + +export function LogoutConfirmModal({ + isOpen, + onClose, + onBackupAndLogout, + onLogoutWithoutBackup, + isLoggingOut = false, +}: LogoutConfirmModalProps) { + const [logoutStatus, setLogoutStatus] = useState('Closing connections...'); + + useEffect(() => { + if (!isLoggingOut) { + setLogoutStatus('Closing connections...'); + return; + } + const timer = setTimeout(() => { + setLogoutStatus('Clearing wallet data...'); + }, 800); + return () => clearTimeout(timer); + }, [isLoggingOut]); + + return ( + {} : onClose} size="sm" showOrbs={false}> +
+
+ +
+

Logout from Wallet?

+

+ All local data will be deleted. Make sure you have a backup to restore your wallet later. +

+
+ +
+ {isLoggingOut ? ( +
+ + Logging out... +
+
+ {logoutStatus} +
+
+ ) : ( + <> + + + + + + + )} +
+ + ); +} diff --git a/src/components/wallet/shared/RegisterNametagModal.tsx b/src/components/wallet/shared/RegisterNametagModal.tsx new file mode 100644 index 0000000..90f02db --- /dev/null +++ b/src/components/wallet/shared/RegisterNametagModal.tsx @@ -0,0 +1,216 @@ +import { useState, useCallback, useEffect } from 'react'; +import { X, Loader2, ArrowRight, Tag, CheckCircle2, AlertCircle } from 'lucide-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useSphereContext } from '@/sdk/context'; +import { SPHERE_KEYS } from '@/sdk/queryKeys'; + +type NametagAvailability = 'idle' | 'checking' | 'available' | 'taken'; + +interface RegisterNametagModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function RegisterNametagModal({ isOpen, onClose }: RegisterNametagModalProps) { + const [nametagInput, setNametagInput] = useState(''); + const [error, setError] = useState(null); + const [isBusy, setIsBusy] = useState(false); + const [success, setSuccess] = useState(false); + const [availability, setAvailability] = useState('idle'); + + const { registerNametag, isNametagAvailable } = useSphereContext(); + const queryClient = useQueryClient(); + + // Debounced nametag availability check + useEffect(() => { + const cleanTag = nametagInput.trim().replace(/^@/, ''); + if (!cleanTag || cleanTag.length < 2) { + setAvailability('idle'); + return; + } + + setAvailability('checking'); + const timer = setTimeout(async () => { + try { + const available = await isNametagAvailable(cleanTag); + setAvailability(available ? 'available' : 'taken'); + } catch { + setAvailability('idle'); + } + }, 500); + + return () => clearTimeout(timer); + }, [nametagInput, isNametagAvailable]); + + // Reset state when modal closes + useEffect(() => { + if (!isOpen) { + setNametagInput(''); + setError(null); + setAvailability('idle'); + setSuccess(false); + } + }, [isOpen]); + + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value.toLowerCase(); + if (/^[a-z0-9_\-+.]*$/.test(value)) { + setNametagInput(value); + setError(null); + } + }; + + const canSubmit = nametagInput.trim().length >= 2 && !isBusy && availability !== 'taken' && availability !== 'checking'; + + const handleSubmit = useCallback(async () => { + if (!nametagInput.trim() || isBusy) return; + + setIsBusy(true); + setError(null); + + try { + const cleanTag = nametagInput.trim().replace('@', ''); + + // Double-check availability (debounced check may be stale) + const available = await isNametagAvailable(cleanTag); + if (!available) { + setError(`@${cleanTag} is already taken`); + setAvailability('taken'); + setIsBusy(false); + return; + } + + await registerNametag(cleanTag); + + queryClient.invalidateQueries({ queryKey: SPHERE_KEYS.identity.all }); + queryClient.invalidateQueries({ queryKey: SPHERE_KEYS.payments.all }); + window.dispatchEvent(new Event('wallet-updated')); + + setSuccess(true); + setTimeout(() => { + onClose(); + setSuccess(false); + setNametagInput(''); + }, 1500); + } catch (e) { + setError(e instanceof Error ? e.message : 'Registration failed'); + } finally { + setIsBusy(false); + } + }, [nametagInput, isBusy, isNametagAvailable, registerNametag, queryClient, onClose]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && canSubmit) { + handleSubmit(); + } + }; + + if (!isOpen) return null; + + return ( + <> +
+
+
e.stopPropagation()} + > + {/* Header */} +
+
+ + Register Unicity ID +
+ +
+ + {/* Content */} +
+

+ Choose a unique ID to receive tokens easily without sharing long addresses. +

+ + {success ? ( +
+

Registered successfully!

+
+ ) : ( + <> +
+
+ {availability === 'checking' && } + {availability === 'available' && } + {availability === 'taken' && } + + @unicity + +
+ +
+ + {/* Availability status -- fixed height to prevent layout shift */} +
+ {availability === 'taken' && !error && ( +

+ @{nametagInput} is already taken +

+ )} + {availability === 'available' && ( +

+ @{nametagInput} is available +

+ )} +
+ + + + {error && ( +

+ {error} +

+ )} + + )} +
+
+
+ + ); +} diff --git a/src/components/wallet/shared/SaveWalletModal.tsx b/src/components/wallet/shared/SaveWalletModal.tsx new file mode 100644 index 0000000..7b8bf03 --- /dev/null +++ b/src/components/wallet/shared/SaveWalletModal.tsx @@ -0,0 +1,116 @@ +import { useState } from 'react'; +import { Shield, AlertCircle, FileJson } from 'lucide-react'; +import { BaseModal } from '@/components/ui'; + +interface SaveWalletModalProps { + show: boolean; + onConfirm: (filename: string, password?: string) => void; + onCancel: () => void; + hasMnemonic?: boolean; +} + +export function SaveWalletModal({ show, onConfirm, onCancel, hasMnemonic }: SaveWalletModalProps) { + const [filename, setFilename] = useState('alpha_wallet_backup'); + const [password, setPassword] = useState(''); + const [passwordConfirm, setPasswordConfirm] = useState(''); + const [error, setError] = useState(''); + + if (!show) return null; + + const handleConfirm = () => { + setError(''); + if (password) { + if (password !== passwordConfirm) { + setError('Passwords do not match!'); + return; + } + if (password.length < 4) { + setError('Password must be at least 4 characters'); + return; + } + } + onConfirm(filename, password || undefined); + setFilename('alpha_wallet_backup'); + setPassword(''); + setPasswordConfirm(''); + setError(''); + }; + + return ( + +
+
+ +
+

Backup Wallet

+

+ Export your wallet keys to a JSON file. Keep this safe! +

+
+ +
+ {/* Format indicator */} +
+ + JSON Format + {hasMnemonic && ( + + +mnemonic + + )} +
+ +

+ Includes verification address{hasMnemonic ? ' and recovery phrase' : ''} +

+ + + setFilename(e.target.value)} + className="w-full mb-3 px-3 py-2 bg-neutral-100 dark:bg-neutral-800 rounded text-neutral-800 dark:text-neutral-200 placeholder-neutral-400 border border-neutral-200 dark:border-neutral-700 focus:border-blue-500 outline-none transition-colors" + /> + + + setPassword(e.target.value)} + className="w-full mb-3 px-3 py-2 bg-neutral-100 dark:bg-neutral-800 rounded text-neutral-800 dark:text-neutral-200 placeholder-neutral-400 border border-neutral-200 dark:border-neutral-700 focus:border-blue-500 outline-none transition-colors" + /> + + setPasswordConfirm(e.target.value)} + className="w-full mb-4 px-3 py-2 bg-neutral-100 dark:bg-neutral-800 rounded text-neutral-800 dark:text-neutral-200 placeholder-neutral-400 border border-neutral-200 dark:border-neutral-700 focus:border-blue-500 outline-none transition-colors" + /> + + {error && ( +
+ + {error} +
+ )} + +
+ + +
+
+
+ ); +} diff --git a/src/components/wallet/shared/TokenRow.tsx b/src/components/wallet/shared/TokenRow.tsx new file mode 100644 index 0000000..81d1aa0 --- /dev/null +++ b/src/components/wallet/shared/TokenRow.tsx @@ -0,0 +1,136 @@ +import type { Token } from '@unicitylabs/sphere-sdk'; +import { TokenRegistry } from '@unicitylabs/sphere-sdk'; +import { Box, Copy, CheckCircle2, Loader2 } from 'lucide-react'; +import { useState, memo } from 'react'; + +interface TokenRowProps { + token: Token; + delay: number; + /** If true, animate entrance. If false, render without animation (token was already shown) */ + isNew?: boolean; +} + +// Custom comparison: allow re-render when amount changes +function areTokenPropsEqual(prev: TokenRowProps, next: TokenRowProps): boolean { + return ( + prev.token.id === next.token.id && + prev.token.status === next.token.status && + prev.token.symbol === next.token.symbol && + prev.isNew === next.isNew && + prev.delay === next.delay + ); +} + +// Helper to parse token amount to numeric value +function parseTokenAmount(amount: string | undefined, coinId: string | undefined): number { + try { + if (!amount || !coinId) return 0; + const amountFloat = parseFloat(amount); + const registry = TokenRegistry.getInstance(); + const def = registry.getDefinition(coinId); + const decimals = def?.decimals ?? 6; + const divisor = Math.pow(10, decimals); + return amountFloat / divisor; + } catch { + return 0; + } +} + +// Helper to format numeric value back to display string +function formatTokenAmount(value: number, coinId: string | undefined): string { + try { + if (!coinId) return value.toString(); + const registry = TokenRegistry.getInstance(); + const def = registry.getDefinition(coinId); + const decimals = def?.decimals ?? 6; + return new Intl.NumberFormat('en-US', { + maximumFractionDigits: Math.min(decimals, 6) + }).format(value); + } catch { + return value.toString(); + } +} + +// Static token amount display (replaces AnimatedTokenAmount) +function TokenAmountDisplay({ amount, coinId, symbol }: { + amount: string | undefined; + coinId: string | undefined; + symbol: string | undefined; +}) { + const numericAmount = parseTokenAmount(amount, coinId); + const formatted = formatTokenAmount(numericAmount, coinId); + return {formatted} {symbol || ''}; +} + +export const TokenRow = memo(function TokenRow({ token, delay, isNew = true }: TokenRowProps) { + const [copied, setCopied] = useState(false); + + const handleCopyId = (e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(token.id); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const className = "p-3 rounded-xl bg-neutral-50 dark:bg-neutral-800/30 border border-neutral-200/50 dark:border-white/5 hover:border-neutral-300 dark:hover:border-white/10 transition-all group"; + + const amountDisplay = ( + + ); + + const tokenContent = ( +
+
+
+ {(token.iconUrl || TokenRegistry.getInstance().getIconUrl(token.coinId)) ? ( + {token.symbol} + ) : ( + + )} +
+
+
+ {amountDisplay} +
+
+ ID: {token.id.slice(0, 8)}... + {copied ? : } +
+
+
+
+ {token.status === 'confirmed' ? ( + + Confirmed + + ) : token.status === 'transferring' ? ( + + + Sending + + ) : ( + + + Pending + + )} + + {new Date(token.createdAt).toLocaleDateString()} + +
+
+ ); + + return ( +
+ {tokenContent} +
+ ); +}, areTokenPropsEqual); diff --git a/src/components/wallet/shared/index.ts b/src/components/wallet/shared/index.ts new file mode 100644 index 0000000..3be050a --- /dev/null +++ b/src/components/wallet/shared/index.ts @@ -0,0 +1,6 @@ +export { AssetRow } from './AssetRow'; +export { TokenRow } from './TokenRow'; +export { AddressSelector } from './AddressSelector'; +export { RegisterNametagModal } from './RegisterNametagModal'; +export { BackupWalletModal } from './BackupWalletModal'; +export { LogoutConfirmModal } from './LogoutConfirmModal'; diff --git a/src/platform/extension/SphereProvider.tsx b/src/platform/extension/SphereProvider.tsx new file mode 100644 index 0000000..c98574c --- /dev/null +++ b/src/platform/extension/SphereProvider.tsx @@ -0,0 +1,263 @@ +import React, { useEffect, useState, useCallback, useRef } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { SphereContext, type SphereContextValue } from '@/sdk/context'; +import { SPHERE_KEYS } from '@/sdk/queryKeys'; +import type { WalletIdentity } from '@/sdk/types'; + +async function sendMessage(message: Record): Promise { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(message, (response) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + if (response?.success === false) { + reject(new Error(response.error || 'Unknown error')); + return; + } + resolve(response); + }); + }); +} + +export function ExtensionSphereProvider({ children }: { children: React.ReactNode }) { + const queryClient = useQueryClient(); + const [walletExists, setWalletExists] = useState(false); + const [isUnlocked, setIsUnlocked] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [identity, setIdentity] = useState(null); + const [nametag, setNametag] = useState(null); + const updateCallbacksRef = useRef void>>(new Set()); + + // Fetch initial state + useEffect(() => { + (async () => { + try { + const res = await sendMessage({ type: 'POPUP_GET_STATE' }); + setWalletExists(res.state.hasWallet); + setIsUnlocked(res.state.isUnlocked); + + if (res.state.isUnlocked) { + try { + const idRes = await sendMessage({ type: 'POPUP_GET_IDENTITIES' }); + if (idRes.identities?.[0]) { + const id = idRes.identities[0]; + setIdentity({ + chainPubkey: id.publicKey, + l1Address: id.id, + directAddress: id.id, + nametag: id.label?.startsWith('@') ? id.label.slice(1) : undefined, + }); + } + const ntRes = await sendMessage({ type: 'POPUP_GET_MY_NAMETAG' }); + if (ntRes.nametag) { + setNametag(ntRes.nametag.nametag); + } + } catch {} + } + } catch (err) { + setError((err as Error).message); + } finally { + setIsLoading(false); + } + })(); + }, []); + + // Listen for background broadcasts + useEffect(() => { + const listener = (message: any) => { + if (message.type === 'BALANCES_UPDATED' || message.type === 'WALLET_UPDATE') { + queryClient.invalidateQueries({ queryKey: SPHERE_KEYS.payments.all }); + queryClient.invalidateQueries({ queryKey: SPHERE_KEYS.identity.all }); + updateCallbacksRef.current.forEach(cb => cb()); + } + if (message.type === 'PAYMENT_REQUEST_INCOMING') { + updateCallbacksRef.current.forEach(cb => cb()); + } + }; + chrome.runtime.onMessage.addListener(listener); + return () => chrome.runtime.onMessage.removeListener(listener); + }, [queryClient]); + + const createWallet = useCallback(async (password: string) => { + const res = await sendMessage({ type: 'POPUP_CREATE_WALLET', password }); + setWalletExists(true); + setIsUnlocked(true); + if (res.identity) { + setIdentity({ + chainPubkey: res.identity.publicKey, + l1Address: res.identity.id, + directAddress: res.identity.id, + }); + } + return { mnemonic: res.mnemonic }; + }, []); + + const importWallet = useCallback(async (mnemonic: string, password: string) => { + const res = await sendMessage({ type: 'POPUP_IMPORT_WALLET', mnemonic, password }); + setWalletExists(true); + setIsUnlocked(true); + if (res.identity) { + setIdentity({ + chainPubkey: res.identity.publicKey, + l1Address: res.identity.id, + directAddress: res.identity.id, + }); + } + }, []); + + const unlockWallet = useCallback(async (password: string) => { + const res = await sendMessage({ type: 'POPUP_UNLOCK_WALLET', password }); + setIsUnlocked(true); + if (res.identity) { + setIdentity({ + chainPubkey: res.identity.publicKey, + l1Address: res.identity.id, + directAddress: res.identity.id, + nametag: res.identity.label?.startsWith('@') ? res.identity.label.slice(1) : undefined, + }); + } + // Fetch nametag + try { + const ntRes = await sendMessage({ type: 'POPUP_GET_MY_NAMETAG' }); + if (ntRes.nametag) setNametag(ntRes.nametag.nametag); + } catch {} + }, []); + + const lockWallet = useCallback(async () => { + await sendMessage({ type: 'POPUP_LOCK_WALLET' }); + setIsUnlocked(false); + setIdentity(null); + setNametag(null); + queryClient.clear(); + }, [queryClient]); + + const deleteWallet = useCallback(async () => { + await sendMessage({ type: 'POPUP_RESET_WALLET' }); + setWalletExists(false); + setIsUnlocked(false); + setIdentity(null); + setNametag(null); + queryClient.clear(); + }, [queryClient]); + + const getAssets = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_GET_ASSETS' }); + return res.assets ?? []; + }, []); + + const getTokens = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_GET_TOKENS' }); + return res.tokens ?? []; + }, []); + + const getTransactionHistory = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_GET_TRANSACTION_HISTORY' }); + return res.history ?? []; + }, []); + + const getIdentity = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_GET_IDENTITY' }); + return res.identity ?? null; + }, []); + + const send = useCallback(async (params: { coinId: string; amount: string; recipient: string; memo?: string }) => { + return sendMessage({ type: 'POPUP_SEND_TOKENS', ...params }); + }, []); + + const resolve = useCallback(async (recipient: string) => { + const res = await sendMessage({ type: 'POPUP_RESOLVE_NAMETAG', nametag: recipient }); + return res.resolution ?? null; + }, []); + + const registerNametag = useCallback(async (tag: string) => { + const res = await sendMessage({ type: 'POPUP_REGISTER_NAMETAG', nametag: tag }); + if (res.nametag) setNametag(res.nametag.nametag); + return res.nametag; + }, []); + + const isNametagAvailable = useCallback(async (tag: string) => { + const res = await sendMessage({ type: 'POPUP_CHECK_NAMETAG_AVAILABLE', nametag: tag }); + return res.available ?? false; + }, []); + + const getMyNametag = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_GET_MY_NAMETAG' }); + return res.nametag ?? null; + }, []); + + const getMnemonic = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_GET_MNEMONIC' }); + return res.mnemonic ?? null; + }, []); + + const exportWallet = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_EXPORT_WALLET' }); + return res.walletJson ?? ''; + }, []); + + const getPendingTransactions = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_GET_PENDING_TRANSACTIONS' }); + return res.transactions ?? []; + }, []); + + const approveTransaction = useCallback(async (requestId: string) => { + await sendMessage({ type: 'POPUP_APPROVE_TRANSACTION', requestId }); + }, []); + + const rejectTransaction = useCallback(async (requestId: string) => { + await sendMessage({ type: 'POPUP_REJECT_TRANSACTION', requestId }); + }, []); + + const getAggregatorConfig = useCallback(async () => { + const res = await sendMessage({ type: 'POPUP_GET_AGGREGATOR_CONFIG' }); + return res.config; + }, []); + + const setAggregatorConfig = useCallback(async (config: any) => { + await sendMessage({ type: 'POPUP_SET_AGGREGATOR_CONFIG', config }); + }, []); + + const onWalletUpdate = useCallback((callback: () => void) => { + updateCallbacksRef.current.add(callback); + return () => { updateCallbacksRef.current.delete(callback); }; + }, []); + + const value: SphereContextValue = { + walletExists, + isUnlocked, + isLoading, + error, + identity, + nametag, + createWallet, + importWallet, + unlockWallet, + lockWallet, + deleteWallet, + getAssets, + getTokens, + getTransactionHistory, + getIdentity, + send, + resolve, + registerNametag, + isNametagAvailable, + getMyNametag, + getMnemonic, + exportWallet, + getPendingTransactions, + approveTransaction, + rejectTransaction, + getAggregatorConfig, + setAggregatorConfig, + onWalletUpdate, + }; + + return ( + + {children} + + ); +} diff --git a/src/background/index.ts b/src/platform/extension/background/index.ts similarity index 100% rename from src/background/index.ts rename to src/platform/extension/background/index.ts diff --git a/src/background/message-handler.ts b/src/platform/extension/background/message-handler.ts similarity index 97% rename from src/background/message-handler.ts rename to src/platform/extension/background/message-handler.ts index 29ad7a8..f86d497 100644 --- a/src/background/message-handler.ts +++ b/src/platform/extension/background/message-handler.ts @@ -346,6 +346,30 @@ export async function handlePopupMessage( return { success: true, ...result }; } + case 'POPUP_GET_ASSETS': + return { + success: true, + assets: await walletManager.getAssets(), + }; + + case 'POPUP_GET_TOKENS': + return { + success: true, + tokens: walletManager.getTokenList(), + }; + + case 'POPUP_GET_TRANSACTION_HISTORY': + return { + success: true, + history: walletManager.getTransactionHistory(), + }; + + case 'POPUP_GET_IDENTITY': + return { + success: true, + identity: walletManager.getFullIdentity(), + }; + default: return { success: false, diff --git a/src/background/nametag-mint-service.ts b/src/platform/extension/background/nametag-mint-service.ts similarity index 100% rename from src/background/nametag-mint-service.ts rename to src/platform/extension/background/nametag-mint-service.ts diff --git a/src/background/nostr-keys.ts b/src/platform/extension/background/nostr-keys.ts similarity index 100% rename from src/background/nostr-keys.ts rename to src/platform/extension/background/nostr-keys.ts diff --git a/src/background/providers/chrome-storage-provider.ts b/src/platform/extension/background/providers/chrome-storage-provider.ts similarity index 100% rename from src/background/providers/chrome-storage-provider.ts rename to src/platform/extension/background/providers/chrome-storage-provider.ts diff --git a/src/platform/extension/background/providers/index.ts b/src/platform/extension/background/providers/index.ts new file mode 100644 index 0000000..f1ea61f --- /dev/null +++ b/src/platform/extension/background/providers/index.ts @@ -0,0 +1,2 @@ +export { ChromeStorageProvider, createChromeStorageProvider } from './chrome-storage-provider'; +export type { ChromeStorageProviderConfig } from './chrome-storage-provider'; diff --git a/src/background/storage.ts b/src/platform/extension/background/storage.ts similarity index 100% rename from src/background/storage.ts rename to src/platform/extension/background/storage.ts diff --git a/src/background/wallet-manager.ts b/src/platform/extension/background/wallet-manager.ts similarity index 95% rename from src/background/wallet-manager.ts rename to src/platform/extension/background/wallet-manager.ts index 8de7191..d3f5e32 100644 --- a/src/background/wallet-manager.ts +++ b/src/platform/extension/background/wallet-manager.ts @@ -378,6 +378,75 @@ export class WalletManager { return balances; } + /** + * Get assets list (v0.5.3 API or fallback). + */ + async getAssets(): Promise { + const sphere = this.getSphere(); + try { + // Try v0.5.3 API first + if (typeof sphere.payments.getAssets === 'function') { + return await sphere.payments.getAssets(); + } + // Fallback: aggregate from tokens + return this.getBalances().map(b => ({ + coinId: b.coinId, + symbol: b.symbol, + totalAmount: b.amount, + confirmedAmount: b.amount, + unconfirmedAmount: b.pendingAmount || '0', + decimals: COIN_DECIMALS[b.coinId] ?? DEFAULT_DECIMALS, + })); + } catch (error) { + console.error('[WalletManager] Error getting assets:', error); + return []; + } + } + + /** + * Get individual tokens list. + */ + getTokenList(): any[] { + const sphere = this.getSphere(); + try { + return sphere.payments.getTokens(); + } catch (error) { + console.error('[WalletManager] Error getting tokens:', error); + return []; + } + } + + /** + * Get transaction history. + */ + getTransactionHistory(): any[] { + const sphere = this.getSphere(); + try { + if (typeof sphere.payments.getHistory === 'function') { + return sphere.payments.getHistory(); + } + return []; + } catch (error) { + console.error('[WalletManager] Error getting history:', error); + return []; + } + } + + /** + * Get full identity info for the popup. + */ + getFullIdentity(): any | null { + const sphere = this.getSphere(); + const identity = sphere.identity; + if (!identity) return null; + return { + chainPubkey: identity.chainPubkey, + l1Address: identity.l1Address, + directAddress: identity.directAddress, + nametag: identity.nametag, + }; + } + getBalance(coinId: string): bigint { const sphere = this.getSphere(); try { @@ -468,7 +537,7 @@ export class WalletManager { async purgeInvalidTokens(): Promise<{ purged: number }> { const { invalid } = await this.checkTokenHealth(); for (const tok of invalid) { - await this.getSphere().payments.removeToken(tok.id, undefined, true); + await this.getSphere().payments.removeToken(tok.id); } return { purged: invalid.length }; } diff --git a/src/content/index.ts b/src/platform/extension/content/index.ts similarity index 100% rename from src/content/index.ts rename to src/platform/extension/content/index.ts diff --git a/src/inject/index.ts b/src/platform/extension/inject/index.ts similarity index 100% rename from src/inject/index.ts rename to src/platform/extension/inject/index.ts diff --git a/src/platform/extension/popup/PopupApp.tsx b/src/platform/extension/popup/PopupApp.tsx new file mode 100644 index 0000000..b8a6e1b --- /dev/null +++ b/src/platform/extension/popup/PopupApp.tsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; +import { useWalletStatus } from '@/sdk/hooks'; +import { useSphereContext } from '@/sdk/context'; +import { WalletPanel } from '@/components/wallet/WalletPanel'; +import { UnlockWallet } from '@/components/wallet/UnlockWallet'; + +export function PopupApp() { + const { walletExists, isLoading, isUnlocked } = useWalletStatus(); + const ctx = useSphereContext(); + + if (isLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + if (walletExists && !isUnlocked) { + return ; + } + + return ; +} diff --git a/src/platform/extension/popup/main.tsx b/src/platform/extension/popup/main.tsx new file mode 100644 index 0000000..228f2fd --- /dev/null +++ b/src/platform/extension/popup/main.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ExtensionSphereProvider } from '../SphereProvider'; +import { PopupApp } from './PopupApp'; +import './styles.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + retry: 1, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + +); diff --git a/src/platform/extension/popup/styles.css b/src/platform/extension/popup/styles.css new file mode 100644 index 0000000..1bfcddb --- /dev/null +++ b/src/platform/extension/popup/styles.css @@ -0,0 +1,38 @@ +@import "tailwindcss"; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: #0a0a0a; + color: #fafafa; +} + +#root { + width: 360px; + min-height: 480px; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 4px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 2px; +} + +/* Smooth transitions */ +.transition-colors { + transition-property: color, background-color, border-color; + transition-duration: 150ms; +} diff --git a/src/popup/App.tsx b/src/popup/App.tsx deleted file mode 100644 index 421c897..0000000 --- a/src/popup/App.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Main popup application component. - */ - -import { useEffect } from 'react'; -import { useStore } from './store'; -import { useWallet } from './hooks/useWallet'; -import { CreateWallet } from './components/CreateWallet'; -import { ImportWallet } from './components/ImportWallet'; -import { UnlockWallet } from './components/UnlockWallet'; -import { Dashboard } from './components/Dashboard'; -import { Send } from './components/Send'; -import { Receive } from './components/Receive'; -import { RegisterNametag } from './components/RegisterNametag'; -import { Settings } from './components/Settings'; -import { PendingTransactions } from './components/PendingTransactions'; - -export default function App() { - const { view, loading, error } = useStore(); - const { initialize } = useWallet(); - - // Initialize on mount - useEffect(() => { - initialize(); - }, [initialize]); - - // Loading state - if (view === 'loading') { - return ( -
-
-
-

{error || 'Loading...'}

- {error && ( - - )} -
-
- ); - } - - - return ( -
- {/* Header - only show on certain views */} - {(view === 'create-wallet' || view === 'import-wallet') && ( -
-
-

Sphere Wallet

-
- )} - - {/* Main content based on view */} -
- {view === 'create-wallet' && } - {view === 'import-wallet' && } - {view === 'unlock' && } - {view === 'dashboard' && } - {view === 'send' && } - {view === 'receive' && } - {view === 'register-nametag' && } - {view === 'settings' && } - {view === 'pending-transactions' && } -
- - {/* Global loading overlay - shown when loading but not on initial load */} - {loading && ( -
-
-
- )} -
- ); -} diff --git a/src/popup/components/CreateWallet.tsx b/src/popup/components/CreateWallet.tsx deleted file mode 100644 index c267eb7..0000000 --- a/src/popup/components/CreateWallet.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Create wallet view - initial setup flow. - */ - -import { useState } from 'react'; -import { useStore } from '../store'; -import { useWallet } from '../hooks/useWallet'; - -export function CreateWallet() { - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [error, setError] = useState(''); - const [mnemonic, setMnemonic] = useState(null); - const { loading, setView } = useStore(); - const { createWallet } = useWallet(); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - - if (password.length < 8) { - setError('Password must be at least 8 characters'); - return; - } - - if (password !== confirmPassword) { - setError('Passwords do not match'); - return; - } - - try { - const m = await createWallet(password); - setMnemonic(m); - } catch (err) { - setError((err as Error).message); - } - }; - - // Show mnemonic backup screen after wallet creation - if (mnemonic) { - return ( -
-

Backup Recovery Phrase

- -
-

- Write down these words in order and store them safely. - Anyone with this phrase can access your wallet. -

-
- {mnemonic} -
-
- - -
- ); - } - - return ( -
-

Create New Wallet

- -
-
- - setPassword(e.target.value)} - placeholder="Enter password (min 8 characters)" - required - className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 - text-white placeholder-gray-500 - focus:outline-none focus:border-purple-500" - /> -
- -
- - setConfirmPassword(e.target.value)} - placeholder="Confirm password" - required - className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 - text-white placeholder-gray-500 - focus:outline-none focus:border-purple-500" - /> -
- - {error && ( -
{error}
- )} - - -
- -
-

- Already have a wallet?{' '} - -

-
-
- ); -} diff --git a/src/popup/components/Dashboard.tsx b/src/popup/components/Dashboard.tsx deleted file mode 100644 index 3d9e401..0000000 --- a/src/popup/components/Dashboard.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Main dashboard view - shows balances and quick actions. - */ - -import { useState, useEffect } from 'react'; -import { useStore } from '../store'; -import { useWallet } from '../hooks/useWallet'; -import { ALPHA_COIN_ID, DEFAULT_COIN_SYMBOL } from '@/shared/constants'; - -export function Dashboard() { - const { activeIdentity, balances, setView } = useStore(); - const { lockWallet, getAddress } = useWallet(); - const [address, setAddress] = useState(''); - const [copied, setCopied] = useState(false); - - useEffect(() => { - getAddress().then(setAddress).catch(console.error); - }, [getAddress]); - - // Get primary balance (first balance or one matching ALPHA_COIN_ID) - const primaryBalance = balances.find((b) => b.coinId === ALPHA_COIN_ID) || balances[0]; - - const formatBalance = (amount: string): string => { - const num = parseFloat(amount); - if (isNaN(num) || num === 0) return '0'; - if (num < 0.0001 && num > 0) return '< 0.0001'; - return num.toLocaleString(undefined, { maximumFractionDigits: 4 }); - }; - - const truncateAddress = (addr: string): string => { - if (!addr) return ''; - return `${addr.slice(0, 12)}...${addr.slice(-8)}`; - }; - - const copyAddress = async () => { - await navigator.clipboard.writeText(address); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - return ( -
- {/* Header */} -
-
-
-
-
- {activeIdentity?.label || 'Default'} -
-
-
- -
- - {/* Balance Card */} -
-
Total Balance
-
- {formatBalance(primaryBalance?.amount || '0')} {primaryBalance?.symbol || DEFAULT_COIN_SYMBOL} -
- {primaryBalance?.pendingAmount && primaryBalance.pendingAmount !== '0' && ( -
- +{formatBalance(primaryBalance.pendingAmount)} finalizing... -
- )} - - {/* Address */} -
- - {truncateAddress(address)} - - - {copied ? 'Copied!' : 'Copy'} - -
-
- - {/* Quick Actions */} -
- - - -
- - {/* Token List */} -
-

Tokens

-
- {balances.map((balance) => ( -
-
-
- - {balance.symbol.slice(0, 2)} - -
- {balance.symbol} -
-
- - {formatBalance(balance.amount)} - - {balance.pendingAmount && balance.pendingAmount !== '0' && ( -
- +{formatBalance(balance.pendingAmount)} finalizing -
- )} -
-
- ))} - - {balances.length === 0 && ( -
- No tokens yet -
- )} -
-
- - {/* Lock Button */} - -
- ); -} diff --git a/src/popup/components/ImportWallet.tsx b/src/popup/components/ImportWallet.tsx deleted file mode 100644 index 739373f..0000000 --- a/src/popup/components/ImportWallet.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Import wallet view - restore from backup. - */ - -import { useState } from 'react'; -import { useStore } from '../store'; -import { useWallet } from '../hooks/useWallet'; - -export function ImportWallet() { - const [walletJson, setWalletJson] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const { loading, setView } = useStore(); - const { importWallet } = useWallet(); - - const handleFileUpload = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (event) => { - setWalletJson(event.target?.result as string); - }; - reader.readAsText(file); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - - if (!walletJson.trim()) { - setError('Please paste wallet JSON or upload a file'); - return; - } - - if (!password) { - setError('Password is required'); - return; - } - - try { - await importWallet(walletJson, password); - } catch (err) { - setError((err as Error).message); - } - }; - - return ( -
-

Import Wallet

- -
-
- - -
- -
or
- -
- -