Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -129,7 +129,7 @@ export function PositionsList({ onSelectPosition }: Props) {
{
id: "mark",
header: "Mark",
accessor: (p) => <Numeric value={p.markPrice} format="usd" role="neutral" />,
accessor: (p) => <Numeric value={p.markPrice} format="usd" role="neutral" rolling />,
},
{
id: "liq",
Expand Down Expand Up @@ -176,9 +176,9 @@ export function PositionsList({ onSelectPosition }: Props) {
const role = p.pnlAfterFees >= 0 ? "positive" : "negative"
return (
<>
<Numeric value={p.pnlAfterFees} format="usd" role={role} />
<Numeric value={p.pnlAfterFees} format="usd" role={role} rolling />
{" "}
<Numeric value={p.pnlPercent} format="pct" role={role} />
<Numeric value={p.pnlPercent} format="pct" role={role} rolling />
</>
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DialogTitle,
} from "@workspace/ui/components/dialog"
import { Button } from "@workspace/ui/components/button"
import { cn } from "@workspace/ui/lib/utils"
import { useQuery } from "@tanstack/react-query"
import { createSwapOrder, sendBatchOrderTxn } from "../../lib/stellar"
import { formatUsd } from "../../lib/trade-math"
Expand Down Expand Up @@ -447,9 +448,30 @@ export function ConfirmationDialog({
: ""
}
>
{isSubmitting
? "Submitting..."
: `Confirm ${typeLabel}`}
<span className="grid place-items-center">
<span
aria-hidden={isSubmitting}
className={cn(
"col-start-1 row-start-1 transition-opacity duration-base ease-out",
isSubmitting ? "opacity-0" : "opacity-100"
)}
>
{`Confirm ${typeLabel}`}
</span>
<span
aria-hidden={!isSubmitting}
className={cn(
"col-start-1 row-start-1 flex items-center gap-2 transition-opacity duration-base ease-out",
isSubmitting ? "opacity-100" : "opacity-0"
)}
>
<span
aria-hidden="true"
className="size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent"
/>
Submitting...
</span>
</span>
</Button>
</DialogFooter>
</DialogContent>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DensityProvider } from "@workspace/ui/components/density-provider"
import appCss from "@workspace/ui/globals.css?url"
import { AppProviders } from "../app/providers"
import { RouteAnnouncer } from "../shared/components/RouteAnnouncer"
import { RouteProgressBar } from "../shared/components/RouteProgressBar"

// Update this to your production domain before going live.
const SITE_URL = "https://levee.market"
Expand Down Expand Up @@ -194,6 +195,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<AppProviders>
<TooltipProvider delay={500} timeout={400} closeDelay={0}>
<ToastProvider>
<RouteProgressBar />
{children}
</ToastProvider>
</TooltipProvider>
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/shared/components/RouteProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useRouterState } from "@tanstack/react-router"
import { cn } from "@workspace/ui/lib/utils"

export function RouteProgressBar() {
const isLoading = useRouterState({ select: (state) => state.isLoading })

return (
<div
aria-hidden="true"
className="pointer-events-none fixed inset-x-0 top-0 z-50 h-0.5 overflow-hidden"
>
<div
className={cn(
"h-full origin-left bg-primary transition-[transform,opacity] duration-slow ease-in-out",
isLoading ? "scale-x-75 opacity-100" : "scale-x-0 opacity-0"
)}
/>
</div>
)
}
5 changes: 4 additions & 1 deletion packages/ui/src/components/numeric.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cva } from 'class-variance-authority'
import { cn } from '@workspace/ui/lib/utils'
import { useNumericChangeFeedback } from '../hooks/use-numeric-change-feedback'
import { RollingNumber } from './rolling-number'
import type { VariantProps } from 'class-variance-authority'

const numericVariants = cva('font-mono tabular-nums', {
Expand Down Expand Up @@ -164,6 +165,7 @@ interface NumericProps {
threshold?: number
highlightOnChange?: boolean
coalesceMs?: number
rolling?: boolean
className?: string
}

Expand All @@ -179,6 +181,7 @@ function Numeric({
threshold,
highlightOnChange = false,
coalesceMs = 500,
rolling = false,
className,
}: NumericProps) {
const { className: emphasisClassName } = useNumericChangeFeedback({
Expand Down Expand Up @@ -210,7 +213,7 @@ function Numeric({
data-slot="numeric"
className={cn(numericVariants({ role }), highlightOnChange && emphasisClassName, className)}
>
{display}
{rolling ? <RollingNumber value={display} /> : display}
</span>
)
}
Expand Down
53 changes: 53 additions & 0 deletions packages/ui/src/components/rolling-number.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { cn } from '@workspace/ui/lib/utils'

const DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

interface RollingNumberProps {
value: string
className?: string
}

/**
* Renders a formatted number with each digit on a vertical odometer strip.
* Only transform moves, so a change slides digits without shifting layout.
* Non-digit characters (sign, currency, separators) render as-is.
*/
function RollingNumber({ value, className }: RollingNumberProps) {
const chars = Array.from(value)

return (
<span className={cn('inline-flex tabular-nums', className)}>
<span className="sr-only">{value}</span>
{chars.map((char, index) => {
// Key by position from the right so digits keep their column identity.
const key = chars.length - index
return /\d/.test(char) ? (
<RollingDigit key={key} digit={Number(char)} />
) : (
<span key={key} aria-hidden="true">
{char}
</span>
)
})}
</span>
)
}

function RollingDigit({ digit }: { digit: number }) {
return (
<span aria-hidden="true" className="relative inline-block h-[1lh] w-[1ch] overflow-hidden">
<span
className="flex flex-col transition-transform duration-base ease-out"
style={{ transform: `translateY(-${digit * 10}%)` }}
>
{DIGITS.map((d) => (
<span key={d} className="flex h-[1lh] items-center justify-center">
{d}
</span>
))}
</span>
</span>
)
}

export { RollingNumber }
11 changes: 6 additions & 5 deletions packages/ui/src/components/skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import { cn } from "@workspace/ui/lib/utils"
// ---------------------------------------------------------------------------
// Base primitive
//
// Uses `motion-safe:animate-pulse` so the animation only runs when the user
// has NOT requested reduced motion. A CSS-level safety net in globals.css
// provides belt-and-suspenders coverage for older browsers / non-Tailwind
// class consumers.
// The loading sheen is a transform-only sweep drawn on ::before, so it never
// triggers layout. globals.css disables it under reduced motion.
// ---------------------------------------------------------------------------

function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
aria-hidden="true"
className={cn("bg-muted motion-safe:animate-pulse", className)}
className={cn(
"relative overflow-hidden bg-muted before:absolute before:inset-0 before:-translate-x-full before:animate-skeleton-sweep before:bg-linear-to-r before:from-transparent before:via-foreground/5 before:to-transparent",
className
)}
{...props}
/>
)
Expand Down
11 changes: 11 additions & 0 deletions packages/ui/src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,13 @@
--radius-12: 0.75rem; /* 12px — sponsor logo cards */
--radius-20: 1.25rem; /* 20px — feature/pool/program cards */

/* Skeleton loading sheen (transform-only) */
--animate-skeleton-sweep: skeleton-sweep 1.5s ease-in-out infinite;
@keyframes skeleton-sweep {
from { transform: translateX(-100%); }
to { transform: translateX(100%); }
}

/* Hero title word-rotation (AnimatedTitle) */
--animate-title-in: title-in 0.25s ease-in-out forwards;
--animate-title-out: title-out 0.25s ease-in-out forwards;
Expand Down Expand Up @@ -992,3 +999,7 @@
:root[data-reduced-motion="true"] .so4-confirm-check,
:root[data-reduced-motion="true"] .so4-confirm-moment { animation: none; }
:root[data-reduced-motion="true"] [data-slot="tooltip-content"] { transition: none; }
@media (prefers-reduced-motion: reduce) {
[data-slot="skeleton"]::before { animation: none; display: none; }
}
:root[data-reduced-motion="true"] [data-slot="skeleton"]::before { animation: none; display: none; }
Loading