aura-gui 0.3.5

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.
//! Paleta de Tailwind: `blue(500)`, `slate(900)`, `rose(50)`… devuelven un `Rgba` listo para `.bg(..)`,
//! `.text_color(..)` o `.border_color(..)`. Son constantes: no reservan memoria ni consultan el tema.
//!
//! ```
//! use aura_ui::view::palette::{blue, slate};
//! let fondo = slate(900);
//! let acento = blue(500).with_alpha(0.5); // `bg-blue-500/50`
//! # let _ = (fondo, acento);
//! ```

use crate::core::Rgba;
use crate::view::palette_data::{SHADES, TABLE};

/// Índice de una intensidad de Tailwind: 50 → 0, 100 → 1 … 900 → 9, 950 → 10. Una intensidad que no existe
/// se redondea a la más cercana en vez de fallar.
const fn shade_index(shade: u16) -> usize {
    let index = if shade < 100 { 0 } else { (shade as usize + 50) / 100 };
    if index >= SHADES { SHADES - 1 } else { index }
}

const fn pick(hue: usize, shade: u16) -> Rgba {
    Rgba::hex(TABLE[hue][shade_index(shade)])
}

macro_rules! hues {
    ($($index:expr => $name:ident),* $(,)?) => {
        $(
            #[doc = concat!("`", stringify!($name), "-<intensidad>` de Tailwind (50, 100…900, 950).")]
            pub const fn $name(shade: u16) -> Rgba {
                pick($index, shade)
            }
        )*

        /// Todos los tonos con su nombre, para selectores de color o para buscar por texto.
        pub const HUES: [(&str, fn(u16) -> Rgba); 22] = [$((stringify!($name), $name as fn(u16) -> Rgba)),*];
    };
}

hues! {
    0 => slate, 1 => gray, 2 => zinc, 3 => neutral, 4 => stone, 5 => red, 6 => orange, 7 => amber, 8 => yellow,
    9 => lime, 10 => green, 11 => emerald, 12 => teal, 13 => cyan, 14 => sky, 15 => blue, 16 => indigo,
    17 => violet, 18 => purple, 19 => fuchsia, 20 => pink, 21 => rose,
}

pub const fn white() -> Rgba {
    Rgba::hex(0xffffff)
}

pub const fn black() -> Rgba {
    Rgba::hex(0x000000)
}

pub const fn transparent() -> Rgba {
    Rgba::TRANSPARENT
}

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

    fn rgb(color: Rgba) -> u32 {
        let byte = |value: f32| (value * 255.0).round() as u32;
        byte(color.r) << 16 | byte(color.g) << 8 | byte(color.b)
    }

    #[test]
    fn valores_conocidos_de_tailwind() {
        assert_eq!(rgb(blue(500)), 0x3b82f6);
        assert_eq!(rgb(slate(900)), 0x0f172a);
        assert_eq!(rgb(red(50)), 0xfef2f2);
        assert_eq!(rgb(rose(950)), 0x4c0519);
        assert_eq!(rgb(white()), 0xffffff);
    }

    #[test]
    fn una_intensidad_inexistente_se_redondea() {
        assert_eq!(rgb(blue(0)), rgb(blue(50)));
        assert_eq!(rgb(blue(2000)), rgb(blue(950)));
        assert_eq!(rgb(blue(480)), rgb(blue(500)));
    }

    #[test]
    fn cada_tono_va_de_claro_a_oscuro() {
        let luma = |c: Rgba| 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
        for (name, hue) in HUES {
            let shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950].map(|shade| luma(hue(shade)));
            assert!(shades.windows(2).all(|pair| pair[0] > pair[1]), "{name}: {shades:?}");
        }
    }

    #[test]
    fn la_transparencia_se_conserva() {
        assert_eq!(blue(500).with_alpha(0.5).a, 0.5);
    }
}