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 inventory;
|
||||||
pub mod numput;
|
pub mod numput;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
|
pub mod modal;
|
||||||
|
|
||||||
/// Character page (route `/`). Stub: renders a placeholder until
|
/// Character page (route `/`). Stub: renders a placeholder until
|
||||||
/// ROADMAP phase 2.
|
/// 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 'sidebar';
|
||||||
@use 'nav';
|
@use 'nav';
|
||||||
@use 'inventory';
|
@use 'inventory';
|
||||||
|
@use 'modal';
|
||||||
@use 'vars';
|
@use 'vars';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user