Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-web/src/narrow.rs

//! **On a phone the README opens in its plain grade** (GUN-FIELD-TEST, 2026-11-01), on the
//! host side of the wasm boundary. The typeset page is a drawing about 720 units wide;
//! at 400 px the console shrank it to text nobody could read. Below [`NARROW_PX`] the
//! README panel and a Markdown file's rendered view ask the server's plain grade
//! (`?as=html`) first; the HTML toggle still swaps to the typeset page.

/// Below this many CSS pixels of viewport, the plain grade comes first.
pub const NARROW_PX: f64 = 640.0;

/// Pure: does a viewport this wide open the plain grade first? An unknown width
/// (0) keeps the typeset default.
pub fn plain_first(viewport_width: f64) -> bool {
    viewport_width > 0.0 && viewport_width < NARROW_PX
}

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

    /// ★ A phone reads the plain grade; a desktop the typeset page.
    #[test]
    fn a_phone_opens_the_plain_grade_and_a_desktop_the_typeset_page() {
        assert!(plain_first(400.0), "400 px");
        assert!(plain_first(639.0));
        assert!(!plain_first(640.0));
        assert!(!plain_first(1280.0), "1280 px");
        assert!(!plain_first(0.0), "an unknown width keeps the default");
    }

    /// The body of the first CSS rule for exactly `selector`.
    fn rule<'a>(css: &'a str, selector: &str) -> &'a str {
        css.split(&format!("\n{selector} {{")).nth(1).and_then(|r| r.split('}').next()).unwrap_or("")
    }

    /// ★ **No sideways page at 400 px** (GUN-FIELD-TEST, 2026-11-01). Measured on /repos and
    /// /access: the document was 648 and 578 px wide. A table's screen-reader caption
    /// (`.vh`, `position: absolute`) found no positioned ancestor and was placed in the
    /// initial containing block at the wide table's edge, so the whole page scrolled
    /// although the table scrolled in its own wrapper; and the page header's buttons did
    /// not wrap. The wrapper contains its absolute children and the header wraps.
    #[test]
    fn a_wide_table_and_the_header_buttons_stay_inside_a_phone() {
        let css = include_str!("../styles/console.css");
        assert!(rule(css, ".table-wrap").contains("position: relative"), "the table wrapper contains its caption: {:?}", rule(css, ".table-wrap"));
        assert!(rule(css, ".page-header").contains("flex-wrap: wrap"), "the page header wraps: {:?}", rule(css, ".page-header"));
    }

    /// ★ **Every data table scrolls inside its own wrapper** (GUN-FIELD-TEST, 2026-11-01:
    /// Administration → Storage at 400 px scrolled the whole page, 665 px, under two tables
    /// drawn straight into the panel). A `<table` in a page sits in a `.table-wrap` (or is a
    /// file's or a diff's, which carry their own scrolling wrapper).
    #[test]
    fn every_data_table_scrolls_inside_its_wrapper() {
        let pages: [(&str, &str); 4] = [
            ("pages/admin_storage.rs", include_str!("pages/admin_storage.rs")),
            ("pages/usage.rs", include_str!("pages/usage.rs")),
            ("pages/subscription.rs", include_str!("pages/subscription.rs")),
            ("pages/access.rs", include_str!("pages/access.rs")),
        ];
        let mut bare = Vec::new();
        for (file, src) in pages {
            let lines: Vec<&str> = src.lines().collect();
            for (i, line) in lines.iter().enumerate() {
                if line.contains("<table") && !lines[i.saturating_sub(3)..=i].iter().any(|l| l.contains("table-wrap")) {
                    bare.push(format!("{file}:{}", i + 1));
                }
            }
        }
        assert!(bare.is_empty(), "tables outside a .table-wrap: {bare:?}");
    }

    /// ★ Both places that draw a typeset document ask for the plain grade on a phone.
    #[test]
    fn the_readme_panel_and_the_file_view_follow_the_width() {
        let readme = crate::code::of(include_str!("pages/readme.rs"));
        assert!(readme.contains("crate::narrow::plain_first("), "the README panel");
        let browse = crate::code::of(include_str!("pages/browse.rs"));
        assert!(browse.contains("crate::narrow::plain_first(") && browse.contains("&as=html"), "a Markdown file's rendered view");
    }
}