@shapeshiftoss/swap-widget 0.0.2

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 (157) hide show
  1. package/Dockerfile +87 -0
  2. package/README.md +519 -0
  3. package/dist/assets/PhArrowCircleDown-B305t-Sh.js +14 -0
  4. package/dist/assets/PhArrowClockwise-CJPxQhr_.js +14 -0
  5. package/dist/assets/PhArrowDown-C4hT9BVu.js +14 -0
  6. package/dist/assets/PhArrowLeft-Way4Cu-r.js +14 -0
  7. package/dist/assets/PhArrowRight-Cp5ZME0F.js +14 -0
  8. package/dist/assets/PhArrowSquareOut-NnrK69c5.js +14 -0
  9. package/dist/assets/PhArrowUp-Dph6j8rm.js +14 -0
  10. package/dist/assets/PhArrowUpRight-BpJjRRRh.js +14 -0
  11. package/dist/assets/PhArrowsClockwise-CFUT8X7R.js +14 -0
  12. package/dist/assets/PhArrowsDownUp-BjBI4ckC.js +14 -0
  13. package/dist/assets/PhArrowsLeftRight-CuLpbKRk.js +14 -0
  14. package/dist/assets/PhBank-Dchr4XQJ.js +14 -0
  15. package/dist/assets/PhBrowser-dUu2TrC1.js +14 -0
  16. package/dist/assets/PhCaretDown-78IuKU3V.js +14 -0
  17. package/dist/assets/PhCaretLeft-_aiyH7n1.js +14 -0
  18. package/dist/assets/PhCaretRight-8zJQEuDX.js +14 -0
  19. package/dist/assets/PhCaretUp-CiCxKWwN.js +14 -0
  20. package/dist/assets/PhCheck-CcADx8br.js +14 -0
  21. package/dist/assets/PhCircleHalf-BYzSzd2h.js +14 -0
  22. package/dist/assets/PhClock-UItiXeJq.js +14 -0
  23. package/dist/assets/PhCompass-CUy3uA7t.js +14 -0
  24. package/dist/assets/PhCopy-C-PDgSlZ.js +14 -0
  25. package/dist/assets/PhCreditCard-CWRaUT6F.js +14 -0
  26. package/dist/assets/PhCurrencyDollar-Cqla3AXT.js +14 -0
  27. package/dist/assets/PhDesktop-BBIPrYLl.js +14 -0
  28. package/dist/assets/PhDeviceMobile-BdsPaATg.js +14 -0
  29. package/dist/assets/PhDotsThree-BMXCQl-0.js +14 -0
  30. package/dist/assets/PhEnvelope-yUsis6yU.js +14 -0
  31. package/dist/assets/PhFunnelSimple-Ba7BbWTp.js +14 -0
  32. package/dist/assets/PhGlobe-DgLxBk5U.js +14 -0
  33. package/dist/assets/PhIdentificationCard-Czjmq9Z3.js +14 -0
  34. package/dist/assets/PhImage-BAJ_erz6.js +14 -0
  35. package/dist/assets/PhInfo-Cr4DhVUE.js +14 -0
  36. package/dist/assets/PhLightbulb-BX_7duTr.js +14 -0
  37. package/dist/assets/PhMagnifyingGlass-Mygfryvz.js +14 -0
  38. package/dist/assets/PhPaperPlaneRight-BeuCA660.js +14 -0
  39. package/dist/assets/PhPlus-BMjcHjsT.js +14 -0
  40. package/dist/assets/PhPower-CGffGmdb.js +14 -0
  41. package/dist/assets/PhPuzzlePiece-wm_GpVSi.js +14 -0
  42. package/dist/assets/PhQrCode-DRwRzXCH.js +14 -0
  43. package/dist/assets/PhQuestion-M09j2U0T.js +14 -0
  44. package/dist/assets/PhQuestionMark-BP9EMB6h.js +14 -0
  45. package/dist/assets/PhSealCheck-BfG80yKn.js +14 -0
  46. package/dist/assets/PhSignOut-C5YOWlPT.js +14 -0
  47. package/dist/assets/PhSpinner-BXooDYzU.js +14 -0
  48. package/dist/assets/PhTrash-Biq6rmmL.js +14 -0
  49. package/dist/assets/PhUser-CAgZSpGe.js +14 -0
  50. package/dist/assets/PhVault-l2PHF38a.js +14 -0
  51. package/dist/assets/PhWallet-w25D16It.js +14 -0
  52. package/dist/assets/PhWarning-DPEydIPw.js +14 -0
  53. package/dist/assets/PhWarningCircle-BqjT745H.js +14 -0
  54. package/dist/assets/PhX-s-SmVExa.js +14 -0
  55. package/dist/assets/SwapController-If0bFrNc.js +1 -0
  56. package/dist/assets/ccip-BBB7uCdL.js +1 -0
  57. package/dist/assets/ccip-CkkdfKKI.js +1 -0
  58. package/dist/assets/data-capture-D4gAy-Yc.js +242 -0
  59. package/dist/assets/email-B1AJehis.js +90 -0
  60. package/dist/assets/embedded-wallet-Q6DQopDk.js +410 -0
  61. package/dist/assets/features-B6JAPAFb.js +5 -0
  62. package/dist/assets/index-5jd8RPN9.js +7428 -0
  63. package/dist/assets/index-B4kNtSTk.js +1080 -0
  64. package/dist/assets/index-B753eAob.js +1 -0
  65. package/dist/assets/index-BZoLTv77.js +3 -0
  66. package/dist/assets/index-Bcw9UURX.js +3 -0
  67. package/dist/assets/index-BkrV-ahK.js +2 -0
  68. package/dist/assets/index-CRQzQ_up.js +9 -0
  69. package/dist/assets/index-CSW0Gt5y.js +132 -0
  70. package/dist/assets/index-CZ4jTIsk.js +138 -0
  71. package/dist/assets/index-Cwa8SB3h.js +61 -0
  72. package/dist/assets/index-DEwQgT4r.js +78 -0
  73. package/dist/assets/index-DVfQiQ1d.js +1 -0
  74. package/dist/assets/index-cV3UdaMs.css +1 -0
  75. package/dist/assets/index-hAmu622O.js +43 -0
  76. package/dist/assets/onramp-XcPNCLRU.js +422 -0
  77. package/dist/assets/pay-with-exchange-BeHt4OF4.js +280 -0
  78. package/dist/assets/property-C9JG5tag.js +27 -0
  79. package/dist/assets/receive-7ff4he2S.js +131 -0
  80. package/dist/assets/send-CkMBIRXT.js +554 -0
  81. package/dist/assets/socials-DmmxuLrP.js +250 -0
  82. package/dist/assets/swaps-IHAYRQ0x.js +1050 -0
  83. package/dist/assets/transactions-9Cxdk7o9.js +16 -0
  84. package/dist/assets/v4-BOvFkHkt.js +1 -0
  85. package/dist/assets/w3m-modal-kJzFnBfB.js +891 -0
  86. package/dist/assets/wallet-standard-Dv8LMoUB.js +1 -0
  87. package/dist/favicon.png +0 -0
  88. package/dist/favicon.svg +11 -0
  89. package/dist/icon-192x192.png +0 -0
  90. package/dist/icon-512x512.png +0 -0
  91. package/dist/index.html +33 -0
  92. package/index.html +32 -0
  93. package/package.json +59 -0
  94. package/public/favicon.png +0 -0
  95. package/public/favicon.svg +11 -0
  96. package/public/icon-192x192.png +0 -0
  97. package/public/icon-512x512.png +0 -0
  98. package/railway.toml +10 -0
  99. package/src/api/client.ts +95 -0
  100. package/src/components/AddressInputModal.css +173 -0
  101. package/src/components/AddressInputModal.tsx +250 -0
  102. package/src/components/ApprovalStep.tsx +92 -0
  103. package/src/components/ExecutionStep.tsx +27 -0
  104. package/src/components/InputStep.tsx +431 -0
  105. package/src/components/QuoteSelector.css +146 -0
  106. package/src/components/QuoteSelector.tsx +137 -0
  107. package/src/components/QuotesModal.css +250 -0
  108. package/src/components/QuotesModal.tsx +182 -0
  109. package/src/components/SettingsModal.css +145 -0
  110. package/src/components/SettingsModal.tsx +180 -0
  111. package/src/components/StatusStep.tsx +139 -0
  112. package/src/components/SwapWidget.css +713 -0
  113. package/src/components/SwapWidget.tsx +607 -0
  114. package/src/components/TokenSelectModal.css +438 -0
  115. package/src/components/TokenSelectModal.tsx +487 -0
  116. package/src/components/WalletProvider.tsx +92 -0
  117. package/src/config/appkit.ts +104 -0
  118. package/src/config/wagmi.ts +27 -0
  119. package/src/constants/chains.ts +72 -0
  120. package/src/constants/defaults.ts +28 -0
  121. package/src/constants/swappers.ts +48 -0
  122. package/src/constants/viemChains.ts +69 -0
  123. package/src/contexts/SwapWalletContext.tsx +25 -0
  124. package/src/demo/App.css +458 -0
  125. package/src/demo/App.tsx +481 -0
  126. package/src/demo/main.tsx +25 -0
  127. package/src/hooks/useAssets.ts +180 -0
  128. package/src/hooks/useBalances.ts +822 -0
  129. package/src/hooks/useBitcoinSigning.ts +276 -0
  130. package/src/hooks/useMarketData.ts +169 -0
  131. package/src/hooks/useSolanaSigning.ts +275 -0
  132. package/src/hooks/useStatusPolling.ts +137 -0
  133. package/src/hooks/useSwapApproval.ts +106 -0
  134. package/src/hooks/useSwapDisplayValues.ts +157 -0
  135. package/src/hooks/useSwapExecution.ts +199 -0
  136. package/src/hooks/useSwapHandlers.ts +119 -0
  137. package/src/hooks/useSwapQuote.ts +71 -0
  138. package/src/hooks/useSwapQuoting.ts +102 -0
  139. package/src/hooks/useSwapRates.ts +61 -0
  140. package/src/index.ts +43 -0
  141. package/src/machines/SwapMachineContext.ts +5 -0
  142. package/src/machines/__tests__/guards.test.ts +228 -0
  143. package/src/machines/__tests__/setup.test.ts +7 -0
  144. package/src/machines/__tests__/swapMachine.test.ts +544 -0
  145. package/src/machines/__tests__/types.test.ts +124 -0
  146. package/src/machines/guards.ts +33 -0
  147. package/src/machines/swapMachine.ts +288 -0
  148. package/src/machines/types.ts +54 -0
  149. package/src/services/transactionStatus.ts +322 -0
  150. package/src/types/index.ts +361 -0
  151. package/src/utils/addressValidation.ts +201 -0
  152. package/src/utils/redirect.ts +45 -0
  153. package/src/vite-env.d.ts +10 -0
  154. package/tsconfig.json +23 -0
  155. package/tsconfig.node.json +10 -0
  156. package/vite.config.ts +101 -0
  157. package/vitest.config.ts +18 -0
