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,562 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
4
+ import { X, GripVertical, LucideIcon, Lock, Unlock, ExternalLink, RotateCcw } from 'lucide-react';
5
+
6
+ // App color system - uses CSS variables for theme support
7
+ export type AppColor = 'blue' | 'orange' | 'lime' | 'purple' | 'gray' | 'teal' | 'rose';
8
+
9
+ // CSS variable-based color classes for theme awareness
10
+ export const APP_COLORS: Record<AppColor, { band: string; accent: string; bg: string; text: string }> = {
11
+ blue: {
12
+ band: 'bg-[var(--app-blue-band,#0047ff)]',
13
+ accent: 'border-[var(--app-blue-accent,rgba(0,71,255,0.3))]',
14
+ bg: 'bg-[var(--app-blue-bg,rgba(0,71,255,0.05))]',
15
+ text: 'text-[var(--app-blue-text,#0047ff)]',
16
+ },
17
+ orange: {
18
+ band: 'bg-[var(--app-orange-band,#ff4d00)]',
19
+ accent: 'border-[var(--app-orange-accent,rgba(255,77,0,0.3))]',
20
+ bg: 'bg-[var(--app-orange-bg,rgba(255,77,0,0.05))]',
21
+ text: 'text-[var(--app-orange-text,#ff4d00)]',
22
+ },
23
+ lime: {
24
+ band: 'bg-[var(--app-lime-band,#84cc16)]',
25
+ accent: 'border-[var(--app-lime-accent,rgba(132,204,22,0.3))]',
26
+ bg: 'bg-[var(--app-lime-bg,rgba(132,204,22,0.05))]',
27
+ text: 'text-[var(--app-lime-text,#65a30d)]',
28
+ },
29
+ purple: {
30
+ band: 'bg-[var(--app-purple-band,#7c3aed)]',
31
+ accent: 'border-[var(--app-purple-accent,rgba(124,58,237,0.3))]',
32
+ bg: 'bg-[var(--app-purple-bg,rgba(124,58,237,0.05))]',
33
+ text: 'text-[var(--app-purple-text,#7c3aed)]',
34
+ },
35
+ gray: {
36
+ band: 'bg-[var(--app-gray-band,#6b7280)]',
37
+ accent: 'border-[var(--app-gray-accent,rgba(107,114,128,0.3))]',
38
+ bg: 'bg-[var(--app-gray-bg,rgba(107,114,128,0.05))]',
39
+ text: 'text-[var(--app-gray-text,#6b7280)]',
40
+ },
41
+ teal: {
42
+ band: 'bg-[var(--app-teal-band,#14b8a6)]',
43
+ accent: 'border-[var(--app-teal-accent,rgba(20,184,166,0.3))]',
44
+ bg: 'bg-[var(--app-teal-bg,rgba(20,184,166,0.05))]',
45
+ text: 'text-[var(--app-teal-text,#14b8a6)]',
46
+ },
47
+ rose: {
48
+ band: 'bg-[var(--app-rose-band,#f43f5e)]',
49
+ accent: 'border-[var(--app-rose-accent,rgba(244,63,94,0.3))]',
50
+ bg: 'bg-[var(--app-rose-bg,rgba(244,63,94,0.05))]',
51
+ text: 'text-[var(--app-rose-text,#f43f5e)]',
52
+ },
53
+ };
54
+
55
+ type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' | null;
56
+
57
+ interface DraggableAppProps {
58
+ id: string;
59
+ title: string;
60
+ subtitle?: string;
61
+ subtitleLink?: string;
62
+ icon?: LucideIcon;
63
+ color?: AppColor | string;
64
+ initialPosition?: { x: number; y: number };
65
+ initialSize?: { width: number; height: number };
66
+ minWidth?: number;
67
+ minHeight?: number;
68
+ maxWidth?: number;
69
+ maxHeight?: number;
70
+ resizable?: boolean;
71
+ dismissable?: boolean;
72
+ locked?: boolean;
73
+ onLockChange?: (id: string, locked: boolean) => void;
74
+ onRefresh?: () => void;
75
+ onDismiss?: () => void;
76
+ onPositionChange?: (id: string, pos: { x: number; y: number }) => void;
77
+ onSizeChange?: (id: string, size: { width: number; height: number }) => void;
78
+ onBringToFront?: (id: string) => void;
79
+ children: React.ReactNode;
80
+ className?: string;
81
+ status?: 'normal' | 'alert' | 'success';
82
+ zIndex?: number;
83
+ focusTrigger?: number;
84
+ }
85
+
86
+ export const DraggableApp: React.FC<DraggableAppProps> = ({
87
+ id,
88
+ title,
89
+ subtitle,
90
+ subtitleLink,
91
+ icon: Icon,
92
+ color,
93
+ initialPosition = { x: 20, y: 20 },
94
+ initialSize = { width: 320, height: 'auto' as unknown as number },
95
+ minWidth = 200,
96
+ minHeight = 100,
97
+ maxWidth = 2000,
98
+ maxHeight = 2000,
99
+ resizable = true,
100
+ dismissable = false,
101
+ locked = false,
102
+ onLockChange,
103
+ onRefresh,
104
+ onDismiss,
105
+ onPositionChange,
106
+ onSizeChange,
107
+ onBringToFront,
108
+ children,
109
+ className = '',
110
+ status = 'normal',
111
+ zIndex = 10,
112
+ focusTrigger,
113
+ }) => {
114
+ const isPresetColor = color && color in APP_COLORS;
115
+ const colorScheme = isPresetColor ? APP_COLORS[color as AppColor] : null;
116
+ const customColor = !isPresetColor && color?.startsWith('#') ? color : null;
117
+
118
+ // Committed state (what's saved)
119
+ const [position, setPosition] = useState(initialPosition);
120
+ const [size, setSize] = useState({
121
+ width: typeof initialSize.width === 'number' ? initialSize.width : 320,
122
+ height: typeof initialSize.height === 'number' ? initialSize.height : 200,
123
+ });
124
+
125
+ // Interaction state
126
+ const [isDragging, setIsDragging] = useState(false);
127
+ const [isResizing, setIsResizing] = useState(false);
128
+ const [isLocked, setIsLocked] = useState(locked);
129
+ const [, setIsFocusing] = useState(false);
130
+
131
+ // Refs for smooth dragging/resizing (avoid re-renders during movement)
132
+ const appRef = useRef<HTMLDivElement>(null);
133
+ const dragRef = useRef({ startX: 0, startY: 0, startPosX: 0, startPosY: 0 });
134
+ const resizeRef = useRef({
135
+ direction: null as ResizeDirection,
136
+ startX: 0,
137
+ startY: 0,
138
+ startWidth: 0,
139
+ startHeight: 0,
140
+ startPosX: 0,
141
+ startPosY: 0,
142
+ });
143
+ const prevZIndexRef = useRef(zIndex);
144
+
145
+ // Sync with external props
146
+ useEffect(() => {
147
+ setIsLocked(locked);
148
+ }, [locked]);
149
+
150
+ // Sync position when initialPosition changes (e.g., from tidy operation)
151
+ useEffect(() => {
152
+ setPosition(initialPosition);
153
+ }, [initialPosition.x, initialPosition.y]);
154
+
155
+ useEffect(() => {
156
+ if (typeof initialSize.width === 'number') {
157
+ setSize(prev => ({ ...prev, width: initialSize.width }));
158
+ }
159
+ if (typeof initialSize.height === 'number') {
160
+ setSize(prev => ({ ...prev, height: initialSize.height as number }));
161
+ }
162
+ }, [initialSize.width, initialSize.height]);
163
+
164
+ // Focus animation on zIndex change
165
+ useEffect(() => {
166
+ const prevZ = prevZIndexRef.current;
167
+ prevZIndexRef.current = zIndex;
168
+ if (zIndex > prevZ) {
169
+ setIsFocusing(true);
170
+ const timer = setTimeout(() => setIsFocusing(false), 250);
171
+ return () => clearTimeout(timer);
172
+ }
173
+ }, [zIndex]);
174
+
175
+ useEffect(() => {
176
+ if (focusTrigger !== undefined && focusTrigger > 0) {
177
+ setIsFocusing(true);
178
+ const timer = setTimeout(() => setIsFocusing(false), 250);
179
+ return () => clearTimeout(timer);
180
+ }
181
+ }, [focusTrigger]);
182
+
183
+ // Bring to front when an iframe inside this app receives focus (click).
184
+ // Iframes swallow mouse events so the outer onClick never fires — detect
185
+ // the focus shift via the window blur event instead.
186
+ useEffect(() => {
187
+ const handleWindowBlur = () => {
188
+ setTimeout(() => {
189
+ const active = document.activeElement;
190
+ if (active?.tagName === 'IFRAME' && appRef.current?.contains(active)) {
191
+ onBringToFront?.(id);
192
+ }
193
+ }, 0);
194
+ };
195
+
196
+ window.addEventListener('blur', handleWindowBlur);
197
+ return () => window.removeEventListener('blur', handleWindowBlur);
198
+ }, [id, onBringToFront]);
199
+
200
+ const statusColors = {
201
+ normal: 'border-[var(--color-border-muted,#e5e5e5)]',
202
+ alert: 'border-[var(--color-warning,#ff4d00)]',
203
+ success: 'border-[var(--color-accent,#ccff00)]',
204
+ };
205
+
206
+ // Commit changes (called on mouse up, click, or escape)
207
+ const commitChanges = useCallback(() => {
208
+ if (!appRef.current) return;
209
+
210
+ const rect = appRef.current.getBoundingClientRect();
211
+ const parent = appRef.current.offsetParent as HTMLElement;
212
+ const parentRect = parent?.getBoundingClientRect() || { left: 0, top: 0 };
213
+
214
+ const newX = rect.left - parentRect.left;
215
+ const newY = rect.top - parentRect.top;
216
+ const newWidth = rect.width;
217
+ const newHeight = rect.height;
218
+
219
+ setPosition({ x: newX, y: newY });
220
+ setSize({ width: newWidth, height: newHeight });
221
+
222
+ if (isDragging || isResizing) {
223
+ onPositionChange?.(id, { x: newX, y: newY });
224
+ }
225
+ if (isResizing) {
226
+ onSizeChange?.(id, { width: newWidth, height: newHeight });
227
+ }
228
+
229
+ setIsDragging(false);
230
+ setIsResizing(false);
231
+ }, [id, isDragging, isResizing, onPositionChange, onSizeChange]);
232
+
233
+ // Drag start
234
+ const handleDragStart = (e: React.MouseEvent) => {
235
+ if ((e.target as HTMLElement).closest('button') || (e.target as HTMLElement).closest('input')) {
236
+ return;
237
+ }
238
+ onBringToFront?.(id);
239
+ if (isLocked) return;
240
+
241
+ e.preventDefault();
242
+ setIsDragging(true);
243
+
244
+ dragRef.current = {
245
+ startX: e.clientX,
246
+ startY: e.clientY,
247
+ startPosX: position.x,
248
+ startPosY: position.y,
249
+ };
250
+ };
251
+
252
+ // Resize start
253
+ const handleResizeStart = useCallback((e: React.MouseEvent, direction: ResizeDirection) => {
254
+ if (isLocked || !resizable) return;
255
+ e.preventDefault();
256
+ e.stopPropagation();
257
+ onBringToFront?.(id);
258
+ setIsResizing(true);
259
+
260
+ resizeRef.current = {
261
+ direction,
262
+ startX: e.clientX,
263
+ startY: e.clientY,
264
+ startWidth: size.width,
265
+ startHeight: size.height,
266
+ startPosX: position.x,
267
+ startPosY: position.y,
268
+ };
269
+ }, [isLocked, resizable, size, position, id, onBringToFront]);
270
+
271
+ const handleLockToggle = () => {
272
+ const newLocked = !isLocked;
273
+ setIsLocked(newLocked);
274
+ onLockChange?.(id, newLocked);
275
+ };
276
+
277
+ // Click anywhere to bring to front
278
+ const handleAppClick = (e: React.MouseEvent) => {
279
+ if ((e.target as HTMLElement).closest('button') ||
280
+ (e.target as HTMLElement).closest('input') ||
281
+ (e.target as HTMLElement).closest('a') ||
282
+ (e.target as HTMLElement).closest('textarea') ||
283
+ (e.target as HTMLElement).closest('select')) {
284
+ return;
285
+ }
286
+
287
+ // If we're dragging or resizing, commit on click
288
+ if (isDragging || isResizing) {
289
+ commitChanges();
290
+ return;
291
+ }
292
+
293
+ onBringToFront?.(id);
294
+ };
295
+
296
+ // Mouse move/up effects - use direct DOM manipulation for smoothness
297
+ useEffect(() => {
298
+ if (!isDragging && !isResizing) return;
299
+
300
+ const handleMouseMove = (e: MouseEvent) => {
301
+ if (!appRef.current) return;
302
+
303
+ if (isDragging) {
304
+ const deltaX = e.clientX - dragRef.current.startX;
305
+ const deltaY = e.clientY - dragRef.current.startY;
306
+ const newX = Math.max(0, dragRef.current.startPosX + deltaX);
307
+ const newY = Math.max(0, dragRef.current.startPosY + deltaY);
308
+
309
+ appRef.current.style.left = `${newX}px`;
310
+ appRef.current.style.top = `${newY}px`;
311
+ } else if (isResizing && resizeRef.current.direction) {
312
+ const deltaX = e.clientX - resizeRef.current.startX;
313
+ const deltaY = e.clientY - resizeRef.current.startY;
314
+ const dir = resizeRef.current.direction;
315
+
316
+ let newWidth = resizeRef.current.startWidth;
317
+ let newHeight = resizeRef.current.startHeight;
318
+ let newX = resizeRef.current.startPosX;
319
+ let newY = resizeRef.current.startPosY;
320
+
321
+ // Horizontal
322
+ if (dir.includes('e')) {
323
+ newWidth = Math.min(maxWidth, Math.max(minWidth, resizeRef.current.startWidth + deltaX));
324
+ }
325
+ if (dir.includes('w')) {
326
+ const potentialWidth = resizeRef.current.startWidth - deltaX;
327
+ if (potentialWidth >= minWidth && potentialWidth <= maxWidth) {
328
+ newWidth = potentialWidth;
329
+ newX = resizeRef.current.startPosX + deltaX;
330
+ }
331
+ }
332
+
333
+ // Vertical
334
+ if (dir.includes('s')) {
335
+ newHeight = Math.min(maxHeight, Math.max(minHeight, resizeRef.current.startHeight + deltaY));
336
+ }
337
+ if (dir.includes('n')) {
338
+ const potentialHeight = resizeRef.current.startHeight - deltaY;
339
+ if (potentialHeight >= minHeight && potentialHeight <= maxHeight) {
340
+ newHeight = potentialHeight;
341
+ newY = resizeRef.current.startPosY + deltaY;
342
+ }
343
+ }
344
+
345
+ appRef.current.style.width = `${newWidth}px`;
346
+ appRef.current.style.height = `${newHeight}px`;
347
+ appRef.current.style.left = `${Math.max(0, newX)}px`;
348
+ appRef.current.style.top = `${Math.max(0, newY)}px`;
349
+ }
350
+ };
351
+
352
+ const handleMouseUp = () => {
353
+ commitChanges();
354
+ };
355
+
356
+ const handleKeyDown = (e: KeyboardEvent) => {
357
+ if (e.key === 'Escape') {
358
+ commitChanges();
359
+ }
360
+ };
361
+
362
+ document.addEventListener('mousemove', handleMouseMove);
363
+ document.addEventListener('mouseup', handleMouseUp);
364
+ document.addEventListener('keydown', handleKeyDown);
365
+
366
+ return () => {
367
+ document.removeEventListener('mousemove', handleMouseMove);
368
+ document.removeEventListener('mouseup', handleMouseUp);
369
+ document.removeEventListener('keydown', handleKeyDown);
370
+ };
371
+ }, [isDragging, isResizing, commitChanges, minWidth, minHeight, maxWidth, maxHeight]);
372
+
373
+ const resizeHandleClass = "absolute z-20";
374
+
375
+ return (
376
+ <div
377
+ ref={appRef}
378
+ className={`absolute ${className}`}
379
+ onClick={handleAppClick}
380
+ style={{
381
+ left: position.x,
382
+ top: position.y,
383
+ width: size.width,
384
+ height: size.height,
385
+ minWidth,
386
+ minHeight,
387
+ zIndex: isDragging || isResizing ? 1000 : zIndex,
388
+ transform: 'scale(1)',
389
+ transition: 'none',
390
+ transformOrigin: 'top left',
391
+ willChange: isDragging || isResizing ? 'left, top, width, height' : 'auto',
392
+ }}
393
+ >
394
+ <div
395
+ className={`bg-[var(--color-surface,#ffffff)] border ${colorScheme ? colorScheme.accent : (!customColor ? statusColors[status] : '')} relative overflow-hidden h-full flex flex-col`}
396
+ style={{
397
+ borderColor: customColor ? `${customColor}4D` : undefined,
398
+ boxShadow: isDragging || isResizing
399
+ ? '6px 6px 0 rgba(0,0,0,0.1)'
400
+ : '4px 4px 0 rgba(0,0,0,0.05)',
401
+ }}
402
+ >
403
+ {/* Color Band */}
404
+ {(colorScheme || customColor) && (
405
+ <div
406
+ className={`absolute top-0 left-0 right-0 h-1 ${colorScheme ? colorScheme.band : ''}`}
407
+ style={customColor ? { backgroundColor: customColor } : undefined}
408
+ />
409
+ )}
410
+
411
+ {/* Corner Brackets */}
412
+ <div className="absolute top-1 left-1 w-2 h-2 border-l border-t border-[var(--color-border,#d4d4d8)] opacity-50 pointer-events-none" />
413
+ <div className="absolute top-1 right-1 w-2 h-2 border-r border-t border-[var(--color-border,#d4d4d8)] opacity-50 pointer-events-none" />
414
+ <div className="absolute bottom-1 left-1 w-2 h-2 border-l border-b border-[var(--color-border,#d4d4d8)] opacity-50 pointer-events-none" />
415
+ <div className="absolute bottom-1 right-1 w-2 h-2 border-r border-b border-[var(--color-border,#d4d4d8)] opacity-50 pointer-events-none" />
416
+
417
+ {/* Noise Texture */}
418
+ <div className="absolute inset-0 opacity-[0.02] pointer-events-none bg-[radial-gradient(#000_1px,transparent_1px)] bg-[size:3px_3px]" />
419
+
420
+ {/* Resize Handles */}
421
+ {resizable && !isLocked && (
422
+ <>
423
+ {/* Edge handles */}
424
+ <div
425
+ className={`${resizeHandleClass} top-0 left-3 right-3 h-2 cursor-n-resize`}
426
+ onMouseDown={(e) => handleResizeStart(e, 'n')}
427
+ />
428
+ <div
429
+ className={`${resizeHandleClass} bottom-0 left-3 right-3 h-2 cursor-s-resize`}
430
+ onMouseDown={(e) => handleResizeStart(e, 's')}
431
+ />
432
+ <div
433
+ className={`${resizeHandleClass} left-0 top-3 bottom-3 w-2 cursor-w-resize`}
434
+ onMouseDown={(e) => handleResizeStart(e, 'w')}
435
+ />
436
+ <div
437
+ className={`${resizeHandleClass} right-0 top-3 bottom-3 w-2 cursor-e-resize`}
438
+ onMouseDown={(e) => handleResizeStart(e, 'e')}
439
+ />
440
+
441
+ {/* Corner handles */}
442
+ <div
443
+ className={`${resizeHandleClass} top-0 left-0 w-3 h-3 cursor-nw-resize`}
444
+ onMouseDown={(e) => handleResizeStart(e, 'nw')}
445
+ />
446
+ <div
447
+ className={`${resizeHandleClass} top-0 right-0 w-3 h-3 cursor-ne-resize`}
448
+ onMouseDown={(e) => handleResizeStart(e, 'ne')}
449
+ />
450
+ <div
451
+ className={`${resizeHandleClass} bottom-0 left-0 w-3 h-3 cursor-sw-resize`}
452
+ onMouseDown={(e) => handleResizeStart(e, 'sw')}
453
+ />
454
+ <div
455
+ className={`${resizeHandleClass} bottom-0 right-0 w-5 h-5 cursor-se-resize group/resize`}
456
+ onMouseDown={(e) => handleResizeStart(e, 'se')}
457
+ >
458
+ {/* Visual resize grip */}
459
+ <svg
460
+ className="absolute bottom-1 right-1 w-2.5 h-2.5 text-[var(--color-border,#d4d4d8)] group-hover/resize:text-[var(--color-text-muted,#6b7280)] transition-colors"
461
+ viewBox="0 0 10 10"
462
+ fill="currentColor"
463
+ >
464
+ <circle cx="8.5" cy="1.5" r="1" />
465
+ <circle cx="8.5" cy="5" r="1" />
466
+ <circle cx="8.5" cy="8.5" r="1" />
467
+ <circle cx="5" cy="5" r="1" />
468
+ <circle cx="5" cy="8.5" r="1" />
469
+ <circle cx="1.5" cy="8.5" r="1" />
470
+ </svg>
471
+ </div>
472
+ </>
473
+ )}
474
+
475
+ {/* Header - Draggable */}
476
+ <div
477
+ className={`px-3 py-2 border-b border-[var(--color-border-muted,#e5e5e5)] flex items-center justify-between relative z-10 shrink-0 ${(colorScheme || customColor) ? 'pt-3' : ''} ${
478
+ isLocked ? 'cursor-default' : isDragging ? 'cursor-grabbing' : 'cursor-grab'
479
+ }`}
480
+ onMouseDown={handleDragStart}
481
+ >
482
+ <div className="flex items-center gap-2 min-w-0 flex-1">
483
+ {!isLocked && <GripVertical size={10} className="text-[var(--color-text-faint,#9ca3af)] shrink-0" />}
484
+ {isLocked && <Lock size={10} className="text-[var(--color-text-faint,#9ca3af)] shrink-0" />}
485
+ {Icon && (
486
+ <Icon
487
+ size={12}
488
+ className={`shrink-0 ${colorScheme ? colorScheme.text : 'text-[var(--color-text-muted,#6b7280)]'}`}
489
+ style={customColor ? { color: customColor } : undefined}
490
+ />
491
+ )}
492
+ <span
493
+ className={`font-mono text-[10px] font-bold tracking-widest uppercase select-none shrink-0 ${colorScheme ? colorScheme.text : 'text-[var(--color-text,#0a0a0a)]'}`}
494
+ style={customColor ? { color: customColor } : undefined}
495
+ >
496
+ {title}
497
+ </span>
498
+ {subtitle && (
499
+ <>
500
+ <span className="text-[var(--color-text-faint,#9ca3af)] shrink-0">·</span>
501
+ <span className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)] truncate">
502
+ {subtitle}
503
+ </span>
504
+ {subtitleLink && (
505
+ <a
506
+ href={subtitleLink}
507
+ target="_blank"
508
+ rel="noopener noreferrer"
509
+ className="p-0.5 hover:bg-[var(--color-surface-alt,#fafafa)] rounded transition-colors shrink-0"
510
+ title="Open in new tab"
511
+ onClick={(e) => e.stopPropagation()}
512
+ onMouseDown={(e) => e.stopPropagation()}
513
+ >
514
+ <ExternalLink size={9} className="text-[var(--color-text-muted,#6b7280)]" />
515
+ </a>
516
+ )}
517
+ </>
518
+ )}
519
+ </div>
520
+ <div className="flex items-center gap-1">
521
+ {onRefresh && (
522
+ <button
523
+ onClick={onRefresh}
524
+ className="p-1 hover:bg-[var(--color-surface-alt,#fafafa)] transition-colors"
525
+ title="Refresh app"
526
+ >
527
+ <RotateCcw size={10} className="text-[var(--color-text-muted,#6b7280)]" />
528
+ </button>
529
+ )}
530
+ <button
531
+ onClick={handleLockToggle}
532
+ className="p-1 hover:bg-[var(--color-surface-alt,#fafafa)] transition-colors"
533
+ title={isLocked ? 'Unlock app' : 'Lock app'}
534
+ >
535
+ {isLocked ? (
536
+ <Lock size={10} className="text-[var(--color-text,#0a0a0a)]" />
537
+ ) : (
538
+ <Unlock size={10} className="text-[var(--color-text-muted,#6b7280)]" />
539
+ )}
540
+ </button>
541
+ {dismissable && (
542
+ <button
543
+ onClick={onDismiss}
544
+ className="p-1 hover:bg-[var(--color-surface-alt,#fafafa)] transition-colors"
545
+ >
546
+ <X size={10} className="text-[var(--color-text-muted,#6b7280)]" />
547
+ </button>
548
+ )}
549
+ </div>
550
+ </div>
551
+
552
+ {/* Content */}
553
+ <div
554
+ className="p-3 relative z-10 flex-1 min-h-0 overflow-auto"
555
+ style={{ pointerEvents: isDragging || isResizing ? 'none' : 'auto' }}
556
+ >
557
+ {children}
558
+ </div>
559
+ </div>
560
+ </div>
561
+ );
562
+ };
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { AlertTriangle } from 'lucide-react';
5
+
6
+ interface IFrameAppProps {
7
+ config?: {
8
+ url?: string;
9
+ title?: string;
10
+ };
11
+ }
12
+
13
+ /**
14
+ * SECURITY: Iframe sandbox is hardcoded and NOT configurable by agents.
15
+ * Using "allow-scripts allow-forms" WITHOUT allow-same-origin prevents the
16
+ * embedded page from accessing the parent's cookies, localStorage, sessionStorage,
17
+ * or making same-origin requests to the wallet server.
18
+ *
19
+ * allow-same-origin was intentionally removed -- combining allow-scripts + allow-same-origin
20
+ * effectively negates the sandbox since the iframe can then access and modify the parent.
21
+ */
22
+ const IFRAME_SANDBOX = 'allow-scripts allow-forms';
23
+
24
+ export default function IFrameApp({ config }: IFrameAppProps) {
25
+ const url = config?.url;
26
+ const title = config?.title || 'Embedded Content';
27
+
28
+ if (!url) {
29
+ return (
30
+ <div className="flex flex-col items-center justify-center h-full min-h-[200px] text-[var(--color-text-muted,#6b7280)]">
31
+ <AlertTriangle size={24} className="mb-2" />
32
+ <span className="font-mono text-xs">NO URL CONFIGURED</span>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ // Validate URL - only allow http(s) schemes
38
+ try {
39
+ const parsed = new URL(url);
40
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
41
+ return (
42
+ <div className="flex flex-col items-center justify-center h-full min-h-[200px] text-[var(--color-error,#ff4d00)]">
43
+ <AlertTriangle size={24} className="mb-2" />
44
+ <span className="font-mono text-xs">BLOCKED URL SCHEME</span>
45
+ <span className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)] mt-1">
46
+ Only http/https URLs are allowed
47
+ </span>
48
+ </div>
49
+ );
50
+ }
51
+ } catch {
52
+ return (
53
+ <div className="flex flex-col items-center justify-center h-full min-h-[200px] text-[var(--color-error,#ff4d00)]">
54
+ <AlertTriangle size={24} className="mb-2" />
55
+ <span className="font-mono text-xs">INVALID URL</span>
56
+ <span className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)] mt-1 max-w-full truncate px-4">
57
+ {url}
58
+ </span>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <iframe
65
+ src={url}
66
+ title={title}
67
+ sandbox={IFRAME_SANDBOX}
68
+ className="w-full h-full border-0"
69
+ style={{ minHeight: '200px' }}
70
+ loading="lazy"
71
+ />
72
+ );
73
+ }