@nockchain/rose 0.1.4-nightly.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.github/workflows/artifacts.yml +33 -0
  2. package/.github/workflows/ci.yml +68 -0
  3. package/.github/workflows/publish-sdk.yml +35 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +5 -0
  6. package/.prettierrc +8 -0
  7. package/LICENSE +22 -0
  8. package/README.md +117 -0
  9. package/extension/background/index.ts +1500 -0
  10. package/extension/content/index.ts +59 -0
  11. package/extension/icons/rose.svg +27 -0
  12. package/extension/icons/rose128.png +0 -0
  13. package/extension/icons/rose16.png +0 -0
  14. package/extension/icons/rose256.png +0 -0
  15. package/extension/icons/rose32.png +0 -0
  16. package/extension/icons/rose48.png +0 -0
  17. package/extension/icons/rose512.png +0 -0
  18. package/extension/inpage/index.ts +86 -0
  19. package/extension/manifest.json +48 -0
  20. package/extension/popup/Popup.tsx +94 -0
  21. package/extension/popup/Router.tsx +121 -0
  22. package/extension/popup/assets/arrow-down-icon.svg +3 -0
  23. package/extension/popup/assets/arrow-left-icon.svg +3 -0
  24. package/extension/popup/assets/arrow-right-icon.svg +3 -0
  25. package/extension/popup/assets/arrow-up-icon.svg +3 -0
  26. package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
  27. package/extension/popup/assets/checkmark-icon.svg +3 -0
  28. package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
  29. package/extension/popup/assets/checkmark-success-icon.svg +3 -0
  30. package/extension/popup/assets/clock-icon.svg +3 -0
  31. package/extension/popup/assets/close-x-icon.svg +3 -0
  32. package/extension/popup/assets/copy-icon.svg +6 -0
  33. package/extension/popup/assets/explorer-icon.svg +3 -0
  34. package/extension/popup/assets/eye-off-icon.svg +3 -0
  35. package/extension/popup/assets/eye-open-icon.svg +4 -0
  36. package/extension/popup/assets/feedback-icon.svg +3 -0
  37. package/extension/popup/assets/green-status-dot.svg +3 -0
  38. package/extension/popup/assets/info-icon.svg +3 -0
  39. package/extension/popup/assets/iris-logo-40.svg +27 -0
  40. package/extension/popup/assets/iris-logo-96.svg +27 -0
  41. package/extension/popup/assets/iris-logo-blue.svg +27 -0
  42. package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
  43. package/extension/popup/assets/iris-logo-orange.svg +27 -0
  44. package/extension/popup/assets/iris-logo.svg +27 -0
  45. package/extension/popup/assets/key-icon.svg +3 -0
  46. package/extension/popup/assets/lock-icon-yellow.svg +3 -0
  47. package/extension/popup/assets/lock-icon.svg +3 -0
  48. package/extension/popup/assets/pencil-edit-icon.svg +3 -0
  49. package/extension/popup/assets/permissions-icon.svg +3 -0
  50. package/extension/popup/assets/receipt-icon.svg +5 -0
  51. package/extension/popup/assets/refresh-icon.svg +3 -0
  52. package/extension/popup/assets/settings-gear-icon.svg +8 -0
  53. package/extension/popup/assets/settings-icon.svg +3 -0
  54. package/extension/popup/assets/theme-icon.svg +3 -0
  55. package/extension/popup/assets/trash-bin-icon.svg +3 -0
  56. package/extension/popup/assets/trend-down-arrow.svg +5 -0
  57. package/extension/popup/assets/trend-up-arrow.svg +5 -0
  58. package/extension/popup/assets/user-account-icon.svg +3 -0
  59. package/extension/popup/assets/vector-bottom-left.svg +9 -0
  60. package/extension/popup/assets/vector-left.svg +9 -0
  61. package/extension/popup/assets/vector-right.svg +9 -0
  62. package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
  63. package/extension/popup/assets/vector-top-right.svg +9 -0
  64. package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
  65. package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
  66. package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
  67. package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
  68. package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
  69. package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
  70. package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
  71. package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
  72. package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
  73. package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
  74. package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
  75. package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
  76. package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
  77. package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
  78. package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
  79. package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
  80. package/extension/popup/components/AccountIcon.tsx +78 -0
  81. package/extension/popup/components/AccountSelector.tsx +246 -0
  82. package/extension/popup/components/Alert.tsx +48 -0
  83. package/extension/popup/components/ConfirmModal.tsx +81 -0
  84. package/extension/popup/components/PasswordInput.tsx +49 -0
  85. package/extension/popup/components/ScreenContainer.tsx +17 -0
  86. package/extension/popup/components/SiteIcon.tsx +60 -0
  87. package/extension/popup/components/ThemeToggle.tsx +44 -0
  88. package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
  89. package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
  90. package/extension/popup/components/icons/CheckIcon.tsx +20 -0
  91. package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
  92. package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
  93. package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
  94. package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
  95. package/extension/popup/components/icons/CloseIcon.tsx +26 -0
  96. package/extension/popup/components/icons/CopyIcon.tsx +20 -0
  97. package/extension/popup/components/icons/EditIcon.tsx +20 -0
  98. package/extension/popup/components/icons/EyeIcon.tsx +13 -0
  99. package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
  100. package/extension/popup/components/icons/InfoIcon.tsx +20 -0
  101. package/extension/popup/components/icons/LockIcon.tsx +20 -0
  102. package/extension/popup/components/icons/PlusIcon.tsx +15 -0
  103. package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
  104. package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
  105. package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
  106. package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
  107. package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
  108. package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
  109. package/extension/popup/components/icons/UploadIcon.tsx +20 -0
  110. package/extension/popup/components/icons/WalletIcon.tsx +20 -0
  111. package/extension/popup/contexts/ThemeContext.tsx +105 -0
  112. package/extension/popup/hooks/useApprovalDetection.ts +128 -0
  113. package/extension/popup/hooks/useAutoFocus.ts +36 -0
  114. package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
  115. package/extension/popup/hooks/useClickOutside.ts +33 -0
  116. package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
  117. package/extension/popup/hooks/useFavicon.ts +64 -0
  118. package/extension/popup/hooks/useNumericInput.ts +93 -0
  119. package/extension/popup/index.html +13 -0
  120. package/extension/popup/index.tsx +24 -0
  121. package/extension/popup/screens/AboutScreen.tsx +118 -0
  122. package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
  123. package/extension/popup/screens/HomeScreen.tsx +902 -0
  124. package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
  125. package/extension/popup/screens/LockTimeScreen.tsx +155 -0
  126. package/extension/popup/screens/ReceiveScreen.tsx +149 -0
  127. package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
  128. package/extension/popup/screens/SendReviewScreen.tsx +308 -0
  129. package/extension/popup/screens/SendScreen.tsx +825 -0
  130. package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
  131. package/extension/popup/screens/SettingsScreen.tsx +116 -0
  132. package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
  133. package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
  134. package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
  135. package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
  136. package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
  137. package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
  138. package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
  139. package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
  140. package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
  141. package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
  142. package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
  143. package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
  144. package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
  145. package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
  146. package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
  147. package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
  148. package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
  149. package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
  150. package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
  151. package/extension/popup/screens/system/LockedScreen.tsx +288 -0
  152. package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
  153. package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
  154. package/extension/popup/store.ts +482 -0
  155. package/extension/popup/styles.css +246 -0
  156. package/extension/popup/utils/format.ts +58 -0
  157. package/extension/popup/utils/formatWalletError.ts +36 -0
  158. package/extension/popup/utils/memo.ts +299 -0
  159. package/extension/popup/utils/messaging.ts +16 -0
  160. package/extension/shared/address-encoding.ts +69 -0
  161. package/extension/shared/balance-query.ts +123 -0
  162. package/extension/shared/constants.ts +386 -0
  163. package/extension/shared/currency.ts +128 -0
  164. package/extension/shared/first-name-derivation.ts +128 -0
  165. package/extension/shared/keyfile.ts +58 -0
  166. package/extension/shared/onboarding.ts +78 -0
  167. package/extension/shared/price-api.ts +79 -0
  168. package/extension/shared/rpc-client-browser.ts +315 -0
  169. package/extension/shared/transaction-builder.ts +443 -0
  170. package/extension/shared/types.ts +450 -0
  171. package/extension/shared/utxo-diff.ts +212 -0
  172. package/extension/shared/utxo-store.ts +548 -0
  173. package/extension/shared/utxo-sync.ts +343 -0
  174. package/extension/shared/validators.ts +26 -0
  175. package/extension/shared/vault.ts +1580 -0
  176. package/extension/shared/wallet-crypto.ts +77 -0
  177. package/extension/shared/wasm-utils.ts +76 -0
  178. package/extension/shared/webcrypto.ts +67 -0
  179. package/extension/types/wasm.d.ts +13 -0
  180. package/package.json +39 -0
  181. package/postcss.config.js +6 -0
  182. package/rose-extension-dist.zip +0 -0
  183. package/sdk/README.md +88 -0
  184. package/sdk/examples/app.ts +166 -0
  185. package/sdk/examples/index.html +51 -0
  186. package/sdk/examples/tsconfig.json +15 -0
  187. package/sdk/examples/tx-builder.html +532 -0
  188. package/sdk/examples/tx-builder.ts +1766 -0
  189. package/sdk/package-lock.json +424 -0
  190. package/sdk/package.json +68 -0
  191. package/sdk/src/constants.ts +28 -0
  192. package/sdk/src/errors.ts +74 -0
  193. package/sdk/src/hooks/index.ts +1 -0
  194. package/sdk/src/hooks/use-rose.ts +94 -0
  195. package/sdk/src/index.ts +12 -0
  196. package/sdk/src/provider.ts +396 -0
  197. package/sdk/src/transaction.ts +163 -0
  198. package/sdk/src/types/rose-wasm.d.ts +14 -0
  199. package/sdk/src/types.ts +97 -0
  200. package/sdk/src/wasm.ts +13 -0
  201. package/sdk/tsconfig.json +20 -0
  202. package/sdk/vite.config.examples.ts +32 -0
  203. package/tailwind.config.ts +38 -0
  204. package/tsconfig.json +20 -0
  205. package/vite.config.ts +60 -0
