A screen usually holds more than it should show at once: the response a figure
was read from, the selectors a call answered to. None of that belongs above the
thing it explains, and none of it earns a page of its own. Fold is the line
you open to read it.
The behaviour is the element's
The browser already has this. details holds the open state, summary is the
control, the keyboard reaches it with no tabIndex, and a screen reader says
whether the fold is open before it reads the line. A box with a click handler
gets none of that for free: it needs a role, aria-expanded, a tab stop, key
handlers for Enter and Space, and state to hold what the element was holding
already — and most of it is what gets written wrong. So the behaviour here is
the element's, and what Fold adds is paint.
The elements themselves, rather than boxes rendered as them. A summary is a
list-item, which is where its marker comes from, and that is not a display the
style engine's boxes can spell, so the component writes details and summary
and hands them style objects.
The summary states a floor of 24 pixels, which is what WCAG 2.2 asks of anything offered to a finger. A line at this type size stands about seventeen, so the vertical padding carries the rest.
What goes under the line carries the reading
Fold takes no reading. Table takes Rows<R>
and Tally takes a Figure, 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 }A fold holds whatever is put under it, so that distinction stays with the material and arrives inside the fold rather than around it.
<Fold summary="Selectors this precompile answered to">
<Table
name="selectors"
columns={columns}
id={(row) => row.selector}
rows={selectors}
empty={{ title: 'This precompile answered nothing' }}
/>
</Fold>The summary line is the part to watch, because it is drawn whether or not the
material under it arrived. A count in it is therefore a claim: Selectors (7)
over a query that failed reports seven selectors nobody read. A summary that
names the material stays true in all three branches, and the count belongs
under the line, where the reading is.
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 { Fold } 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 { Fold } from '@luxfi/ui/read'<Fold summary="Raw response">
<pre>{response}</pre>
</Fold>open opens the fold on arrival, for the one somebody came to read.
<Fold summary="Selectors this precompile answered to" open>
<SelectorList of={answered} />
</Fold>The Lux exchange opens a fold in the seat screen, over the powers a seat acts through, and in the precompile screen, over the selectors a call answered to. Both open the same one, which is why this is a component rather than a pattern.
Props
| Prop | Type | Required |
|---|---|---|
summary | ReactNode | yes |
open | boolean | no |
children | ReactNode | no |
summary is the line itself, and it is what the fold is called: besides the
open state it is all a screen reader has to announce, so it has to read on its
own with the fold shut. open is the element's own attribute, passed straight
through. children render in a gui stack under the
line, with $3 of space between them and above them.
The frame takes its colours from paint — --color-border-divider for the
hairline, --color-bg-primary for the ground, --color-text-primary for the
line — so a change of mind about any of them happens in the token layer rather
than here.
What it does not do
Fold is paint on a native element, and it leaves the element's limits where
they are. There is no onToggle and nothing to hold the open state against, so
a caller learns nothing about whether the reader opened it and cannot keep a
copy of that in step. Folds do not group: the name attribute that makes an
exclusive set is not written, so opening one leaves its neighbours as they were.
Nothing is deferred — children are in the document whether the fold is open or
shut, which means the element hides the material rather than postponing it, and
a child that reads on mount reads while the fold is closed. Nothing animates the
reveal. And the component draws no distinction of its own between material that
is absent and material that could not be read: an empty fold body is the same
picture either way, which is the whole reason the components that go under it
take a reading instead of a value.