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
3 changes: 3 additions & 0 deletions .github/workflows/Ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@ jobs:
- name: Typecheck Project
run: npm run typecheck

- name: Test Project
run: npm test

- name: Build Project
run: npm run build
57 changes: 57 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
--nx-up-soft: rgba(38, 192, 138, 0.14);
--nx-down: #f0546b;
--nx-down-soft: rgba(240, 84, 107, 0.14);
/* Start colour of a tick flash; it fades out from here. */
--nx-up-flash: rgba(38, 192, 138, 0.32);
--nx-down-flash: rgba(240, 84, 107, 0.32);
--nx-warn: #e8b339;

--nx-radius-sm: 6px;
Expand Down Expand Up @@ -177,6 +180,36 @@
background-size: 32px 32px;
}

/*
Motion. Every animation here reports a change in the data: a price that
ticked, a level that resized, a trade that printed. Nothing loops for
decoration. The reduced-motion rule in the base layer collapses them all.
*/
.nx-flash-up {
animation: nx-flash-up 900ms ease-out;
}
.nx-flash-down {
animation: nx-flash-down 900ms ease-out;
}
.nx-enter {
animation: nx-enter 220ms ease-out backwards;
}
.nx-trade-in-up {
animation:
nx-enter 260ms ease-out backwards,
nx-flash-up 1200ms ease-out;
}
.nx-trade-in-down {
animation:
nx-enter 260ms ease-out backwards,
nx-flash-down 1200ms ease-out;
}
/* Panels settling in on first paint. `backwards` rather than `both`, so
no transform lingers to trap the market picker's dropdown. */
.nx-rise {
animation: nx-rise 480ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}

.no-scrollbar::-webkit-scrollbar {
display: none;
}
Expand Down Expand Up @@ -206,6 +239,30 @@
}
}

/* Single-keyframe animations ease back to the element's own styles. */
@keyframes nx-flash-up {
from {
background-color: var(--nx-up-flash);
}
}
@keyframes nx-flash-down {
from {
background-color: var(--nx-down-flash);
}
}
@keyframes nx-enter {
from {
opacity: 0;
transform: translateY(-4px);
}
}
@keyframes nx-rise {
from {
opacity: 0;
transform: translateY(8px);
}
}

/* Page-level scrollbar, matching the panes. */
::-webkit-scrollbar {
width: 10px;
Expand Down
27 changes: 21 additions & 6 deletions app/trade/[market]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function TradePage() {
<Appbar TrueButton={1} />

<main className="mx-auto max-w-[1600px] p-3">
<div className="nx-panel mb-3 overflow-visible">
<div className="nx-panel nx-rise mb-3 overflow-visible">
<MarketBar market={market} ticker={ticker} />
</div>

Expand All @@ -41,15 +41,24 @@ export default function TradePage() {
than pushing the page.
*/}
<div className="grid gap-3 lg:grid-cols-12 lg:grid-rows-[440px_300px]">
<div className="nx-panel h-[380px] overflow-hidden lg:col-span-6 lg:row-span-2 lg:h-auto">
<div
className="nx-panel nx-rise h-[380px] overflow-hidden lg:col-span-6 lg:row-span-2 lg:h-auto"
style={{ animationDelay: "60ms" }}
>
<PriceChart market={market} />
</div>

<div className="nx-panel h-[560px] overflow-hidden lg:col-span-3 lg:row-span-2 lg:h-auto">
<div
className="nx-panel nx-rise h-[560px] overflow-hidden lg:col-span-3 lg:row-span-2 lg:h-auto"
style={{ animationDelay: "120ms" }}
>
<OrderBook market={market} onSelectPrice={setSelectedPrice} />
</div>

<div className="nx-panel overflow-hidden lg:col-span-3">
<div
className="nx-panel nx-rise overflow-hidden lg:col-span-3"
style={{ animationDelay: "180ms" }}
>
<OrderForm
// Remount per market so no quantity or side carries over.
key={market}
Expand All @@ -59,8 +68,14 @@ export default function TradePage() {
/>
</div>

<div className="nx-panel h-[300px] overflow-hidden lg:col-span-3 lg:h-auto">
<TradeTape market={market} />
<div
className="nx-panel nx-rise h-[300px] overflow-hidden lg:col-span-3 lg:h-auto"
style={{ animationDelay: "240ms" }}
>
{/* Remount per market: polling keeps the last data across a
dependency change, which showed the previous market's trades
until the first poll for the new one landed. */}
<TradeTape key={market} market={market} />
</div>
</div>

Expand Down
63 changes: 12 additions & 51 deletions app/utils/ChartManager.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
import {
CandlestickSeries,
CandlestickSeriesPartialOptions,
ColorType,
createChart as createLightWeightChart,
CrosshairMode,
IChartApi,
ISeriesApi,
UTCTimestamp,
} from "lightweight-charts";

export class ChartManager {
private candleSeries: ISeriesApi<"Candlestick">;
private lastUpdateTime: number = 0;
private chart: IChartApi;
private currentBar: {
open: number | null;
high: number | null;
low: number | null;
close: number | null;
} = {
open: null,
high: null,
low: null,
close: null,
};



constructor(
ref: HTMLElement,
initialData: { timestamp: number; open: number; high: number; low: number; close: number; volume?: number }[],
layout: { background: string; color: string }
layout: { background: string; color: string },
/** Decimal places on the price axis; the library default of 2 flattens sub-cent markets. */
precision = 2
) {
if (typeof window === "undefined") {
throw new Error("ChartManager must be instantiated in the browser");
Expand Down Expand Up @@ -74,23 +59,20 @@ export class ChartManager {
});
this.chart = chart;

const seriesOptions: CandlestickSeriesPartialOptions = {
const candleSeries = chart.addSeries(CandlestickSeries, {
upColor: "#26c08a",
downColor: "#f0546b",
borderVisible: false,
wickUpColor: "#26c08a",
wickDownColor: "#f0546b",
};

// lightweight-charts v5 uses addSeries instead of addCandlestickSeries.
if ("addCandlestickSeries" in chart) {
// @ts-expect-error - legacy signature is still valid at runtime
this.candleSeries = chart.addCandlestickSeries(seriesOptions);
} else {
this.candleSeries = chart.addSeries(CandlestickSeries, seriesOptions);
}
priceFormat: {
type: "price",
precision,
minMove: 10 ** -precision,
},
});

this.candleSeries.setData(
candleSeries.setData(
initialData.map((data) => ({
...data,
time: (data.timestamp / 1000) as UTCTimestamp,
Expand All @@ -100,28 +82,7 @@ export class ChartManager {
chart.timeScale().fitContent();
}


public update(updatedPrice: { close: number; low: number; high: number; open: number; newCandleInitiated?: boolean; time?: number }) {
if (!this.lastUpdateTime) {
this.lastUpdateTime = new Date().getTime();
}

this.candleSeries.update({
time: (this.lastUpdateTime / 1000) as UTCTimestamp,
close: updatedPrice.close,
low: updatedPrice.low,
high: updatedPrice.high,
open: updatedPrice.open,
});

if (updatedPrice.newCandleInitiated && updatedPrice.time) {
this.lastUpdateTime = updatedPrice.time;
}
}



public destroy() {
this.chart.remove();
}
}
}
Loading
Loading