LP TERMINAL — terminal-style LP frontend for Robinhood Chain

Static SPA over UP33 (ve(3,3)) + the official Uniswap v2/v3 deployments:
browse and add liquidity across all three, manage CL/v2 positions, one-token
ZAP adds, aggregator vs native swaps, and LP-based limit orders. Ships with a
zero-dependency pool indexer for uniswap discovery.

Browser-wallet signing only; no key material anywhere.
This commit is contained in:
labrinyang
2026-07-17 19:00:10 +08:00
commit 4e317eff6d
77 changed files with 22678 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { formatUnits } from 'viem'
import { fmtAmount } from '../lib/format'
export function Btn(props: {
onClick?: () => void
disabled?: boolean
tone?: 'default' | 'danger' | 'ghost'
big?: boolean
busy?: boolean
title?: string
children: ReactNode
}) {
const cls = ['btn', props.tone === 'danger' ? 'danger' : '', props.tone === 'ghost' ? 'ghost' : '', 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
}) {
return (
<input
className="input"
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 = e.target.value.replace(',', '.')
if (v === '' || /^\d*\.?\d*$/.test(v)) props.onChange(v)
}}
/>
)
}