@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,103 @@
1
+ import { Box, Button, ButtonBase, styled, Typography } from '@mui/material'
2
+ import type React from 'react'
3
+
4
+ /**
5
+ * Quick-set preset pill. Mirrors the percentage chips' pill look but is always
6
+ * visible (no reveal-on-hover), since presets are the card's primary control.
7
+ */
8
+ export const PriceChip: React.FC<React.ComponentProps<typeof Button>> = styled(
9
+ Button
10
+ )(({ theme }) => ({
11
+ minWidth: 0,
12
+ padding: theme.spacing(0.5, 1.5),
13
+ fontSize: 12,
14
+ fontWeight: 700,
15
+ lineHeight: 1.3334,
16
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.common.onBackground} 4%, transparent)`,
17
+ color: theme.vars.palette.text.primary,
18
+ transition: 'background-color 200ms ease',
19
+ '&:hover': {
20
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.common.onBackground} 8%, transparent)`,
21
+ },
22
+ }))
23
+
24
+ /**
25
+ * Editable variant of the preset pill: looks identical to {@link PriceChip} but
26
+ * wraps an inline input so the user can type an arbitrary percentage premium
27
+ * (`+__%`) instead of picking a fixed preset.
28
+ */
29
+ export const EditablePriceChip: React.FC<React.ComponentProps<typeof Box>> =
30
+ styled(Box)(({ theme }) => ({
31
+ display: 'inline-flex',
32
+ alignItems: 'center',
33
+ minWidth: 0,
34
+ padding: theme.spacing(0.5, 1.5),
35
+ borderRadius: theme.vars.shape.borderRadius,
36
+ fontSize: 12,
37
+ fontWeight: 700,
38
+ lineHeight: 1.3334,
39
+ color: theme.vars.palette.text.primary,
40
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.common.onBackground} 4%, transparent)`,
41
+ transition: 'background-color 200ms ease',
42
+ cursor: 'text',
43
+ '&:hover, &:focus-within': {
44
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.common.onBackground} 8%, transparent)`,
45
+ },
46
+ '&[aria-disabled="true"]': {
47
+ opacity: 0.5,
48
+ pointerEvents: 'none',
49
+ },
50
+ }))
51
+
52
+ /** Bare input inside {@link EditablePriceChip}; inherits the pill's typography. */
53
+ export const PriceChipInput: React.FC<React.ComponentProps<'input'>> = styled(
54
+ 'input'
55
+ )(({ theme }) => ({
56
+ appearance: 'none',
57
+ border: 'none',
58
+ outline: 'none',
59
+ background: 'transparent',
60
+ padding: 0,
61
+ margin: 0,
62
+ font: 'inherit',
63
+ color: 'inherit',
64
+ textAlign: 'right',
65
+ minWidth: '1ch',
66
+ '&::placeholder': {
67
+ color: theme.vars.palette.text.secondary,
68
+ opacity: 1,
69
+ },
70
+ }))
71
+
72
+ export const UnitSymbol: React.FC<React.ComponentProps<typeof Typography>> =
73
+ styled(Typography)(({ theme }) => ({
74
+ fontSize: 18,
75
+ fontWeight: 600,
76
+ lineHeight: 1.4,
77
+ whiteSpace: 'nowrap',
78
+ color: theme.vars.palette.text.primary,
79
+ }))
80
+
81
+ /**
82
+ * Invert affordance anchored to the far right of the lead row. Styled as a
83
+ * pill to mirror the percentage chips, but always visible (no reveal-on-hover).
84
+ */
85
+ export const InvertChip: React.FC<React.ComponentProps<typeof ButtonBase>> =
86
+ styled(ButtonBase)(({ theme }) => ({
87
+ display: 'flex',
88
+ alignItems: 'center',
89
+ justifyContent: 'center',
90
+ gap: theme.spacing(0.5),
91
+ minWidth: 0,
92
+ padding: theme.spacing(0.5, 1.5),
93
+ borderRadius: '9999px',
94
+ fontSize: 12,
95
+ fontWeight: 700,
96
+ lineHeight: 1.3334,
97
+ color: theme.vars.palette.text.primary,
98
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.common.onBackground} 4%, transparent)`,
99
+ transition: 'background-color 200ms ease',
100
+ '&:hover': {
101
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.common.onBackground} 8%, transparent)`,
102
+ },
103
+ }))
@@ -0,0 +1,153 @@
1
+ import SwapHorizIcon from '@mui/icons-material/SwapHoriz'
2
+ import type { CardProps } from '@mui/material'
3
+ import { Skeleton } from '@mui/material'
4
+ import { type ChangeEvent, type JSX, useEffect, useRef, useState } from 'react'
5
+ import { useTranslation } from 'react-i18next'
6
+ import { useLimitMarketRate } from '../../hooks/useLimitMarketRate.js'
7
+ import { useLinkedLimitFields } from '../../hooks/useLinkedLimitFields.js'
8
+ import { useRoutes } from '../../hooks/useRoutes.js'
9
+ import { useToken } from '../../hooks/useToken.js'
10
+ import { FormKeyHelper } from '../../stores/form/types.js'
11
+ import { useFieldActions } from '../../stores/form/useFieldActions.js'
12
+ import { useFieldValues } from '../../stores/form/useFieldValues.js'
13
+ import { formatInputAmount } from '../../utils/format.js'
14
+ import { applyPriceOffset, invertPrice } from '../../utils/limitOrder.js'
15
+ import {
16
+ AmountCard,
17
+ amountHeight,
18
+ CardBodyRow,
19
+ CardHeaderRow,
20
+ LargeInput,
21
+ } from '../AmountInputCard/AmountInputCard.style.js'
22
+ import { CardTitle } from '../Card/CardTitle.js'
23
+ import { InvertChip } from './LimitPriceCard.style.js'
24
+ import { LimitPricePresets } from './LimitPricePresets.js'
25
+
26
+ const cleanDisplay = (value: string): string =>
27
+ value ? formatInputAmount(value, 8) : ''
28
+
29
+ export const LimitPriceCard: React.FC<CardProps> = (props): JSX.Element => {
30
+ const { t } = useTranslation()
31
+ const isEditingRef = useRef(false)
32
+ const [inputValue, setInputValue] = useState('')
33
+
34
+ const [
35
+ fromChainId,
36
+ fromTokenAddress,
37
+ toChainId,
38
+ toTokenAddress,
39
+ priceInverted,
40
+ ] = useFieldValues(
41
+ FormKeyHelper.getChainKey('from'),
42
+ FormKeyHelper.getTokenKey('from'),
43
+ FormKeyHelper.getChainKey('to'),
44
+ FormKeyHelper.getTokenKey('to'),
45
+ 'priceInverted'
46
+ )
47
+ const { token: fromToken } = useToken(fromChainId, fromTokenAddress)
48
+ const { token: toToken } = useToken(toChainId, toTokenAddress)
49
+
50
+ const { isFetching } = useRoutes()
51
+
52
+ // limitPrice is derived from the from/to amount fields, not stored.
53
+ const { limitPrice, setLimitPrice } = useLinkedLimitFields()
54
+ const { setFieldValue } = useFieldActions()
55
+
56
+ const hasTokens = Boolean(fromToken && toToken)
57
+ const showSkeleton = isFetching && !limitPrice
58
+ const leadToken = priceInverted ? toToken : fromToken
59
+ const unitToken = priceInverted ? fromToken : toToken
60
+
61
+ const marketCanonical = useLimitMarketRate()
62
+
63
+ // Canonical price is orientation-independent; flip only for display.
64
+ const displayPrice = priceInverted ? invertPrice(limitPrice) : limitPrice
65
+
66
+ // biome-ignore lint/correctness/useExhaustiveDependencies: pair fields are intentional re-run triggers, not read in the body
67
+ useEffect(() => {
68
+ setFieldValue('toAmount', '', { isDirty: false, isTouched: false })
69
+ setFieldValue('selectedRouteId', undefined)
70
+ }, [fromChainId, fromTokenAddress, toChainId, toTokenAddress])
71
+
72
+ // Keep the input in sync with derived/preset/invert updates, but never yank
73
+ // the value out from under an actively-typing user.
74
+ useEffect(() => {
75
+ if (!isEditingRef.current) {
76
+ setInputValue(cleanDisplay(displayPrice))
77
+ }
78
+ }, [displayPrice])
79
+
80
+ const handleChange = (
81
+ event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
82
+ ): void => {
83
+ isEditingRef.current = true
84
+ const formatted = formatInputAmount(event.target.value, 18, true)
85
+ setInputValue(formatted)
86
+ const numeric = Number(formatted)
87
+ if (!formatted || Number.isNaN(numeric) || numeric <= 0) {
88
+ setLimitPrice('')
89
+ return
90
+ }
91
+ // Canonical price is orientation-independent; un-flip the displayed value.
92
+ const canonical = priceInverted ? 1 / numeric : numeric
93
+ setLimitPrice(String(canonical))
94
+ }
95
+
96
+ const handleBlur = (): void => {
97
+ isEditingRef.current = false
98
+ setInputValue(cleanDisplay(displayPrice))
99
+ }
100
+
101
+ const handleInvert = (): void => {
102
+ isEditingRef.current = false
103
+ setFieldValue('priceInverted', !priceInverted)
104
+ }
105
+
106
+ // Apply a percentage offset to the canonical market rate (0 = at market).
107
+ const handlePreset = (pct: number): void => {
108
+ if (marketCanonical === undefined) {
109
+ return
110
+ }
111
+ isEditingRef.current = false
112
+ setLimitPrice(applyPriceOffset(String(marketCanonical), pct))
113
+ }
114
+
115
+ const label = hasTokens
116
+ ? t('limitOrder.priceLabel', { symbol: leadToken?.symbol })
117
+ : t('limitOrder.price')
118
+
119
+ return (
120
+ <AmountCard {...props} mask={false}>
121
+ <CardHeaderRow>
122
+ <CardTitle sx={{ padding: 0 }}>{label}</CardTitle>
123
+ {hasTokens ? (
124
+ <InvertChip onClick={handleInvert} aria-label="invert price">
125
+ {unitToken?.symbol}
126
+ <SwapHorizIcon sx={{ fontSize: 16 }} />
127
+ </InvertChip>
128
+ ) : null}
129
+ </CardHeaderRow>
130
+ <CardBodyRow>
131
+ {showSkeleton ? (
132
+ <Skeleton variant="rounded" height={amountHeight} sx={{ flex: 1 }} />
133
+ ) : (
134
+ <LargeInput
135
+ size="small"
136
+ autoComplete="off"
137
+ placeholder="0"
138
+ inputProps={{ inputMode: 'decimal' }}
139
+ onChange={handleChange}
140
+ onBlur={handleBlur}
141
+ value={inputValue}
142
+ name="limitPrice"
143
+ disabled={!hasTokens}
144
+ />
145
+ )}
146
+ </CardBodyRow>
147
+ <LimitPricePresets
148
+ onSelect={handlePreset}
149
+ disabled={!hasTokens || marketCanonical === undefined}
150
+ />
151
+ </AmountCard>
152
+ )
153
+ }
@@ -0,0 +1,139 @@
1
+ import { type ChangeEvent, type JSX, useEffect, useRef, useState } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { ChipContainer } from '../AmountInputCard/PercentageChips.style.js'
4
+ import {
5
+ EditablePriceChip,
6
+ PriceChip,
7
+ PriceChipInput,
8
+ } from './LimitPriceCard.style.js'
9
+
10
+ /** Quick-set presets, as percentage offsets from market (0 = at market). */
11
+ const PRESETS = [0, 1, 5, 10] as const
12
+
13
+ interface LimitPricePresetsProps {
14
+ /** Apply a percentage offset from the market rate (0 = at market). */
15
+ onSelect: (pct: number) => void
16
+ /** Disables the free-form custom pill (no tokens / no market rate yet). */
17
+ disabled: boolean
18
+ }
19
+
20
+ /**
21
+ * Row of quick-set price controls: fixed percentage presets plus a free-form
22
+ * "custom %" pill that collapses to a button until engaged. Owns its own
23
+ * custom-input state so the card only deals with applying an offset.
24
+ */
25
+ export const LimitPricePresets = ({
26
+ onSelect,
27
+ disabled,
28
+ }: LimitPricePresetsProps): JSX.Element => {
29
+ const { t } = useTranslation()
30
+ const customInputRef = useRef<HTMLInputElement>(null)
31
+ const [customPct, setCustomPct] = useState('')
32
+ const [customFocused, setCustomFocused] = useState(false)
33
+
34
+ // The custom pill renders as a plain button until engaged; when it flips into
35
+ // edit mode (focused), move the caret into the freshly-mounted input.
36
+ useEffect(() => {
37
+ if (customFocused) {
38
+ customInputRef.current?.focus()
39
+ }
40
+ }, [customFocused])
41
+
42
+ // Picking a fixed preset clears any custom value so the custom pill collapses
43
+ // back to its "Custom" button instead of showing a stale, no-longer-applied %.
44
+ const handlePresetClick = (pct: number): void => {
45
+ setCustomPct('')
46
+ setCustomFocused(false)
47
+ onSelect(pct)
48
+ }
49
+
50
+ // Free-form offset pill: apply the typed percentage live so the price input
51
+ // tracks the offset as the user types. Accepts an optional leading +/- sign
52
+ // so the price can be set above (premium) or below (discount) market.
53
+ const handleCustomChange = (event: ChangeEvent<HTMLInputElement>): void => {
54
+ const raw = event.target.value
55
+ if (raw && !/^[+-]?\d*\.?\d*$/.test(raw)) {
56
+ return
57
+ }
58
+ setCustomPct(raw)
59
+ const numeric = Number(raw)
60
+ // Bare '', '+', '-', '.' etc. aren't applicable numbers yet — Number() maps
61
+ // the signs/dot to NaN, and '' to 0, so guard both.
62
+ if (raw === '' || Number.isNaN(numeric)) {
63
+ return
64
+ }
65
+ onSelect(numeric)
66
+ }
67
+
68
+ const handleCustomKeyDown = (
69
+ event: React.KeyboardEvent<HTMLInputElement>
70
+ ): void => {
71
+ if (event.key === 'Enter') {
72
+ event.currentTarget.blur()
73
+ }
74
+ }
75
+
76
+ // On blur, surface an explicit sign: a bare positive entry like "10" reads as
77
+ // "+10" so the direction (above/below market) is unambiguous once collapsed.
78
+ const handleCustomBlur = (): void => {
79
+ setCustomFocused(false)
80
+ setCustomPct((prev) => {
81
+ if (!prev || prev.startsWith('+') || prev.startsWith('-')) {
82
+ return prev
83
+ }
84
+ return Number.isNaN(Number(prev)) ? prev : `+${prev}`
85
+ })
86
+ }
87
+
88
+ // Collapse to a plain "Custom" label only while empty and unfocused; once the
89
+ // user engages (focus) or has typed a value, reveal the `+__%` affixes.
90
+ const showCustomLabel = customPct === '' && !customFocused
91
+
92
+ return (
93
+ <ChipContainer sx={{ marginTop: 0.5, flexWrap: 'wrap' }}>
94
+ {PRESETS.map((pct) => (
95
+ <PriceChip key={pct} onClick={() => handlePresetClick(pct)}>
96
+ {pct === 0 ? t('limitOrder.market') : `+${pct}%`}
97
+ </PriceChip>
98
+ ))}
99
+ {showCustomLabel ? (
100
+ <PriceChip disabled={disabled} onClick={() => setCustomFocused(true)}>
101
+ {t('limitOrder.custom')}
102
+ </PriceChip>
103
+ ) : (
104
+ <EditablePriceChip
105
+ aria-disabled={disabled}
106
+ onClick={(event) => {
107
+ // `aria-disabled` only announces the state; enforce it so a
108
+ // programmatic click can't apply an offset while disabled.
109
+ if (disabled) {
110
+ return
111
+ }
112
+ const input = event.currentTarget.querySelector('input')
113
+ input?.focus()
114
+ const numeric = Number(customPct)
115
+ if (customPct && !Number.isNaN(numeric)) {
116
+ onSelect(numeric)
117
+ }
118
+ }}
119
+ >
120
+ <PriceChipInput
121
+ ref={customInputRef}
122
+ inputMode="decimal"
123
+ autoComplete="off"
124
+ placeholder="0"
125
+ aria-label={t('limitOrder.custom')}
126
+ value={customPct}
127
+ onChange={handleCustomChange}
128
+ onKeyDown={handleCustomKeyDown}
129
+ onFocus={() => setCustomFocused(true)}
130
+ onBlur={handleCustomBlur}
131
+ disabled={disabled}
132
+ style={{ width: `${Math.max(customPct.length, 1)}ch` }}
133
+ />
134
+ %
135
+ </EditablePriceChip>
136
+ )}
137
+ </ChipContainer>
138
+ )
139
+ }
@@ -0,0 +1,113 @@
1
+ import type { Route } from '@lifi/sdk'
2
+ import LocalGasStationRounded from '@mui/icons-material/LocalGasStationRounded'
3
+ import { Box, Skeleton, Typography } from '@mui/material'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js'
6
+ import { SmallAvatar } from '../Avatar/SmallAvatar.js'
7
+ import type { CardProps } from '../Card/Card.js'
8
+ import { Card } from '../Card/Card.js'
9
+ import { IconTypography } from '../IconTypography.js'
10
+
11
+ interface RouteProviderCardProps {
12
+ route: Route
13
+ active?: boolean
14
+ }
15
+
16
+ export const RouteProviderCard: React.FC<
17
+ RouteProviderCardProps & Omit<CardProps, 'variant'>
18
+ > = ({ route, active, onClick, ...other }) => {
19
+ const { t } = useTranslation()
20
+
21
+ const provider = route.steps[0]?.toolDetails
22
+ const { gasCostUSD } = getAccumulatedFeeCostsBreakdown(route)
23
+
24
+ return (
25
+ <Card
26
+ type={active ? 'selected' : 'default'}
27
+ selectionColor="secondary"
28
+ onClick={onClick}
29
+ sx={{ py: 1.5, px: 2, ...(onClick ? { cursor: 'pointer' } : {}) }}
30
+ {...other}
31
+ >
32
+ <Box
33
+ sx={{
34
+ display: 'flex',
35
+ alignItems: 'center',
36
+ justifyContent: 'space-between',
37
+ gap: 1,
38
+ }}
39
+ >
40
+ <Box
41
+ sx={{
42
+ display: 'flex',
43
+ alignItems: 'center',
44
+ gap: 1,
45
+ minWidth: 0,
46
+ }}
47
+ >
48
+ <SmallAvatar src={provider?.logoURI} alt={provider?.name} size={20}>
49
+ {provider?.name?.[0]}
50
+ </SmallAvatar>
51
+ <Typography
52
+ noWrap
53
+ sx={{
54
+ fontSize: 14,
55
+ fontWeight: 600,
56
+ color: 'text.primary',
57
+ }}
58
+ >
59
+ {provider?.name}
60
+ </Typography>
61
+ </Box>
62
+ <Box
63
+ sx={{
64
+ display: 'flex',
65
+ alignItems: 'center',
66
+ }}
67
+ >
68
+ <IconTypography component="span" sx={{ mr: 0.5, fontSize: 16 }}>
69
+ <LocalGasStationRounded fontSize="inherit" />
70
+ </IconTypography>
71
+ <Typography
72
+ data-value={gasCostUSD}
73
+ sx={{
74
+ fontSize: 14,
75
+ color: 'text.primary',
76
+ fontWeight: 600,
77
+ lineHeight: 1,
78
+ }}
79
+ >
80
+ {!gasCostUSD
81
+ ? t('main.fees.free')
82
+ : t('format.currency', {
83
+ value: gasCostUSD,
84
+ })}
85
+ </Typography>
86
+ </Box>
87
+ </Box>
88
+ </Card>
89
+ )
90
+ }
91
+
92
+ export const RouteProviderCardSkeleton: React.FC<Omit<CardProps, 'variant'>> = (
93
+ props
94
+ ) => {
95
+ return (
96
+ <Card sx={{ py: 1.5, px: 2 }} {...props}>
97
+ <Box
98
+ sx={{
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ justifyContent: 'space-between',
102
+ gap: 1,
103
+ }}
104
+ >
105
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
106
+ <Skeleton variant="circular" width={20} height={20} />
107
+ <Skeleton variant="text" width={80} height={20} />
108
+ </Box>
109
+ <Skeleton variant="text" width={56} height={20} />
110
+ </Box>
111
+ </Card>
112
+ )
113
+ }
@@ -0,0 +1,64 @@
1
+ import { Stack } from '@mui/material'
2
+ import { useRoutes } from '../../hooks/useRoutes.js'
3
+ import { useToAddressRequirements } from '../../hooks/useToAddressRequirements.js'
4
+ import { useFieldActions } from '../../stores/form/useFieldActions.js'
5
+ import { useFieldValues } from '../../stores/form/useFieldValues.js'
6
+ import type { CardProps } from '../Card/Card.js'
7
+ import { RouteNotFoundCard } from '../RouteCard/RouteNotFoundCard.js'
8
+ import {
9
+ RouteProviderCard,
10
+ RouteProviderCardSkeleton,
11
+ } from '../RouteCard/RouteProviderCard.js'
12
+
13
+ export const LimitOrderRoutes: React.FC<CardProps> = (props) => {
14
+ const { routes, isLoading, isFetching, isFetched } = useRoutes()
15
+ const [toAddress, selectedRouteId] = useFieldValues(
16
+ 'toAddress',
17
+ 'selectedRouteId'
18
+ )
19
+ const { setFieldValue } = useFieldActions()
20
+ const { requiredToAddress } = useToAddressRequirements()
21
+
22
+ const currentRoute = routes?.[0]
23
+
24
+ if (!currentRoute && !isLoading && !isFetching && !isFetched) {
25
+ return null
26
+ }
27
+
28
+ const toAddressUnsatisfied = currentRoute && requiredToAddress && !toAddress
29
+ const allowInteraction = !toAddressUnsatisfied
30
+
31
+ const activeRouteId =
32
+ routes?.find((route) => route.id === selectedRouteId)?.id ??
33
+ currentRoute?.id
34
+
35
+ return (
36
+ <Stack
37
+ direction="column"
38
+ spacing={1}
39
+ sx={props.sx}
40
+ className={props.className}
41
+ >
42
+ {isLoading && !currentRoute ? (
43
+ Array.from({ length: 2 }).map((_, index) => (
44
+ <RouteProviderCardSkeleton key={index} />
45
+ ))
46
+ ) : !currentRoute ? (
47
+ <RouteNotFoundCard />
48
+ ) : (
49
+ routes?.map((route) => (
50
+ <RouteProviderCard
51
+ key={route.id}
52
+ route={route}
53
+ active={route.id === activeRouteId}
54
+ onClick={
55
+ allowInteraction
56
+ ? () => setFieldValue('selectedRouteId', route.id)
57
+ : undefined
58
+ }
59
+ />
60
+ ))
61
+ )}
62
+ </Stack>
63
+ )
64
+ }
@@ -16,7 +16,7 @@ import { RouteNotFoundCard } from '../RouteCard/RouteNotFoundCard.js'
16
16
  export const Routes: React.FC<CardProps> = (props) => {
17
17
  const { t } = useTranslation()
18
18
  const navigate = useNavigate()
19
- const { mode, modeOptions, showSingleRoute } = useWidgetConfig()
19
+ const { mode, modeOptions, showSingleRoute, defaultUI } = useWidgetConfig()
20
20
  const {
21
21
  routes,
22
22
  isLoading,
@@ -51,7 +51,8 @@ export const Routes: React.FC<CardProps> = (props) => {
51
51
 
52
52
  return (
53
53
  <Card {...props}>
54
- <CardTitle>{title}</CardTitle>
54
+ {/* The cards layout's Receive card already carries this header. */}
55
+ {defaultUI?.layout === 'cards' ? null : <CardTitle>{title}</CardTitle>}
55
56
  <ProgressToNextUpdate
56
57
  updatedAt={dataUpdatedAt || Date.now()}
57
58
  timeToUpdate={refetchTime}
@@ -0,0 +1,41 @@
1
+ import { Box, ButtonBase, styled, Typography } from '@mui/material'
2
+ import type React from 'react'
3
+
4
+ export const SettingsPanelContainer: React.FC<
5
+ React.ComponentProps<typeof Box>
6
+ > = styled(Box)(({ theme }) => ({
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ gap: theme.spacing(1),
10
+ }))
11
+
12
+ export const SettingsPanelButton: React.FC<
13
+ React.ComponentProps<typeof ButtonBase>
14
+ > = styled(ButtonBase)(({ theme }) => ({
15
+ display: 'flex',
16
+ width: '100%',
17
+ justifyContent: 'space-between',
18
+ alignItems: 'center',
19
+ gap: theme.spacing(1),
20
+ padding: theme.spacing(2),
21
+ borderRadius: theme.vars.shape.borderRadius,
22
+ }))
23
+
24
+ export const SettingsPanelTitle: React.FC<
25
+ React.ComponentProps<typeof Typography>
26
+ > = styled(Typography)(({ theme }) => ({
27
+ fontSize: 14,
28
+ lineHeight: 1,
29
+ fontWeight: 700,
30
+ color: theme.vars.palette.text.primary,
31
+ }))
32
+
33
+ export const SettingsPanelValue: React.FC<
34
+ React.ComponentProps<typeof Typography>
35
+ > = styled(Typography)(({ theme }) => ({
36
+ fontSize: 14,
37
+ lineHeight: 1,
38
+ fontWeight: 500,
39
+ color: theme.vars.palette.text.secondary,
40
+ whiteSpace: 'nowrap',
41
+ }))