aura-gui 0.2.2

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Piezas de estructura que casi toda pantalla repite: cabeceras, secciones, filas de ajuste y estados vacíos.
//!
//! Son funciones que devuelven un `Div` ya compuesto con recetas del tema; se encadenan como cualquier elemento.
//!
//! ```no_run
//! use aura_ui::prelude::*;
//! div().page().p_6().gap_4()
//!     .child(page_header("Ajustes", "Se guardan al instante"))
//!     .child(section("Tema", "Cambia toda la aplicación"))
//!     .child(setting_row("Animaciones", "Iconos vivos y transiciones", switch(true, "anim", |_, _| {})));
//! ```
//!
//! Las medidas viven en [`metrics`] para que una app pueda alinear sus propias piezas con las del kit.

use crate::core::px;
use crate::view::prelude::*;

/// Medidas compartidas por las piezas de estructura.
pub mod metrics {
    use crate::core::{Pixels, px};

    /// Alto mínimo de una fila de ajuste.
    pub const ROW_HEIGHT: Pixels = px(44.0);
    /// Separación entre el título y su pista.
    pub const TEXT_GAP: Pixels = px(4.0);
}

/// Cabecera de pantalla: título grande y, si la hay, una pista debajo.
pub fn page_header(title: impl Into<String>, subtitle: impl Into<String>) -> Div {
    let subtitle = subtitle.into();
    let mut header = div().vstack().gap(metrics::TEXT_GAP).child(div().heading().child(title.into()));
    if !subtitle.is_empty() {
        header = header.child(div().caption().child(subtitle));
    }
    header
}

/// Título de sección con una pista corta debajo; la pista vacía no ocupa espacio.
pub fn section(title: impl Into<String>, hint: impl Into<String>) -> Div {
    let hint = hint.into();
    let mut block = div().vstack().gap(metrics::TEXT_GAP).child(div().text_sm().child(title.into()));
    if !hint.is_empty() {
        block = block.child(div().caption().child(hint));
    }
    block
}

/// Fila de ajuste: descripción a la izquierda, control a la derecha.
pub fn setting_row(title: impl Into<String>, hint: impl Into<String>, control: impl IntoElement) -> Div {
    div()
        .hstack()
        .gap_3()
        .min_h(metrics::ROW_HEIGHT)
        .py(px(4.0))
        .child(div().flexible().child(section(title, hint)))
        .child(control)
}

/// Campo de formulario: etiqueta encima del control.
pub fn field(label: impl Into<String>, control: impl IntoElement) -> Div {
    div().vstack().gap(metrics::TEXT_GAP).child(div().text_sm().child(label.into())).child(control)
}

/// Hueco vacío y centrado, para listas sin resultados.
pub fn empty_state(title: impl Into<String>, hint: impl Into<String>) -> Div {
    let hint = hint.into();
    let mut state =
        div().vstack().flex_1().center().gap(metrics::TEXT_GAP).child(div().text_base().child(title.into()));
    if !hint.is_empty() {
        state = state.child(div().caption().child(hint));
    }
    state
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::view::theme::Token;

    #[test]
    fn setting_row_reparte_descripcion_y_control() {
        let mut row = setting_row("Tema", "Cambia todo", div().w_10());
        assert_eq!(row.style().direction, Some(crate::view::style::Direction::Row));
        assert_eq!(row.style().align_items, Some(crate::view::style::Align::Center));
        assert_eq!(row.style().min_height, Some(crate::view::style::Length::Px(metrics::ROW_HEIGHT)));
    }

    #[test]
    fn caption_y_muted_usan_el_token_secundario() {
        let mut caption = div().caption();
        assert_eq!(caption.style().text_color, Some(Token::TextMuted.into()));
        assert_eq!(caption.style().font_size, Some(px(12.0)));
    }

    #[test]
    fn empty_state_ocupa_el_hueco_y_centra() {
        let mut empty = empty_state("Sin resultados", "Prueba con otra búsqueda");
        let style = empty.style();
        assert_eq!(style.flex_grow, Some(1.0));
        assert_eq!(style.align_items, Some(crate::view::style::Align::Center));
        assert_eq!(style.justify_content, Some(crate::view::style::Justify::Center));
    }

    #[test]
    fn field_apila_etiqueta_y_control() {
        let mut field = field("Modelo", div().w_10());
        assert_eq!(field.style().direction, Some(crate::view::style::Direction::Column));
        assert_eq!(field.style().gap_y, Some(metrics::TEXT_GAP));
    }
}