blinc_cn 0.6.0

Blinc Component Library - shadcn-style themed components built on blinc_layout primitives
//! Kbd component - keyboard shortcut display
//!
//! A styled inline element for displaying keyboard shortcuts.
//!
//! # Example
//!
//! ```ignore
//! use blinc_cn::prelude::*;
//!
//! fn build_ui() -> impl ElementBuilder {
//!     div().flex_row().gap(4.0).items_center().children([
//!         text("Press"),
//!         cn::kbd("⌘"),
//!         text("+"),
//!         cn::kbd("K"),
//!         text("to search"),
//!     ])
//! }
//! ```

use std::cell::OnceCell;

use blinc_layout::div::{FontFamily, GenericFont};
use blinc_layout::prelude::*;
use blinc_theme::{ColorToken, RadiusToken, ThemeState};

/// Size variants for the Kbd component
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum KbdSize {
    /// Small size (text: 10px, padding: 2px 4px)
    Small,
    /// Medium size (text: 12px, padding: 2px 6px) - default
    #[default]
    Medium,
    /// Large size (text: 14px, padding: 4px 8px)
    Large,
}

/// Configuration for the Kbd component
struct KbdConfig {
    text: blinc_layout::binding::Reactive<String>,
    size: KbdSize,
}

// `Reactive<T>` carries type-erased handles and isn't `Clone`, so the
// derive can't apply; every variant's payload is.
impl Clone for KbdConfig {
    fn clone(&self) -> Self {
        Self {
            text: crate::reactive_props::clone_reactive(&self.text),
            size: self.size,
        }
    }
}

impl std::fmt::Debug for KbdConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("KbdConfig")
            .field("text", &crate::reactive_props::current(&self.text))
            .field("size", &self.size)
            .finish()
    }
}

/// Builder for the Kbd component
pub struct KbdBuilder {
    config: KbdConfig,
    built: OnceCell<Kbd>,
}

impl std::fmt::Debug for KbdBuilder {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("KbdBuilder")
            .field("config", &self.config)
            .finish()
    }
}

impl KbdBuilder {
    /// Create a new Kbd builder with the given text.
    ///
    /// Takes a plain string or a bound source. A bound key rebuilds
    /// through `deps()` on change: text content has no property writer,
    /// so a new string needs a new text node.
    pub fn new(text: impl blinc_layout::binding::IntoReactive<String>) -> Self {
        Self {
            config: KbdConfig {
                text: text.into_reactive(),
                size: KbdSize::Medium,
            },
            built: OnceCell::new(),
        }
    }

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

    /// Get or build the component
    fn get_or_build(&self) -> &Kbd {
        ::blinc_layout::build_once::build_once(&self.built, || self.build_component())
    }

    /// Build the Kbd component
    fn build_component(&self) -> Kbd {
        // Sizes are layout; the fill, border, radius, shadow and text colour
        // are the `.cn-kbd` rule.
        let (font_size, px, py) = match self.config.size {
            KbdSize::Small => (10.0, 4.0, 2.0),
            KbdSize::Medium => (12.0, 6.0, 2.0),
            KbdSize::Large => (14.0, 8.0, 4.0),
        };

        let inner = div()
            .class("cn-kbd")
            .items_center()
            .justify_center()
            .w_fit()
            .padding_x_px(px)
            .padding_y_px(py)
            .child_box(crate::reactive_props::reactive_text(
                &self.config.text,
                move |t| {
                    t.size(font_size)
                        .font_family(FontFamily::generic(GenericFont::Monospace))
                        .weight(FontWeight::SemiBold)
                        .medium()
                        .no_wrap()
                },
            ));

        Kbd { inner }
    }
}

/// Built Kbd component
pub struct Kbd {
    inner: Div,
}

impl Kbd {
    /// 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
    }
}

impl std::fmt::Debug for Kbd {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Kbd").finish()
    }
}

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

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

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

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

    fn layout_style(&self) -> Option<&taffy::Style> {
        self.get_or_build().inner.layout_style()
    }

    fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
        ElementBuilder::event_handlers(&self.get_or_build().inner)
    }

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

impl ElementBuilder for Kbd {
    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 layout_style(&self) -> Option<&taffy::Style> {
        self.inner.layout_style()
    }

    fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
        ElementBuilder::event_handlers(&self.inner)
    }

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

/// Create a Kbd component to display a keyboard shortcut
///
/// # Example
///
/// ```ignore
/// // Simple key
/// cn::kbd("K")
///
/// // Modifier key
/// cn::kbd("⌘")
///
/// // With size
/// cn::kbd("Enter").size(KbdSize::Large)
/// ```
pub fn kbd(text: impl blinc_layout::binding::IntoReactive<String>) -> KbdBuilder {
    KbdBuilder::new(text)
}

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

    #[test]
    fn test_kbd_sizes() {
        // Just verify the enum values exist
        assert_eq!(KbdSize::default(), KbdSize::Medium);
    }
}