@@ -0,0 +1,902 @@
1
+ import { useState, useLayoutEffect, useEffect, useRef } from 'react';
2
+ import { useStore } from '../store';
3
+ import { useTheme } from '../contexts/ThemeContext';
4
+ import { truncateAddress } from '../utils/format';
5
+ import { send } from '../utils/messaging';
6
+ import { INTERNAL_METHODS, NOCK_TO_NICKS, STORAGE_KEYS } from '../../shared/constants';
7
+ import type { Account } from '../../shared/types';
8
+ import { AccountIcon } from '../components/AccountIcon';
9
+ import { EyeIcon } from '../components/icons/EyeIcon';
10
+ import { EyeOffIcon } from '../components/icons/EyeOffIcon';
11
+ import { CheckIcon } from '../components/icons/CheckIcon';
12
+ import { SendPaperPlaneIcon } from '../components/icons/SendPaperPlaneIcon';
13
+ import { ReceiveCircleIcon } from '../components/icons/ReceiveCircleIcon';
14
+ import { ReceiveArrowIcon } from '../components/icons/ReceiveArrowIcon';
15
+ import { SentArrowIcon } from '../components/icons/SentArrowIcon';
16
+ import { ArrowUpRightIcon } from '../components/icons/ArrowUpRightIcon';
17
+
18
+ import WalletDropdownArrow from '../assets/wallet-dropdown-arrow.svg';
19
+ import LockIconAsset from '../assets/lock-icon.svg';
20
+ import SettingsIconAsset from '../assets/settings-icon.svg';
21
+ import TrendUpArrow from '../assets/trend-up-arrow.svg';
22
+ import TrendDownArrow from '../assets/trend-down-arrow.svg';
23
+ import ExplorerIcon from '../assets/explorer-icon.svg';
24
+ import PermissionsIcon from '../assets/permissions-icon.svg';
25
+ import FeedbackIcon from '../assets/feedback-icon.svg';
26
+ import CopyIcon from '../assets/copy-icon.svg';
27
+ import KeyIcon from '../assets/key-icon.svg';
28
+ import PencilEditIcon from '../assets/pencil-edit-icon.svg';
29
+ import RefreshIcon from '../assets/refresh-icon.svg';
30
+ import ReceiptIcon from '../assets/receipt-icon.svg';
31
+
32
+ import './HomeScreen.tailwind.css';
33
+
34
+ /** HomeScreen */
35
+ export function HomeScreen() {
36
+ const {
37
+ navigate,
38
+ wallet,
39
+ syncWallet,
40
+ fetchBalance,
41
+ fetchPrice,
42
+ walletTransactions,
43
+ fetchWalletTransactions,
44
+ setSelectedTransaction,
45
+ isBalanceFetching,
46
+ isInitialized,
47
+ priceUsd,
48
+ priceChange24h,
49
+ isPriceFetching,
50
+ } = useStore();
51
+ const { theme } = useTheme();
52
+ const [balanceHidden, setBalanceHidden] = useState(false);
53
+ const [walletDropdownOpen, setWalletDropdownOpen] = useState(false);
54
+ const [settingsDropdownOpen, setSettingsDropdownOpen] = useState(false);
55
+ const [isRefreshing, setIsRefreshing] = useState(false);
56
+ const [copiedAddress, setCopiedAddress] = useState(false);
57
+ const [isConnected, setIsConnected] = useState(true);
58
+ const [hasV0, setHasV0] = useState(false);
59
+ const headerRef = useRef<HTMLDivElement>(null);
60
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
61
+ const [isTransactionsStuck, setIsTransactionsStuck] = useState(false);
62
+
63
+ useLayoutEffect(() => {
64
+ const el = headerRef.current;
65
+ const container = scrollContainerRef.current;
66
+ const updateHeaderHeight = () => {
67
+ const h = el?.offsetHeight ?? 0;
68
+ container?.style.setProperty('--header-h', `${h}px`);
69
+ };
70
+ updateHeaderHeight();
71
+ window.addEventListener('resize', updateHeaderHeight);
72
+ return () => window.removeEventListener('resize', updateHeaderHeight);
73
+ }, []);
74
+
75
+ // Detect when transactions section is stuck at top
76
+ useLayoutEffect(() => {
77
+ const container = scrollContainerRef.current;
78
+ if (!container) return;
79
+
80
+ const handleScroll = () => {
81
+ const headerHeight = headerRef.current?.offsetHeight ?? 64;
82
+ const balanceSectionHeight = 140;
83
+ // When scrolled past the balance section, snap to full width
84
+ const isStuck = container.scrollTop >= balanceSectionHeight;
85
+ setIsTransactionsStuck(isStuck);
86
+ };
87
+
88
+ container.addEventListener('scroll', handleScroll);
89
+ handleScroll(); // Call once on mount
90
+ return () => container.removeEventListener('scroll', handleScroll);
91
+ }, []);
92
+
93
+ // Fetch wallet transactions on mount and when account changes
94
+ useEffect(() => {
95
+ fetchWalletTransactions();
96
+ }, [wallet.currentAccount?.address]);
97
+
98
+ // Listen for storage changes to wallet transactions and auto-refresh UI
99
+ // This keeps the UI in sync when background sync updates transaction status
100
+ useEffect(() => {
101
+ const listener = (changes: { [key: string]: chrome.storage.StorageChange }) => {
102
+ if (changes[STORAGE_KEYS.WALLET_TX_STORE]) {
103
+ fetchWalletTransactions();
104
+ }
105
+ };
106
+ chrome.storage.onChanged.addListener(listener);
107
+ return () => chrome.storage.onChanged.removeListener(listener);
108
+ }, []);
109
+
110
+ // Check RPC connection status on mount and after balance fetching completes
111
+ // (RPC calls update the status in background, so re-check after they finish)
112
+ useEffect(() => {
113
+ async function checkConnection() {
114
+ const result = await send<{ connected?: boolean }>(
115
+ INTERNAL_METHODS.GET_CONNECTION_STATUS,
116
+ []
117
+ );
118
+ setIsConnected(result?.connected ?? true);
119
+ }
120
+ // Check when balance fetching completes (not while fetching)
121
+ if (!isBalanceFetching) {
122
+ checkConnection();
123
+ }
124
+ }, [isBalanceFetching]);
125
+
126
+ // Load balance hidden preference on mount
127
+ useEffect(() => {
128
+ chrome.storage.local.get([STORAGE_KEYS.BALANCE_HIDDEN]).then(result => {
129
+ const raw = (result as Record<string, unknown>)[STORAGE_KEYS.BALANCE_HIDDEN];
130
+ setBalanceHidden(typeof raw === 'boolean' ? raw : Boolean(raw));
131
+ });
132
+ }, []);
133
+
134
+ // Toggle balance visibility and persist setting
135
+ function toggleBalanceHidden() {
136
+ const newValue = !balanceHidden;
137
+ setBalanceHidden(newValue);
138
+ chrome.storage.local.set({ [STORAGE_KEYS.BALANCE_HIDDEN]: newValue });
139
+ }
140
+
141
+ // Get accounts from vault (filter out hidden accounts)
142
+ const accounts = (wallet.accounts || []).filter(acc => !acc.hidden);
143
+ const currentAccount = wallet.currentAccount || accounts[0];
144
+
145
+ useEffect(() => {
146
+ let isMounted = true;
147
+ async function checkHasV0Mnemonic() {
148
+ const result = await send<{ ok?: boolean; has?: boolean; error?: string }>(
149
+ INTERNAL_METHODS.HAS_V0_MNEMONIC,
150
+ []
151
+ );
152
+ if (result?.ok && isMounted) {
153
+ setHasV0(Boolean(result.has));
154
+ }
155
+ }
156
+ checkHasV0Mnemonic();
157
+ return () => {
158
+ isMounted = false;
159
+ };
160
+ }, []);
161
+
162
+ // Lock wallet handler
163
+ async function handleLockWallet() {
164
+ const result = await send<{ ok?: boolean }>(INTERNAL_METHODS.LOCK, []);
165
+
166
+ if (result?.ok) {
167
+ syncWallet({
168
+ ...wallet,
169
+ locked: true,
170
+ });
171
+ navigate('locked');
172
+ }
173
+ }
174
+
175
+ // Account switching handler
176
+ async function handleSwitchAccount(index: number) {
177
+ const result = await send<{ ok?: boolean; account?: Account; error?: string }>(
178
+ INTERNAL_METHODS.SWITCH_ACCOUNT,
179
+ [index]
180
+ );
181
+
182
+ if (result?.ok && result.account) {
183
+ // Get cached balance for the new account (or 0 if not cached)
184
+ const cachedBalance = wallet.accountBalances[result.account.address] ?? 0;
185
+
186
+ const updatedWallet = {
187
+ ...wallet,
188
+ currentAccount: result.account,
189
+ address: result.account.address,
190
+ balance: cachedBalance,
191
+ availableBalance: cachedBalance,
192
+ };
193
+ syncWallet(updatedWallet);
194
+
195
+ // Fetch balance and transactions for the switched account
196
+ fetchBalance();
197
+ fetchWalletTransactions();
198
+ }
199
+
200
+ setWalletDropdownOpen(false);
201
+ }
202
+
203
+ // Account creation handler
204
+ async function handleAddAccount() {
205
+ const result = await send<{ ok?: boolean; account?: Account; error?: string }>(
206
+ INTERNAL_METHODS.CREATE_ACCOUNT,
207
+ []
208
+ );
209
+
210
+ if (result?.ok && result.account) {
211
+ const updatedWallet = {
212
+ ...wallet,
213
+ accounts: [...wallet.accounts, result.account],
214
+ currentAccount: result.account,
215
+ address: result.account.address,
216
+ balance: 0, // Reset balance to 0 for new account
217
+ accountBalances: {
218
+ ...wallet.accountBalances,
219
+ [result.account.address]: 0, // Initialize new account balance to 0
220
+ },
221
+ };
222
+ syncWallet(updatedWallet);
223
+
224
+ // Fetch balance for the newly created account
225
+ fetchBalance();
226
+ }
227
+
228
+ setWalletDropdownOpen(false);
229
+ }
230
+
231
+ // Refresh balance handler
232
+ async function handleRefreshBalance() {
233
+ setIsRefreshing(true);
234
+ try {
235
+ // Fetch balance (which syncs UTXOs from chain)
236
+ await fetchBalance();
237
+
238
+ // Fetch latest wallet transactions
239
+ await fetchWalletTransactions();
240
+ } finally {
241
+ setIsRefreshing(false);
242
+ // Connection status is automatically re-checked by useEffect when isBalanceFetching changes
243
+ }
244
+ }
245
+
246
+ // Use available balance (confirmed - pending outflow) as the primary display
247
+ const displayBalance = wallet.availableBalance;
248
+ const balance = displayBalance.toLocaleString('en-US', {
249
+ minimumFractionDigits: 2,
250
+ maximumFractionDigits: 2,
251
+ });
252
+
253
+ // Calculate USD value based on available balance and price
254
+ const totalBalanceUsd = displayBalance * priceUsd;
255
+ const formattedUsdValue = totalBalanceUsd.toLocaleString('en-US', {
256
+ minimumFractionDigits: 2,
257
+ maximumFractionDigits: 2,
258
+ });
259
+ const formattedPercentChange = Math.abs(priceChange24h).toFixed(2);
260
+
261
+ const walletName = currentAccount?.name || 'Wallet';
262
+
263
+ // Fetch price on mount and when account changes
264
+ useEffect(() => {
265
+ fetchPrice();
266
+ }, [fetchPrice]);
267
+ const walletAddress = truncateAddress(currentAccount?.address);
268
+ const fullAddress = currentAccount?.address || '';
269
+
270
+ // Helper to convert WalletTransaction status to display status
271
+ const getDisplayStatus = (status: string): 'pending' | 'confirmed' | 'failed' | 'expired' => {
272
+ switch (status) {
273
+ case 'confirmed':
274
+ return 'confirmed';
275
+ case 'failed':
276
+ return 'failed';
277
+ case 'expired':
278
+ return 'expired';
279
+ default:
280
+ return 'pending';
281
+ }
282
+ };
283
+
284
+ // Filter to show outgoing and incoming transactions (exclude 'self' which is internal transfers)
285
+ const displayTransactions = walletTransactions.filter(
286
+ tx => tx.direction === 'outgoing' || tx.direction === 'incoming'
287
+ );
288
+
289
+ // Group wallet transactions by date
290
+ const transactionsByDate = displayTransactions.reduce(
291
+ (acc, tx) => {
292
+ const date = new Date(tx.createdAt).toLocaleDateString('en-US', {
293
+ day: 'numeric',
294
+ month: 'short',
295
+ year: 'numeric',
296
+ });
297
+
298
+ if (!acc[date]) {
299
+ acc[date] = [];
300
+ }
301
+
302
+ // Convert amount from nicks to NOCK
303
+ const amountNock = (tx.amount || 0) / NOCK_TO_NICKS;
304
+ const type = tx.direction === 'outgoing' ? 'sent' : 'received';
305
+ // For incoming transactions, show sender if known, otherwise leave empty
306
+ const address = tx.direction === 'outgoing' ? tx.recipient : tx.sender;
307
+
308
+ // Only show USD value if we have historical price stored
309
+ const usdValue = tx.priceUsdAtTime
310
+ ? `$${(amountNock * tx.priceUsdAtTime).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
311
+ : null;
312
+
313
+ acc[date].push({
314
+ type,
315
+ from: truncateAddress(address || ''),
316
+ amount:
317
+ type === 'sent'
318
+ ? `-${amountNock.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} NOCK`
319
+ : `${amountNock.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} NOCK`,
320
+ usdValue,
321
+ status: getDisplayStatus(tx.status),
322
+ confirmations: tx.confirmations,
323
+ txid: tx.txHash || tx.id,
324
+ originalTx: tx, // Keep reference to original transaction
325
+ });
326
+
327
+ return acc;
328
+ },
329
+ {} as Record<string, any[]>
330
+ );
331
+
332
+ const transactions = Object.entries(transactionsByDate).map(([date, items]) => ({
333
+ date,
334
+ items,
335
+ }));
336
+
337
+ return (
338
+ <div
339
+ className="w-[357px] h-[600px] overflow-hidden relative"
340
+ style={{ backgroundColor: 'var(--color-home-fill)', color: 'var(--color-text-primary)' }}
341
+ >
342
+ {/* Scroll container */}
343
+ <div
344
+ ref={scrollContainerRef}
345
+ className="relative h-full overflow-y-auto scroll-thin flex flex-col"
346
+ >
347
+ {/* Sticky header */}
348
+ <header
349
+ ref={headerRef}
350
+ className="sticky top-0 z-40 backdrop-blur"
351
+ style={{ backgroundColor: 'var(--color-home-fill)' }}
352
+ >
353
+ <div className="px-4 py-3 flex items-center justify-between min-h-[64px]">
354
+ <div
355
+ className="flex items-center gap-2"
356
+ role="button"
357
+ tabIndex={0}
358
+ onClick={() => setWalletDropdownOpen(o => !o)}
359
+ onKeyDown={e => {
360
+ if (e.key === 'Enter' || e.key === ' ') {
361
+ e.preventDefault();
362
+ setWalletDropdownOpen(o => !o);
363
+ }
364
+ }}
365
+ aria-label="Wallet menu"
366
+ >
367
+ <div
368
+ className="relative h-10 w-10 rounded-tile grid place-items-center"
369
+ style={{ backgroundColor: 'var(--color-bg)' }}
370
+ >
371
+ <AccountIcon
372
+ styleId={currentAccount?.iconStyleId}
373
+ color={currentAccount?.iconColor}
374
+ className="h-6 w-6"
375
+ />
376
+ <div
377
+ className="absolute -bottom-px -right-0.5 h-2 w-2 rounded-full"
378
+ style={{
379
+ backgroundColor: isConnected ? 'var(--color-green)' : 'var(--color-red)',
380
+ }}
381
+ title={isConnected ? 'Connected' : 'Disconnected'}
382
+ />
383
+ </div>
384
+ <div className="flex flex-col min-w-0">
385
+ <div
386
+ className="font-sans text-[14px] font-medium leading-[18px] tracking-[0.14px] flex items-center gap-1"
387
+ style={{ color: 'var(--color-text-primary)' }}
388
+ >
389
+ {walletName}
390
+ <img src={WalletDropdownArrow} alt="" className="h-3 w-3" />
391
+ </div>
392
+ <div
393
+ className="font-sans text-[13px] leading-[18px] tracking-[0.26px] flex items-center gap-2"
394
+ style={{ color: 'var(--color-text-muted)' }}
395
+ >
396
+ <span className="truncate">{walletAddress}</span>
397
+ <button
398
+ type="button"
399
+ className="shrink-0 opacity-70 hover:opacity-40"
400
+ onClick={async e => {
401
+ e.stopPropagation();
402
+ try {
403
+ await navigator.clipboard.writeText(fullAddress);
404
+ setCopiedAddress(true);
405
+ setTimeout(() => setCopiedAddress(false), 2000);
406
+ } catch (err) {
407
+ console.error('Failed to copy address:', err);
408
+ }
409
+ }}
410
+ aria-label="Copy address"
411
+ >
412
+ {copiedAddress ? (
413
+ <CheckIcon className="h-3 w-3" />
414
+ ) : (
415
+ <img src={CopyIcon} alt="" className="h-3 w-3" />
416
+ )}
417
+ </button>
418
+ </div>
419
+ </div>
420
+ </div>
421
+
422
+ <div className="flex items-center gap-2">
423
+ <button
424
+ className="h-8 w-8 rounded-tile hover:bg-black/5 grid place-items-center"
425
+ onClick={handleLockWallet}
426
+ >
427
+ <img src={LockIconAsset} alt="Lock" className="h-5 w-5" />
428
+ </button>
429
+ <button
430
+ className="h-8 w-8 rounded-tile hover:bg-black/5 grid place-items-center"
431
+ onClick={() => setSettingsDropdownOpen(o => !o)}
432
+ >
433
+ <img src={SettingsIconAsset} alt="Settings" className="h-5 w-5" />
434
+ </button>
435
+ </div>
436
+ </div>
437
+ </header>
438
+
439
+ {/* Wallet dropdown */}
440
+ {walletDropdownOpen && (
441
+ <>
442
+ <div className="fixed inset-0 z-40" onClick={() => setWalletDropdownOpen(false)} />
443
+ <div
444
+ className="fixed top-[64px] left-2 right-2 rounded-xl z-50 max-h-[400px] overflow-y-auto"
445
+ style={{
446
+ backgroundColor: 'var(--color-bg)',
447
+ border: '1px solid var(--color-surface-700)',
448
+ boxShadow: '0 4px 12px 0 rgba(5, 5, 5, 0.12)',
449
+ }}
450
+ >
451
+ <div className="p-2">
452
+ {accounts.map(account => {
453
+ const isSelected = currentAccount?.index === account.index;
454
+ const showSelection = accounts.length > 1 && isSelected;
455
+ return (
456
+ <button
457
+ key={account.index}
458
+ onClick={() => handleSwitchAccount(account.index)}
459
+ className="wallet-dropdown-item w-full flex items-center gap-2 p-2 rounded-tile border transition"
460
+ style={{
461
+ backgroundColor: showSelection ? 'var(--color-bg)' : 'transparent',
462
+ borderColor: showSelection ? 'var(--color-text-primary)' : 'transparent',
463
+ }}
464
+ onMouseEnter={e => {
465
+ if (!showSelection) {
466
+ e.currentTarget.style.backgroundColor = 'var(--color-surface-900)';
467
+ }
468
+ }}
469
+ onMouseLeave={e => {
470
+ if (!showSelection) {
471
+ e.currentTarget.style.backgroundColor = showSelection
472
+ ? 'var(--color-bg)'
473
+ : 'transparent';
474
+ }
475
+ }}
476
+ >
477
+ <div
478
+ className="h-10 w-10 rounded-tile grid place-items-center"
479
+ style={{ backgroundColor: 'var(--color-bg)' }}
480
+ >
481
+ <AccountIcon
482
+ styleId={account.iconStyleId}
483
+ color={account.iconColor}
484
+ className="h-6 w-6"
485
+ />
486
+ </div>
487
+ <div className="flex-1 text-left">
488
+ <div
489
+ className="text-[14px] leading-[18px] font-medium"
490
+ style={{ color: 'var(--color-text-primary)' }}
491
+ >
492
+ {account.name}
493
+ </div>
494
+ <div
495
+ className="text-[13px] leading-[18px] tracking-[0.26px]"
496
+ style={{ color: 'var(--color-text-muted)' }}
497
+ >
498
+ {truncateAddress(account.address)}
499
+ </div>
500
+ </div>
501
+ <div
502
+ className="wallet-balance text-[14px] font-medium whitespace-nowrap"
503
+ style={{ color: 'var(--color-text-primary)' }}
504
+ >
505
+ {(wallet.accountBalances[account.address] ?? 0).toLocaleString('en-US', {
506
+ minimumFractionDigits: 2,
507
+ maximumFractionDigits: 2,
508
+ })}{' '}
509
+ NOCK
510
+ </div>
511
+ <div
512
+ className="wallet-settings-icon h-10 w-10 rounded-tile hidden items-center justify-center"
513
+ style={{ backgroundColor: 'var(--color-surface-700)' }}
514
+ onClick={e => {
515
+ e.stopPropagation();
516
+ setWalletDropdownOpen(false);
517
+ navigate('wallet-settings');
518
+ }}
519
+ >
520
+ <img src={PencilEditIcon} alt="Edit wallet" className="h-5 w-5" />
521
+ </div>
522
+ </button>
523
+ );
524
+ })}
525
+ </div>
526
+ <div className="h-px" style={{ backgroundColor: 'var(--color-divider)' }} />
527
+ <div className="p-2">
528
+ <button
529
+ className="w-full h-12 font-medium rounded-lg"
530
+ style={{ backgroundColor: 'var(--color-text-primary)', color: 'var(--color-bg)' }}
531
+ onClick={handleAddAccount}
532
+ >
533
+ Add Wallet
534
+ </button>
535
+ </div>
536
+ </div>
537
+ </>
538
+ )}
539
+
540
+ {/* Settings dropdown */}
541
+ {settingsDropdownOpen && (
542
+ <>
543
+ <div className="fixed inset-0 z-40" onClick={() => setSettingsDropdownOpen(false)} />
544
+ <div
545
+ className="fixed top-[64px] right-2 w-[245px] rounded-xl p-2 z-50 flex flex-col gap-1"
546
+ style={{
547
+ backgroundColor: 'var(--color-bg)',
548
+ border: '1px solid var(--color-surface-700)',
549
+ boxShadow: '0 4px 12px 0 rgba(5, 5, 5, 0.12)',
550
+ }}
551
+ >
552
+ {hasV0 && (
553
+ <DropdownItem
554
+ icon={KeyIcon}
555
+ label="Upgrade v0 → v1"
556
+ onClick={() => {
557
+ setSettingsDropdownOpen(false);
558
+ navigate('onboarding-import-v0');
559
+ }}
560
+ />
561
+ )}
562
+ <DropdownItem
563
+ icon={ExplorerIcon}
564
+ label="View on explorer"
565
+ onClick={() =>
566
+ window.open(`https://nockblocks.com/address/${currentAccount?.address}`, '_blank')
567
+ }
568
+ />
569
+ <DropdownItem
570
+ icon={PermissionsIcon}
571
+ label="Wallet permissions"
572
+ onClick={() => {
573
+ setSettingsDropdownOpen(false);
574
+ navigate('wallet-permissions');
575
+ }}
576
+ />
577
+ <DropdownItem
578
+ icon={SettingsIconAsset}
579
+ label="Settings"
580
+ onClick={() => {
581
+ setSettingsDropdownOpen(false);
582
+ navigate('settings');
583
+ }}
584
+ />
585
+ <div className="h-px my-1" style={{ backgroundColor: 'var(--color-divider)' }} />
586
+ <DropdownItem
587
+ icon={FeedbackIcon}
588
+ label="Wallet feedback"
589
+ onClick={() => window.open('https://nockchain.net/feedback', '_blank')}
590
+ />
591
+ </div>
592
+ </>
593
+ )}
594
+
595
+ {/* Sticky balance block (lower z) */}
596
+ <div
597
+ className="sticky top-[var(--header-h)] z-10 px-4 pt-1"
598
+ style={{ backgroundColor: 'var(--color-home-fill)' }}
599
+ >
600
+ <div className="mb-3">
601
+ <div className="flex items-baseline gap-[6px]">
602
+ {!isInitialized || (isBalanceFetching && wallet.balance === 0) ? (
603
+ <>
604
+ <div className="h-[40px] w-32 rounded skeleton-shimmer" />
605
+ <div className="h-[28px] w-16 rounded skeleton-shimmer" />
606
+ </>
607
+ ) : (
608
+ <>
609
+ <div
610
+ className="font-display font-semibold text-[36px] leading-[40px] tracking-[-0.72px]"
611
+ style={{ color: 'var(--color-text-primary)' }}
612
+ >
613
+ {balanceHidden ? '••••••' : balance}
614
+ </div>
615
+ <div
616
+ className="font-display text-[24px] leading-[28px] tracking-[-0.48px]"
617
+ style={{ color: 'var(--color-text-muted)' }}
618
+ >
619
+ NOCK
620
+ </div>
621
+ </>
622
+ )}
623
+ <button
624
+ className="ml-1"
625
+ style={{ color: 'var(--color-text-muted)' }}
626
+ onClick={toggleBalanceHidden}
627
+ aria-label="Toggle balance visibility"
628
+ >
629
+ {balanceHidden ? (
630
+ <EyeOffIcon className="h-4 w-4" />
631
+ ) : (
632
+ <EyeIcon className="h-4 w-4" />
633
+ )}
634
+ </button>
635
+ <button
636
+ className="ml-1"
637
+ style={{ color: 'var(--color-text-muted)' }}
638
+ onClick={handleRefreshBalance}
639
+ disabled={isRefreshing || isBalanceFetching}
640
+ aria-label="Refresh balance"
641
+ >
642
+ <img
643
+ src={RefreshIcon}
644
+ alt="Refresh"
645
+ className={`h-4 w-4 ${isRefreshing || isBalanceFetching ? 'animate-spin' : ''}`}
646
+ style={{
647
+ opacity: isRefreshing || isBalanceFetching ? 0.5 : 1,
648
+ }}
649
+ />
650
+ </button>
651
+ </div>
652
+ <div className="mt-1 text-[13px] font-medium leading-[18px] flex items-center gap-1">
653
+ {isPriceFetching || (isBalanceFetching && wallet.balance === 0) ? (
654
+ <div className="h-[14px] w-36 rounded skeleton-shimmer" />
655
+ ) : (
656
+ <>
657
+ <img
658
+ src={priceChange24h >= 0 ? TrendUpArrow : TrendDownArrow}
659
+ alt={priceChange24h >= 0 ? 'up' : 'down'}
660
+ className="h-4 w-4"
661
+ />
662
+ <span
663
+ style={{
664
+ color: priceChange24h >= 0 ? 'var(--color-green)' : 'var(--color-red)',
665
+ }}
666
+ >
667
+ {balanceHidden
668
+ ? '••••• •••••'
669
+ : `$${formattedUsdValue} (${priceChange24h >= 0 ? '+' : '-'}${formattedPercentChange}%)`}
670
+ </span>
671
+ </>
672
+ )}
673
+ </div>
674
+ </div>
675
+
676
+ {/* Actions */}
677
+ <div className="grid grid-cols-2 gap-2 mb-3">
678
+ <div className="relative">
679
+ <button
680
+ className="btn-primary w-full rounded-card shadow-card flex flex-col items-start justify-center gap-4 p-3 font-sans text-[14px] font-medium transition-all hover:opacity-90 active:scale-[0.98]"
681
+ style={{ color: '#000' }}
682
+ onClick={() => navigate('send')}
683
+ >
684
+ <SendPaperPlaneIcon className="h-5 w-5" />
685
+ Send
686
+ </button>
687
+ </div>
688
+ <button
689
+ className="btn-secondary rounded-card shadow-card flex flex-col items-start justify-center gap-4 p-3 font-sans text-[14px] font-medium transition-all hover:opacity-90 active:scale-[0.98]"
690
+ style={{
691
+ color: 'var(--color-text-primary)',
692
+ }}
693
+ onClick={() => navigate('receive')}
694
+ >
695
+ <ReceiveCircleIcon className="h-5 w-5" />
696
+ Receive
697
+ </button>
698
+ </div>
699
+ </div>
700
+
701
+ <section
702
+ className={`relative z-20 shadow-card rounded-t-xl transition-all duration-300 flex-1 flex flex-col ${
703
+ isTransactionsStuck ? '' : 'mx-2 mt-4'
704
+ }`}
705
+ style={{
706
+ backgroundColor: 'var(--color-home-accent)',
707
+ border: '1px solid var(--color-divider)',
708
+ }}
709
+ >
710
+ {/* Sticky header inside the sheet (matches scroll state 2) */}
711
+ <div
712
+ className="sticky top-[var(--header-h)] z-10 px-4 py-3 rounded-t-xl"
713
+ style={{
714
+ backgroundColor: 'var(--color-home-accent)',
715
+ borderBottom: '1px solid var(--color-divider)',
716
+ }}
717
+ >
718
+ <div className="flex items-center justify-between">
719
+ <h2
720
+ className="font-display text-[14px] font-medium"
721
+ style={{ color: 'var(--color-text-primary)' }}
722
+ >
723
+ Recent Transactions
724
+ </h2>
725
+ <a
726
+ href={`https://nockblocks.com/address/${currentAccount?.address}`}
727
+ target="_blank"
728
+ rel="noopener noreferrer"
729
+ className="text-[12px] font-medium rounded-full pl-[12px] pr-[16px] py-[3px] flex items-center gap-[4px] transition-opacity"
730
+ style={{
731
+ border: '1px solid var(--color-text-primary)',
732
+ color: 'var(--color-text-primary)',
733
+ }}
734
+ onMouseEnter={e => (e.currentTarget.style.opacity = '0.7')}
735
+ onMouseLeave={e => (e.currentTarget.style.opacity = '1')}
736
+ >
737
+ <ArrowUpRightIcon className="h-[10px] w-[10px]" />
738
+ View all
739
+ </a>
740
+ </div>
741
+ </div>
742
+
743
+ {/* Groups */}
744
+ <div className="px-4 pb-6 flex-1 flex flex-col">
745
+ {displayTransactions.length === 0 ? (
746
+ /* Empty state */
747
+ <div className="flex flex-col items-center justify-center gap-2 flex-1">
748
+ <div
749
+ className="h-10 w-10 rounded-full flex items-center justify-center"
750
+ style={{ backgroundColor: 'var(--color-tx-icon)' }}
751
+ >
752
+ <img src={ReceiptIcon} alt="" className="h-5 w-5" />
753
+ </div>
754
+ <div className="text-center">
755
+ <p
756
+ className="font-display font-medium text-[14px] leading-[18px] tracking-[0.14px] m-0"
757
+ style={{ color: 'var(--color-text-muted)' }}
758
+ >
759
+ {displayBalance === 0
760
+ ? 'Your wallet is ready to receive NOCK.'
761
+ : 'Make your first NOCK transaction.'}
762
+ </p>
763
+ </div>
764
+ </div>
765
+ ) : (
766
+ transactions.map((group, idx) => (
767
+ <div
768
+ key={idx}
769
+ className={idx === 0 ? 'pt-4' : 'pt-4'}
770
+ style={idx !== 0 ? { borderTop: '1px solid var(--color-divider)' } : undefined}
771
+ >
772
+ <div
773
+ className="font-display font-medium text-[14px] leading-[18px] tracking-[0.14px] mb-3"
774
+ style={{ color: 'var(--color-text-muted)' }}
775
+ >
776
+ {group.date}
777
+ </div>
778
+ <div>
779
+ {group.items.map((t, i) => (
780
+ <button
781
+ key={i}
782
+ className="w-full flex items-start gap-3 py-3 rounded-lg px-0 -mx-0 overflow-hidden"
783
+ onClick={() => {
784
+ setSelectedTransaction(t.originalTx);
785
+ navigate('tx-details');
786
+ }}
787
+ >
788
+ <div
789
+ className="h-10 w-10 shrink-0 rounded-full grid place-items-center"
790
+ style={{ backgroundColor: 'var(--color-tx-icon)' }}
791
+ >
792
+ {t.type === 'received' ? (
793
+ <ReceiveArrowIcon
794
+ className="h-4 w-4"
795
+ style={{ color: 'var(--color-text-muted)' }}
796
+ />
797
+ ) : (
798
+ <SentArrowIcon
799
+ className="h-4 w-4"
800
+ style={{ color: 'var(--color-text-muted)' }}
801
+ />
802
+ )}
803
+ </div>
804
+ <div className="flex-1 min-w-0 text-left">
805
+ <div
806
+ className="text-[14px] font-medium truncate"
807
+ style={{ color: 'var(--color-text-primary)' }}
808
+ >
809
+ {t.type === 'received' ? 'Received' : 'Sent'}
810
+ </div>
811
+ <div
812
+ className="text-[12px] flex items-center gap-1.5 truncate"
813
+ style={{ color: 'var(--color-text-muted)' }}
814
+ >
815
+ {t.status === 'pending' && (
816
+ <>
817
+ <span style={{ color: '#C88414' }}>Pending</span>
818
+ <span>·</span>
819
+ </>
820
+ )}
821
+ {t.status === 'failed' && (
822
+ <>
823
+ <span style={{ color: 'var(--color-red)' }}>Failed</span>
824
+ <span>·</span>
825
+ </>
826
+ )}
827
+ {t.status === 'expired' && (
828
+ <>
829
+ <span style={{ color: 'var(--color-text-muted)' }}>Expired</span>
830
+ <span>·</span>
831
+ </>
832
+ )}
833
+ <span className="truncate">{t.from}</span>
834
+ </div>
835
+ </div>
836
+ <div className="text-right shrink-0 pr-0">
837
+ <div
838
+ className="text-[14px] font-medium whitespace-nowrap"
839
+ style={{
840
+ color:
841
+ t.type === 'received'
842
+ ? 'var(--color-green)'
843
+ : 'var(--color-text-primary)',
844
+ }}
845
+ >
846
+ {t.amount}
847
+ </div>
848
+ {t.usdValue && (
849
+ <div
850
+ className="text-[12px] whitespace-nowrap"
851
+ style={{ color: 'var(--color-text-muted)' }}
852
+ >
853
+ {t.usdValue}
854
+ </div>
855
+ )}
856
+ </div>
857
+ </button>
858
+ ))}
859
+ </div>
860
+ </div>
861
+ ))
862
+ )}
863
+ </div>
864
+ </section>
865
+ </div>
866
+ </div>
867
+ );
868
+ }
869
+
870
+ function DropdownItem({
871
+ icon,
872
+ label,
873
+ onClick,
874
+ }: {
875
+ icon: string;
876
+ label: string;
877
+ onClick: () => void;
878
+ }) {
879
+ return (
880
+ <button
881
+ onClick={onClick}
882
+ className="w-full flex items-center gap-2 p-2 rounded-lg text-left transition-colors"
883
+ style={{ backgroundColor: 'transparent' }}
884
+ onMouseEnter={e => {
885
+ e.currentTarget.style.backgroundColor = 'var(--color-surface-900)';
886
+ }}
887
+ onMouseLeave={e => {
888
+ e.currentTarget.style.backgroundColor = 'transparent';
889
+ }}
890
+ >
891
+ <div
892
+ className="h-8 w-8 rounded-tile grid place-items-center"
893
+ style={{ backgroundColor: 'var(--color-surface-800)' }}
894
+ >
895
+ <img src={icon} className="h-5 w-5" alt="" />
896
+ </div>
897
+ <span className="text-[14px] font-medium" style={{ color: 'var(--color-text-primary)' }}>
898
+ {label}
899
+ </span>
900
+ </button>
901
+ );
902
+ }