kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
use crate::components::icon::Icon;
use crate::components::icon_source::IconSource;
use crate::theme::use_theme;
use kael::{prelude::FluentBuilder as _, *};
use std::rc::Rc;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum AlertVariant {
    #[default]
    Info,
    Success,
    Warning,
    Error,
}

impl AlertVariant {
    fn default_icon(&self) -> &'static str {
        match self {
            AlertVariant::Info => "info",
            AlertVariant::Success => "check-circle",
            AlertVariant::Warning => "alert-triangle",
            AlertVariant::Error => "alert-circle",
        }
    }
}

#[derive(IntoElement)]
pub struct Alert {
    variant: AlertVariant,
    title: Option<SharedString>,
    description: Option<SharedString>,
    icon: Option<IconSource>,
    show_icon: bool,
    dismissible: bool,
    on_dismiss: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
    action: Option<(SharedString, Rc<dyn Fn(&mut Window, &mut App)>)>,
    style: StyleRefinement,
}

impl Alert {
    pub fn new() -> Self {
        Self {
            variant: AlertVariant::default(),
            title: None,
            description: None,
            icon: None,
            show_icon: true,
            dismissible: false,
            on_dismiss: None,
            action: None,
            style: StyleRefinement::default(),
        }
    }

    pub fn info() -> Self {
        Self::new().variant(AlertVariant::Info)
    }

    pub fn success() -> Self {
        Self::new().variant(AlertVariant::Success)
    }

    pub fn warning() -> Self {
        Self::new().variant(AlertVariant::Warning)
    }

    pub fn error() -> Self {
        Self::new().variant(AlertVariant::Error)
    }

    pub fn variant(mut self, variant: AlertVariant) -> Self {
        self.variant = variant;
        self
    }

    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
        self.title = Some(title.into());
        self
    }

    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
        self.description = Some(description.into());
        self
    }

    pub fn icon(mut self, icon: impl Into<IconSource>) -> Self {
        self.icon = Some(icon.into());
        self
    }

    pub fn show_icon(mut self, show: bool) -> Self {
        self.show_icon = show;
        self
    }

    pub fn dismissible(mut self, dismissible: bool) -> Self {
        self.dismissible = dismissible;
        self
    }

    pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
        self.on_dismiss = Some(Rc::new(handler));
        self.dismissible = true;
        self
    }

    pub fn action(
        mut self,
        label: impl Into<SharedString>,
        handler: impl Fn(&mut Window, &mut App) + 'static,
    ) -> Self {
        self.action = Some((label.into(), Rc::new(handler)));
        self
    }

    fn get_colors(&self, theme: &crate::theme::Theme) -> (Hsla, Hsla, Hsla) {
        match self.variant {
            AlertVariant::Info => (
                theme.tokens.primary.opacity(0.1),
                theme.tokens.primary,
                theme.tokens.primary,
            ),
            AlertVariant::Success => {
                let success_color: Hsla = rgb(0x22c55e).into();
                (success_color.opacity(0.1), success_color, success_color)
            }
            AlertVariant::Warning => {
                let warning_color: Hsla = rgb(0xf59e0b).into();
                (warning_color.opacity(0.1), warning_color, warning_color)
            }
            AlertVariant::Error => (
                theme.tokens.destructive.opacity(0.1),
                theme.tokens.destructive,
                theme.tokens.destructive,
            ),
        }
    }
}

impl Default for Alert {
    fn default() -> Self {
        Self::new()
    }
}

impl Styled for Alert {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}

impl RenderOnce for Alert {
    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
        let theme = use_theme();
        let (bg_color, border_color, accent_color) = self.get_colors(&theme);
        let user_style = self.style;

        let icon_source = self
            .icon
            .unwrap_or_else(|| IconSource::Named(self.variant.default_icon().into()));

        let has_content = self.title.is_some() || self.description.is_some();

        div()
            .flex()
            .w_full()
            .p(px(16.0))
            .rounded(theme.tokens.radius_md)
            .bg(bg_color)
            .border_1()
            .border_color(border_color.opacity(0.3))
            .gap(px(12.0))
            .when(self.show_icon, |this| {
                this.child(
                    div()
                        .flex_shrink_0()
                        .pt(px(2.0))
                        .child(Icon::new(icon_source).size(px(20.0)).color(accent_color)),
                )
            })
            .when(has_content, |this| {
                this.child(
                    div()
                        .flex()
                        .flex_col()
                        .flex_1()
                        .gap(px(4.0))
                        .when_some(self.title.clone(), |this, title| {
                            this.child(
                                div()
                                    .text_sm()
                                    .font_weight(FontWeight::SEMIBOLD)
                                    .text_color(theme.tokens.foreground)
                                    .child(title),
                            )
                        })
                        .when_some(self.description.clone(), |this, desc| {
                            this.child(
                                div()
                                    .text_sm()
                                    .text_color(theme.tokens.muted_foreground)
                                    .child(desc),
                            )
                        })
                        .when_some(self.action.clone(), |this, (label, handler)| {
                            this.child(
                                div().mt(px(8.0)).child(
                                    div()
                                        .id("alert-action")
                                        .text_sm()
                                        .font_weight(FontWeight::MEDIUM)
                                        .text_color(accent_color)
                                        .cursor(CursorStyle::PointingHand)
                                        .hover(|style| style.opacity(0.8))
                                        .on_mouse_down(MouseButton::Left, move |_, window, cx| {
                                            (handler)(window, cx);
                                        })
                                        .child(label),
                                ),
                            )
                        }),
                )
            })
            .when(self.dismissible, |this| {
                let dismiss_handler = self.on_dismiss.clone();
                this.child(
                    div()
                        .flex_shrink_0()
                        .id("alert-dismiss")
                        .cursor(CursorStyle::PointingHand)
                        .rounded(theme.tokens.radius_sm)
                        .p(px(4.0))
                        .hover(|style| style.bg(theme.tokens.muted.opacity(0.5)))
                        .on_mouse_down(MouseButton::Left, move |_, window, cx| {
                            if let Some(ref handler) = dismiss_handler {
                                (handler)(window, cx);
                            }
                        })
                        .child(
                            Icon::new("x")
                                .size(px(16.0))
                                .color(theme.tokens.muted_foreground),
                        ),
                )
            })
            .map(|this| {
                let mut div = this;
                div.style().refine(&user_style);
                div
            })
    }
}

pub fn alert() -> Alert {
    Alert::new()
}