From 9a37675f1463b0b96e8634f76fbf463781bb7d35 Mon Sep 17 00:00:00 2001 From: anderdc Date: Mon, 4 May 2026 21:14:12 -0500 Subject: [PATCH 1/4] cleanup --- src/components/CopyableAddress.tsx | 23 +- src/components/dashboard/EventFeed.tsx | 17 +- src/components/dashboard/MinerRatesTable.tsx | 13 +- src/components/dashboard/OrderbookDepth.tsx | 13 +- src/components/dashboard/SwapTracker.tsx | 17 +- src/pages/ReservationDetailPage.tsx | 282 ++++++++++--------- src/pages/SwapDetailPage.tsx | 178 +++++++----- src/theme.ts | 14 +- 8 files changed, 298 insertions(+), 259 deletions(-) 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 ( - + { - - 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" diff --git a/src/components/dashboard/MinerRatesTable.tsx b/src/components/dashboard/MinerRatesTable.tsx index 4860e6c..d64cfa3 100644 --- a/src/components/dashboard/MinerRatesTable.tsx +++ b/src/components/dashboard/MinerRatesTable.tsx @@ -375,19 +375,14 @@ 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. - + + Sort by rate or capacity to find the best counterparty. } arrow diff --git a/src/components/dashboard/OrderbookDepth.tsx b/src/components/dashboard/OrderbookDepth.tsx index db64b4b..f8658a6 100644 --- a/src/components/dashboard/OrderbookDepth.tsx +++ b/src/components/dashboard/OrderbookDepth.tsx @@ -232,19 +232,16 @@ const OrderbookDepth: React.FC = () => { - - What is this? - - + + This orderbook visualizes the cumulative liquidity available across all active miners at various exchange rates. - - + + The background bars form a volume profile: the market equilibrium point is where the left and right profiles match in width. - + } arrow diff --git a/src/components/dashboard/SwapTracker.tsx b/src/components/dashboard/SwapTracker.tsx index a6db9a0..3168af0 100644 --- a/src/components/dashboard/SwapTracker.tsx +++ b/src/components/dashboard/SwapTracker.tsx @@ -102,17 +102,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" diff --git a/src/pages/ReservationDetailPage.tsx b/src/pages/ReservationDetailPage.tsx index 29c5a45..51c01d7 100644 --- a/src/pages/ReservationDetailPage.tsx +++ b/src/pages/ReservationDetailPage.tsx @@ -23,7 +23,13 @@ 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, +} from '../components'; import ExtensionChip, { deriveReservationExtensionStatus, } from '../components/ExtensionChip'; @@ -40,6 +46,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'; @@ -163,7 +172,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 */} { label="Swap initiated" detail={ isInitiated - ? `Quorum confirmed → swap #${r.swapId}` + ? `quorum confirmed → Swap #${r.swapId}` : isTerminal - ? 'Did not initiate' - : 'Pending validator quorum' + ? 'did not initiate' + : 'pending validator quorum' } /> - {/* 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 +323,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. + Validators usually pick up the send within a block. The sender + address must match — mismatched txs are rejected. - )} + + )} - {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} - - - )} - - {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 +415,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 && ( diff --git a/src/pages/SwapDetailPage.tsx b/src/pages/SwapDetailPage.tsx index bee1db7..5326508 100644 --- a/src/pages/SwapDetailPage.tsx +++ b/src/pages/SwapDetailPage.tsx @@ -40,6 +40,9 @@ type TimelineStep = { failed: boolean; }; +const fmtBlock = (b: string | number): string => + Number(b).toLocaleString('en-US'); + const getStatusColor = ( status: string, palette: { status: Record }, @@ -191,58 +194,79 @@ 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 */} @@ -290,7 +314,7 @@ const SwapDetailPage: React.FC = () => { color: step.done ? stepColor : 'text.secondary', }} > - {step.block ? `Block #${step.block}` : '\u2014'} + {step.block ? `Block ${fmtBlock(step.block)}` : '\u2014'} ); @@ -321,7 +345,7 @@ const SwapDetailPage: React.FC = () => { color: 'text.secondary', }} > - Block #{swap.timeoutBlock} + Block {fmtBlock(swap.timeoutBlock)} {!isTimedOut && swap.status !== 'COMPLETED' && currentBlock > 0 && ( @@ -452,26 +476,7 @@ const SwapDetailPage: React.FC = () => { )} - {/* 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 +499,54 @@ 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 +586,7 @@ const SwapDetailPage: React.FC = () => { color: 'text.secondary', }} > - #{event.blockNumber} + {fmtBlock(event.blockNumber)} {event.taoAmount && ( Date: Mon, 4 May 2026 22:26:44 -0500 Subject: [PATCH 2/4] ui: detail page redesign, dashboard polish, shared timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reservation + swap detail pages: lead with the trade summary (`X TAO → Y BTC`), drop redundant cards, comma-format block numbers, rename "Reserved until" → "Window: A → B", merge swap Transactions / User-sends / User-receives into a single Flow card with the tx hash sitting next to its own leg. Render the swap's terminal Completed row with a green ✓ and the Timeout row with a red ✗ — only the terminal row carries semantic color, intermediate "done" rows stay neutral. Hide Completed on timed-out swaps and Timeout on completed swaps. Shared Timeline component: lift `` + `` into src/components/Timeline.tsx so both detail pages render their lifecycle stepper from the same source. Reservation timeline now matches the swap timeline visually (unicode glyphs, matching typography). Optional `color` prop is the escape hatch for the swap's terminal-row green/red. Tooltip overhaul: global MuiTooltip styleOverride to a paper-style look (pure surface, regular weight, no shadow, sharp corners) so it stops looking "cheap"; drop the bold `What is this?` preamble in all four dashboard info tooltips; remove `cursor: help` from OrderbookDepth column headers; CopyableAddress tooltip stays single-line (`whiteSpace: nowrap`, `maxWidth: none`). CopyableAddress click fix: handler calls preventDefault + stopPropagation so clicking an address inside a `` card copies without navigating. Renders UID + truncated hotkey side-by-side in the reservation Miner row instead of feeding the concatenation through the address middle-truncator. Dashboard polish: ReservationsTracker now renders a skeleton (was a plain "Loading…" line) — every dashboard component has a skeleton. All inner dashboard cards consistently use `surface.light` for their bg (was a mix of `background.paper` / transparent), so dark mode isn't erratic between black and grey cards. Detail-page Cards keep `background.paper` (woodsmoke in dark) intentionally. Surface tones: lighten light-mode card surface from BRAND.gray (#eef0f3, also the border tone) to a dedicated #f4f6f8 so cards stop sharing tone with borders. Mirror in dark mode by pulling surface-light from 92/8 woodsmoke/white (#1d1f20) to 96/4 (#131517) so cards integrate with bg instead of stamping on top. Semantic terminal colors: introduce `--color-success` (#15803d) and `--color-danger` (#b91c1c). Used only on terminal "finality" rows (swap detail timeline + status chip) and on the dashboard transaction list's COMPLETED / TIMED_OUT status text. Not wired into `--color-status-completed`/`--color-status-timed-out` so reservation timelines and other "done" states stay neutral. Dashboard progress bar stays a single neutral grey regardless of status. --- src/components/Timeline.tsx | 98 +++++++++++ src/components/dashboard/EventFeed.tsx | 2 +- src/components/dashboard/OrderbookDepth.tsx | 6 +- .../dashboard/ReservationsTracker.tsx | 21 +-- src/components/dashboard/Skeletons.tsx | 61 ++++++- src/components/dashboard/StatsPanel.tsx | 2 +- src/components/dashboard/SwapTracker.tsx | 14 +- src/components/index.ts | 2 + src/index.css | 22 ++- src/pages/ReservationDetailPage.tsx | 97 ++--------- src/pages/SwapDetailPage.tsx | 161 +++++++----------- src/theme.ts | 12 +- 12 files changed, 282 insertions(+), 216 deletions(-) create mode 100644 src/components/Timeline.tsx diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx new file mode 100644 index 0000000..84a1068 --- /dev/null +++ b/src/components/Timeline.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { Stack, Typography, useTheme } from '@mui/material'; +import { FONTS } from '../theme'; + +export type TimelineStepState = 'done' | 'active' | 'pending' | 'failed'; + +const STATE_GLYPH: Record = { + 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 f302c03..03f124e 100644 --- a/src/components/dashboard/EventFeed.tsx +++ b/src/components/dashboard/EventFeed.tsx @@ -128,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/OrderbookDepth.tsx b/src/components/dashboard/OrderbookDepth.tsx index f8658a6..757d266 100644 --- a/src/components/dashboard/OrderbookDepth.tsx +++ b/src/components/dashboard/OrderbookDepth.tsx @@ -307,7 +307,7 @@ const OrderbookDepth: React.FC = () => { arrow placement="top" > - + Rate (TAO) @@ -318,7 +318,7 @@ const OrderbookDepth: React.FC = () => { arrow placement="top" > - + Capacity @@ -334,7 +334,6 @@ const OrderbookDepth: React.FC = () => { display: 'inline-flex', alignItems: 'center', gap: 0.75, - cursor: 'help', }} > @@ -352,7 +351,6 @@ const OrderbookDepth: React.FC = () => { display: 'inline-flex', alignItems: 'center', gap: 0.75, - cursor: 'help', }} > 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 3168af0..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; }; @@ -140,7 +142,7 @@ const SwapTracker: React.FC = () => { p: 4, textAlign: 'center', borderRadius: 0, - backgroundColor: 'background.paper', + backgroundColor: 'surface.light', border: '1px solid', borderColor: 'divider', }} @@ -182,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', @@ -245,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..ad9d1e8 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, @@ -46,6 +48,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 +89,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 51c01d7..091aa3a 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, @@ -29,12 +25,14 @@ import { 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, @@ -106,18 +104,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' @@ -252,13 +249,16 @@ const ReservationDetailPage: React.FC = () => { {/* Lifecycle stepper */} - - Timeline + + - { : 'send funds to the miner — usually within a block' } /> - { ); }; -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 5326508..ccaa921 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; @@ -47,11 +55,13 @@ 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; }; @@ -96,7 +106,7 @@ const SwapDetailPage: React.FC = () => { : undefined; const refundPending = refundEvent?.eventType === 'SlashPending'; - const steps: TimelineStep[] = [ + const steps: SwapStep[] = [ { label: 'Initiated', block: swap.initiatedBlock, @@ -273,82 +283,46 @@ const SwapDetailPage: React.FC = () => { 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 ${fmtBlock(step.block)}` : '\u2014'} - - - ); - })} - {/* Timeout line */} - {swap.timeoutBlock && ( - - - {isTimedOut ? '\u23F1' : '\u23F1'} - - - Timeout - - - Block {fmtBlock(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 && ( <> {' '} ( @@ -359,8 +333,9 @@ const SwapDetailPage: React.FC = () => { remaining) )} - - + + } + /> )} {swap.timeoutBlock && !isTimedOut && swap.status !== 'COMPLETED' && ( { ); }; -/* ---- 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 2ff49a2..cee297d 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -24,8 +24,10 @@ 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)', @@ -45,13 +47,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)', From 6968137cea44c838f3139b5ba796120d0795192f Mon Sep 17 00:00:00 2001 From: anderdc Date: Mon, 4 May 2026 22:33:31 -0500 Subject: [PATCH 3/4] tweaks --- src/components/dashboard/OrderbookDepth.tsx | 13 ++++++++++--- src/index.css | 8 ++++++-- src/theme.ts | 7 +++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/dashboard/OrderbookDepth.tsx b/src/components/dashboard/OrderbookDepth.tsx index 757d266..50135c1 100644 --- a/src/components/dashboard/OrderbookDepth.tsx +++ b/src/components/dashboard/OrderbookDepth.tsx @@ -83,6 +83,8 @@ const OrderbookDepth: React.FC = () => { ); }; + // Match Active Rates' table rhythm: tighter horizontal padding than + // MUI default (16px → 8px) so cells breathe before content wraps. const headerSx = { fontFamily: FONTS.mono, fontSize: '0.65rem', @@ -91,12 +93,14 @@ const OrderbookDepth: React.FC = () => { backgroundColor: theme.palette.background.default, textTransform: 'uppercase' as const, letterSpacing: '0.05em', + px: 1, }; const cellSx = { fontFamily: FONTS.mono, fontSize: '0.75rem', borderBottom: `1px solid ${theme.palette.divider}`, + px: 1, }; const { data: miners, isLoading } = useMiners(); @@ -371,11 +375,14 @@ const OrderbookDepth: React.FC = () => { : theme.palette.primary.main; const taoThemeColor = TAO_COLOR; - const leftGradColor = `color-mix(in srgb, ${assetThemeColor} 10%, transparent)`; + // Lighter wash than the previous 10%/8% so rows read like + // Active Rates' clean rows; bars still suggest volume but + // don't visually dominate the cell content. + const leftGradColor = `color-mix(in srgb, ${assetThemeColor} 6%, transparent)`; const rightGradColor = theme.palette.mode === 'dark' - ? 'color-mix(in srgb, var(--color-white) 10%, transparent)' - : 'color-mix(in srgb, var(--color-woodsmoke) 8%, transparent)'; + ? 'color-mix(in srgb, var(--color-white) 6%, transparent)' + : 'color-mix(in srgb, var(--color-woodsmoke) 5%, transparent)'; return ( Date: Tue, 5 May 2026 04:01:57 +0000 Subject: [PATCH 4/4] style: auto-format code with prettier/eslint --- src/components/dashboard/EventFeed.tsx | 8 ++++---- src/components/dashboard/MinerRatesTable.tsx | 4 +++- src/pages/ReservationDetailPage.tsx | 4 +--- src/pages/SwapDetailPage.tsx | 21 ++++++++++---------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/components/dashboard/EventFeed.tsx b/src/components/dashboard/EventFeed.tsx index 03f124e..590a6ff 100644 --- a/src/components/dashboard/EventFeed.tsx +++ b/src/components/dashboard/EventFeed.tsx @@ -91,10 +91,10 @@ const EventFeed: React.FC = () => { - 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 diff --git a/src/components/dashboard/MinerRatesTable.tsx b/src/components/dashboard/MinerRatesTable.tsx index 3bfe86b..1ecc7ab 100644 --- a/src/components/dashboard/MinerRatesTable.tsx +++ b/src/components/dashboard/MinerRatesTable.tsx @@ -373,7 +373,9 @@ const MinerRatesTable: React.FC = () => { (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. + + Sort by rate or capacity to find the best counterparty. + } arrow diff --git a/src/pages/ReservationDetailPage.tsx b/src/pages/ReservationDetailPage.tsx index 091aa3a..912f162 100644 --- a/src/pages/ReservationDetailPage.tsx +++ b/src/pages/ReservationDetailPage.tsx @@ -33,7 +33,6 @@ import ExtensionChip, { deriveReservationExtensionStatus, } from '../components/ExtensionChip'; - const minerSendToAddress = ( fromChain: string | null, miner: Miner | undefined, @@ -219,8 +218,7 @@ const ReservationDetailPage: React.FC = () => { '&:hover': { textDecoration: 'underline' }, }} > - Funded · Swap #{r.swapId}{' '} - + Funded · Swap #{r.swapId} ) : ( { isTerminalCompleted ? 'var(--color-success)' : undefined } label={step.label} - detail={step.block ? `Block ${fmtBlock(step.block)}` : '\u2014'} + detail={ + step.block ? `Block ${fmtBlock(step.block)}` : '\u2014' + } /> ); })} @@ -474,7 +476,12 @@ const SwapDetailPage: React.FC = () => { netRecv && swap.destChain ? formatAmount(netRecv, swap.destChain) : null; - const hasSend = !!(sentAmount || sentFrom || sentTo || swap.sourceTxHash); + const hasSend = !!( + sentAmount || + sentFrom || + sentTo || + swap.sourceTxHash + ); const hasRecv = !!(recvAmount || recvFrom || recvTo || swap.destTxHash); if (!hasSend && !hasRecv) return null; return ( @@ -494,11 +501,7 @@ const SwapDetailPage: React.FC = () => { {sentFrom && } {sentTo && } {swap.sourceTxHash && ( - + )} )} @@ -506,9 +509,7 @@ const SwapDetailPage: React.FC = () => { Receives - {swap.destChain - ? ` · ${swap.destChain.toUpperCase()}` - : ''} + {swap.destChain ? ` · ${swap.destChain.toUpperCase()}` : ''} {recvAmount && (