euv-example 0.3.3

An example application demonstrating the euv UI framework with reactive signals, custom components, and WebAssembly.
Documentation
use crate::*;

/// Renders a navigation item link with active state styling.
///
/// # Arguments
///
/// - `Signal<String>`: The reactive signal holding the current route.
/// - `&str`: The display label for the navigation item.
/// - `&str`: The target route path.
///
/// # Returns
///
/// - `VirtualNode`: The navigation item virtual DOM tree.
pub fn nav_item(route_signal: Signal<String>, label: &str, target: &str) -> VirtualNode {
    let target_string: String = target.to_string();
    let current_route_value: String = route_signal.get();
    let is_active: bool = current_route_value == target;
    html! {
        a {
            href: format!("#{}", target_string)
            class: if { is_active } { c_nav_item_active() } else { c_nav_item_inactive() }
            onclick: link_handler(target_string)
            label
        }
    }
}

/// Renders a mobile navigation item link that closes the drawer on navigation.
///
/// # Arguments
///
/// - `Signal<String>`: The reactive signal holding the current route.
/// - `Signal<bool>`: The reactive signal controlling the mobile nav drawer visibility.
/// - `&str`: The display label for the navigation item.
/// - `&str`: The target route path.
///
/// # Returns
///
/// - `VirtualNode`: The mobile navigation item virtual DOM tree.
fn mobile_nav_item(
    route_signal: Signal<String>,
    drawer_open: Signal<bool>,
    label: &str,
    target: &str,
) -> VirtualNode {
    let target_string: String = target.to_string();
    let current_route_value: String = route_signal.get();
    let is_active: bool = current_route_value == target;
    let nav_target: String = target_string.clone();
    html! {
        a {
            href: format!("#{}", target_string)
            class: if { is_active } { c_nav_item_active() } else { c_nav_item_inactive() }
            onclick: move |_event: NativeEvent| {
                navigate(&nav_target);
                drawer_open.set(false);
            }
            label
        }
    }
}

/// Builds the navigation items list for the desktop sidebar.
///
/// # Arguments
///
/// - `Signal<String>`: The reactive signal holding the current route.
///
/// # Returns
///
/// - `VirtualNode`: The scrollable nav items container virtual DOM tree.
fn build_desktop_nav_items(route_signal: Signal<String>) -> VirtualNode {
    html! {
        div {
            class: c_nav_items_scroll()
            { nav_item(route_signal, "Signals", "/signals") }
            { nav_item(route_signal, "Event", "/event") }
            { nav_item(route_signal, "List", "/list") }
            { nav_item(route_signal, "Conditional", "/conditional") }
            { nav_item(route_signal, "Modal", "/modal") }
            { nav_item(route_signal, "Select", "/select") }
            { nav_item(route_signal, "Async", "/async") }
            { nav_item(route_signal, "Form", "/form") }
            { nav_item(route_signal, "Timer", "/timer") }
            { nav_item(route_signal, "Animation", "/animation") }
            { nav_item(route_signal, "Browser", "/browser") }
            { nav_item(route_signal, "Lifecycle", "/lifecycle") }
            { nav_item(route_signal, "Custom Attrs", "/custom-attrs") }
        }
    }
}

/// Builds the navigation items list for the mobile drawer.
///
/// Navigation items close the drawer on click.
///
/// # Arguments
///
/// - `Signal<String>`: The reactive signal holding the current route.
/// - `Signal<bool>`: The reactive signal controlling the mobile nav drawer visibility.
///
/// # Returns
///
/// - `VirtualNode`: The scrollable nav items container virtual DOM tree.
fn build_mobile_nav_items(route_signal: Signal<String>, drawer_open: Signal<bool>) -> VirtualNode {
    html! {
        div {
            class: c_nav_items_scroll()
            { mobile_nav_item(route_signal, drawer_open, "Signals", "/signals") }
            { mobile_nav_item(route_signal, drawer_open, "Event", "/event") }
            { mobile_nav_item(route_signal, drawer_open, "List", "/list") }
            { mobile_nav_item(route_signal, drawer_open, "Conditional", "/conditional") }
            { mobile_nav_item(route_signal, drawer_open, "Modal", "/modal") }
            { mobile_nav_item(route_signal, drawer_open, "Select", "/select") }
            { mobile_nav_item(route_signal, drawer_open, "Async", "/async") }
            { mobile_nav_item(route_signal, drawer_open, "Form", "/form") }
            { mobile_nav_item(route_signal, drawer_open, "Timer", "/timer") }
            { mobile_nav_item(route_signal, drawer_open, "Animation", "/animation") }
            { mobile_nav_item(route_signal, drawer_open, "Browser", "/browser") }
            { mobile_nav_item(route_signal, drawer_open, "Lifecycle", "/lifecycle") }
            { mobile_nav_item(route_signal, drawer_open, "Custom Attrs", "/custom-attrs") }
        }
    }
}

