@lifi/widget 4.1.0 → 4.2.0

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 (204) hide show
  1. package/dist/esm/AppDefault.js +14 -0
  2. package/dist/esm/AppDefault.js.map +1 -1
  3. package/dist/esm/components/AmountInput/AmountInput.js +2 -1
  4. package/dist/esm/components/AmountInput/AmountInput.js.map +1 -1
  5. package/dist/esm/components/AmountInput/AmountInputEndAdornment.js +4 -2
  6. package/dist/esm/components/AmountInput/AmountInputEndAdornment.js.map +1 -1
  7. package/dist/esm/components/AmountInput/AmountInputEndAdornment.style.js +1 -1
  8. package/dist/esm/components/AmountInput/AmountInputEndAdornment.style.js.map +1 -1
  9. package/dist/esm/components/AmountInput/PriceFormHelperText.js +4 -2
  10. package/dist/esm/components/AmountInput/PriceFormHelperText.js.map +1 -1
  11. package/dist/esm/components/AmountInputCard/AmountInputCard.style.js +98 -0
  12. package/dist/esm/components/AmountInputCard/AmountInputCard.style.js.map +1 -0
  13. package/dist/esm/components/AmountInputCard/AmountInputCardPair.js +25 -0
  14. package/dist/esm/components/AmountInputCard/AmountInputCardPair.js.map +1 -0
  15. package/dist/esm/components/AmountInputCard/BalanceDisplay.js +35 -0
  16. package/dist/esm/components/AmountInputCard/BalanceDisplay.js.map +1 -0
  17. package/dist/esm/components/AmountInputCard/BalanceDisplay.style.js +11 -0
  18. package/dist/esm/components/AmountInputCard/BalanceDisplay.style.js.map +1 -0
  19. package/dist/esm/components/AmountInputCard/FiatValueToggle.js +41 -0
  20. package/dist/esm/components/AmountInputCard/FiatValueToggle.js.map +1 -0
  21. package/dist/esm/components/AmountInputCard/PercentageChips.js +80 -0
  22. package/dist/esm/components/AmountInputCard/PercentageChips.js.map +1 -0
  23. package/dist/esm/components/AmountInputCard/PercentageChips.style.js +43 -0
  24. package/dist/esm/components/AmountInputCard/PercentageChips.style.js.map +1 -0
  25. package/dist/esm/components/AmountInputCard/ReceiveAmountCard.js +137 -0
  26. package/dist/esm/components/AmountInputCard/ReceiveAmountCard.js.map +1 -0
  27. package/dist/esm/components/AmountInputCard/SendAmountCard.js +102 -0
  28. package/dist/esm/components/AmountInputCard/SendAmountCard.js.map +1 -0
  29. package/dist/esm/components/Avatar/Avatar.js +12 -3
  30. package/dist/esm/components/Avatar/Avatar.js.map +1 -1
  31. package/dist/esm/components/Chains/VirtualizedChainList.js +2 -1
  32. package/dist/esm/components/Chains/VirtualizedChainList.js.map +1 -1
  33. package/dist/esm/components/Header/HeaderNavigationTabs.js +14 -1
  34. package/dist/esm/components/Header/HeaderNavigationTabs.js.map +1 -1
  35. package/dist/esm/components/IconCircle/IconCircle.style.js +1 -1
  36. package/dist/esm/components/LimitOrderSettings/LimitOrderSettings.js +99 -0
  37. package/dist/esm/components/LimitOrderSettings/LimitOrderSettings.js.map +1 -0
  38. package/dist/esm/components/LimitOrderSettings/LimitOrderSettings.style.js +52 -0
  39. package/dist/esm/components/LimitOrderSettings/LimitOrderSettings.style.js.map +1 -0
  40. package/dist/esm/components/LimitPriceCard/LimitPriceCard.js +115 -0
  41. package/dist/esm/components/LimitPriceCard/LimitPriceCard.js.map +1 -0
  42. package/dist/esm/components/LimitPriceCard/LimitPriceCard.style.js +89 -0
  43. package/dist/esm/components/LimitPriceCard/LimitPriceCard.style.js.map +1 -0
  44. package/dist/esm/components/LimitPriceCard/LimitPricePresets.js +91 -0
  45. package/dist/esm/components/LimitPriceCard/LimitPricePresets.js.map +1 -0
  46. package/dist/esm/components/RouteCard/RouteProviderCard.js +118 -0
  47. package/dist/esm/components/RouteCard/RouteProviderCard.js.map +1 -0
  48. package/dist/esm/components/Routes/LimitOrderRoutes.js +34 -0
  49. package/dist/esm/components/Routes/LimitOrderRoutes.js.map +1 -0
  50. package/dist/esm/components/Routes/Routes.js +2 -2
  51. package/dist/esm/components/Routes/Routes.js.map +1 -1
  52. package/dist/esm/components/SettingsPanel/SettingsPanel.js +51 -0
  53. package/dist/esm/components/SettingsPanel/SettingsPanel.js.map +1 -0
  54. package/dist/esm/components/SettingsPanel/SettingsPanel.style.js +33 -0
  55. package/dist/esm/components/SettingsPanel/SettingsPanel.style.js.map +1 -0
  56. package/dist/esm/components/SettingsPanel/utils.js +14 -0
  57. package/dist/esm/components/SettingsPanel/utils.js.map +1 -0
  58. package/dist/esm/components/SwapButton/SwapButton.js +50 -0
  59. package/dist/esm/components/SwapButton/SwapButton.js.map +1 -0
  60. package/dist/esm/components/SwapButton/SwapButton.style.js +24 -0
  61. package/dist/esm/components/SwapButton/SwapButton.style.js.map +1 -0
  62. package/dist/esm/components/TokenPillButton/TokenPillButton.js +52 -0
  63. package/dist/esm/components/TokenPillButton/TokenPillButton.js.map +1 -0
  64. package/dist/esm/components/TokenPillButton/TokenPillButton.style.js +39 -0
  65. package/dist/esm/components/TokenPillButton/TokenPillButton.style.js.map +1 -0
  66. package/dist/esm/config/version.d.ts +1 -1
  67. package/dist/esm/config/version.js +1 -1
  68. package/dist/esm/config/version.js.map +1 -1
  69. package/dist/esm/hooks/useChainSearch.js +2 -1
  70. package/dist/esm/hooks/useChainSearch.js.map +1 -1
  71. package/dist/esm/hooks/useLimitMarketRate.js +24 -0
  72. package/dist/esm/hooks/useLimitMarketRate.js.map +1 -0
  73. package/dist/esm/hooks/useLinkedLimitFields.js +49 -0
  74. package/dist/esm/hooks/useLinkedLimitFields.js.map +1 -0
  75. package/dist/esm/hooks/useRoutes.js +23 -6
  76. package/dist/esm/hooks/useRoutes.js.map +1 -1
  77. package/dist/esm/hooks/useTokens.js +16 -14
  78. package/dist/esm/hooks/useTokens.js.map +1 -1
  79. package/dist/esm/hooks/useWideVariant.js +5 -2
  80. package/dist/esm/hooks/useWideVariant.js.map +1 -1
  81. package/dist/esm/i18n/en.json +22 -0
  82. package/dist/esm/index.d.ts +2 -2
  83. package/dist/esm/pages/MainPage/LimitPageContent.js +23 -0
  84. package/dist/esm/pages/MainPage/LimitPageContent.js.map +1 -0
  85. package/dist/esm/pages/MainPage/MainPage.js +19 -22
  86. package/dist/esm/pages/MainPage/MainPage.js.map +1 -1
  87. package/dist/esm/pages/MainPage/MainPageActions.js +28 -0
  88. package/dist/esm/pages/MainPage/MainPageActions.js.map +1 -0
  89. package/dist/esm/pages/MainPage/ReviewButton.js +9 -2
  90. package/dist/esm/pages/MainPage/ReviewButton.js.map +1 -1
  91. package/dist/esm/pages/RoutePriorityPage.js +48 -0
  92. package/dist/esm/pages/RoutePriorityPage.js.map +1 -0
  93. package/dist/esm/pages/SettingsPage/BridgeAndExchangeSettings.js +2 -2
  94. package/dist/esm/pages/SettingsPage/BridgeAndExchangeSettings.js.map +1 -1
  95. package/dist/esm/pages/SettingsPage/RoutePrioritySettings.js +12 -27
  96. package/dist/esm/pages/SettingsPage/RoutePrioritySettings.js.map +1 -1
  97. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippagePage.js +133 -0
  98. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippagePage.js.map +1 -0
  99. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.js +14 -72
  100. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.js.map +1 -1
  101. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.style.js +26 -14
  102. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.style.js.map +1 -1
  103. package/dist/esm/pages/SettingsPage/ThemeSettings.js +4 -0
  104. package/dist/esm/pages/SettingsPage/ThemeSettings.js.map +1 -1
  105. package/dist/esm/pages/TransactionPage/TransactionReview.js +4 -1
  106. package/dist/esm/pages/TransactionPage/TransactionReview.js.map +1 -1
  107. package/dist/esm/providers/I18nProvider/I18nProvider.js +1 -1
  108. package/dist/esm/providers/I18nProvider/enResource.d.ts +1 -1
  109. package/dist/esm/providers/I18nProvider/enResource.js +1 -1
  110. package/dist/esm/providers/I18nProvider/types.d.ts +2 -2
  111. package/dist/esm/providers/SDKClientProvider.js +25 -24
  112. package/dist/esm/providers/SDKClientProvider.js.map +1 -1
  113. package/dist/esm/providers/WidgetProvider/WidgetProvider.js +22 -21
  114. package/dist/esm/providers/WidgetProvider/WidgetProvider.js.map +1 -1
  115. package/dist/esm/stores/form/URLSearchParamsBuilder.js +14 -3
  116. package/dist/esm/stores/form/URLSearchParamsBuilder.js.map +1 -1
  117. package/dist/esm/stores/form/createFormStore.js +6 -2
  118. package/dist/esm/stores/form/createFormStore.js.map +1 -1
  119. package/dist/esm/stores/form/getDefaultValuesFromQueryString.js +3 -0
  120. package/dist/esm/stores/form/getDefaultValuesFromQueryString.js.map +1 -1
  121. package/dist/esm/stores/form/types.d.ts +9 -1
  122. package/dist/esm/stores/form/types.d.ts.map +1 -1
  123. package/dist/esm/stores/form/types.js.map +1 -1
  124. package/dist/esm/stores/form/useClearAmountFields.js +1 -0
  125. package/dist/esm/stores/form/useClearAmountFields.js.map +1 -1
  126. package/dist/esm/stores/navigationTabs/useNavigationTabsStore.js +4 -2
  127. package/dist/esm/stores/navigationTabs/useNavigationTabsStore.js.map +1 -1
  128. package/dist/esm/stores/navigationTabs/utils.js +38 -8
  129. package/dist/esm/stores/navigationTabs/utils.js.map +1 -1
  130. package/dist/esm/types/events.d.ts +10 -1
  131. package/dist/esm/types/events.d.ts.map +1 -1
  132. package/dist/esm/types/events.js +2 -0
  133. package/dist/esm/types/events.js.map +1 -1
  134. package/dist/esm/types/widget.d.ts +9 -0
  135. package/dist/esm/types/widget.d.ts.map +1 -1
  136. package/dist/esm/types/widget.js.map +1 -1
  137. package/dist/esm/utils/format.d.ts +1 -1
  138. package/dist/esm/utils/format.d.ts.map +1 -1
  139. package/dist/esm/utils/format.js +2 -2
  140. package/dist/esm/utils/format.js.map +1 -1
  141. package/dist/esm/utils/limitOrder.js +61 -0
  142. package/dist/esm/utils/limitOrder.js.map +1 -0
  143. package/dist/esm/utils/navigationRoutes.d.ts +2 -0
  144. package/dist/esm/utils/navigationRoutes.d.ts.map +1 -1
  145. package/dist/esm/utils/navigationRoutes.js +6 -0
  146. package/dist/esm/utils/navigationRoutes.js.map +1 -1
  147. package/package.json +11 -11
  148. package/src/AppDefault.tsx +16 -0
  149. package/src/components/AmountInput/AmountInputEndAdornment.style.ts +1 -1
  150. package/src/components/AmountInput/AmountInputEndAdornment.tsx +5 -2
  151. package/src/components/AmountInputCard/AmountInputCard.style.tsx +157 -0
  152. package/src/components/AmountInputCard/AmountInputCardPair.tsx +25 -0
  153. package/src/components/AmountInputCard/BalanceDisplay.style.tsx +9 -0
  154. package/src/components/AmountInputCard/BalanceDisplay.tsx +46 -0
  155. package/src/components/AmountInputCard/FiatValueToggle.tsx +55 -0
  156. package/src/components/AmountInputCard/PercentageChips.style.tsx +60 -0
  157. package/src/components/AmountInputCard/PercentageChips.tsx +100 -0
  158. package/src/components/AmountInputCard/ReceiveAmountCard.tsx +219 -0
  159. package/src/components/AmountInputCard/SendAmountCard.tsx +179 -0
  160. package/src/components/Avatar/Avatar.tsx +8 -3
  161. package/src/components/Header/HeaderNavigationTabs.tsx +19 -2
  162. package/src/components/LimitOrderSettings/LimitOrderSettings.style.tsx +69 -0
  163. package/src/components/LimitOrderSettings/LimitOrderSettings.tsx +124 -0
  164. package/src/components/LimitPriceCard/LimitPriceCard.style.tsx +103 -0
  165. package/src/components/LimitPriceCard/LimitPriceCard.tsx +153 -0
  166. package/src/components/LimitPriceCard/LimitPricePresets.tsx +139 -0
  167. package/src/components/RouteCard/RouteProviderCard.tsx +113 -0
  168. package/src/components/Routes/LimitOrderRoutes.tsx +64 -0
  169. package/src/components/Routes/Routes.tsx +3 -2
  170. package/src/components/SettingsPanel/SettingsPanel.style.tsx +41 -0
  171. package/src/components/SettingsPanel/SettingsPanel.tsx +73 -0
  172. package/src/components/SettingsPanel/utils.ts +27 -0
  173. package/src/components/SwapButton/SwapButton.style.tsx +28 -0
  174. package/src/components/SwapButton/SwapButton.tsx +71 -0
  175. package/src/components/TokenPillButton/TokenPillButton.style.tsx +50 -0
  176. package/src/components/TokenPillButton/TokenPillButton.tsx +81 -0
  177. package/src/config/version.ts +1 -1
  178. package/src/hooks/useLimitMarketRate.ts +39 -0
  179. package/src/hooks/useLinkedLimitFields.ts +74 -0
  180. package/src/hooks/useRoutes.ts +45 -4
  181. package/src/hooks/useWideVariant.ts +8 -2
  182. package/src/pages/MainPage/LimitPageContent.tsx +18 -0
  183. package/src/pages/MainPage/MainPage.tsx +43 -30
  184. package/src/pages/MainPage/MainPageActions.tsx +35 -0
  185. package/src/pages/MainPage/ReviewButton.tsx +13 -1
  186. package/src/pages/RoutePriorityPage.tsx +55 -0
  187. package/src/pages/SettingsPage/RoutePrioritySettings.tsx +14 -38
  188. package/src/pages/SettingsPage/SlippageSettings/SlippagePage.tsx +176 -0
  189. package/src/pages/SettingsPage/SlippageSettings/SlippageSettings.style.tsx +32 -25
  190. package/src/pages/SettingsPage/SlippageSettings/SlippageSettings.tsx +14 -128
  191. package/src/pages/SettingsPage/ThemeSettings.tsx +4 -0
  192. package/src/stores/form/URLSearchParamsBuilder.tsx +25 -3
  193. package/src/stores/form/createFormStore.ts +7 -3
  194. package/src/stores/form/getDefaultValuesFromQueryString.ts +11 -0
  195. package/src/stores/form/types.ts +14 -1
  196. package/src/stores/form/useClearAmountFields.ts +1 -0
  197. package/src/stores/navigationTabs/types.ts +6 -0
  198. package/src/stores/navigationTabs/useNavigationTabsStore.tsx +8 -3
  199. package/src/stores/navigationTabs/utils.ts +67 -5
  200. package/src/types/events.ts +12 -0
  201. package/src/types/widget.ts +9 -0
  202. package/src/utils/format.ts +6 -2
  203. package/src/utils/limitOrder.ts +88 -0
  204. package/src/utils/navigationRoutes.ts +6 -0
