tinkr 0.0.43

Tinkr is a web framework for quickly building full-stack web applications with Leptos.
Documentation
use leptos::ev;
use leptos::prelude::*;

#[component]
pub fn Label(
    #[prop(into)] for_id: String,
    children: Children,
    #[prop(optional)] required: bool,
    #[prop(optional)] class: Option<&'static str>,
) -> impl IntoView {
    let default_class = "block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1";
    let final_class = class.unwrap_or(default_class);

    view! {
        <label for=for_id class=final_class>
            {children()}
            {if required {
                view! { <span class="text-red-500">" *"</span> }.into_any()
            } else {
                view! {}.into_any()
            }}
        </label>
    }
}

#[component]
pub fn Input(
    #[prop(into)] id: String,
    #[prop(into)] r#type: String,
    value: Signal<String>,
    on_input: impl Fn(String) + 'static,
    #[prop(optional)] placeholder: Option<&'static str>,
    #[prop(optional)] required: bool,
    #[prop(optional)] disabled: bool,
    #[prop(optional)] class: Option<&'static str>,
) -> impl IntoView {
    let default_class = "w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed";
    let final_class = class.unwrap_or(default_class);

    view! {
        <input
            type=r#type
            id=id.clone()
            name=id
            class=final_class
            prop:value=move || value.get()
            on:input=move |e| on_input(event_target_value(&e))
            placeholder=placeholder.unwrap_or("")
            required=required
            disabled=disabled
        />
    }
}

#[component]
pub fn Textarea(
    #[prop(into)] id: String,
    value: Signal<String>,
    on_input: impl Fn(String) + 'static,
    #[prop(optional)] placeholder: Option<&'static str>,
    #[prop(optional)] rows: Option<i32>,
    #[prop(optional)] required: bool,
    #[prop(optional)] disabled: bool,
    #[prop(optional)] class: Option<&'static str>,
) -> impl IntoView {
    let default_class = "w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed";
    let final_class = class.unwrap_or(default_class);

    view! {
        <textarea
            id=id.clone()
            name=id
            class=final_class
            rows=rows.unwrap_or(3)
            prop:value=move || value.get()
            on:input=move |e| on_input(event_target_value(&e))
            placeholder=placeholder.unwrap_or("")
            required=required
            disabled=disabled
        />
    }
}

#[component]
pub fn FormButton(
    children: Children,
    #[prop(optional)] r#type: Option<&'static str>,
    #[prop(optional)] variant: Option<&'static str>,
    #[prop(optional)] disabled: Option<Signal<bool>>,
    #[prop(optional)] on_click: Option<Box<dyn Fn(ev::MouseEvent)>>,
    #[prop(optional)] class: Option<&'static str>,
) -> impl IntoView {
    let button_type = r#type.unwrap_or("button");
    let variant = variant.unwrap_or("primary");

    let base_class = "px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";

    let variant_class = match variant {
        "primary" => "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 dark:focus:ring-offset-neutral-800",
        "secondary" => "bg-neutral-200 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-300 dark:hover:bg-neutral-600 focus:ring-neutral-500 dark:focus:ring-offset-neutral-800",
        _ => "bg-neutral-200 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-300 dark:hover:bg-neutral-600 focus:ring-neutral-500 dark:focus:ring-offset-neutral-800",
    };

    let final_class = if let Some(custom_class) = class {
        format!("{} {} {}", base_class, variant_class, custom_class)
    } else {
        format!("{} {}", base_class, variant_class)
    };

    view! {
        <button
            type=button_type
            class=final_class
            disabled=move || disabled.map(|d| d.get()).unwrap_or(false)
            on:click=move |e| {
                if let Some(handler) = &on_click {
                    handler(e);
                }
            }
        >
            {children()}
        </button>
    }
}

#[component]
pub fn FormGroup(
    children: Children,
    #[prop(optional)] class: Option<&'static str>,
) -> impl IntoView {
    let default_class = "space-y-4";
    let final_class = class.unwrap_or(default_class);

    view! { <div class=final_class>{children()}</div> }
}

#[component]
pub fn FormField(
    children: Children,
    #[prop(optional)] class: Option<&'static str>,
) -> impl IntoView {
    let default_class = "";
    let final_class = class.unwrap_or(default_class);

    view! { <div class=final_class>{children()}</div> }
}

#[component]
pub fn FormActions(
    children: Children,
    #[prop(optional)] class: Option<&'static str>,
) -> impl IntoView {
    let default_class = "flex gap-4 pt-4";
    let final_class = class.unwrap_or(default_class);

    view! { <div class=final_class>{children()}</div> }
}