From 0c2eda76211dcebdaa356e89506d9a3b8ed2ba93 Mon Sep 17 00:00:00 2001 From: yaroslav k Date: Sun, 16 Aug 2026 11:28:19 +0300 Subject: [PATCH] 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. --- src/components/mod.rs | 1 + src/components/modal.rs | 106 ++++++++++++++++++++++++++++++++++++++++ style/_modal.scss | 53 ++++++++++++++++++++ style/main.scss | 1 + 4 files changed, 161 insertions(+) create mode 100644 src/components/modal.rs create mode 100644 style/_modal.scss diff --git a/src/components/mod.rs b/src/components/mod.rs index 4968a28..b50702e 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -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. diff --git a/src/components/modal.rs b/src/components/modal.rs new file mode 100644 index 0000000..27b38ae --- /dev/null +++ b/src/components/modal.rs @@ -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, + title: RwSignal, + message: RwSignal, + cancel_label: RwSignal, + confirm_label: RwSignal>, + action: RwSignal>, +} + +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, + message: impl Into, + confirm_label: impl Into, + 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, message: impl Into) { + 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! { + + + + } +} diff --git a/style/_modal.scss b/style/_modal.scss new file mode 100644 index 0000000..a25209a --- /dev/null +++ b/style/_modal.scss @@ -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; +} diff --git a/style/main.scss b/style/main.scss index 757b017..3236259 100644 --- a/style/main.scss +++ b/style/main.scss @@ -6,6 +6,7 @@ @use 'sidebar'; @use 'nav'; @use 'inventory'; +@use 'modal'; @use 'vars';