@@ -0,0 +1,219 @@
1
+ import type { Route } from '@lifi/sdk'
2
+ import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
3
+ import SwapVertIcon from '@mui/icons-material/SwapVert'
4
+ import type { CardProps } from '@mui/material'
5
+ import { Box, Skeleton, Tooltip } from '@mui/material'
6
+ import { type JSX, type ReactNode, useLayoutEffect, useRef } from 'react'
7
+ import { useTranslation } from 'react-i18next'
8
+ import { useRoutes } from '../../hooks/useRoutes.js'
9
+ import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
10
+ import { useFieldValues } from '../../stores/form/useFieldValues.js'
11
+ import { useInputModeStore } from '../../stores/inputMode/useInputModeStore.js'
12
+ import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js'
13
+ import { getPriceImpact } from '../../utils/getPriceImpact.js'
14
+ import { fitInputText } from '../../utils/input.js'
15
+ import { CardTitle } from '../Card/CardTitle.js'
16
+ import { ProgressToNextUpdate } from '../ProgressToNextUpdate.js'
17
+ import { TokenPillButton } from '../TokenPillButton/TokenPillButton.js'
18
+ import {
19
+ AmountCard,
20
+ AmountDisplay,
21
+ amountHeight,
22
+ CardBodyRow,
23
+ CardFooterRow,
24
+ CardHeaderRow,
25
+ FooterText,
26
+ footerFontSize,
27
+ maxInputFontSize,
28
+ minInputFontSize,
29
+ ToggleButton,
30
+ } from './AmountInputCard.style.js'
31
+
32
+ const formType = 'to' as const
33
+
34
+ type ReceiveCardProps = CardProps & { mask?: boolean }
35
+
36
+ const ReceiveAmountCardLayout = ({
37
+ cardProps,
38
+ title,
39
+ headerEnd,
40
+ amount,
41
+ footerStart,
42
+ footerEnd,
43
+ }: {
44
+ cardProps: ReceiveCardProps
45
+ title: string
46
+ headerEnd?: ReactNode
47
+ amount: ReactNode
48
+ footerStart: ReactNode
49
+ footerEnd?: ReactNode
50
+ }): JSX.Element => (
51
+ <AmountCard {...cardProps} formType={formType}>
52
+ <CardHeaderRow>
53
+ <CardTitle sx={{ padding: 0 }}>{title}</CardTitle>
54
+ {headerEnd}
55
+ </CardHeaderRow>
56
+ <CardBodyRow>
57
+ {amount}
58
+ <TokenPillButton formType={formType} />
59
+ </CardBodyRow>
60
+ <CardFooterRow>
61
+ {footerStart}
62
+ {footerEnd}
63
+ </CardFooterRow>
64
+ </AmountCard>
65
+ )
66
+
67
+ const PriceImpactBadge = ({
68
+ priceImpact,
69
+ }: {
70
+ priceImpact: number | undefined
71
+ }): JSX.Element | null => {
72
+ const { t } = useTranslation()
73
+ if (priceImpact === undefined) {
74
+ return null
75
+ }
76
+ return (
77
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
78
+ <FooterText>
79
+ {t('format.percent', { value: priceImpact, usePlusSign: true })}
80
+ </FooterText>
81
+ <Tooltip title={t('tooltip.priceImpact')}>
82
+ <InfoOutlinedIcon
83
+ sx={{ fontSize: 16, color: 'text.secondary', cursor: 'help' }}
84
+ />
85
+ </Tooltip>
86
+ </Box>
87
+ )
88
+ }
89
+
90
+ const useReceivePriceImpact = (
91
+ route: Route | undefined
92
+ ): number | undefined => {
93
+ const { hiddenUI } = useWidgetConfig()
94
+ if (!route || hiddenUI?.routeCardPriceImpact) {
95
+ return undefined
96
+ }
97
+ return getPriceImpact({
98
+ fromAmount: BigInt(route.fromAmount),
99
+ toAmount: BigInt(route.toAmount),
100
+ fromToken: route.fromToken,
101
+ toToken: route.toToken,
102
+ })
103
+ }
104
+
105
+ /**
106
+ * The receive amount is a read-only quote derived from the best route, with a
107
+ * fiat/token toggle and price impact in the footer. In limit mode the amount is
108
+ * still the quote for the current limit (the limit price drives it via the
109
+ * dedicated price card); it is never edited here, and the header shows the
110
+ * refetch progress indicator with a "Buy" title.
111
+ */
112
+ export const ReceiveAmountCard: React.FC<ReceiveCardProps> = (
113
+ props
114
+ ): JSX.Element => {
115
+ const { t } = useTranslation()
116
+ const amountRef = useRef<HTMLSpanElement>(null)
117
+ const { inputMode, toggleInputMode } = useInputModeStore()
118
+ const showFiat = inputMode[formType] === 'price'
119
+ const { hiddenUI, mode } = useWidgetConfig()
120
+ const isLimit = mode === 'limit'
121
+
122
+ const [selectedRouteId] = useFieldValues('selectedRouteId')
123
+ const { routes, isFetching, dataUpdatedAt, refetchTime, refetch } =
124
+ useRoutes()
125
+ // Reflect the route the user selected in the routes list (falling back to the
126
+ // best route), so the buy amount matches the highlighted provider card.
127
+ const route = routes?.find((r) => r.id === selectedRouteId) ?? routes?.[0]
128
+ const priceImpact = useReceivePriceImpact(route)
129
+ const showPriceImpact = !hiddenUI?.routeCardPriceImpact
130
+
131
+ const receiveAmount = route?.toAmount
132
+ ? formatTokenAmount(BigInt(route.toAmount), route.toToken.decimals)
133
+ : undefined
134
+ // Only show skeletons on the initial fetch.
135
+ const showSkeleton = isFetching && !receiveAmount
136
+
137
+ const fiatValue =
138
+ receiveAmount && route?.toToken.priceUSD
139
+ ? formatTokenPrice(receiveAmount, route.toToken.priceUSD)
140
+ : 0
141
+ const canToggle = !!receiveAmount && !!route?.toToken.priceUSD
142
+
143
+ const mainDisplay = showFiat
144
+ ? t('format.currency', { value: fiatValue })
145
+ : t('format.tokenAmount', { value: receiveAmount || '0' })
146
+ const footerDisplay = showFiat
147
+ ? t('format.tokenAmount', { value: receiveAmount || '0' })
148
+ : t('format.currency', { value: fiatValue })
149
+
150
+ // biome-ignore lint/correctness/useExhaustiveDependencies: effect must run on value change
151
+ useLayoutEffect(() => {
152
+ if (amountRef.current) {
153
+ fitInputText(maxInputFontSize, minInputFontSize, amountRef.current)
154
+ }
155
+ }, [mainDisplay])
156
+
157
+ return (
158
+ <ReceiveAmountCardLayout
159
+ cardProps={props}
160
+ title={isLimit ? t('header.buy') : t('header.receive')}
161
+ headerEnd={
162
+ isLimit && (isFetching || routes?.length) ? (
163
+ <ProgressToNextUpdate
164
+ updatedAt={dataUpdatedAt || Date.now()}
165
+ timeToUpdate={refetchTime}
166
+ isLoading={isFetching}
167
+ onClick={() => refetch()}
168
+ sx={{ padding: 0 }}
169
+ />
170
+ ) : undefined
171
+ }
172
+ amount={
173
+ showSkeleton ? (
174
+ <Skeleton variant="rounded" height={amountHeight} sx={{ flex: 1 }} />
175
+ ) : (
176
+ <AmountDisplay
177
+ ref={amountRef}
178
+ sx={{ color: 'text.primary', opacity: receiveAmount ? 1 : 0.5 }}
179
+ >
180
+ {mainDisplay}
181
+ </AmountDisplay>
182
+ )
183
+ }
184
+ footerStart={
185
+ showSkeleton ? (
186
+ <Skeleton
187
+ variant="text"
188
+ width={72}
189
+ sx={{ fontSize: footerFontSize }}
190
+ />
191
+ ) : (
192
+ <ToggleButton
193
+ clickable={canToggle}
194
+ onClick={canToggle ? () => toggleInputMode(formType) : undefined}
195
+ aria-label={canToggle ? 'Toggle fiat value' : undefined}
196
+ >
197
+ <FooterText>{footerDisplay}</FooterText>
198
+ {canToggle ? (
199
+ <SwapVertIcon sx={{ fontSize: 14, color: 'text.secondary' }} />
200
+ ) : null}
201
+ </ToggleButton>
202
+ )
203
+ }
204
+ footerEnd={
205
+ showSkeleton ? (
206
+ showPriceImpact ? (
207
+ <Skeleton
208
+ variant="text"
209
+ width={56}
210
+ sx={{ fontSize: footerFontSize }}
211
+ />
212
+ ) : null
213
+ ) : (
214
+ <PriceImpactBadge priceImpact={priceImpact} />
215
+ )
216
+ }
217
+ />
218
+ )
219
+ }
@@ -0,0 +1,179 @@
1
+ import type { CardProps } from '@mui/material'
2
+ import type { ChangeEvent, JSX } from 'react'
3
+ import { useLayoutEffect, useRef, useState } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useLinkedLimitFields } from '../../hooks/useLinkedLimitFields.js'
6
+ import { useToken } from '../../hooks/useToken.js'
7
+ import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
8
+ import { FormKeyHelper } from '../../stores/form/types.js'
9
+ import { useFieldActions } from '../../stores/form/useFieldActions.js'
10
+ import { useFieldValues } from '../../stores/form/useFieldValues.js'
11
+ import { useInputModeStore } from '../../stores/inputMode/useInputModeStore.js'
12
+ import {
13
+ formatInputAmount,
14
+ formatTokenPrice,
15
+ priceToTokenAmount,
16
+ usdDecimals,
17
+ } from '../../utils/format.js'
18
+ import { fitInputText } from '../../utils/input.js'
19
+ import { CardTitle } from '../Card/CardTitle.js'
20
+ import { TokenPillButton } from '../TokenPillButton/TokenPillButton.js'
21
+ import {
22
+ AmountCard,
23
+ CardBodyRow,
24
+ CardFooterRow,
25
+ CardHeaderRow,
26
+ LargeInput,
27
+ maxInputFontSize,
28
+ minInputFontSize,
29
+ } from './AmountInputCard.style.js'
30
+ import { BalanceDisplay } from './BalanceDisplay.js'
31
+ import { FiatValueToggle } from './FiatValueToggle.js'
32
+ import { PercentageChips } from './PercentageChips.js'
33
+
34
+ export const SendAmountCard: React.FC<CardProps & { mask?: boolean }> = (
35
+ props
36
+ ): JSX.Element => {
37
+ const { t } = useTranslation()
38
+ const { disabledUI, mode, modeOptions } = useWidgetConfig()
39
+ const ref = useRef<HTMLInputElement>(null)
40
+ const isEditingRef = useRef(false)
41
+ const [formattedPriceInput, setFormattedPriceInput] = useState('')
42
+
43
+ const formType = 'from' as const
44
+
45
+ const [chainId, tokenAddress, value] = useFieldValues(
46
+ FormKeyHelper.getChainKey(formType),
47
+ FormKeyHelper.getTokenKey(formType),
48
+ FormKeyHelper.getAmountKey(formType)
49
+ )
50
+
51
+ const { token } = useToken(chainId, tokenAddress)
52
+ const { setFieldValue } = useFieldActions()
53
+ const { inputMode } = useInputModeStore()
54
+ const { setSendAmount } = useLinkedLimitFields()
55
+ const currentInputMode = inputMode[formType]
56
+ const disabled = disabledUI?.fromAmount
57
+
58
+ // In limit mode the send amount drives the linked receive amount; otherwise
59
+ // it is a plain form-field write.
60
+ const applyFromAmount = (value: string): void => {
61
+ if (mode === 'limit') {
62
+ setSendAmount(value)
63
+ } else {
64
+ setFieldValue('fromAmount', value, { isDirty: true, isTouched: true })
65
+ }
66
+ }
67
+
68
+ const title =
69
+ mode === 'limit'
70
+ ? t('header.sell')
71
+ : mode === 'custom' && modeOptions?.custom?.type === 'deposit'
72
+ ? t('header.deposit')
73
+ : t('header.send')
74
+
75
+ let displayValue: string
76
+ if (isEditingRef.current) {
77
+ if (currentInputMode === 'price') {
78
+ displayValue = formattedPriceInput
79
+ } else {
80
+ displayValue = value as string
81
+ }
82
+ } else {
83
+ if (currentInputMode === 'price') {
84
+ const priceValue = formatTokenPrice(value as string, token?.priceUSD)
85
+ displayValue = formatInputAmount(
86
+ priceValue.toFixed(usdDecimals),
87
+ usdDecimals
88
+ )
89
+ } else {
90
+ displayValue = value as string
91
+ }
92
+ }
93
+
94
+ const handleChange = (
95
+ event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
96
+ ): void => {
97
+ const { value: inputValue } = event.target
98
+ isEditingRef.current = true
99
+
100
+ if (currentInputMode === 'price') {
101
+ const cleanInputValue = inputValue.replace('$', '')
102
+ const formattedValue = formatInputAmount(
103
+ cleanInputValue,
104
+ usdDecimals,
105
+ true
106
+ )
107
+ const tokenValue = priceToTokenAmount(formattedValue, token?.priceUSD)
108
+ setFormattedPriceInput(formattedValue)
109
+ applyFromAmount(tokenValue)
110
+ } else {
111
+ const formattedValue = formatInputAmount(
112
+ inputValue,
113
+ token?.decimals,
114
+ true
115
+ )
116
+ applyFromAmount(formattedValue)
117
+ }
118
+ }
119
+
120
+ const handleBlur = (
121
+ event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
122
+ ): void => {
123
+ const { value: inputValue } = event.target
124
+ isEditingRef.current = false
125
+
126
+ if (currentInputMode === 'price') {
127
+ const cleanInputValue = inputValue.replace('$', '')
128
+ const formattedValue = formatInputAmount(cleanInputValue, usdDecimals)
129
+ const tokenValue = priceToTokenAmount(formattedValue, token?.priceUSD)
130
+ const formattedAmount = formatInputAmount(tokenValue, token?.decimals)
131
+ applyFromAmount(formattedAmount)
132
+ } else {
133
+ const formattedValue = formatInputAmount(inputValue, token?.decimals)
134
+ applyFromAmount(formattedValue)
135
+ }
136
+ }
137
+
138
+ // biome-ignore lint/correctness/useExhaustiveDependencies: effect must run on value change
139
+ useLayoutEffect(() => {
140
+ if (ref.current) {
141
+ fitInputText(maxInputFontSize, minInputFontSize, ref.current)
142
+ }
143
+ }, [displayValue])
144
+
145
+ return (
146
+ <AmountCard {...props} formType={formType}>
147
+ <CardHeaderRow>
148
+ <CardTitle sx={{ padding: 0 }}>{title}</CardTitle>
149
+ <PercentageChips formType={formType} />
150
+ </CardHeaderRow>
151
+ <CardBodyRow>
152
+ <LargeInput
153
+ inputRef={ref}
154
+ size="small"
155
+ autoComplete="off"
156
+ placeholder={currentInputMode === 'price' ? '$0' : '0'}
157
+ inputProps={{ inputMode: 'decimal' }}
158
+ onChange={handleChange}
159
+ onBlur={handleBlur}
160
+ value={
161
+ currentInputMode === 'price'
162
+ ? displayValue
163
+ ? `$${displayValue}`
164
+ : ''
165
+ : displayValue
166
+ }
167
+ name="fromAmount"
168
+ disabled={disabled}
169
+ required
170
+ />
171
+ <TokenPillButton formType={formType} />
172
+ </CardBodyRow>
173
+ <CardFooterRow>
174
+ <FiatValueToggle formType={formType} />
175
+ <BalanceDisplay formType={formType} />
176
+ </CardFooterRow>
177
+ </AmountCard>
178
+ )
179
+ }
@@ -8,15 +8,20 @@ import { SmallAvatarSkeleton } from './SmallAvatar.js'
8
8
  export const AvatarBadgedDefault: React.FC<{
9
9
  sx?: SxProps<Theme>
10
10
  chain?: ExtendedChain
11
- }> = ({ sx, chain }) => {
11
+ avatarSize?: number
12
+ badgeSize?: number
13
+ }> = ({ sx, chain, avatarSize, badgeSize }) => {
12
14
  return (
13
15
  <Badge
14
16
  overlap="circular"
15
17
  anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
16
- badgeContent={<ChainBadgeContent chain={chain} />}
18
+ badgeContent={<ChainBadgeContent chain={chain} size={badgeSize} />}
17
19
  sx={sx}
18
20
  >
19
- <AvatarDefault />
21
+ <AvatarDefault
22
+ badgeSize={badgeSize}
23
+ sx={avatarSize ? { width: avatarSize, height: avatarSize } : undefined}
24
+ />
20
25
  </Badge>
21
26
  )
22
27
  }
