Skip to main contentSkip to navigation

Every mark this package draws, in every state it has, on one page — the coin, the glyphs, the emblems, the strokes, the cog, the mark, the currency corner, the file that does not arrive, and the way a figure is set.

Eight modules draw everything on a Lux surface that is not a word or a box. They share a rule and are easy to draw twice: a mark is recognised faster than a string is read, and two hand-drawn copies of one mark are two marks. This page is the whole family at once, so a drawing that has drifted is visible beside the one it drifted from.

Every asset here is invented and says so. The assets are called Example A through D, their addresses count up from one, and the three pictures a build would serve are drawn inline in the demo rather than fetched from anywhere. Nothing on this page is a reading of a chain.

Loading...

What each one is for

Coin draws an asset — its mark, its symbol, its name where there is room, and whatever the row states about it. Glyph draws the fifteen marks that sit beside a word. Emblem draws the eight figures set behind a band, which carry no meaning and no label. Caret, Wedge and Tick are the three strokes: one that says a control opens, one that stands beside a wordmark, one that says which entry in a list is in force. Cog is the mark a person looks for to adjust something. Mark is a word about the thing beside it. Money is the currency a sum is stated in. Picture draws a file the build serves and holds what stands there when it does not arrive. figures is how every number on the surface is written.

Nothing is fetched to draw a mark

An indexer answers with an id, a symbol, a name and decimals, and no image, so most assets have no mark at all. A component that expected a URL would draw a broken frame for most of a chain's tokens. What a build serves is stated once, at its root, and everything below resolves against it:

import { Marks } from '@luxfi/ui/read'

<Marks value={{
  token:  (symbol) => served.has(symbol) ? `/tokens/${symbol}.svg` : null,
  chain:  (key)    => chains.has(key)    ? `/chains/${key}.png`    : null,
  region: (code)   => flags.has(code)    ? `/flags/${code}.svg`    : null,
}}>
  {children}
</Marks>

Each answers null for a name it has no file for, and null is the ordinary answer rather than the exception. Where a token has no mark, Coin draws the asset's own letters — at most three, because two collide on a chain whose symbols share a first letter. Where a region has no flag, Money draws the two regional indicators the reader's own system knows.

A build that states nothing draws letters everywhere, which always resolve and never fetch. That is what this page does for the five assets it does not serve.

A path is not a picture

A file a build names can still fail to fetch — renamed under a hash, blocked, or 404 from a host that used to have it — and a bare <img> has no answer for that: Chrome draws its own torn-picture glyph, alt="" included, clipped by whatever radius the box carries. The state the resolver exists to prevent arrives one step later.

So whether the file arrived is a state, and Picture holds it. Each of the three places a Lux surface draws a served file says what stands there instead: a token's mark falls back to the asset's letters, the chain badge on it to nothing, the flag beside a currency to the region's two indicators. The row loses a picture, never its subject.

<Picture src={identity.icon} size={32} instead={<Letters of={identity} />} />

An error that fires before React attaches its handler is an error React never hears, which is what happens on a page rendered on a server: the browser has already tried the fetch by the time the tree hydrates. So the same question is asked once more on mount, where a finished image measuring nothing is one that did not arrive — a picture carrying no intrinsic size of its own still reports the 150 a browser gives it, so only a failure reads zero.

The shade is the address

A mark with no picture is a disc and three letters, and the disc's lightness is an FNV-1a hash of the lowercased address, in twelve steps. The same asset is the same disc on every screen and across reloads, which is the only property that makes it recognisable.

Twelve steps rather than a percentage, because the two lightnesses a disc needs — its ground and its letters — come from one number and so cannot drift apart. And lightness rather than hue, because this system reserves colour for exactly two meanings. A wall of tinted discs spends colour on identity and gives a reader a second thing that looks like state and is not.

A figure is not a string until a locale has been applied to it

842 is 842 in English and in French; 4,210,000 is 4 210 000 there and 42.1 lakh in Hindi; $1.28M is 1,28 M $ in Canadian French. Every one of those is a fact about a reader, and toLocaleString('en-US') pins them all to one reader while the language menu says otherwise.

So figures takes the reading and returns every way that reader's numbers are written. It is a pure function of the reading, it caches its formatters — Intl resolves a locale on construction, which is a hundred times the cost of formatting with one — and it rolls no arithmetic of its own. A K/M/B ladder is an English convention; CLDR gives Japanese 万 and 億 at four and eight digits, and Intl already carries CLDR.

const write = figures({ locale: 'en-US', currency: 'USD' })

write.worth(1_284_003)   // $1.28M
write.percent(0.0421)    // +4.21%
write.rate(0.0025)       // 0.25%
write.money(Number.NaN)  // —

The last line is the rule the whole family exists for. A figure that is not a number has no rendering, and a component that invents one has put a fabrication on the screen where a reader expects a reading.

The currency corner takes a reading, not a currency

Money is a flag and a stroke, and the list behind it writes each currency's own symbol out of the same Intl table the figures beside it come from. It takes the whole reading for that reason: a currency and a language as two props are two chances for the list to write US$ where the panel writes $.

<Money
  reading={{ locale, currency }}
  of={['USD', 'EUR', 'GBP', 'JPY', 'CAD']}
  set={setCurrency}
  say="Currency"
/>

Nothing is converted by the press. The amount is what somebody types and the currency is what they type it in, which is the pair the other side is handed; converting would be a second claim, at a rate this component does not have.

The flag is filed under the ISO 3166 region, which ISO 4217 already put in the first two characters of every code — EUR included, whose EU is the Union's own region code. region('EUR') is exported so a build can key its flags the same way rather than guessing.

The list is a listbox, so it answers the four arrows, Home and End, and carries one tab stop rather than one per entry. That stop is the entry in force, or the first entry where the currency in force is not among those offered — a saved preference that outlived a change to the list. Tied to the tick alone it would go with it, leaving a list a reader can open and not reach a row of.

The API

// The asset, and what a build serves for it
Marks({ value: { token?, chain?, region? }, children })
identify(of: Of, marks?: Marks): Identity
useIdentity(of: Of): Identity
useMarks(): Marks
letters(symbol?: string, address?: string): string   // one to three characters

// The asset, drawn
Coin({ symbol?, name?, address?, chain?, size?, amount? })
Coins({ of: readonly Of[], say?, size? })
shade(address?: string): number                       // 0 to 11
SIZE  // unit 16 · pill 24 · row 32 · card 32 · list 36 · panel 36 · page 56
CARET // 24, the stroke beside a pill that opens a chooser

// The drawings
Glyph({ of: Glyph, size?, colour? })
Emblem({ of: Emblem, size, behind?, turn? })
Caret({ size?, colour?, to? })
Wedge({ size?, colour? })
Tick({ size?, colour? })
Cog({ size?, colour? })
Mark({ paint?: Paint, children? })
Picture({ src, size, instead?, style? })       // a served file, or what stands there

// The currency, and the numbers
Money({ reading: Reading, of: readonly string[], set, say })
region(code: string): string
figures(of: Reading): Figures
digits(typed: string, places?: number): string
moved(now: number | null, before: number | null): number | null
NONE  // '—', the mark a cell carries where there is nothing to draw

What these are painted with is not here. Colour, height and weight are @luxfi/ui/material, which is the material of every Lux surface rather than of these parts.