Skip to main content

max / makeover-geometry

5.1 KB · 127 lines History Blame Raw
1 # makeover-geometry
2
3 The invariant half of the make-family design system. [`makeover`]https://makenot.work/git/max/makeover
4 resolves colour, which varies by theme; this crate carries everything that does
5 not.
6
7 ## Spacing is a relationship, not a size
8
9 The Mac OS 8 Human Interface Guidelines specify white space by what two things
10 are being separated, and define no grid unit. A control and its satellite
11 pop-up sit 4 pixels apart; peers stacked in a list get 6; a group box's inner
12 margin is 10; separated groups and rows of push buttons get 12.
13
14 So the vocabulary here names the relationship and lets the size follow, the same
15 way `surface-raised` names an intent and lets the hex follow:
16
17 | Relationship | What it separates |
18 |---|---|
19 | `gap-bound` | a control and the thing it belongs to |
20 | `gap-peer` | items of the same kind in a list |
21 | `gap-group` | a container's inner margin, sibling groups |
22 | `gap-section` | separated groups, rows of actions |
23 | `gap-pane` | panel padding, content shells |
24 | `gap-page` | the outermost shell margin |
25
26 Whether a gap should be 6px or 8px is unanswerable on its own. Whether two
27 things are peers is not. That is the whole argument for the layer.
28
29 A raw `step-*` scale sits underneath for distances no relationship describes,
30 such as an optical nudge inside a badge. Reaching for it is a smell worth a
31 second look.
32
33 ## Ratios, not pixel counts
34
35 The deliberate departure from the HIG, which is written in hard device pixels
36 because in 1997 there was one pixel density and one text size.
37
38 Every step is a ratio of one base unit, `--geometry-base`, which defaults to
39 `1rem`. At that base the ratios land exactly on the HIG's numbers, so nothing
40 is lost in translation. What is gained: the layout tracks the user's text size
41 instead of fighting it, an accessibility setting is one value rather than a
42 sweep, and the scale means the same thing at any display density.
43
44 ```css
45 --step-snug: calc(var(--geometry-base) * 3 / 8); /* 6px at the default base */
46 --gap-peer: var(--step-snug);
47 ```
48
49 ## Density presets
50
51 Because no call site names a size, a preset can change what every relationship
52 resolves to without touching one of them. Mobile and desktop builds should
53 differ mostly by which preset they emit.
54
55 The presets are not scaled copies of each other, which is also why a single
56 base scalar could not express this. Touch is a claim about the contact patch
57 and nothing else: a fingertip is coarse, so adjacent things that do different
58 things need more room between them to be hit reliably.
59
60 | Gap | Pointer | Touch |
61 |---|---|---|
62 | bound | 4 | 4 |
63 | peer | 6 | 10 |
64 | group | 10 | 12 |
65 | section | 12 | 16 |
66 | pane | 24 | 24 |
67 | page | 32 | 32 |
68
69 `gap-peer`, `gap-group` and `gap-section` separate distinct tap targets, so
70 they open. `gap-pane` and `gap-page` are shells rather than targets, so the
71 input device has no opinion about them and they hold. How much screen you have
72 is a separate question from what you are pointing with, and it does not belong
73 on this axis. `gap-bound` never moves: it is the one relationship that says
74 "these are one object", and separating it would say the opposite.
75
76 The one rule across presets is that Touch never resolves tighter than Pointer.
77
78 ```rust
79 use makeover_geometry::{Density, geometry_css_vars, gap_css_overrides};
80
81 let mut css = geometry_css_vars(Density::Pointer);
82 css.push_str(&gap_css_overrides(".ui-mode-mobile", Density::Touch));
83 ```
84
85 The override block emits only the relational layer. The base and the scale are
86 declared once.
87
88 ## Surfaces, and why a terminal is not a third preset
89
90 A terminal is not a density. It is a surface whose smallest representable step
91 is one cell rather than one pixel, and that single difference is the whole of
92 it. `Ratio::quanta` takes a quantum and answers in whole units of it, so the
93 relational vocabulary reaches a character grid with nothing added.
94
95 Quantising is not a terminal special case either. A display quantises to the
96 pixel; it is only that rounding 6.0 to the nearest pixel is uninteresting, while
97 rounding three eighths of a cell to the nearest cell decides the layout.
98
99 ```rust
100 use makeover_geometry::{Density, Gap, Surface};
101
102 let t = Surface::terminal();
103 assert_eq!(t.gap(Gap::Peer, Density::Pointer), 0); // cells
104 assert_eq!(t.gap(Gap::Section, Density::Pointer), 1);
105 assert_eq!(t.gap(Gap::Page, Density::Pointer), 2);
106 ```
107
108 `bound` and `peer` collapsing to zero cells is correct rather than lossy: in a
109 grid that dense, both relationships are expressed by adjacency. A coarse surface
110 genuinely has fewer distinctions available, and the model should say so instead
111 of inventing a gap to keep six names apart. What it must never do is *invert*
112 two relationships, and there is a test across several quanta pinning that.
113
114 So three orthogonal axes: `Gap` is what is being separated, `Density` is who is
115 operating it, `Surface` is what it is drawn on.
116
117 ## Consumers
118
119 Web surfaces bake the CSS in at build time; unlike colour, none of this changes
120 at runtime, so there is nothing for JS to apply on load. egui and ratatui
121 surfaces resolve through `Surface` instead, which is the reason this is a crate
122 rather than a stylesheet.
123
124 ## Licence
125
126 MIT.
127