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
The stylesheet half needs no markup of the app’s to change: an app that writes its own markup keeps every line of it and gains the classes, which is why adopting the generated stylesheet is not a migration.
Adoption is not a deletion either. A rule loses its depth declarations and gains a variant selector next to it, so an app’s file stays about the same size. What moves is where depth is defined, not how much CSS exists.
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’s names, and the hosts’ markup
The vocabulary’s members (fields, choices, columns, figures, meters, charts,
acts and the rest) are drawn by the quasi hosts: quasi-webview for this
renderer. What phase B keeps here is everything those hosts and the
hand-written apps have to agree with this crate about:
form: the one escaper,form::Markupandform::Filling, the field classes and the rules behind them. One function covers element text and attribute values, where the apps needed four and had to choose correctly at every call site.list: column names as classes, the kind, part and flow classes, the cell wrapper and sizer, and theshort-Nladder a table narrows by.chart,figure,meter: class lists and rules only.facetrendersmakeover_layout::Facetitself: 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.placeholderdraws the stand-in a region shows while it has nothing.
Every class a host’s markup can carry is in one of the modules’ class lists,
and vocabulary::names is their union with the generated sheet.
§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_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.
§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-layout’s Affordance
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:
@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 them 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.
§Form rules
The field markup is the hosts’, and the look is this crate’s, because a renderer inventing how a label or an error reads is the drift the vocabulary check exists to catch. A few rules spend a mark the markup carries rather than a class:
- A markdown field is marked
data-format="markdown", andform::editor_rulesspends 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. This crate renders no markdown: the pane is filled by whatever binds the editor, which is where the host’s sanitiser already is. - A suggestion’s second line,
.form-suggestion-detail, is drawn in--content-mutedrather than in the disabled token. A detail orients rather than refuses, and every other secondary line in this crate reads the same way,.form-option-detailand.form-unitincluded. - An unavailable option’s reason is
.form-option-reason, and a group with an error carrieshas-errorwhile its message carriesvisible(form::FIELD_STATE_CLASSES).
§A cell says what it holds
CellPart names the four things a cell holds,
and table_rules turns them into .cell-value, .cell-tokens,
.cell-actions and .cell-link. The value and the link take a colour: a
token carries its own tone and an action is a control rather than text.
The colour goes on .cell-value rather than on .cell. On the container it
cascades into the parts that are not text, and a control in a cell is painted
as text, which is the drift
RowPart::intent prevents for list rows.
A cell’s part is a CellPart and never a RowPart: the two answer
different questions, and only one of them is about a cell.
§A table narrows itself
A table a description produced knows its columns at render time, so no rule
can be written per table: it would have to travel with the markup, as a
<style> element that needs style-src 'unsafe-inline' or in a head an htmx
fragment swap does not carry. table_rules lays every table out as flex
rows instead, each column starting at its declared floor and giving up room
by its Priority, so the browser narrows a
table at the width its floors add up to. A host puts the column’s classes
on every cell (its name through list::push_column_name, its width and
drop tiers from list::CELL_WIDTH_CLASSES and list::CELL_DROP_CLASSES,
its kind from list::kind_class), and every cell’s contents go in
list::CELL_IN. A header row emitted by a renderer’s own code has to do
both too, or the header and the body disagree about which column just
closed and how wide the rest are.
.button takes the four tones as colour, off data-tone, the way the badge
does, so a destructive button has somewhere for its tone to land. A list is
reset rather than left as a bulleted list.
RowPart::revealed_on_hover is not honoured. Hiding a row’s actions until
hover hides them from pointer users alone, who are the ones scanning a list
to learn what can be done to a row, and every escape the rule grows
(focus-within for the keyboard, a capability gate for a fingertip) is a
report that hiding was wrong for somebody.
§The depth classes are not controls
.raised is a statement about shape and carries no interactive set, so the
vocabulary has a raised surface that is merely an object. An app that wants
one does not have to take a control class and cancel the control half.
.button is the same depth and a control, and takes its states from
surface_rules, which is where a state belongs: on the thing that claims
to answer a pointer. .card is a raised object, and it answers a pointer
only when the element it is written on is a control, a, button,
label, or anything carrying data-act, so one name serves the card
that is read and the card that is pressed.
§Substitution, three ways
A theme with no surface-well is answered differently by each renderer,
which is why substitution belongs to a renderer and not to the description:
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§
- build
- Build-script support: the generated files every webview consumer
materialises from its
build.rs, and the checks that keep its hand-written frontend agreeing with them. - chart
- The stylesheet half of a chart: a run of magnitudes against one axis, drawn as bars.
- facet
- A dimension a set is narrowed by, rendered as a list of values.
- figure
- The stylesheet half of a figure with a caption, and a strip of them.
- form
- Phase B, the forms half: the escaper, the field classes and the rules a described field’s markup is drawn with.
- list
- The classes a list’s or a table’s columns, rows and cells carry.
- meter
- The stylesheet half of a meter: a proportion, drawn as a bar.
- placeholder
- What a region shows when it is not showing its content.
- reset
- What an HTML element brings uninvited, and how a primitive gives it back.
- vocabulary
- Every class this crate is responsible for, as a set rather than one name at a time.
Structs§
- Emit
- How the emitted CSS and markup are shaped, and the copy they carry.
Constants§
- CSS_
LAYER - The cascade layer every stylesheet the make-family generates is wrapped in.
- DENSITY_
CHOICE_ ATTR - The attribute a
FieldKind::Densitypicker carries, which is howVARS_JSfinds it. - DENSITY_
STORE_ KEY - The browser storage key a device’s density choice is kept under.
- RUN_
CLASSES - Every class
fallback_classcan return, plus the run itself. - STRIP_
CONTAINER - The container a vertical tab strip’s arrangement names when it can fold the strip over its pane.
- STRIP_
FOLD - The inline size of
STRIP_CONTAINERunder which a vertical strip runs across its pane. An arrangement that folds the strip folds at this width, or the tabs draw one way while laid out the other. - VARS_
ATTR - The attribute an element’s custom properties ride in, instead of
style. - VARS_JS
- The script that applies
VARS_ATTR, for a host to serve beside its other assets. - VERSION
- This crate’s version, as the generated stylesheet reports it.
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.
- class
- A prefixed class name.
- component_
rules - The component layer: every named thing phase A emits.
- density_
options_ into - Write the options of a density picker, in
DENSITY_CHOICESorder. - 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.
- fallback_
class - The class a run carries for what it does when it is tight.
- 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.
- option_
class - The class an option of a selector carries, which is what the rules key off.
- pressable_
card - The compound a card’s interactive states are written against: the class on an element that is itself a control.
- push_
class - A prefixed class name, written into a buffer the caller already has.
- stylesheet
- The whole phase-A stylesheet: properties, depth rules and components, in
CSS_LAYER, under a generated-file banner.