/// Renders the desktop layout with a persistent left sidebar navigation.
///
/// # Arguments
///
/// - `Signal<String>`: The reactive signal holding the current route.
/// - `Signal<String>`: The reactive signal holding the current theme.
/// - `Signal<String>`: The reactive signal holding the root class name.
/// - `Signal<bool>`: The reactive signal controlling vconsole panel visibility.
///
/// # Returns
///
/// - `VirtualNode`: The desktop application shell virtual DOM tree.
fn desktop_layout(
    route_signal: Signal<String>,
    theme_signal: Signal<String>,
    root_class_signal: Signal<String>,
    panel_open: Signal<bool>,
) -> VirtualNode {
    html! {
        div {
            class: root_class_signal
            nav {
                class: c_app_nav()
                a {
                    href: "https://github.com/euv-dev/euv"
                    target: "_blank"
                    class: c_nav_header()
                    span {
                        class: c_nav_logo()
                        "E"
                    }
                    span {
                        class: c_inline()
                        "Euv"
                    }
                }
                p {
                    class: c_nav_section_label()
                    "Pages"
                }
                { build_desktop_nav_items(route_signal) }
                div {
                    class: c_nav_theme_toggle()
                    button {
                        class: c_nav_theme_button()
                        onclick: toggle_theme(theme_signal)
                        if { theme_signal.get() == "dark" } {
                            ""
                        } else {
                            "🌙"
                        }
                    }
                }
                a {
                    href: "https://github.com/euv-dev/euv"
                    target: "_blank"
                    class: c_nav_footer()
                    "Built with Euv & WASM"
                }
            }
            main {
                class: c_app_main()
                { render_page(route_signal) }
            }
            { vconsole_panel(panel_open) }
        }
    }
}

/// Renders the mobile layout with a top header bar and a slide-out navigation drawer.
///
/// # Arguments
///
/// - `Signal<String>`: The reactive signal holding the current route.
/// - `Signal<String>`: The reactive signal holding the current theme.
/// - `Signal<String>`: The reactive signal holding the root class name.
/// - `Signal<bool>`: The reactive signal controlling vconsole panel visibility.
/// - `Signal<bool>`: The reactive signal controlling the mobile nav drawer visibility.
///
/// # Returns
///
/// - `VirtualNode`: The mobile application shell virtual DOM tree.
fn mobile_layout(
    route_signal: Signal<String>,
    theme_signal: Signal<String>,
    root_class_signal: Signal<String>,
    panel_open: Signal<bool>,
    drawer_open: Signal<bool>,
) -> VirtualNode {
    html! {
        div {
            class: root_class_signal
            header {
                class: c_mobile_header()
                div {
                    class: c_mobile_header_left()
                    button {
                        class: if { drawer_open.get() } { c_mobile_menu_button_active() } else { c_mobile_menu_button() }
                        onclick: use_toggle(drawer_open)
                        ""
                    }
                    span {
                        class: c_nav_logo()
                        "E"
                    }
                    span {
                        class: c_inline()
                        "euv"
                    }
                }
                button {
                    class: c_mobile_menu_button()
                    onclick: toggle_theme(theme_signal)
                    if { theme_signal.get() == "dark" } {
                        ""
                    } else {
                        "🌙"
                    }
                }
            }
            main {
                class: c_mobile_main()
                { render_page(route_signal) }
            }
            { vconsole_panel(panel_open) }
            if { drawer_open.get() } {
                div {
                    class: c_mobile_overlay()
                    onclick: move |_event: NativeEvent| {
                        drawer_open.set(false);
                    }
                }
                nav {
                    class: c_mobile_nav_drawer()
                    div {
                        class: c_mobile_nav_drawer_header()
                        a {
                            href: "https://github.com/euv-dev/euv"
                            target: "_blank"
                            class: c_nav_header()
                            span {
                                class: c_nav_logo()
                                "E"
                            }
                            span {
                                class: c_inline()
                                "Euv"
                            }
                        }
                        button {
                            class: c_mobile_nav_drawer_close()
                            onclick: move |_event: NativeEvent| {
                                drawer_open.set(false);
                            }
                            ""
                        }
                    }
                    p {
                        class: c_nav_section_label()
                        "Pages"
                    }
                    { build_mobile_nav_items(route_signal, drawer_open) }
                    div {
                        class: c_nav_theme_toggle()
                        button {
                            class: c_nav_theme_button()
                            onclick: toggle_theme(theme_signal)
                            if { theme_signal.get() == "dark" } {
                                ""
                            } else {
                                "🌙"
                            }
                        }
                    }
                    a {
                        href: "https://github.com/euv-dev/euv"
                        target: "_blank"
                        class: c_nav_footer()
                        "Built with Euv & WASM"
                    }
                }
            } else {
                ""
            }
        }
    }
}

