Skip to main contentSkip to navigation

The figures a surface opens with, each carrying whether it was read, so a request that went unanswered is not drawn as a zero.

Tally is the row of figures across the top of a Lux surface — volume, open interest, fees, trades. A person arriving wants to know whether the place is busy before they look at anything in it, and a handful of figures answers that faster than a chart does.

It takes a reading rather than a number. On a surface reporting what a chain and an indexer say, a figure that came back as nothing and a request that never came back are a fact and a fabrication, and $0 looks the same either way. A tally that draws an unanswered request as $0 reports a venue that emptied.

Loading...

The reading

type Figure =
  | { at: 'reading' }
  | { at: 'failed'; why: string }
  | { at: 'read'; it: string | null }

read carries the figure as text the caller has already written, and it: null is the answer that is genuinely nothing. failed carries a reason and cannot be spelled as an empty string, which is what puts the flatten-to-zero mistake out of reach.

Each reading puts a different word where the figure goes.

ReadingWhat the tile shows
reading…
read, with a figurethe figure
read, with nonenone
failednot read, and the reason under the row

A dash in all four places is the substitution this component exists to avoid: a figure of nothing and a reading that never landed, told apart by nothing on the screen. Colour separates them again — only a tile carrying a figure is drawn in --color-text-primary, and the other three stay in the secondary colour, so the difference is visible before the words are read.

The same distinction over a list of rows is Table, whose Rows<R> is the same three constructors with the rows in place of a figure.

The reason, said once

Tiles that failed for the same reason state it once, under the row. Where the reasons differ, each is stated with the name of the figure it belongs to. The alternative is the same sentence repeated across the top of the screen, louder than the figures it replaced and no more informative than one of them.

Change

change is a signed percentage beside the figure, drawn only where the reading is read. A rise takes + and the good hue, a fall takes the bad one. Zero takes neither and stays quiet: painting it green because it is not negative reports a flat day as a good one.

Installation

npm install @luxfi/ui @hanzo/gui @hanzo/ui @hanzo/design @tanstack/react-query
import '@luxfi/ui/tokens.css'
import '@hanzo/design/styles.css'
import { AppProvider } from '@luxfi/ui/provider'
import { Tally, money, moved, type Figure, type Stat } from '@luxfi/ui/read'

AppProvider goes once at the root of the app and mounts the gui engine and a query client. The two sheets are where the names resolve: colour from @luxfi/ui/tokens.css, spacing, elevation and measure from @hanzo/design.

Usage

import { Tally, money, moved, type Figure, type Stat } from '@luxfi/ui/read'
const stats: Stat[] = [
  { name: 'Volume, 24h', of: volume, change: moved(today, yesterday) },
  { name: 'Open interest', of: openInterest },
  { name: 'Fees, 24h', of: fees },
  { name: 'Trades, 24h', of: trades },
]

<Tally of={stats} />

The caller is what turns a query into a reading, and that mapping is the only place the distinction can be lost.

const q = useQuery({ queryKey: ['venue'], queryFn: readVenue })

const volume: Figure = q.isPending
  ? { at: 'reading' }
  : q.isError
    ? { at: 'failed', why: String(q.error) }
    : { at: 'read', it: q.data.volume === null ? null : money(q.data.volume) }

Writing the figure

brief is the compact form a headline uses; money is the same with a currency in front. A figure is held to one line and clipped at the tile's edge, so 1,284,003,117.42 arrives on screen as some prefix of itself, and a truncated figure is a different figure.

CallResult
brief(842)842
brief(4210)4.2K
brief(4_210_000)4.21M
brief(4_210_000_000)4.21B
brief(-4_210_000)-4.21M
money(1_284_003)$1.28M

moved(now, before) is what one figure did against the one before it, as a percentage. It answers null rather than zero where there is nothing to compare against, where either figure is not finite, and where before is zero — a rise from nothing is not a percentage, and the arithmetic for it is Infinity, which would render as Infinity% beside a real number.

Props

Tally

PropTypeDescription
ofreadonly Stat[]The figures, drawn in the order given.
childrenReactNodeAnything belonging under the row. Drawn after the reasons.

Stat

PropTypeDescription
namestringWhat the figure is, in the fewest words that stay unambiguous. It labels the tile, keys it, and prefixes the reason where reasons differ.
ofFigureThe reading.
changenumber | nullSigned percentage against an earlier figure. Absent or null draws nothing.

Figures

FunctionSignature
brief(n: number) => string
money(n: number) => string
moved(now: number | null, before: number | null) => number | null

How it is laid out

The row is Grid from @hanzo/ui/grid — a real display: grid, given columns={{ min: 150, max: 4 }}. Tracks auto-fill the width above a floor of 150 pixels, or the whole width where the container is narrower than that, and a second floor of a quarter of the row is what holds the count to four. The tiles reflow to the space they are given rather than at a breakpoint.

A tile is gui stacks and SizableText carrying style props, and its colours are token names from @luxfi/ui/tokens.css rather than literals. See Theming for the token layer and gui.hanzo.ai for the engine.

What it does not do

It does not read anything, and it does not decide what a reading is: the caller maps a query onto Figure, and a caller that reports a failure as { at: 'read', it: '$0' } writes exactly the fabrication the type exists to prevent, which nothing in the component can catch. It does not format the figure either — it is text by the time a tile sees it, and brief and money are there to be used rather than applied for you. It does not compute change, and it does not poll, refresh, or carry a period selector; there is no trend line or sparkline behind a tile. It also says nothing about when a figure was read: a reading from an hour ago is drawn exactly like one from this second, so a surface that needs to say how old a figure is has to say it somewhere else.