Skip to main content

max / makeover-layout

0.14.0: a surface can sit over the page, and a table cell says what it holds Two additive members on two non_exhaustive enums, in one release. Publishing twice for two additive changes is waste, and the cascade below this crate is nine repos. Depth::Overlay. The enum named raised, well, sunken and flat, and could not name a surface that sits over the page rather than in it. Every renderer had the surface already and none could be reached from a description: makeover-tui's Palette::overlay, makeover-immediate's Palette::elevation since 0.10.0, makeover-webview's --elevation-overlay. What was missing was the route. It takes a fill and no bevel, which is the wave-2 rule stated as code: a surface over the page is lifted off it, a surface in the page is cut into it. bevel() gets its own arm rather than joining Flat | Sunken, because those two already give two different reasons for having no edge and this is a third. pressed() leaves it alone; an overlay is a surface, not a control. CellPart, which is RowPart for tables. A row's parts have carried their own content intent since 0.2.0, so .row-actions inherits rather than taking a text colour. A cell had no such vocabulary, makeover-webview emitted one undifferentiated .cell, and a button inside a cell was painted as text. That is the exact drift RowPart::intent prevents for rows, unprevented for tables. Four members, counted off what quasi's Cell was measured to hold rather than off the shape of RowPart: value, tokens (33 cells across 22 server templates), actions (30 rows carrying a control, 5 beside a value), link (35 cells across 18 templates). No hover-reveal member: RowPart's was retired at 0.13.0, a cell's actions are shown at rest everywhere measured, and a member nothing uses is one three renderers owe an answer for.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-11 18:17 UTC
Signed with PGP, not checked
Commit: 47bb01992982931954b1fd099b9c8c24baf12885
Parent: e5e9aa8
2 files changed, +149 insertions, -2 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.13.0"
3 + version = "0.14.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 +148 -1
@@ -160,7 +160,25 @@
160 160 //! with a width and a priority and could not say that a table is ordered by a
161 161 //! column, so a described table could draw no caret and offer no reordering.
162 162 //!
163 - //! What each of those deliberately leaves out is the address — what pressing a
163 + //! 0.14.0 is two additive members on two `#[non_exhaustive]` enums, released
164 + //! together because publishing twice for that is waste and the cascade below
165 + //! this crate is nine repos.
166 + //!
167 + //! - [`Depth::Overlay`]. The enum could say raised, well, sunken and flat, and
168 + //! could not say that a surface sits *over* the page. Every renderer already
169 + //! had the surface — `makeover-tui`'s `Palette::overlay`,
170 + //! `makeover-immediate`'s `Palette::elevation`, `makeover-webview`'s
171 + //! `--elevation-overlay` — and none of them could be reached from a
172 + //! description. buckets_of_money has 16 modals waiting on it.
173 + //! - [`CellPart`], which is [`RowPart`] for tables. A row's parts have carried
174 + //! their own content intent since 0.2.0, so `.row-actions` inherits rather
175 + //! than taking a text colour; a table cell had no such vocabulary and
176 + //! `makeover-webview` emitted one undifferentiated `.cell`, so a button in a
177 + //! cell was painted as text. The four members are the four things quasi's
178 + //! `Cell` was measured to hold, and the count is in that crate's history
179 + //! rather than assumed here.
180 + //!
181 + //! What each of the 0.12.0 findings deliberately leaves out is the address — what pressing a
164 182 //! header calls, and where an empty state's "Add your first project" button
165 183 //! goes. That is the boundary this crate is defined by, and four findings moved
166 184 //! across it rather than being answered here.
@@ -366,6 +384,21 @@
366 384 /// Recessed and level-with are different claims, and only one of them
367 385 /// needs a colour.
368 386 Sunken,
387 + /// A surface sitting *over* the page rather than in it. A modal, a popover,
388 + /// a menu.
389 + ///
390 + /// Takes elevation and no bevel: a surface overlaying the page is lifted
391 + /// off it, and a surface in the page is cut into it. That is the same
392 + /// pairing rule the rest of the enum holds, applied to the one case where
393 + /// the separation is not an edge at all — the lift and the scrim behind it
394 + /// are already saying where the surface is.
395 + ///
396 + /// Every renderer had the surface before it had this variant.
397 + /// `makeover-tui` carries `Palette::overlay`, `makeover-immediate` gained
398 + /// `Palette::elevation` at 0.10.0, and `makeover-webview` emits
399 + /// `--elevation-overlay`. What was missing was the route from a description
400 + /// to any of them, which is why this is one variant rather than a feature.
401 + Overlay,
369 402 }
370 403
371 404 impl Depth {
@@ -377,6 +410,11 @@
377 410 // because nothing separates it from its surroundings, and Sunken has
378 411 // none because its colour is already doing the separating.
379 412 Self::Flat | Self::Sunken => None,
413 + // A third reason to have no edge, which is why it gets its own arm
414 + // rather than joining the two above: an overlay is separated by the
415 + // lift and by the scrim behind it, so an edge would be a second
416 + // answer to a question already answered.
417 + Self::Overlay => None,
380 418 Self::Raised => Some(Bevel::Raised),
381 419 Self::Well => Some(Bevel::Inset),
382 420 }
@@ -393,10 +431,14 @@
393 431 Self::Raised => Some(Fill::Raised),
394 432 Self::Well => Some(Fill::Well),
395 433 Self::Sunken => Some(Fill::Sunken),
434 + Self::Overlay => Some(Fill::Overlay),
396 435 }
397 436 }
398 437
399 438 /// Pressing a raised region reads as a well, and nothing else moves.
439 + ///
440 + /// [`Depth::Overlay`] is untouched along with the rest: an overlay is a
441 + /// surface, not a control, so there is nothing there to press.
400 442 #[must_use]
401 443 pub const fn pressed(self) -> Self {
402 444 match self {
@@ -1657,6 +1699,65 @@
1657 1699 }
1658 1700 }
1659 1701
1702 + /// What a table cell holds.
1703 + ///
1704 + /// [`RowPart`] for tables, and it exists for the same reason: a part that
1705 + /// carries a control is not text, and a renderer with one class for the whole
1706 + /// cell paints it as though it were. `makeover-webview` emitted a single
1707 + /// `.cell` until 0.25.0, so a button in a cell inherited the cell's content
1708 + /// colour, which is the exact drift [`RowPart::intent`] prevents for rows and
1709 + /// prevented for nothing here.
1710 + ///
1711 + /// Four members, and the count is what quasi's `Cell` was measured to carry:
1712 + /// a value, tokens (33 cells across 22 server templates), actions (30 rows
1713 + /// carrying a control, 5 beside a value) and a link (35 cells across 18
1714 + /// templates). Nothing was added past what something holds.
1715 + ///
1716 + /// `#[non_exhaustive]` for [`RowPart`]'s reason: growth here must not be a
1717 + /// lockstep event across three renderers.
1718 + ///
1719 + /// # No hover-reveal
1720 + ///
1721 + /// [`RowPart`] carried a `revealed_on_hover` until 0.13.0 retired it, and this
1722 + /// enum never gets one. A cell's actions are shown at rest in every consumer
1723 + /// measured, and a member nothing uses is one three renderers owe an answer
1724 + /// for.
1725 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1726 + #[non_exhaustive]
1727 + pub enum CellPart {
1728 + /// The cell's own text.
1729 + Value,
1730 + /// Small labelled things in the cell: a status badge, a chip.
1731 + Tokens,
1732 + /// Controls that act on what the row is about.
1733 + Actions,
1734 + /// The cell's value, where the value is itself a link.
1735 + Link,
1736 + }
1737 +
1738 + impl CellPart {
1739 + /// The content intent the part takes.
1740 + ///
1741 + /// One part is text and three are not, so three answer with the intent
1742 + /// inheriting already gives. That is [`RowPart::intent`]'s shape with the
1743 + /// text side narrower: a cell's secondary and muted readings are the
1744 + /// column's business, not the cell's.
1745 + #[must_use]
1746 + pub const fn intent(self) -> &'static str {
1747 + match self {
1748 + Self::Value => "content",
1749 + // A token carries its own tone, and a part-level intent underneath
1750 + // it would fight the token sitting on it.
1751 + Self::Tokens => "content",
1752 + // Actions carry controls rather than text.
1753 + Self::Actions => "content",
1754 + // A link takes the action colour from the control it is, rather
1755 + // than the cell's text colour from the cell it sits in.
1756 + Self::Link => "content",
1757 + }
1758 + }
1759 + }
1760 +
1660 1761 #[cfg(test)]
1661 1762 mod tests {
1662 1763 use super::*;
@@ -1963,6 +2064,23 @@
1963 2064 // Only raised regions respond to being pressed.
1964 2065 assert_eq!(Depth::Flat.pressed(), Depth::Flat);
1965 2066 assert_eq!(Depth::Well.pressed(), Depth::Well);
2067 + // An overlay is a surface, not a control.
2068 + assert_eq!(Depth::Overlay.pressed(), Depth::Overlay);
2069 + }
2070 +
2071 + #[test]
2072 + fn an_overlay_is_lifted_rather_than_edged() {
2073 + // The wave-2 rule: a surface over the page takes elevation, a surface
2074 + // in the page takes a bevel. Both halves come off the one Depth, so
2075 + // they cannot disagree.
2076 + assert_eq!(Depth::Overlay.fill(), Some(Fill::Overlay));
2077 + assert_eq!(Depth::Overlay.bevel(), None);
2078 +
2079 + // Three depths have no bevel and they are not the same claim. Flat has
2080 + // nothing to separate from, Sunken's colour is doing the separating,
2081 + // and an overlay is separated by the lift.
2082 + assert_ne!(Depth::Overlay.fill(), Depth::Sunken.fill());
2083 + assert_ne!(Depth::Overlay.fill(), Depth::Flat.fill());
1966 2084 }
1967 2085
1968 2086 #[test]
@@ -2034,6 +2152,35 @@
2034 2152 assert_eq!(RowPart::Tokens.intent(), "content");
2035 2153 }
2036 2154
2155 + #[test]
2156 + fn a_cell_part_names_an_intent_and_only_the_value_is_text() {
2157 + // The table half of what RowPart::intent does for rows. A cell holding
2158 + // a control and a cell holding text answered alike until 0.14.0, and a
2159 + // control in a cell took the cell's text colour.
2160 + assert_eq!(CellPart::Value.intent(), "content");
2161 +
2162 + for part in [CellPart::Tokens, CellPart::Actions, CellPart::Link] {
2163 + // Each for its own reason -- a token carries its tone, an action is
2164 + // a control, a link takes the action colour -- and all three reach
2165 + // the intent inheriting already gives.
2166 + assert_eq!(part.intent(), CellPart::Value.intent(), "{part:?}");
2167 + }
2168 + }
2169 +
2170 + #[test]
2171 + fn every_cell_part_answers_with_a_token_and_never_a_value() {
2172 + for part in [
2173 + CellPart::Value,
2174 + CellPart::Tokens,
2175 + CellPart::Actions,
2176 + CellPart::Link,
2177 + ] {
2178 + let intent = part.intent();
2179 + assert!(!intent.is_empty(), "{part:?} names nothing");
2180 + assert!(!intent.starts_with('#'), "{part:?} looks like a value");
2181 + }
2182 + }
2183 +
2037 2184 #[test]
2038 2185 fn a_separator_is_what_tells_a_section_from_a_subsection() {
2039 2186 assert!(Heading::Section.separated());