| 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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
|