hikari-components 0.2.2

Core UI components (40+) for the Hikari design system
// hi-components/src/basic/textarea.rs
// Textarea component

use hikari_palette::classes::{ClassesBuilder, InputClass};

use crate::prelude::*;
use crate::styled::StyledComponent;

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

#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum TextareaStatus {
    #[default]
    Default,
    Error,
    Success,
}

#[define_props]
pub struct TextareaProps {
    #[default]
    pub value: String,

    #[default]
    pub oninput: Option<EventHandler<String>>,

    #[default]
    pub placeholder: Option<String>,

    #[default]
    pub disabled: bool,

    #[default]
    pub readonly: bool,

    #[default(3)]
    pub rows: u32,

    #[default]
    pub size: TextareaSize,

    #[default]
    pub maxlength: Option<u32>,

    #[default]
    pub class: String,

    pub status: TextareaStatus,
}

///
///
///
///
#[component]
pub fn Textarea(props: TextareaProps) -> Element {
    let size_class = match props.size {
        TextareaSize::Small => InputClass::InputSm,
        TextareaSize::Medium => InputClass::InputMd,
        TextareaSize::Large => InputClass::InputLg,
    };

    let textarea_classes = ClassesBuilder::new()
        .add_typed(InputClass::Input)
        .add_typed(size_class)
        .add_typed_if(InputClass::InputDisabled, props.disabled)
        .add_typed_if(
            InputClass::InputError,
            matches!(props.status, TextareaStatus::Error),
        )
        .add_typed_if(
            InputClass::InputSuccess,
            matches!(props.status, TextareaStatus::Success),
        )
        .add(&props.class)
        .build();

    rsx! {
        textarea {
            class: textarea_classes,
            disabled: props.disabled,
            readonly: props.readonly,
            placeholder: props.placeholder.clone().unwrap_or_default(),
            value: "{props.value}",
            rows: props.rows,
            maxlength: props.maxlength.unwrap_or(0),
            "aria-invalid": if matches!(props.status, TextareaStatus::Error) { Some("true".to_string()) } else { None },
            "aria-label": props.placeholder.clone(),
            oninput: move |e: InputEvent| {
                if let Some(handler) = props.oninput.as_ref() {
                    handler.call(e.data.clone());
                }
            },
        }
    }
}

pub struct TextareaComponent;

impl StyledComponent for TextareaComponent {
    fn styles() -> &'static str {
        r#"
.hi-input {
  width: 100%;
  padding: 10px 16px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--hi-text-primary);
  background: var(--hi-surface);
  border: 1px solid var(--hi-border);
  border-radius: 6px;
  transition: all 0.2s;
  font-family: inherit;
  resize: vertical;
}

.hi-input:focus {
  outline: none;
  border-color: var(--hi-color-primary);
  box-shadow: 0 0 0 3px rgba(0, 160, 233, 0.1);
}

.hi-input::placeholder {
  color: var(--hi-text-secondary);
}

.hi-input-sm {
  padding: 8px 12px;
  font-size: 13px;
}

.hi-input-md {
  padding: 10px 16px;
  font-size: 14px;
}

.hi-input-lg {
  padding: 12px 20px;
  font-size: 15px;
}

.hi-input-disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background: var(--hi-surface-light);
}

[data-theme="dark"] .hi-input {
  background: var(--hi-background);
  border-color: var(--hi-border);
  color: var(--hi-text-primary);
}

[data-theme="dark"] .hi-input:focus {
  background: var(--hi-background);
  border-color: var(--hi-color-primary);
  box-shadow: 0 0 0 3px rgba(0, 160, 233, 0.1);
}

[data-theme="dark"] .hi-input::placeholder {
  color: var(--hi-text-secondary);
}

[data-theme="dark"] .hi-input-disabled {
  background: var(--hi-surface);
}
"#
    }

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