feat: trade range input implementation (#287)
This commit is contained in:
parent
0123685f79
commit
c0d62cd8f0
@ -30,8 +30,8 @@ const mockedDepositedVault: DepositedVault = {
|
|||||||
},
|
},
|
||||||
cap: {
|
cap: {
|
||||||
denom: 'mock',
|
denom: 'mock',
|
||||||
max: 10,
|
max: BN(10),
|
||||||
used: 1,
|
used: BN(1),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
src/components/Icons/VerticalThreeLine.svg
Normal file
3
src/components/Icons/VerticalThreeLine.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 1 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<line x1="0.5" y1="0.5" x2="0.5" y2="7.5" stroke="currentColor" stroke-linecap="round" stroke-dasharray="2 2"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 190 B |
@ -41,4 +41,5 @@ export { default as Subtract } from 'components/Icons/Subtract.svg'
|
|||||||
export { default as SwapIcon } from 'components/Icons/SwapIcon.svg'
|
export { default as SwapIcon } from 'components/Icons/SwapIcon.svg'
|
||||||
export { default as TrashBin } from 'components/Icons/TrashBin.svg'
|
export { default as TrashBin } from 'components/Icons/TrashBin.svg'
|
||||||
export { default as Wallet } from 'components/Icons/Wallet.svg'
|
export { default as Wallet } from 'components/Icons/Wallet.svg'
|
||||||
|
export { default as VerticalThreeLine } from 'components/Icons/VerticalThreeLine.svg'
|
||||||
// @endindex
|
// @endindex
|
||||||
|
69
src/components/RangeInput/InputOverlay.tsx
Normal file
69
src/components/RangeInput/InputOverlay.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { VerticalThreeLine } from 'components/Icons'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: number
|
||||||
|
marginThreshold?: number
|
||||||
|
max: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOverlay({ max, value, marginThreshold }: Props) {
|
||||||
|
// 33 is the thumb width
|
||||||
|
const thumbPosPercent = 100 / (max / value)
|
||||||
|
const thumbPadRight = (thumbPosPercent / 100) * 33
|
||||||
|
const markPosPercent = 100 / (max / (marginThreshold ?? 1))
|
||||||
|
const markPadRight = (markPosPercent / 100) * 33
|
||||||
|
const hasPastMarginThreshold = marginThreshold ? value >= marginThreshold : undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={className.marksWrapper}
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(270deg, rgba(255, 97, 141, 0.89) 0%, rgba(66, 58, 70, 0.05) 100%)',
|
||||||
|
backgroundSize: `${thumbPosPercent}%`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from(Array(9).keys()).map((i) => (
|
||||||
|
<div key={`mark-${i}`} className={className.mark} />
|
||||||
|
))}
|
||||||
|
{marginThreshold && (
|
||||||
|
<div
|
||||||
|
key='margin-mark'
|
||||||
|
className={className.marginMarkContainer}
|
||||||
|
style={{ left: `calc(${markPosPercent}% - ${markPadRight}px)` }}
|
||||||
|
>
|
||||||
|
<div className={className.marginMark} />
|
||||||
|
<div className={className.marginMarkOverlay}>
|
||||||
|
<VerticalThreeLine className='h-2 w-[1px]' />
|
||||||
|
<div className={!hasPastMarginThreshold ? 'opacity-50' : 'opacity-100'}>Margin</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={className.inputThumbOverlay}
|
||||||
|
style={{ left: `calc(${thumbPosPercent}% - ${thumbPadRight}px)` }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = {
|
||||||
|
inputThumbOverlay: `
|
||||||
|
w-[33px] h-4 absolute text-[10px] top-[5px]
|
||||||
|
pointer-events-none text-center font-bold
|
||||||
|
bg-pink shadow-md border-b-0 border-1
|
||||||
|
border-solid rounded-sm backdrop-blur-sm border-white/10
|
||||||
|
`,
|
||||||
|
mark: `w-1 h-1 bg-black rounded-full bg-opacity-30`,
|
||||||
|
marginMark: `w-1 h-1 bg-white rounded-full`,
|
||||||
|
marginMarkContainer: 'absolute w-[33px] flex justify-center',
|
||||||
|
marginMarkOverlay: 'absolute top-2.5 flex-col text-xs w-[33px] items-center flex',
|
||||||
|
marksWrapper: `absolute flex-1 flex w-full justify-evenly bg-no-repeat
|
||||||
|
top-[8.5px] pointer-events-none pt-[2.5px] pb-[2.5px] rounded-lg`,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InputOverlay
|
80
src/components/RangeInput/index.tsx
Normal file
80
src/components/RangeInput/index.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { ChangeEvent, useCallback } from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import InputOverlay from 'components/RangeInput/InputOverlay'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: number
|
||||||
|
onChange: (value: number) => void
|
||||||
|
wrapperClassName?: string
|
||||||
|
disabled?: boolean
|
||||||
|
max: number
|
||||||
|
marginThreshold?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function RangeInput(props: Props) {
|
||||||
|
const { value, max, onChange, wrapperClassName, disabled, marginThreshold } = props
|
||||||
|
|
||||||
|
const handleOnChange = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(parseInt(event.target.value))
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(className.containerDefault, wrapperClassName, {
|
||||||
|
[className.disabled]: !disabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={className.inputWrapper}>
|
||||||
|
<input
|
||||||
|
className={className.input}
|
||||||
|
type='range'
|
||||||
|
value={value}
|
||||||
|
max={max}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
<InputOverlay max={max} marginThreshold={marginThreshold} value={value} />
|
||||||
|
</div>
|
||||||
|
<div className={className.legendWrapper}>
|
||||||
|
<span>0</span>
|
||||||
|
<span>{max}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = {
|
||||||
|
containerDefault: 'relative min-h-3 w-full transition-opacity',
|
||||||
|
disabled: 'pointer-events-none opacity-50',
|
||||||
|
legendWrapper: 'flex w-full justify-between text-xs text-opacity-50 text-white font-bold',
|
||||||
|
inputWrapper: 'relative h-[30px]',
|
||||||
|
input: `
|
||||||
|
relative w-full appearance-none bg-transparent cursor-pointer
|
||||||
|
|
||||||
|
[&::-webkit-slider-runnable-track]:bg-white
|
||||||
|
[&::-webkit-slider-runnable-track]:bg-opacity-20
|
||||||
|
[&::-webkit-slider-runnable-track]:h-[9px]
|
||||||
|
[&::-webkit-slider-runnable-track]:rounded-lg
|
||||||
|
|
||||||
|
[&::-moz-range-track]:bg-white
|
||||||
|
[&::-moz-range-track]:bg-opacity-20
|
||||||
|
[&::-moz-range-track]:h-1
|
||||||
|
[&::-moz-range-track]:pb-[5px]
|
||||||
|
[&::-moz-range-track]:rounded-lg
|
||||||
|
|
||||||
|
[&::-webkit-slider-thumb]:appearance-none
|
||||||
|
[&::-webkit-slider-thumb]:-mt-1
|
||||||
|
[&::-webkit-slider-thumb]:w-[33px]
|
||||||
|
[&::-webkit-slider-thumb]:h-4
|
||||||
|
|
||||||
|
[&::-moz-range-thumb]:appearance-none
|
||||||
|
[&::-moz-range-thumb]:opacity-0
|
||||||
|
[&::-moz-range-thumb]:w-[33px]
|
||||||
|
[&::-moz-range-thumb]:h-4
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RangeInput
|
@ -1,11 +1,12 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import Loading from 'components/Loading'
|
import Loading from 'components/Loading'
|
||||||
import Text from 'components/Text'
|
import Text from 'components/Text'
|
||||||
import Divider from 'components/Divider'
|
import Divider from 'components/Divider'
|
||||||
|
import RangeInput from 'components/RangeInput'
|
||||||
import AssetSelector from './AssetSelector/AssetSelector'
|
import AssetSelector from 'components/Trade/TradeModule/AssetSelector/AssetSelector'
|
||||||
|
|
||||||
function Content() {
|
function Content() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@ -25,6 +26,8 @@ function Fallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TradeModule() {
|
export default function TradeModule() {
|
||||||
|
const [value, setValue] = useState(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -35,6 +38,10 @@ export default function TradeModule() {
|
|||||||
>
|
>
|
||||||
<AssetSelector />
|
<AssetSelector />
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
<div className='p-4'>
|
||||||
|
<RangeInput max={4000} marginThreshold={2222} value={value} onChange={setValue} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@ module.exports = {
|
|||||||
warning: '#F79009',
|
warning: '#F79009',
|
||||||
'warning-bg': '#FEC84B',
|
'warning-bg': '#FEC84B',
|
||||||
white: '#FFF',
|
white: '#FFF',
|
||||||
|
pink: '#de587f',
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
Loading…
Reference in New Issue
Block a user