mirror of
https://github.com/labrinyang/lp-terminal.git
synced 2026-07-27 13:17:43 +00:00
bca538e7e3
One commit because the pieces do not compile apart: the shared copy, the tab shell and the data layer all changed together, and splitting them further would mean inventing intermediate states that never existed. SHEEP CHOICE — the terminal's own swap. Quotes come from a solver that splits one trade across several pools instead of forcing it down a single path, and returns a ready-to-sign Settler transaction; the UI draws the split leg by leg and scores every venue against one shared fee-free baseline, so the card that says it pays most actually does. The Kyber transaction path is gone — Kyber is read-only USD valuation now, and there is no Kyber calldata to sign. BRIDGE — deposits from other chains over Relay, Across and the native portal, priced side by side and sorted by what actually reaches you. No fee on any of them. In-flight transfers get a countdown and survive a reload. ZAP — add liquidity holding neither side of the pair; whatever needs swapping is done in the same flow, with an optional stake-after step. Uniswap V2 liquidity now shows up under POSITIONS. Swaps in flight are persisted, so a refresh mid-swap no longer loses the transaction. Pair labels copy their token and pool addresses, and jump to DexScreener. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
2.9 KiB
TypeScript
102 lines
2.9 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { formatUnits } from 'viem'
|
|
import { fmtAmount, sanitizeAmountInput } from '../lib/format'
|
|
|
|
export function Btn(props: {
|
|
onClick?: () => void
|
|
disabled?: boolean
|
|
tone?: 'default' | 'danger' | 'ghost' | 'amber'
|
|
big?: boolean
|
|
busy?: boolean
|
|
title?: string
|
|
children: ReactNode
|
|
}) {
|
|
const cls = ['btn', props.tone === 'default' ? '' : props.tone, props.big ? 'big' : '']
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
return (
|
|
<button className={cls} onClick={props.onClick} disabled={props.disabled || props.busy} title={props.title}>
|
|
{props.busy ? <span className="spin">▮</span> : props.children}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
export function Badge(props: { tone?: 'green' | 'amber' | 'red' | 'cyan' | 'dim'; children: ReactNode }) {
|
|
return <span className={`badge ${props.tone ?? ''}`}>{props.children}</span>
|
|
}
|
|
|
|
export function Stat(props: { k: string; v: ReactNode; sub?: ReactNode }) {
|
|
return (
|
|
<div className="stat">
|
|
<div className="k">{props.k}</div>
|
|
<div className="v">{props.v}</div>
|
|
{props.sub !== undefined && <div className="sub">{props.sub}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** amount input row: label + NumInput + wallet balance + MAX chip */
|
|
export function AmountRow(props: {
|
|
sym: string
|
|
value: string
|
|
onChange: (v: string) => void
|
|
bal?: bigint
|
|
dec: number
|
|
onMax: (v: string) => void
|
|
disabled?: boolean
|
|
note?: string
|
|
}) {
|
|
const { t } = useTranslation()
|
|
return (
|
|
<div className="form-row">
|
|
<span className="lbl">{props.sym}</span>
|
|
<NumInput value={props.value} onChange={props.onChange} disabled={props.disabled} width={220} />
|
|
{props.bal !== undefined && (
|
|
<>
|
|
<span className="dim mono-sm">
|
|
{t('common.bal')} {fmtAmount(props.bal, props.dec)}
|
|
</span>
|
|
<button
|
|
className="chip"
|
|
onClick={() => props.onMax(formatUnits(props.bal!, props.dec))}
|
|
disabled={props.disabled}
|
|
>
|
|
{t('common.max')}
|
|
</button>
|
|
</>
|
|
)}
|
|
{props.note && <span className="amber mono-sm">{props.note}</span>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** numeric text input that only accepts decimal strings */
|
|
export function NumInput(props: {
|
|
value: string
|
|
onChange: (v: string) => void
|
|
placeholder?: string
|
|
disabled?: boolean
|
|
width?: number
|
|
invalid?: boolean
|
|
/** clamp typed fraction digits (token precision); 18 = EVM max */
|
|
decimals?: number
|
|
}) {
|
|
return (
|
|
<input
|
|
className={`input${props.invalid ? ' invalid' : ''}`}
|
|
style={props.width ? { width: props.width } : undefined}
|
|
inputMode="decimal"
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
placeholder={props.placeholder ?? '0.0'}
|
|
value={props.value}
|
|
disabled={props.disabled}
|
|
onChange={(e) => {
|
|
const v = sanitizeAmountInput(e.target.value, props.decimals ?? 18)
|
|
if (v !== null) props.onChange(v)
|
|
}}
|
|
/>
|
|
)
|
|
}
|