07 · foundations
6 topics

Data. Held exactly, shown one way.

Money as integers, days as plain strings, changes with a sign, series in the palette. Every data component follows these, so a screen reads the same wherever the number comes from.

view .md
Money
minor units

Money is an integer of minor units: 123456 is 1,234.56 in a currency with cents. Never a float, which drifts after a few sums.

Amount shows it one way everywhere: mono with tabular figures, the real minus sign, the symbol in the muted ink. MoneyInput takes it in, from any pasted format.

Set the locale, the currency and the time zone once, near the root, with FormatProvider. Every component that formats reads it.

app/layout.tsx
tsx
<FormatProvider locale="pt-BR" currency="BRL" timeZone="America/Sao_Paulo">
  {children}
</FormatProvider>

<Amount value={-123456} />          // −R$ 1.234,56
<Amount value={950000} tone="auto" /> // +R$ 9.500,00, in the success ink
Dates
YYYY-MM-DD

A day is a plain string, 2026-09-28, never a Date: a Date carries an instant, and a time zone can move it to the day before.

Today is today in the account's time zone, from FormatProvider. Words and the first day of the week come from the locale.

lib/format holds the helpers: today, addDays, addMonths, compareDates, formatDate, formatDateRange.

Changes
Delta

A change is an arrow, a sign and words. Color only says whether it is good news, and intent decides which way is good: spending up is bad, income up is good.

A percentage needs a positive base. From zero or below, Delta shows the difference in value; never +300% from nothing.

A first value reads new. A missing one is a dash with its reason, never a zero.

Series colors
chart-1 to 8

Series take the palette by name: chart-1 is the brand's hue, the other seven turn it by 45°. Every theme recolors its charts without a line of code.

Store the key a person picked, such as chart-3, never a hex. SwatchPicker hands back the key and names each swatch after the hue it shows.

The status colors appear in a chart only for results above and below zero, and always with a sign.

States
loading, empty, error

A data component takes loading and draws its own skeleton in its final shape: Metric, DataTable, ContributionGrid. Nothing jumps when the values arrive.

Empty and error are composed where the words are known: EmptyState for nothing yet and for filters that hide everything, Alert for a failure that stays, a Badge for data that is stale.

A failed load is never an empty list, which would read as current. DataTable takes an error and shows it in place of the rows.

Hiding values
Redact

RedactProvider hides amounts and marked values behind a mask of their width, for a screen share or a café. Amount, Metric and RollingNumber follow it; wrap anything else in Redact.

It hides from view, not from the page: the values stay in the HTML. It is no place for a secret.

app-shell.tsx
tsx
const [hidden, setHidden] = useState(false)

<RedactProvider hidden={hidden}>
  <Button variant="ghost" aria-pressed={hidden} onClick={() => setHidden(!hidden)}>hide values</Button>
  <Metric label="balance" value={<Amount value={balance} />} />
</RedactProvider>
Active theme: entrepta, dark mode.