@@ -1,6 +1,9 @@
1
- import type { JSX } from 'react'
1
+ import { type JSX, useEffect, useRef } from 'react'
2
+ import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'
2
3
  import { useNavigationTabLabel } from '../../stores/navigationTabs/useNavigationTabLabel.js'
3
4
  import { useNavigationTabsStore } from '../../stores/navigationTabs/useNavigationTabsStore.js'
5
+ import { WidgetEvent } from '../../types/events.js'
6
+ import type { NavigationTabKey } from '../../types/widget.js'
4
7
  import { HeaderTabs } from './HeaderTabs.js'
5
8
 
6
9
  export const HeaderNavigationTabs = (): JSX.Element | null => {
@@ -10,6 +13,20 @@ export const HeaderNavigationTabs = (): JSX.Element | null => {
10
13
  store.setActiveTab,
11
14
  ])
12
15
  const getLabel = useNavigationTabLabel()
16
+ const emitter = useWidgetEvents()
17
+ const previousTabRef = useRef<NavigationTabKey | undefined>(undefined)
18
+
19
+ // Emit on the first resolved tab and on every subsequent switch.
20
+ useEffect(() => {
21
+ if (!activeTab || previousTabRef.current === activeTab) {
22
+ return
23
+ }
24
+ emitter.emit(WidgetEvent.NavigationTabChanged, {
25
+ tab: activeTab,
26
+ previousTab: previousTabRef.current,
27
+ })
28
+ previousTabRef.current = activeTab
29
+ }, [emitter, activeTab])
13
30
 