@@ -0,0 +1,250 @@
1
+ .ssw-quotes-modal-backdrop {
2
+ position: fixed;
3
+ inset: 0;
4
+ background: rgba(0, 0, 0, 0.75);
5
+ backdrop-filter: blur(8px);
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ z-index: 1000;
10
+ padding: 20px;
11
+ animation: ssw-fade-in 0.15s ease;
12
+ }
13
+
14
+ @keyframes ssw-fade-in {
15
+ from {
16
+ opacity: 0;
17
+ }
18
+ to {
19
+ opacity: 1;
20
+ }
21
+ }
22
+
23
+ .ssw-quotes-modal {
24
+ width: 100%;
25
+ max-width: 400px;
26
+ max-height: 70vh;
27
+ background: var(--ssw-bg-secondary, #12121c);
28
+ border-radius: var(--ssw-radius-widget);
29
+ border: 1px solid var(--ssw-border, rgba(255, 255, 255, 0.08));
30
+ display: flex;
31
+ flex-direction: column;
32
+ overflow: hidden;
33
+ animation: ssw-slide-up 0.2s ease;
34
+ }
35
+
36
+ @keyframes ssw-slide-up {
37
+ from {
38
+ opacity: 0;
39
+ transform: translateY(10px);
40
+ }
41
+ to {
42
+ opacity: 1;
43
+ transform: translateY(0);
44
+ }
45
+ }
46
+
47
+ .ssw-quotes-modal-header {
48
+ display: flex;
49
+ align-items: flex-start;
50
+ justify-content: space-between;
51
+ padding: 20px 20px 16px;
52
+ flex-shrink: 0;
53
+ }
54
+
55
+ .ssw-quotes-header-content {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 4px;
59
+ }
60
+
61
+ .ssw-quotes-modal-title {
62
+ font-size: 18px;
63
+ font-weight: 600;
64
+ color: var(--ssw-text-primary, #ffffff);
65
+ margin: 0;
66
+ }
67
+
68
+ .ssw-quotes-modal-subtitle {
69
+ font-size: 13px;
70
+ color: var(--ssw-text-secondary, #a0a0b0);
71
+ }
72
+
73
+ .ssw-quotes-modal-close {
74
+ padding: 6px;
75
+ border: none;
76
+ background: none;
77
+ border-radius: var(--ssw-radius-xs);
78
+ color: var(--ssw-text-muted, #6b6b80);
79
+ cursor: pointer;
80
+ transition: all 0.15s ease;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ margin: -2px -6px 0 0;
85
+ }
86
+
87
+ .ssw-quotes-modal-close:hover {
88
+ color: var(--ssw-text-primary, #ffffff);
89
+ background: var(--ssw-bg-hover, rgba(255, 255, 255, 0.05));
90
+ }
91
+
92
+ .ssw-quotes-modal-list {
93
+ flex: 1;
94
+ overflow-y: auto;
95
+ padding: 0 12px 12px;
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 6px;
99
+ scrollbar-width: thin;
100
+ scrollbar-color: var(--ssw-border) transparent;
101
+ }
102
+
103
+ .ssw-quotes-modal-list::-webkit-scrollbar {
104
+ width: 6px;
105
+ }
106
+
107
+ .ssw-quotes-modal-list::-webkit-scrollbar-track {
108
+ background: transparent;
109
+ }
110
+
111
+ .ssw-quotes-modal-list::-webkit-scrollbar-thumb {
112
+ background: var(--ssw-border);
113
+ border-radius: 3px;
114
+ }
115
+
116
+ .ssw-quotes-modal-list::-webkit-scrollbar-thumb:hover {
117
+ background: var(--ssw-border-hover);
118
+ }
119
+
120
+ .ssw-quote-row {
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: space-between;
124
+ padding: 14px 16px;
125
+ border: 1px solid transparent;
126
+ border-radius: var(--ssw-radius-md);
127
+ background: var(--ssw-bg-tertiary, #1a1a2e);
128
+ cursor: pointer;
129
+ transition: all 0.15s ease;
130
+ text-align: left;
131
+ width: 100%;
132
+ gap: 12px;
133
+ }
134
+
135
+ .ssw-quote-row:hover {
136
+ background: var(--ssw-bg-hover, rgba(255, 255, 255, 0.05));
137
+ }
138
+
139
+ .ssw-quote-row.ssw-best {
140
+ border-color: var(--ssw-accent, #3861fb);
141
+ background: color-mix(in srgb, var(--ssw-accent, #3861fb) 6%, transparent);
142
+ }
143
+
144
+ .ssw-quote-row.ssw-best:hover {
145
+ background: color-mix(in srgb, var(--ssw-accent, #3861fb) 12%, transparent);
146
+ }
147
+
148
+ .ssw-quote-row.ssw-selected {
149
+ border-color: var(--ssw-success, #00d395);
150
+ background: rgba(0, 211, 149, 0.06);
151
+ }
152
+
153
+ .ssw-quote-row-left {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 12px;
157
+ min-width: 0;
158
+ }
159
+
160
+ .ssw-quote-row-icon {
161
+ width: 36px;
162
+ height: 36px;
163
+ border-radius: var(--ssw-radius-sm);
164
+ object-fit: contain;
165
+ background: var(--ssw-bg-secondary, #12121c);
166
+ flex-shrink: 0;
167
+ }
168
+
169
+ .ssw-quote-row-icon-placeholder {
170
+ width: 36px;
171
+ height: 36px;
172
+ border-radius: var(--ssw-radius-sm);
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ font-size: 15px;
177
+ font-weight: 600;
178
+ color: white;
179
+ flex-shrink: 0;
180
+ }
181
+
182
+ .ssw-quote-row-info {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 2px;
186
+ min-width: 0;
187
+ }
188
+
189
+ .ssw-quote-row-name-row {
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 8px;
193
+ flex-wrap: wrap;
194
+ }
195
+
196
+ .ssw-quote-row-name {
197
+ font-size: 14px;
198
+ font-weight: 600;
199
+ color: var(--ssw-text-primary, #ffffff);
200
+ }
201
+
202
+ .ssw-quote-row-best {
203
+ padding: 2px 6px;
204
+ border-radius: var(--ssw-radius-2xs);
205
+ background: var(--ssw-accent, #3861fb);
206
+ color: white;
207
+ font-size: 9px;
208
+ font-weight: 700;
209
+ text-transform: uppercase;
210
+ letter-spacing: 0.04em;
211
+ }
212
+
213
+ .ssw-quote-row-diff {
214
+ padding: 2px 6px;
215
+ border-radius: var(--ssw-radius-2xs);
216
+ background: rgba(239, 68, 68, 0.12);
217
+ color: #ef4444;
218
+ font-size: 10px;
219
+ font-weight: 600;
220
+ }
221
+
222
+ .ssw-quote-row-time {
223
+ font-size: 12px;
224
+ color: var(--ssw-text-muted, #6b6b80);
225
+ }
226
+
227
+ .ssw-quote-row-right {
228
+ display: flex;
229
+ flex-direction: column;
230
+ align-items: flex-end;
231
+ gap: 2px;
232
+ flex-shrink: 0;
233
+ }
234
+
235
+ .ssw-quote-row-amount {
236
+ font-size: 15px;
237
+ font-weight: 600;
238
+ color: var(--ssw-text-primary, #ffffff);
239
+ }
240
+
241
+ .ssw-quote-row-symbol {
242
+ font-size: 13px;
243
+ font-weight: 500;
244
+ color: var(--ssw-text-secondary, #a0a0b0);
245
+ }
246
+
247
+ .ssw-quote-row-usd {
248
+ font-size: 12px;
249
+ color: var(--ssw-text-muted, #6b6b80);
250
+ }
@@ -0,0 +1,182 @@
1
+ import './QuotesModal.css'
2
+
3
+ import { bnOrZero } from '@shapeshiftoss/utils'
4
+ import { useCallback, useEffect, useMemo } from 'react'
5
+
6
+ import { getSwapperColor, getSwapperIcon } from '../constants/swappers'
7
+ import { formatUsdValue } from '../hooks/useMarketData'
8
+ import type { Asset, TradeRate } from '../types'
9
+ import { formatAmount } from '../types'
10
+
11
+ const useLockBodyScroll = (isLocked: boolean) => {
12
+ useEffect(() => {
13
+ if (!isLocked) return
14
+ const originalOverflow = document.body.style.overflow
15
+ document.body.style.overflow = 'hidden'
16
+ return () => {
17
+ document.body.style.overflow = originalOverflow
18
+ }
19
+ }, [isLocked])
20
+ }
21
+
22
+ type QuotesModalProps = {
23
+ isOpen: boolean
24
+ onClose: () => void
25
+ rates: TradeRate[]
26
+ selectedRate: TradeRate | null
27
+ onSelectRate: (rate: TradeRate) => void
28
+ buyAsset: Asset
29
+ sellAsset: Asset
30
+ sellAmountBaseUnit: string
31
+ buyAssetUsdPrice?: string
32
+ }
33
+
34
+ const calculateSavingsPercent = (bestAmount: string, currentAmount: string): string | null => {
35
+ const best = bnOrZero(bestAmount)
36
+ const current = bnOrZero(currentAmount)
37
+ if (best.isZero()) return null
38
+ const diff = best.minus(current).div(best).times(100)
39
+ return diff.gt(0.1) ? diff.toFixed(2) : null
40
+ }
41
+
42
+ export const QuotesModal = ({
43
+ isOpen,
44
+ onClose,
45
+ rates,
46
+ selectedRate,
47
+ onSelectRate,
48
+ buyAsset,
49
+ sellAsset,
50
+ sellAmountBaseUnit,
51
+ buyAssetUsdPrice,
52
+ }: QuotesModalProps) => {
53
+ useLockBodyScroll(isOpen)
54
+
55
+ const handleBackdropClick = useCallback(
56
+ (e: React.MouseEvent) => {
57
+ if (e.target === e.currentTarget) {
58
+ onClose()
59
+ }
60
+ },
61
+ [onClose],
62
+ )
63
+
64
+ const handleSelectRate = useCallback(
65
+ (rate: TradeRate) => {
66
+ onSelectRate(rate)
67
+ onClose()
68
+ },
69
+ [onSelectRate, onClose],
70
+ )
71
+
72
+ const sortedRates = useMemo(() => {
73
+ return [...rates]
74
+ .filter(r => !r.error && r.buyAmountCryptoBaseUnit !== '0')
75
+ .sort((a, b) => {
76
+ const aAmount = bnOrZero(a.buyAmountCryptoBaseUnit)
77
+ const bAmount = bnOrZero(b.buyAmountCryptoBaseUnit)
78
+ return bAmount.minus(aAmount).toNumber()
79
+ })
80
+ }, [rates])
81
+
82
+ const bestRate = useMemo(() => sortedRates[0], [sortedRates])
83
+ const bestBuyAmount = bestRate?.buyAmountCryptoBaseUnit ?? '0'
84
+
85
+ if (!isOpen) return null
86
+
87
+ return (
88
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
89
+ <div
90
+ className='ssw-quotes-modal-backdrop'
91
+ onClick={handleBackdropClick}
92
+ onKeyDown={e => e.key === 'Escape' && onClose()}
93
+ role='dialog'
94
+ aria-modal='true'
95
+ aria-labelledby='quotes-modal-title'
96
+ >
97
+ <div className='ssw-quotes-modal'>
98
+ <div className='ssw-quotes-modal-header'>
99
+ <div className='ssw-quotes-header-content'>
100
+ <h2 id='quotes-modal-title' className='ssw-quotes-modal-title'>
101
+ Select Route
102
+ </h2>
103
+ <span className='ssw-quotes-modal-subtitle'>
104
+ {formatAmount(sellAmountBaseUnit, sellAsset.precision)} {sellAsset.symbol} →{' '}
105
+ {buyAsset.symbol}
106
+ </span>
107
+ </div>
108
+ <button className='ssw-quotes-modal-close' onClick={onClose} type='button'>
109
+ <svg
110
+ width='20'
111
+ height='20'
112
+ viewBox='0 0 24 24'
113
+ fill='none'
114
+ stroke='currentColor'
115
+ strokeWidth='2'
116
+ >
117
+ <path d='M18 6L6 18M6 6l12 12' />
118
+ </svg>
119
+ </button>
120
+ </div>
121
+
122
+ <div className='ssw-quotes-modal-list'>
123
+ {sortedRates.map((rate, index) => {
124
+ const buyAmount = rate.buyAmountCryptoBaseUnit ?? '0'
125
+ const estimatedTime = rate.estimatedExecutionTimeMs
126
+ const isBest = index === 0
127
+ const isSelected = selectedRate?.id === rate.id
128
+ const swapperIcon = getSwapperIcon(rate.swapperName)
129
+ const swapperColor = getSwapperColor(rate.swapperName)
130
+ const formattedBuyAmount = formatAmount(buyAmount, buyAsset.precision)
131
+ const usdValue = formatUsdValue(buyAmount, buyAsset.precision, buyAssetUsdPrice)
132
+ const savingsPercent = isBest ? null : calculateSavingsPercent(bestBuyAmount, buyAmount)
133
+ const estimatedSeconds = estimatedTime ? Math.round(estimatedTime / 1000) : 0
134
+ const hasTime = estimatedSeconds > 0
135
+
136
+ return (
137
+ <button
138
+ key={rate.id}
139
+ className={`ssw-quote-row ${isSelected ? 'ssw-selected' : ''} ${
140
+ isBest ? 'ssw-best' : ''
141
+ }`}
142
+ onClick={() => handleSelectRate(rate)}
143
+ type='button'
144
+ >
145
+ <div className='ssw-quote-row-left'>
146
+ {swapperIcon ? (
147
+ <img src={swapperIcon} alt={rate.swapperName} className='ssw-quote-row-icon' />
148
+ ) : (
149
+ <div
150
+ className='ssw-quote-row-icon-placeholder'
151
+ style={{ backgroundColor: swapperColor }}
152
+ >
153
+ {rate.swapperName.charAt(0)}
154
+ </div>
155
+ )}
156
+ <div className='ssw-quote-row-info'>
157
+ <div className='ssw-quote-row-name-row'>
158
+ <span className='ssw-quote-row-name'>{rate.swapperName}</span>
159
+ {isBest && <span className='ssw-quote-row-best'>Best</span>}
160
+ {savingsPercent && (
161
+ <span className='ssw-quote-row-diff'>-{savingsPercent}%</span>
162
+ )}
163
+ </div>
164
+ {hasTime && <span className='ssw-quote-row-time'>~{estimatedSeconds}s</span>}
165
+ </div>
166
+ </div>
167
+
168
+ <div className='ssw-quote-row-right'>
169
+ <span className='ssw-quote-row-amount'>
170
+ {formattedBuyAmount}{' '}
171
+ <span className='ssw-quote-row-symbol'>{buyAsset.symbol}</span>
172
+ </span>
173
+ <span className='ssw-quote-row-usd'>{usdValue}</span>
174
+ </div>
175
+ </button>
176
+ )
177
+ })}
178
+ </div>
179
+ </div>
180
+ </div>
181
+ )
182
+ }
@@ -0,0 +1,145 @@
1
+ .ssw-settings-modal {
2
+ background: var(--ssw-bg-secondary);
3
+ border-radius: var(--ssw-radius-lg);
4
+ width: 100%;
5
+ max-width: 400px;
6
+ overflow: hidden;
7
+ box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2);
8
+ animation: ssw-settings-slide-up 0.2s ease;
9
+ }
10
+
11
+ @keyframes ssw-settings-slide-up {
12
+ from {
13
+ opacity: 0;
14
+ transform: translateY(10px);
15
+ }
16
+ to {
17
+ opacity: 1;
18
+ transform: translateY(0);
19
+ }
20
+ }
21
+
22
+ .ssw-settings-content {
23
+ padding: 20px 24px 24px;
24
+ }
25
+
26
+ .ssw-settings-section {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: 12px;
30
+ }
31
+
32
+ .ssw-settings-label {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 6px;
36
+ font-size: 14px;
37
+ font-weight: 500;
38
+ color: var(--ssw-text-secondary);
39
+ }
40
+
41
+ .ssw-info-btn {
42
+ background: none;
43
+ border: none;
44
+ padding: 2px;
45
+ cursor: pointer;
46
+ color: var(--ssw-text-muted);
47
+ border-radius: 50%;
48
+ transition: color 0.15s ease;
49
+ }
50
+
51
+ .ssw-info-btn:hover {
52
+ color: var(--ssw-text-secondary);
53
+ }
54
+
55
+ .ssw-slippage-options {
56
+ display: flex;
57
+ gap: 8px;
58
+ }
59
+
60
+ .ssw-slippage-btn {
61
+ flex: 1;
62
+ padding: 10px 12px;
63
+ border: 1px solid var(--ssw-border);
64
+ border-radius: var(--ssw-radius-sm);
65
+ background: var(--ssw-bg-tertiary);
66
+ color: var(--ssw-text-primary);
67
+ font-size: 14px;
68
+ font-weight: 500;
69
+ cursor: pointer;
70
+ transition: all 0.15s ease;
71
+ }
72
+
73
+ .ssw-slippage-btn:hover {
74
+ border-color: var(--ssw-accent);
75
+ }
76
+
77
+ .ssw-slippage-btn.ssw-selected {
78
+ background: var(--ssw-accent-light);
79
+ border-color: var(--ssw-accent);
80
+ color: var(--ssw-accent);
81
+ }
82
+
83
+ .ssw-slippage-custom {
84
+ flex: 1.2;
85
+ position: relative;
86
+ display: flex;
87
+ align-items: center;
88
+ }
89
+
90
+ .ssw-slippage-custom input {
91
+ width: 100%;
92
+ padding: 10px 28px 10px 12px;
93
+ border: 1px solid var(--ssw-border);
94
+ border-radius: var(--ssw-radius-sm);
95
+ background: var(--ssw-bg-tertiary);
96
+ color: var(--ssw-text-primary);
97
+ font-size: 14px;
98
+ font-weight: 500;
99
+ outline: none;
100
+ transition: border-color 0.15s ease;
101
+ }
102
+
103
+ .ssw-slippage-custom input:focus {
104
+ border-color: var(--ssw-accent);
105
+ }
106
+
107
+ .ssw-slippage-custom input::placeholder {
108
+ color: var(--ssw-text-muted);
109
+ font-weight: 400;
110
+ }
111
+
112
+ .ssw-slippage-custom.ssw-selected input {
113
+ background: var(--ssw-accent-light);
114
+ border-color: var(--ssw-accent);
115
+ }
116
+
117
+ .ssw-slippage-suffix {
118
+ position: absolute;
119
+ right: 12px;
120
+ color: var(--ssw-text-secondary);
121
+ font-size: 14px;
122
+ pointer-events: none;
123
+ }
124
+
125
+ .ssw-slippage-warning {
126
+ display: flex;
127
+ align-items: flex-start;
128
+ gap: 8px;
129
+ padding: 10px 12px;
130
+ border-radius: var(--ssw-radius-sm);
131
+ background: rgba(255, 193, 7, 0.1);
132
+ color: #ffc107;
133
+ font-size: 13px;
134
+ line-height: 1.4;
135
+ }
136
+
137
+ .ssw-slippage-warning svg {
138
+ flex-shrink: 0;
139
+ margin-top: 1px;
140
+ }
141
+
142
+ .ssw-slippage-warning.ssw-error {
143
+ background: rgba(244, 67, 54, 0.1);
144
+ color: #f44336;
145
+ }