soma-ui 0.1.0

A Leptos 0.8 component library: 160+ accessible UI components, blocks, charts, and a ChartDB-style schema diagram — with a prebuilt stylesheet.
Documentation
use leptos::prelude::*;
use crate::icons::{Icon, icondata};
use crate::components::shared::{MENU_ITEM_CLASS, FOCUS_RING, CONTROL_MOTION};

// ponytail: click-catcher (fixed inset-0 z-40) handles outside-click close.
// Arrow-key roving focus is the documented upgrade path — not built here.

#[component]
pub fn Select(
    #[prop(into)] value: RwSignal<String>,
    #[prop(optional, into)] placeholder: String,
    children: ChildrenFn,
) -> impl IntoView {
    let open = RwSignal::new(false);
    provide_context(open);
    provide_context(value);

    let placeholder = StoredValue::new(placeholder);
    let children = StoredValue::new(children);

    view! {
        <div class="relative inline-block w-full">
            <SelectTrigger value=value placeholder=placeholder.get_value() />
            {children.with_value(|c| c())}
        </div>
    }
}

#[component]
fn SelectTrigger(value: RwSignal<String>, placeholder: String) -> impl IntoView {
    let open = use_context::<RwSignal<bool>>()
        .expect("SelectTrigger must be inside Select");

    let trigger_class = format!(
        "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 text-sm text-foreground placeholder:text-muted-foreground shadow-elev-sm hover:border-ring/60 {} {}",
        CONTROL_MOTION, FOCUS_RING
    );
    view! {
        <button
            type="button"
            class=trigger_class
            on:click=move |_| open.update(|v| *v = !*v)
        >
            <span class=move || {
                if value.get().is_empty() { "text-muted-foreground" } else { "text-foreground" }
            }>
                {move || {
                    let v = value.get();
                    if v.is_empty() { placeholder.clone() } else { v }
                }}
            </span>
            <Icon icon=Signal::derive(|| icondata::LuChevronDown) width="16" height="16" />
        </button>
    }
}

#[component]
pub fn SelectContent(children: ChildrenFn) -> impl IntoView {
    let open = use_context::<RwSignal<bool>>()
        .expect("SelectContent must be inside Select");
    let close = move |_| open.set(false);

    let children = StoredValue::new(children);

    view! {
        <Show when=move || open.get()>
            <div class="fixed inset-0 z-40" on:click=close />
            <div class="absolute z-50 mt-1 w-full rounded-md border border-border bg-card p-1 shadow-md animate-scale-in">
                {children.with_value(|c| c())}
            </div>
        </Show>
    }
}

#[component]
pub fn SelectItem(#[prop(into)] value: String, children: Children) -> impl IntoView {
    let open = use_context::<RwSignal<bool>>()
        .expect("SelectItem must be inside Select");
    let selected = use_context::<RwSignal<String>>()
        .expect("SelectItem must be inside Select");

    let item_value = StoredValue::new(value);

    let handle_click = move |_| {
        selected.set(item_value.get_value());
        open.set(false);
    };

    view! {
        <div
            class=MENU_ITEM_CLASS
            on:click=handle_click
        >
            {children()}
            <Show when=move || selected.get() == item_value.get_value()>
                <Icon icon=Signal::derive(|| icondata::LuCheck) width="14" height="14" />
            </Show>
        </div>
    }
}