Skip to main content

max / makeover-timing

The time axis of the make-family design system: a duration is named by what it is waiting for.

git clone https://makenot.work/git/max/makeover-timing.git git clone git@ssh.makenot.work:max/makeover-timing.git
Name Size
/ dist/
/ scripts/
/ src/
· .gitignore 99 B
· bento.toml 333 B
· Cargo.toml 1.7 KB
· LICENSE 1.0 KB
· README.md 4.6 KB
· rust-toolchain.toml 86 B

README

makeover-timing

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.

A duration is named by what it is waiting for

IntentWhat it waits forResolves to
reverta control returning to its resting label after confirming1500ms
cleara status line emptying itself2000ms
dismissa transient notice’s lifetime3000ms
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-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:

use makeover_timing::timing_css;

std::fs::write("static/timing.css", timing_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:

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:

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.