wavefunk-ui 0.1.5

Askama and htmx UI component base for Wave Funk Rust applications.
Documentation
#[test]
fn gallery_example_uses_reusable_classes_for_static_layout() {
    let source = include_str!("../examples/axum_gallery.rs");

    assert!(
        !source.contains(r#"style=""#),
        "examples/axum_gallery.rs should use reusable classes instead of inline style attributes"
    );
}

#[test]
fn gallery_example_exposes_state_switches() {
    let source = include_str!("../examples/axum_gallery.rs");

    assert!(source.contains("state=open"));
    assert!(source.contains("state=drawer"));
    assert!(source.contains("state=loading"));
    assert!(source.contains("Open overlays"));
}

#[test]
fn axum_gallery_exposes_real_htmx_backend_routes() {
    let source = include_str!("../examples/axum_gallery.rs");

    for route in [
        r#".route("/components/{section}", get(component_page))"#,
        r#".route("/fragments/components/{section}", get(component_fragment))"#,
        r#".route("/profile", post(save_profile))"#,
        r#".route("/fragments/table", get(table_fragment))"#,
        r#".route("/fragments/loading", get(loading_fragment))"#,
    ] {
        assert!(source.contains(route), "missing route: {route}");
    }

    for htmx_attr in [
        r##"hx-target="#gallery-main""##,
        r#"hx-push-url"#,
        r#"HtmlAttr::hx_post("/profile")"#,
        r#"HtmlAttr::hx_get("/fragments/table")"#,
        r##"HtmlAttr::new("hx-indicator", "#profile-saving")"##,
        r#"HtmlAttr::hx_trigger("keyup changed delay:250ms, search")"#,
    ] {
        assert!(
            source.contains(htmx_attr),
            "missing htmx example: {htmx_attr}"
        );
    }

    for primitive in [
        "PageHeader::new(section.title)",
        "HtmxPartial::new(section.title, TrustedHtml::new(&main))",
        ".with_nav(TrustedHtml::new(&nav))",
        "FilterBar::new(TrustedHtml::new(&filter_input))",
        "RowSelect::new(\"workflow\", name, \"Select workflow\")",
        "OwnedDataTable::new(&headers, rows)",
        "OwnedDataTableCell::strong(name.to_owned())",
        "BulkActionBar::new(\"1 selected\", TrustedHtml::new(&bulk_delete))",
        "TableFooter::new(TrustedHtml::new(\"Showing 1-4 of 4\"))",
    ] {
        assert!(
            source.contains(primitive),
            "gallery should exercise reusable page/partial primitive: {primitive}"
        );
    }

    for section in [
        "Forms and submission",
        "Data tables and filters",
        "Feedback and overlays",
        "Layout and navigation",
        "Extended primitives",
        "Migration-ready patterns",
    ] {
        assert!(
            source.contains(section),
            "sidebar/page should expose {section}"
        );
    }

    assert!(
        source.contains("Modal::new(\n        \"Confirm deployment\"")
            && source.contains(")\n    .large()"),
        "gallery should exercise large modal sizing"
    );
}

#[test]
fn gallery_example_exposes_migration_readiness_components() {
    let source = include_str!("../examples/axum_gallery.rs");

    for primitive in [
        "SplitShell::new(",
        "FormPanel::new(",
        "Modeline::new(",
        "ModelineSegment::",
        "ContextSwitcher::new(",
        "Sidenav::new(",
        "SidenavSection::new(",
        "SidenavItem::link(",
        "SecretValue::new(",
        "Checklist::new(",
        "CodeGrid::new(",
        "CodeBlock::new(",
        "SnippetTabs::new(",
        "StrengthMeter::new(",
        "SettingsSection::new(",
        "MinibufferEcho::info(",
        "InlineFormRow::new(",
        "CopyableValue::new(",
        "CredentialStatusList::new(",
        "ConfirmAction::new(",
        "ObjectFieldset::new(",
        "RepeatableArray::new(",
        "CurrentUpload::new(",
        "ReferenceSelect::new(",
        "MarkdownTextarea::new(",
        "RichTextHost::new(",
        r#"HtmlAttr::new("data-wf-dirty-guard", "true")"#,
        r##"HtmlAttr::new("data-wf-submit-spinner", "#profile-saving")"##,
    ] {
        assert!(
            source.contains(primitive),
            "gallery should exercise migration primitive: {primitive}"
        );
    }
}

#[test]
fn gallery_example_exposes_app_shell_extension_points() {
    let source = include_str!("../examples/axum_gallery.rs");

    for extension in [
        ".with_head(TrustedHtml::new(",
        ".with_topbar(TrustedHtml::new(&shell_topbar))",
        ".with_htmx_sse()",
        ".with_scripts(TrustedHtml::new(",
        "gallery-shell-config",
    ] {
        assert!(
            source.contains(extension),
            "gallery should demonstrate AppShell extension point: {extension}"
        );
    }
}

#[test]
fn app_shell_contains_mobile_overflow_guards() {
    let css = include_str!("../static/wavefunk/css/03-layout.css");
    let utilities = include_str!("../static/wavefunk/css/05-utilities.css");
    let components = include_str!("../static/wavefunk/css/04-components.css");

    assert!(css.contains("max-width: 100vw"));
    assert!(css.contains("overflow-x: auto"));
    assert!(css.contains("scrollbar-width: none"));
    assert!(utilities.contains(".wf-g > * { min-width: 0; }"));
    assert!(utilities.contains(".wf-f > * { min-width: 0; }"));
    assert!(components.contains(".wf-step { min-width: 0; }"));
    assert!(components.contains(".wf-stepper { flex-direction: column; }"));
}

#[test]
fn dropzone_hidden_input_does_not_force_scroll_width() {
    let css = include_str!("../static/wavefunk/css/04-components.css");

    assert!(css.contains(".wf-dropzone-input {\n  position: absolute; inset: 0;"));
    assert!(css.contains("width: 100%; height: 100%;"));
    assert!(css.contains("cursor: pointer"));
    assert!(css.contains("font-size: 0"));
}

#[test]
fn panel_row_components_extend_separators_to_panel_borders() {
    let css = include_str!("../static/wavefunk/css/04-components.css");
    let rule_start = css
        .find(".wf-panel-bleed,")
        .expect("panel row components should share a panel bleed selector");
    let selector_end = css[rule_start..]
        .find('{')
        .expect("panel bleed selector should have a declaration block");
    let selectors = &css[rule_start..rule_start + selector_end];

    for selector in [
        ".wf-table",
        ".wf-tablewrap",
        ".wf-dl",
        ".wf-stepper",
        ".wf-accordion",
        ".wf-faq",
        ".wf-statusbar",
        ".wf-rank",
        ".wf-feed",
        ".wf-empty.bordered",
    ] {
        assert!(
            selectors.contains(selector),
            "{selector} should bleed to panel borders when it draws row separators"
        );
    }

    assert!(css.contains("margin-inline: calc(0px - var(--wf-panel-pad-x, var(--space-4)))"));
    assert!(css.contains("width: calc(100% + var(--wf-panel-pad-x, var(--space-4)) + var(--wf-panel-pad-x, var(--space-4)))"));
    assert!(css.contains(".wf-panel-body > .wf-accordion .wf-accordion-trigger"));
    assert!(css.contains(".wf-panel-body > .wf-faq .wf-faq-row"));
}

#[test]
fn panel_bleed_preserves_inner_content_inset() {
    let css = include_str!("../static/wavefunk/css/04-components.css");

    for rule in [
        ".wf-panel-body > .wf-table:not(.flush) th:first-child",
        ".wf-panel-body > .wf-tablewrap .wf-table:not(.flush) th:first-child",
        ".wf-panel-body > .wf-tablewrap > .wf-filterbar",
        ".wf-panel-body > .wf-dl dt",
        ".wf-panel-body > .wf-rank .wf-rank-row",
        ".wf-panel-body > .wf-feed .wf-feed-row",
        ".wf-panel-body > .wf-statusbar",
        ".wf-panel-body > .wf-minibuffer",
        ".wf-panel-body > .wf-stepper > .wf-step:first-child",
    ] {
        assert!(
            css.contains(rule),
            "{rule} should restore content inset after panel bleed"
        );
    }

    assert!(css.contains("calc(20px + var(--wf-panel-pad-x, var(--space-4)))"));
    assert!(css.contains("calc(16px + var(--wf-panel-pad-x, var(--space-4)))"));
    assert!(css.contains("calc(12px + var(--wf-panel-pad-x, var(--space-4)))"));
}

#[test]
fn embedded_assets_are_canonical_to_this_crate() {
    let manifest = include_str!("../Cargo.toml");
    let justfile = include_str!("../justfile");
    let readme = include_str!("../README.md");
    let agent_notes = include_str!("../AGENTS.md");

    assert!(manifest.contains(r#""/AGENTS.md""#));
    assert!(manifest.contains(r#""/justfile""#));
    assert!(
        !justfile.contains("vendor-design"),
        "justfile should not expose a recipe that overwrites crate CSS from another repo"
    );
    assert!(
        !justfile.contains("../design"),
        "justfile should not copy CSS or fonts from ../design"
    );
    assert!(readme.contains("source of truth for Wave Funk runtime assets"));
    assert!(agent_notes.contains("Do not sync CSS or fonts from `../design`"));
}