hikari-components 0.3.12

Core UI components (buttons, inputs, tables, modals) for the Hikari design system
// hi-components/src/feedback/drawer.rs
// Drawer component with Arknights + FUI styling

use hikari_palette::classes::{ClassesBuilder, DrawerClass, UtilityClass};

use crate::{
    prelude::*,
    style_builder::{CssProperty, StyleStringBuilder},
    styled::StyledComponent,
};

#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum DrawerPlacement {
    #[default]
    Right,
    Left,
    Top,
    Bottom,
}

#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum DrawerSize {
    #[default]
    Medium,
    Small,
    Large,
}

#[define_props]
pub struct DrawerProps {
    pub open: bool,

    pub on_close: Option<EventHandler<MouseEvent>>,

    pub placement: DrawerPlacement,

    pub size: DrawerSize,

    pub mask_closable: bool,

    pub title: Option<String>,

    pub footer: Option<Element>,

    pub class: String,

    pub children: Element,
}

///
///
///
///
///
#[component]
pub fn Drawer(props: DrawerProps) -> Element {
    let on_close = props.on_close;
    let mask_closable = props.mask_closable;

    let on_close_for_mask = on_close.clone();
    let handle_mask_click = move |e: MouseEvent| {
        if mask_closable && let Some(handler) = on_close_for_mask.as_ref() {
            handler.call(e);
        }
    };

    let (placement_class, _size_width, _size_height) = match (props.placement, props.size) {
        (DrawerPlacement::Right, DrawerSize::Small) => (DrawerClass::Right, "300px", "100%"),
        (DrawerPlacement::Right, DrawerSize::Medium) => (DrawerClass::Right, "500px", "100%"),
        (DrawerPlacement::Right, DrawerSize::Large) => (DrawerClass::Right, "700px", "100%"),
        (DrawerPlacement::Left, DrawerSize::Small) => (DrawerClass::Left, "300px", "100%"),
        (DrawerPlacement::Left, DrawerSize::Medium) => (DrawerClass::Left, "500px", "100%"),
        (DrawerPlacement::Left, DrawerSize::Large) => (DrawerClass::Left, "700px", "100%"),
        (DrawerPlacement::Top, DrawerSize::Small) => (DrawerClass::Top, "100%", "300px"),
        (DrawerPlacement::Top, DrawerSize::Medium) => (DrawerClass::Top, "100%", "500px"),
        (DrawerPlacement::Top, DrawerSize::Large) => (DrawerClass::Top, "100%", "700px"),
        (DrawerPlacement::Bottom, DrawerSize::Small) => (DrawerClass::Bottom, "100%", "300px"),
        (DrawerPlacement::Bottom, DrawerSize::Medium) => (DrawerClass::Bottom, "100%", "500px"),
        (DrawerPlacement::Bottom, DrawerSize::Large) => (DrawerClass::Bottom, "100%", "700px"),
    };

    let drawer_style = use_memo(move || {
        if props.open {
            StyleStringBuilder::new()
                .add(CssProperty::Display, "block")
                .add(CssProperty::Opacity, "1")
                .build_clean()
        } else {
            StyleStringBuilder::new()
                .add(CssProperty::Display, "none")
                .add(CssProperty::Opacity, "0")
                .build_clean()
        }
    });

    let mask_style = use_memo(move || {
        if props.open {
            StyleStringBuilder::new()
                .add(CssProperty::Opacity, "1")
                .build_clean()
        } else {
            StyleStringBuilder::new()
                .add(CssProperty::Opacity, "0")
                .build_clean()
        }
    });

    let drawer_classes = ClassesBuilder::new()
        .add(DrawerClass::Drawer)
        .add(placement_class)
        .add_raw(&props.class)
        .build();

    rsx! {
        if props.open {
            // Mask overlay
            div {
                class: DrawerClass::Mask.as_class(),
                style: mask_style,
                onclick: handle_mask_click,
            }

            // Drawer panel
            div {
                class: drawer_classes,
                style: drawer_style,

                // Header
                if let Some(title) = props.title {
                    div { class: DrawerClass::Header.as_class(),
                        div { class: DrawerClass::Title.as_class(), "{title}" }
                        button {
                            class: DrawerClass::Close.as_class(),
                            onclick: move |e| {
                                if let Some(handler) = on_close.as_ref() {
                                    handler.call(e);
                                }
                            },
                            svg {
                                view_box: "0 0 24 24",
                                fill: "none",
                                stroke: "currentColor",
                                stroke_width: "2",
                                line { x1: "18", y1: "6", x2: "6", y2: "18" }
                                line { x1: "6", y1: "6", x2: "18", y2: "18" }
                            }
                        }
                    }
                }

                // Body
                div { class: DrawerClass::Body.as_class(),
                    { props.children }
                }

                // Footer
                if let Some(footer) = props.footer {
                    div { class: DrawerClass::Footer.as_class(),
                        { footer }
                    }
                }
            }
        }
    }
}

