Skip to main content

max / makeover-layout

Strip historical narrative from documentation Remove what a doc used to say, when it changed, the incidents that justified a rule, finished migration narration, and counts and versions that rot. State the rules in the present tense instead. Keep every instruction, prohibition and threshold, and keep the measurements that make a rule actionable. Public-facing docs keep their explanatory voice.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-08-31 01:51 UTC
Signed with PGP, not checked
Commit: 5fbe48ddf909a9d79797cae9e0812093c50cdc4b
Parent: d8f2974
3 files changed, +11 insertions, -517 deletions
M README.md +10 -18
@@ -23,19 +23,17 @@
23 23 ## What is here
24 24
25 25 **Depth**: `Bevel`, `Edge`, `Fill`, `Depth`. Fill and bevel are named together,
26 - because naming them apart is what let them disagree. Every consumer had at least
27 - one region carrying a raised bevel over a recessed fill; audiofiles fixed it and
28 - wrote the bug down, Balanced Breakfast still had twelve of them a year later.
29 - `Depth` makes that unrepresentable.
26 + because naming them apart lets them disagree: a region can carry a raised bevel
27 + over a recessed fill. `Depth` makes that unrepresentable.
30 28
31 29 **Components**: `Token` (a badge does not answer a click, a chip does), `Notice`
32 30 (a toast is transient, a banner is persistent), `RowPart`, `Heading`, `Selector`,
33 31 `Readiness`, and `Tone`, the one intent family they share.
34 32
35 33 **Schemas**: `Field` for forms, `Column` for lists and tables. A column carries a
36 - drop priority, which is what replaces addressing columns by position. Both
37 - webview apps hide narrow-screen columns with `nth-child`, so inserting a column
38 - silently hides the wrong one.
34 + drop priority, which is what replaces addressing columns by position. Addressing columns by
35 + `nth-child` means inserting a column silently hides the wrong one on a narrow
36 + screen.
39 37
40 38 **Structure**: `Region` for the parts of a screen, `Arrangement` for how a screen
41 39 is put together. Two arrangements, because the two apps have two. `Showing` says
@@ -48,9 +46,9 @@
48 46
49 47 ## What is not here, on purpose
50 48
51 - Validation. `Field` describes fields, not constraints. Neither app has a shared
52 - story, and a schema that describes one without the other acquires a constraint
53 - layer per app, which is how the divergence this crate exists to end got started.
49 + Validation. `Field` describes fields, not constraints. A schema that describes
50 + fields without constraints acquires a constraint layer per app, which is the
51 + divergence this crate exists to end.
54 52
55 53 ## Where the description stops
56 54
@@ -68,14 +66,8 @@
68 66
69 67 ## Status
70 68
71 - On crates.io at 0.2.0, published 2026-07-29. 0.1.0 went out the day before, and
72 - the sequencing decision behind it was not to publish until a second renderer had
73 - exercised the vocabulary; three had, and it cost the description exactly one
74 - deletion (`Fill::fallback`, which turned out to be renderer policy).
75 -
76 - 0.2.0 went out on a different basis: no renderer has been through the components,
77 - schemas or structure yet. So expect the first renderer to force a correction, and
78 - expect that correction to be a 0.3.0 with a migration rather than an edit.
69 + On crates.io. A vocabulary correction ships as a major bump with a migration
70 + rather than as an edit.
79 71
80 72 Design lives in the wiki note `makeover-layout`; the backlog is in GoingsOn under
81 73 the project of the same name.
M build.rs +1 -2
@@ -7,8 +7,7 @@
7 7 //! version bump here is tree-wide rather than per-repo. Intended: version skew
8 8 //! across consumers is the thing being prevented.
9 9 //!
10 - //! The guard only bites between versions that both declare `links`, so it
11 - //! stops future skew rather than skew against anything published before 0.16.0.
10 + //! The guard only bites between versions that both declare `links`.
12 11
13 12 fn main() {
14 13 println!("cargo::rerun-if-changed=build.rs");
M src/lib.rs -497
@@ -24,564 +24,52 @@
24 24 //!
25 25 //! # Scope
26 26 //!
27 - //! Depth came first: the bevel and the surfaces it shapes. That much was
28 - //! settled the hard way — the vocabulary here was read off audiofiles'
29 - //! `ui::theme` and `ui::widgets`, which are the only implementation written
30 - //! by a consumer with no CSS, then checked against both webview apps. All
31 - //! three agreed once Balanced Breakfast's fills were corrected.
32 - //!
33 - //! 0.2.0 adds the rest of the description, each member drawn the same way,
34 - //! from what the three apps already hand-write rather than from a taxonomy:
35 - //!
36 - //! - Components. [`Token`] (badge against chip), [`Notice`] (toast against
37 - //! banner), [`RowPart`], [`Heading`], [`Selector`], [`Readiness`], and
38 - //! [`Tone`], which is the one intent family they share.
39 - //! - Schemas. [`Field`] for forms and [`Column`] for lists and tables.
40 - //! - Structure. [`Region`] for the parts of a screen, [`Arrangement`] for how
41 - //! a screen is put together.
42 - //!
43 - //! **Validation** was absent on purpose here, on the grounds that neither app
44 - //! had a shared story. That reasoning is retired — see 0.11.0 below, which is
45 - //! where the constraints arrived and why the argument did not survive contact
46 - //! with what the apps were measured to do.
47 - //!
48 - //! 0.3.0 closes a gap the first real adoption found, which is what adopting
49 - //! against goingson first was for. [`Selector`] described only the *chosen*
50 - //! option, so an unchosen one fell through to [`Depth::Flat`] and no renderer
51 - //! drew it; goingson's tab strip recesses its unchosen tabs by hand and could
52 - //! not delete the line, because being recessed is *why* the chosen tab reads as
53 - //! coming forward. So [`Selector::unchosen`] joins `chosen`, and saying it
54 - //! needed [`Fill::Sunken`] and [`Depth::Sunken`]: a surface set back by colour
55 - //! with no edge, which is neither a well nor level-with.
56 - //!
57 - //! 0.7.0 adds [`State`], the interaction axis, closing the gap that adopting
58 - //! against three apps rather than one made visible. The description named
59 - //! rest and, through [`Depth::pressed`], pressed. It named neither focus nor
60 - //! disabled, so `makeover-webview` emitted a hover rule and stopped, and each
61 - //! consumer completed the primitive from outside by out-specifying a rule it
62 - //! did not own: 19 such rules in goingson, 21 in the MNW server, a further set
63 - //! in Balanced Breakfast, and three focus rings that do not match. The axis is
64 - //! deliberately two members wide, because hover and pressed belong where they
65 - //! already are. [`State`]'s own docs carry that argument.
66 - //!
67 - //! 0.8.0 finishes [`Field`], which described a field well enough to label it and
68 - //! not well enough to draw it. Writing `makeover-webview`'s form emitter found
69 - //! three things missing and the renderer supplied all three from outside: the
70 - //! current value, a select's options, and the placeholder. Two of those move
71 - //! here and one does not.
72 - //!
73 - //! - [`Field::placeholder`] is user-facing text sitting beside `label` and
74 - //! `hint`. There was never a reading on which it was renderer state; it was
75 - //! outside only because adding a field to a published struct is breaking.
76 - //! - [`Field::options`] moves because every renderer needs them and each was
77 - //! going to invent its own shape. [`Choice`] is the shape `makeover-webview`
78 - //! already arrived at, taken as-is rather than redesigned.
79 - //! - The current value stays renderer-side and is not coming here. It is the
80 - //! one of the three that is genuinely state: a webview reads it out of the
81 - //! DOM, an immediate-mode renderer holds a `&mut` to the app's own field, and
82 - //! a description that carried it would be a form model.
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 - //!
110 - //! 0.10.0 adds [`Meter`], a proportion carried as a pair rather than as a
111 - //! percentage. Its own docs carry the argument; the short form is that the
112 - //! percentage shape had already been tried in goingson and had already needed a
113 - //! companion flag to recover what rounding and clamping threw away.
114 - //!
115 - //! 0.11.0 is four members from the quasi proving ground, batched into one
116 - //! release because pre-1.0 a minor is breaking and a cascade is nine repos.
117 - //! Three findings that arrived with them turned out not to belong here at all:
118 - //! this crate has no notion of an action, a route or a destination, so anything
119 - //! asking what a control *calls* was never the vocabulary's to say.
120 - //!
121 - //! - [`Figure`], a value with a caption. goingson had five of them across five
122 - //! screens with five class vocabularies for the one shape, which is the
123 - //! divergence this crate exists to end, sitting in plain sight and counted for
124 - //! the first time.
125 - //! - [`RowPart::Proportion`], so a [`Meter`] can sit in a row. `Meter` reached
126 - //! two of its seven sites at 0.10.0 and the other five are row-shaped. Exactly
127 - //! [`RowPart::Tokens`]'s problem with a different payload, and it takes
128 - //! `Tokens`' answer: the part carries the description of a bar, not a node.
129 - //! - [`Field::max_length`], [`Field::min`] and [`Field::max`], joining
130 - //! [`Field::required`], which had been sitting here as the sole constraint
131 - //! while the header above claimed there were none. The set stops before
132 - //! `pattern`, which fails the renderer test and is one site in one app.
133 - //! - [`FieldKind::File`]. Every host has an honest answer — a native picker, an
134 - //! `<input type="file">`, a path prompt, an argument — and it carries no
135 - //! accepted-types list because `accept` appears at zero sites in either app.
136 - //!
137 - //! The evidence rule changed under these, and it is worth recording because four
138 - //! earlier decisions were made under the old one. The two-app test said a shape
139 - //! earns a word once a second app wants it. It is backwards: a rule that
140 - //! withholds a word until a second app has duplicated the code guarantees the
141 - //! duplication, and app three writes it a third time. The bar is now generic
142 - //! against bespoke — is this furniture any app would have, or is it this app's
143 - //! own? Bespoke keeps [`Region::Bespoke`], which already carries a completion
144 - //! heatmap and is the right answer for a calendar nobody will build twice.
145 - //!
146 - //! 0.12.0 is two more from the same proving ground, and the same sorting
147 - //! happened first: six findings came out of a measurement of goingson's whole
148 - //! frontend, and four of them turned out to be asking what a control *calls*,
149 - //! which this crate cannot say. The two that were really here:
150 - //!
151 - //! - [`Readiness`] grows from two states to four. It named `Ready` and
152 - //! `Pending` and stopped, so a screen whose list came back empty had nothing
153 - //! to say about it; goingson draws an empty state at 27 sites and Balanced
154 - //! Breakfast at 9. `Empty` and `Failed` are the same axis rather than a new
155 - //! member beside it, because a region shows one of the four and never two.
156 - //! `#[non_exhaustive]` arrives with them, the pairing [`RowPart`] made at
157 - //! 0.9.0 and for the same reason.
158 - //! - [`Column::sortable`], [`Column::sorted`] and [`Sort`]. The one finding in
159 - //! the set that completes a member rather than adding one: `Column` shipped
160 - //! with a width and a priority and could not say that a table is ordered by a
161 - //! column, so a described table could draw no caret and offer no reordering.
162 - //!
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 - //! 0.15.0 adds [`FieldKind::Date`] and [`FieldKind::DateTime`], on the argument
182 - //! [`FieldKind::Email`] was admitted on: a webview emits a different `type=`,
183 - //! which is a native picker, the platform's validation and a different keyboard
184 - //! on a touch device. Described as text with a "YYYY-MM-DD" hint, all three are
185 - //! lost.
186 - //!
187 - //! Two members and not one or five, from a count rather than from symmetry: 13
188 - //! sites of `date` and 13 of `datetime-local` across the MNW server and
189 - //! goingson, and zero of `time`, `month` or `week`. The wire format each takes
190 - //! is named here as [`DATE_FORMAT`] and [`DATETIME_FORMAT`], because a host
191 - //! left to pick its own would disagree with a server silently, and
192 - //! [`FieldKind::temporal`] is the pair asked about once rather than at each
193 - //! renderer. `FieldKind`'s own comment claiming `radio` was the last HTML input
194 - //! type missing was already false when 0.8.1 wrote it; these are what it was
195 - //! missing.
196 - //!
197 - //! What each of the 0.12.0 findings deliberately leaves out is the address — what pressing a
198 - //! header calls, and where an empty state's "Add your first project" button
199 - //! goes. That is the boundary this crate is defined by, and four findings moved
200 - //! across it rather than being answered here.
201 - //!
202 - //! 0.19.0 narrows [`State`] to [`State::Disabled`] alone. `State::Focus` is
203 - //! gone: a description never states what has focus, because what focus *is*
204 - //! differs per host and every renderer had already decided for itself — the
205 - //! webview draws it from `:focus-visible`, egui refused the variant outright,
206 - //! and quasi-tui honoured it once at startup and overrode it thereafter.
207 - //!
208 - //! 0.20.0 adds [`Region::Widget`], the third tier, and `#[non_exhaustive]` to
209 - //! [`Region`] with it. Every vocabulary finding until now had two answers
210 - //! available — grow the primitive set, or [`Region::Bespoke`] — and a whole
211 - //! class of thing is wrong for both. A carousel is not a primitive, because a
212 - //! terminal has none and that is the test `Node::Html` failed. It is not
213 - //! bespoke either, because bespoke is what one app owns and every part of a
214 - //! carousel is furniture plus members this crate already has.
215 - //!
216 - //! The cost of the binary was that refusing a primitive was expensive: the app
217 - //! hand-rolls the thing forever, so the pressure always ran toward growing the
218 - //! primitive set with one host's idioms. A named assembly changes what "no"
219 - //! costs without changing what the vocabulary can say.
220 - //!
221 - //! MNW's carousel is the first consumer and was the finding that started it: one
222 - //! partial, three pages, an ordered set of frames with a position, prev/next and
223 - //! a dot strip, all of it sayable already and none of it nameable. See wiki
224 - //! `widget-tier` for the ownership model, which is why this member carries a
225 - //! name a renderer may decline to know.
226 - //!
227 - //! 0.21.0 adds [`Image`] and [`Fit`], found by trying to describe MNW's
228 - //! carousel under 0.20.0's widget tier and getting one step in. Nothing named a
229 - //! picture. The vocabulary could say a number with a caption, a badge, a meter
230 - //! and a table, and could not say the thing three of MNW's public pages are
231 - //! mostly made of.
232 - //!
233 - //! It reads as an oversight and is a measurement: 24 `<img>` sites across 22
234 - //! MNW templates, against one in goingson and none in Balanced Breakfast or
235 - //! audiofiles. A picture is furniture a *content platform* has, and MNW is the
236 - //! only one in the tree, so the evidence never arrived from the two-app
237 - //! direction the earlier rule looked in. Under the generic-against-bespoke bar
238 - //! it is not close: a picture is not one app's own.
239 - //!
240 - //! A primitive rather than a widget, which is worth stating now that the tier
241 - //! makes it a real question. A widget is an assembly of things already sayable
242 - //! and a picture is a leaf, assembled from nothing. It also passes the test
243 - //! `Node::Html` failed — every host has an honest answer, including a terminal,
244 - //! which has a graphics protocol or has [`Image::alt`].
245 - //!
246 - //! [`Image`] carries no source, the split [`Act`] already makes: an address is
247 - //! not this crate's to hold. See its own docs, which is where the argument is.
248 - //!
249 - //! 0.22.0 finishes [`Image`], which 0.21.0 shipped unable to say how much room
250 - //! a picture needs. Without that a renderer cannot reserve space, so a picture
251 - //! occupies nothing until its bytes arrive and then takes its full height at
252 - //! once. Measured on MNW's landing page: a 478px jump per frame and a
253 - //! cumulative layout shift of 0.087 for the page.
254 - //!
255 - //! - [`Image::intrinsic`], the picture's own dimensions, carried as [`Extent`].
256 - //! A fact about the asset rather than a display size, which is what keeps it
257 - //! on this side of the deferral rule: 5120x3412 is what the file *is*, and no
258 - //! renderer can learn it without fetching the bytes.
259 - //! - [`Loading`], and the default flips to [`Loading::Eager`]. 0.21.0 emitted
260 - //! the webview's `loading="lazy"` for every picture, which read one
261 - //! consumer's habit as a rule. Deferring a picture that is on screen at first
262 - //! paint saves nothing and makes its shift land later. The carousel is the
263 - //! case that proves this cannot be one renderer-wide setting: its first frame
264 - //! is on screen and its others are not, in one widget, at one moment.
265 - //!
266 - //! 0.23.0 adds [`Showing`], which is three open findings collapsing into one
267 - //! member. A tab group could not say which tab was open, a carousel could not
268 - //! say which frame was up, and a disclosure could not say whether its child was
269 - //! showing. All three are the same missing sentence, and while it was missing a
270 - //! renderer had two moves: match on a widget name, or draw every child.
271 - //!
272 - //! So the widget tier was taking the blame for a gap one level below it. With
273 - //! this a renderer derives its chrome from the description — labels get a strip,
274 - //! no labels get previous/position/next — once, for every widget there will ever
275 - //! be, and [`Region::Widget`]'s name goes back to being app vocabulary a
276 - //! renderer may decline to know.
277 - //!
278 - //! Only the kind lives here. Which child is up, and what each child is called,
279 - //! sit with whatever holds the regions, the same split [`Selector`] already made
280 - //! against `Node::Select`.
281 - //!
282 - //! 0.27.0 adds [`Region::Group`], which closes a gap this crate had carried
283 - //! since 0.2.0 without noticing: [`Heading::Section`] is documented as naming a
284 - //! block within the screen, and there was no block. A section heading is a leaf
285 - //! beside the things it names, so the description could say a section had
286 - //! *started* and never that one had ended.
287 - //!
288 - //! Found by asking how a screen distinguishes groups of settings by colour, and
289 - //! the answer turned out to be two findings rather than one. This is the first
290 - //! and it is the precondition: there is nothing to tint until there is a
291 - //! container. The second — every renderer already resolves `category.one`
292 - //! through `category.six` and no description can reach any of them — is filed
293 - //! and not shipped here.
294 - //!
295 - //! What the colour question settled anyway, because it shapes this member: the
296 - //! group carries no colour and no ordinal. A renderer distinguishing sibling
297 - //! groups derives the assignment from their order, which is
298 - //! [`Region::Columns`]' reasoning about counts applied to colour — the children
299 - //! say, and a value here would be a second source for something the description
300 - //! already states by containing them.
301 - //!
302 - //! 0.28.0 is what audiofiles' forms port found it could not say, three findings
303 - //! filed against a working conversion rather than guessed at in advance. All
304 - //! three are about a *question* rather than about a control, which is the line
305 - //! this crate keeps having to redraw.
306 - //!
307 - //! - [`FieldKind::Range`] and [`Field::step`]. A bounded number the user drags
308 - //! across, where both ends being on screen is what the question means. The
309 - //! reading to resist is that this is [`FieldKind::Number`] with bounds, and it
310 - //! is [`FieldKind::Radio`]'s argument again: a validated number can be out of
311 - //! range and a slider cannot, so the bounds stop being a rule and become the
312 - //! control's extent. [`Field::bounded`] is the check a renderer asks, since a
313 - //! range missing an end has nothing to draw.
314 - //! - [`Choice::unavailable`]. An option that is real, worth showing, and cannot
315 - //! be picked yet. Without it an app either drops the option — and the user
316 - //! never learns it is there — or hand-rolls the control outside the
317 - //! description, which is what audiofiles' instrument panel did: a permanently
318 - //! disabled radio plus a hand-written line saying what would enable it.
319 - //! `#[non_exhaustive]` arrives on [`Choice`] in the same release, so this is
320 - //! the last breaking addition to it.
321 - //! - Not a member at all: [`Field::placeholder`] on a chooser. It was sayable
322 - //! already and no renderer read it, so a select with nothing chosen showed an
323 - //! empty box and the instruction lived on a disabled button elsewhere. The
324 - //! renderers moved, not the description.
325 - //!
326 - //! 0.29.1 adds [`Awaiting`], which is the sentence [`Readiness`] could say about
327 - //! a region and could not say about a control. A described screen could state
328 - //! that a list was on its way and could not state that the button just pressed
329 - //! is doing the thing it was pressed for, so every renderer's in-flight
330 - //! treatment was the app's to hand-write. The MNW server hand-writes it 57 times
331 - //! and hand-writes the guard against a second press twice, which is the half
332 - //! that matters going missing on a codebase that sells things.
333 - //!
334 - //! The mark is the fact that something outstanding will complete, once, in
335 - //! expected finite time. Deliberately not remoteness, since a heavy local query
336 - //! waits too, and deliberately not slowness, which is a judgement rather than a
337 - //! property. It carries an optional amount, stated only when the amount is
338 - //! measured, and it carries no duration at all: a renderer draws what is done
339 - //! over what there is plus the time so far, and never an estimate of what is
340 - //! left.
341 - //!
342 - //! One mark and two readings, which is what keeps a slow region from being
343 - //! hand-split into its own route the way MNW's payout summary is: a pressed
344 - //! control goes busy and locks, a region fed by an awaiting call stands in as
345 - //! [`Readiness::Pending`] and fills when it lands.
346 - //!
347 - //! A patch release for a new member, which is the 0.27.5 precedent rather than a
348 - //! new rule: nothing existing changed shape, so every consumer already asking
349 - //! for 0.29 keeps resolving and the suite below this crate does not have to move
350 - //! for a type only quasi reads. The minor releases above were minor because they
351 - //! also narrowed something.
352 - //!
353 - //! 0.30.0 is two members batched into one release, which is 0.11.0's precedent
354 - //! and its reasoning: pre-1.0 a minor is breaking, the cascade below this crate
355 - //! is seven repos, and paying that twice in a week for two unrelated words is
356 - //! the tax the batching exists to avoid.
357 - //!
358 - //! - [`FieldKind::Rich`], a field whose value is markdown source. The editing
359 - //! counterpart of prose already carried as markdown, and it is renderable
360 - //! everywhere for the reason the carrying is: editing markdown is editing
361 - //! text. It buys a renderer permission to offer a preview or a syntax pass and
362 - //! buys a host reading the value back the knowledge of what it holds; a
363 - //! renderer with neither draws a textarea. It says nothing about when the
364 - //! value is saved, because autosave is a clock. Measured against four MNW
365 - //! section editors that are one shape written four times.
366 - //! [`FieldKind::multiline`] arrives with it, since the pair is now two members
367 - //! every renderer has to ask about.
368 - //! - [`Facet`], [`Selecting`], [`FacetValue`] and [`Standing`]: a named
369 - //! dimension a set is narrowed by. MNW's discover page filters six ways
370 - //! through six mechanisms, and its filter rows carry a tick box *and* a
371 - //! chevron only because a tag's selection and a tag's browse position were
372 - //! held separately. One word covers all six, and [`Selecting::Subtree`] is the
373 - //! member that made it an enum rather than a bool: a tree's selection is
374 - //! branches taken and branches pruned, which no flat mode can express, and
375 - //! making one gesture do browsing and filtering together is what lets the
376 - //! second mechanism go. [`Standing`] has four members rather than a bool for
377 - //! the tree's sake — a value in force because an ancestor is, is not a value
378 - //! somebody picked. [`FacetValue`] splits an identifier from a label for
379 - //! [`Choice`]'s reason and one of its own: two leaves under different parents
380 - //! are legitimately both called "Ambient", and the path is what tells them
381 - //! apart and what nearest-ancestor-wins resolves over. Deliberately wider than
382 - //! that one page: audiofiles' library browser and goingson's filters are the
383 - //! same shape.
384 - //!
385 - //! 0.39.0 gives an *option* its second line. [`Choice::detail`] is the sentence
386 - //! that says what picking one means, and it is one member and not two. Measured
387 - //! 2026-08-29 (`5e21dcfc`), off the MNW server's un-ported markup.
388 - //!
389 - //! - **Six sites, and four of them had already folded it into the label.**
390 - //! `<strong>Public</strong>: Anyone can see this repository` in git settings,
391 - //! the same shape in the project-basics AI tier and the cart's currency
392 - //! conversion, `Mislabeled (wrong AI tier or category)` in the report modal.
393 - //! The item-type and item-pricing wizards give it its own span. The described
394 - //! screens fold it in miniature: `Every 15 minutes (recommended)`. One fact,
395 - //! six spellings, and the folding is what says the member was missing.
396 - //! - **It is not a price**, and the count is what decided that rather than
397 - //! taste. The site that asked carries a name, a price and a description; the
398 - //! tree's other three priced tier lists are not option lists at all — each
399 - //! card carries its own submit, so each is a region with a heading, a fact
400 - //! and an act and is sayable today. A price member would have one consumer
401 - //! and would mean growing a money type this crate does not have.
402 - //! - **Where it is drawn is [`unavailable`](Choice::unavailable)'s question met
403 - //! a third time**, and it takes the same answer: its own element in a radio
404 - //! group, run into the option's own text in a `<select>`, a row of its own on
405 - //! a terminal. A rule the crate had already paid for twice is the cheapest
406 - //! evidence that one member is the right size.
407 - //! - **This does not reopen 0.35.0.** That release ruled [`Candidate`] a type
408 - //! of its own rather than a `Choice` with a second line, and it stays one:
409 - //! what separates them is that an option is picked out of a set the user can
410 - //! see whole and a candidate is offered out of one nobody can see. The
411 - //! argument 0.35.0 made against a member here — that it would land on every
412 - //! option list including the ones with nowhere to draw it — is answered by
413 - //! the two sentences above: it lands as `None` on the ones that do not ask,
414 - //! and the one host with nowhere to draw it already had a rule.
415 - //!
416 - //! 0.38.0 describes the theme picker. [`FieldKind::Theme`], [`ThemeChoice`],
417 - //! [`ThemeVariant`], [`Contrast`], [`Field::themes`] and [`Field::follows`] are
418 - //! one screen's control named as furniture. Ruled by Max 2026-08-28
419 - //! (`70028e00`), against the grouped option list that was the obvious answer.
420 - //!
421 - //! - **The measurement is what rejected `Choice::group`.** `optgroup` appears
422 - //! at exactly one live site in the tree, in the one app not yet ported, and
423 - //! the non-theme grouping count is zero. Two apps' grouped pickers were
424 - //! deleted by their ports and both described replacements dropped the
425 - //! grouping on purpose. So the thing that recurs is not option lists that
426 - //! group; it is this picker, hand-written three times.
427 - //! - **Two of the four facts cannot come from an app.** A theme's group and its
428 - //! measured contrast tier come off the resolved theme, so the layer that
429 - //! loaded it is the only party holding them. `Choice::new(id, "{name}
430 - //! ({variant})")` is what all three apps wrote, and it turns structure into
431 - //! prose and drops the tier entirely.
432 - //! - **The order is the grouping**, rather than a returned list of groups. A
433 - //! renderer that draws headings walks the run of one variant; one that cannot
434 - //! still gets the useful order, and neither shape is made to flatten the
435 - //! other's.
436 - //! - **[`ThemeVariant`] is spelled twice on purpose.** This crate takes no
437 - //! dependencies, so it cannot name `makeover::Variant`, and a renderer that
438 - //! groups needs the groups as values. The adopter converts, in a three-arm
439 - //! match.
440 - //! - **The cost is stated rather than found later.** This is the first member
441 - //! here that names a subject instead of a shape of answer. It stays narrow: a
442 - //! theme picker, not a general host-resolved list. A second such list is when
443 - //! the generalisation gets measured.
444 - //!
445 - //! 0.35.0 gives a suggestion its second line. [`Candidate`] is the entry in a
446 - //! field's suggestion list: a value, a label, and the [`detail`] that orients
447 - //! it. Ruled by Max 2026-08-21 (`1fcf2e9b`) after the combobox member shipped
448 - //! and was then held against the two sites it was designed from, which is an
449 - //! order worth not repeating.
450 - //!
451 - //! - **A candidate is not a [`Choice`], and the difference is in the reading
452 - //! rather than the writing.** Both submit one string and read as another. An
453 - //! option is picked out of a set the user can see whole; a candidate is
454 - //! offered out of a set nobody can see, so it has to say what tells it from
455 - //! its neighbours. Both measured sites draw that second string today, by
456 - //! hand, in a second span.
457 - //! - **It is its own type rather than a member on [`Choice`].** `Choice` is the
458 - //! most-consumed struct in the vocabulary and the member would have landed on
459 - //! every option list in the tree the day it shipped, including the ones with
460 - //! nowhere to draw it.
461 - //! - **[`Candidate`] is `#[non_exhaustive]` from birth**, which is the whole of
462 - //! what 0.28.0 cost and is not being paid twice.
463 - //! - No `unavailable`. A suggestion that cannot be picked is a row a route
464 - //! should not have offered.
465 - //!
466 - //! [`detail`]: Candidate::detail
467 - //!
468 - //! 0.34.0 gives an interval a description. [`FieldKind::Interval`] and
469 - //! [`Field::upper_name`] say that two values are one question with two ends.
470 - //! Ruled by Max 2026-08-21 against audiofiles' six filter axes and the MNW
471 - //! server's price pair, which is HTML saying the grouping in an ARIA
472 - //! `role="group"` and nowhere else.
473 - //!
474 - //! - **The ends constrain each other, and nothing else in the vocabulary could
475 - //! say so.** Two [`FieldKind::Number`] fields are two questions: a renderer
476 - //! draws two labels with no relationship, and [`Field::error`] attaches to one
477 - //! side of a fault that belongs to both.
478 - //! - **It is not [`FieldKind::Range`]**, which is the reading to resist and the
479 - //! same resistance `Range` needed against `Number`. A range is one value
480 - //! inside an extent; this is two, and the extent is a bound on each rather
481 - //! than the question's meaning.
482 - //! - **Both names are stated rather than derived.** Measured the same day, the
483 - //! two sites disagree about affix order -- `bpm_min`/`bpm_max` against
484 - //! `min_price`/`max_price` -- so any rule renames one of them. One member
485 - //! instead of a naming convention this crate would own forever, and which
486 - //! member a name sits in is what says which end it is.
487 - //! - The crossing rule is not enforced, exactly as [`Field::min`] is not. What
488 - //! the description buys is one place to report the fault rather than two.
489 - //!
490 - //! [`Field::upper_name`] is a member on a struct that is not
491 - //! `#[non_exhaustive]`, so this is a formal break for crates.io and zero
492 - //! call-site edits in the tree: every literal builds on a constructor.
493 - //!
494 - //! 0.33.0 gives a number its unit. [`Field::unit`] carries what the value is
495 - //! measured in, and [`FieldKind::measurable`] says which kinds read it. Decided
496 - //! by Max 2026-08-21 (`32215e21`) against eight sites across four audiofiles
497 - //! files that had each independently put the unit in parentheses at the end of
498 - //! the label -- three of them written while the gap was a known open question.
499 - //!
500 - //! - **A unit is a fact about the value, not part of the question's name.** The
501 - //! two readings come apart the moment anything reads a field back rather than
502 - //! drawing it, which is the argument that decided it.
503 - //! - **The convention it replaces froze the worst placement.** A label is the
504 - //! sentence above the control, so unit-in-label was the same answer on every
505 - //! host -- including the host that had somewhere better, since egui's slider
506 - //! already draws a suffix beside the readout, which is what these controls did
507 - //! before they were described.
508 - //! - A string rather than a closed family, which is [`Curve`]'s argument
509 - //! inverted and correctly so: a curve is a mapping this crate computes, and a
510 - //! unit is a symbol it only carries. The measured set is `GiB`, `dBFS`, `s`
511 - //! and `ms`, and this crate does not know what the next consumer measures in.
512 - //!
513 - //! Additive: absent is what every field meant before.
514 - //!
515 - //! 0.32.0 is the slider's real shape. **The data of a slider is a fraction and
516 - //! a function taking numbers to numbers** (Max, 2026-08-21), so [`Curve`]
517 - //! arrives and [`Field::curve`] with it. [`Field::min`] and [`Field::max`] were
518 - //! never the control's extent: a slider's extent is always 0 to 1, and the
519 - //! bounds are `f(0)` and `f(1)`. Linear is the constant-slope case, which is
520 - //! why the mapping was invisible — under it the extent and the bounds coincide
521 - //! numerically — and why four renderers each hard-coded it without anyone
522 - //! deciding to.
523 - //!
Lines truncated