Skip to main content

Crate makeover_webview

Crate makeover_webview 

Source
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::Markup and form::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 the short-N ladder a table narrows by.
  • chart, figure, meter: class lists and rules only.
  • facet renders makeover_layout::Facet itself: 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.
  • placeholder draws 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_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-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", and form::editor_rules 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. 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-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, .form-option-detail and .form-unit included.
  • An unavailable option’s reason is .form-option-reason, and a group with an error carries has-error while its message carries visible (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-immediate substitutes the page in Rust.
  • makeover-tui refuses 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.
RUN_CLASSES
Every class fallback_class can return, plus the run itself.
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-shadow value.
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.
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.