pub struct DrawerComponent;

impl StyledComponent for DrawerComponent {
    fn styles() -> &'static str {
        r#"
.hi-drawer-mask {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(4px);
  animation: hi-drawer-mask-fade-in 0.2s ease-out;
}

@keyframes hi-drawer-mask-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.hi-drawer {
  position: fixed;
  z-index: 1001;
  background: var(--hi-background);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
  display: flex;
  flex-direction: column;
  animation: hi-drawer-slide-in 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}

[data-theme="dark"] .hi-drawer {
  background: var(--hi-surface);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}

.hi-drawer-right {
  top: 0;
  right: 0;
  height: 100vh;
  border-left: 1px solid var(--hi-border);
}

.hi-drawer-left {
  top: 0;
  left: 0;
  height: 100vh;
  border-right: 1px solid var(--hi-border);
}

.hi-drawer-top {
  top: 0;
  left: 0;
  width: 100vw;
  border-bottom: 1px solid var(--hi-border);
}

.hi-drawer-bottom {
  bottom: 0;
  left: 0;
  width: 100vw;
  border-top: 1px solid var(--hi-border);
}

@keyframes hi-drawer-slide-in {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

.hi-drawer-left {
  animation-name: hi-drawer-slide-in-left;
}

@keyframes hi-drawer-slide-in-left {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

.hi-drawer-top {
  animation-name: hi-drawer-slide-in-top;
}

@keyframes hi-drawer-slide-in-top {
  from {
    transform: translateY(-100%);
  }
  to {
    transform: translateY(0);
  }
}

.hi-drawer-bottom {
  animation-name: hi-drawer-slide-in-bottom;
}

@keyframes hi-drawer-slide-in-bottom {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

.hi-drawer-header {
  padding: 16px 24px;
  border-bottom: 1px solid var(--hi-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.hi-drawer-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--hi-text-primary);
}

.hi-drawer-close {
  background: transparent;
  border: none;
  padding: 4px;
  cursor: pointer;
  color: var(--hi-text-secondary);
  border-radius: 4px;
  transition: all 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
}

.hi-drawer-close:hover {
  background: var(--hi-color-hover);
  color: var(--hi-text-primary);
}

.hi-drawer-body {
  flex: 1;
  overflow-y: auto;
  padding: 24px;
  color: var(--hi-text-primary);
}

.hi-drawer-footer {
  padding: 16px 24px;
  border-top: 1px solid var(--hi-border);
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.hi-drawer-body::-webkit-scrollbar {
  width: 6px;
}

.hi-drawer-body::-webkit-scrollbar-track {
  background: transparent;
}

.hi-drawer-body::-webkit-scrollbar-thumb {
  background: var(--hi-border);
  border-radius: 3px;
}

.hi-drawer-body::-webkit-scrollbar-thumb:hover {
  background: var(--hi-color-border);
}
"#
    }

    fn name() -> &'static str {
        "drawer"
    }
}