0xtrails 0.1.3 → 0.1.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 (111) hide show
  1. package/dist/address.d.ts.map +1 -1
  2. package/dist/analytics.d.ts +18 -0
  3. package/dist/analytics.d.ts.map +1 -1
  4. package/dist/apiClient.d.ts +1 -1
  5. package/dist/apiClient.d.ts.map +1 -1
  6. package/dist/{ccip-CWd4g9uZ.js → ccip-dLSEJjCf.js} +55 -55
  7. package/dist/cctpqueue.d.ts +1 -1
  8. package/dist/cctpqueue.d.ts.map +1 -1
  9. package/dist/constants.d.ts +1 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/decoders.d.ts +58 -0
  12. package/dist/decoders.d.ts.map +1 -0
  13. package/dist/ens.d.ts +6 -0
  14. package/dist/ens.d.ts.map +1 -1
  15. package/dist/error.d.ts +7 -0
  16. package/dist/error.d.ts.map +1 -1
  17. package/dist/{index-BTUBzx4R.js → index-BXbaLmtt.js} +25694 -24507
  18. package/dist/index.js +2 -2
  19. package/dist/intents.d.ts +4 -4
  20. package/dist/intents.d.ts.map +1 -1
  21. package/dist/metaTxns.d.ts +1 -1
  22. package/dist/metaTxns.d.ts.map +1 -1
  23. package/dist/mode.d.ts +1 -1
  24. package/dist/mode.d.ts.map +1 -1
  25. package/dist/preconditions.d.ts +1 -1
  26. package/dist/preconditions.d.ts.map +1 -1
  27. package/dist/prepareSend.d.ts +30 -24
  28. package/dist/prepareSend.d.ts.map +1 -1
  29. package/dist/prices.d.ts +1 -1
  30. package/dist/prices.d.ts.map +1 -1
  31. package/dist/proxyCaller.d.ts +0 -1
  32. package/dist/proxyCaller.d.ts.map +1 -1
  33. package/dist/tokenBalances.d.ts +1 -1
  34. package/dist/tokenBalances.d.ts.map +1 -1
  35. package/dist/tokens.d.ts.map +1 -1
  36. package/dist/trails.d.ts +1 -1
  37. package/dist/trails.d.ts.map +1 -1
  38. package/dist/transactions.d.ts +4 -0
  39. package/dist/transactions.d.ts.map +1 -1
  40. package/dist/utils.d.ts +6 -0
  41. package/dist/utils.d.ts.map +1 -1
  42. package/dist/widget/components/DebugScreensDropdown.d.ts.map +1 -1
  43. package/dist/widget/components/ErrorDisplay.d.ts +9 -0
  44. package/dist/widget/components/ErrorDisplay.d.ts.map +1 -0
  45. package/dist/widget/components/FundSendForm.d.ts +1 -1
  46. package/dist/widget/components/FundSendForm.d.ts.map +1 -1
  47. package/dist/widget/components/OriginTransferInformation.d.ts +10 -0
  48. package/dist/widget/components/OriginTransferInformation.d.ts.map +1 -0
  49. package/dist/widget/components/PaySendForm.d.ts +1 -1
  50. package/dist/widget/components/PaySendForm.d.ts.map +1 -1
  51. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  52. package/dist/widget/components/Receipt.d.ts.map +1 -1
  53. package/dist/widget/components/Receive.d.ts +12 -0
  54. package/dist/widget/components/Receive.d.ts.map +1 -0
  55. package/dist/widget/components/Swap.d.ts +5 -1
  56. package/dist/widget/components/Swap.d.ts.map +1 -1
  57. package/dist/widget/components/SwapDisplay.d.ts +9 -0
  58. package/dist/widget/components/SwapDisplay.d.ts.map +1 -0
  59. package/dist/widget/components/TransferPendingVertical.d.ts +2 -0
  60. package/dist/widget/components/TransferPendingVertical.d.ts.map +1 -1
  61. package/dist/widget/components/YellowWarningAnimation.d.ts +2 -0
  62. package/dist/widget/components/YellowWarningAnimation.d.ts.map +1 -0
  63. package/dist/widget/hooks/useDebugScreens.d.ts +22 -0
  64. package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -0
  65. package/dist/widget/hooks/useSendForm.d.ts +7 -3
  66. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  67. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  68. package/dist/widget/index.js +1 -1
  69. package/dist/widget/widget.d.ts.map +1 -1
  70. package/package.json +16 -15
  71. package/src/address.ts +3 -0
  72. package/src/analytics.ts +105 -4
  73. package/src/apiClient.ts +1 -1
  74. package/src/cctpqueue.ts +1 -1
  75. package/src/constants.ts +3 -0
  76. package/src/decoders.ts +310 -0
  77. package/src/ens.ts +16 -1
  78. package/src/error.ts +86 -1
  79. package/src/intents.ts +10 -2
  80. package/src/metaTxns.ts +1 -1
  81. package/src/mode.ts +1 -1
  82. package/src/preconditions.ts +1 -1
  83. package/src/prepareSend.ts +432 -114
  84. package/src/prices.ts +5 -1
  85. package/src/proxyCaller.ts +2 -14
  86. package/src/tokenBalances.ts +21 -17
  87. package/src/tokens.ts +62 -3
  88. package/src/trails.ts +2 -2
  89. package/src/transactions.ts +35 -18
  90. package/src/utils.ts +28 -0
  91. package/src/widget/compiled.css +2 -2
  92. package/src/widget/components/DebugScreensDropdown.tsx +3 -0
  93. package/src/widget/components/ErrorDisplay.tsx +150 -0
  94. package/src/widget/components/FundSendForm.tsx +62 -9
  95. package/src/widget/components/OriginTransferInformation.tsx +59 -0
  96. package/src/widget/components/PaySendForm.tsx +65 -12
  97. package/src/widget/components/QRCodeDeposit.tsx +5 -5
  98. package/src/widget/components/QrCode.tsx +1 -1
  99. package/src/widget/components/QuoteDetails.tsx +93 -25
  100. package/src/widget/components/Receipt.tsx +296 -103
  101. package/src/widget/components/Receive.tsx +146 -0
  102. package/src/widget/components/RecentTokens.tsx +1 -1
  103. package/src/widget/components/Swap.tsx +206 -85
  104. package/src/widget/components/SwapDisplay.tsx +68 -0
  105. package/src/widget/components/TokenSelector.tsx +1 -1
  106. package/src/widget/components/TransferPendingVertical.tsx +162 -112
  107. package/src/widget/components/YellowWarningAnimation.tsx +146 -0
  108. package/src/widget/hooks/useDebugScreens.ts +583 -0
  109. package/src/widget/hooks/useSendForm.ts +46 -4
  110. package/src/widget/hooks/useTokenList.ts +8 -27
  111. package/src/widget/widget.tsx +201 -484
