A screen is a stack of these: the application's band across the top, the route under it, the footer. What makes them one page is that each lays the same three tracks, so the column edge does not move from one to the next.
The room is a track
That is the whole reason this is a grid rather than a box with padding, and it buys one thing that padding cannot give at any price: a row that has to run the full width stays in the flow.
<Page>
<h1>Treasury</h1>
<Cell col="bleed">
<Chart />
</Cell>
<Facts rows={rows} />
</Page>With padding, that chart has to escape the element it is nested in, and the escape is a negative margin whose number is a copy of the padding — two places holding one fact, and a page that goes crooked the day one of them moves.
Every direct child lands in the column without saying so, because that is the
common case and a child that silently landed in a margin is exactly the failure
a named track is for. A Cell that places itself wins: it writes grid-column
inline, which outranks the rule.
The fold is intrinsic
The rails are 1fr above the gutter and the gutter below it:
[bleed-start] minmax(var(--gutter), 1fr)
[column-start] minmax(0, var(--container-max))
[column-end] minmax(var(--gutter), 1fr) [bleed-end]Above the column's width the rails take the rest and the column is centred; below it they give way to their floor and the column takes what is left. There is no breakpoint in it, so there is no breakpoint to keep in step with the ladder, and a page nested inside a narrow region behaves the way it does at a narrow viewport.
--gutter is the design layer's, and it is max(1rem, safe-area-inset) — so on
a phone held sideways the column clears the notch without anything here knowing
what a notch is.
It composes the fleet's grid
Page renders @hanzo/ui/grid's Grid with a track list
and a class. It is not a second grid: tracks() still floors every flexible
column at minmax(0, 1fr), which is what stops one long unbreakable string from
widening its own column and narrowing every sibling, and Cell is the same
Cell.
The gap is the row gap only. The rails are the space either side, so a gap across the columns would add a second one and push the column off centre by half of it.
Installation
npm install @luxfi/ui @hanzo/gui @hanzo/ui @hanzo/design @tanstack/react-queryimport '@hanzo/design/styles.css'
import '@luxfi/ui/tokens.css'
import { Page } from '@luxfi/ui/read'
import { Cell } from '@hanzo/ui/grid'The rule that puts a child in the column ships with the component rather than
in the token sheet. A placement rule in a sheet a caller may not have loaded is
a page that silently rearranges — the children auto-place one per track, so the
first one lands in the left margin at a seventh of the width and nothing reports
it. React hoists and de-duplicates it, so any number of pages contribute one
rule. --gutter and --container-max come from @hanzo/design.
Props
| Prop | Type | Default | |
|---|---|---|---|
max | string | paint.column | How wide the column may get |
inset | string | var(--gutter) | The least room either side |
gap | number | string | $4 | Between the rows |
children | ReactNode | — |
Anything else goes to the underlying Grid, so style, id, role and the
data attributes pass through.
What it does not do
Page lays columns and says nothing about rows: they size to their content, in
document order, and a screen that wants a sidebar wants a grid of its own inside
the column rather than a fourth track here. It sets no vertical rhythm and no
maximum height, and it does not center anything vertically.
It is one level. A page built from three of these — a band, a route, a footer —
is three sibling grids that agree because they are given the same max, not one
grid that guarantees it; passing a different max to one of them moves that
band's column and nothing warns.