//! 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. //! //! Some of what phase A emits is not a look but the withdrawal of one. A //! renderer that picks its element from the description inherits that element's //! user-agent chrome, and [`reset`] is where a primitive says which parts of it //! were never asked for. //! //! # 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 //! //! Measured against goingson's `styles.css` rather than against a component //! list: `.btn`, `.card` and `.tag, .badge` 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. //! //! # Interaction states //! //! [`interactive_rules`] emits four states, 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 wraps in its own layer. //! //! Emitting the states here is what keeps an app from completing the primitive //! from outside, by out-specifying a rule it does not own. Those overrides are //! also what breaks under cascade layers: an app that declares `@layer` puts //! its own rules in a named layer, and unlayered declarations outrank every //! named layer regardless of specificity. //! //! Hover sits inside a capability query. `makeover-touch` answers whether a //! fingertip has hover and `makeover-geometry` spells the condition; this crate //! asks and does not decide, so no app has to take the hover state back on //! touch. //! //! # 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` 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**. An unlayered generated file therefore beats every rule an app owns, //! 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. The layer belongs here rather than in each app, because an //! app cannot fix it from its own stylesheet: the fix is to layer the file it //! does not own. //! //! 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. Rules an app generates from //! [`list::narrowing_css`] and [`list::grid_template_columns`] 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. //! //! # Suggestions //! //! `Outcome::Suggestions` 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, and it is drawn in //! `--content-muted` rather than in the disabled token. A detail orients rather //! than refuses, and every other secondary line in this crate reads the same //! way. The class is `.form-suggestion-detail`. //! //! # An interval is one question with two ends //! //! [`makeover_layout::FieldKind::Interval`] emits a `role="group"` named by the //! field's label, holding one `` per end. //! //! - **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 make this crate the owner of a delimiter that either end could contain. //! //! # A number's unit is adjacent text //! //! HTML has no unit attribute and inventing one would be markup nothing reads, //! so `Field::unit` is a `` after the control. 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. What that buys is a unit a consumer can read back rather //! than a suffix on a label it would have to parse. //! //! # A curve this renderer can carry, and one it declines //! //! A range takes its granularity from the curve (`Field::curve.step()`), every //! other kind keeps `Field::step`, and `Curve::Linear` emits a plain range. //! //! **A constant-ratio curve emits a linear track, and that is the answer, not a //! debt.** HTML has no logarithmic range input, so a described screen asking //! for one is asking the browser for something it does not have, the same class //! of request as [`makeover_layout::FieldKind::Date`] on a host with no //! calendar. The renderer answers with the nearest control the host really //! offers and keeps every fact that survives the translation: the extent, the //! granularity, and the value's own units. What does not survive is resolution //! at the small end. The value submitted is still a value in the field's own //! units, which is what every handler on this path reads. //! //! The alternatives are worse in the specific way this stack exists to avoid. //! Shipping JS that maps thumb position to value puts app code back in the //! renderer. Changing what the control submits from a value to a fraction moves //! the mapping to whoever reads the form, and a server reading these forms with //! its own handlers would take a fraction where a value is expected, silently. //! //! When this reopens: the day a described screen on the webview path asks for a //! non-linear range. The answer then is mapping in `quasi-router`, where one //! implementation serves every host, not JS here. //! //! # A markdown field gets a preview //! //! A [`makeover_layout::FieldKind::Rich`] field is marked //! `data-format="markdown"`, and [`form::editor_rules`] is what spends that //! mark. The Write/Preview pair is a segmented control, so it takes the depth, //! the focus ring and the chosen state from rules that already exist; 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 is //! for. A permission taken and not spent turns every conversion into a //! regression. //! //! **This crate 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 move that guarantee into a crate with no view of the //! host's content-security posture. //! //! # Ranges, ghost text, and an option that cannot be picked //! //! - `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. //! - `Choice::detail` takes the same split for the same reason: a //! `.form-option-detail` span in a radio group, run into the text of a //! `