| 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 |
## 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 wants **more** room between things |
| 57 |
you tap and **less** around the edges, since a fingertip is coarse and outer |
| 58 |
margin is screen you do not get to use. `gap-peer` and `gap-section` open up; |
| 59 |
`gap-pane` and `gap-page` tighten. `gap-bound` never moves — it is the one |
| 60 |
relationship that says "these are one object". |
| 61 |
|
| 62 |
```rust |
| 63 |
use makeover_geometry::{Density, geometry_css_vars, gap_css_overrides}; |
| 64 |
|
| 65 |
let mut css = geometry_css_vars(Density::Pointer); |
| 66 |
css.push_str(&gap_css_overrides(".ui-mode-mobile", Density::Touch)); |
| 67 |
``` |
| 68 |
|
| 69 |
The override block emits only the relational layer. The base and the scale are |
| 70 |
declared once. |
| 71 |
|
| 72 |
## Surfaces, and why a terminal is not a third preset |
| 73 |
|
| 74 |
A terminal is not a density. It is a surface whose smallest representable step |
| 75 |
is one cell rather than one pixel, and that single difference is the whole of |
| 76 |
it. `Ratio::quanta` takes a quantum and answers in whole units of it, so the |
| 77 |
relational vocabulary reaches a character grid with nothing added. |
| 78 |
|
| 79 |
Quantising is not a terminal special case either. A display quantises to the |
| 80 |
pixel; it is only that rounding 6.0 to the nearest pixel is uninteresting, while |
| 81 |
rounding three eighths of a cell to the nearest cell decides the layout. |
| 82 |
|
| 83 |
```rust |
| 84 |
use makeover_geometry::{Density, Gap, Surface}; |
| 85 |
|
| 86 |
let t = Surface::terminal(); |
| 87 |
assert_eq!(t.gap(Gap::Peer, Density::Pointer), 0); // cells |
| 88 |
assert_eq!(t.gap(Gap::Section, Density::Pointer), 1); |
| 89 |
assert_eq!(t.gap(Gap::Page, Density::Pointer), 2); |
| 90 |
``` |
| 91 |
|
| 92 |
`bound` and `peer` collapsing to zero cells is correct rather than lossy: in a |
| 93 |
grid that dense, both relationships are expressed by adjacency. A coarse surface |
| 94 |
genuinely has fewer distinctions available, and the model should say so instead |
| 95 |
of inventing a gap to keep six names apart. What it must never do is *invert* |
| 96 |
two relationships, and there is a test across several quanta pinning that. |
| 97 |
|
| 98 |
So three orthogonal axes: `Gap` is what is being separated, `Density` is who is |
| 99 |
operating it, `Surface` is what it is drawn on. |
| 100 |
|
| 101 |
## Consumers |
| 102 |
|
| 103 |
Web surfaces bake the CSS in at build time; unlike colour, none of this changes |
| 104 |
at runtime, so there is nothing for JS to apply on load. egui and ratatui |
| 105 |
surfaces resolve through `Surface` instead, which is the reason this is a crate |
| 106 |
rather than a stylesheet. |
| 107 |
|
| 108 |
## Licence |
| 109 |
|
| 110 |
MIT. |
| 111 |
|