Expand description
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_rulesemitsDepth::pressedwhole. goingson presses to--surface-sunkentoday and will press to--surface-well, and hovers to--surface-overlaytoday and will hover to--hover-surface. Sincesurface-wellinverts by theme wheresurface-sunkendoes not, a dark theme presses lighter than it hovers. That falls out ofmakeover’s own derivation, which says outright thatsurface-sunkencannot 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:
@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.17.0: the depth classes stop being controls
depth_rules gave .raised the whole interactive set. A depth is a
statement about shape, so that left the vocabulary with no raised surface
that is merely an object, and an app wanting one had two moves: write its
own class from tokens, or take a control class and cancel the control half.
goingson took the second, in three variants over sixteen elements
(.card--static at 14 call sites, .card--muted at 2, .card--shell at 1),
each re-asserting the resting fill and bevel on :hover and :active.
Measured before changing it: .raised is emitted into goingson, Balanced
Breakfast and the MNW server, and none of the three has a single call site.
The states were unasked-for everywhere at once, and dropping them costs no
migration anywhere.
.card and .button are unchanged. They are the same depth and controls,
and they take their states from [surface_rules], which is where a state
belongs: on the thing that claims to answer a pointer.
§Substitution, three ways
Fill::Well has no colour on makeover before 2.3.0, and each renderer
answers that differently, which is the evidence that dropping
Fill::fallback from the description was right:
makeover-immediatesubstitutes the page in Rust.makeover-tuirefuses to substitute and draws an edge instead, because a terminal would quantise the two together.- here, CSS already has the mechanism:
var(--surface-well, var(--surface-page))falls back in the browser, and nothing in Rust decides anything.
Modules§
- figure
- A figure with a caption, and a strip of them.
- form
- Phase B, the forms half:
makeover_layout::Fieldrendered to HTML. - list
- Column layout and row structure for lists and tables.
- meter
- A proportion, rendered as a bar.
- placeholder
- What a region shows when it is not showing its content.
Structs§
- Emit
- How the emitted CSS is shaped.
Constants§
- CSS_
LAYER - The cascade layer every stylesheet the make-family generates is wrapped in.
Functions§
- bevel_
properties - The custom properties both bevels resolve through.
- bevel_
shadow - The two-tone edge as a
box-shadowvalue. - bevel_
var - The CSS custom property holding a bevel’s composition.
- component_
rules - The component layer: every named thing phase A emits.
- depth_
class - The class name for a depth.
- depth_
declarations - The fill and edge declarations for a depth, as a rule body.
- depth_
rule - One rule giving a selector a depth, or nothing when the depth declares nothing.
- depth_
rules - One rule per depth: its fill and its edge, together.
- disabled_
rule - Present, visible, and not answering.
- fill_
var - A
var()reference to a fill intent, with the browser’s own fallback where the intent may be absent. - focus_
rule - The keyboard focus ring, placed by the depth it lands on.
- in_
css_ layer - Wrap generated CSS in
CSS_LAYER. - interactive_
rules - Every state a selector that answers a click implies: hover, pressed, focus and disabled, in that order.
- stylesheet
- The whole phase-A stylesheet: properties, depth rules and components, in
CSS_LAYER, under a generated-file banner.