Files
lp-terminal_github/src/components/ui.tsx
T
labrinyang bca538e7e3 feat: SHEEP CHOICE routing, cross-chain deposits, and any-token ZAP
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>
2026-07-23 00:01:52 +08:00

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)
}}
/>
)
}