//! 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. //! //! [`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.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 `