@lifi/widget 4.1.0 → 4.3.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 (210) 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/useFilteredByTokenBalances.js +4 -1
  72. package/dist/esm/hooks/useFilteredByTokenBalances.js.map +1 -1
  73. package/dist/esm/hooks/useLimitMarketRate.js +24 -0
  74. package/dist/esm/hooks/useLimitMarketRate.js.map +1 -0
  75. package/dist/esm/hooks/useLinkedLimitFields.js +49 -0
  76. package/dist/esm/hooks/useLinkedLimitFields.js.map +1 -0
  77. package/dist/esm/hooks/useRoutes.js +23 -6
  78. package/dist/esm/hooks/useRoutes.js.map +1 -1
  79. package/dist/esm/hooks/useTokens.js +16 -14
  80. package/dist/esm/hooks/useTokens.js.map +1 -1
  81. package/dist/esm/hooks/useWideVariant.js +5 -2
  82. package/dist/esm/hooks/useWideVariant.js.map +1 -1
  83. package/dist/esm/i18n/en.json +22 -0
  84. package/dist/esm/index.d.ts +2 -2
  85. package/dist/esm/pages/MainPage/LimitPageContent.js +23 -0
  86. package/dist/esm/pages/MainPage/LimitPageContent.js.map +1 -0
  87. package/dist/esm/pages/MainPage/MainPage.js +19 -22
  88. package/dist/esm/pages/MainPage/MainPage.js.map +1 -1
  89. package/dist/esm/pages/MainPage/MainPageActions.js +28 -0
  90. package/dist/esm/pages/MainPage/MainPageActions.js.map +1 -0
  91. package/dist/esm/pages/MainPage/ReviewButton.js +9 -2
  92. package/dist/esm/pages/MainPage/ReviewButton.js.map +1 -1
  93. package/dist/esm/pages/RoutePriorityPage.js +48 -0
  94. package/dist/esm/pages/RoutePriorityPage.js.map +1 -0
  95. package/dist/esm/pages/SettingsPage/BridgeAndExchangeSettings.js +2 -2
  96. package/dist/esm/pages/SettingsPage/BridgeAndExchangeSettings.js.map +1 -1
  97. package/dist/esm/pages/SettingsPage/RoutePrioritySettings.js +12 -27
  98. package/dist/esm/pages/SettingsPage/RoutePrioritySettings.js.map +1 -1
  99. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippagePage.js +133 -0
  100. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippagePage.js.map +1 -0
  101. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.js +14 -72
  102. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.js.map +1 -1
  103. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.style.js +26 -14
  104. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.style.js.map +1 -1
  105. package/dist/esm/pages/SettingsPage/ThemeSettings.js +4 -0
  106. package/dist/esm/pages/SettingsPage/ThemeSettings.js.map +1 -1
  107. package/dist/esm/pages/TransactionPage/TransactionReview.js +4 -1
  108. package/dist/esm/pages/TransactionPage/TransactionReview.js.map +1 -1
  109. package/dist/esm/providers/I18nProvider/I18nProvider.js +1 -1
  110. package/dist/esm/providers/I18nProvider/enResource.d.ts +1 -1
  111. package/dist/esm/providers/I18nProvider/enResource.js +1 -1
  112. package/dist/esm/providers/I18nProvider/types.d.ts +2 -2
  113. package/dist/esm/providers/SDKClientProvider.js +25 -24
  114. package/dist/esm/providers/SDKClientProvider.js.map +1 -1
  115. package/dist/esm/providers/WidgetProvider/WidgetProvider.js +22 -21
  116. package/dist/esm/providers/WidgetProvider/WidgetProvider.js.map +1 -1
  117. package/dist/esm/stores/form/URLSearchParamsBuilder.js +14 -3
  118. package/dist/esm/stores/form/URLSearchParamsBuilder.js.map +1 -1
  119. package/dist/esm/stores/form/createFormStore.js +6 -2
  120. package/dist/esm/stores/form/createFormStore.js.map +1 -1
  121. package/dist/esm/stores/form/getDefaultValuesFromQueryString.js +3 -0
  122. package/dist/esm/stores/form/getDefaultValuesFromQueryString.js.map +1 -1
  123. package/dist/esm/stores/form/types.d.ts +9 -1
  124. package/dist/esm/stores/form/types.d.ts.map +1 -1
  125. package/dist/esm/stores/form/types.js.map +1 -1
  126. package/dist/esm/stores/form/useClearAmountFields.js +1 -0
  127. package/dist/esm/stores/form/useClearAmountFields.js.map +1 -1
  128. package/dist/esm/stores/navigationTabs/useNavigationTabsStore.js +4 -2
  129. package/dist/esm/stores/navigationTabs/useNavigationTabsStore.js.map +1 -1
  130. package/dist/esm/stores/navigationTabs/utils.js +38 -8
  131. package/dist/esm/stores/navigationTabs/utils.js.map +1 -1
  132. package/dist/esm/types/events.d.ts +10 -1
  133. package/dist/esm/types/events.d.ts.map +1 -1
  134. package/dist/esm/types/events.js +2 -0
  135. package/dist/esm/types/events.js.map +1 -1
  136. package/dist/esm/types/widget.d.ts +10 -0
  137. package/dist/esm/types/widget.d.ts.map +1 -1
  138. package/dist/esm/types/widget.js.map +1 -1
  139. package/dist/esm/utils/format.d.ts +1 -1
  140. package/dist/esm/utils/format.d.ts.map +1 -1
  141. package/dist/esm/utils/format.js +2 -2
  142. package/dist/esm/utils/format.js.map +1 -1
  143. package/dist/esm/utils/limitOrder.js +61 -0
  144. package/dist/esm/utils/limitOrder.js.map +1 -0
  145. package/dist/esm/utils/navigationRoutes.d.ts +2 -0
  146. package/dist/esm/utils/navigationRoutes.d.ts.map +1 -1
  147. package/dist/esm/utils/navigationRoutes.js +6 -0
  148. package/dist/esm/utils/navigationRoutes.js.map +1 -1
  149. package/dist/esm/utils/token.js +38 -3
  150. package/dist/esm/utils/token.js.map +1 -1
  151. package/package.json +11 -11
  152. package/src/AppDefault.tsx +16 -0
  153. package/src/components/AmountInput/AmountInputEndAdornment.style.ts +1 -1
  154. package/src/components/AmountInput/AmountInputEndAdornment.tsx +5 -2
  155. package/src/components/AmountInputCard/AmountInputCard.style.tsx +157 -0
  156. package/src/components/AmountInputCard/AmountInputCardPair.tsx +25 -0
  157. package/src/components/AmountInputCard/BalanceDisplay.style.tsx +9 -0
  158. package/src/components/AmountInputCard/BalanceDisplay.tsx +46 -0
  159. package/src/components/AmountInputCard/FiatValueToggle.tsx +55 -0
  160. package/src/components/AmountInputCard/PercentageChips.style.tsx +60 -0
  161. package/src/components/AmountInputCard/PercentageChips.tsx +100 -0
  162. package/src/components/AmountInputCard/ReceiveAmountCard.tsx +219 -0
  163. package/src/components/AmountInputCard/SendAmountCard.tsx +179 -0
  164. package/src/components/Avatar/Avatar.tsx +8 -3
  165. package/src/components/Header/HeaderNavigationTabs.tsx +19 -2
  166. package/src/components/LimitOrderSettings/LimitOrderSettings.style.tsx +69 -0
  167. package/src/components/LimitOrderSettings/LimitOrderSettings.tsx +124 -0
  168. package/src/components/LimitPriceCard/LimitPriceCard.style.tsx +103 -0
  169. package/src/components/LimitPriceCard/LimitPriceCard.tsx +153 -0
  170. package/src/components/LimitPriceCard/LimitPricePresets.tsx +139 -0
  171. package/src/components/RouteCard/RouteProviderCard.tsx +113 -0
  172. package/src/components/Routes/LimitOrderRoutes.tsx +64 -0
  173. package/src/components/Routes/Routes.tsx +3 -2
  174. package/src/components/SettingsPanel/SettingsPanel.style.tsx +41 -0
  175. package/src/components/SettingsPanel/SettingsPanel.tsx +73 -0
  176. package/src/components/SettingsPanel/utils.ts +27 -0
  177. package/src/components/SwapButton/SwapButton.style.tsx +28 -0
  178. package/src/components/SwapButton/SwapButton.tsx +71 -0
  179. package/src/components/TokenPillButton/TokenPillButton.style.tsx +50 -0
  180. package/src/components/TokenPillButton/TokenPillButton.tsx +81 -0
  181. package/src/config/version.ts +1 -1
  182. package/src/hooks/useFilteredByTokenBalances.ts +7 -2
  183. package/src/hooks/useLimitMarketRate.ts +39 -0
  184. package/src/hooks/useLinkedLimitFields.ts +74 -0
  185. package/src/hooks/useRoutes.ts +45 -4
  186. package/src/hooks/useWideVariant.ts +8 -2
  187. package/src/pages/MainPage/LimitPageContent.tsx +18 -0
  188. package/src/pages/MainPage/MainPage.tsx +43 -30
  189. package/src/pages/MainPage/MainPageActions.tsx +35 -0
  190. package/src/pages/MainPage/ReviewButton.tsx +13 -1
  191. package/src/pages/RoutePriorityPage.tsx +55 -0
  192. package/src/pages/SettingsPage/RoutePrioritySettings.tsx +14 -38
  193. package/src/pages/SettingsPage/SlippageSettings/SlippagePage.tsx +176 -0
  194. package/src/pages/SettingsPage/SlippageSettings/SlippageSettings.style.tsx +32 -25
  195. package/src/pages/SettingsPage/SlippageSettings/SlippageSettings.tsx +14 -128
  196. package/src/pages/SettingsPage/ThemeSettings.tsx +4 -0
  197. package/src/stores/form/URLSearchParamsBuilder.tsx +25 -3
  198. package/src/stores/form/createFormStore.ts +7 -3
  199. package/src/stores/form/getDefaultValuesFromQueryString.ts +11 -0
  200. package/src/stores/form/types.ts +14 -1
  201. package/src/stores/form/useClearAmountFields.ts +1 -0
  202. package/src/stores/navigationTabs/types.ts +6 -0
  203. package/src/stores/navigationTabs/useNavigationTabsStore.tsx +8 -3
  204. package/src/stores/navigationTabs/utils.ts +67 -5
  205. package/src/types/events.ts +12 -0
  206. package/src/types/widget.ts +10 -0
  207. package/src/utils/format.ts +6 -2
  208. package/src/utils/limitOrder.ts +88 -0
  209. package/src/utils/navigationRoutes.ts +6 -0
  210. package/src/utils/token.ts +71 -20
@@ -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
+ }
@@ -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
+ }