# makeover-timing
The time axis of the make-family design system.
[`makeover`](https://makenot.work/git/max/makeover) resolves colour,
[`makeover-geometry`](https://makenot.work/git/max/makeover-geometry) resolves
distance, and
[`makeover-layout`](https://makenot.work/git/max/makeover-layout) names what a
thing is without resolving anything. This crate answers the remaining question
of the same shape: how long.
## A duration is named by what it is waiting for
| `revert` | a control returning to its resting label after confirming | 1500ms |
| `clear` | a status line emptying itself | 2000ms |
| `dismiss` | a transient notice's lifetime | 3000ms |
| `debounce` | typing settling before the work starts | 150ms |
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-peer` 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.
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.
A divergence between renderers is a bug report, not an axis.
## What is deliberately not a timing intent
**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, 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. Read
together those are one statement, not two durations. A message the user must
not miss does not go away on its own, `makeover-layout` already has the word
for that (such a message is not `Notice::transient`), and a banner has no
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.
## 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.
`Motion` has one rung, `fade` at 300ms, because the tree has one measured
transition. It grows when something is measured, not when a scale looks short.
## Using it
Web surfaces bake the stylesheet in at build time. Nothing here changes at
runtime, so there is no load-time JS step:
```rust
use makeover_timing::timing_css;
std::fs::write("static/timing.css", timing_css())?;
```
```css
:root {
--timing-revert: 1500ms;
--timing-clear: 2000ms;
--timing-dismiss: 3000ms;
--timing-debounce: 150ms;
--motion-fade: 300ms;
}
```
egui and ratatui surfaces resolve through `Duration` instead, which is the
reason this is a crate rather than a stylesheet:
```rust
use makeover_timing::{Intent, Motion, notice_lifetime};
let settle = Intent::Debounce.duration();
let gone = notice_lifetime(true).map(|life| life + Motion::Fade.duration());
```
`notice_lifetime` is the seam the crate was built for. `makeover-layout`
documents `Notice::Toast` as "transient, stacked, dismisses itself" and says
nothing about when; this says when, on the renderer's side of the line. It
takes the bool rather than the enum so this crate stays off `makeover-layout`'s
dependency graph, and the bool is exactly what the description asserts.
## Where the numbers came from
Every value is a count from the tree, not a preference:
```text
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.
## Licence
MIT.