//! The webview renderer for [`makeover_layout`]. //! //! //! //! # The renderer that needs no palette //! //! `makeover-immediate` and `makeover-tui` both take a `Palette`, because egui //! and a terminal need an actual colour before they can put anything on //! screen. A webview does not: `var(--surface-raised)` *is* the late binding, //! and the browser resolves it against whatever `themes.js` last wrote onto //! `:root`. //! //! So this crate emits text naming intents, and never learns a colour. It is //! the deferral rule with no adapter in the way, and it is why the webview was //! always the wrong renderer to derive a vocabulary from: it can express //! anything, so it never pushes back. //! //! # Phase A: the stylesheet //! //! This module emits component CSS and no markup, deliberately. GoingsOn has //! 145 `innerHTML` sites and Balanced Breakfast 175 `createElement` sites, so //! moving markup is a migration where adopting a generated stylesheet is not. //! The apps keep every line of their markup and gain the classes. //! //! It is not a deletion either, which this header claimed until the measurement //! came in. Adoption across goingson removed 49 declarations net and *added* 25 //! lines: a rule loses its depth declarations and gains a variant selector next //! to it, so the file stays the same size. What phase A moves is where depth is //! defined, not how much CSS exists. Numbers and method in the wiki note under //! "The deletion test, run". //! //! The bevel properties are byte-identical to what both apps already //! hand-write, which is asserted below. //! //! # Phase B: the markup, one description at a time //! //! [`form`] renders [`makeover_layout::Field`], which is the half of phase B //! whose description is settled. It emits strings, because both apps //! interpolate their fields into larger string-built forms and returning nodes //! would rewrite those too. It owns its own escaping, on the reasoning in that //! module: a Rust encoder can cover element text and attribute values with one //! function, where the apps need four and have to choose correctly at every //! call site. //! //! [`facet`] renders `makeover_layout::Facet`: a dimension a set is narrowed by, //! and the one phase-B emitter whose markup an app is not keeping, because the //! markup it replaces was two mechanisms rather than one. A tag's selection and //! a tag's browse position were separate state on MNW's discover page, which is //! why every filter row there carries a tick box *and* a chevron; one gesture //! doing both is what lets the second one go. //! //! [`list`] is the other half: column tracks, the narrowing rules, and the cell //! containers a row is made of. It stops at the cell boundary and does not //! render what goes inside one, on the reasoning in that module. So phase B is //! now the frame around content in both directions, and what an app still owns //! is the content itself. //! //! # What phase A settled, and what it costs //! //! Decided 2026-07-29 against goingson's `styles.css` rather than against a //! component list. The useful finding there was that `.btn` (line 644), //! `.card` (768) and `.tag, .badge` (882) each hand-write the same //! composition, so three quarters of phase A is one rule with several names. //! //! Two of the four decisions change how goingson looks, and adoption should //! not be described as a pure deletion: //! //! - **Pressed carries its fill.** [`interactive_rules`] emits //! [`Depth::pressed`] whole. goingson presses to `--surface-sunken` today and //! will press to `--surface-well`, and hovers to `--surface-overlay` today //! and will hover to `--hover-surface`. Since `surface-well` inverts by theme //! where `surface-sunken` does not, a dark theme presses *lighter* than it //! hovers. That falls out of `makeover`'s own derivation, which says outright //! that `surface-sunken` cannot serve as a well, so if it reads wrong the //! answer is there and not here. //! - **Badges go flat.** See [`token_rules`]. //! //! The other two: the progress trough is renderer-local and the scrollbar //! track was dropped ([`component_rules`]), and no class prefix ships by //! default, so adoption means deleting the app's hand-written rule in the same //! commit that adds the generated one. `.card`, `.badge` and the tab classes //! all already exist in goingson, and while both rules exist the cascade order //! decides which wins. That is the one real risk in adopting this, and it is //! why the migration lands per component rather than in one commit. //! //! # 0.10.0: the states this crate used to leave to its consumers //! //! [`interactive_rules`] emitted hover and pressed and stopped, because //! `makeover-layout` modelled no interaction state. Focus and disabled were //! therefore unsayable, and every app completed the primitive from outside the //! only way that works: by out-specifying a rule it does not own. goingson //! carries 19 such rules and the MNW server 21, and the three focus rings do //! not match each other. //! //! That also blocked the cascade-layer work outright. An app that declares //! `@layer` puts its own rules in a named layer, and unlayered declarations //! outrank every named layer regardless of specificity, so all of those //! overrides lose in the commit that adopts layers. They cannot simply be //! deleted, because they are the only thing supplying the missing states. //! Emitting the states here is what turns that adoption into a deletion. //! //! Four states now, in emission order, and the order is load-bearing: they are //! all specificity (0,2,0), so disabled beats hover by coming last and by //! nothing else. Nothing here reaches for `:not(:disabled)`, which would raise //! a selector this crate will shortly be wrapping in its own layer. //! //! Hover additionally sits inside a capability query now. `makeover-touch` //! answers whether a fingertip has hover and `makeover-geometry` spells the //! condition; this crate asks and does not decide. goingson's section 60 exists //! solely to take the hover state back on touch, which is a fight it should //! never have been handed. //! //! # 0.11.0: the layer contract //! //! [`stylesheet`] emits into the `makeover` cascade layer ([`CSS_LAYER`], which //! lives in `makeover-geometry` because that is the one crate every CSS emitter //! in the family already depends on). `makeover-geometry` 0.6.0 does the same //! for `geometry.css`. //! //! The cascade resolves origin and importance, then layer, then specificity, //! then source order, and **unlayered normal declarations outrank every named //! layer**. So before this, an app that declared `@layer base, components, //! responsive` put every rule it owns into a named layer and lost all of them to //! this unlayered file, regardless of specificity and regardless of loading //! last. Nothing errors when that happens: the CSS is valid, the minifier is //! happy, and buttons and badges look subtly wrong. //! //! That is why the layer belongs here rather than in each app. An app cannot fix //! it from its own stylesheet, because the fix is to layer the file it does not //! own. //! //! **What it flips**, and the reason each app wants a look when it bumps the //! pin: a generated rule that currently beats an app rule by being more specific //! stops beating it. The direction is always "the app wins", which is what the //! apps already assume, but a hand-written rule an app thought was dead can come //! back to life. //! //! An app should declare the order once, or the layer's position is decided by //! whichever generated file the browser happens to see first: //! //! ```css //! @layer makeover, base, components, responsive; //! ``` //! //! [`in_css_layer`] is re-exported for an app that assembles its own stylesheet //! from this crate's pieces. goingson builds `tables.css` in its own `build.rs` //! out of [`list::narrowing_css`] and [`list::grid_template_columns`], and those //! rules are as generated as the ones here, so they belong in the same layer and //! this crate cannot put them there on the app's behalf. //! //! # 0.12.0: the ring gets its own width //! //! [`focus_rule`] reused [`Emit::border_width`] and emitted a 1px ring. That was //! an implementation convenience dressed as consistency with the invalid-field //! ring: a bevel and a focus indicator answer different questions, and only one //! of them has to be noticed from across a desk. //! //! Caught while adopting 0.11.0 into goingson, by the check the adoption tasks //! ask for. Every consumer had already written its own ring and all three chose //! at least 2px: the MNW server 2px across 10 rules, Balanced Breakfast 2px, //! goingson 2px on three rules and 3px on the one covering twelve selectors. The //! design system was the only thing in the tree saying 1px, so deleting the app //! rules in favour of it would have thinned the focus indicator everywhere. //! //! [`Emit::focus_width`] now carries it, defaulting to `2px`, and the offset is //! the same magnitude with its sign off the depth. Both values are the measured //! consensus rather than a new opinion. //! //! # 0.57.0: a suggestion's second line, in place of a reason it cannot be picked //! //! `1fcf2e9b`, ruled by Max 2026-08-21. `Outcome::Suggestions` now carries //! `Candidate` rather than `Choice`, and a candidate has no `unavailable`: a //! suggestion that cannot be picked is a row a route should not have offered. //! What it has instead is a `detail`, the line that tells it from a row reading //! the same -- "Format" under audio, software, writing and video. //! //! So `.form-suggestion-why` is gone and `.form-suggestion-detail` is here, and //! the difference is not only the name. `why` was drawn in the disabled token, //! which said the row was refusing. A detail orients rather than refuses, so it //! is `--content-muted`, which is what every other secondary line in this crate //! reads as. //! //! No consumer is affected: the only emitter is `quasi-webview`, which moves in //! the same cascade. //! //! # 0.56.0: an interval, as the group MNW already wrote by hand //! //! [`makeover_layout::FieldKind::Interval`] emits a `role="group"` named by the //! field's label, holding one `` per end. That is not a //! shape invented here: MNW's discover sidebar has a `role="group"` with //! `aria-labelledby` over `min_price` and `max_price` today, written by hand //! because nothing in the description could say the two boxes were one //! question. The markup is what the measurement found, and this emits it. //! //! - **The group carries the error and the descriptions**, on the split //! [`makeover_layout::FieldKind::Radio`] already uses here: what is wrong is //! the answer, and a crossed interval is not the fault of either end. //! - **Both boxes take the whole extent.** `min`, `max` and `step` describe the //! axis, so they are written twice. The crossing rule is not emitted, because //! HTML has no attribute for it and the description does not carry it: it //! comes back as an error on the group, like every other refusal. //! - **Which end is which is `aria-label` and nothing more.** The description //! states direction structurally, by which member holds which name, and never //! in words. Visible Min and Max captions are a page's own and reach the //! group through [`form::Filling::trailing`]. //! //! [`form::Value::Between`] is the second value. A separator inside one string //! would have made this crate the owner of a delimiter that either end could //! contain. //! //! # 0.55.0: a number's unit, as adjacent text //! //! `makeover-layout` 0.33.0's `Field::unit`. HTML has no unit attribute and //! inventing one would be markup nothing reads, so it is a `` after the //! control -- and it is named in `aria-describedby` rather than left as //! decoration, because a number and what it is measured in are one fact and //! reading the first without the second is reading it wrong. //! //! This is the renderer the member costs something. egui already had somewhere //! better than the label to put a unit; here the two placements are equally //! adjacent, and what is bought is that the unit is a value a consumer can read //! back rather than a suffix on a label it would have to parse. //! //! # 0.54.0: a curve this renderer can carry, and one it cannot //! //! `makeover-layout` 0.32.0's `Curve`. A range takes its granularity from the //! curve now (`Field::curve.step()`), every other kind keeps `Field::step`, and //! `Curve::Linear` emits exactly what it emitted before. //! //! **A constant-ratio curve still emits a linear track, and that is a //! shortfall rather than a decision.** HTML has no logarithmic range input, so //! honouring one here means either shipping JS that maps the thumb position to //! a value -- renderer-side app code, which is the thing this stack exists to //! delete -- or changing what the control submits from a value to a fraction, //! and then something has to map it on the way back in. That something is not //! this crate: the MNW server reads these forms with its own handlers, so a //! fraction arriving where a value is expected would be silent. Filed rather //! than guessed at. //! //! What is not lost: the extent, the granularity, and the value's own units. //! What is lost is resolution at the small end, which is exactly the reason an //! app asked for a ratio in the first place. No consumer is affected today -- //! every described range on this path is linear. //! //! # 0.52.0: a markdown field gets the preview it was permitted //! //! 0.50.0 marked a [`makeover_layout::FieldKind::Rich`] field with //! `data-format="markdown"` and stopped there, and nothing read the mark: no //! rule, no markup, no selector anywhere in the tree. So a described editor was //! a plain textarea carrying an attribute, and MNW converting its four //! hand-written section editors onto the member would have *lost* the //! Write/Preview pair one of them already has. A permission taken and not spent //! turns every conversion into a regression. //! //! [`form::editor_rules`] and the chrome beside it are the spending. The pair is //! a segmented control, so it takes the depth, the focus ring and the chosen //! state from rules that already existed; the preview pane is a well, because it //! stands where the control stood. Both are gated on the attribute rather than //! on a class, which is what the attribute was for. //! //! **This crate still renders no markdown.** The pane arrives empty and is //! filled by whatever binds the editor, which is where the host's sanitiser //! already is. A converter here would have moved that guarantee into a crate //! with no view of the host's content-security posture. //! //! # 0.47.0: a range, a chooser's ghost text, and an option that cannot be //! picked yet //! //! `makeover-layout` 0.28.0's three form findings, all of them cheap here and //! none of them cheap in the app that found them. //! //! - `FieldKind::Range` emits ``, and `Field::step` emits //! `step`. The step is emitted only when the description carries one: the //! browser's own default is `step="1"`, which is what a description means by //! saying nothing, and is also what turns a 0-to-1 threshold into a //! two-position control. //! - A select with nothing chosen emits a disabled, selected, valueless first //! option carrying `Field::placeholder`. HTML has no placeholder attribute on //! `` option has //! room for no element at all, so the reason runs into its text. //! //! # 0.25.0: a cell says what it holds //! //! 0.23.0 gave a table its layout and left every cell the same. One `.cell` //! carried the whole thing, so a cell holding text and a cell holding a button //! were one class and one content colour, and a control in a cell was painted //! as text. That is the drift [`RowPart::intent`](makeover_layout::RowPart) //! has prevented for list rows since 0.2.0 and prevented for nothing here. //! //! makeover-layout 0.14.0's [`CellPart`](makeover_layout::CellPart) names the //! four things a cell holds, and [`table_rules`] turns them into //! `.cell-value`, `.cell-tokens`, `.cell-actions` and `.cell-link`. Only the //! first takes a colour: a token carries its own tone, an action is a control //! rather than text, and a link takes the action colour from the anchor it is. //! //! The colour going on `.cell-value` rather than on `.cell` is the fix rather //! than an implementation detail. On the container it cascades into the parts //! that are not text, which is the bug said in one rule. //! //! [`list::Cell::part`] is `Option` here, where it was //! `Option`. A table cell borrowing the list row's vocabulary was the //! drift with a type on it: the two answer different questions, and only one of //! them was ever about a cell. //! //! # 0.23.0: a table lays itself out, and a row shows its controls //! //! Three things a description could say and this renderer had no rule for, //! found together by rendering the MNW server's SSH-keys settings tab through //! `quasi` and preferring the hand-written Askama original. //! //! **A table had no layout at all.** [`list::narrowing_css`] emits the track //! list, and it has to be called with the columns, so it works where the //! columns are known at build time: goingson builds `tables.css` in its own //! `build.rs` and is untouched. A table a description produced knows its //! columns at render time, and the rules would have had to travel with the //! markup: a `