auramaxx 1.0.0-alpha.4

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 (363) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +112 -0
  3. package/bin/aurawallet.js +121 -0
  4. package/docs/ADAPTERS.md +467 -0
  5. package/docs/API.md +2679 -0
  6. package/docs/APPS.md +198 -0
  7. package/docs/ARCHITECTURE.md +350 -0
  8. package/docs/AUTH.md +698 -0
  9. package/docs/BEST-PRACTICES.md +121 -0
  10. package/docs/CLI.md +61 -0
  11. package/docs/DEVELOPING-APPS.md +452 -0
  12. package/docs/EXTENSION.md +97 -0
  13. package/docs/JOBS.md +33 -0
  14. package/docs/MCP.md +76 -0
  15. package/docs/PROTOCOL.md +142 -0
  16. package/docs/SETUP.md +219 -0
  17. package/docs/WORKSPACE.md +672 -0
  18. package/docs/agent-auth.md +63 -0
  19. package/docs/aura-file.md +48 -0
  20. package/docs/credentials.md +53 -0
  21. package/docs/external/getting-started.md +65 -0
  22. package/docs/external/overview.md +45 -0
  23. package/docs/external/use-cases.md +48 -0
  24. package/docs/external/why-aura.md +35 -0
  25. package/docs/jobs/connect-agent.md +77 -0
  26. package/docs/jobs/migrate-from-dotenv.md +79 -0
  27. package/docs/jobs/recover-from-lockout.md +72 -0
  28. package/docs/jobs/secure-ci.md +63 -0
  29. package/docs/oauth2.md +42 -0
  30. package/docs/passkeys.md +60 -0
  31. package/docs/security.md +540 -0
  32. package/docs/specs/aura-open-protocol.md +61 -0
  33. package/docs/specs/aura-provider-plugin.md +24 -0
  34. package/docs/specs/aura-registry-model.md +31 -0
  35. package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
  36. package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
  37. package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
  38. package/docs/specs/fixtures/valid-basic.aura +4 -0
  39. package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
  40. package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
  41. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  42. package/docs/totp.md +40 -0
  43. package/docs/wallet/AI.md +508 -0
  44. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  45. package/docs/wallet/README.md +47 -0
  46. package/docs/wallet/STRATEGY.md +89 -0
  47. package/next.config.ts +21 -0
  48. package/package.json +151 -0
  49. package/postcss.config.mjs +8 -0
  50. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  51. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  52. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  53. package/prisma/migrations/migration_lock.toml +3 -0
  54. package/prisma/schema.prisma +447 -0
  55. package/public/logo-chevron.svg +31 -0
  56. package/public/logo-concentric.svg +31 -0
  57. package/public/logo-crosshatch.svg +39 -0
  58. package/public/logo-dashed.svg +39 -0
  59. package/public/logo-horizontal.svg +31 -0
  60. package/public/logo-m56.svg +64 -0
  61. package/public/logo.webp +0 -0
  62. package/scripts/add-app.js +245 -0
  63. package/scripts/init.sh +57 -0
  64. package/scripts/migrate-apikeys-to-credentials.ts +35 -0
  65. package/scripts/sandbox-agent-flow.sh +235 -0
  66. package/scripts/sandbox.sh +175 -0
  67. package/scripts/validate-job-docs.mjs +125 -0
  68. package/server/abi/SwapHelper.json +438 -0
  69. package/server/cli/approval.ts +447 -0
  70. package/server/cli/commands/app.ts +204 -0
  71. package/server/cli/commands/cron.ts +24 -0
  72. package/server/cli/commands/doctor.ts +1007 -0
  73. package/server/cli/commands/env.ts +456 -0
  74. package/server/cli/commands/init.ts +752 -0
  75. package/server/cli/commands/mcp.ts +125 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/shell-hook.ts +468 -0
  78. package/server/cli/commands/start.ts +62 -0
  79. package/server/cli/commands/status.ts +59 -0
  80. package/server/cli/commands/stop.ts +14 -0
  81. package/server/cli/commands/token.ts +180 -0
  82. package/server/cli/commands/unlock.ts +49 -0
  83. package/server/cli/commands/vault.ts +417 -0
  84. package/server/cli/index.ts +328 -0
  85. package/server/cli/lib/aura-parser.ts +64 -0
  86. package/server/cli/lib/credential-create.ts +74 -0
  87. package/server/cli/lib/credential-resolve.ts +254 -0
  88. package/server/cli/lib/dotenv-migrate.ts +116 -0
  89. package/server/cli/lib/dotenv-parser.ts +146 -0
  90. package/server/cli/lib/http.ts +91 -0
  91. package/server/cli/lib/init-steps.ts +76 -0
  92. package/server/cli/lib/local-agent-trust.ts +45 -0
  93. package/server/cli/lib/process.ts +136 -0
  94. package/server/cli/lib/prompt.ts +85 -0
  95. package/server/cli/lib/theme.ts +240 -0
  96. package/server/cli/socket.ts +570 -0
  97. package/server/cli/transport-client.ts +50 -0
  98. package/server/cron/index.ts +137 -0
  99. package/server/cron/job.ts +31 -0
  100. package/server/cron/jobs/balance-sync.ts +436 -0
  101. package/server/cron/jobs/incoming-scan.ts +506 -0
  102. package/server/cron/jobs/native-price.ts +70 -0
  103. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  104. package/server/cron/jobs/strategy-runner.ts +175 -0
  105. package/server/cron/scheduler.ts +125 -0
  106. package/server/index.ts +406 -0
  107. package/server/lib/adapters/factory.ts +110 -0
  108. package/server/lib/adapters/index.ts +19 -0
  109. package/server/lib/adapters/router.ts +297 -0
  110. package/server/lib/adapters/telegram.ts +645 -0
  111. package/server/lib/adapters/types.ts +89 -0
  112. package/server/lib/adapters/webhook.ts +95 -0
  113. package/server/lib/address.ts +49 -0
  114. package/server/lib/agent-auth/contracts.ts +1194 -0
  115. package/server/lib/agent-profiles.ts +328 -0
  116. package/server/lib/ai.ts +285 -0
  117. package/server/lib/api-registry/contracts.ts +86 -0
  118. package/server/lib/api-registry/validation.ts +172 -0
  119. package/server/lib/apikey-migration.ts +189 -0
  120. package/server/lib/app-installer.ts +505 -0
  121. package/server/lib/app-tokens.ts +247 -0
  122. package/server/lib/auth.ts +314 -0
  123. package/server/lib/batch.ts +242 -0
  124. package/server/lib/cold.ts +874 -0
  125. package/server/lib/config.ts +381 -0
  126. package/server/lib/credential-access-audit.ts +85 -0
  127. package/server/lib/credential-access-policy.ts +110 -0
  128. package/server/lib/credential-health.ts +343 -0
  129. package/server/lib/credential-import.ts +487 -0
  130. package/server/lib/credential-scope.ts +87 -0
  131. package/server/lib/credential-shares.ts +190 -0
  132. package/server/lib/credential-transport.ts +342 -0
  133. package/server/lib/credential-vault.ts +77 -0
  134. package/server/lib/credentials.ts +333 -0
  135. package/server/lib/crypto.ts +8 -0
  136. package/server/lib/db.ts +15 -0
  137. package/server/lib/defaults.ts +366 -0
  138. package/server/lib/dex/index.ts +80 -0
  139. package/server/lib/dex/relay.ts +235 -0
  140. package/server/lib/dex/types.ts +59 -0
  141. package/server/lib/dex/uniswap.ts +370 -0
  142. package/server/lib/e2e-agent/artifacts.ts +36 -0
  143. package/server/lib/e2e-agent/contracts.ts +112 -0
  144. package/server/lib/e2e-agent/validation.ts +135 -0
  145. package/server/lib/encrypt.ts +128 -0
  146. package/server/lib/error.ts +20 -0
  147. package/server/lib/events.ts +205 -0
  148. package/server/lib/hot.ts +357 -0
  149. package/server/lib/key-fingerprint.ts +28 -0
  150. package/server/lib/logger.ts +331 -0
  151. package/server/lib/network.ts +137 -0
  152. package/server/lib/notifications.ts +219 -0
  153. package/server/lib/oauth2-refresh.ts +241 -0
  154. package/server/lib/oursecret.ts +54 -0
  155. package/server/lib/passkey-credential.ts +360 -0
  156. package/server/lib/passkey.ts +68 -0
  157. package/server/lib/permissions.ts +248 -0
  158. package/server/lib/pino.ts +24 -0
  159. package/server/lib/policy-preview.ts +138 -0
  160. package/server/lib/price.ts +338 -0
  161. package/server/lib/prices.ts +34 -0
  162. package/server/lib/project-scope.ts +239 -0
  163. package/server/lib/resolve-action.ts +427 -0
  164. package/server/lib/resolve.ts +36 -0
  165. package/server/lib/sessions.ts +632 -0
  166. package/server/lib/solana/connection.ts +26 -0
  167. package/server/lib/solana/jupiter.ts +128 -0
  168. package/server/lib/solana/transfer.ts +108 -0
  169. package/server/lib/solana/wallet.ts +136 -0
  170. package/server/lib/strategy/emits.ts +21 -0
  171. package/server/lib/strategy/engine.ts +1305 -0
  172. package/server/lib/strategy/executor.ts +115 -0
  173. package/server/lib/strategy/hook-context.ts +158 -0
  174. package/server/lib/strategy/hooks.ts +990 -0
  175. package/server/lib/strategy/index.ts +28 -0
  176. package/server/lib/strategy/installer.ts +305 -0
  177. package/server/lib/strategy/loader.ts +256 -0
  178. package/server/lib/strategy/message.ts +235 -0
  179. package/server/lib/strategy/repository.ts +218 -0
  180. package/server/lib/strategy/session-logger.ts +693 -0
  181. package/server/lib/strategy/sources.ts +288 -0
  182. package/server/lib/strategy/state.ts +189 -0
  183. package/server/lib/strategy/templates.ts +403 -0
  184. package/server/lib/strategy/tick.ts +404 -0
  185. package/server/lib/strategy/types.ts +230 -0
  186. package/server/lib/swap.ts +3 -0
  187. package/server/lib/temp.ts +86 -0
  188. package/server/lib/token-metadata.ts +86 -0
  189. package/server/lib/token-safety.ts +200 -0
  190. package/server/lib/token-search.ts +444 -0
  191. package/server/lib/totp.ts +194 -0
  192. package/server/lib/transactions.ts +123 -0
  193. package/server/lib/transport.ts +75 -0
  194. package/server/lib/txhistory/decoder.ts +262 -0
  195. package/server/lib/txhistory/enricher.ts +652 -0
  196. package/server/lib/txhistory/index.ts +391 -0
  197. package/server/lib/txhistory/signatures.ts +59 -0
  198. package/server/lib/verified-summary.ts +421 -0
  199. package/server/mcp/profile-policy.ts +30 -0
  200. package/server/mcp/server.ts +619 -0
  201. package/server/mcp/tools.ts +523 -0
  202. package/server/middleware/auth.ts +119 -0
  203. package/server/middleware/requestLogger.ts +84 -0
  204. package/server/routes/actions.ts +459 -0
  205. package/server/routes/adapters.ts +703 -0
  206. package/server/routes/addressbook.ts +113 -0
  207. package/server/routes/ai.ts +34 -0
  208. package/server/routes/apikeys.ts +295 -0
  209. package/server/routes/apps.ts +601 -0
  210. package/server/routes/auth.ts +457 -0
  211. package/server/routes/backup.ts +340 -0
  212. package/server/routes/batch.ts +270 -0
  213. package/server/routes/bookmarks.ts +162 -0
  214. package/server/routes/credential-shares.ts +198 -0
  215. package/server/routes/credential-vaults.ts +154 -0
  216. package/server/routes/credentials.ts +1290 -0
  217. package/server/routes/dashboard.ts +71 -0
  218. package/server/routes/defaults.ts +124 -0
  219. package/server/routes/fund.ts +229 -0
  220. package/server/routes/import.ts +352 -0
  221. package/server/routes/launch.ts +665 -0
  222. package/server/routes/lock.ts +54 -0
  223. package/server/routes/logs.ts +68 -0
  224. package/server/routes/nuke.ts +111 -0
  225. package/server/routes/passkey-credentials.ts +99 -0
  226. package/server/routes/passkey.ts +346 -0
  227. package/server/routes/portfolio.ts +217 -0
  228. package/server/routes/price.ts +63 -0
  229. package/server/routes/resolve.ts +31 -0
  230. package/server/routes/security.ts +45 -0
  231. package/server/routes/send-evm.ts +241 -0
  232. package/server/routes/send-solana.ts +281 -0
  233. package/server/routes/send.ts +178 -0
  234. package/server/routes/setup.ts +210 -0
  235. package/server/routes/strategy.ts +894 -0
  236. package/server/routes/swap-evm.ts +353 -0
  237. package/server/routes/swap-solana.ts +177 -0
  238. package/server/routes/swap.ts +356 -0
  239. package/server/routes/token.ts +247 -0
  240. package/server/routes/unlock.ts +403 -0
  241. package/server/routes/wallet-assets.ts +361 -0
  242. package/server/routes/wallet-transactions.ts +515 -0
  243. package/server/routes/wallet.ts +710 -0
  244. package/server/types.ts +146 -0
  245. package/skills/aurawallet/SKILL.md +739 -0
  246. package/skills/aurawallet-setup/SKILL.md +74 -0
  247. package/skills/security-review/SKILL.md +148 -0
  248. package/src/app/api/agent-requests/route.ts +30 -0
  249. package/src/app/api/apps/install/route.ts +126 -0
  250. package/src/app/api/apps/manifests/route.ts +16 -0
  251. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  252. package/src/app/api/events/route.ts +92 -0
  253. package/src/app/api/page.tsx +212 -0
  254. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  255. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  256. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  257. package/src/app/api/workspace/[id]/route.ts +168 -0
  258. package/src/app/api/workspace/auth.ts +34 -0
  259. package/src/app/api/workspace/config/route.ts +106 -0
  260. package/src/app/api/workspace/import/route.ts +127 -0
  261. package/src/app/api/workspace/route.ts +116 -0
  262. package/src/app/app/page.tsx +2122 -0
  263. package/src/app/apple-icon.png +0 -0
  264. package/src/app/docs/page.tsx +178 -0
  265. package/src/app/favicon.ico +0 -0
  266. package/src/app/globals.css +572 -0
  267. package/src/app/health/page.tsx +5 -0
  268. package/src/app/hello/page.tsx +15 -0
  269. package/src/app/icon.png +0 -0
  270. package/src/app/layout.tsx +34 -0
  271. package/src/app/page.tsx +986 -0
  272. package/src/app/providers.tsx +90 -0
  273. package/src/app/share/[token]/page.tsx +295 -0
  274. package/src/components/ChainSelector.tsx +144 -0
  275. package/src/components/HumanActionBar.tsx +695 -0
  276. package/src/components/NotificationDrawer.tsx +129 -0
  277. package/src/components/apps/AgentKeysApp.tsx +490 -0
  278. package/src/components/apps/App.tsx +153 -0
  279. package/src/components/apps/AppGrid.tsx +15 -0
  280. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  281. package/src/components/apps/DraggableApp.tsx +562 -0
  282. package/src/components/apps/IFrameApp.tsx +73 -0
  283. package/src/components/apps/LogsApp.tsx +360 -0
  284. package/src/components/apps/SendApp.tsx +394 -0
  285. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  286. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  287. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  288. package/src/components/apps/TokenApp.tsx +319 -0
  289. package/src/components/apps/TransactionsApp.tsx +438 -0
  290. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  291. package/src/components/apps/index.ts +13 -0
  292. package/src/components/design-system/Button.tsx +53 -0
  293. package/src/components/design-system/ChainIndicator.tsx +65 -0
  294. package/src/components/design-system/ChainSelector.tsx +137 -0
  295. package/src/components/design-system/ConfirmationModal.tsx +106 -0
  296. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  297. package/src/components/design-system/Drawer.tsx +123 -0
  298. package/src/components/design-system/FilterDropdown.tsx +72 -0
  299. package/src/components/design-system/Modal.tsx +206 -0
  300. package/src/components/design-system/Popover.tsx +142 -0
  301. package/src/components/design-system/TextInput.tsx +85 -0
  302. package/src/components/design-system/Toggle.tsx +58 -0
  303. package/src/components/design-system/index.ts +11 -0
  304. package/src/components/docs/DocsThemeToggle.tsx +49 -0
  305. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  306. package/src/components/icons/ChainIcons.tsx +72 -0
  307. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  308. package/src/components/layout/ContentArea.tsx +21 -0
  309. package/src/components/layout/TabBar.tsx +278 -0
  310. package/src/components/layout/WalletSidebar.tsx +1033 -0
  311. package/src/components/layout/index.ts +4 -0
  312. package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
  313. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  314. package/src/components/vault/ApiKeysConsole.tsx +1080 -0
  315. package/src/components/vault/AuditConsole.tsx +584 -0
  316. package/src/components/vault/CredentialDetail.tsx +455 -0
  317. package/src/components/vault/CredentialEmpty.tsx +55 -0
  318. package/src/components/vault/CredentialField.tsx +361 -0
  319. package/src/components/vault/CredentialForm.tsx +1212 -0
  320. package/src/components/vault/CredentialList.tsx +165 -0
  321. package/src/components/vault/CredentialRow.tsx +97 -0
  322. package/src/components/vault/CredentialShareModal.tsx +178 -0
  323. package/src/components/vault/CredentialVault.tsx +754 -0
  324. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  325. package/src/components/vault/ImportCredentialsModal.tsx +515 -0
  326. package/src/components/vault/LargeTypeModal.tsx +64 -0
  327. package/src/components/vault/PasswordGenerator.tsx +224 -0
  328. package/src/components/vault/TOTPDisplay.tsx +123 -0
  329. package/src/components/vault/VaultSidebar.tsx +413 -0
  330. package/src/components/vault/types.ts +54 -0
  331. package/src/context/AuthContext.tsx +337 -0
  332. package/src/context/PriceContext.tsx +113 -0
  333. package/src/context/ThemeContext.tsx +164 -0
  334. package/src/context/WebSocketContext.tsx +269 -0
  335. package/src/context/WorkspaceContext.tsx +668 -0
  336. package/src/hooks/index.ts +3 -0
  337. package/src/hooks/useAgentActions.ts +368 -0
  338. package/src/hooks/useBalance.ts +103 -0
  339. package/src/hooks/useBalances.ts +129 -0
  340. package/src/instrumentation.ts +12 -0
  341. package/src/lib/api.ts +449 -0
  342. package/src/lib/app-loader.ts +148 -0
  343. package/src/lib/app-registry.ts +178 -0
  344. package/src/lib/app-sdk.ts +157 -0
  345. package/src/lib/audit-console-adapter.ts +151 -0
  346. package/src/lib/auth-client.ts +75 -0
  347. package/src/lib/config.ts +74 -0
  348. package/src/lib/crypto.ts +112 -0
  349. package/src/lib/db.ts +21 -0
  350. package/src/lib/docs.ts +390 -0
  351. package/src/lib/events.ts +361 -0
  352. package/src/lib/pino.ts +24 -0
  353. package/src/lib/theme-handlers.ts +168 -0
  354. package/src/lib/theme.ts +351 -0
  355. package/src/lib/tokenData.ts +378 -0
  356. package/src/lib/vault-crypto.ts +129 -0
  357. package/src/lib/websocket-server.ts +302 -0
  358. package/src/lib/websocket-setup.ts +79 -0
  359. package/src/lib/wordlist.ts +2050 -0
  360. package/src/lib/workspace-handlers.ts +285 -0
  361. package/start.sh +80 -0
  362. package/tailwind.config.ts +99 -0
  363. package/tsconfig.json +42 -0
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useMemo, useState } from 'react';
4
+ import { Copy, ExternalLink, Wallet } from 'lucide-react';
5
+ import { api, Api } from '@/lib/api';
6
+ import type { WalletLinkMetaV1 } from './types';
7
+
8
+ interface WalletSummary {
9
+ address: string;
10
+ tier: 'cold' | 'hot' | 'temp';
11
+ chain: string;
12
+ balance?: string;
13
+ name?: string;
14
+ label?: string;
15
+ }
16
+
17
+ interface CredentialWalletWidgetProps {
18
+ walletLink: WalletLinkMetaV1;
19
+ }
20
+
21
+ const shortAddress = (addr: string) => `${addr.slice(0, 6)}...${addr.slice(-4)}`;
22
+
23
+ function explorerUrl(chain: string, address: string): string {
24
+ if (chain === 'solana' || chain === 'solana-devnet') return `https://solscan.io/account/${address}`;
25
+ if (chain === 'polygon') return `https://polygonscan.com/address/${address}`;
26
+ if (chain === 'ethereum') return `https://etherscan.io/address/${address}`;
27
+ return `https://basescan.org/address/${address}`;
28
+ }
29
+
30
+ export const CredentialWalletWidget: React.FC<CredentialWalletWidgetProps> = ({ walletLink }) => {
31
+ const [wallet, setWallet] = useState<WalletSummary | null>(null);
32
+ const [loading, setLoading] = useState(true);
33
+ const [copyOk, setCopyOk] = useState(false);
34
+ const normalizedChain = walletLink.chain.toLowerCase();
35
+
36
+ useEffect(() => {
37
+ let mounted = true;
38
+ (async () => {
39
+ try {
40
+ const res = await api.get<{ wallets: WalletSummary[] }>(Api.Wallet, '/wallets');
41
+ const match = (res.wallets || []).find((w) =>
42
+ w.address.toLowerCase() === walletLink.walletAddress.toLowerCase() && w.chain.toLowerCase() === normalizedChain,
43
+ ) || null;
44
+ if (mounted) setWallet(match);
45
+ } catch {
46
+ if (mounted) setWallet(null);
47
+ } finally {
48
+ if (mounted) setLoading(false);
49
+ }
50
+ })();
51
+ return () => { mounted = false; };
52
+ }, [walletLink.walletAddress, normalizedChain, walletLink.chain]);
53
+
54
+ const resolvedName = useMemo(() => wallet?.name || wallet?.label || walletLink.label || 'Linked Wallet', [wallet, walletLink.label]);
55
+
56
+ return (
57
+ <div className="mt-2 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background-alt,#f4f4f5)] p-3">
58
+ <div className="flex items-center justify-between mb-2">
59
+ <div className="font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)] inline-flex items-center gap-1">
60
+ <Wallet size={11} /> Linked Wallet
61
+ </div>
62
+ <div className="font-mono text-[8px] uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">
63
+ {walletLink.tier} · {walletLink.chain}
64
+ </div>
65
+ </div>
66
+
67
+ <div className="font-mono text-[11px] text-[var(--color-text,#0a0a0a)] mb-1">{resolvedName}</div>
68
+ <div className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)] mb-2 break-all">
69
+ {walletLink.walletAddress}
70
+ </div>
71
+
72
+ {loading ? (
73
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">Loading wallet state…</div>
74
+ ) : wallet ? (
75
+ <div className="font-mono text-[9px] text-[var(--color-success,#22c55e)] mb-2">Wallet resolved{wallet.balance ? ` · Balance: ${wallet.balance}` : ''}</div>
76
+ ) : (
77
+ <div className="font-mono text-[9px] text-[var(--color-warning,#f59e0b)] mb-2">Stale link: wallet not found in current wallet list</div>
78
+ )}
79
+
80
+ <div className="flex gap-2">
81
+ <button
82
+ type="button"
83
+ className="px-2 py-1 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px]"
84
+ onClick={async () => {
85
+ await navigator.clipboard.writeText(walletLink.walletAddress);
86
+ setCopyOk(true);
87
+ setTimeout(() => setCopyOk(false), 1200);
88
+ }}
89
+ >
90
+ <span className="inline-flex items-center gap-1"><Copy size={10} /> {copyOk ? 'COPIED' : shortAddress(walletLink.walletAddress)}</span>
91
+ </button>
92
+ <a
93
+ href={explorerUrl(normalizedChain, walletLink.walletAddress)}
94
+ target="_blank"
95
+ rel="noreferrer"
96
+ className="px-2 py-1 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px] inline-flex items-center gap-1"
97
+ >
98
+ <ExternalLink size={10} /> Explorer
99
+ </a>
100
+ </div>
101
+ </div>
102
+ );
103
+ };
@@ -0,0 +1,515 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useCallback } from 'react';
4
+ import { Upload, FileText, AlertCircle, CheckCircle, Info } from 'lucide-react';
5
+ import { Modal, Button } from '@/components/design-system';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ interface ImportSource {
12
+ label: string;
13
+ format: string;
14
+ supported: boolean;
15
+ }
16
+
17
+ const IMPORT_SOURCES: ImportSource[] = [
18
+ { label: '1Password', format: '1password-csv', supported: true },
19
+ { label: 'Bitwarden', format: 'bitwarden-csv', supported: true },
20
+ { label: 'Chrome', format: 'chrome-csv', supported: true },
21
+ { label: 'Firefox', format: 'firefox-csv', supported: true },
22
+ { label: 'iCloud Keychain', format: 'icloud-csv', supported: false },
23
+ { label: 'LastPass', format: 'lastpass-csv', supported: false },
24
+ ];
25
+
26
+ type DuplicateStrategy = 'skip' | 'rename' | 'overwrite';
27
+
28
+ const STRATEGY_LABELS: Record<DuplicateStrategy, string> = {
29
+ skip: 'Skip',
30
+ rename: 'Rename',
31
+ overwrite: 'Create Anyway',
32
+ };
33
+
34
+ interface PreviewCredential {
35
+ name: string;
36
+ type: string;
37
+ url?: string;
38
+ fieldCount: number;
39
+ isDuplicate: boolean;
40
+ duplicateMatch?: string;
41
+ }
42
+
43
+ interface PreviewResult {
44
+ success: boolean;
45
+ total: number;
46
+ duplicates: number;
47
+ credentials: PreviewCredential[];
48
+ error?: string;
49
+ }
50
+
51
+ interface ImportResult {
52
+ success: boolean;
53
+ imported: number;
54
+ skipped: number;
55
+ errors: { row: number; reason: string }[];
56
+ error?: string;
57
+ }
58
+
59
+ type Step = 'select' | 'preview' | 'result';
60
+
61
+ interface ImportCredentialsModalProps {
62
+ isOpen: boolean;
63
+ onClose: () => void;
64
+ onComplete: () => void;
65
+ vaultId: string | null;
66
+ vaultName: string;
67
+ walletBaseUrl: string;
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Helpers
72
+ // ---------------------------------------------------------------------------
73
+
74
+ function getToken(): string | null {
75
+ if (typeof window === 'undefined') return null;
76
+ return sessionStorage.getItem('aurawallet_admin_token');
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Component
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export const ImportCredentialsModal: React.FC<ImportCredentialsModalProps> = ({
84
+ isOpen,
85
+ onClose,
86
+ onComplete,
87
+ vaultId,
88
+ vaultName,
89
+ walletBaseUrl,
90
+ }) => {
91
+ const [step, setStep] = useState<Step>('select');
92
+ const [selectedFormat, setSelectedFormat] = useState<string>('1password-csv');
93
+ const [file, setFile] = useState<File | null>(null);
94
+ const [duplicateStrategy, setDuplicateStrategy] = useState<DuplicateStrategy>('skip');
95
+ const [preview, setPreview] = useState<PreviewResult | null>(null);
96
+ const [result, setResult] = useState<ImportResult | null>(null);
97
+ const [loading, setLoading] = useState(false);
98
+ const [error, setError] = useState<string | null>(null);
99
+ const fileInputRef = useRef<HTMLInputElement>(null);
100
+
101
+ const reset = useCallback(() => {
102
+ setStep('select');
103
+ setSelectedFormat('1password-csv');
104
+ setFile(null);
105
+ setDuplicateStrategy('skip');
106
+ setPreview(null);
107
+ setResult(null);
108
+ setLoading(false);
109
+ setError(null);
110
+ if (fileInputRef.current) fileInputRef.current.value = '';
111
+ }, []);
112
+
113
+ const handleClose = useCallback(() => {
114
+ reset();
115
+ onClose();
116
+ }, [reset, onClose]);
117
+
118
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
119
+
120
+ const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
121
+ const f = e.target.files?.[0] ?? null;
122
+ if (f && f.size > MAX_FILE_SIZE) {
123
+ setError(`File too large (${(f.size / 1024 / 1024).toFixed(1)}MB). Maximum is 10MB.`);
124
+ setFile(null);
125
+ if (fileInputRef.current) fileInputRef.current.value = '';
126
+ return;
127
+ }
128
+ setFile(f);
129
+ setError(null);
130
+ }, []);
131
+
132
+ const buildFormData = useCallback(
133
+ (dryRun: boolean) => {
134
+ if (!file) return null;
135
+ const fd = new FormData();
136
+ fd.append('file', file);
137
+ fd.append('format', selectedFormat);
138
+ fd.append('vaultId', vaultId || 'primary');
139
+ if (dryRun) fd.append('dryRun', 'true');
140
+ fd.append('duplicateStrategy', duplicateStrategy);
141
+ return fd;
142
+ },
143
+ [file, selectedFormat, vaultId, duplicateStrategy],
144
+ );
145
+
146
+ const handlePreview = useCallback(async () => {
147
+ if (!file) return;
148
+ setLoading(true);
149
+ setError(null);
150
+ try {
151
+ const fd = buildFormData(true);
152
+ if (!fd) return;
153
+ const token = getToken();
154
+ const res = await fetch(`${walletBaseUrl}/credentials/import`, {
155
+ method: 'POST',
156
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
157
+ body: fd,
158
+ });
159
+ const contentType = res.headers.get('content-type') || '';
160
+ if (!contentType.includes('application/json')) {
161
+ setError(`Server error: ${res.status} ${res.statusText}`);
162
+ return;
163
+ }
164
+ const data: PreviewResult = await res.json();
165
+ if (!res.ok || !data.success) {
166
+ setError(data.error || `Request failed: ${res.status}`);
167
+ return;
168
+ }
169
+ setPreview(data);
170
+ setStep('preview');
171
+ } catch (err) {
172
+ setError(err instanceof Error ? err.message : 'Preview failed');
173
+ } finally {
174
+ setLoading(false);
175
+ }
176
+ }, [file, buildFormData, walletBaseUrl]);
177
+
178
+ const handleImport = useCallback(async () => {
179
+ setLoading(true);
180
+ setError(null);
181
+ try {
182
+ const fd = buildFormData(false);
183
+ if (!fd) return;
184
+ const token = getToken();
185
+ const res = await fetch(`${walletBaseUrl}/credentials/import`, {
186
+ method: 'POST',
187
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
188
+ body: fd,
189
+ });
190
+ const ct = res.headers.get('content-type') || '';
191
+ if (!ct.includes('application/json')) {
192
+ setError(`Server error: ${res.status} ${res.statusText}`);
193
+ return;
194
+ }
195
+ const data: ImportResult = await res.json();
196
+ if (!res.ok) {
197
+ setError(data.error || `Request failed: ${res.status}`);
198
+ return;
199
+ }
200
+ setResult(data);
201
+ setStep('result');
202
+ } catch (err) {
203
+ setError(err instanceof Error ? err.message : 'Import failed');
204
+ } finally {
205
+ setLoading(false);
206
+ }
207
+ }, [buildFormData, walletBaseUrl]);
208
+
209
+ const handleDone = useCallback(() => {
210
+ onComplete();
211
+ handleClose();
212
+ }, [onComplete, handleClose]);
213
+
214
+ // -------------------------------------------------------------------------
215
+ // Render helpers
216
+ // -------------------------------------------------------------------------
217
+
218
+ const selectedSource = IMPORT_SOURCES.find((s) => s.format === selectedFormat);
219
+
220
+ const renderSelectStep = () => (
221
+ <div className="space-y-5">
222
+ {/* Source selector */}
223
+ <div>
224
+ <label className="block text-[9px] font-bold tracking-widest uppercase text-[var(--color-text-muted,#6b7280)] mb-2">
225
+ Source
226
+ </label>
227
+ <div className="grid grid-cols-1 gap-1.5">
228
+ {IMPORT_SOURCES.map((source) => (
229
+ <button
230
+ key={source.format}
231
+ onClick={() => source.supported && setSelectedFormat(source.format)}
232
+ disabled={!source.supported}
233
+ className={`flex items-center gap-2 px-3 py-2 text-left transition-colors border ${
234
+ selectedFormat === source.format
235
+ ? 'border-[var(--color-accent,#ccff00)] bg-[var(--color-accent,#ccff00)]/5'
236
+ : source.supported
237
+ ? 'border-[var(--color-border,#d4d4d8)] hover:bg-[var(--color-background-alt,#f4f4f5)]'
238
+ : 'border-[var(--color-border,#d4d4d8)] opacity-40 cursor-not-allowed'
239
+ }`}
240
+ >
241
+ <FileText size={12} className="text-[var(--color-text-muted,#6b7280)] flex-shrink-0" />
242
+ <span className="text-[10px] font-mono tracking-wider uppercase text-[var(--color-text,#0a0a0a)]">
243
+ {source.label}
244
+ </span>
245
+ {!source.supported && (
246
+ <span className="ml-auto text-[8px] font-mono tracking-wider uppercase text-[var(--color-text-faint,#9ca3af)]">
247
+ Coming soon
248
+ </span>
249
+ )}
250
+ </button>
251
+ ))}
252
+ </div>
253
+ </div>
254
+
255
+ {/* File input */}
256
+ <div>
257
+ <label className="block text-[9px] font-bold tracking-widest uppercase text-[var(--color-text-muted,#6b7280)] mb-2">
258
+ File
259
+ </label>
260
+ <input
261
+ ref={fileInputRef}
262
+ type="file"
263
+ accept=".csv"
264
+ onChange={handleFileChange}
265
+ className="hidden"
266
+ />
267
+ <button
268
+ onClick={() => fileInputRef.current?.click()}
269
+ className="w-full flex items-center justify-center gap-2 px-4 py-4 border-2 border-dashed border-[var(--color-border,#d4d4d8)] hover:border-[var(--color-text-muted,#6b7280)] transition-colors"
270
+ >
271
+ <Upload size={14} className="text-[var(--color-text-muted,#6b7280)]" />
272
+ <span className="text-[10px] font-mono tracking-wider text-[var(--color-text-muted,#6b7280)]">
273
+ {file ? file.name : 'Choose CSV file'}
274
+ </span>
275
+ </button>
276
+ </div>
277
+
278
+ {/* Vault target */}
279
+ <div className="flex items-center gap-2 px-2 py-1.5 bg-[var(--color-background-alt,#f4f4f5)]">
280
+ <Info size={10} className="text-[var(--color-text-faint,#9ca3af)] flex-shrink-0" />
281
+ <span className="text-[9px] font-mono tracking-wider text-[var(--color-text-muted,#6b7280)]">
282
+ Importing to <strong className="text-[var(--color-text,#0a0a0a)]">{vaultName}</strong>
283
+ </span>
284
+ </div>
285
+
286
+ {/* Error */}
287
+ {error && (
288
+ <div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-200">
289
+ <AlertCircle size={12} className="text-red-500 flex-shrink-0 mt-0.5" />
290
+ <span className="text-[10px] font-mono text-red-700">{error}</span>
291
+ </div>
292
+ )}
293
+
294
+ {/* Actions */}
295
+ <div className="flex gap-2 justify-end">
296
+ <Button variant="secondary" size="sm" onClick={handleClose}>
297
+ CANCEL
298
+ </Button>
299
+ <Button
300
+ size="sm"
301
+ onClick={handlePreview}
302
+ loading={loading}
303
+ disabled={!file || !selectedSource?.supported}
304
+ >
305
+ PREVIEW
306
+ </Button>
307
+ </div>
308
+ </div>
309
+ );
310
+
311
+ const renderPreviewStep = () => {
312
+ if (!preview) return null;
313
+ return (
314
+ <div className="space-y-4">
315
+ {/* Summary */}
316
+ <div className="flex gap-4">
317
+ <div className="text-center">
318
+ <div className="text-[18px] font-bold font-mono text-[var(--color-text,#0a0a0a)]">
319
+ {preview.total}
320
+ </div>
321
+ <div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
322
+ Total
323
+ </div>
324
+ </div>
325
+ {preview.duplicates > 0 && (
326
+ <div className="text-center">
327
+ <div className="text-[18px] font-bold font-mono text-amber-600">
328
+ {preview.duplicates}
329
+ </div>
330
+ <div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
331
+ Duplicates
332
+ </div>
333
+ </div>
334
+ )}
335
+ </div>
336
+
337
+ {/* Preview list */}
338
+ <div className="max-h-[240px] overflow-y-auto border border-[var(--color-border,#d4d4d8)]">
339
+ {preview.credentials.length === 0 ? (
340
+ <div className="px-4 py-8 text-center text-[10px] font-mono text-[var(--color-text-muted,#6b7280)]">
341
+ No credentials found in file
342
+ </div>
343
+ ) : (
344
+ preview.credentials.map((c, i) => (
345
+ <div
346
+ key={i}
347
+ className={`flex items-center gap-2 px-3 py-1.5 border-b border-[var(--color-border,#d4d4d8)] last:border-b-0 ${
348
+ c.isDuplicate ? 'bg-amber-50/50' : ''
349
+ }`}
350
+ >
351
+ <div className="flex-1 min-w-0">
352
+ <div className="text-[10px] font-mono text-[var(--color-text,#0a0a0a)] truncate">
353
+ {c.name}
354
+ </div>
355
+ {c.url && (
356
+ <div className="text-[8px] font-mono text-[var(--color-text-faint,#9ca3af)] truncate">
357
+ {c.url}
358
+ </div>
359
+ )}
360
+ </div>
361
+ <span className="text-[8px] font-mono tracking-wider uppercase text-[var(--color-text-faint,#9ca3af)] flex-shrink-0">
362
+ {c.type}
363
+ </span>
364
+ {c.isDuplicate && (
365
+ <span className="text-[8px] font-mono tracking-wider uppercase text-amber-600 flex-shrink-0">
366
+ DUP
367
+ </span>
368
+ )}
369
+ </div>
370
+ ))
371
+ )}
372
+ {preview.total > preview.credentials.length && (
373
+ <div className="px-3 py-1.5 text-[9px] font-mono text-[var(--color-text-faint,#9ca3af)] text-center">
374
+ + {preview.total - preview.credentials.length} more
375
+ </div>
376
+ )}
377
+ </div>
378
+
379
+ {/* Duplicate strategy */}
380
+ {preview.duplicates > 0 && (
381
+ <div>
382
+ <label className="block text-[9px] font-bold tracking-widest uppercase text-[var(--color-text-muted,#6b7280)] mb-1.5">
383
+ Duplicate handling
384
+ </label>
385
+ <div className="flex gap-1.5">
386
+ {(['skip', 'rename', 'overwrite'] as DuplicateStrategy[]).map((s) => (
387
+ <button
388
+ key={s}
389
+ onClick={() => setDuplicateStrategy(s)}
390
+ className={`px-3 py-1.5 text-[9px] font-mono tracking-wider uppercase border transition-colors ${
391
+ duplicateStrategy === s
392
+ ? 'border-[var(--color-accent,#ccff00)] bg-[var(--color-accent,#ccff00)]/10 text-[var(--color-text,#0a0a0a)]'
393
+ : 'border-[var(--color-border,#d4d4d8)] text-[var(--color-text-muted,#6b7280)] hover:bg-[var(--color-background-alt,#f4f4f5)]'
394
+ }`}
395
+ >
396
+ {STRATEGY_LABELS[s]}
397
+ </button>
398
+ ))}
399
+ </div>
400
+ </div>
401
+ )}
402
+
403
+ {/* Error */}
404
+ {error && (
405
+ <div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-200">
406
+ <AlertCircle size={12} className="text-red-500 flex-shrink-0 mt-0.5" />
407
+ <span className="text-[10px] font-mono text-red-700">{error}</span>
408
+ </div>
409
+ )}
410
+
411
+ {/* Actions */}
412
+ <div className="flex gap-2 justify-end">
413
+ <Button variant="secondary" size="sm" onClick={() => { setStep('select'); setError(null); if (fileInputRef.current) fileInputRef.current.value = ''; }}>
414
+ BACK
415
+ </Button>
416
+ <Button
417
+ size="sm"
418
+ onClick={handleImport}
419
+ loading={loading}
420
+ disabled={preview.credentials.length === 0}
421
+ >
422
+ IMPORT {duplicateStrategy === 'skip' ? preview.total - preview.duplicates : preview.total} CREDENTIAL{(duplicateStrategy === 'skip' ? preview.total - preview.duplicates : preview.total) !== 1 ? 'S' : ''}
423
+ </Button>
424
+ </div>
425
+ </div>
426
+ );
427
+ };
428
+
429
+ const renderResultStep = () => {
430
+ if (!result) return null;
431
+ const hasErrors = result.errors.length > 0;
432
+ return (
433
+ <div className="space-y-4">
434
+ {/* Result icon */}
435
+ <div className="flex items-center gap-3">
436
+ {result.success ? (
437
+ <CheckCircle size={20} className="text-green-600" />
438
+ ) : (
439
+ <AlertCircle size={20} className="text-red-500" />
440
+ )}
441
+ <div>
442
+ <div className="text-[11px] font-mono font-bold text-[var(--color-text,#0a0a0a)]">
443
+ {result.success ? 'Import Complete' : 'Import Failed'}
444
+ </div>
445
+ </div>
446
+ </div>
447
+
448
+ {/* Counts */}
449
+ <div className="flex gap-6">
450
+ <div className="text-center">
451
+ <div className="text-[18px] font-bold font-mono text-green-600">{result.imported}</div>
452
+ <div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
453
+ Imported
454
+ </div>
455
+ </div>
456
+ {result.skipped > 0 && (
457
+ <div className="text-center">
458
+ <div className="text-[18px] font-bold font-mono text-[var(--color-text-muted,#6b7280)]">
459
+ {result.skipped}
460
+ </div>
461
+ <div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
462
+ Skipped
463
+ </div>
464
+ </div>
465
+ )}
466
+ {hasErrors && (
467
+ <div className="text-center">
468
+ <div className="text-[18px] font-bold font-mono text-red-500">
469
+ {result.errors.length}
470
+ </div>
471
+ <div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
472
+ Errors
473
+ </div>
474
+ </div>
475
+ )}
476
+ </div>
477
+
478
+ {/* Error details */}
479
+ {hasErrors && (
480
+ <div className="max-h-[120px] overflow-y-auto border border-red-200 bg-red-50">
481
+ {result.errors.map((e, i) => (
482
+ <div
483
+ key={i}
484
+ className="px-3 py-1 text-[9px] font-mono text-red-700 border-b border-red-100 last:border-b-0"
485
+ >
486
+ Row {e.row}: {e.reason}
487
+ </div>
488
+ ))}
489
+ </div>
490
+ )}
491
+
492
+ {/* Actions */}
493
+ <div className="flex justify-end">
494
+ <Button size="sm" onClick={handleDone}>
495
+ DONE
496
+ </Button>
497
+ </div>
498
+ </div>
499
+ );
500
+ };
501
+
502
+ return (
503
+ <Modal
504
+ isOpen={isOpen}
505
+ onClose={handleClose}
506
+ title="Import Credentials"
507
+ subtitle="Credential_Import"
508
+ size="md"
509
+ >
510
+ {step === 'select' && renderSelectStep()}
511
+ {step === 'preview' && renderPreviewStep()}
512
+ {step === 'result' && renderResultStep()}
513
+ </Modal>
514
+ );
515
+ };
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Modal } from '@/components/design-system';
5
+
6
+ interface LargeTypeModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ value: string;
10
+ }
11
+
12
+ export const LargeTypeModal: React.FC<LargeTypeModalProps> = ({ isOpen, onClose, value }) => {
13
+ const characters = Array.from(value ?? '');
14
+
15
+ const renderCharacter = (char: string): string => {
16
+ if (char === ' ') return '␠';
17
+ if (char === '\n') return '↵';
18
+ if (char === '\t') return '⇥';
19
+ return char;
20
+ };
21
+
22
+ return (
23
+ <Modal
24
+ isOpen={isOpen}
25
+ onClose={onClose}
26
+ title=""
27
+ size="lg"
28
+ hideHeader
29
+ hideTitle
30
+ contentClassName="!p-0"
31
+ >
32
+ <button
33
+ type="button"
34
+ onClick={onClose}
35
+ className="w-full min-h-[360px] px-6 py-8 flex flex-col items-center justify-center text-center"
36
+ >
37
+ <div className="w-full max-w-[900px] max-h-[70vh] overflow-auto px-1">
38
+ <div className="grid gap-2 [grid-template-columns:repeat(auto-fit,minmax(56px,1fr))]">
39
+ {characters.map((char, index) => (
40
+ <div
41
+ key={`${index}-${char}`}
42
+ data-testid={`large-type-char-${index + 1}`}
43
+ className="rounded-sm border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#ffffff)] px-1 py-2"
44
+ >
45
+ <div className="font-mono text-3xl font-bold leading-none text-[var(--color-text,#0a0a0a)] select-all">
46
+ {renderCharacter(char)}
47
+ </div>
48
+ <div
49
+ data-testid={`large-type-index-${index + 1}`}
50
+ className="mt-2 font-mono text-[9px] uppercase tracking-widest text-[var(--color-text-faint,#9ca3af)]"
51
+ >
52
+ {index + 1}
53
+ </div>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ </div>
58
+ <div className="mt-8 font-mono text-[9px] tracking-widest uppercase text-[var(--color-text-faint,#9ca3af)]">
59
+ Click anywhere to dismiss
60
+ </div>
61
+ </button>
62
+ </Modal>
63
+ );
64
+ };