feat: cyberpunk HUD charts + drop "test" from faucet copy
All checks were successful
deploy / deploy (push) Successful in 1m41s
All checks were successful
deploy / deploy (push) Successful in 1m41s
- SlippageCurve: segmented depth-charge meter with a volt redline at the 2%
slippage limit, replacing the flat bar.
- DepthHistory: area trace with survey grid, peak marker and a pulsing live
'now' node.
- HealthRadial: ticked radial dial with reticle crosshairs instead of the
plain arc.
- Faucet/action copy: "Get test hUSD" -> "Get hUSD", "Get test {SYM}" ->
"Get {SYM}", "test hUSD minted" -> "hUSD minted".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9f00841623
commit
a0da0712c7
5 changed files with 167 additions and 85 deletions
|
|
@ -196,9 +196,9 @@ export function DepositPanel({
|
|||
? `Enter ${symbol} amount`
|
||||
: "Enter amount"
|
||||
: needsUsdFaucet
|
||||
? "Get test hUSD"
|
||||
? "Get hUSD"
|
||||
: needsStockFaucet
|
||||
? `Get test ${symbol}`
|
||||
? `Get ${symbol}`
|
||||
: needsUsdApprove
|
||||
? "Approve hUSD"
|
||||
: needsStockApprove
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
/**
|
||||
* 30-day depth sparkline. Historical points are estimated: a deterministic,
|
||||
* 30-day depth trace. Historical points are estimated: a deterministic,
|
||||
* per-market random walk that converges to the current live depth figure, so
|
||||
* the line is stable across renders and always ends at truth.
|
||||
* the line is stable across renders and always ends at truth. Rendered as a
|
||||
* cyberpunk area trace with grid, glow and hi/now field markers.
|
||||
*/
|
||||
const W = 220;
|
||||
const H = 48;
|
||||
const W = 240;
|
||||
const H = 58;
|
||||
const DAYS = 30;
|
||||
const TOP = 7;
|
||||
const BOT = H - 8;
|
||||
|
||||
function seeded(i: number, salt: number) {
|
||||
const x = Math.sin(i * 91.7 + salt * 47.3) * 24634.5453;
|
||||
|
|
@ -34,40 +37,50 @@ export function DepthHistory({
|
|||
const max = Math.max(...values);
|
||||
const min = Math.min(...values);
|
||||
const span = max - min || 1;
|
||||
const pts = values
|
||||
.map((v, i) => {
|
||||
const x = (i / (DAYS - 1)) * W;
|
||||
const y = H - 6 - ((v - min) / span) * (H - 12);
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
})
|
||||
.join(" ");
|
||||
const xAt = (i: number) => (i / (DAYS - 1)) * W;
|
||||
const yAt = (v: number) => BOT - ((v - min) / span) * (BOT - TOP);
|
||||
|
||||
const pts = values.map((v, i) => `${xAt(i).toFixed(1)},${yAt(v).toFixed(1)}`).join(" ");
|
||||
const idxMax = values.indexOf(max);
|
||||
const gid = `dh-${salt}`;
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${W} ${H}`}
|
||||
className="h-12 w-full"
|
||||
className="h-14 w-full"
|
||||
aria-hidden
|
||||
style={{ filter: "drop-shadow(0 0 4px rgba(200, 255, 0, 0.45))" }}
|
||||
style={{ filter: "drop-shadow(0 0 3px rgba(200, 255, 0, 0.4))" }}
|
||||
>
|
||||
<polyline
|
||||
points={pts}
|
||||
fill="none"
|
||||
stroke="#c8ff00"
|
||||
strokeWidth="1.5"
|
||||
strokeOpacity="0.85"
|
||||
/>
|
||||
<polygon
|
||||
points={`0,${H} ${pts} ${W},${H}`}
|
||||
fill="#c8ff00"
|
||||
opacity="0.045"
|
||||
/>
|
||||
<line x1="0" y1={H - 0.5} x2={W} y2={H - 0.5} stroke="#2a2a2a" strokeWidth="1" />
|
||||
<circle
|
||||
cx={W}
|
||||
cy={H - 6 - ((values[DAYS - 1] - min) / span) * (H - 12)}
|
||||
r="2.5"
|
||||
fill="#c8ff00"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#c8ff00" stopOpacity="0.22" />
|
||||
<stop offset="100%" stopColor="#c8ff00" stopOpacity="0.015" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* survey grid */}
|
||||
{[0.25, 0.5, 0.75].map((g) => (
|
||||
<line key={g} x1="0" y1={TOP + g * (BOT - TOP)} x2={W} y2={TOP + g * (BOT - TOP)} stroke="#1c1c1c" strokeWidth="1" />
|
||||
))}
|
||||
{[0.25, 0.5, 0.75].map((g) => (
|
||||
<line key={g} x1={g * W} y1={TOP} x2={g * W} y2={BOT} stroke="#1c1c1c" strokeWidth="1" />
|
||||
))}
|
||||
|
||||
<polygon points={`0,${BOT} ${pts} ${W},${BOT}`} fill={`url(#${gid})`} />
|
||||
<polyline points={pts} fill="none" stroke="#c8ff00" strokeWidth="1.5" strokeOpacity="0.9" />
|
||||
|
||||
{/* peak marker */}
|
||||
<circle cx={xAt(idxMax)} cy={yAt(max)} r="2" fill="none" stroke="#e9ffb3" strokeWidth="1" />
|
||||
|
||||
{/* baseline */}
|
||||
<line x1="0" y1={BOT + 0.5} x2={W} y2={BOT + 0.5} stroke="#2a2a2a" strokeWidth="1" />
|
||||
|
||||
{/* live 'now' node */}
|
||||
<line x1={W - 0.5} y1={TOP} x2={W - 0.5} y2={BOT} stroke="#c8ff00" strokeOpacity="0.35" strokeWidth="1" strokeDasharray="2 3" />
|
||||
<circle cx={W} cy={yAt(values[DAYS - 1])} r="2.6" fill="#c8ff00">
|
||||
<animate attributeName="r" values="2.6;4;2.6" dur="2.4s" repeatCount="indefinite" />
|
||||
<animate attributeName="fill-opacity" values="1;0.55;1" dur="2.4s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,70 @@
|
|||
import { healthLabel } from "@/lib/math";
|
||||
|
||||
/** SVG radial gauge for the Market Health Score. */
|
||||
const TICKS = 34; // segments around the 270° dial
|
||||
const START = 135; // degrees, y-down; gap sits at the bottom
|
||||
const SWEEP = 270;
|
||||
const CX = 40;
|
||||
const CY = 40;
|
||||
|
||||
function pt(r: number, deg: number): [number, number] {
|
||||
const a = (deg * Math.PI) / 180;
|
||||
return [CX + r * Math.cos(a), CY + r * Math.sin(a)];
|
||||
}
|
||||
|
||||
/** Ticked radial dial for the Market Health Score. */
|
||||
export function HealthRadial({ score }: { score: number }) {
|
||||
const { label, tone } = healthLabel(score);
|
||||
const color = tone === "mint" ? "#c8ff00" : tone === "violet" ? "#8a5cff" : "#ff3b00";
|
||||
const r = 34;
|
||||
const c = 2 * Math.PI * r;
|
||||
const filled = (score / 100) * c * 0.75; // 270 degree arc
|
||||
const lit = Math.round((Math.max(0, Math.min(score, 100)) / 100) * TICKS);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<svg viewBox="0 0 80 80" className="h-16 w-16" aria-hidden>
|
||||
<g transform="rotate(135 40 40)">
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke="#2a2a2a"
|
||||
strokeWidth="6"
|
||||
strokeDasharray={`${c * 0.75} ${c}`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="6"
|
||||
strokeDasharray={`${filled} ${c}`}
|
||||
strokeLinecap="butt"
|
||||
style={{ filter: `drop-shadow(0 0 4px ${color})` }}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dasharray"
|
||||
from={`0 ${c}`}
|
||||
to={`${filled} ${c}`}
|
||||
dur="0.9s"
|
||||
fill="freeze"
|
||||
calcMode="spline"
|
||||
keySplines="0.25 0.1 0.25 1"
|
||||
/>
|
||||
</circle>
|
||||
{/* outer reticle ring */}
|
||||
<circle cx={CX} cy={CY} r="37" fill="none" stroke="#1c1c1c" strokeWidth="1" />
|
||||
|
||||
{/* dial ticks */}
|
||||
<g style={{ filter: `drop-shadow(0 0 2.5px ${color})` }}>
|
||||
{Array.from({ length: TICKS }, (_, i) => {
|
||||
const deg = START + (i / (TICKS - 1)) * SWEEP;
|
||||
const on = i < lit;
|
||||
const [x1, y1] = pt(24, deg);
|
||||
const [x2, y2] = pt(on ? 33 : 30, deg);
|
||||
return (
|
||||
<line
|
||||
key={i}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke={on ? color : "#2a2a2a"}
|
||||
strokeWidth={on ? 2 : 1.4}
|
||||
strokeLinecap="butt"
|
||||
>
|
||||
{on && (
|
||||
<animate
|
||||
attributeName="stroke-opacity"
|
||||
from="0"
|
||||
to="1"
|
||||
dur="0.4s"
|
||||
begin={`${i * 0.02}s`}
|
||||
fill="freeze"
|
||||
/>
|
||||
)}
|
||||
</line>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* crosshair ticks at 12 / 3 / 9 o'clock */}
|
||||
{[270, 0, 180].map((deg) => {
|
||||
const [x1, y1] = pt(15, deg);
|
||||
const [x2, y2] = pt(18, deg);
|
||||
return <line key={deg} x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3d3d3d" strokeWidth="1" />;
|
||||
})}
|
||||
|
||||
<text
|
||||
x="40"
|
||||
x={CX}
|
||||
y="44"
|
||||
textAnchor="middle"
|
||||
fill="#f2f2f2"
|
||||
|
|
|
|||
|
|
@ -2,26 +2,76 @@ import { slippageFor } from "@/lib/math";
|
|||
import { fmtUsd } from "@/lib/format";
|
||||
|
||||
const SIZES = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000];
|
||||
const CELLS = 34; // meter segments per row
|
||||
const STEP = 3; // cell pitch in viewBox units
|
||||
|
||||
/** Horizontal bars: estimated slippage per trade size at current depth. */
|
||||
/**
|
||||
* Segmented depth-charge meter: estimated slippage per trade size at current
|
||||
* depth, drawn as a lit-cell HUD bar with a volt redline at the 2% limit.
|
||||
*/
|
||||
export function SlippageCurve({ liquidityUsd }: { liquidityUsd: number }) {
|
||||
const max = slippageFor(SIZES[SIZES.length - 1], liquidityUsd);
|
||||
// shared 2% danger limit, mapped onto the meter (off-scale in deep books)
|
||||
const limitX = max > 0 ? Math.min((0.02 / max), 1) * CELLS * STEP : CELLS * STEP;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{SIZES.map((size) => {
|
||||
<div className="space-y-[7px]">
|
||||
{SIZES.map((size, row) => {
|
||||
const s = slippageFor(size, liquidityUsd);
|
||||
const width = max > 0 ? (s / max) * 100 : 0;
|
||||
const frac = max > 0 ? s / max : 0;
|
||||
const hot = s > 0.02;
|
||||
const on = Math.max(1, Math.round(frac * CELLS));
|
||||
const color = hot ? "#ff3b00" : "#c8ff00";
|
||||
return (
|
||||
<div key={size} className="flex items-center gap-3 text-xs">
|
||||
<span className="num w-14 shrink-0 text-right text-fog">{fmtUsd(size)}</span>
|
||||
<svg className="h-3 flex-1" preserveAspectRatio="none" viewBox="0 0 100 10" aria-hidden>
|
||||
<rect x="0" y="3" width="100" height="4" fill="#2a2a2a" />
|
||||
<rect x="0" y="3" width={Math.max(width, 1)} height="4" fill={hot ? "#ff3b00" : "#c8ff00"}>
|
||||
<animate attributeName="width" from="0" to={Math.max(width, 1)} dur="0.7s" fill="freeze" />
|
||||
</rect>
|
||||
<div key={size} className="flex items-center gap-3">
|
||||
<span className="num w-14 shrink-0 text-right font-mono text-[10px] text-fog">
|
||||
{fmtUsd(size)}
|
||||
</span>
|
||||
<svg
|
||||
className="h-3.5 flex-1"
|
||||
preserveAspectRatio="none"
|
||||
viewBox={`0 0 ${CELLS * STEP} 12`}
|
||||
aria-hidden
|
||||
style={{ filter: `drop-shadow(0 0 2.5px ${color})` }}
|
||||
>
|
||||
{Array.from({ length: CELLS }, (_, i) => {
|
||||
const lit = i < on;
|
||||
return (
|
||||
<rect
|
||||
key={i}
|
||||
x={i * STEP}
|
||||
y="2"
|
||||
width={STEP - 0.9}
|
||||
height="8"
|
||||
fill={lit ? color : "#2a2a2a"}
|
||||
opacity={lit ? 1 : 0.55}
|
||||
>
|
||||
{lit && (
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
from="0"
|
||||
to="1"
|
||||
dur="0.4s"
|
||||
begin={`${row * 0.05 + i * 0.011}s`}
|
||||
fill="freeze"
|
||||
/>
|
||||
)}
|
||||
</rect>
|
||||
);
|
||||
})}
|
||||
{limitX < CELLS * STEP && (
|
||||
<line
|
||||
x1={limitX}
|
||||
y1="0"
|
||||
x2={limitX}
|
||||
y2="12"
|
||||
stroke="#5200ff"
|
||||
strokeWidth="0.9"
|
||||
strokeDasharray="1.6 1.6"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<span className={`num w-14 shrink-0 ${hot ? "text-danger" : "text-mint"}`}>
|
||||
<span className={`num w-14 shrink-0 font-mono text-xs ${hot ? "text-danger" : "text-mint"}`}>
|
||||
{(s * 100).toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export default function AppBoard() {
|
|||
if (!isConnected) return openConnectModal?.();
|
||||
if (amount === 0n) return say("enter an amount to deposit");
|
||||
if (needsFaucet) {
|
||||
lastAction.current = "test hUSD minted to wallet";
|
||||
lastAction.current = "hUSD minted to wallet";
|
||||
writeContract({
|
||||
address: deployment.usd,
|
||||
abi: mockERC20Abi,
|
||||
|
|
@ -189,7 +189,7 @@ export default function AppBoard() {
|
|||
: amount === 0n
|
||||
? "Enter amount"
|
||||
: needsFaucet
|
||||
? "Get test hUSD"
|
||||
? "Get hUSD"
|
||||
: needsApprove
|
||||
? "Approve hUSD"
|
||||
: "Deposit hUSD";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue