diff --git a/src/components/CopyableAddress.tsx b/src/components/CopyableAddress.tsx index eaee6e9..e36b576 100644 --- a/src/components/CopyableAddress.tsx +++ b/src/components/CopyableAddress.tsx @@ -18,14 +18,33 @@ const CopyableAddress: React.FC = ({ }) => { const [copied, setCopied] = useState(false); - const handleClick = () => { + const handleClick = (e: React.MouseEvent) => { + // Prevent enclosing / RouterLink from navigating when the address is + // nested inside a clickable card. + e.preventDefault(); + e.stopPropagation(); navigator.clipboard.writeText(address); setCopied(true); setTimeout(() => setCopied(false), 1500); }; return ( - + = { + done: '●', // ● + failed: '✗', // ✗ + active: '○', // ○ + pending: '○', // ○ +}; + +export const TimelineStep: React.FC<{ + state: TimelineStepState; + label: string; + detail?: React.ReactNode; + /** + * Override the default glyph (e.g. '⏱' for a Timeout row). + */ + glyph?: string; + /** + * Override the state-derived color. Use sparingly — reserved for + * terminal "finality" rows that need to pop (e.g. green ✓ on a + * completed swap, red ✗ on a timed-out swap). + */ + color?: string; + /** + * Default 80 — bump if labels in your timeline are wordier. + */ + labelMinWidth?: number; +}> = ({ state, label, detail, glyph, color, labelMinWidth = 80 }) => { + const theme = useTheme(); + const stepColor = + color ?? + (state === 'done' + ? theme.palette.status.completed + : state === 'failed' + ? theme.palette.status.timedOut + : state === 'active' + ? theme.palette.status.active + : theme.palette.text.secondary); + return ( + + + {glyph ?? STATE_GLYPH[state]} + + + {label} + + {detail != null && detail !== '' && ( + + {detail} + + )} + + ); +}; + +export const SectionTitle: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( + + {children} + +); diff --git a/src/components/dashboard/EventFeed.tsx b/src/components/dashboard/EventFeed.tsx index ffbaa42..590a6ff 100644 --- a/src/components/dashboard/EventFeed.tsx +++ b/src/components/dashboard/EventFeed.tsx @@ -90,17 +90,12 @@ const EventFeed: React.FC = () => { - - What is this? - - - A real-time stream of events emitted by the smart contract and - the underlying chain — exchange lifecycle, network collateral - changes, validator votes, and reservations. Newest events appear - at the top. - - + + A real-time stream of events emitted by the smart contract and the + underlying chain — exchange lifecycle, network collateral changes, + validator votes, and reservations. Newest events appear at the + top. + } arrow placement="right" @@ -133,7 +128,7 @@ const EventFeed: React.FC = () => { sx={{ p: 1.5, borderRadius: 0, - backgroundColor: 'background.paper', + backgroundColor: 'surface.light', border: '1px solid', borderColor: 'divider', transition: 'border-color 0.2s', diff --git a/src/components/dashboard/MinerRatesTable.tsx b/src/components/dashboard/MinerRatesTable.tsx index a0b693d..1ecc7ab 100644 --- a/src/components/dashboard/MinerRatesTable.tsx +++ b/src/components/dashboard/MinerRatesTable.tsx @@ -366,19 +366,16 @@ const MinerRatesTable: React.FC = () => { - - What is this? - - + + Live exchange rates quoted across the Allways network. Each row represents an active network node; both directions (BTC→TAO and TAO→BTC) are shown when quoted, with the spread between them being the network's margin. - - + + Sort by rate or capacity to find the best counterparty. - + } arrow diff --git a/src/components/dashboard/ReservationsTracker.tsx b/src/components/dashboard/ReservationsTracker.tsx index bf8bdd7..c50e4bb 100644 --- a/src/components/dashboard/ReservationsTracker.tsx +++ b/src/components/dashboard/ReservationsTracker.tsx @@ -23,6 +23,7 @@ import { formatAmount, formatTimeUntilBlock, } from '../../utils/format'; +import { ReservationsTrackerSkeleton } from './Skeletons'; const STATUS_COLORS = (palette: { status: { active: string; fulfilled: string; timedOut: string }; @@ -40,11 +41,15 @@ const ReservationsTracker: React.FC = () => { const { data: miners } = useMiners(); const { data: chainState } = useChainState(); const { data: protocol } = useProtocolConstants(); + const [searchAddr, setSearchAddr] = useState(''); const reservations = data ?? []; const colors = STATUS_COLORS(theme.palette); const currentBlock = chainState?.currentBlock ?? 0; - const [searchAddr, setSearchAddr] = useState(''); + if (isLoading && !data) { + return ; + } + const trimmed = searchAddr.trim().toLowerCase(); const filtered = trimmed ? reservations.filter((r: Reservation) => @@ -126,19 +131,7 @@ const ReservationsTracker: React.FC = () => { - {isLoading && ( - - Loading… - - )} - - {!isLoading && filtered.length === 0 && ( + {filtered.length === 0 && ( ( ); +export const ReservationsTrackerSkeleton: React.FC = () => ( + + + + + + + {[0, 1, 2].map((i) => ( + + + + + + + + ))} + + +); + export const SwapTrackerSkeleton: React.FC = () => ( = ({ sx={{ p: 2.5, borderRadius: 0, - backgroundColor: 'background.paper', + backgroundColor: 'surface.light', border: '1px solid', borderColor: 'divider', textAlign: 'center', diff --git a/src/components/dashboard/SwapTracker.tsx b/src/components/dashboard/SwapTracker.tsx index a6db9a0..96e9023 100644 --- a/src/components/dashboard/SwapTracker.tsx +++ b/src/components/dashboard/SwapTracker.tsx @@ -37,11 +37,13 @@ const getStatusColor = ( }; }, ): string => { + // Terminal states pop with semantic color — completion green / timeout red. + // In-flight states keep their muted blue tints. const map: Record = { ACTIVE: palette.status.active, FULFILLED: palette.status.fulfilled, - COMPLETED: palette.status.completed, - TIMED_OUT: palette.status.timedOut, + COMPLETED: 'var(--color-success)', + TIMED_OUT: 'var(--color-danger)', }; return map[status] ?? palette.status.active; }; @@ -102,17 +104,12 @@ const SwapTracker: React.FC = () => { - - What is this? - - - Every transaction on the network in chronological order, with - its current status and progress through the lifecycle: Initiated - → Fulfilled → Completed (or Timed Out). Click a row to see the - full timeline. - - + + Every transaction on the network in chronological order, with its + current status and progress through the lifecycle: Initiated → + Fulfilled → Completed (or Timed Out). Click a row to see the full + timeline. + } arrow placement="right" @@ -145,7 +142,7 @@ const SwapTracker: React.FC = () => { p: 4, textAlign: 'center', borderRadius: 0, - backgroundColor: 'background.paper', + backgroundColor: 'surface.light', border: '1px solid', borderColor: 'divider', }} @@ -187,7 +184,7 @@ const SwapTracker: React.FC = () => { sx={{ p: 2, borderRadius: 0, - backgroundColor: 'background.paper', + backgroundColor: 'surface.light', border: '1px solid', borderColor: 'divider', textDecoration: 'none', @@ -250,7 +247,9 @@ const SwapTracker: React.FC = () => { borderRadius: 0, backgroundColor: theme.palette.border.light, '& .MuiLinearProgress-bar': { - backgroundColor: color, + // Bar fill stays neutral regardless of status; + // status text carries the green/red signal. + backgroundColor: theme.palette.border.medium, borderRadius: 0, }, }} diff --git a/src/components/index.ts b/src/components/index.ts index dc139fa..d634208 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,6 +5,8 @@ export { default as Card } from './Card'; export { default as HoverCard } from './HoverCard'; export { default as LabelValue } from './LabelValue'; export { default as PageWrapper } from './PageWrapper'; +export { TimelineStep, SectionTitle } from './Timeline'; +export type { TimelineStepState } from './Timeline'; export { SEO } from './SEO'; export type { SEOProps } from './SEO'; export * from './dashboard'; diff --git a/src/index.css b/src/index.css index bc30b6c..2435f2b 100644 --- a/src/index.css +++ b/src/index.css @@ -10,8 +10,10 @@ /* ── Semantic / theme-aware tokens (light defaults) ── */ --color-bg: var(--color-offwhite); --color-surface: var(--color-white); - --color-surface-light: var(--color-gray); - --color-surface-elevated: var(--color-gray); + /* Card surfaces sit between bg (#fbfbfb) and gray (#eef0f3) — light enough + to feel clean against the page, still distinct from pure white. */ + --color-surface-light: #f4f6f8; + --color-surface-elevated: #f4f6f8; --color-text-primary: var(--color-woodsmoke); --color-text-secondary: color-mix( in srgb, @@ -23,8 +25,12 @@ var(--color-woodsmoke) 40%, transparent ); - --color-border: var(--color-gray); - --color-border-light: var(--color-gray); + /* Borders match MUI's default outlined-input edge weight (≈ + rgba(0,0,0,0.23)) so cards read at the same visual weight as + the dashboard search box — light enough to feel quiet, dark + enough to define the card. */ + --color-border: #d1d5db; + --color-border-light: #d1d5db; --color-border-medium: color-mix( in srgb, var(--color-woodsmoke) 25%, @@ -46,6 +52,15 @@ ); --color-status-completed: var(--color-text-secondary); --color-status-timed-out: var(--color-text-muted); + /* Final-finality semantic colors — applied only on the swap detail + timeline's terminal Completed/Timeout rows and on the dashboard + transaction list's terminal status text. Intentionally not wired into + `--color-status-completed`/`--color-status-timed-out` so reservation + timelines and other "done" states stay neutral. + Tuned to read as semantic green/red without being shouty against + small uppercase mono text. */ + --color-success: #15803d; + --color-danger: #b91c1c; --color-status-collateral: color-mix( in srgb, var(--color-secondary) 75%, @@ -78,14 +93,17 @@ [data-theme='dark'] { --color-bg: var(--color-woodsmoke); --color-surface: var(--color-woodsmoke); + /* Pull surfaces closer to bg so cards feel integrated with the page, + not stamped on top — borders carry the structure. Mirrors the + light-mode move from BRAND.gray (#eef0f3) to a softer #f4f6f8. */ --color-surface-light: color-mix( in srgb, - var(--color-woodsmoke) 92%, + var(--color-woodsmoke) 96%, var(--color-white) ); --color-surface-elevated: color-mix( in srgb, - var(--color-woodsmoke) 86%, + var(--color-woodsmoke) 92%, var(--color-white) ); --color-text-primary: var(--color-white); diff --git a/src/pages/ReservationDetailPage.tsx b/src/pages/ReservationDetailPage.tsx index 29c5a45..912f162 100644 --- a/src/pages/ReservationDetailPage.tsx +++ b/src/pages/ReservationDetailPage.tsx @@ -10,10 +10,6 @@ import { } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked'; -import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; -import CancelIcon from '@mui/icons-material/Cancel'; import { useChainState, useMinerByHotkey, @@ -23,13 +19,20 @@ import { import type { Miner } from '../api/models'; import { FONTS } from '../theme'; import { applyFee, formatAmount, formatTimeUntilBlock } from '../utils/format'; -import { BlockIndicator, Card, LabelValue, PageWrapper } from '../components'; +import { + BlockIndicator, + Card, + CopyableAddress, + LabelValue, + PageWrapper, + SectionTitle, + TimelineStep, + type TimelineStepState, +} from '../components'; import ExtensionChip, { deriveReservationExtensionStatus, } from '../components/ExtensionChip'; -type StageState = 'done' | 'current' | 'awaiting' | 'pending' | 'failed'; - const minerSendToAddress = ( fromChain: string | null, miner: Miner | undefined, @@ -40,6 +43,9 @@ const minerSendToAddress = ( return null; }; +const fmtBlock = (b: string | number): string => + Number(b).toLocaleString('en-US'); + const relativeTime = (iso: string): string => { const ms = Date.now() - new Date(iso).getTime(); if (ms < 60_000) return 'just now'; @@ -97,18 +103,17 @@ const ReservationDetailPage: React.FC = () => { const isInitiated = r.status === 'INITIATED'; const isTerminal = r.status === 'EXPIRED' || r.status === 'CANCELLED'; - const reservedStage: StageState = isTerminal ? 'failed' : 'done'; + const reservedStage: TimelineStepState = isTerminal ? 'failed' : 'done'; // Funds + confirmation collapse into one step: send → detect → confirm. - // 'awaiting' = user owes the send; 'current' = detected, waiting on conf - // blocks; 'done' = confirmed (which is what INITIATED requires). - const sendConfirmStage: StageState = isTerminal + // 'active' covers both "user still needs to send" and "send detected, + // awaiting confirmations" — the visual signal is identical and the + // step `detail` text disambiguates. + const sendConfirmStage: TimelineStepState = isTerminal ? 'failed' : isInitiated ? 'done' - : fundsSeen - ? 'current' - : 'awaiting'; - const initiatedStage: StageState = isTerminal + : 'active'; + const initiatedStage: TimelineStepState = isTerminal ? 'failed' : isInitiated ? 'done' @@ -163,7 +168,7 @@ const ReservationDetailPage: React.FC = () => { alignItems="center" justifyContent="space-between" spacing={2} - sx={{ mb: 3 }} + sx={{ mb: 1 }} > { + {/* Trade summary — the lead, not a card */} + + + {sourceLine}{' '} + + → + {' '} + {destLine} + + {isInitiated && r.swapId ? ( + + Funded · Swap #{r.swapId} + + ) : ( + + {r.status === 'ACTIVE' && !fundsSeen + ? `Awaiting funds${ + currentBlock > 0 + ? ` · ${formatTimeUntilBlock(parseInt(r.reservedUntilBlock, 10), currentBlock)} remaining` + : '' + }` + : r.status === 'ACTIVE' && fundsSeen + ? 'Funds detected · confirming' + : r.status === 'EXPIRED' + ? 'Expired before funds were sent' + : r.status === 'CANCELLED' + ? 'Cancelled before initiating' + : ''} + + )} + + {/* Lifecycle stepper */} - - Timeline + + - - - {/* Action / status guidance */} - - {r.status === 'ACTIVE' && !fundsSeen && ( + {/* Action / status guidance — only when ACTIVE; header subline covers other states */} + {r.status === 'ACTIVE' && !fundsSeen && ( + { color: 'text.primary', }} > - If not yet sent, send {sourceLine} from the - source address to the miner before block{' '} - #{r.reservedUntilBlock} - {currentBlock > 0 && ( - <> - {' '} - ( - {formatTimeUntilBlock( - parseInt(r.reservedUntilBlock, 10), - currentBlock, - )}{' '} - remaining) - - )} - . + Send {sourceLine} from the source address before + block {fmtBlock(r.reservedUntilBlock)}. {sendToAddr && ( @@ -264,124 +322,77 @@ const ReservationDetailPage: React.FC = () => { color: 'text.secondary', }} > - Already sent? Validators usually pick it up within a block — this - page will update automatically. - - - Validators reject any source tx whose sender doesn't match — keep - this address consistent. - - - )} - - {r.status === 'ACTIVE' && fundsSeen && ( - - - Validators detected the source transaction. - - - Awaiting source-tx confirmations to verify legitimacy before - initiating the swap. The reservation may extend up to 2× while - validators wait for chain finality. - - - )} - - {r.status === 'INITIATED' && r.swapId && ( - - - Funds received and confirmed. This reservation initiated swap # - {r.swapId}. - - - View swap #{r.swapId} + Validators usually pick up the send within a block. The sender + address must match — mismatched txs are rejected. - )} + + )} - {r.status === 'EXPIRED' && ( + {r.status === 'ACTIVE' && fundsSeen && ( + - Reservation expired before funds were sent. The miner is now free - for other users — start a new reservation to swap. + Awaiting source-tx confirmations to verify legitimacy before + initiating the swap. The reservation may extend up to 2× while + validators wait for chain finality. - )} - - {r.status === 'CANCELLED' && ( - - Reservation was cancelled before initiating a swap. - - )} - + + )} {/* Details */} - - - + + + Miner + + {miner?.uid !== undefined && ( + + UID {miner.uid} + + )} + {miner?.uid !== undefined && ( + + · + + )} + + 0 - ? `Block #${r.reservedUntilBlock} (${formatTimeUntilBlock(parseInt(r.reservedUntilBlock, 10), currentBlock)} remaining)` - : `Block #${r.reservedUntilBlock}` + ? `${fmtBlock(r.reservedAtBlock)} → ${fmtBlock(r.reservedUntilBlock)} (${formatTimeUntilBlock(parseInt(r.reservedUntilBlock, 10), currentBlock)} left)` + : `${fmtBlock(r.reservedAtBlock)} → ${fmtBlock(r.reservedUntilBlock)}` } /> {(extensionStatus.kind !== 'none' || r.extensionsUsed > 0) && ( @@ -403,14 +414,14 @@ const ReservationDetailPage: React.FC = () => { <> 0 - ? `Block #${extensionStatus.finalizableAt} (~${formatTimeUntilBlock(extensionStatus.finalizableAt, currentBlock)}) if uncontested` - : `Block #${extensionStatus.finalizableAt} if uncontested` + ? `Block ${fmtBlock(extensionStatus.finalizableAt)} (~${formatTimeUntilBlock(extensionStatus.finalizableAt, currentBlock)}) if uncontested` + : `Block ${fmtBlock(extensionStatus.finalizableAt)} if uncontested` } /> {extensionStatus.proposedBy && ( @@ -430,66 +441,4 @@ const ReservationDetailPage: React.FC = () => { ); }; -const Stage: React.FC<{ - state: StageState; - label: string; - detail: string; -}> = ({ state, label, detail }) => { - const theme = useTheme(); - const Icon = - state === 'done' - ? CheckCircleIcon - : state === 'current' - ? HourglassEmptyIcon - : state === 'failed' - ? CancelIcon - : RadioButtonUncheckedIcon; - const color = - state === 'done' - ? theme.palette.status.completed - : state === 'current' - ? theme.palette.status.active - : state === 'awaiting' - ? theme.palette.status.active - : state === 'failed' - ? theme.palette.status.timedOut - : theme.palette.text.disabled; - const labelColor = - state === 'pending' - ? theme.palette.text.secondary - : theme.palette.text.primary; - return ( - - - - - {label} - - - - {detail} - - - ); -}; - export default ReservationDetailPage; diff --git a/src/pages/SwapDetailPage.tsx b/src/pages/SwapDetailPage.tsx index bee1db7..1bca50b 100644 --- a/src/pages/SwapDetailPage.tsx +++ b/src/pages/SwapDetailPage.tsx @@ -17,7 +17,15 @@ import { } from '../api'; import { FONTS } from '../theme'; import CopyableAddress from '../components/CopyableAddress'; -import { BlockIndicator, Card, LabelValue, PageWrapper } from '../components'; +import { + BlockIndicator, + Card, + LabelValue, + PageWrapper, + SectionTitle, + TimelineStep, + type TimelineStepState, +} from '../components'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import { applyFee, @@ -32,7 +40,7 @@ import ExtensionChip, { deriveSwapExtensionStatus, } from '../components/ExtensionChip'; -type TimelineStep = { +type SwapStep = { label: string; block: string | null; timestamp: string | null; @@ -40,15 +48,20 @@ type TimelineStep = { failed: boolean; }; +const fmtBlock = (b: string | number): string => + Number(b).toLocaleString('en-US'); + const getStatusColor = ( status: string, palette: { status: Record }, ): string => { + // Terminal states pop with semantic color — completion green / timeout red. + // In-flight states keep their muted blue tints. const map: Record = { ACTIVE: palette.status.active, FULFILLED: palette.status.fulfilled, - COMPLETED: palette.status.completed, - TIMED_OUT: palette.status.timedOut, + COMPLETED: 'var(--color-success)', + TIMED_OUT: 'var(--color-danger)', }; return map[status] ?? palette.status.active; }; @@ -93,7 +106,7 @@ const SwapDetailPage: React.FC = () => { : undefined; const refundPending = refundEvent?.eventType === 'SlashPending'; - const steps: TimelineStep[] = [ + const steps: SwapStep[] = [ { label: 'Initiated', block: swap.initiatedBlock, @@ -191,140 +204,127 @@ const SwapDetailPage: React.FC = () => { )} - + {/* Trade summary — the lead, not a card */} + {(() => { + const sourceLine = + swap.sourceAmount && swap.sourceChain + ? formatAmount(swap.sourceAmount, swap.sourceChain) + : null; + const net = applyFee(swap.destAmount, protocol?.feeDivisor); + const destLine = + net && swap.destChain ? formatAmount(net, swap.destChain) : null; + const rate = formatRateLine( + swap.sourceAmount, + swap.sourceChain, + swap.destAmount, + swap.destChain, + ); + // One-sided headlines look awkward; only render when both legs known. + // Single amounts still appear per-leg in the Flow card below. + if (!sourceLine || !destLine) return null; + return ( + + + {sourceLine}{' '} + + → + {' '} + {destLine} + + {rate && ( + + {rate} + + )} + + ); + })()} + + {/* Status helper — skip COMPLETED (chip already says it) */} + {swap.status !== 'COMPLETED' && ( {swap.status === 'ACTIVE' && "Awaiting miner fulfillment — they're sending the destination funds now. Validators will mark it FULFILLED once the destination tx confirms."} {swap.status === 'FULFILLED' && 'Miner delivered the destination funds. Validators are voting to confirm on-chain — once quorum lands, the swap completes.'} - {swap.status === 'COMPLETED' && 'Exchange completed.'} {swap.status === 'TIMED_OUT' && (refundPending ? 'Miner did not deliver in time. Slash is pending — user must claim the refund on-chain with `alw claim`.' : "Miner did not deliver in time. The slashed collateral was paid directly to the user's address.")} - - - {/* Summary */} - {swap.sourceChain && swap.destChain && ( - - - {swap.sourceAmount && swap.sourceChain && ( - - )} - {swap.destAmount && - swap.destChain && - (() => { - const net = applyFee(swap.destAmount, protocol?.feeDivisor); - return net ? ( - - ) : null; - })()} - {(() => { - const rate = formatRateLine( - swap.sourceAmount, - swap.sourceChain, - swap.destAmount, - swap.destChain, - ); - return rate ? : null; - })()} - - )} {/* Timeline */} Timeline - {steps.map((step) => { - const stepColor = step.done - ? 'var(--color-status-completed)' - : step.failed - ? 'var(--color-status-timed-out)' - : 'text.secondary'; - return ( - - - {step.done ? '\u25CF' : step.failed ? '\u2717' : '\u25CB'} - - - {step.label} - - - {step.block ? `Block #${step.block}` : '\u2014'} - - - ); - })} - {/* Timeout line */} - {swap.timeoutBlock && ( - - - {isTimedOut ? '\u23F1' : '\u23F1'} - - - Timeout - - - Block #{swap.timeoutBlock} - {!isTimedOut && - swap.status !== 'COMPLETED' && - currentBlock > 0 && ( + {/* Hide Completed row on timed-out swaps \u2014 it never completed. + Hide Timeout row on completed swaps \u2014 it never fired. */} + {/* Hide Completed row on timed-out swaps \u2014 it never completed. + Hide Timeout row on completed swaps \u2014 it never fired. + Only the terminal row that actually fired carries semantic + color (green \u2713 for success, red \u2717 for timeout); other + "done" rows stay neutral so the eye lands on finality. */} + {steps + .filter((s) => !(isTimedOut && s.label === 'Completed')) + .map((step) => { + const stepState: TimelineStepState = step.done + ? 'done' + : step.failed + ? 'failed' + : 'pending'; + const isTerminalCompleted = + step.label === 'Completed' && step.done; + return ( + + ); + })} + {swap.timeoutBlock && swap.status !== 'COMPLETED' && ( + + Block {fmtBlock(swap.timeoutBlock)} + {!isTimedOut && currentBlock > 0 && ( <> {' '} ( @@ -335,8 +335,9 @@ const SwapDetailPage: React.FC = () => { remaining) )} - - + + } + /> )} {swap.timeoutBlock && !isTimedOut && swap.status !== 'COMPLETED' && ( { )} - {/* Transactions */} - {(swap.sourceTxHash || swap.destTxHash) && ( - - Transactions - - - - - - )} - - {/* Transaction flow */} + {/* Flow \u2014 sends and receives in one card, each with its own tx hash */} {(() => { // Resolve "from" / "to" addresses for each leg from the user's POV. // sourceChain === 'tao': user sends TAO from their hotkey → miner hotkey; @@ -494,37 +476,53 @@ const SwapDetailPage: React.FC = () => { netRecv && swap.destChain ? formatAmount(netRecv, swap.destChain) : null; + const hasSend = !!( + sentAmount || + sentFrom || + sentTo || + swap.sourceTxHash + ); + const hasRecv = !!(recvAmount || recvFrom || recvTo || swap.destTxHash); + if (!hasSend && !hasRecv) return null; return ( - <> - {(sentAmount || sentFrom || sentTo) && ( - - User sends + + + {hasSend && ( + + Sends + {swap.sourceChain + ? ` · ${swap.sourceChain.toUpperCase()}` + : ''} + {sentAmount && ( )} - {sentFrom && ( - + {sentFrom && } + {sentTo && } + {swap.sourceTxHash && ( + )} - {sentTo && } - - )} - {(recvAmount || recvFrom || recvTo) && ( - - User receives + )} + {hasRecv && ( + + Receives + {swap.destChain ? ` · ${swap.destChain.toUpperCase()}` : ''} + {recvAmount && ( )} - {recvFrom && ( - + {recvFrom && } + {recvTo && } + {swap.destTxHash && ( + )} - {recvTo && } - - )} - + )} + + ); })()} @@ -564,7 +562,7 @@ const SwapDetailPage: React.FC = () => { color: 'text.secondary', }} > - #{event.blockNumber} + {fmtBlock(event.blockNumber)} {event.taoAmount && ( { ); }; -/* ---- Shared sub-components ---- */ - -const SectionTitle: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => ( - - {children} - -); +/* ---- Page-local sub-components ---- */ const LabelAddr: React.FC<{ label: string; address: string }> = ({ label, diff --git a/src/theme.ts b/src/theme.ts index 01cac72..ae99111 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -24,13 +24,18 @@ const lightPalette = { primary: BRAND.primary, bg: BRAND.offwhite, surface: BRAND.white, - surfaceLight: BRAND.gray, - surfaceElevated: BRAND.gray, + // Mirror of --color-surface-light / --color-surface-elevated in index.css. + // Lighter than BRAND.gray so card surfaces don't share the border tone. + surfaceLight: '#f4f6f8', + surfaceElevated: '#f4f6f8', textPrimary: BRAND.woodsmoke, textSecondary: 'rgba(9, 11, 13, 0.6)', textMuted: 'rgba(9, 11, 13, 0.4)', - border: BRAND.gray, - borderLight: BRAND.gray, + // Mirror of --color-border / --color-border-light in index.css. + // Matches MUI's default outlined-input border weight so cards + // and the search field read at the same visual weight. + border: '#d1d5db', + borderLight: '#d1d5db', borderMedium: 'rgba(9, 11, 13, 0.25)', statusActive: 'var(--color-status-active)', statusFulfilled: 'var(--color-status-fulfilled)', @@ -45,13 +50,13 @@ const lightPalette = { // Dark surface tints are pre-computed equivalents of the index.css color-mix() // expressions so theme.palette.surface.* and var(--color-surface-*) resolve to -// the same color: 92% woodsmoke + 8% white = #1d1f20, 86% + 14% = #2b2d2f. +// the same color: 96% woodsmoke + 4% white = #131517, 92% + 8% = #1d1f20. const darkPalette = { ...lightPalette, bg: BRAND.woodsmoke, surface: BRAND.woodsmoke, - surfaceLight: '#1d1f20', - surfaceElevated: '#2b2d2f', + surfaceLight: '#131517', + surfaceElevated: '#1d1f20', textPrimary: BRAND.white, textSecondary: 'rgba(255, 255, 255, 0.6)', textMuted: 'rgba(255, 255, 255, 0.4)', @@ -276,16 +281,20 @@ export function createAppTheme(mode: ThemeMode): Theme { tooltip: { fontFamily: FONTS.body, fontSize: '0.75rem', + fontWeight: 400, + letterSpacing: 0, borderRadius: 0, - backgroundColor: v('surface-elevated'), + backgroundColor: v('surface'), color: v('text-primary'), - border: `1px solid ${v('border-light')}`, - padding: '8px 12px', + border: `1px solid ${v('border-medium')}`, + padding: '6px 10px', + boxShadow: 'none', }, arrow: { - color: v('surface-elevated'), + color: v('surface'), '&::before': { - border: `1px solid ${v('border-light')}`, + border: `1px solid ${v('border-medium')}`, + backgroundColor: v('surface'), }, }, },