@@ -2,10 +2,13 @@ import { ExternalLink } from "lucide-react"
2
2
  import type React from "react"
3
3
  import { useEffect, useState } from "react"
4
4
  import type { TransactionState } from "../../transactions.js"
5
- import { TokenImage } from "./TokenImage.js"
6
5
  import type { PrepareSendQuote } from "../../prepareSend.js"
7
6
  import { QuoteDetails } from "./QuoteDetails.js"
8
7
  import { formatElapsed } from "../../utils.js"
8
+ import { SwapDisplay } from "./SwapDisplay.js"
9
+ import { OriginTransferInformation } from "./OriginTransferInformation.js"
10
+ import { ScreenHeader } from "./ScreenHeader.js"
11
+ import { ChevronRight } from "lucide-react"
9
12
 
10
13
  // Checkmark icon for completed steps
11
14
  const CheckmarkIcon = () => (
@@ -37,6 +40,23 @@ const XIcon = () => (
37
40
  </svg>
38
41
  )
39
42
 
43
+ const AlertIcon = () => (
44
+ <svg
45
+ className="w-4 h-4 text-white"
46
+ fill="none"
47
+ viewBox="0 0 24 24"
48
+ stroke="currentColor"
49
+ >
50
+ <title>An internal meta tx failure occurred</title>
51
+ <path
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ strokeWidth={2}
55
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
56
+ />
57
+ </svg>
58
+ )
59
+
40
60
  // Spinner icon for active steps
41
61
  const SpinnerIcon = () => (
42
62
  <svg
@@ -65,6 +85,8 @@ interface TransferPendingProps {
65
85
  transactionStates: TransactionState[]
66
86
  quote?: PrepareSendQuote | null
67
87
  timestamp?: number
88
+ mode?: "pay" | "fund" | "earn" | "swap" | "receive"
89
+ onContinue?: () => void
68
90
  }
69
91
 
70
92
  export const TransferPending: React.FC<TransferPendingProps> = ({
@@ -72,6 +94,8 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
72
94
  transactionStates,
73
95
  quote,
74
96
  timestamp,
97
+ mode,
98
+ onContinue,
75
99
  }) => {
76
100
  const [showContent, setShowContent] = useState(false)
77
101
  const [activePendingIndex, setActivePendingIndex] = useState(-1)
@@ -80,6 +104,15 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
80
104
  const [showTimeoutWarning, setShowTimeoutWarning] = useState(false)
81
105
  const [activePendingTimer, setActivePendingTimer] = useState(0)
82
106
 
107
+ const showContinueButton =
108
+ (transactionStates?.some((tx) => tx.state === "failed") ||
109
+ transactionStates?.some((tx) =>
110
+ tx?.decodedGuestModuleEvents?.some(
111
+ (event) => event.type === "CallFailed",
112
+ ),
113
+ )) &&
114
+ onContinue
115
+
83
116
  useEffect(() => {
84
117
  const timeoutTimer = setTimeout(() => {
85
118
  setShowTimeoutWarning(true)
@@ -200,30 +233,6 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
200
233
  onElapsedTime(elapsedSeconds)
201
234
  }, [onElapsedTime, elapsedSeconds])
202
235
 
203
- // Calculate time difference
204
- const getTimeAgo = () => {
205
- if (!timestamp) return "Sent just now"
206
-
207
- const now = Date.now()
208
- const diffInSeconds = Math.floor((now - timestamp) / 1000)
209
-
210
- if (diffInSeconds < 1) return "Sent just now"
211
- if (diffInSeconds === 1) return "Sent 1 second ago"
212
- if (diffInSeconds < 60) return `Sent ${diffInSeconds} seconds ago`
213
-
214
- const diffInMinutes = Math.floor(diffInSeconds / 60)
215
- if (diffInMinutes === 1) return "Sent 1 minute ago"
216
- if (diffInMinutes < 60) return `Sent ${diffInMinutes} minutes ago`
217
-
218
- const diffInHours = Math.floor(diffInMinutes / 60)
219
- if (diffInHours === 1) return "Sent 1 hour ago"
220
- if (diffInHours < 24) return `Sent ${diffInHours} hours ago`
221
-
222
- const diffInDays = Math.floor(diffInHours / 24)
223
- if (diffInDays === 1) return "Sent 1 day ago"
224
- return `Sent ${diffInDays} days ago`
225
- }
226
-
227
236
  // Compute effective states: a tx is only confirmed if all previous are confirmed, else pending
228
237
  const effectiveStates: TransactionState[] = []
229
238
  let foundUnconfirmed = false
@@ -244,13 +253,27 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
244
253
  const isCompleted = tx.state === "confirmed"
245
254
  const isFailed = tx.state === "failed"
246
255
 
256
+ // Check if this transaction has meta tx errors
257
+ const hasMetaTxError = tx?.decodedGuestModuleEvents?.some(
258
+ (event) => event.type === "CallFailed",
259
+ )
260
+
261
+ // Check if any previous transaction has failed (to disable spinners after failure)
262
+ const hasPreviousFailure = transactionStates
263
+ .slice(0, index)
264
+ .some((prevTx) =>
265
+ prevTx?.decodedGuestModuleEvents?.some(
266
+ (event) => event.type === "CallFailed",
267
+ ),
268
+ )
269
+
247
270
  // Determine step state
248
271
  let stepState: "completed" | "active" | "pending" | "failed"
249
272
  if (isFailed) {
250
273
  stepState = "failed"
251
274
  } else if (isCompleted) {
252
275
  stepState = "completed"
253
- } else if (isActivePending) {
276
+ } else if (isActivePending && !hasPreviousFailure) {
254
277
  stepState = "active"
255
278
  } else {
256
279
  stepState = "pending"
@@ -263,6 +286,9 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
263
286
 
264
287
  switch (stepState) {
265
288
  case "completed":
289
+ if (hasMetaTxError) {
290
+ return `${baseStyles} bg-orange-500 scale-100`
291
+ }
266
292
  return `${baseStyles} bg-green-600 dark:bg-green-500 scale-100`
267
293
  case "active":
268
294
  return `${baseStyles} bg-blue-600 dark:bg-blue-500 scale-110`
@@ -287,7 +313,8 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
287
313
  {/* Circle with state indicator */}
288
314
  <div className="flex-shrink-0">
289
315
  <div className={getCircleStyles()}>
290
- {stepState === "completed" && <CheckmarkIcon />}
316
+ {stepState === "completed" && !hasMetaTxError && <CheckmarkIcon />}
317
+ {stepState === "completed" && hasMetaTxError && <AlertIcon />}
291
318
  {stepState === "failed" && <XIcon />}
292
319
  {stepState === "active" && <SpinnerIcon />}
293
320
  </div>
@@ -331,99 +358,38 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
331
358
  }
332
359
 
333
360
  return (
334
- <div className="space-y-8 flex flex-col items-center justify-center py-8">
335
- <h2
336
- className={`text-2xl font-bold transition-all duration-500 ease-out ${
337
- showContent ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
338
- } text-gray-900 dark:text-white`}
339
- >
340
- Transaction Status
341
- </h2>
342
-
343
- {/* Transfer Information */}
361
+ <div className="space-y-2 flex flex-col items-center justify-center py-8">
344
362
  <div
345
- className={`w-full max-w-sm p-3 trails-border-radius-container transition-all duration-500 ease-out delay-100 ${
363
+ className={`w-full max-w-sm transition-all duration-500 ease-out ${
346
364
  showContent ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
347
- } trails-bg-secondary trails-text-secondary`}
365
+ }`}
348
366
  >
349
- <div className="flex items-start justify-between">
350
- {/* Left side - Chain and Token images with token name */}
351
- <div className="flex items-start space-x-2">
352
- {/* Token Image and Name */}
353
- <div className="flex items-center space-x-2">
354
- <div style={{ width: "24px", height: "24px" }}>
355
- <TokenImage
356
- imageUrl={quote?.originToken.imageUrl}
357
- symbol={quote?.originToken.symbol}
358
- chainId={quote?.originChain.id}
359
- size={24}
360
- />
361
- </div>
362
- <div className="flex flex-col">
363
- <span className="text-xs font-medium text-left text-gray-900 dark:text-white">
364
- {quote?.originToken.name}
365
- </span>
366
- <span className="text-xs text-gray-500 dark:text-gray-400 flex items-center">
367
- {getTimeAgo()}
368
- <span
369
- className="ml-1 font-mono animate-pulse text-gray-500 dark:text-gray-400"
370
- title="Elapsed time"
371
- >
372
- {formatElapsed(elapsedSeconds)}
373
- </span>
374
- </span>
375
- </div>
376
- </div>
377
- </div>
378
-
379
- {/* Right side - USD value and amount */}
380
- <div className="text-right">
381
- <div className="text-xs font-medium text-right text-gray-900 dark:text-white">
382
- {quote?.originAmountUsdDisplay}
383
- </div>
384
- <div className="text-xs text-gray-600 dark:text-gray-400">
385
- {quote?.originAmountFormatted} {quote?.originToken.symbol}
386
- </div>
387
- </div>
388
- </div>
367
+ <ScreenHeader
368
+ headerContent={
369
+ mode === "swap" ? "Swap in progress" : "Transaction Status"
370
+ }
371
+ headerContentAlign="center"
372
+ />
389
373
  </div>
390
374
 
391
- {/* Timeout Warning */}
392
- {showTimeoutWarning && (
375
+ {/* Transfer Information Display */}
376
+ {quote && (
393
377
  <div
394
- className={`w-full max-w-sm transition-all duration-500 ease-out delay-150 ${
378
+ className={`w-full max-w-sm transition-all duration-500 ease-out delay-100 ${
395
379
  showContent
396
380
  ? "opacity-100 translate-y-0"
397
381
  : "opacity-0 translate-y-4"
398
382
  }`}
399
383
  >
400
- <div className="p-4 trails-border-radius-container text-sm bg-yellow-50 border border-solid border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-700/50">
401
- <div className="flex items-start space-x-3">
402
- <svg
403
- className="w-5 h-5 mt-0.5 flex-shrink-0 text-yellow-600 dark:text-yellow-400"
404
- fill="none"
405
- viewBox="0 0 24 24"
406
- stroke="currentColor"
407
- >
408
- <title>Warning</title>
409
- <path
410
- strokeLinecap="round"
411
- strokeLinejoin="round"
412
- strokeWidth={2}
413
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
414
- />
415
- </svg>
416
- <div>
417
- <p className="font-medium text-yellow-800 dark:text-yellow-300">
418
- Transaction taking longer than expected
419
- </p>
420
- <p className="mt-1 text-xs text-yellow-700 dark:text-yellow-400">
421
- This transaction is taking longer than expected. Please reach
422
- out to support for help if the issue persists.
423
- </p>
424
- </div>
425
- </div>
426
- </div>
384
+ {mode === "swap" ? (
385
+ <SwapDisplay quote={quote} elapsedSeconds={elapsedSeconds} />
386
+ ) : (
387
+ <OriginTransferInformation
388
+ quote={quote}
389
+ elapsedSeconds={elapsedSeconds}
390
+ timestamp={timestamp}
391
+ />
392
+ )}
427
393
  </div>
428
394
  )}
429
395
 
@@ -435,19 +401,44 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
435
401
  className={`absolute left-2.5 top-5 bottom-5 w-0.5 transition-all duration-500 ease-out bg-gray-300 dark:bg-gray-700 ${showLine ? "animate-line-grow" : "h-0"}`}
436
402
  />
437
403
 
438
- {/* Progress line for completed steps */}
404
+ {/* Progress line for completed steps (excluding those with meta tx errors) */}
439
405
  {activePendingIndex > 0 && showLine && (
440
406
  <div
441
407
  className="absolute left-2.5 top-5 w-0.5 bg-green-500 transition-all duration-1000 ease-out"
442
408
  style={{
443
- height: `${(activePendingIndex / (transactionStates.length - 1)) * 80}%`,
409
+ height: `${(() => {
410
+ // Find the last step that was completed without meta tx errors
411
+ let lastGoodStepIndex = -1
412
+ for (let i = 0; i < transactionStates.length; i++) {
413
+ const tx = transactionStates[i]
414
+ if (
415
+ tx &&
416
+ tx.state === "confirmed" &&
417
+ !tx?.decodedGuestModuleEvents?.some(
418
+ (event) => event.type === "CallFailed",
419
+ )
420
+ ) {
421
+ lastGoodStepIndex = i
422
+ } else if (
423
+ tx?.decodedGuestModuleEvents?.some(
424
+ (event) => event.type === "CallFailed",
425
+ )
426
+ ) {
427
+ // Stop at first failure
428
+ break
429
+ }
430
+ }
431
+ return lastGoodStepIndex >= 0
432
+ ? (lastGoodStepIndex / (transactionStates.length - 1)) * 80
433
+ : 0
434
+ })()}%`,
444
435
  maxHeight: "calc(100% - 20px)",
445
436
  }}
446
437
  />
447
438
  )}
448
439
 
449
440
  {/* Steps */}
450
- <div className="space-y-6">
441
+ <div className="space-y-6 mt-4">
451
442
  {effectiveStates.map((tx, index) => (
452
443
  <div
453
444
  key={`${tx.transactionHash}-${index}`}
@@ -465,6 +456,26 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
465
456
  </div>
466
457
  </div>
467
458
 
459
+ {/* Continue Button - Show only when there are failures */}
460
+ {showContinueButton && (
461
+ <div
462
+ className={`w-full max-w-sm transition-all duration-500 ease-out delay-150 mt-4 flex justify-center ${
463
+ showContent
464
+ ? "opacity-100 translate-y-0"
465
+ : "opacity-0 translate-y-4"
466
+ }`}
467
+ >
468
+ <button
469
+ type="button"
470
+ onClick={onContinue}
471
+ className="inline-flex items-center gap-1 px-4 py-2 trails-border-radius-button font-medium transition-colors cursor-pointer bg-blue-500 hover:bg-blue-600 text-white dark:bg-blue-600 dark:hover:bg-blue-700"
472
+ >
473
+ Continue
474
+ <ChevronRight className="w-4 h-4" />
475
+ </button>
476
+ </div>
477
+ )}
478
+
468
479
  {/* Details Section */}
469
480
  <div
470
481
  className={`w-full max-w-sm transition-all duration-500 ease-out delay-200 ${
@@ -475,6 +486,45 @@ export const TransferPending: React.FC<TransferPendingProps> = ({
475
486
  <QuoteDetails quote={quote} showContent={true} />
476
487
  </div>
477
488
  </div>
489
+
490
+ {/* Timeout Warning */}
491
+ {showTimeoutWarning && !showContinueButton && (
492
+ <div
493
+ className={`w-full max-w-sm transition-all duration-500 ease-out delay-250 ${
494
+ showContent
495
+ ? "opacity-100 translate-y-0"
496
+ : "opacity-0 translate-y-4"
497
+ }`}
498
+ >
499
+ <div className="p-4 trails-border-radius-container text-sm bg-yellow-50 border border-solid border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-700/50">
500
+ <div className="flex items-start space-x-3">
501
+ <svg
502
+ className="w-5 h-5 mt-0.5 flex-shrink-0 text-yellow-600 dark:text-yellow-400"
503
+ fill="none"
504
+ viewBox="0 0 24 24"
505
+ stroke="currentColor"
506
+ >
507
+ <title>Warning</title>
508
+ <path
509
+ strokeLinecap="round"
510
+ strokeLinejoin="round"
511
+ strokeWidth={2}
512
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
513
+ />
514
+ </svg>
515
+ <div>
516
+ <p className="font-medium text-yellow-800 dark:text-yellow-300">
517
+ Transaction taking longer than expected
518
+ </p>
519
+ <p className="mt-1 text-xs text-yellow-700 dark:text-yellow-400">
520
+ This transaction is taking longer than expected. Please reach
521
+ out to support for help if the issue persists.
522
+ </p>
523
+ </div>
524
+ </div>
525
+ </div>
526
+ </div>
527
+ )}
478
528
  </div>
479
529
  )
480
530
  }
@@ -0,0 +1,146 @@
1
+ import { type CSSProperties, useEffect, useState } from "react"
2
+
3
+ export function YellowWarningAnimation() {
4
+ const [animate, setAnimate] = useState(false)
5
+
6
+ useEffect(() => {
7
+ const timer = setTimeout(() => {
8
+ setAnimate(true)
9
+ }, 100)
10
+
11
+ return () => clearTimeout(timer)
12
+ }, [])
13
+
14
+ const styles: Record<string, CSSProperties> = {
15
+ container: {
16
+ width: "80px",
17
+ height: "80px",
18
+ position: "relative" as const,
19
+ },
20
+ circleContainer: {
21
+ position: "relative" as const,
22
+ width: "100%",
23
+ height: "100%",
24
+ },
25
+ circleFill: {
26
+ position: "absolute" as const,
27
+ top: 0,
28
+ left: 0,
29
+ width: "100%",
30
+ height: "100%",
31
+ borderRadius: "50%",
32
+ backgroundColor: "#f59e0b",
33
+ transform: animate ? "scale(1)" : "scale(0)",
34
+ opacity: animate ? 1 : 0,
35
+ transition: "transform 0.15s ease-out 0.3s, opacity 0.15s ease-out 0.3s",
36
+ },
37
+ progressRing: {
38
+ position: "absolute" as const,
39
+ top: 0,
40
+ left: 0,
41
+ width: "100%",
42
+ height: "100%",
43
+ transform: "rotate(-90deg)",
44
+ },
45
+ circle: {
46
+ fill: "none",
47
+ stroke: "url(#yellowGradient)",
48
+ strokeWidth: 4,
49
+ strokeLinecap: "round" as const,
50
+ strokeDasharray: 226,
51
+ strokeDashoffset: animate ? 0 : 226,
52
+ transition: "stroke-dashoffset 0.3s ease-out",
53
+ },
54
+ iconContainer: {
55
+ position: "absolute" as const,
56
+ top: "50%",
57
+ left: "50%",
58
+ transform: animate
59
+ ? "translate(-50%, -50%) scale(1)"
60
+ : "translate(-50%, -50%) scale(0)",
61
+ opacity: animate ? 1 : 0,
62
+ zIndex: 10,
63
+ transition: "transform 0.1s ease-out 0.4s, opacity 0.1s ease-out 0.4s",
64
+ },
65
+ trianglePath: {
66
+ fill: "none",
67
+ stroke: "white",
68
+ strokeWidth: 2.5,
69
+ strokeLinecap: "round" as const,
70
+ strokeLinejoin: "round" as const,
71
+ // Approx perimeter; animate drawing
72
+ strokeDasharray: 120,
73
+ strokeDashoffset: animate ? 0 : 120,
74
+ transition: "stroke-dashoffset 0.2s ease-out 0.45s",
75
+ },
76
+ exclamationPath: {
77
+ fill: "none",
78
+ stroke: "white",
79
+ strokeWidth: 2.5,
80
+ strokeLinecap: "round" as const,
81
+ strokeLinejoin: "round" as const,
82
+ strokeDasharray: 20,
83
+ strokeDashoffset: animate ? 0 : 20,
84
+ transition: "stroke-dashoffset 0.15s ease-out 0.55s",
85
+ },
86
+ dot: {
87
+ fill: "white",
88
+ opacity: animate ? 1 : 0,
89
+ transition: "opacity 0.1s ease-out 0.65s",
90
+ },
91
+ }
92
+
93
+ return (
94
+ <div style={styles.container}>
95
+ <div style={styles.circleContainer}>
96
+ {/* Circle fill */}
97
+ <div style={styles.circleFill} />
98
+
99
+ {/* Progress ring */}
100
+ <svg style={styles.progressRing}>
101
+ <defs>
102
+ <radialGradient
103
+ id="yellowGradient"
104
+ cx="50%"
105
+ cy="50%"
106
+ r="50%"
107
+ gradientUnits="objectBoundingBox"
108
+ >
109
+ <stop
110
+ offset="0%"
111
+ style={{ stopColor: "#f59e0b", stopOpacity: 0 }}
112
+ />
113
+ <stop
114
+ offset="70%"
115
+ style={{ stopColor: "#f59e0b", stopOpacity: 0.3 }}
116
+ />
117
+ <stop
118
+ offset="100%"
119
+ style={{ stopColor: "#f59e0b", stopOpacity: 1 }}
120
+ />
121
+ </radialGradient>
122
+ </defs>
123
+ <circle cx="40" cy="40" r="36" style={styles.circle} />
124
+ </svg>
125
+
126
+ {/* Warning icon */}
127
+ <div style={styles.iconContainer}>
128
+ <svg
129
+ width="48"
130
+ height="48"
131
+ viewBox="0 0 24 24"
132
+ role="img"
133
+ aria-labelledby="warning-icon-title"
134
+ >
135
+ <title id="warning-icon-title">Warning icon</title>
136
+ {/* Triangle outline */}
137
+ <path d="M12 3 L22 20 H2 Z" style={styles.trianglePath} />
138
+ {/* Exclamation mark */}
139
+ <path d="M12 9 L12 14" style={styles.exclamationPath} />
140
+ <circle cx="12" cy="17" r="1.5" style={styles.dot} />
141
+ </svg>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ )
146
+ }