| 1 |
# makeover-timing |
| 2 |
|
| 3 |
The time axis of the make-family design system. |
| 4 |
[`makeover`](https://makenot.work/git/max/makeover) resolves colour, |
| 5 |
[`makeover-geometry`](https://makenot.work/git/max/makeover-geometry) resolves |
| 6 |
distance, and |
| 7 |
[`makeover-layout`](https://makenot.work/git/max/makeover-layout) names what a |
| 8 |
thing is without resolving anything. This crate answers the remaining question |
| 9 |
of the same shape: how long. |
| 10 |
|
| 11 |
## A duration is named by what it is waiting for |
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
| `revert` | a control returning to its resting label after confirming | 1500ms | |
| 16 |
| `clear` | a status line emptying itself | 2000ms | |
| 17 |
| `dismiss` | a transient notice's lifetime | 3000ms | |
| 18 |
| `debounce` | typing settling before the work starts | 150ms | |
| 19 |
|
| 20 |
Whether a toast should live 3000ms or 3500ms is unanswerable on its own. |
| 21 |
Whether a message is a toast or a banner is not. That is the whole argument for |
| 22 |
the layer, and it is the same one `gap-peer` makes about six pixels. |
| 23 |
|
| 24 |
## One duration per intent, on every renderer |
| 25 |
|
| 26 |
An intent resolves to exactly one duration everywhere. Not one per renderer, |
| 27 |
not one per theme. |
| 28 |
|
| 29 |
Distance has a renderer axis because a fingertip is coarser than a cursor and a |
| 30 |
terminal cell is coarser than a pixel: the surface differs. Time does not |
| 31 |
differ that way. A second is a second in a browser, in egui and in a terminal, |
| 32 |
and the reader waiting it out is the same reader. |
| 33 |
|
| 34 |
A divergence between renderers is a bug report, not an axis. |
| 35 |
|
| 36 |
## What is deliberately not a timing intent |
| 37 |
|
| 38 |
**A race is not an intent.** Waiting 300ms before navigating because the write |
| 39 |
"should" have landed, or 150ms before closing a dropdown so a mousedown can |
| 40 |
beat the blur, is a synchronisation bug wearing a duration's clothes. Naming |
| 41 |
those would launder them into design decisions and give every future one a |
| 42 |
token to hide behind. They get fixed per screen. |
| 43 |
|
| 44 |
**Severity is not an intent, and it is not a fifth number.** Two renderers |
| 45 |
already reached for one: MNW gives an error toast 6000ms against an ordinary |
| 46 |
one's 3000ms, and audiofiles' footer never expires an error at all. Read |
| 47 |
together those are one statement, not two durations. A message the user must |
| 48 |
not miss does not go away on its own, `makeover-layout` already has the word |
| 49 |
for that (such a message is not `Notice::transient`), and a banner has no |
| 50 |
lifetime. |
| 51 |
|
| 52 |
**A poll interval is not an intent.** A retry backoff, a health check, an |
| 53 |
update check: those are answerable from what they talk to, not from what a |
| 54 |
reader can follow. |
| 55 |
|
| 56 |
## Motion is a separate axis |
| 57 |
|
| 58 |
`Intent` says how long a state lasts. `Motion` says how long a change takes. |
| 59 |
CSS itself draws that line, a `setTimeout` against a `transition-duration`, and |
| 60 |
folding the two together is what makes a "timing scale" unusable: 300ms of fade |
| 61 |
and 3000ms of toast are not two rungs of one ramp. |
| 62 |
|
| 63 |
`Motion` has one rung, `fade` at 300ms, because the tree has one measured |
| 64 |
transition. It grows when something is measured, not when a scale looks short. |
| 65 |
|
| 66 |
## Using it |
| 67 |
|
| 68 |
Web surfaces bake the stylesheet in at build time. Nothing here changes at |
| 69 |
runtime, so there is no load-time JS step: |
| 70 |
|
| 71 |
```rust |
| 72 |
use makeover_timing::timing_css; |
| 73 |
|
| 74 |
std::fs::write("static/timing.css", timing_css())?; |
| 75 |
``` |
| 76 |
|
| 77 |
```css |
| 78 |
:root { |
| 79 |
--timing-revert: 1500ms; |
| 80 |
--timing-clear: 2000ms; |
| 81 |
--timing-dismiss: 3000ms; |
| 82 |
--timing-debounce: 150ms; |
| 83 |
|
| 84 |
--motion-fade: 300ms; |
| 85 |
} |
| 86 |
``` |
| 87 |
|
| 88 |
egui and ratatui surfaces resolve through `Duration` instead, which is the |
| 89 |
reason this is a crate rather than a stylesheet: |
| 90 |
|
| 91 |
```rust |
| 92 |
use makeover_timing::{Intent, Motion, notice_lifetime}; |
| 93 |
|
| 94 |
let settle = Intent::Debounce.duration(); |
| 95 |
let gone = notice_lifetime(true).map(|life| life + Motion::Fade.duration()); |
| 96 |
``` |
| 97 |
|
| 98 |
`notice_lifetime` is the seam the crate was built for. `makeover-layout` |
| 99 |
documents `Notice::Toast` as "transient, stacked, dismisses itself" and says |
| 100 |
nothing about when; this says when, on the renderer's side of the line. It |
| 101 |
takes the bool rather than the enum so this crate stays off `makeover-layout`'s |
| 102 |
dependency graph, and the bool is exactly what the description asserts. |
| 103 |
|
| 104 |
## Where the numbers came from |
| 105 |
|
| 106 |
Every value is a count from the tree, not a preference: |
| 107 |
|
| 108 |
```text |
| 109 |
revert 1500ms MNW: 6 hand-rolled sites, plus core/clipboard.ts's own default |
| 110 |
clear 2000ms MNW: 4 sites |
| 111 |
dismiss 3000ms MNW: the toast renderer's lifetime |
| 112 |
debounce 150ms audiofiles SEARCH_DEBOUNCE, MNW docs-search.js |
| 113 |
fade 300ms MNW: TOAST_FADE_MS, matching the .fade-out transition |
| 114 |
``` |
| 115 |
|
| 116 |
The one contested value is the debounce, where MNW's two category typeaheads |
| 117 |
sit at 200ms against everything else's 150ms. 150 wins on the count and on the |
| 118 |
cross-renderer agreement, and the 200s conform. |
| 119 |
|
| 120 |
## Licence |
| 121 |
|
| 122 |
MIT. |
| 123 |
|