makeover-webview 0.111.0

The webview renderer for makeover-layout. Emits CSS, and is the one renderer that needs no palette: var() is the late binding, so resolution stays with the browser. With the build feature, writes a consumer's generated frontend files and checks the hand-written ones against them.
Documentation
//! The stylesheet half of a figure with a caption, and a strip of them.
//!
//! The markup is drawn by the quasi hosts (`quasi-webview` for this renderer),
//! which own the vocabulary member. This module owns the classes that markup
//! carries and the rules that draw them. Left to an app, one shape grows a class
//! name per screen: `task-overview-stat`, `stat-box`, `month-stat-item`,
//! `contact-summary-stat`, `sync-stat`.
//!
//! # The reading order is markup, not CSS
//!
//! Visually the value is set large over a small caption. A screen reader meeting
//! "17" before it knows what was counted has to hold the number until the noun
//! arrives, so the markup carries its own accessible name ("Current Streak: 17")
//! and the two spans are hidden from the reader that has already been told.
//! The arrangement and the type scale are the app's, and these rules name no
//! size, the same line [`meter`](crate::meter) holds when it emits the tones and
//! never the width.

use crate::{Emit, class};
use makeover_layout::{Intent as _, Tone};
use std::fmt::Write as _;

/// Every class a figure's markup can carry.
///
/// [`crate::facet::FACET_CLASSES`]' obligation. `figures` is the strip around
/// them, and the sheet lays it out as a wrapping row of tiles a section gap
/// apart.
pub const FIGURE_CLASSES: &[&str] = &[
    "figures",
    "figure",
    "figure-value",
    "figure-caption",
    "figure-change",
];

