Skip to main content

Crate makeover_timing

Crate makeover_timing 

Source
Expand description

The time axis of the make-family design system.

makeover resolves colour, makeover-geometry resolves distance, and makeover-layout names what a thing is without resolving anything. This crate answers the remaining question of the same shape: how long.

The move is the one the family makes everywhere. A duration is named by what it is waiting for, and the number follows:

IntentWhat it waits forResolves to
Intent::Reverta control returning to its resting label after confirming1500ms
Intent::Cleara status line emptying itself2000ms
Intent::Dismissa transient notice’s lifetime3000ms
Intent::Debouncetyping settling before the work starts150ms

Whether a toast should live 3000ms or 3500ms is unanswerable on its own. Whether a message is a toast or a banner is not. That is the whole argument for the layer, and it is the same one Gap makes about six pixels.

§One duration per intent, on every renderer

An intent resolves to exactly one duration everywhere. Not one per renderer, not one per theme.

The alternative was considered and rejected on 2026-08-21: a per-renderer table, resolving the way Density resolves gaps. Distance has a renderer axis because a fingertip is coarser than a cursor and a terminal cell is coarser than a pixel — the surface differs. Time does not differ that way. A second is a second in a browser, in egui and in a terminal, and the reader waiting it out is the same reader. What looked like a renderer disagreement in the tree turned out to be drift: MNW debounced one typeahead at 150ms and two others at 200ms, in one repo, on one renderer, and no per-renderer table would have caught that.

So a divergence here is a bug report, not an axis.

§What is deliberately not a timing intent

Three classes were measured out of scope on 2026-08-18, and leaving them out is most of what makes the four above coherent.

A race is not an intent. Waiting 300ms before navigating because the write “should” have landed, or 150ms before closing a dropdown so a mousedown can beat the blur, is a synchronisation bug wearing a duration’s clothes. Naming those would launder them into design decisions and give every future one a token to hide behind. They get fixed per screen.

Severity is not an intent either, and it is not a fifth number. Two renderers already reached for one: MNW gives an error toast 6000ms against an ordinary one’s 3000ms, and audiofiles’ footer never expires an error at all while an ordinary message goes at 30s. Read together those are not two durations, they are one statement — a message the user must not miss does not go away on its own — and makeover-layout already has the word for it: such a message is not Notice::transient. It is a banner, and a banner has no lifetime. See notice_lifetime.

A poll interval is not an intent. A retry backoff, a health check, an update check: those are answerable from what they talk to, not from what a reader can follow, and nothing here has an opinion about them.

§Motion is a separate axis

Intent says how long a state lasts. Motion says how long a change takes. CSS itself draws that line — a setTimeout against a transition-duration — and folding the two together is what makes a “timing scale” unusable: 300ms of fade and 3000ms of toast are not two rungs of one ramp, they are answers to different questions.

Motion has one rung today because the tree has one measured transition. It is an enum rather than a constant so the second one has somewhere to go, and it grows when something is measured, not when a scale looks short.

§Where the numbers came from

Every value below is a count from the tree, taken 2026-08-18 and re-checked 2026-08-21, not a preference:

revert     1500ms   MNW: 6 hand-rolled sites, plus core/clipboard.ts's own default
clear      2000ms   MNW: 4 sites
dismiss    3000ms   MNW: the toast renderer's lifetime
debounce    150ms   audiofiles SEARCH_DEBOUNCE, MNW docs-search.js
fade        300ms   MNW: TOAST_FADE_MS, matching the .fade-out transition

The one contested value is the debounce, where MNW’s two category typeaheads sit at 200ms against everything else’s 150ms. 150 wins on the count and on the cross-renderer agreement, and the 200s conform.

§Consumers

Web surfaces bake timing_css in at build time. Nothing here changes at runtime, so there is no load-time JS step, exactly as with geometry. egui and ratatui surfaces read Intent::duration instead, which is why this is a crate rather than a stylesheet.

Enums§

Intent
A duration named by what it is waiting for.
Motion
How long a change takes, as opposed to how long a state lasts.

Functions§

notice_lifetime
How long a notice lives, given whether the description calls it transient.
timing_css
The time axis as a stylesheet, inside the family’s cascade layer.
timing_css_declarations
Emit the time axis as CSS declarations, no selector.
timing_css_vars
Emit the whole time axis as a :root { … } block.