14
31
  if (!tabs.length || !activeTab) {
15
32
  return null
@@ -19,7 +36,7 @@ export const HeaderNavigationTabs = (): JSX.Element | null => {
19
36
  <HeaderTabs
20
37
  tabs={tabs.map((key) => ({ key, label: getLabel(key) }))}
21
38
  value={activeTab}
22
- onChange={(key) => setActiveTab(key)}
39
+ onChange={setActiveTab}
23
40
  />
24
41
  )
25
42
  }
@@ -0,0 +1,69 @@
1
+ import { Box, ButtonBase, styled, Typography } from '@mui/material'
2
+ import type React from 'react'
3
+
4
+ export const Container: React.FC<React.ComponentProps<typeof Box>> = styled(
5
+ Box
6
+ )(({ theme }) => ({
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ gap: theme.spacing(1),
10
+ }))
11
+
12
+ export const SettingsRow: React.FC<React.ComponentProps<typeof Box>> = styled(
13
+ Box
14
+ )(({ theme }) => ({
15
+ display: 'flex',
16
+ justifyContent: 'space-between',
17
+ alignItems: 'center',
18
+ gap: theme.spacing(2),
19
+ padding: theme.spacing(1.5, 2),
20
+ }))
21
+
22
+ export const ToggleLabel: React.FC<React.ComponentProps<typeof Box>> = styled(
23
+ Box
24
+ )(({ theme }) => ({
25
+ fontSize: 14,
26
+ fontWeight: 700,
27
+ lineHeight: 1,
28
+ color: theme.vars.palette.text.primary,
29
+ flex: '1 0 0',
30
+ minWidth: 0,
31
+ }))
32
+
33
+ export const ExpiryValue: React.FC<React.ComponentProps<typeof Box>> = styled(
34
+ Box
35
+ )(({ theme }) => ({
36
+ display: 'flex',
37
+ alignItems: 'center',
38
+ gap: theme.spacing(0.5),
39
+ color: theme.vars.palette.text.secondary,
40
+ height: 14,
41
+ }))
42
+
43
+ export const SettingItemButton: React.FC<
44
+ React.ComponentProps<typeof ButtonBase>
45
+ > = styled(ButtonBase)(({ theme }) => ({
46
+ display: 'flex',
47
+ justifyContent: 'space-between',
48
+ alignItems: 'center',
49
+ width: '100%',
50
+ padding: theme.spacing(2),
51
+ borderRadius: theme.vars.shape.borderRadius,
52
+ }))
53
+
54
+ export const SettingItemTitle: React.FC<
55
+ React.ComponentProps<typeof Typography>
56
+ > = styled(Typography)(({ theme }) => ({
57
+ fontSize: 14,
58
+ fontWeight: 700,
59
+ color: theme.vars.palette.text.primary,
60
+ lineHeight: 1,
61
+ }))
62
+
63
+ export const SettingItemValue: React.FC<
64
+ React.ComponentProps<typeof Typography>
65
+ > = styled(Typography)(({ theme }) => ({
66
+ fontSize: 14,
67
+ fontWeight: 700,
68
+ color: theme.vars.palette.text.primary,
69
+ }))
@@ -0,0 +1,124 @@
1
+ import Check from '@mui/icons-material/Check'
2
+ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
3
+ import type { BoxProps } from '@mui/material'
4
+ import { Menu, MenuItem } from '@mui/material'
5
+ import { type ChangeEvent, type JSX, type MouseEvent, useState } from 'react'
6
+ import { useTranslation } from 'react-i18next'
7
+ import { useFieldActions } from '../../stores/form/useFieldActions.js'
8
+ import { useFieldValues } from '../../stores/form/useFieldValues.js'
9
+ import { formatDuration } from '../../utils/format.js'
10
+ import { Card } from '../Card/Card.js'
11
+ import { Switch } from '../Switch.js'
12
+ import {
13
+ Container,
14
+ ExpiryValue,
15
+ SettingItemButton,
16
+ SettingItemTitle,
17
+ SettingItemValue,
18
+ SettingsRow,
19
+ ToggleLabel,
20
+ } from './LimitOrderSettings.style.js'
21
+
22
+ /** Selectable order-validity durations, in seconds. */
23
+ const DURATIONS = [3600, 86400, 604800, 2592000, 31536000]
24
+
25
+ export const LimitOrderSettings: React.FC<BoxProps> = (props): JSX.Element => {
26
+ const { t, i18n } = useTranslation()
27
+ const [validUntil, partiallyFillable] = useFieldValues(
28
+ 'validUntil',
29
+ 'partiallyFillable'
30
+ )
31
+ const { setFieldValue } = useFieldActions()
32
+ const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
33
+ const open = Boolean(anchorEl)
34
+
35
+ const handleOpen = (event: MouseEvent<HTMLElement>): void => {
36
+ setAnchorEl(event.currentTarget)
37
+ }
38
+
39
+ const handleClose = (): void => {
40
+ setAnchorEl(null)
41
+ }
42
+
43
+ const handleSelect = (duration: number): void => {
44
+ setFieldValue('validUntil', duration)
45
+ setAnchorEl(null)
46
+ }
47
+
48
+ const handlePartiallyFillableChange = (
49
+ event: ChangeEvent<HTMLInputElement>
50
+ ): void => {
51
+ setFieldValue('partiallyFillable', event.target.checked)
52
+ }
53
+
54
+ return (
55
+ <Container {...props}>
56
+ <Card>
57
+ <SettingItemButton
58
+ onClick={handleOpen}
59
+ disableRipple
60
+ aria-haspopup="menu"
61
+ aria-expanded={open}
62
+ >
63
+ <SettingItemTitle>{t('limitOrder.expiresIn')}</SettingItemTitle>
64
+ <ExpiryValue>
65
+ <SettingItemValue>
66
+ {formatDuration(validUntil, i18n.language, 'long')}
67
+ </SettingItemValue>
68
+ <KeyboardArrowDownIcon
69
+ sx={{ fontSize: 20, height: '18px', marginTop: '2px' }}
70
+ />
71
+ </ExpiryValue>
72
+ </SettingItemButton>
73
+ </Card>
74
+ <Menu
75
+ anchorEl={anchorEl}
76
+ open={open}
77
+ onClose={handleClose}
78
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
79
+ transformOrigin={{ vertical: 'top', horizontal: 'right' }}
80
+ slotProps={{
81
+ paper: {
82
+ sx: (theme) => ({
83
+ border: `1px solid ${theme.vars.palette.grey[300]}`,
84
+ boxShadow: 'none',
85
+ ...theme.applyStyles('dark', {
86
+ borderColor: theme.vars.palette.grey[800],
87
+ }),
88
+ }),
89
+ },
90
+ }}
91
+ >
92
+ {DURATIONS.map((duration) => (
93
+ <MenuItem
94
+ key={duration}
95
+ onClick={() => handleSelect(duration)}
96
+ sx={(theme) => ({
97
+ justifyContent: 'space-between',
98
+ gap: 4,
99
+ fontSize: 14,
100
+ fontWeight: 700,
101
+ lineHeight: '18px',
102
+ color: theme.vars.palette.text.primary,
103
+ })}
104
+ >
105
+ {formatDuration(duration, i18n.language, 'long')}
106
+ {duration === validUntil ? (
107
+ <Check color="primary" fontSize="small" />
108
+ ) : null}
109
+ </MenuItem>
110
+ ))}
111
+ </Menu>
112
+ <Card>
113
+ <SettingsRow>
114
+ <ToggleLabel>{t('limitOrder.partiallyFillable')}</ToggleLabel>
115
+ <Switch
116
+ checked={partiallyFillable}
117
+ onChange={handlePartiallyFillableChange}
118
+ aria-label={t('limitOrder.partiallyFillable')}
119
+ />
120
+ </SettingsRow>
121
+ </Card>
122
+ </Container>
123
+ )
124
+ }