pub(crate) fn figure_rules(opts: &Emit) -> String {
    let figures = class("figures", opts);
    let figure = class("figure", opts);
    let value = class("figure-value", opts);
    let caption = class("figure-caption", opts);
    let change = class("figure-change", opts);
    let mut css = String::new();

    // A strip of tiles that wraps rather than overflows, a section apart. It
    // was quasi-webview's arrangement sheet's to say, for a name this crate
    // emits.
    //
    // **The strip is one object, and its cells are even.** It was a flex row
    // with a gap and nothing else, so a set of figures read as its values and
    // captions scattered along a line: every figure started where the last one
    // ended, no column formed, and four numbers looked like eight loose words.
    // That is the defect [`facts_rules`] already records for a labelled row,
    // and the answer is the same one, alignment only the SET can give, which
    // is why `Node::Stats` carries the set rather than each figure.
    //
    // A ground with its own air, sized to what it holds. The ground is what
    // makes a set read as a set; the derived width is what stops it reading as
    // a banner with its contents in one corner.
    //
    // `max-content` under `max-width: 100%` rather than `fit-content`, which
    // says the same thing and is not a value this crate may name: the literal
    // guard takes `max-content` as "a derived width and not one anybody chose"
    // and refuses `fit-content`, and the pair comes to the same layout.
    //
    // Both halves were measured on MNW's waitlist at 1440, where the strip
    // holds four figures and the page runs 1376px. Stretching the cells to
    // share the line (`flex: 1 1 0`) put `4 Pending` and `15 Creators` at
    // opposite ends of the screen with a third of the window empty between
    // them, which trades scattered text for a scattered row. Letting the band
    // run full width left the four figures huddled in its left corner. Cells at
    // their natural width inside a band that hugs them is the one of the three
    // that reads as a group.
    //
    // `max-width: 100%` so the band still yields at a narrow window: the cells
    // wrap inside it rather than pushing the document sideways.
    //
    // The ground is a step of the theme's own neutral ramp, never a tone, and
    // the fallback chain is the one this crate's header states for a well, so a
    // theme naming no sunken surface gets a readable band rather than a missing
    // colour. Not a raised surface per figure: that makes four cards out of a
    // summary, and a summary is rarely the point of the page it sits on.
    let _ = writeln!(
        css,
        ".{figures} {{\n    display: flex;\n    flex-wrap: wrap;\n    gap: var(--gap-pane);\n    \
         padding: var(--gap-section);\n    width: max-content;\n    max-width: 100%;\n    \
         background: var(--surface-sunken, var(--surface-raised));\n}}"
    );

    // `content` literally. A figure's value is the thing itself, at full
    // weight, wiki `three-tone-convention` classes it "active, emphasised",
    // and both other renderers already draw it that way (makeover-tui
    // `piece.rs:391` bold, makeover-immediate `widget.rs:244`). It reached
    // here through `Tone::Neutral.token()` and came out `content-muted`, so
    // the headline number sat at the colour of its own caption.
    //
    // Bold for the same reason, and the half that was still missing: goingson's
    // project strip read "3 / Open tasks" with the number at the caption's size
    // and weight, so the set did not lead with its values, while the terminal
    // and the immediate host both drew them bold.
    //
    // And a size of its own, a step of the text scale above the body, with
    // the caption a step below it: in colour alone the two read as one
    // sentence, and MNW could not draw a label-value record as a figure
    // without trading a readable line for two same-sized words. A terminal
    // has one size and leans on weight; this one has a scale.
    let _ = writeln!(
        css,
        ".{figure} > .{value} {{\n    color: var(--content);\n    font-weight: bold;\n    \
         font-size: var(--text-title);\n}}"
    );
    let _ = writeln!(
        css,
        ".{figure} > .{caption} {{\n    color: var(--content-muted);\n    font-size: var(--text-note);\n}}"
    );
    let _ = writeln!(
        css,
        ".{figure} > .{change} {{\n    color: var(--content-muted);\n}}"
    );

    // A toned figure tones one part and never the caption. The caption is the
    // noun and stays muted.
    //
    // Which part depends on whether there is a change, and that is the whole of
    // what 0.13.0 changed here. A figure with a delta is an ordinary number that
    // has moved in a direction worth reading, so the delta takes the colour and
    // the number stays plain; a figure without one has nowhere else to put it.
    // `:has` is what lets one attribute mean both, and the alternative was the
    // emitter deciding by writing the attribute onto a different element, which
    // leaves two elements able to disagree about a figure's one meaning.
    // The tone is an edge, and neither number is drawn in it. Both were until
    // 0.86.0, and both failed the same way `{tone}-surface`'s derivation
    // comment describes: measured on goingson, a toned value read 1.24:1, 1.53:1
    // and 2.27:1 against the ground it sits on. A figure's value is the
    // largest text on the screen and the one most worth reading, so it keeps
    // `content` whatever the tone is.
    //
    // An edge rather than the tinted fill a chip and a button take. A figure
    // has no box of its own, it is a bare pair of spans in a flex strip,
    // and giving the toned ones a fill would give a strip of figures two
    // different shapes depending on which of them had news. The rule sits on
    // the side so a neutral figure is untouched and the tint never has to exist.
    //
    // This also retires the `:has` split. It existed to choose which of the two
    // elements took the colour; with neither taking it, a figure with a delta
    // and a figure without one want the same rule, and the two elements can no
    // longer disagree about one tone because neither states it.
    for tone in [Tone::Info, Tone::Success, Tone::Warning, Tone::Danger] {
        let _ = writeln!(
            css,
            ".{figure}[data-tone=\"{0}\"] {{\n    border-inline-start: {1} solid \
             var(--{0});\n    padding-inline-start: var(--step-snug);\n}}",
            tone.token(),
            opts.focus_width
        );
    }
    css
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Every class the markup can carry has a rule, which is what
    /// `FIGURE_CLASSES` exists to keep true.
    #[test]
    fn every_class_this_module_names_has_a_rule() {
        let css = figure_rules(&Emit::default());
        for name in FIGURE_CLASSES {
            assert!(css.contains(&format!(".{name}")), "{name} has no rule");
        }
    }
}