tinkr 0.0.43

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

use crate::components::Button;

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

#[derive(Clone, Copy)]
pub struct ModalState {
    pub visible: RwSignal<bool>,
}

#[component]
pub fn Modal(
    show: Signal<bool>,
    #[prop(optional)] on_close: Option<Callback<()>>,
    #[prop(optional)] title: Option<String>,
    #[prop(optional)] size: ModalSize,
    children: ChildrenFn,
) -> impl IntoView {
    let modals = use_context::<RwSignal<Vec<(usize, ModalState)>>>()
        .expect("ModalProvider context not found");

    let modal_id = StoredValue::new(None::<usize>);
    let visible = RwSignal::new(false);
    let next_id = StoredValue::new(0_usize);

    Effect::new(move || {
        let is_visible = show.get();

        if is_visible {
            if modal_id.get_value().is_none() {
                let id = next_id.get_value();
                next_id.set_value(id + 1);

                let state = ModalState { visible };
                modals.update(|modals| {
                    modals.push((id, state));
                });
                modal_id.set_value(Some(id));
            }
            visible.set(true);
        } else {
            visible.set(false);
        }
    });

    let size_class = match size {
        ModalSize::Small => "max-w-md",
        ModalSize::Medium => "max-w-lg",
        ModalSize::Large => "max-w-2xl",
        ModalSize::ExtraLarge => "max-w-4xl",
        ModalSize::FullWidth => "max-w-7xl",
    };

    let close_modal = move |_| {
        if let Some(callback) = on_close {
            callback.run(());
        }
        visible.set(false);
    };

    view! {
        <Show when=move || visible.get() fallback=|| ()>
            <div
                class="fixed inset-0 z-50 overflow-y-auto"
                aria-labelledby="modal-title"
                role="dialog"
                aria-modal="true"
            >
                <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
                    <div
                        class="fixed inset-0 dark:bg-neutral-800/50 bg-neutral-100/50 backdrop-blur-lg transition-opacity touch-none"
                        aria-hidden="true"
                        on:click=close_modal
                    ></div>

                    <div class=tw_merge!(
                        "inline-block align-top bg-white dark:bg-neutral-800 rounded-lg container text-left overflow-hidden shadow-xl transform sm:my-8 sm:align-middle {} w-full shadow-xl", size_class
                    )>
                        <div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4 relative dark:bg-neutral-900 bg-white rounded-md">
                            <div class="flex justify-between items-center mb-4">
                                {title
                                    .as_ref()
                                    .map(|t| {
                                        view! {
                                            <h3
                                                class="text-lg leading-6 font-medium text-neutral-900 dark:text-neutral-100 mb-4"
                                                id="modal-title"
                                            >
                                                {t.clone()}
                                            </h3>
                                        }
                                    })} <Button on:click=close_modal>
                                    <Icon icon=phosphor_leptos::X />
                                </Button>
                            </div>
                            {children()}
                        </div>
                    </div>
                </div>
            </div>
        </Show>
    }
}

#[component]
pub fn ModalProvider(children: Children) -> impl IntoView {
    let modals = RwSignal::new(vec![] as Vec<(usize, ModalState)>);
    provide_context(modals);

    view! { {children()} }
}