Expand description
The renderer-agnostic half of the make-family design system.
makeover answers what colour, and varies by theme. makeover-geometry
answers how much space, and varies by density and surface. This crate
answers what the thing is, and varies by nothing.
§The deferral rule
A description names intents and relationships, never values. Say
Fill::Raised, never #D9DDF4. Say Gap::Peer, never 6px. What is
left once colour and spacing are deferred is composition: which edges
are lit, what inverts on press, what nests in what.
The constraint that shapes all of it: a renderer that can only paint
rectangles has to be able to express the result. egui has no
box-shadow: inset and one stroke per widget with no per-side control; a
terminal has box-drawing characters and one cell of resolution, and cannot
draw a two-tone lit edge at all. A description that assumes per-side edges
is a CSS description wearing a neutral name. So this crate names the
intent — this region is a well — and each renderer chooses an expression
it can actually produce, including dropping half of one.
§Scope
Depth came first: the bevel and the surfaces it shapes. That much was
settled the hard way — the vocabulary here was read off audiofiles’
ui::theme and ui::widgets, which are the only implementation written
by a consumer with no CSS, then checked against both webview apps. All
three agreed once Balanced Breakfast’s fills were corrected.
0.2.0 adds the rest of the description, each member drawn the same way, from what the three apps already hand-write rather than from a taxonomy:
- Components.
Token(badge against chip),Notice(toast against banner),RowPart,Heading,Selector,Readiness, andTone, which is the one intent family they share. - Schemas.
Fieldfor forms andColumnfor lists and tables. - Structure.
Regionfor the parts of a screen,Arrangementfor how a screen is put together.
Validation was absent on purpose here, on the grounds that neither app had a shared story. That reasoning is retired — see 0.11.0 below, which is where the constraints arrived and why the argument did not survive contact with what the apps were measured to do.
0.3.0 closes a gap the first real adoption found, which is what adopting
against goingson first was for. Selector described only the chosen
option, so an unchosen one fell through to Depth::Flat and no renderer
drew it; goingson’s tab strip recesses its unchosen tabs by hand and could
not delete the line, because being recessed is why the chosen tab reads as
coming forward. So Selector::unchosen joins chosen, and saying it
needed Fill::Sunken and Depth::Sunken: a surface set back by colour
with no edge, which is neither a well nor level-with.
0.7.0 adds State, the interaction axis, closing the gap that adopting
against three apps rather than one made visible. The description named
rest and, through Depth::pressed, pressed. It named neither focus nor
disabled, so makeover-webview emitted a hover rule and stopped, and each
consumer completed the primitive from outside by out-specifying a rule it
did not own: 19 such rules in goingson, 21 in the MNW server, a further set
in Balanced Breakfast, and three focus rings that do not match. The axis is
deliberately two members wide, because hover and pressed belong where they
already are. State’s own docs carry that argument.
0.8.0 finishes Field, which described a field well enough to label it and
not well enough to draw it. Writing makeover-webview’s form emitter found
three things missing and the renderer supplied all three from outside: the
current value, a select’s options, and the placeholder. Two of those move
here and one does not.
Field::placeholderis user-facing text sitting besidelabelandhint. There was never a reading on which it was renderer state; it was outside only because adding a field to a published struct is breaking.Field::optionsmoves because every renderer needs them and each was going to invent its own shape.Choiceis the shapemakeover-webviewalready arrived at, taken as-is rather than redesigned.- The current value stays renderer-side and is not coming here. It is the
one of the three that is genuinely state: a webview reads it out of the
DOM, an immediate-mode renderer holds a
&mutto the app’s own field, and a description that carried it would be a form model.
0.9.0 opens RowPart, which was the last closed enum in the vocabulary,
and adds RowPart::Tokens. Both halves come from the same finding, made
by the first two real screens described through the router rather than by
reading a stylesheet.
A goingson project card carries two trailing badges, a type and a toned
status; a contact card carries a primary email and a strip of tags. Meta
is one slot and one string, so both ports joined their facts with a
separator and lost what the second one was: a status reads as text where it
used to read as colour. Token already says exactly the right thing — a
small labelled thing with a kind, a tone and an optional action — and could
only ever be a node in its own right, never inside a row.
So the missing thing was permission rather than a concept. Tokens is that
permission, and #[non_exhaustive] arrives with it so the next member is not
a lockstep event across three renderers. The pairing is the point: this
enum’s own consumer in makeover-webview carried a comment predicting it
would stop compiling one day, which is a lockstep break written down and
waited for rather than prevented.
Balanced Breakfast was checked before the member was added, because one
consumer wanting something is not evidence. It packs a count and two icon
buttons into the same single Meta slot while leaving Actions empty, so
the slot was already straining under a second consumer for a different
reason.
0.10.0 adds Meter, a proportion carried as a pair rather than as a
percentage. Its own docs carry the argument; the short form is that the
percentage shape had already been tried in goingson and had already needed a
companion flag to recover what rounding and clamping threw away.
0.11.0 is four members from the quasi proving ground, batched into one release because pre-1.0 a minor is breaking and a cascade is nine repos. Three findings that arrived with them turned out not to belong here at all: this crate has no notion of an action, a route or a destination, so anything asking what a control calls was never the vocabulary’s to say.
Figure, a value with a caption. goingson had five of them across five screens with five class vocabularies for the one shape, which is the divergence this crate exists to end, sitting in plain sight and counted for the first time.RowPart::Proportion, so aMetercan sit in a row.Meterreached two of its seven sites at 0.10.0 and the other five are row-shaped. ExactlyRowPart::Tokens’s problem with a different payload, and it takesTokens’ answer: the part carries the description of a bar, not a node.Field::max_length,Field::minandField::max, joiningField::required, which had been sitting here as the sole constraint while the header above claimed there were none. The set stops beforepattern, which fails the renderer test and is one site in one app.FieldKind::File. Every host has an honest answer — a native picker, an<input type="file">, a path prompt, an argument — and it carries no accepted-types list becauseacceptappears at zero sites in either app.
The evidence rule changed under these, and it is worth recording because four
earlier decisions were made under the old one. The two-app test said a shape
earns a word once a second app wants it. It is backwards: a rule that
withholds a word until a second app has duplicated the code guarantees the
duplication, and app three writes it a third time. The bar is now generic
against bespoke — is this furniture any app would have, or is it this app’s
own? Bespoke keeps Region::Bespoke, which already carries a completion
heatmap and is the right answer for a calendar nobody will build twice.
0.12.0 is two more from the same proving ground, and the same sorting happened first: six findings came out of a measurement of goingson’s whole frontend, and four of them turned out to be asking what a control calls, which this crate cannot say. The two that were really here:
Readinessgrows from two states to four. It namedReadyandPendingand stopped, so a screen whose list came back empty had nothing to say about it; goingson draws an empty state at 27 sites and Balanced Breakfast at 9.EmptyandFailedare the same axis rather than a new member beside it, because a region shows one of the four and never two.#[non_exhaustive]arrives with them, the pairingRowPartmade at 0.9.0 and for the same reason.Column::sortable,Column::sortedandSort. The one finding in the set that completes a member rather than adding one:Columnshipped with a width and a priority and could not say that a table is ordered by a column, so a described table could draw no caret and offer no reordering.
What each of those deliberately leaves out is the address — what pressing a header calls, and where an empty state’s “Add your first project” button goes. That is the boundary this crate is defined by, and four findings moved across it rather than being answered here.
§Where the description stops
The bespoke widgets, a day-plan timeline and a kanban board and a calendar, are not describable here and will not become describable. A description expressive enough to produce a timeline is a widget library wearing a description’s name. Generate the boring 80% so the bespoke 20% gets the attention.
Region::Bespoke is how that limit is stated rather than hidden. The
description names the place and the app owns the contents, so a screen
containing a timeline is still a whole screen and still routable. Without
it, the four goingson screens that make the app worth using would need a
second, undescribed path beside the router, and two paths is how a
vocabulary starts drifting from its app again.
Structs§
- Choice
- One option offered by a field
FieldKind::offers_optionsaccepts. - Column
- One column of a table.
- Field
- One field of a form.
- Figure
- One figure with a caption: a number and what it counts.
- Meter
- How much of a set is done.
Enums§
- Arrangement
- How a screen is laid out.
- Bevel
- Which way the light falls across a two-tone edge.
- Depth
- How a region sits relative to the surface behind it.
- Edge
- One side of a bevel, named by the intent it takes.
- Field
Kind - What kind of value a form field takes.
- Fill
- A surface intent a region is filled with.
- Heading
- How far down the heading tree a title sits.
- Notice
- Something the app is telling the user, unprompted.
- Priority
- What a column is worth when there is not room for all of them.
- Readiness
- What is in a region right now.
- Region
- A named part of a screen.
- RowPart
- The parts of a list row.
- Selector
- A control that picks between things.
- Sort
- Which way a column is ordered.
- State
- An interaction state a region can be in, beside whatever
Depthit is. - Token
- A small labelled thing that sits inside something else.
- Tone
- What a region is saying, when it is saying something.
- Width
- How much room a column asks for.
Traits§
- Intent
- A colour intent this crate refers to but never resolves.