Skip to main content

max / makeover-timing

4.6 KB · 123 lines History Blame Raw
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 | Intent | What it waits for | Resolves to |
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