/// Renders the route-matched page content.
///
/// # Arguments
///
/// - `Signal<String>`: The reactive signal holding the current route.
///
/// # Returns
///
/// - `VirtualNode`: The matched page virtual DOM tree.
fn render_page(route_signal: Signal<String>) -> VirtualNode {
    let current_route: String = route_signal.get();
    match current_route.as_str() {
        "/" | "/signals" => page_signals(),
        "/event" => page_event(),
        "/list" => page_list(),
        "/conditional" => page_conditional(),
        "/modal" => page_modal(),
        "/select" => page_select(),
        "/async" => page_async_demo(),
        "/form" => page_form(),
        "/timer" => page_timer(),
        "/animation" => page_animation(),
        "/browser" => page_browser(),
        "/lifecycle" => page_lifecycle(),
        "/custom-attrs" => page_custom_attrs(),
        _ => page_not_found(),
    }
}

/// Renders the application shell with navigation and route-based page content.
///
/// Detects viewport size and switches between desktop sidebar layout and
/// mobile header + drawer layout accordingly.
///
/// # Returns
///
/// - `VirtualNode`: The root application virtual DOM tree.
pub fn app() -> VirtualNode {
    init_console();
    let route_signal: Signal<String> = use_signal(current_route);
    let panel_open: Signal<bool> = use_signal(|| false);
    let drawer_open: Signal<bool> = use_signal(|| false);
    let mobile_signal: Signal<bool> = use_resize();
    let theme_state: ThemeState = use_theme();
    let theme_signal: Signal<String> = theme_state.theme;
    let root_class_signal: Signal<String> = theme_state.root_class;
    use_hash_change(route_signal);
    let is_mobile: bool = mobile_signal.get();
    let current_root_class: String = if is_mobile {
        format!(
            "{} {}",
            c_mobile_app_root().get_name(),
            theme_class_name(&theme_signal.get())
        )
    } else {
        root_class_signal.get()
    };
    let root_class: Signal<String> = use_signal(move || current_root_class);
    watch!(mobile_signal, theme_signal, |mobile, theme| {
        if mobile {
            root_class.set(format!(
                "{} {}",
                c_mobile_app_root().get_name(),
                theme_class_name(&theme)
            ));
        } else {
            root_class.set(format!(
                "{} {}",
                c_app_root().get_name(),
                theme_class_name(&theme)
            ));
        }
    });
    if is_mobile {
        mobile_layout(
            route_signal,
            theme_signal,
            root_class,
            panel_open,
            drawer_open,
        )
    } else {
        desktop_layout(route_signal, theme_signal, root_class, panel_open)
    }
}