Skip to main content

max / makeover-layout

0.25.0: name peer columns, and measure the other two refusals Region::Columns. Peers across, all equals, which is the shape Split is not: a split's two panes stand in master-detail, where the left chooses what the right shows. These choose nothing about each other. It carries no count and no share. How many is what the children say, and a count here would be a second source for something the description already states by containing them; how wide is settled by peers being equal. What happens when there is no room -- scroll, wrap, collapse to one -- is presentation and every host may answer differently. A terminal stacking them vertically is honouring this member, not degrading it. A Region rather than an Arrangement, because a board is usually a region inside a page that also has a band over it. As an Arrangement a screen would be either a board or a list-detail and never a band above a board, and Arrangement::share would need an answer peers do not have. The header's remaining two refusals are measured rather than repeated. Kanban is this one member: held against goingson's tasks-kanban.js every card fact was already sayable and only "these columns are peers" was not. Calendar is no members and no consumer -- a month grid renders as a Table today, the monthly review already describes its month as a list of days and marks today with a badge, events-calendar.js was deleted in August, and no MNW template mentions one. One sentence refused three things for one reason and the reason was wrong three different ways. Count the members.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-15 16:25 UTC
Signed with PGP, not checked
Commit: 3b44f6adc589ab5bdd23b1ddb094b4e9a5a041d9
Parent: 64581dc
2 files changed, +99 insertions, -4 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.24.0"
3 + version = "0.25.0"
4 4 edition = "2024"
5 5 # One copy of this vocabulary per dependency graph, enforced by cargo rather
6 6 # than by remembering. Two versions of a description layer in one build means
M src/lib.rs +98 -3
@@ -320,9 +320,36 @@
320 320 //! Those are presentation, they stay the renderer's, and [`Track`] carries none
321 321 //! of them. What changed is the conclusion, not the principle.
322 322 //!
323 - //! A kanban board and a calendar have not been measured the same way. They are
324 - //! open questions rather than settled refusals, and whoever needs one should
325 - //! count the missing members first.
323 + //! ## The other two, measured 2026-08-15
324 + //!
325 + //! The same sentence refused a kanban board and a calendar. Both were counted
326 + //! the way the timeline should have been, and neither came out where the
327 + //! refusal put it.
328 + //!
329 + //! **Kanban: one member, and it is [`Region::Columns`].** Held against
330 + //! goingson's `tasks-kanban.js`, every card fact was already sayable — title,
331 + //! project, due date, the blocked and unblocks badges, subtask progress, the
332 + //! open action and the context menu are `Row`'s existing parts. A column is a
333 + //! heading, a count and a list. What nothing could say was that the columns are
334 + //! *peers*: [`Arrangement`] offers list-detail and sidebar-content, and a board
335 + //! described as either is a lie about the screen. Dragging a card between
336 + //! columns never entered into it — a drop's effect is "set status", a discrete
337 + //! action `Row`'s menu already carries, and the drag itself is affordance.
338 + //!
339 + //! **Calendar: no members, and no consumer.** A month grid renders today as a
340 + //! [`Table`](crate::Column): seven weekday columns, weeks as rows, blanks for
341 + //! the offset. goingson's monthly review reached this conclusion before this
342 + //! note did and describes its month as a list of days that had something on
343 + //! them, marking today with an ordinary badge. What the tree actually contains
344 + //! is two completion heatmaps — one scalar per day — and no calendar at all:
345 + //! `events-calendar.js` was deleted 2026-08-11 in favour of a segmented list,
346 + //! and no MNW template mentions one. So the refusal was defending a screen
347 + //! nobody has. If one is built, measure again; the facts already fit and only
348 + //! the grid's shape would be in question.
349 + //!
350 + //! The pattern worth keeping from all three: one sentence refused three things
351 + //! for one reason, and the reason was wrong three different ways. Count the
352 + //! members.
326 353 //!
327 354 //! [`Region::Bespoke`] remains for the genuinely app-owned, and its
328 355 //! justification does not depend on the withdrawn claim. The
@@ -1836,6 +1863,36 @@
1836 1863 Pane,
1837 1864 /// Two panes side by side, where the left chooses what the right shows.
1838 1865 Split,
1866 + /// Peer regions across, all of them equals.
1867 + ///
1868 + /// A kanban board's columns, and the shape [`Split`](Self::Split) is not:
1869 + /// a split's two panes stand in a master-detail relationship, where the
1870 + /// left chooses what the right shows. These choose nothing about each
1871 + /// other. Each is a whole region and the set is the arrangement.
1872 + ///
1873 + /// # What it does not carry
1874 + ///
1875 + /// **How many.** The children say, and a count here would be a second
1876 + /// source for something the description already states by containing them.
1877 + ///
1878 + /// **How wide.** Peers are equal by definition, so there is no [`Share`] to
1879 + /// state. A board whose columns wanted different widths would be a
1880 + /// different member, and no app has one.
1881 + ///
1882 + /// **What happens when there is no room.** Scroll across, wrap, or collapse
1883 + /// to one column at a time: all three are right on some host, none is
1884 + /// derivable from the description, and every one of them is presentation.
1885 + /// A terminal that stacks them vertically is honouring this, not degrading
1886 + /// it.
1887 + ///
1888 + /// # Why it is not an `Arrangement`
1889 + ///
1890 + /// [`Arrangement`] is the page's shape, and a board is usually a region
1891 + /// *inside* a page that also has a band over it. Naming it here composes;
1892 + /// naming it there would make a screen either a board or a list-detail and
1893 + /// never a band above a board. It also keeps [`Arrangement::share`]
1894 + /// meaningful, which a peer arrangement has no answer for.
1895 + Columns,
1839 1896 /// A set of panes, one visible at a time, with a [`Selector::Tabs`] above.
1840 1897 TabGroup,
1841 1898 /// Content over a scrim, taking input until dismissed.
@@ -1927,6 +1984,10 @@
1927 1984 pub const fn depth(self) -> Depth {
1928 1985 match self {
1929 1986 Self::Band | Self::Sidebar | Self::Split | Self::TabGroup => Depth::Flat,
1987 + // Flat, and it is the container rather than the columns. Each
1988 + // column is its own region and brings its own depth; a well here
1989 + // would put a second edge around a row of wells.
1990 + Self::Columns => Depth::Flat,
1930 1991 // A pane is looked into, the same as a table body or a tag tree.
1931 1992 Self::Pane => Depth::Well,
1932 1993 Self::Modal => Depth::Raised,
@@ -1979,6 +2040,7 @@
1979 2040 | Self::Sidebar
1980 2041 | Self::Pane
1981 2042 | Self::Split
2043 + | Self::Columns
1982 2044 | Self::TabGroup
1983 2045 | Self::Modal => None,
1984 2046 }
@@ -3421,6 +3483,39 @@
3421 3483 assert!(!Selector::Toggle.abutting());
3422 3484 }
3423 3485
3486 + #[test]
3487 + fn columns_are_peers_and_a_split_is_not() {
3488 + // The distinction the member exists for. A split's two panes stand in a
3489 + // master-detail relationship; columns choose nothing about each other.
3490 + // Both are flat, so depth cannot tell them apart and the doc has to.
3491 + assert_eq!(Region::Columns.depth(), Depth::Flat);
3492 + assert_eq!(Region::Split.depth(), Depth::Flat);
3493 + assert_ne!(Region::Columns, Region::Split);
3494 + }
3495 +
3496 + #[test]
3497 + fn columns_carry_no_count_and_no_share() {
3498 + // The two things a board is always asked to carry and must not. How
3499 + // many is what the children say; how wide is settled by "peers are
3500 + // equal".
3501 + //
3502 + // The guard is the binding itself and it is a compile-time one: adding
3503 + // a field to `Columns` stops this line compiling, which is a better
3504 + // failure than any assertion about it. Written out rather than inlined
3505 + // for exactly that reason.
3506 + let columns: Region<'_> = Region::Columns;
3507 + assert_eq!(columns.name(), None);
3508 + }
3509 +
3510 + #[test]
3511 + fn columns_are_described_and_the_escape_hatch_is_still_one_member() {
3512 + // A board's contents are ordinary description all the way down, so a
3513 + // renderer that does not lay them across still draws every column.
3514 + // Stacking them vertically is honouring this member, not degrading it.
3515 + assert!(Region::Columns.described());
3516 + assert!(!Region::Bespoke { name: "timeline" }.described());
3517 + }
3518 +
3424 3519 #[test]
3425 3520 fn a_span_never_has_zero_minutes_however_it_is_asked_for() {
3426 3521 // Every renderer divides by this. A caller passing a backwards or empty