blinc_cn 0.6.0

Blinc Component Library - shadcn-style themed components built on blinc_layout primitives
//! Label component - styled text label for form elements
//!
//! A themed label component following shadcn/ui patterns.
//!
//! # Example
//!
//! ```ignore
//! use blinc_cn::prelude::*;
//!
//! // Basic label
//! cn::label("Email")
//!
//! // Required field label
//! cn::label("Password").required()
//!
//! // Disabled label
//! cn::label("Username").disabled()
//! ```

use blinc_layout::prelude::*;
use blinc_theme::{ColorToken, ThemeState, TypographyTokens};

/// Label size variants
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LabelSize {
    /// Small label (12px)
    Small,
    /// Medium/default label (14px)
    #[default]
    Medium,
    /// Large label (16px)
    Large,
}

impl LabelSize {
    fn font_size(&self, typography: &TypographyTokens) -> f32 {
        match self {
            LabelSize::Small => typography.text_xs,
            LabelSize::Medium => typography.text_sm,
            LabelSize::Large => typography.text_base,
        }
    }
}

/// Styled Label component
pub struct Label {
    /// The fully-built inner element
    inner: Div,
}

impl Label {
    /// Create a new label with the given text
    pub fn new(text: impl blinc_layout::binding::IntoReactive<String>) -> Self {
        Self::with_config(LabelConfig::new(text.into_reactive()))
    }

    /// Create from a full configuration
    fn with_config(config: LabelConfig) -> Self {
        let theme = ThemeState::get();
        let typography = theme.typography();
        let font_size = config.size.font_size(&typography);

        // The text's colour, the asterisk's and the dimmed look while
        // `disabled` holds are `.cn-label` rules.
        let inner = if config.required {
            // No `w_fit()` / `h_fit()`: both set `align_self: Start` as a
            // side effect, which overrides the parent's `align-items`.
            // In a row that pinned the label to the top, so a label beside
            // a padded chip sat a couple of pixels high. Width is the main
            // axis in a row and already hugs; `flex_shrink_0` keeps it from
            // being squeezed.
            div()
                .flex_shrink_0()
                .flex_row()
                .gap(2.0)
                .child_box(crate::reactive_props::reactive_text(
                    &config.text,
                    move |t| t.class("cn-label-text").size(font_size).medium().no_wrap(),
                ))
                .child(
                    text("*")
                        .class("cn-label-required")
                        .size(font_size)
                        .medium(),
                )
        } else {
            div()
                .flex_shrink_0()
                .child_box(crate::reactive_props::reactive_text(
                    &config.text,
                    move |t| t.class("cn-label-text").size(font_size).medium().no_wrap(),
                ))
        }
        .class("cn-label")
        .class_when("cn-label--disabled", config.disabled.clone())
        .attr_when("disabled", "", config.disabled);

        Self { inner }
    }

    /// Add a CSS class for selector matching
    pub fn class(mut self, name: impl AsRef<str>) -> Self {
        self.inner = self.inner.class(name);
        self
    }

    /// Set the element ID for CSS selector matching
    pub fn id(mut self, id: &str) -> Self {
        self.inner = self.inner.id(id);
        self
    }
}

/// Internal configuration for building a Label
pub(crate) struct LabelConfig {
    text: blinc_layout::binding::Reactive<String>,
    size: LabelSize,
    required: bool,
    disabled: blinc_layout::binding::Reactive<bool>,
}

impl Clone for LabelConfig {
    fn clone(&self) -> Self {
        Self {
            text: self.text.clone(),
            size: self.size,
            required: self.required,
            disabled: self.disabled.clone(),
        }
    }
}

impl LabelConfig {
    fn new(text: blinc_layout::binding::Reactive<String>) -> Self {
        Self {
            text,
            size: LabelSize::default(),
            required: false,
            disabled: blinc_layout::binding::Reactive::Const(false),
        }
    }
}

/// Builder for creating Label components with fluent API
pub struct LabelBuilder {
    /// Internal configuration (pub for testing)
    pub(crate) config: LabelConfig,
    /// Cached built Label - built lazily on first access
    built: std::cell::OnceCell<Label>,
}

impl LabelBuilder {
    /// Create a new label builder with the given text
    pub fn new(text: impl blinc_layout::binding::IntoReactive<String>) -> Self {
        Self {
            config: LabelConfig::new(text.into_reactive()),
            built: std::cell::OnceCell::new(),
        }
    }

    /// Get or build the inner Label
    fn get_or_build(&self) -> &Label {
        ::blinc_layout::build_once::build_once(&self.built, || {
            Label::with_config(self.config.clone())
        })
    }

    /// Set the label size
    pub fn size(mut self, size: LabelSize) -> Self {
        self.config.size = size;
        self
    }

    /// Mark the label as required (shows asterisk)
    pub fn required(mut self) -> Self {
        self.config.required = true;
        self
    }

    /// Dim the label while `disabled` holds: a bool, Signal or Computed.
    pub fn disabled(mut self, disabled: impl blinc_layout::binding::IntoReactive<bool>) -> Self {
        self.config.disabled = disabled.into_reactive();
        self
    }

    /// Build the final Label component
    pub fn build_component(self) -> Label {
        Label::with_config(self.config)
    }
}

impl ElementBuilder for Label {
    fn build(&self, tree: &mut blinc_layout::tree::LayoutTree) -> blinc_layout::tree::LayoutNodeId {
        self.inner.build(tree)
    }

    fn render_props(&self) -> blinc_layout::element::RenderProps {
        self.inner.render_props()
    }

    fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
        self.inner.children_builders()
    }

    fn element_type_id(&self) -> blinc_layout::div::ElementTypeId {
        self.inner.element_type_id()
    }

    fn element_classes(&self) -> &[std::sync::Arc<str>] {
        self.inner.element_classes()
    }
}

impl ElementBuilder for LabelBuilder {
    fn build(&self, tree: &mut blinc_layout::tree::LayoutTree) -> blinc_layout::tree::LayoutNodeId {
        self.get_or_build().build(tree)
    }

    fn render_props(&self) -> blinc_layout::element::RenderProps {
        self.get_or_build().render_props()
    }

    fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
        self.get_or_build().children_builders()
    }

    fn element_type_id(&self) -> blinc_layout::div::ElementTypeId {
        self.get_or_build().element_type_id()
    }

    fn element_classes(&self) -> &[std::sync::Arc<str>] {
        self.get_or_build().element_classes()
    }
}

/// Create a styled label component
pub fn label(text: impl blinc_layout::binding::IntoReactive<String>) -> LabelBuilder {
    LabelBuilder::new(text)
}

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

    #[test]
    fn test_label_size_values() {
        let typography = TypographyTokens::default();
        assert_eq!(LabelSize::Small.font_size(&typography), typography.text_xs);
        assert_eq!(LabelSize::Medium.font_size(&typography), typography.text_sm);
        assert_eq!(
            LabelSize::Large.font_size(&typography),
            typography.text_base
        );
    }

    #[test]
    fn test_label_builder() {
        // Use the label() function which returns a LabelBuilder
        let lbl = label("Username").required().size(LabelSize::Large);

        // LabelBuilder has config field with the settings
        assert!(lbl.config.required);
        assert_eq!(lbl.config.size, LabelSize::Large);
    }
}