Skip to main content

max / makeover-webview

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: e5ff051c38625064f526e4a647ff4b77eb29188b
Parent: 7942bd7
10 files changed, +220 insertions, -375 deletions
M README.md +15 -17
@@ -17,10 +17,9 @@
17 17
18 18 ## Phase A: the stylesheet
19 19
20 - No markup here, deliberately. GoingsOn has 145 `innerHTML` sites and Balanced
21 - Breakfast 175 `createElement` sites, so moving markup is a migration while
22 - adopting a generated stylesheet is a deletion. The apps keep every line of their
23 - markup and gain the classes.
20 + No markup here, deliberately. Both webview apps build their markup in JS, so
21 + moving markup is a migration while adopting a generated stylesheet is a
22 + deletion. The apps keep every line of their markup and gain the classes.
24 23
25 24 ```css
26 25 :root {
@@ -33,9 +32,9 @@
33 32 .card:active { background: var(--surface-well, var(--surface-page)); box-shadow: var(--bevel-inset); }
34 33 ```
35 34
36 - Those two custom properties are byte-identical to what both apps already
37 - hand-write, which is asserted in the tests. Adoption removes duplicated lines
38 - rather than changing a pixel.
35 + Those two custom properties are byte-identical to what both apps hand-write,
36 + which is asserted in the tests. Adoption removes duplicated lines rather than
37 + changing a pixel.
39 38
40 39 The depth classes are surfaces and nothing more. A raised object that is not
41 40 pressable is spelled `.raised`, and the states belong to the named surfaces:
@@ -43,8 +42,7 @@
43 42 press, focus and disabled.
44 43
45 44 `.card:active` is the one thing this renderer gets for free: the cascade carries
46 - a pressed state that an immediate-mode renderer resolves per call site, eighteen
47 - of them in audiofiles.
45 + a pressed state that an immediate-mode renderer resolves per call site.
48 46
49 47 ## Phase B: form markup
50 48
@@ -61,10 +59,10 @@
61 59 ```
62 60
63 61 It emits strings because both apps interpolate fields into larger string-built
64 - forms, and it escapes them itself. One escaper covers both sinks: goingson needs
65 - four and a correct choice at 543 call sites only because `textContent`
66 - serialization will not encode a double quote, which is not a constraint Rust
67 - has. The single hole is `form::Markup`, which a caller has to name.
62 + forms, and it escapes them itself. One escaper covers both sinks, because the
63 + `textContent` serialization that forces a JS app to pick between escapers is not
64 + a constraint Rust has. The single hole is `form::Markup`, which a caller has to
65 + name.
68 66
69 67 Three things the emitter does that neither app does at initial render:
70 68
@@ -80,9 +78,9 @@
80 78
81 79 ## Substitution, three ways
82 80
83 - `Fill::Well` has no colour on makeover before 2.3.0, and the three renderers
84 - answer that differently. That is the evidence that taking `Fill::fallback` out of
85 - the description was right:
81 + A theme with no `surface-well` is answered differently by each of the three
82 + renderers, which is why substitution belongs to a renderer and not to the
83 + description:
86 84
87 85 - `makeover-immediate` substitutes the page in Rust.
88 86 - `makeover-tui` refuses to substitute and draws an edge, because a terminal
@@ -92,7 +90,7 @@
92 90
93 91 ## Status
94 92
95 - On crates.io at 0.5.1, which is the CSS half only; the form markup is unreleased.
93 + On crates.io, the CSS half only; the form markup is unreleased.
96 94 Consumers are GoingsOn and Balanced Breakfast, both through makeover-build.
97 95
98 96 `cargo run --example dump` prints the stylesheet.
M build.rs +3 -5
@@ -4,12 +4,10 @@
4 4 //!
5 5 //! This crate decides class names. Two versions in one build means the
6 6 //! stylesheet a build script generates and the markup the runtime emits can
7 - //! come off different vocabularies and still compile, which is what goingson
8 - //! was doing until 2026-08-11: layout.css written by 0.25.1 and tables.css by
9 - //! 0.26.0, from one build.rs run.
7 + //! come off different vocabularies and still compile: one build.rs run writing
8 + //! layout.css from one version and tables.css from another.
10 9 //!
11 - //! The guard only bites between versions that both declare `links`, so it
12 - //! stops future skew rather than skew against anything published before 0.28.0.
10 + //! The guard only bites between versions that both declare `links`.
13 11
14 12 fn main() {
15 13 println!("cargo::rerun-if-changed=build.rs");
M src/corpus.rs +5 -8
@@ -1,15 +1,12 @@
1 1 //! A render of everything this crate can emit, scraped for class names.
2 2 //!
3 3 //! [`vocabulary::names`](crate::vocabulary::names) claims to hold every class
4 - //! this crate can put in markup, and until 0.59.0 nothing checked the claim. It
5 - //! was wrong by a whole family: `quasi-webview`'s own corpus guard found
6 - //! `cell-fill`, `form-group` and `form-label` coming out in rendered documents
7 - //! and had to carry them in a `MAKEOVER_UNLISTED` constant of its own, because
8 - //! an app checking its stylesheet against our set alone concludes that its
9 - //! rules for them are dead and deletes live styling.
4 + //! this crate can put in markup, and this is what checks the claim. A name
5 + //! missing from that set is worse than a name missing from the stylesheet: an
6 + //! app checking its own stylesheet against the set concludes that its rules for
7 + //! the name are dead and deletes live styling.
10 8 //!
11 - //! What that guard recorded is also why this is a render and not a scan of the
12 - //! emitters: a careful read of `quasi-webview`'s emitters produced 35 names and
9 + //! This is a render and not a scan of the emitters: a careful read of `quasi-webview`'s emitters produced 35 names and
13 10 //! its corpus found 14 more. A class assembled at runtime -- a width class, a
14 11 //! drop class, a state appended to an attribute already open -- is a literal
15 12 //! nowhere in this source, and that is the shape of every name that was
M src/figure.rs +4 -5
@@ -1,8 +1,7 @@
1 1 //! A figure with a caption, and a strip of them.
2 2 //!
3 - //! The fourth phase-B emitter. `makeover_layout::Figure` arrived at 0.11.0 after
4 - //! goingson turned out to have five of these across five screens, each with its
5 - //! own class names for the one shape: `task-overview-stat`, `stat-box`,
3 + //! The fourth phase-B emitter, for `makeover_layout::Figure`. Left to an app,
4 + //! one shape grows a class name per screen: `task-overview-stat`, `stat-box`,
6 5 //! `month-stat-item`, `contact-summary-stat`, `sync-stat`.
7 6 //!
8 7 //! # Why the strip has its own function
@@ -144,8 +143,8 @@
144 143
145 144 /// Several figures as one strip, written into a buffer the caller already has.
146 145 ///
147 - /// [`figures_html`]'s streaming form, byte-identical to it. A strip is where the
148 - /// per-figure `String` used to be paid for once per tile.
146 + /// [`figures_html`]'s streaming form, byte-identical to it. A strip is where a
147 + /// per-figure `String` would otherwise be paid for once per tile.
149 148 pub fn figures_html_into(figures: &[Figure<'_>], opts: &Emit, out: &mut String) {
150 149 out.push_str("<div class=\"");
151 150 push_class(out, "figures", opts);
M src/form.rs +18 -33
@@ -26,14 +26,10 @@
26 26 //!
27 27 //! # What the description does not carry
28 28 //!
29 - //! One thing: the **current value**, which arrives in [`Filling`].
30 - //!
31 - //! It used to be three. Writing this emitter is what found them, and the other
32 - //! two turned out not to be renderer state at all — the placeholder is
33 - //! user-facing text that sits with `label` and `hint`, and a select's options
34 - //! are needed by every renderer, which is how each of them ends up inventing a
35 - //! near-miss of the same struct. Both moved down into `makeover-layout` 0.8.0,
36 - //! `Choice` included, and this crate reads them off [`Field`] now.
29 + //! One thing: the **current value**, which arrives in [`Filling`]. The
30 + //! placeholder and a select's options are not renderer state: the first is
31 + //! user-facing text sitting with `label` and `hint`, and the second is needed by
32 + //! every renderer, so both are read off [`Field`].
37 33 //!
38 34 //! The value stays, and it is not a leftover. A webview reads it back out of
39 35 //! the DOM, an immediate-mode renderer writes through a `&mut`, and a terminal
@@ -55,11 +51,9 @@
55 51 /// which reads the generated sheet, so the unruled half of a renderer's
56 52 /// vocabulary can only be written down.
57 53 ///
58 - /// What went wrong without it: an app checking its stylesheet against
59 - /// [`crate::vocabulary::names`] concluded that its live `.form-group` and
60 - /// `.form-label` rules matched nothing and were safe to delete. quasi-webview
61 - /// carried them in a `MAKEOVER_UNLISTED` constant of its own until 0.59.0
62 - /// rather than let that happen.
54 + /// What goes wrong without it: an app checking its stylesheet against
55 + /// [`crate::vocabulary::names`] concludes that its live `.form-group` and
56 + /// `.form-label` rules match nothing and are safe to delete.
63 57 pub const FIELD_CLASSES: &[&str] = &[
64 58 "field",
65 59 "form-checkbox-label",
@@ -122,10 +116,9 @@
122 116 /// select holds is the `value` of one of [`Field::options`]'s
123 117 /// [`Choice`]s.
124 118 ///
125 - /// It carried the options too until makeover-layout 0.8.0 moved them onto
126 - /// the field, which collapsed a `Chosen { options, value }` variant into
127 - /// this one. `makeover-immediate` arrived at the same single-variant shape
128 - /// on its own, from the other direction.
119 + /// The options are the field's and never this type's, which is what keeps
120 + /// a `Chosen { options, value }` variant from existing.
121 + /// `makeover-immediate` carries the same single-variant shape.
129 122 Text(&'a str),
130 123 /// A checkbox, on or off.
131 124 On(bool),
@@ -139,8 +132,6 @@
139 132 /// Either end may be empty while the other stands. "Over 120 BPM" is a
140 133 /// lower end and no upper one, and it is an answer rather than a
141 134 /// half-filled form.
142 - ///
143 - /// Added 0.56.0 with makeover-layout 0.34.0.
144 135 Between {
145 136 /// What the lower box holds now.
146 137 lower: &'a str,
@@ -240,10 +231,9 @@
240 231 /// The form the emitters use. [`escape`] is this with a `String` allocated
241 232 /// around it, and the allocation is the whole difference: a described screen
242 233 /// escapes once per attribute and once per run of text, so a function that
243 - /// returns a `String` allocates a few thousand times to produce one page, where
244 - /// a template engine writes its escaped bytes straight into the output buffer.
245 - /// Measured 2026-08-14 against a real pane, that gap was 85% of a 42x rendering
246 - /// cost, and this is the half of the fix that lives in this crate.
234 + /// returns a `String` allocates a few thousand times to produce one page,
235 + /// where a template engine writes its escaped bytes straight into the output
236 + /// buffer.
247 237 ///
248 238 /// Sound in element text and in a double-quoted attribute alike, which is the
249 239 /// property `textContent`-based escaping cannot have. Both sinks are covered by
@@ -338,7 +328,7 @@
338 328 /// appears more than once.
339 329 /// The `accept` attribute, from the description's accept list.
340 330 ///
341 - /// makeover-layout 0.31.0. The list is comma-joined because that is the
331 + /// The list is comma-joined because that is the
342 332 /// attribute's own format, and each entry writes itself: a family is its
343 333 /// wildcard media type, a media type is itself, a suffix is itself with its
344 334 /// leading dot. Nothing is normalised on the way through -- `.tar.gz` is two
@@ -1018,8 +1008,7 @@
1018 1008 /// control rendered into a document with no script is a control that looks live
1019 1009 /// and answers nothing. Nothing is hidden here and no control is shown until
1020 1010 /// whatever binds the editor sets `data-ready` on the wrapper, so a reader with
1021 - /// no script gets the textarea alone -- what 0.50.0 emitted -- and a reader with
1022 - /// script gets the modes. A bound editor says which mode it is in with
1011 + /// no script gets the textarea alone and a reader with script gets the modes. A bound editor says which mode it is in with
1023 1012 /// `data-mode`, and [`editor_rules`] reads that.
1024 1013 fn push_editor_open(out: &mut String, opts: &Emit) {
1025 1014 // The mark sits on the wrapper as well as on the control, saying one thing
@@ -1185,13 +1174,9 @@
1185 1174
1186 1175 /// The rules an option's second line needs.
1187 1176 ///
1188 - /// [`unit_rules`]' argument, and it is worth saying why this one is ruled where
1189 - /// `.form-option-reason` beside it is not: that class shipped at 0.28.0, before
1190 - /// the test the two functions above state — rule what has no app counterpart to
1191 - /// keep — and nothing has been written against it since. This one has none
1192 - /// either, and an unruled second line renders identically to the label it sits
1193 - /// under, which is a worse default than the hand-written markup it replaces.
1194 - /// MNW spells that markup `.card--selectable-desc` and mutes it.
1177 + /// [`unit_rules`]' argument: rule what has no app counterpart to keep. An
1178 + /// unruled second line renders identically to the label it sits under, which is
1179 + /// a worse default than the hand-written markup it replaces.
1195 1180 ///
1196 1181 /// Colour only, and muted, which is the same reading `.form-unit` and
1197 1182 /// `.form-suggestion-detail` take: the line orients the label rather than
M src/lib.rs +133 -245
@@ -62,9 +62,8 @@
62 62 //!
63 63 //! # What phase A settled, and what it costs
64 64 //!
65 - //! Decided 2026-07-29 against goingson's `styles.css` rather than against a
66 - //! component list. The useful finding there was that `.btn` (line 644),
67 - //! `.card` (768) and `.tag, .badge` (882) each hand-write the same
65 + //! Measured against goingson's `styles.css` rather than against a component
66 + //! list: `.btn`, `.card` and `.tag, .badge` each hand-write the same
68 67 //! composition, so three quarters of phase A is one rule with several names.
69 68 //!
70 69 //! Two of the four decisions change how goingson looks, and adoption should
@@ -88,57 +87,39 @@
88 87 //! decides which wins. That is the one real risk in adopting this, and it is
89 88 //! why the migration lands per component rather than in one commit.
90 89 //!
91 - //! # 0.10.0: the states this crate used to leave to its consumers
90 + //! # Interaction states
92 91 //!
93 - //! [`interactive_rules`] emitted hover and pressed and stopped, because
94 - //! `makeover-layout` modelled no interaction state. Focus and disabled were
95 - //! therefore unsayable, and every app completed the primitive from outside the
96 - //! only way that works: by out-specifying a rule it does not own. goingson
97 - //! carries 19 such rules and the MNW server 21, and the three focus rings do
98 - //! not match each other.
92 + //! [`interactive_rules`] emits four states, in emission order, and the order is
93 + //! load-bearing: they are all specificity (0,2,0), so disabled beats hover by
94 + //! coming last and by nothing else. Nothing here reaches for `:not(:disabled)`,
95 + //! which would raise a selector this crate wraps in its own layer.
99 96 //!
100 - //! That also blocked the cascade-layer work outright. An app that declares
101 - //! `@layer` puts its own rules in a named layer, and unlayered declarations
102 - //! outrank every named layer regardless of specificity, so all of those
103 - //! overrides lose in the commit that adopts layers. They cannot simply be
104 - //! deleted, because they are the only thing supplying the missing states.
105 - //! Emitting the states here is what turns that adoption into a deletion.
97 + //! Emitting the states here is what keeps an app from completing the primitive
98 + //! from outside, by out-specifying a rule it does not own. Those overrides are
99 + //! also what breaks under cascade layers: an app that declares `@layer` puts
100 + //! its own rules in a named layer, and unlayered declarations outrank every
101 + //! named layer regardless of specificity.
106 102 //!
107 - //! Four states now, in emission order, and the order is load-bearing: they are
108 - //! all specificity (0,2,0), so disabled beats hover by coming last and by
109 - //! nothing else. Nothing here reaches for `:not(:disabled)`, which would raise
110 - //! a selector this crate will shortly be wrapping in its own layer.
103 + //! Hover sits inside a capability query. `makeover-touch` answers whether a
104 + //! fingertip has hover and `makeover-geometry` spells the condition; this crate
105 + //! asks and does not decide, so no app has to take the hover state back on
106 + //! touch.
111 107 //!
112 - //! Hover additionally sits inside a capability query now. `makeover-touch`
113 - //! answers whether a fingertip has hover and `makeover-geometry` spells the
114 - //! condition; this crate asks and does not decide. goingson's section 60 exists
115 - //! solely to take the hover state back on touch, which is a fight it should
116 - //! never have been handed.
117 - //!
118 - //! # 0.11.0: the layer contract
108 + //! # The layer contract
119 109 //!
120 110 //! [`stylesheet`] emits into the `makeover` cascade layer ([`CSS_LAYER`], which
121 111 //! lives in `makeover-geometry` because that is the one crate every CSS emitter
122 - //! in the family already depends on). `makeover-geometry` 0.6.0 does the same
123 - //! for `geometry.css`.
112 + //! in the family already depends on). `makeover-geometry` does the same for
113 + //! `geometry.css`.
124 114 //!
125 115 //! The cascade resolves origin and importance, then layer, then specificity,
126 116 //! then source order, and **unlayered normal declarations outrank every named
127 - //! layer**. So before this, an app that declared `@layer base, components,
128 - //! responsive` put every rule it owns into a named layer and lost all of them to
129 - //! this unlayered file, regardless of specificity and regardless of loading
130 - //! last. Nothing errors when that happens: the CSS is valid, the minifier is
131 - //! happy, and buttons and badges look subtly wrong.
132 - //!
133 - //! That is why the layer belongs here rather than in each app. An app cannot fix
134 - //! it from its own stylesheet, because the fix is to layer the file it does not
135 - //! own.
136 - //!
137 - //! **What it flips**, and the reason each app wants a look when it bumps the
138 - //! pin: a generated rule that currently beats an app rule by being more specific
139 - //! stops beating it. The direction is always "the app wins", which is what the
140 - //! apps already assume, but a hand-written rule an app thought was dead can come
141 - //! back to life.
117 + //! layer**. An unlayered generated file therefore beats every rule an app owns,
118 + //! regardless of specificity and regardless of loading last. Nothing errors when
119 + //! that happens: the CSS is valid, the minifier is happy, and buttons and badges
120 + //! look subtly wrong. The layer belongs here rather than in each app, because an
121 + //! app cannot fix it from its own stylesheet: the fix is to layer the file it
122 + //! does not own.
142 123 //!
143 124 //! An app should declare the order once, or the layer's position is decided by
144 125 //! whichever generated file the browser happens to see first:
@@ -148,54 +129,25 @@
148 129 //! ```
149 130 //!
150 131 //! [`in_css_layer`] is re-exported for an app that assembles its own stylesheet
151 - //! from this crate's pieces. goingson builds `tables.css` in its own `build.rs`
152 - //! out of [`list::narrowing_css`] and [`list::grid_template_columns`], and those
153 - //! rules are as generated as the ones here, so they belong in the same layer and
154 - //! this crate cannot put them there on the app's behalf.
132 + //! from this crate's pieces. Rules an app generates from
133 + //! [`list::narrowing_css`] and [`list::grid_template_columns`] are as generated
134 + //! as the ones here, so they belong in the same layer and this crate cannot put
135 + //! them there on the app's behalf.
155 136 //!
156 - //! # 0.12.0: the ring gets its own width
137 + //! # Suggestions
157 138 //!
158 - //! [`focus_rule`] reused [`Emit::border_width`] and emitted a 1px ring. That was
159 - //! an implementation convenience dressed as consistency with the invalid-field
160 - //! ring: a bevel and a focus indicator answer different questions, and only one
161 - //! of them has to be noticed from across a desk.
139 + //! `Outcome::Suggestions` carries `Candidate` rather than `Choice`, and a
140 + //! candidate has no `unavailable`: a suggestion that cannot be picked is a row
141 + //! a route should not have offered. What it has instead is a `detail`, the line
142 + //! that tells it from a row reading the same, and it is drawn in
143 + //! `--content-muted` rather than in the disabled token. A detail orients rather
144 + //! than refuses, and every other secondary line in this crate reads the same
145 + //! way. The class is `.form-suggestion-detail`.
162 146 //!
163 - //! Caught while adopting 0.11.0 into goingson, by the check the adoption tasks
164 - //! ask for. Every consumer had already written its own ring and all three chose
165 - //! at least 2px: the MNW server 2px across 10 rules, Balanced Breakfast 2px,
166 - //! goingson 2px on three rules and 3px on the one covering twelve selectors. The
167 - //! design system was the only thing in the tree saying 1px, so deleting the app
168 - //! rules in favour of it would have thinned the focus indicator everywhere.
169 - //!
170 - //! [`Emit::focus_width`] now carries it, defaulting to `2px`, and the offset is
171 - //! the same magnitude with its sign off the depth. Both values are the measured
172 - //! consensus rather than a new opinion.
173 - //!
174 - //! # 0.57.0: a suggestion's second line, in place of a reason it cannot be picked
175 - //!
176 - //! `1fcf2e9b`, ruled by Max 2026-08-21. `Outcome::Suggestions` now carries
177 - //! `Candidate` rather than `Choice`, and a candidate has no `unavailable`: a
178 - //! suggestion that cannot be picked is a row a route should not have offered.
179 - //! What it has instead is a `detail`, the line that tells it from a row reading
180 - //! the same -- "Format" under audio, software, writing and video.
181 - //!
182 - //! So `.form-suggestion-why` is gone and `.form-suggestion-detail` is here, and
183 - //! the difference is not only the name. `why` was drawn in the disabled token,
184 - //! which said the row was refusing. A detail orients rather than refuses, so it
185 - //! is `--content-muted`, which is what every other secondary line in this crate
186 - //! reads as.
187 - //!
188 - //! No consumer is affected: the only emitter is `quasi-webview`, which moves in
189 - //! the same cascade.
190 - //!
191 - //! # 0.56.0: an interval, as the group MNW already wrote by hand
147 + //! # An interval is one question with two ends
192 148 //!
193 149 //! [`makeover_layout::FieldKind::Interval`] emits a `role="group"` named by the
194 - //! field's label, holding one `<input type="number">` per end. That is not a
195 - //! shape invented here: MNW's discover sidebar has a `role="group"` with
196 - //! `aria-labelledby` over `min_price` and `max_price` today, written by hand
197 - //! because nothing in the description could say the two boxes were one
198 - //! question. The markup is what the measurement found, and this emits it.
150 + //! field's label, holding one `<input type="number">` per end.
199 151 //!
200 152 //! - **The group carries the error and the descriptions**, on the split
201 153 //! [`makeover_layout::FieldKind::Radio`] already uses here: what is wrong is
@@ -210,27 +162,21 @@
210 162 //! group through [`form::Filling::trailing`].
211 163 //!
212 164 //! [`form::Value::Between`] is the second value. A separator inside one string
213 - //! would have made this crate the owner of a delimiter that either end could
214 - //! contain.
165 + //! would make this crate the owner of a delimiter that either end could contain.
215 166 //!
216 - //! # 0.55.0: a number's unit, as adjacent text
167 + //! # A number's unit is adjacent text
217 168 //!
218 - //! `makeover-layout` 0.33.0's `Field::unit`. HTML has no unit attribute and
219 - //! inventing one would be markup nothing reads, so it is a `<span>` after the
220 - //! control -- and it is named in `aria-describedby` rather than left as
221 - //! decoration, because a number and what it is measured in are one fact and
222 - //! reading the first without the second is reading it wrong.
169 + //! HTML has no unit attribute and inventing one would be markup nothing reads,
170 + //! so `Field::unit` is a `<span>` after the control. It is named in
171 + //! `aria-describedby` rather than left as decoration, because a number and what
172 + //! it is measured in are one fact and reading the first without the second is
173 + //! reading it wrong. What that buys is a unit a consumer can read back rather
174 + //! than a suffix on a label it would have to parse.
223 175 //!
224 - //! This is the renderer the member costs something. egui already had somewhere
225 - //! better than the label to put a unit; here the two placements are equally
226 - //! adjacent, and what is bought is that the unit is a value a consumer can read
227 - //! back rather than a suffix on a label it would have to parse.
176 + //! # A curve this renderer can carry, and one it declines
228 177 //!
229 - //! # 0.54.0: a curve this renderer can carry, and one it declines
230 - //!
231 - //! `makeover-layout` 0.32.0's `Curve`. A range takes its granularity from the
232 - //! curve now (`Field::curve.step()`), every other kind keeps `Field::step`, and
233 - //! `Curve::Linear` emits exactly what it emitted before.
178 + //! A range takes its granularity from the curve (`Field::curve.step()`), every
179 + //! other kind keeps `Field::step`, and `Curve::Linear` emits a plain range.
234 180 //!
235 181 //! **A constant-ratio curve emits a linear track, and that is the answer, not a
236 182 //! debt.** HTML has no logarithmic range input, so a described screen asking
@@ -245,45 +191,30 @@
245 191 //! The alternatives are worse in the specific way this stack exists to avoid.
246 192 //! Shipping JS that maps thumb position to value puts app code back in the
247 193 //! renderer. Changing what the control submits from a value to a fraction moves
248 - //! the mapping to whoever reads the form, and the MNW server reads these forms
249 - //! with its own handlers, so a fraction arriving where a value is expected
250 - //! would be silent.
194 + //! the mapping to whoever reads the form, and a server reading these forms with
195 + //! its own handlers would take a fraction where a value is expected, silently.
251 196 //!
252 197 //! When this reopens: the day a described screen on the webview path asks for a
253 198 //! non-linear range. The answer then is mapping in `quasi-router`, where one
254 - //! implementation serves every host, not JS here. Nothing asks today. No
255 - //! described screen in the tree uses a non-linear curve; the one real consumer
256 - //! is audiofiles' ADSR (`instrument_panel.rs:643`), which is egui and stays
257 - //! egui because audiofiles takes only `quasi-immediate` and
258 - //! `makeover-immediate`; and the two webview-served consumers, goingson and the
259 - //! MNW server, use no [`makeover_layout::FieldKind::Range`] at all.
199 + //! implementation serves every host, not JS here.
260 200 //!
261 - //! # 0.52.0: a markdown field gets the preview it was permitted
201 + //! # A markdown field gets a preview
262 202 //!
263 - //! 0.50.0 marked a [`makeover_layout::FieldKind::Rich`] field with
264 - //! `data-format="markdown"` and stopped there, and nothing read the mark: no
265 - //! rule, no markup, no selector anywhere in the tree. So a described editor was
266 - //! a plain textarea carrying an attribute, and MNW converting its four
267 - //! hand-written section editors onto the member would have *lost* the
268 - //! Write/Preview pair one of them already has. A permission taken and not spent
269 - //! turns every conversion into a regression.
203 + //! A [`makeover_layout::FieldKind::Rich`] field is marked
204 + //! `data-format="markdown"`, and [`form::editor_rules`] is what spends that
205 + //! mark. The Write/Preview pair is a segmented control, so it takes the depth,
206 + //! the focus ring and the chosen state from rules that already exist; the
207 + //! preview pane is a well, because it stands where the control stood. Both are
208 + //! gated on the attribute rather than on a class, which is what the attribute is
209 + //! for. A permission taken and not spent turns every conversion into a
210 + //! regression.
270 211 //!
271 - //! [`form::editor_rules`] and the chrome beside it are the spending. The pair is
272 - //! a segmented control, so it takes the depth, the focus ring and the chosen
273 - //! state from rules that already existed; the preview pane is a well, because it
274 - //! stands where the control stood. Both are gated on the attribute rather than
275 - //! on a class, which is what the attribute was for.
212 + //! **This crate renders no markdown.** The pane arrives empty and is filled by
213 + //! whatever binds the editor, which is where the host's sanitiser already is. A
214 + //! converter here would move that guarantee into a crate with no view of the
215 + //! host's content-security posture.
276 216 //!
277 - //! **This crate still renders no markdown.** The pane arrives empty and is
278 - //! filled by whatever binds the editor, which is where the host's sanitiser
279 - //! already is. A converter here would have moved that guarantee into a crate
280 - //! with no view of the host's content-security posture.
281 - //!
282 - //! # 0.47.0: a range, a chooser's ghost text, and an option that cannot be
283 - //! picked yet
284 - //!
285 - //! `makeover-layout` 0.28.0's three form findings, all of them cheap here and
286 - //! none of them cheap in the app that found them.
217 + //! # Ranges, ghost text, and an option that cannot be picked
287 218 //!
288 219 //! - `FieldKind::Range` emits `<input type="range">`, and `Field::step` emits
289 220 //! `step`. The step is emitted only when the description carries one: the
@@ -298,96 +229,65 @@
298 229 //! differs by control and the difference is forced: a radio group gets a
299 230 //! `.form-option-reason` span beside the label, and a `<select>` option has
300 231 //! room for no element at all, so the reason runs into its text.
301 - //! - `Choice::detail` takes the same split for the same reason, one release
302 - //! later (makeover-layout 0.39.0): a `.form-option-detail` span in a radio
303 - //! group, run into the text of a `<select>`'s option. An option carrying both
304 - //! reads what it is before why it cannot be picked.
232 + //! - `Choice::detail` takes the same split for the same reason: a
233 + //! `.form-option-detail` span in a radio group, run into the text of a
234 + //! `<select>`'s option. An option carrying both reads what it is before why it
235 + //! cannot be picked.
305 236 //!
306 - //! # 0.25.0: a cell says what it holds
237 + //! # A cell says what it holds
307 238 //!
308 - //! 0.23.0 gave a table its layout and left every cell the same. One `.cell`
309 - //! carried the whole thing, so a cell holding text and a cell holding a button
310 - //! were one class and one content colour, and a control in a cell was painted
311 - //! as text. That is the drift [`RowPart::intent`](makeover_layout::RowPart)
312 - //! has prevented for list rows since 0.2.0 and prevented for nothing here.
239 + //! [`CellPart`](makeover_layout::CellPart) names the four things a cell holds,
240 + //! and [`table_rules`] turns them into `.cell-value`, `.cell-tokens`,
241 + //! `.cell-actions` and `.cell-link`. Only the first takes a colour: a token
242 + //! carries its own tone, an action is a control rather than text, and a link
243 + //! takes the action colour from the anchor it is.
313 244 //!
314 - //! makeover-layout 0.14.0's [`CellPart`](makeover_layout::CellPart) names the
315 - //! four things a cell holds, and [`table_rules`] turns them into
316 - //! `.cell-value`, `.cell-tokens`, `.cell-actions` and `.cell-link`. Only the
317 - //! first takes a colour: a token carries its own tone, an action is a control
318 - //! rather than text, and a link takes the action colour from the anchor it is.
245 + //! The colour goes on `.cell-value` rather than on `.cell`. On the container it
246 + //! cascades into the parts that are not text, and a control in a cell is painted
247 + //! as text, which is the drift
248 + //! [`RowPart::intent`](makeover_layout::RowPart) prevents for list rows.
319 249 //!
320 - //! The colour going on `.cell-value` rather than on `.cell` is the fix rather
321 - //! than an implementation detail. On the container it cascades into the parts
322 - //! that are not text, which is the bug said in one rule.
250 + //! [`list::Cell::part`] is `Option<CellPart>` and never `Option<RowPart>`: the
251 + //! two answer different questions, and only one of them is about a cell.
323 252 //!
324 - //! [`list::Cell::part`] is `Option<CellPart>` here, where it was
325 - //! `Option<RowPart>`. A table cell borrowing the list row's vocabulary was the
326 - //! drift with a type on it: the two answer different questions, and only one of
327 - //! them was ever about a cell.
253 + //! # A table lays itself out
328 254 //!
329 - //! # 0.23.0: a table lays itself out, and a row shows its controls
330 - //!
331 - //! Three things a description could say and this renderer had no rule for,
332 - //! found together by rendering the MNW server's SSH-keys settings tab through
333 - //! `quasi` and preferring the hand-written Askama original.
334 - //!
335 - //! **A table had no layout at all.** [`list::narrowing_css`] emits the track
336 - //! list, and it has to be called with the columns, so it works where the
337 - //! columns are known at build time: goingson builds `tables.css` in its own
338 - //! `build.rs` and is untouched. A table a description produced knows its
339 - //! columns at render time, and the rules would have had to travel with the
340 - //! markup: a `<style>` element per table, which needs `style-src
341 - //! 'unsafe-inline'` that the MNW server is working to drop, or the head, which
342 - //! an htmx fragment swap does not carry. [`table_rules`] lays a table out with
343 - //! `display: table` instead, which aligns columns across rows knowing nothing
344 - //! about how many there are. [`Priority`](makeover_layout::Priority) hiding
345 - //! moves from a generated rule per dropped column to one rule per drop class,
346 - //! and [`list::column_classes`] is what puts those classes on a cell. A header
347 - //! row emitted by a renderer's own code should call it too, or the header and
255 + //! [`list::narrowing_css`] emits the track list and has to be called with the
256 + //! columns, so it works where the columns are known at build time. A table a
257 + //! description produced knows its columns at render time, and the rules would
258 + //! have to travel with the markup: a `<style>` element per table, which needs
259 + //! `style-src 'unsafe-inline'`, or the head, which an htmx fragment swap does
260 + //! not carry. [`table_rules`] lays a table out with `display: table` instead,
261 + //! which aligns columns across rows knowing nothing about how many there are.
262 + //! [`Priority`](makeover_layout::Priority) hiding is one rule per drop class,
263 + //! and [`list::column_classes`] is what puts those classes on a cell. **A header
264 + //! row emitted by a renderer's own code has to call it too**, or the header and
348 265 //! the body disagree about which column just dropped.
349 266 //!
350 - //! **A destructive button had nowhere for its tone to land.** `.button` carried
351 - //! no tone, on the reading that a control's colour is its surface. Every
352 - //! consumer had written the danger rule itself. It joins the badge in taking
353 - //! the four tones as colour, off `data-tone`.
267 + //! `.button` takes the four tones as colour, off `data-tone`, the way the badge
268 + //! does, so a destructive button has somewhere for its tone to land. A list is
269 + //! reset rather than left as a bulleted list.
354 270 //!
355 - //! **A list rendered as a bulleted list**, because nothing here reset the `ul`
356 - //! a renderer emits for one.
271 + //! `RowPart::revealed_on_hover` is not honoured. Hiding a row's actions until
272 + //! hover hides them from pointer users alone, who are the ones scanning a list
273 + //! to learn what can be done to a row, and every escape the rule grows
274 + //! (`focus-within` for the keyboard, a capability gate for a fingertip) is a
275 + //! report that hiding was wrong for somebody.
357 276 //!
358 - //! `RowPart::revealed_on_hover` also stops being honoured, which its own doc
359 - //! sanctioned: a renderer decides. It was retired outright in makeover-layout
360 - //! 0.13.0, once this had been its only consumer for a release. The rule hid a
361 - //! row's actions until hover,
362 - //! and every escape it grew was a report that hiding was wrong for somebody:
363 - //! `focus-within` for the keyboard, the capability gate for a fingertip with no
364 - //! way to unhide. What survived hid the controls from pointer users alone, who
365 - //! are the ones scanning a list to learn what can be done to a row.
277 + //! # The depth classes are not controls
366 278 //!
367 - //! # 0.17.0: the depth classes stop being controls
279 + //! `.raised` is a statement about shape and carries no interactive set, so the
280 + //! vocabulary has a raised surface that is merely an object. An app that wants
281 + //! one does not have to take a control class and cancel the control half.
368 282 //!
369 - //! [`depth_rules`] gave `.raised` the whole interactive set. A depth is a
370 - //! statement about shape, so that left the vocabulary with no raised surface
371 - //! that is merely an object, and an app wanting one had two moves: write its
372 - //! own class from tokens, or take a control class and cancel the control half.
373 - //! goingson took the second, in three variants over sixteen elements
374 - //! (`.card--static` at 14 call sites, `.card--muted` at 2, `.card--shell` at 1),
375 - //! each re-asserting the resting fill and bevel on `:hover` and `:active`.
376 - //!
377 - //! Measured before changing it: `.raised` is emitted into goingson, Balanced
378 - //! Breakfast and the MNW server, and none of the three has a single call site.
379 - //! The states were unasked-for everywhere at once, and dropping them costs no
380 - //! migration anywhere.
381 - //!
382 - //! `.card` and `.button` are unchanged. They are the same depth *and* controls,
383 - //! and they take their states from [`surface_rules`], which is where a state
384 - //! belongs: on the thing that claims to answer a pointer.
283 + //! `.card` and `.button` are the same depth *and* controls, and they take their
284 + //! states from [`surface_rules`], which is where a state belongs: on the thing
285 + //! that claims to answer a pointer.
385 286 //!
386 287 //! # Substitution, three ways
387 288 //!
388 - //! `Fill::Well` has no colour on makeover before 2.3.0, and each renderer
389 - //! answers that differently, which is the evidence that dropping
390 - //! `Fill::fallback` from the description was right:
289 + //! A theme with no `surface-well` is answered differently by each renderer,
290 + //! which is why substitution belongs to a renderer and not to the description:
391 291 //!
392 292 //! - `makeover-immediate` substitutes the page in Rust.
393 293 //! - `makeover-tui` refuses to substitute and draws an edge instead, because a
@@ -448,11 +348,9 @@
448 348 pub border_width: &'static str,
449 349 /// Focus ring thickness, as a CSS length.
450 350 ///
451 - /// Separate from [`border_width`](Self::border_width), which it reused
452 - /// until 0.12.0. That reuse was an implementation convenience dressed as
453 - /// consistency, and it emitted a 1px ring: a bevel and a focus indicator
454 - /// are answering different questions, and only one of them has to be
455 - /// noticed from across a desk.
351 + /// Separate from [`border_width`](Self::border_width), and never derived
352 + /// from it: a bevel and a focus indicator answer different questions, and
353 + /// only one of them has to be noticed from across a desk.
456 354 ///
457 355 /// The default is the measured consensus rather than a new opinion. Every
458 356 /// consumer had already written its own ring and all three chose at least
@@ -585,12 +483,11 @@
585 483
586 484 /// A prefixed class name.
587 485 ///
588 - /// Public since 0.27.0, for the renderers that emit markup this crate does not.
589 - /// A screen renderer writing `class="row"` has to prefix it the way the
590 - /// stylesheet half does or a prefixed app gets rules matching everything except
591 - /// the elements that renderer wrote, and the failure is invisible: the CSS
592 - /// stays valid and one element is unstyled. quasi-webview carried a byte
593 - /// identical copy of this function until it could call this one.
486 + /// Public, for the renderers that emit markup this crate does not. A screen
487 + /// renderer writing `class="row"` has to prefix it the way the stylesheet half
488 + /// does, or a prefixed app gets rules matching everything except the elements
489 + /// that renderer wrote, and the failure is invisible: the CSS stays valid and
490 + /// one element is unstyled. Call this rather than copying it.
594 491 #[must_use]
595 492 pub fn class(name: &str, opts: &Emit) -> String {
596 493 let mut out = String::with_capacity(opts.class_prefix.len() + name.len());
@@ -601,10 +498,10 @@
601 498 /// A prefixed class name, written into a buffer the caller already has.
602 499 ///
603 500 /// The form the emitters use, and the reason it exists is [`escape_into`]'s:
604 - /// every class on every element went through a `format!` before 0.40.0,
605 - /// including the default case where the prefix is empty and the answer is the
606 - /// argument. A described table row carried roughly eighty transient
607 - /// allocations, and this and the escaper were most of them.
501 + /// putting every class on every element through a `format!` allocates even in
502 + /// the default case, where the prefix is empty and the answer is the argument.
503 + /// A described table row carries roughly eighty transient allocations that way,
504 + /// and this and the escaper are most of them.
608 505 ///
609 506 /// [`class`] stays for callers holding a name rather than a buffer.
610 507 ///
@@ -644,8 +541,8 @@
644 541 /// thought about this", which is what comments are for.
645 542 ///
646 543 /// The two halves are emitted independently because [`Depth::Sunken`] has a
647 - /// fill and no bevel. Requiring both, which this did before makeover-layout
648 - /// 0.3.0, silently dropped the fill for exactly that case. Independent does not
544 + /// fill and no bevel. Requiring both would silently drop the fill for exactly
545 + /// that case. Independent does not
649 546 /// mean unpaired: both halves still come off one `Depth`, so they cannot
650 547 /// disagree about what the region is.
651 548 #[must_use]
@@ -806,11 +703,10 @@
806 703 /// one of them and have to be unpicked when this output moves inside its own
807 704 /// cascade layer.
808 705 ///
809 - /// Re-asserted on **both** axes, through [`rest_declarations`], which is the
810 - /// half this got wrong until 0.68.0. `depth_declarations` alone is empty for
811 - /// [`Depth::Flat`], so the three flat controls this crate emits -- both facet
812 - /// arms and a suggestion entry -- won the contest with nothing to say and kept
813 - /// the hover surface underneath a control that had stopped answering.
706 + /// Re-asserted on **both** axes, through [`rest_declarations`].
707 + /// `depth_declarations` alone is empty for [`Depth::Flat`], so a flat control
708 + /// would win the contest with nothing to say and keep the hover surface
709 + /// underneath a control that had stopped answering.
814 710 #[must_use]
815 711 pub fn disabled_rule(selector: &str, depth: Depth) -> String {
816 712 format!(
@@ -844,8 +740,7 @@
844 740 ///
845 741 /// `depth` is the selector's **rest** depth, used to place the focus ring and
846 742 /// to restore the surface under a disabled control. The pressed rule keeps
847 - /// inverting from [`Depth::Raised`] regardless, which is what every caller got
848 - /// before this parameter existed: a tab's unchosen depth is
743 + /// inverting from [`Depth::Raised`] regardless: a tab's unchosen depth is
849 744 /// [`Depth::Sunken`], and `Sunken.pressed()` is `Sunken`, so deriving the press
850 745 /// from the rest depth would leave a tab with no press at all.
851 746 #[must_use]
@@ -872,12 +767,9 @@
872 767 /// same depth *and* controls, and they get their states from
873 768 /// [`surface_rules`].
874 769 ///
875 - /// This class carried the interactive set until 0.17.0, which left the
876 - /// vocabulary with no raised surface that is merely an object. Consumers that
877 - /// needed one took a control class and cancelled half of it instead: sixteen
878 - /// elements in goingson across three `.card--*` variants, each re-asserting the
879 - /// resting fill and bevel on `:hover` and `:active`. Nothing anywhere used
880 - /// `.raised` itself, so the states were unasked-for in every consumer at once.
770 + /// Giving this class the interactive set leaves the vocabulary with no raised
771 + /// surface that is merely an object, so a consumer that needs one has to take a
772 + /// control class and cancel half of it.
881 773 #[must_use]
882 774 pub fn depth_rules(opts: &Emit) -> String {
883 775 let mut css = String::new();
@@ -949,11 +841,10 @@
949 841 /// because it is the absence of a status and not a status called "none".
950 842 /// Text that goes somewhere.
951 843 ///
952 - /// The one inline control, and the vocabulary had no word for it until a
953 - /// description layer needed one. A table cell has carried `cell-link` since
954 - /// 0.14.0's part list, deliberately unruled because the cell's own rule covers
955 - /// it; what was missing is the same thing outside a table, which is what a
Lines truncated
M src/list.rs +22 -29
@@ -104,8 +104,8 @@
104 104 /// already has.
105 105 ///
106 106 /// [`column_class`]'s streaming form. It is the one that runs per cell per row,
107 - /// and it used to allocate twice to get there: once for `col-<name>` and once
108 - /// for the prefix in front of it.
107 + /// where the allocating form pays twice: once for `col-<name>` and once for the
108 + /// prefix in front of it.
109 109 pub fn push_column_class(out: &mut String, column: &Column<'_>, opts: &Emit) {
110 110 out.push_str(opts.class_prefix);
111 111 out.push_str("col-");
@@ -117,8 +117,8 @@
117 117 /// # Why this is not escaping
118 118 ///
119 119 /// The name is the one app-supplied string this crate puts in a class attribute
120 - /// rather than in text or an `aria-label`, and until 0.41.0 it went in raw. A
121 - /// column named `a" onclick="steal()` emitted
120 + /// rather than in text or an `aria-label`, so it may never go in raw. A column
121 + /// named `a" onclick="steal()` emitted
122 122 ///
123 123 /// ```html
124 124 /// <div class="cell col-a" onclick="steal() cell-fill cell-keeps">
@@ -296,10 +296,9 @@
296 296 /// nothing but controls says so in the description's own words rather than
297 297 /// in the app's.
298 298 ///
299 - /// This was `Option<RowPart>` until 0.25.0, which was the drift
300 - /// `makeover-layout` 0.14.0 named: a table cell borrowing the list row's
301 - /// vocabulary, because the table side had none. A row's parts answer a
302 - /// different question (which of six emphases this run of text takes) from a
299 + /// `Option<CellPart>` and never `Option<RowPart>`: a table cell borrowing
300 + /// the list row's vocabulary is drift. A row's parts answer a different
301 + /// question (which of six emphases this run of text takes) from a
303 302 /// cell's (whether this is text, tokens, controls or a link).
304 303 ///
305 304 /// `None` for a cell mixing parts. A cell holding a value *and* a strip of
@@ -325,23 +324,19 @@
325 324
326 325 /// The class for a row part.
327 326 ///
328 - /// This comment used to say `RowPart` was the one closed enum left here, and
329 - /// that gaining a member would stop this compiling — "the same lockstep break
330 - /// `non_exhaustive` was added elsewhere to end". makeover-layout 0.9.0 ended
331 - /// it: the enum gained [`RowPart::Tokens`] and `#[non_exhaustive]` in the same
332 - /// release, so the prediction was paid off rather than waited for.
327 + /// [`RowPart`] is `#[non_exhaustive]`, so a member added upstream does not stop
328 + /// this compiling.
333 329 ///
334 330 /// The fallback is what that costs. A member added upstream lands here as a
335 331 /// bare `row-part` with no rule of its own, which is a thing rendering plainly
336 332 /// rather than a build that stops. Grep this function when adopting a new
337 333 /// makeover-layout.
338 334 ///
339 - /// Public since 0.27.0. A row's parts are emitted by whoever builds the row
340 - /// element, and that is not always this crate: `cells_html` emits a table's
341 - /// cells, but a list row carries the app's identity and hooks, so a screen
342 - /// renderer writes it. quasi-webview wrote this list out a second time to do
343 - /// that, which made the obligation in the paragraph above land on a function
344 - /// its author would not think to grep.
335 + /// Public, because a row's parts are emitted by whoever builds the row element
336 + /// and that is not always this crate: `cells_html` emits a table's cells, but a
337 + /// list row carries the app's identity and hooks, so a screen renderer writes
338 + /// it. A renderer that reimplements this list rather than calling it takes on
339 + /// the obligation above without knowing it.
345 340 /// Every class [`part_class`] can return, including the fallback.
346 341 ///
347 342 /// Beside the match rather than derived from it, because a `match` over a
@@ -369,10 +364,9 @@
369 364
370 365 /// The classes a row inside a hierarchy carries.
371 366 ///
372 - /// `edf33114`. Emitted by quasi-webview since `674dc30f` and styled by nothing
373 - /// until 2026-08-30, which is why they are declared here now: the generated
374 - /// sheet defines rules for them, so the vocabulary has to say it does or the
375 - /// check that every emitted name has a rule cannot see them.
367 + /// Declared here because the generated sheet defines rules for them: the
368 + /// vocabulary has to say so, or the check that every emitted name has a rule
369 + /// cannot see them.
376 370 pub const NESTING_CLASSES: &[&str] = &["row-nested", "row-branch", "row-disclose"];
377 371
378 372 /// The class for a part's flow, if it needs one.
@@ -435,12 +429,11 @@
435 429
436 430 /// The class for a cell part.
437 431 ///
438 - /// [`part_class`]'s table half, added with `makeover-layout` 0.14.0's
439 - /// [`CellPart`]. The fallback is there for the same reason and costs the same
440 - /// thing: a member added upstream lands as a bare `cell-part` with no rule of
441 - /// its own, rather than as a build that stops. Grep this function too when
442 - /// adopting a new makeover-layout, and public since 0.27.0 for the reason
443 - /// [`part_class`] is.
432 + /// [`part_class`]'s table half, for [`CellPart`]. The fallback is there for the
433 + /// same reason and costs the same thing: a member added upstream lands as a
434 + /// bare `cell-part` with no rule of its own, rather than as a build that stops.
435 + /// Grep this function too when adopting a new makeover-layout. Public for the
436 + /// reason [`part_class`] is.
444 437 #[must_use]
445 438 pub fn cell_part_class(part: CellPart) -> &'static str {
446 439 match part {
@@ -1,10 +1,8 @@
1 1 //! What a region shows when it is not showing its content.
2 2 //!
3 - //! The fifth phase-B emitter. `makeover_layout::Readiness` grew from two states
4 - //! to four at 0.12.0, and this is where the two new ones become markup: goingson
5 - //! drew an empty state at 27 sites across 12 files and Balanced Breakfast at 9,
6 - //! each app with its own class family, and the families had already drifted
7 - //! into `empty-state--error` against `error-state` for the same fact.
3 + //! The fifth phase-B emitter, where `makeover_layout::Readiness` becomes
4 + //! markup. Left to the apps, each grows its own class family and the families
5 + //! drift: `empty-state--error` against `error-state` for the same fact.
8 6 //!
9 7 //! # Why one function for three states
10 8 //!
@@ -117,7 +115,7 @@
117 115 /// The `data-state` value for a state.
118 116 ///
119 117 /// A wildcard rather than a total match, because `Readiness` is
120 - /// `#[non_exhaustive]` as of 0.12.0. A state added upstream draws the plain
118 + /// `#[non_exhaustive]`. A state added upstream draws the plain
121 119 /// stand-in with no state of its own, which is a box rendering without its
122 120 /// colour rather than a build that stops.
123 121 fn state_name(state: Readiness) -> &'static str {
M src/reset.rs +9 -15
@@ -3,9 +3,8 @@
3 3 //! The renderer picks an element from the description (a link that writes is a
4 4 //! `<button>`, a described set of values is a `<ul>`) and the element arrives
5 5 //! carrying a user-agent look nobody asked for. Withdrawing that look is a
6 - //! recurring ask rather than an edge case, and it was written by hand three
7 - //! times before this module existed: twice byte-identically for a list, once
8 - //! for a link, with no arm aware of the others.
6 + //! recurring ask rather than an edge case, and left to each arm it gets written
7 + //! by hand once per arm with no arm aware of the others.
9 8 //!
10 9 //! # Why this is a withdrawal and not a depth
11 10 //!
@@ -121,14 +120,10 @@
121 120 /// still a control, and a link gives it back because it is a word in a
122 121 /// sentence.
123 122 ///
124 - /// The shadow was absent until 0.68.0, and the hole was invisible for the
125 - /// worst reason: the MNW server's own handoff hands `box-shadow` back on
126 - /// `button.link`, and with nothing in the layer to hand back
127 - /// `revert-layer` rolled past to the UA default, which happens to be
128 - /// `none`. So the app was compensating for a gap here and the right answer
129 - /// arrived by luck. Measured across the three webview consumers before
130 - /// closing it: the MNW server is the only one with a bare `button` rule
131 - /// setting any chrome at all, and its computed value does not move.
123 + /// The shadow has to be here rather than left to an app's own handoff. With
124 + /// nothing in the layer to hand back, `revert-layer` rolls past to the UA
125 + /// default, and an app that looks correct is compensating for the gap by
126 + /// luck.
132 127 pub const TEXT_BUTTON: Self = Self::NOTHING
133 128 .and(Chrome::Fill)
134 129 .and(Chrome::Edge)
@@ -205,10 +200,9 @@
205 200 );
206 201 }
207 202
208 - /// The three sets, spelled out. Two of them are the bytes hand-written arms
209 - /// emitted before the reset was named; the third, `TEXT_BUTTON`, is those
210 - /// bytes plus the `box-shadow` 0.68.0 added, which is the one place the
211 - /// reset deliberately says more than what it replaced.
203 + /// The three sets, spelled out. Two are the bytes a hand-written arm emits;
204 + /// the third, `TEXT_BUTTON`, is those bytes plus a `box-shadow`, which is
205 + /// the one place the reset deliberately says more than what it replaces.
212 206 #[test]
213 207 fn the_named_sets_emit_what_they_replaced() {
214 208 assert_eq!(
@@ -3,8 +3,8 @@
3 3 //!
4 4 //! The naming functions ([`crate::class`], [`crate::option_class`],
5 5 //! [`crate::list::part_class`], [`crate::list::cell_part_class`]) answer "what
6 - //! is this one thing called". That is half the agreement, and 0.27.0 shipped
7 - //! it. The other half is the set: a checker cannot ask "is this app rule
6 + //! is this one thing called". That is half the agreement. The other half is
7 + //! the set: a checker cannot ask "is this app rule
8 8 //! re-specifying something makeover already defines" without the list, and this
9 9 //! crate is the only place that knows it, because this crate writes the sheet.
10 10 //!
@@ -70,13 +70,9 @@
70 70 /// # The unruled half is written down, module by module
71 71 ///
72 72 /// One list per module that emits markup, each beside its emitters, and this
73 - /// is their union. That shape is the fix for how the set was wrong until
74 - /// 0.59.0: it held four deliberate omissions and the emitters had fifteen, so
75 - /// `cell-fill`, `form-group`, `form-label` and a dozen others came out in
76 - /// documents that this function said were impossible. An app reading it
77 - /// concluded its live rules for them were dead and would have deleted them,
78 - /// which is why quasi-webview carried a `MAKEOVER_UNLISTED` constant of its own
79 - /// to put them back.
73 + /// is their union. Keeping the omissions beside the emitters is what stops the
74 + /// set drifting from what actually comes out in a document. A name this
75 + /// function omits is a name an app reads as dead and deletes live rules for.
80 76 ///
81 77 /// [`crate::corpus`] is what keeps the union honest, and it renders rather than
82 78 /// reading the source: a width class, a drop class and a state appended to an
@@ -180,9 +176,8 @@
180 176 /// by the classes in their selectors, so a rule carrying no class at all is
181 177 /// invisible to it. `button { color: var(--content) }` is exactly that, and it
182 178 /// sets the same property the generated `.button` does on every described act
183 - /// in the app -- including the tone of a destructive one, which is how a delete
184 - /// came to look like an ordinary button for months with the check reporting
185 - /// nothing.
179 + /// in the app -- including the tone of a destructive one, so a delete comes to
180 + /// look like an ordinary button with the check reporting nothing.
186 181 ///
187 182 /// Only a selector arm that is one bare compound counts: `button`,
188 183 /// `button:hover`, `input[type="text"]`. A scoped arm (`.page button`) reaches