Skip to main content

max / makeover-layout

Open RowPart, and let a row carry tokens RowPart was the last closed enum in the vocabulary. It gains Tokens and #[non_exhaustive] in the same release, because the pairing is the point: this enum's own consumer in makeover-webview carried a comment predicting it would stop compiling one day, which is a lockstep break written down and waited for rather than prevented. The finding came from the first two screens described through the router rather than from reading a stylesheet. A goingson project card carries two trailing badges, a type and a toned status; a contact card carries a primary email and a strip of tags. Meta is one slot and one string, so both ports joined their facts with a separator and lost what the second one was: a status read as text where it used to read as colour. Token already said the right thing and could only ever be a node in its own right, never inside a row, so what was missing was permission rather than a concept. Meta and Tokens stay separate rather than one widened slot. The line is whether the thing has its own standing: "3 files" is meta, an amber status and a clickable tag are tokens. A renderer can right-align one string and cannot usefully do the same to a strip of chips. Balanced Breakfast was checked before the member was added, because one consumer wanting something is not evidence. It packs a count and two icon buttons into the same single Meta slot while leaving Actions empty, so the slot was already straining under a second consumer for a different reason.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-09 01:57 UTC
Signed with PGP, not checked
Commit: 304c76afcee0f626494f62c75fe5304ca96c116b
Parent: 7a2b3e2
2 files changed, +74 insertions, -6 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.8.2"
3 + version = "0.9.0"
4 4 edition = "2024"
5 5 description = "The renderer-agnostic half of the make-family design system: what a thing IS, named as intents and relationships and never as values. Colour defers to makeover, spacing to makeover-geometry; what is left is composition."
6 6 license = "MIT"
M src/lib.rs +73 -5
@@ -81,6 +81,32 @@
81 81 //! DOM, an immediate-mode renderer holds a `&mut` to the app's own field, and
82 82 //! a description that carried it would be a form model.
83 83 //!
84 + //! 0.9.0 opens [`RowPart`], which was the last closed enum in the vocabulary,
85 + //! and adds [`RowPart::Tokens`]. Both halves come from the same finding, made
86 + //! by the first two real screens described through the router rather than by
87 + //! reading a stylesheet.
88 + //!
89 + //! A goingson project card carries two trailing badges, a type and a toned
90 + //! status; a contact card carries a primary email *and* a strip of tags. `Meta`
91 + //! is one slot and one string, so both ports joined their facts with a
92 + //! separator and lost what the second one was: a status reads as text where it
93 + //! used to read as colour. [`Token`] already says exactly the right thing — a
94 + //! small labelled thing with a kind, a tone and an optional action — and could
95 + //! only ever be a node in its own right, never inside a row.
96 + //!
97 + //! So the missing thing was permission rather than a concept. `Tokens` is that
98 + //! permission, and `#[non_exhaustive]` arrives with it so the next member is not
99 + //! a lockstep event across three renderers. The pairing is the point: this
100 + //! enum's own consumer in `makeover-webview` carried a comment predicting it
101 + //! would stop compiling one day, which is a lockstep break written down and
102 + //! waited for rather than prevented.
103 + //!
104 + //! Balanced Breakfast was checked before the member was added, because one
105 + //! consumer wanting something is not evidence. It packs a count and two icon
106 + //! buttons into the same single `Meta` slot while leaving `Actions` empty, so
107 + //! the slot was already straining under a second consumer for a different
108 + //! reason.
109 + //!
84 110 //! # Where the description stops
85 111 //!
86 112 //! The bespoke widgets, a day-plan timeline and a kanban board and a calendar,
@@ -543,12 +569,29 @@
543 569
544 570 /// The parts of a list row.
545 571 ///
546 - /// Four, taken from Balanced Breakfast, which is the only consumer that had all
547 - /// of them (`row-primary`, `row-secondary`, `row-meta`, `row-actions`).
548 - /// audiofiles has two and no slot structure at all, so it gains meta and
549 - /// actions as real work rather than a rename; goingson moves off
572 + /// Four to begin with, taken from Balanced Breakfast, which was the only
573 + /// consumer that had all of them (`row-primary`, `row-secondary`, `row-meta`,
574 + /// `row-actions`). audiofiles has two and no slot structure at all, so it gains
575 + /// meta and actions as real work rather than a rename; goingson moves off
550 576 /// `task-row` / `task-cell`.
577 + ///
578 + /// [`Tokens`](Self::Tokens) joined at 0.9.0, and `#[non_exhaustive]` with it.
579 + /// See the crate header for why the two arrived together.
580 + ///
581 + /// # Meta against Tokens
582 + ///
583 + /// The line is whether the thing has its own standing. `Meta` is one short
584 + /// trailing fact about the row, written as text: a count, a size, a date.
585 + /// `Tokens` is a set of small labelled things, each of which can be toned and
586 + /// can answer a click. "3 files" is meta. A status badge that is amber, and a
587 + /// tag you can click to filter by, are tokens.
588 + ///
589 + /// Keeping them apart is what a single widened slot would have foreclosed. A
590 + /// renderer can right-align one string and cannot usefully do the same to a
591 + /// strip of chips, and a fact that is not clickable should not be drawn as
592 + /// though it were.
551 593 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
594 + #[non_exhaustive]
552 595 pub enum RowPart {
553 596 /// The thing itself. What the row is called.
554 597 Primary,
@@ -558,6 +601,14 @@
558 601 Meta,
559 602 /// Controls that act on this row.
560 603 Actions,
604 + /// Small labelled things belonging to the row: badges, chips, tags.
605 + ///
606 + /// Each carries its own [`Token`] kind and [`Tone`], so a renderer with no
607 + /// colour still has the kind to work with, and one with no chips still has
608 + /// the label. That is the constrained-consumer test this vocabulary exists
609 + /// to pass, and it is why the tone lives on the token rather than on the
610 + /// part.
611 + Tokens,
561 612 }
562 613
563 614 impl RowPart {
@@ -583,6 +634,9 @@
583 634 Self::Meta => "content-muted",
584 635 // Actions carry controls rather than text, so they inherit.
585 636 Self::Actions => "content",
637 + // So do tokens: each one carries its own tone, and a part-level
638 + // intent underneath it would fight the token that sits on it.
639 + Self::Tokens => "content",
586 640 }
587 641 }
588 642 }
@@ -1323,7 +1377,12 @@
1323 1377
1324 1378 #[test]
1325 1379 fn only_the_actions_part_hides_until_hovered() {
1326 - for p in [RowPart::Primary, RowPart::Secondary, RowPart::Meta] {
1380 + for p in [
1381 + RowPart::Primary,
1382 + RowPart::Secondary,
1383 + RowPart::Meta,
1384 + RowPart::Tokens,
1385 + ] {
1327 1386 assert!(!p.revealed_on_hover(), "{p:?} should always be visible");
1328 1387 }
1329 1388 assert!(RowPart::Actions.revealed_on_hover());
@@ -1333,6 +1392,15 @@
1333 1392 assert_eq!(RowPart::Meta.intent(), "content-muted");
1334 1393 }
1335 1394
1395 + #[test]
1396 + fn a_token_part_carries_no_intent_of_its_own() {
1397 + // Each token carries its own tone, so a part-level intent underneath
1398 + // would fight the thing sitting on it. Same reasoning as actions, which
1399 + // is why they answer alike.
1400 + assert_eq!(RowPart::Tokens.intent(), RowPart::Actions.intent());
1401 + assert_eq!(RowPart::Tokens.intent(), "content");
1402 + }
1403 +
1336 1404 #[test]
1337 1405 fn a_separator_is_what_tells_a_section_from_a_subsection() {
1338 1406 assert!(Heading::Section.separated());