Skip to main contentSkip to navigation

A word about the thing it sits beside, outlined rather than filled, in one of the colours paint allows.

A figure on a chain surface rarely describes itself. Which generation a position belongs to, whether a range is still in play, that a number is this interface's arithmetic rather than something the chain said — each qualifies the thing next to it and is not the thing. Mark is that qualifier: one or two words in a rule, set inline beside what they are about.

Loading...

A word, not a colour

Every mark has to survive being read aloud in sequence with what it sits beside. That rules out an icon, and it rules out colour carrying the meaning on its own: a green ring around a number says nothing to a reader who cannot see it, and nothing to one who can but has not been given the legend. So the statement is the text — in range, out of range, derived — and the colour only agrees with it.

An outline, not a fill

The rule is one pixel and the fill is nothing. A row of filled chips reads as a row of buttons, and someone who tries to press the one saying out of range has been told something false about what the screen does. Being inline matters for the same reason: a figure and the word derived after it are one statement, and a flex box between them breaks the line and makes the qualifier look like the next fact. textAlign is stated as left because the other half of these sit inside controls that centre their own text.

It takes no reading

The rest of @luxfi/ui/read takes a reading rather than a value, because a component that has flattened there is nothing and we could not ask into an empty list cannot tell them apart afterwards.

type Rows<R> =
  | { at: 'reading' }
  | { at: 'read'; rows: R[] }
  | { at: 'failed'; why: string }

Mark is the member of the set that does not. It takes children and a colour, and whoever places it has already read whatever the word reports. Nothing in the package draws one on your behalf: Table writes its own failure through Answer, and Tally paints its own change, so a mark appears where a caller puts it and says what the caller wrote.

That is also where the discipline lands. <Mark paint={paint.good}> beside a figure that is still reading is a claim the reading has not made, and no type will catch it.

The colours a mark may take

Paint is a closed union, not string. The source gives the reason twice over. gui's style props accept a theme token type that a literal var(--…) satisfies and a widened string does not. And the set is itself the point — a yes, a no, an unanswered question, and two neutral weights. A further colour would be a distinction with no legend.

ValueTokenWhat it says
paint.quiet--color-text-secondarythe default: qualifying, not judging
paint.plain--color-text-primarythe same, at the weight of the text it follows
paint.good--color-status-gooda yes
paint.warn--color-status-warnan unanswered question
paint.bad--color-status-bada no

Only two of those are hues. --color-status-warn resolves to --lux-state-notice, which tokens.css defines as the loudest neutral rather than a colour, because attention is not a hue in this palette. The other two resolve to --lux-state-error and --lux-state-success, the only hues the brand permits outside the achromatic ladder, both held to AA as text on the card fill.

Because the union is over literal token strings, a colour that arrives as a string will not type-check, and neither will another entry from paint — paint.link and paint.wrong are real tokens and are not things a mark may be.

Where the colours come from

paint is the whole vocabulary this surface is painted with, in the design system's own names, and the five above are a slice of it. It exists because the same handful of colour names had been written out in five files, so a change of mind about a hairline was five edits and a chance for one of them to disagree.

It carries more than colour: elevation (raised, floating, lit, sheen), radius and measure (round, column, prose), the focus ring, and a field's own separate names, which the engine paints from --color-input-* rather than from the surface ladder. Some entries are groups of style props rather than single values, and spread onto a component.

import { Card } from '@luxfi/ui/gui'
import { paint } from '@luxfi/ui/read'

<Card {...paint.card} focusVisibleStyle={paint.focus} />

Names, not values. A component asking for paint.raised is asking for the shadow this design system calls a raised thing, and it moves when the system moves. A component carrying 0 4px 8px rgba(…) is a second design system with one user.

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 { Mark, paint, type Paint } 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 { Mark, paint, type Paint } from '@luxfi/ui/read'
<Mark>epoch 41</Mark>
<Mark paint={position.inRange ? paint.good : paint.bad}>
  {position.inRange ? 'in range' : 'out of range'}
</Mark>

Beside a figure, the mark and the figure are one sentence.

import { SizableText, XStack } from '@luxfi/ui/gui'

<XStack gap="$2" alignItems="baseline">
  <SizableText size="$7" fontWeight="600">{fees}</SizableText>
  <Mark>derived</Mark>
</XStack>

Props

PropTypeRequiredDefault
paintPaintnopaint.quiet
childrenReactNodeyes—

Paint is exported alongside the component, so a caller storing a colour can name its type: const colour: Paint = paint.good.

What it does not do

Mark holds no state, reads nothing, and validates nothing. It will not check that the word agrees with the colour — <Mark paint={paint.good}>refused</Mark> renders exactly as written — and it does not know whether the reading behind the word arrived. It takes no press handler and adds no focus behaviour, so it never enters the tab order and is not a control to be made into one; a mark that needs to be pressed is a button with a mark in it. It has no icon, no dot, no dismiss, and no variants beyond the colour, and nothing truncates: a long phrase pushes the line it sits on rather than eliding. The line box is a fixed 18 pixels while the font size is a token, so at a much larger type setting the text stops tracking the rule around it. And it announces nothing of its own to a screen reader — it is a span, read in sequence with its neighbour, which is the reason the meaning has to be in the words.