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.
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.
| Reading | What the tile shows |
|---|---|
reading | … |
read, with a figure | the figure |
read, with none | none |
failed | not 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-queryimport '@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.
| Call | Result |
|---|---|
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
| Prop | Type | Description |
|---|---|---|
of | readonly Stat[] | The figures, drawn in the order given. |
children | ReactNode | Anything belonging under the row. Drawn after the reasons. |
Stat
| Prop | Type | Description |
|---|---|---|
name | string | What the figure is, in the fewest words that stay unambiguous. It labels the tile, keys it, and prefixes the reason where reasons differ. |
of | Figure | The reading. |
change | number | null | Signed percentage against an earlier figure. Absent or null draws nothing. |
Figures
| Function | Signature |
|---|---|
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.