Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useRef } from "react"
import { useAnimatedRows } from "@workspace/ui/hooks/use-animated-rows"
import { Skeleton } from "@workspace/ui/components/skeleton"
import {
Table,
Expand All @@ -19,6 +20,7 @@ type Props = {
export function RecentTradesTape({ symbol }: Props) {
const { trades, status, isLoading } = useRecentTrades(symbol)
const containerRef = useRef<HTMLDivElement>(null)
const attachTradeRow = useAnimatedRows(trades, (trade) => trade.id, true, isLoading)

return (
<div className="flex h-full w-full flex-col overflow-hidden text-xs">
Expand Down Expand Up @@ -77,7 +79,7 @@ export function RecentTradesTape({ symbol }: Props) {
</TableHeader>
<TableBody>
{trades.map((trade) => (
<TableRow key={trade.id} interactive={false} className="font-mono text-[11px]">{/* ds-allow: dense trade row font size */}
<TableRow key={trade.id} ref={(node) => attachTradeRow(trade.id, node)} interactive={false} className="font-mono text-[11px]">{/* ds-allow: dense trade row font size */}
<TableCell className="font-medium text-foreground">
{formatUsd(trade.price, { decimals: 4 })}
</TableCell>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react"
import { useAnimatedRows } from "@workspace/ui/hooks/use-animated-rows"
import { Button } from "@workspace/ui/components/button"
import { DataTable } from "@workspace/ui/components/data-table"
import { StatusBadge } from "@workspace/ui/components/status-badge"
Expand Down Expand Up @@ -178,6 +179,8 @@ export function OrdersList() {
},
]

const attachMobileOrder = useAnimatedRows(orders, (order) => order.clientOrderId, true, isLoading)

return (
<>
{isDisabled && (
Expand All @@ -194,6 +197,7 @@ export function OrdersList() {
<DataTable
columns={columns}
data={orders}
animateRows
isLoading={isLoading}
emptyMessage={isDisabled ? "Indexer disabled - showing contract-only data" : "No open orders"}
keyExtractor={(o) => o.clientOrderId}
Expand All @@ -217,7 +221,7 @@ export function OrdersList() {
const statusLabel =
cancelling === o.key ? "Cancelling" : o.awaitingIndex ? "Pending index" : o.status === "frozen" ? "Frozen" : "Open"
return (
<div key={o.clientOrderId} role="listitem">
<div key={o.clientOrderId} ref={(node) => attachMobileOrder(o.clientOrderId, node)} role="listitem">
<AccountOrderCard
rowKey={o.clientOrderId}
marketName={o.marketName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export function PositionsList({ onSelectPosition }: Props) {
<DataTable
columns={columns}
data={positions}
animateRows
isLoading={isLoading}
emptyMessage="No open positions"
emptyAction={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export function TradeHistoryList() {
<DataTable
columns={columns}
data={rows}
animateRows
isLoading={history.isLoading}
emptyMessage={history.error ? "Trade history unavailable" : `No executed fills in ${HISTORY_TIME_RANGE_LABEL[filters.range].toLowerCase()}`}
keyExtractor={(row) => row.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { formatAddress } from "@/shared/lib/format"
type Props = {
open: boolean
onClose: () => void
onConfirmed?: (hash: string) => void
tradeState: ReturnType<typeof useTradeState>
sizeUsd: number
entryPrice: number
Expand All @@ -50,6 +51,7 @@ type Props = {
export function ConfirmationDialog({
open,
onClose,
onConfirmed,
tradeState,
sizeUsd,
entryPrice,
Expand Down Expand Up @@ -304,10 +306,12 @@ export function ConfirmationDialog({
leverage: effectiveLeverage,
}

await sendBatchOrderTxn(account, {
const confirmedHash = await sendBatchOrderTxn(account, {
createOrders: [parentOrder, ...sidecarCreateOrders],
})

// sendBatchOrderTxn waits for ledger confirmation before resolving.
onConfirmed?.(confirmedHash)
clearSidecarOrders()
}
onClose()
Expand Down
19 changes: 19 additions & 0 deletions apps/web/src/features/trade/components/trade-panel/TradePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react"
import { explorerTxUrl } from "@/app/config/network"
import {
Tabs,
TabsContent,
Expand Down Expand Up @@ -45,6 +46,7 @@ type TradePanelProps = {
export function TradePanel({ trade }: TradePanelProps) {
const { getMidPrice, isStale } = useTokenPrices()
const [confirmOpen, setConfirmOpen] = useState(false)
const [confirmedHash, setConfirmedHash] = useState<string | null>(null)
const [priceValidationError, setPriceValidationError] = useState<string | null>(null)
const [previousMode, setPreviousMode] = useState<TradeMode>(trade.tradeMode)
const account = useWalletStore((state) => state.address)
Expand Down Expand Up @@ -330,10 +332,27 @@ export function TradePanel({ trade }: TradePanelProps) {
{tradeType} {!tradeFlags.isSwap && toTokenLabel}
</Button>

{confirmedHash && (
<div role="status" aria-live="polite" className="so4-confirm-moment flex items-center gap-2 rounded-md border border-success/30 bg-success/10 p-3 text-xs text-foreground">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="so4-confirm-check size-5 shrink-0 text-success">
<circle cx="12" cy="12" r="10" />
<path d="m7 12 3 3 7-7" />
</svg>
<span className="flex-1">
<strong className="block font-semibold">Order confirmed on ledger</strong>
Your orders and positions update as indexing completes.
</span>
<a className="shrink-0 text-primary underline-offset-2 hover:underline focus-visible:underline" href={explorerTxUrl(confirmedHash)} target="_blank" rel="noopener noreferrer">
View tx
</a>
</div>
)}

{/* ── Confirmation modal ───────────────────────────────────── */}
<ConfirmationDialog
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirmed={setConfirmedHash}
tradeState={trade}
sizeUsd={sizeUsd}
entryPrice={entryPrice}
Expand Down
9 changes: 6 additions & 3 deletions apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createRootRoute,
} from "@tanstack/react-router"
import { ToastProvider } from "@workspace/ui/components/toast"
import { TooltipProvider } from "@workspace/ui/components/tooltip"
import { DensityProvider } from "@workspace/ui/components/density-provider"
import appCss from "@workspace/ui/globals.css?url"
import { AppProviders } from "../app/providers"
Expand Down Expand Up @@ -191,9 +192,11 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<body>
<DensityProvider>
<AppProviders>
<ToastProvider>
{children}
</ToastProvider>
<TooltipProvider delay={500} timeout={400} closeDelay={0}>
<ToastProvider>
{children}
</ToastProvider>
</TooltipProvider>
</AppProviders>
</DensityProvider>
<Scripts />
Expand Down
63 changes: 60 additions & 3 deletions apps/web/src/ui/theme-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useState } from "react"
import { createContext, useContext, useEffect, useRef, useState } from "react"
import type {ReactNode} from "react";

export type Theme = "dark" | "light" | "system"
Expand Down Expand Up @@ -36,9 +36,66 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
return localStorage.getItem(HIGH_CONTRAST_KEY) === "true"
})

const activeTransition = useRef<ViewTransition | null>(null)

const setTheme = (next: Theme) => {
if (typeof window !== "undefined") localStorage.setItem(STORAGE_KEY, next)
setThemeState(next)
if (typeof window === "undefined") {
setThemeState(next)
return
}
const apply = () => {
localStorage.setItem(STORAGE_KEY, next)
const resolved = resolve(next)
const root = document.documentElement
root.classList.remove("dark", "light")
root.classList.add(resolved)
setThemeState(next)
}

// Honour system accessibility preferences and avoid animating when the
// effective appearance does not change.
if (
resolve(next) === resolve(theme) ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches ||
document.documentElement.dataset.reducedMotion === "true" ||
!document.startViewTransition
) {
activeTransition.current?.skipTransition()
apply()
return
}

// A second toggle may interrupt the first reveal without losing focus.
activeTransition.current?.skipTransition()
const trigger = document.activeElement?.getBoundingClientRect()
const x = trigger ? trigger.left + trigger.width / 2 : window.innerWidth / 2
const y = trigger ? trigger.top + trigger.height / 2 : window.innerHeight / 2
const radius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y),
)

const transition = document.startViewTransition(apply)
activeTransition.current = transition
void transition.ready.then(() => {
const css = getComputedStyle(document.documentElement)
const duration = Number.parseFloat(css.getPropertyValue("--duration-moderate")) || 200
const easing = css.getPropertyValue("--ease-out").trim() || "ease-out"
try {
document.documentElement.animate(
[
{ clipPath: `circle(0px at ${x}px ${y}px)` },
{ clipPath: `circle(${radius}px at ${x}px ${y}px)` },
],
{ duration, easing, fill: "both", pseudoElement: "::view-transition-new(root)" },
)
} catch {
// Unsupported pseudo-element animation: the class change still works.
}
}).catch(() => {})
void transition.finished.finally(() => {
if (activeTransition.current === transition) activeTransition.current = null
})
}

const setHighContrast = (enabled: boolean) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/components/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react'
import { cn } from '@workspace/ui/lib/utils'
import { Skeleton } from '@workspace/ui/components/skeleton'
import { EmptyState } from '@workspace/ui/components/empty-state'
import { useAnimatedRows } from '@workspace/ui/hooks/use-animated-rows'
import {
Table,
TableBody,
Expand Down Expand Up @@ -31,6 +32,8 @@ interface DataTableProps<T> {
keyExtractor: (row: T) => string
onRowClick?: (row: T) => void
selectedRowKey?: string
/** FLIP insertion/removal/reorder animations for live trading data. */
animateRows?: boolean
className?: string
}

Expand All @@ -43,8 +46,11 @@ function DataTable<T>({
keyExtractor,
onRowClick,
selectedRowKey,
animateRows = false,
className,
}: DataTableProps<T>) {
const attachAnimatedRow = useAnimatedRows(data, keyExtractor, animateRows, isLoading)

if (isLoading) {
return (
<Table containerClassName={className}>
Expand Down Expand Up @@ -98,6 +104,7 @@ function DataTable<T>({
return (
<TableRow
key={key}
ref={animateRows ? (node) => attachAnimatedRow(key, node) : undefined}
interactive={!!onRowClick}
aria-current={isSelected ? 'true' : undefined}
tabIndex={onRowClick ? 0 : undefined}
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function TooltipContent({
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
"z-50 max-w-xs origin-(--transform-origin) rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground ring-1 ring-foreground/10 shadow-md has-data-[slot=tooltip-description]:flex-col has-data-[slot=tooltip-description]:items-start has-data-[slot=tooltip-description]:gap-0 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-inline-start-2 data-[side=inline-start]:slide-in-from-inline-end-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:ms-auto **:data-[slot=kbd]:rounded-sm **:data-[slot=kbd]:border **:data-[slot=kbd]:border-border **:data-[slot=kbd]:bg-muted **:data-[slot=kbd]:px-1 **:data-[slot=kbd]:py-0.5 **:data-[slot=kbd]:text-10 **:data-[slot=kbd]:font-mono **:data-[slot=kbd]:leading-none data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"z-50 max-w-xs origin-(--transform-origin) rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground ring-1 ring-foreground/10 shadow-md has-data-[slot=tooltip-description]:flex-col has-data-[slot=tooltip-description]:items-start has-data-[slot=tooltip-description]:gap-0 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-inline-start-2 data-[side=inline-start]:slide-in-from-inline-end-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:ms-auto **:data-[slot=kbd]:rounded-sm **:data-[slot=kbd]:border **:data-[slot=kbd]:border-border **:data-[slot=kbd]:bg-muted **:data-[slot=kbd]:px-1 **:data-[slot=kbd]:py-0.5 **:data-[slot=kbd]:text-10 **:data-[slot=kbd]:font-mono **:data-[slot=kbd]:leading-none ",
className
)}
{...props}
Expand Down
Loading
Loading