hikari-components 0.2.2

Core UI components (40+) for the Hikari design system
//! Responsive hooks for Hikari components
//!
//! This module provides hooks for responsive design and media queries,
//! allowing components to adapt to different screen sizes.
//!
//! # Breakpoints
//!
//! - **Mobile**: 0-640px
//! - **Tablet**: 641-1023px
//! - **Desktop**: ≥1024px

use tairitsu_hooks::ReactiveSignal;

use crate::{platform::{inner_width as platform_inner_width, on_resize}, prelude::*};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Breakpoint {
    Mobile = 0,
    Tablet = 641,
    Desktop = 1024,
}

impl Breakpoint {
    pub fn min_width(&self) -> u32 {
        match self {
            Breakpoint::Mobile => 0,
            Breakpoint::Tablet => 641,
            Breakpoint::Desktop => 1024,
        }
    }

    pub fn max_width(&self) -> Option<u32> {
        match self {
            Breakpoint::Mobile => Some(640),
            Breakpoint::Tablet => Some(1023),
            Breakpoint::Desktop => None,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScreenSize {
    Mobile,
    Tablet,
    Desktop,
}

impl ScreenSize {
    pub fn is_mobile(&self) -> bool {
        matches!(self, ScreenSize::Mobile)
    }

    pub fn is_tablet_or_larger(&self) -> bool {
        matches!(self, ScreenSize::Tablet | ScreenSize::Desktop)
    }

    pub fn is_desktop_or_larger(&self) -> bool {
        matches!(self, ScreenSize::Desktop)
    }
}

pub fn use_screen_size() -> ReactiveSignal<ScreenSize> {
    let screen_size = use_signal(get_screen_size_from_window);

    let screen_size_clone = screen_size.clone();
    use_effect(move || {
        let screen_size = screen_size_clone.clone();
        on_resize(move || {
            screen_size.set(get_screen_size_from_window());
        });
    });

    screen_size
}

pub fn use_is_mobile() -> Signal<bool> {
    let screen_size = use_screen_size();
    use_memo(move || screen_size.read().is_mobile()).value()
}

pub fn use_is_desktop() -> Signal<bool> {
    let screen_size = use_screen_size();
    use_memo(move || screen_size.read().is_desktop_or_larger()).value()
}

fn get_screen_size_from_window() -> ScreenSize {
    let width = platform_inner_width() as u32;
    match width {
        0..=640 => ScreenSize::Mobile,
        641..=1023 => ScreenSize::Tablet,
        1024.. => ScreenSize::Desktop,
    }
}

pub fn use_media_query(min_width: Option<u32>, max_width: Option<u32>) -> ReactiveSignal<bool> {
    let matches = use_signal(|| check_media_query(min_width, max_width));

    let matches_clone = matches.clone();
    use_effect(move || {
        let matches = matches_clone.clone();
        on_resize(move || {
            matches.set(check_media_query(min_width, max_width));
        });
    });

    matches
}

fn check_media_query(min_width: Option<u32>, max_width: Option<u32>) -> bool {
    let width = platform_inner_width() as u32;

    if let Some(min) = min_width
        && width < min
    {
        return false;
    }

    if let Some(max) = max_width
        && width > max
    {
        return false;
    }

    true
}

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

    #[test]
    fn test_breakpoint_widths() {
        assert_eq!(Breakpoint::Mobile.min_width(), 0);
        assert_eq!(Breakpoint::Mobile.max_width(), Some(640));
        assert_eq!(Breakpoint::Tablet.min_width(), 641);
        assert_eq!(Breakpoint::Tablet.max_width(), Some(1023));
        assert_eq!(Breakpoint::Desktop.min_width(), 1024);
        assert_eq!(Breakpoint::Desktop.max_width(), None);
    }

    #[test]
    fn test_screen_size_checks() {
        assert!(ScreenSize::Mobile.is_mobile());
        assert!(!ScreenSize::Mobile.is_tablet_or_larger());
        assert!(!ScreenSize::Mobile.is_desktop_or_larger());

        assert!(!ScreenSize::Desktop.is_mobile());
        assert!(ScreenSize::Desktop.is_tablet_or_larger());
        assert!(ScreenSize::Desktop.is_desktop_or_larger());
    }
}