modal: in-app dialog for confirms and import errors
ModalState carries title/message/action as RwSignals; the action is read at click time so a late confirm wins. Overlay click and Cancel close; the confirm button runs the stored action. Edition-2024 note: method calls on non-Copy captures move by value inside nested move closures, so handlers touch state.open/state.action fields directly.
This commit is contained in:
parent
043977cc1f
commit
0c2eda7621
@ -21,6 +21,7 @@ pub mod nav;
|
||||
pub mod inventory;
|
||||
pub mod numput;
|
||||
pub mod item;
|
||||
pub mod modal;
|
||||
|
||||
/// Character page (route `/`). Stub: renders a placeholder until
|
||||
/// ROADMAP phase 2.
|
||||
|
||||
106
src/components/modal.rs
Normal file
106
src/components/modal.rs
Normal file
@ -0,0 +1,106 @@
|
||||
//! In-app modal dialogs: confirmations and error messages.
|
||||
//!
|
||||
//! `ModalState` is created in `App` and provided through Leptos context;
|
||||
//! the header's Save/Load/Clear handlers open dialogs and [`Modal`]
|
||||
//! renders them. The dialog language is Russian.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Shared state of the modal overlay.
|
||||
///
|
||||
/// `open` decides visibility; `title`/`message`/`cancel_label` and
|
||||
/// `confirm_label` (None for error dialogs) drive the text; `action` runs
|
||||
/// when the confirm button is clicked.
|
||||
#[derive(Clone)]
|
||||
pub struct ModalState {
|
||||
open: RwSignal<bool>,
|
||||
title: RwSignal<String>,
|
||||
message: RwSignal<String>,
|
||||
cancel_label: RwSignal<String>,
|
||||
confirm_label: RwSignal<Option<String>>,
|
||||
action: RwSignal<Arc<dyn Fn() + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl ModalState {
|
||||
/// Creates a closed modal with empty content.
|
||||
pub fn new () -> Self {
|
||||
Self {
|
||||
open: RwSignal::new(false),
|
||||
title: RwSignal::new(String::new()),
|
||||
message: RwSignal::new(String::new()),
|
||||
cancel_label: RwSignal::new(String::new()),
|
||||
confirm_label: RwSignal::new(None),
|
||||
action: RwSignal::new(Arc::new(|| {})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a confirmation dialog: Cancel plus the given confirm button.
|
||||
pub fn confirm (
|
||||
&self,
|
||||
title: impl Into<String>,
|
||||
message: impl Into<String>,
|
||||
confirm_label: impl Into<String>,
|
||||
action: impl Fn() + Send + Sync + 'static,
|
||||
) {
|
||||
self.title.set(title.into());
|
||||
self.message.set(message.into());
|
||||
self.cancel_label.set("Отмена".to_string());
|
||||
self.confirm_label.set(Some(confirm_label.into()));
|
||||
self.action.set(Arc::new(action));
|
||||
self.open.set(true);
|
||||
}
|
||||
|
||||
/// Opens an error dialog with a single Close button.
|
||||
pub fn error (&self, title: impl Into<String>, message: impl Into<String>) {
|
||||
self.title.set(title.into());
|
||||
self.message.set(message.into());
|
||||
self.cancel_label.set("Закрыть".to_string());
|
||||
self.confirm_label.set(None);
|
||||
self.open.set(true);
|
||||
}
|
||||
|
||||
/// Closes the dialog.
|
||||
pub fn close (&self) {
|
||||
self.open.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the modal overlay; empty when no dialog is open.
|
||||
///
|
||||
/// Clicking the overlay background closes the dialog; clicks inside the
|
||||
/// dialog do not. The confirm button runs the action captured in
|
||||
/// `ModalState` (read at click time, so a late `confirm` wins).
|
||||
#[component]
|
||||
pub fn Modal (state: ModalState) -> impl IntoView {
|
||||
view! {
|
||||
<Show when=move || state.open.get()>
|
||||
<div class="modal-overlay" on:click=move |_| state.open.set(false)>
|
||||
<div
|
||||
class="modal"
|
||||
on:click=move |ev: leptos::ev::MouseEvent| ev.stop_propagation()
|
||||
>
|
||||
<h3>{move || state.title.get()}</h3>
|
||||
<p>{move || state.message.get()}</p>
|
||||
<div class="modal-actions">
|
||||
<button class="modal-cancel" on:click=move |_| state.open.set(false)>
|
||||
{move || state.cancel_label.get()}
|
||||
</button>
|
||||
<Show when=move || state.confirm_label.get().is_some()>
|
||||
<button
|
||||
class="modal-confirm"
|
||||
on:click=move |_| {
|
||||
(state.action.get())();
|
||||
state.open.set(false);
|
||||
}
|
||||
>
|
||||
{move || state.confirm_label.get().unwrap_or_default()}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
53
style/_modal.scss
Normal file
53
style/_modal.scss
Normal file
@ -0,0 +1,53 @@
|
||||
// Modal dialog: dark overlay, centered card, crimson confirm button.
|
||||
|
||||
@use 'mixins';
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.modal {
|
||||
max-width: 480px;
|
||||
padding: 24px 32px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.8);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 12px;
|
||||
@include mixins.heading;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 20px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
button.modal-cancel {
|
||||
@include mixins.button;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 6px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.modal-confirm {
|
||||
@include mixins.button;
|
||||
padding: 6px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -6,6 +6,7 @@
|
||||
@use 'sidebar';
|
||||
@use 'nav';
|
||||
@use 'inventory';
|
||||
@use 'modal';
|
||||
@use 'vars';
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user