| 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 |
|
| 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 |
## Type, named the same way |
| 34 |
|
| 35 |
The type axis makes the same move. `Text` names what a piece of text is and the |
| 36 |
size follows: |
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
| `text-fine` | timestamps, badges, legal lines | 12px | |
| 41 |
| `text-note` | metadata, table cells, captions, form help | 14px | |
| 42 |
| `text-body` | running copy | 16px | |
| 43 |
| `text-lead` | emphasised copy, card titles | 18px | |
| 44 |
| `text-subhead` | the third heading level | 20px | |
| 45 |
| `text-head` | section headings, the second level | 24px | |
| 46 |
| `text-title` | the page's own title | 32px | |
| 47 |
| `text-display` | display copy, above the hierarchy | 40px | |
| 48 |
| `text-hero` | a landing hero, at most one per page | 48px | |
| 49 |
|
| 50 |
Whether a caption should be 13px or 14px cannot be reviewed. Whether a piece of |
| 51 |
text is a caption can. |
| 52 |
|
| 53 |
Nothing below 12px. A size under that is a legibility problem rather than a |
| 54 |
tier, and text that should recede can recede by colour or weight instead. |
| 55 |
|
| 56 |
Type does not shift on touch. Density is a claim about the contact patch, and |
| 57 |
text is not a tap target; the reader's own root font size is already the knob, |
| 58 |
and it moves this whole ramp. |
| 59 |
|
| 60 |
## Ratios, not pixel counts |
| 61 |
|
| 62 |
The deliberate departure from the HIG, which is written in hard device pixels |
| 63 |
because in 1997 there was one pixel density and one text size. |
| 64 |
|
| 65 |
Every step is a ratio of one base unit, `--geometry-base`, which defaults to |
| 66 |
`1rem`. At that base the ratios land exactly on the HIG's numbers, so nothing |
| 67 |
is lost in translation. What is gained: the layout tracks the user's text size |
| 68 |
instead of fighting it, an accessibility setting is one value rather than a |
| 69 |
sweep, and the scale means the same thing at any display density. |
| 70 |
|
| 71 |
```css |
| 72 |
--step-snug: calc(var(--geometry-base) * 3 / 8); /* 6px at the default base */ |
| 73 |
--gap-peer: var(--step-snug); |
| 74 |
``` |
| 75 |
|
| 76 |
## Density presets |
| 77 |
|
| 78 |
Because no call site names a size, a preset can change what every relationship |
| 79 |
resolves to without touching one of them. Mobile and desktop builds should |
| 80 |
differ mostly by which preset they emit. |
| 81 |
|
| 82 |
The presets are not scaled copies of each other, which is also why a single |
| 83 |
base scalar could not express this. Touch is a claim about the contact patch |
| 84 |
and nothing else: a fingertip is coarse, so adjacent things that do different |
| 85 |
things need more room between them to be hit reliably. |
| 86 |
|
| 87 |
|
| 88 |
|
| 89 |
| bound | 4 | 4 | |
| 90 |
| peer | 6 | 10 | |
| 91 |
| group | 10 | 12 | |
| 92 |
| section | 12 | 16 | |
| 93 |
| pane | 24 | 24 | |
| 94 |
| page | 32 | 32 | |
| 95 |
|
| 96 |
`gap-peer`, `gap-group` and `gap-section` separate distinct tap targets, so |
| 97 |
they open. `gap-pane` and `gap-page` are shells rather than targets, so the |
| 98 |
input device has no opinion about them and they hold. How much screen you have |
| 99 |
is a separate question from what you are pointing with, and it does not belong |
| 100 |
on this axis. `gap-bound` never moves: it is the one relationship that says |
| 101 |
"these are one object", and separating it would say the opposite. |
| 102 |
|
| 103 |
The one rule across presets is that Touch never resolves tighter than Pointer. |
| 104 |
|
| 105 |
```rust |
| 106 |
use makeover_geometry::{Density, geometry_css_vars, gap_css_overrides}; |
| 107 |
|
| 108 |
let mut css = geometry_css_vars(Density::Pointer); |
| 109 |
css.push_str(&gap_css_overrides(".ui-mode-mobile", Density::Touch)); |
| 110 |
``` |
| 111 |
|
| 112 |
The override block emits only the relational layer. The base and the scale are |
| 113 |
declared once. |
| 114 |
|
| 115 |
## Surfaces, and why a terminal is not a third preset |
| 116 |
|
| 117 |
A terminal is not a density. It is a surface whose smallest representable step |
| 118 |
is one cell rather than one pixel, and that single difference is the whole of |
| 119 |
it. `Ratio::quanta` takes a quantum and answers in whole units of it, so the |
| 120 |
relational vocabulary reaches a character grid with nothing added. |
| 121 |
|
| 122 |
Quantising is not a terminal special case either. A display quantises to the |
| 123 |
pixel; it is only that rounding 6.0 to the nearest pixel is uninteresting, while |
| 124 |
rounding three eighths of a cell to the nearest cell decides the layout. |
| 125 |
|
| 126 |
```rust |
| 127 |
use makeover_geometry::{Density, Gap, Surface}; |
| 128 |
|
| 129 |
let t = Surface::terminal(); |
| 130 |
assert_eq!(t.gap(Gap::Peer, Density::Pointer), 0); // cells |
| 131 |
assert_eq!(t.gap(Gap::Section, Density::Pointer), 1); |
| 132 |
assert_eq!(t.gap(Gap::Page, Density::Pointer), 2); |
| 133 |
``` |
| 134 |
|
| 135 |
`bound` and `peer` collapsing to zero cells is correct rather than lossy: in a |
| 136 |
grid that dense, both relationships are expressed by adjacency. A coarse surface |
| 137 |
genuinely has fewer distinctions available, and the model should say so instead |
| 138 |
of inventing a gap to keep six names apart. What it must never do is *invert* |
| 139 |
two relationships, and there is a test across several quanta pinning that. |
| 140 |
|
| 141 |
So three orthogonal axes: `Gap` is what is being separated, `Density` is who is |
| 142 |
operating it, `Surface` is what it is drawn on. |
| 143 |
|
| 144 |
## Consumers |
| 145 |
|
| 146 |
Web surfaces bake the CSS in at build time; unlike colour, none of this changes |
| 147 |
at runtime, so there is nothing for JS to apply on load. egui and ratatui |
| 148 |
surfaces resolve through `Surface` instead, which is the reason this is a crate |
| 149 |
rather than a stylesheet. |
| 150 |
|
| 151 |
## Licence |
| 152 |
|
| 153 |
MIT. |
| 154 |
|