diff --git a/harbor-ui/assets/icons/cancel.svg b/harbor-ui/assets/icons/cancel.svg
new file mode 100644
index 0000000..77843b6
--- /dev/null
+++ b/harbor-ui/assets/icons/cancel.svg
@@ -0,0 +1,4 @@
+
diff --git a/harbor-ui/assets/icons/small_check.svg b/harbor-ui/assets/icons/small_check.svg
new file mode 100644
index 0000000..f1f635e
--- /dev/null
+++ b/harbor-ui/assets/icons/small_check.svg
@@ -0,0 +1,3 @@
+
diff --git a/harbor-ui/src/components/button.rs b/harbor-ui/src/components/button.rs
index 6fb135f..d1de274 100644
--- a/harbor-ui/src/components/button.rs
+++ b/harbor-ui/src/components/button.rs
@@ -60,6 +60,58 @@ pub fn h_button(text_str: &str, icon: SvgIcon, loading: bool) -> Button<'_, Mess
.height(Length::Fixed(64.))
}
+pub fn h_small_button(text_str: &str, icon: SvgIcon, loading: bool) -> Button<'_, Message, Theme> {
+ let spinner: Element<'static, Message, Theme> = the_spinner();
+ let svg = map_icon(icon, 16., 16.);
+ let content = if loading {
+ row![spinner].align_y(iced::Alignment::Center)
+ } else if text_str.is_empty() {
+ row![svg].align_y(iced::Alignment::Center)
+ } else {
+ row![svg, text(text_str).size(16.)]
+ .align_y(iced::Alignment::Center)
+ .spacing(8)
+ };
+
+ Button::new(center(content))
+ .style(move |theme, status| {
+ let gray = lighten(theme.palette().background, 0.5);
+
+ let border_color = if loading || matches!(status, Status::Disabled) {
+ gray
+ } else {
+ Color::WHITE
+ };
+
+ let border = Border {
+ color: border_color,
+ width: 1.5,
+ radius: (8.).into(),
+ };
+
+ let background = if loading {
+ theme.palette().background
+ } else {
+ match status {
+ Status::Hovered => lighten(theme.palette().background, 0.1),
+ Status::Pressed => darken(Color::BLACK, 0.1),
+ _ => theme.palette().background,
+ }
+ };
+
+ let text_color = if loading { gray } else { Color::WHITE };
+
+ button::Style {
+ background: Some(background.into()),
+ text_color,
+ border,
+ shadow: Shadow::default(),
+ }
+ })
+ .width(Length::Fill)
+ .height(Length::Fixed(40.))
+}
+
pub fn sidebar_button(
text_str: &str,
icon: SvgIcon,
diff --git a/harbor-ui/src/components/confirm_modal.rs b/harbor-ui/src/components/confirm_modal.rs
new file mode 100644
index 0000000..95c2f8e
--- /dev/null
+++ b/harbor-ui/src/components/confirm_modal.rs
@@ -0,0 +1,82 @@
+use iced::widget::{button, center, column, container, row, stack, text};
+use iced::{Color, Element, Length, Shadow, Theme, Vector};
+
+use crate::Message;
+
+use super::{h_small_button, light_container_style, SvgIcon};
+
+#[derive(Debug, Clone)]
+pub struct ConfirmModalState {
+ pub title: String,
+ pub description: String,
+ pub confirm_action: Box,
+ pub cancel_action: Box,
+ pub confirm_button_text: String,
+}
+
+impl Default for ConfirmModalState {
+ fn default() -> Self {
+ Self {
+ title: "Confirm Action".to_string(),
+ description: "Are you sure you want to proceed?".to_string(),
+ confirm_action: Box::new(Message::SetConfirmModal(None)),
+ cancel_action: Box::new(Message::SetConfirmModal(None)),
+ confirm_button_text: "Confirm".to_string(),
+ }
+ }
+}
+
+pub fn confirm_modal<'a>(
+ content: Element<'a, Message>,
+ state: &'a ConfirmModalState,
+) -> Element<'a, Message> {
+ let modal_content = container(
+ column![
+ text(&state.title).size(24),
+ text(&state.description),
+ row![
+ h_small_button("Cancel", SvgIcon::SmallClose, false)
+ .on_press((*state.cancel_action).clone()),
+ h_small_button(&state.confirm_button_text, SvgIcon::SmallCheck, false)
+ .on_press((*state.confirm_action).clone()),
+ ]
+ .spacing(10)
+ ]
+ .spacing(20),
+ )
+ .width(400)
+ .padding(24)
+ .style(|theme: &Theme| container::Style {
+ background: Some(theme.palette().background.into()),
+ text_color: Some(theme.palette().text),
+ border: light_container_style(theme).border,
+ shadow: Shadow {
+ color: Color::from_rgba8(0, 0, 0, 0.5),
+ offset: Vector::new(4.0, 4.0),
+ blur_radius: 8.0,
+ },
+ });
+
+ stack![
+ content,
+ // This layer blocks all pointer events from reaching the content below
+ // TODO: can we do something cleaner?
+ button(container(text("")).width(Length::Fill).height(Length::Fill))
+ .on_press((*state.cancel_action).clone())
+ .style(|_theme: &Theme, _state| button::Style::default()),
+ container(center(modal_content))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .style(|_theme: &Theme| container::Style {
+ background: Some(
+ Color {
+ a: 0.8,
+ ..Color::BLACK
+ }
+ .into()
+ ),
+ ..container::Style::default()
+ })
+ ]
+ .into()
+}
diff --git a/harbor-ui/src/components/federation_item.rs b/harbor-ui/src/components/federation_item.rs
index 8a0df45..5f786b1 100644
--- a/harbor-ui/src/components/federation_item.rs
+++ b/harbor-ui/src/components/federation_item.rs
@@ -1,11 +1,14 @@
use crate::Message;
use harbor_client::db_models::FederationItem;
use iced::{
- widget::{column, container, row, text},
- Alignment, Element,
+ widget::{column, container, horizontal_space, row, text},
+ Alignment, Element, Length,
};
-use super::{h_balance_display, h_button, light_container_style, map_icon, subtitle, SvgIcon};
+use super::{
+ h_balance_display, h_button, h_small_button, light_container_style, map_icon, subtitle,
+ ConfirmModalState, SvgIcon,
+};
pub fn h_federation_item(item: &FederationItem, invite_code: Option) -> Element {
let FederationItem {
@@ -43,9 +46,19 @@ pub fn h_federation_item(item: &FederationItem, invite_code: Option) ->
None => {
column = column.push(h_balance_display(*balance));
- let remove_button = h_button("Remove Mint", SvgIcon::Trash, false)
- .on_press(Message::RemoveFederation(*id));
- column = column.push(remove_button);
+ let remove_button = h_small_button("", SvgIcon::Trash, false).on_press(
+ Message::SetConfirmModal(Some(ConfirmModalState {
+ title: "Are you sure?".to_string(),
+ description: format!("This will remove {} from your list of mints.", name),
+ confirm_action: Box::new(Message::RemoveFederation(*id)),
+ cancel_action: Box::new(Message::SetConfirmModal(None)),
+ confirm_button_text: "Remove Mint".to_string(),
+ })),
+ );
+ column = column.push(row![
+ horizontal_space().width(Length::Fill),
+ remove_button.width(48)
+ ]);
}
}
diff --git a/harbor-ui/src/components/icon.rs b/harbor-ui/src/components/icon.rs
index 12454f3..42db983 100644
--- a/harbor-ui/src/components/icon.rs
+++ b/harbor-ui/src/components/icon.rs
@@ -20,6 +20,7 @@ pub enum SvgIcon {
Qr,
Restart,
SmallClose,
+ SmallCheck,
Bolt,
Chain,
Eye,
@@ -56,6 +57,7 @@ pub fn map_icon<'a>(icon: SvgIcon, width: f32, height: f32) -> Svg<'a, Theme> {
SvgIcon::Qr => icon_handle!("qr.svg"),
SvgIcon::Restart => icon_handle!("restart.svg"),
SvgIcon::SmallClose => icon_handle!("small_close.svg"),
+ SvgIcon::SmallCheck => icon_handle!("small_check.svg"),
SvgIcon::Bolt => icon_handle!("bolt.svg"),
SvgIcon::Chain => icon_handle!("chain.svg"),
SvgIcon::Eye => icon_handle!("eye.svg"),
diff --git a/harbor-ui/src/components/mod.rs b/harbor-ui/src/components/mod.rs
index ef8db62..82e4f2b 100644
--- a/harbor-ui/src/components/mod.rs
+++ b/harbor-ui/src/components/mod.rs
@@ -62,3 +62,6 @@ pub use balance_display::*;
pub mod absolute_overlay;
pub use absolute_overlay::*;
+
+mod confirm_modal;
+pub use confirm_modal::*;
diff --git a/harbor-ui/src/main.rs b/harbor-ui/src/main.rs
index 3320b0d..8971fd4 100644
--- a/harbor-ui/src/main.rs
+++ b/harbor-ui/src/main.rs
@@ -120,6 +120,7 @@ pub enum Message {
UIHandlerLoaded(Arc),
// Local state changes
Navigate(Route),
+ SetConfirmModal(Option),
ReceiveAmountChanged(String),
ReceiveStateReset,
SendDestInputChanged(String),
@@ -141,6 +142,8 @@ pub enum Message {
TransferAmountInputChanged(String),
UrlClicked(String),
SelectTransaction(Option),
+ // Batch multiple messages together
+ Batch(Vec),
// Async commands we fire from the UI to core
Noop,
Send(String),
@@ -177,6 +180,8 @@ pub struct HarborWallet {
selected_transaction: Option,
federation_list: Vec,
active_federation_id: Option,
+ // Modal
+ confirm_modal: Option,
// Welcome screen
init_status: WelcomeStatus,
seed_input_str: String,
@@ -317,6 +322,9 @@ impl HarborWallet {
println!("Core loaded");
Task::none()
}
+ Message::Batch(messages) => {
+ Task::batch(messages.into_iter().map(|msg| self.update(msg)))
+ }
// Internal app state stuff like navigation and text inputs
Message::Navigate(route) => {
match self.active_route {
@@ -729,6 +737,10 @@ impl HarborWallet {
self.selected_transaction = transaction;
Task::none()
}
+ Message::SetConfirmModal(modal_state) => {
+ self.confirm_modal = modal_state;
+ Task::none()
+ }
// Handle any messages we get from core
Message::CoreMessage(msg) => match msg.msg {
CoreUIMsg::Sending => {
@@ -936,6 +948,8 @@ impl HarborWallet {
self.clear_add_federation_state();
// Route to the mints list
self.active_route = Route::Mints(routes::MintSubroute::List);
+ // We probably got here because of a modal so we should close the modal
+ self.confirm_modal = None;
Task::perform(async {}, |_| {
Message::AddToast(Toast {
title: "Mint removed".to_string(),
@@ -1039,7 +1053,13 @@ impl HarborWallet {
Route::Welcome => crate::routes::welcome(self),
};
- ToastManager::new(active_route, &self.toasts, Message::CloseToast).into()
+ let content = if let Some(modal_state) = &self.confirm_modal {
+ crate::components::confirm_modal(active_route, modal_state)
+ } else {
+ active_route
+ };
+
+ ToastManager::new(content, &self.toasts, Message::CloseToast).into()
}
fn theme(&self) -> iced::Theme {
diff --git a/harbor-ui/src/routes/settings.rs b/harbor-ui/src/routes/settings.rs
index dea9389..1078123 100644
--- a/harbor-ui/src/routes/settings.rs
+++ b/harbor-ui/src/routes/settings.rs
@@ -26,6 +26,26 @@ pub fn settings(harbor: &HarborWallet) -> Element {
status: ToastStatus::Bad,
}));
+ let test_confirm_modal_button = h_button("Test Confirm Modal", SvgIcon::Shield, false)
+ .on_press(Message::SetConfirmModal(Some(
+ crate::components::ConfirmModalState {
+ title: "Test Modal".to_string(),
+ description:
+ "This is a test of the confirm modal. Are you sure you want to proceed?"
+ .to_string(),
+ confirm_action: Box::new(Message::Batch(vec![
+ Message::AddToast(Toast {
+ title: "You confirmed!".to_string(),
+ body: None,
+ status: ToastStatus::Good,
+ }),
+ Message::SetConfirmModal(None),
+ ])),
+ cancel_action: Box::new(Message::SetConfirmModal(None)),
+ confirm_button_text: "Confirm".to_string(),
+ },
+ )));
+
let column = match (harbor.settings_show_seed_words, &harbor.seed_words) {
(true, Some(s)) => {
let button = h_button("Hide Seed Words", SvgIcon::EyeClosed, false)
@@ -47,7 +67,8 @@ pub fn settings(harbor: &HarborWallet) -> Element {
button,
onchain_receive_checkbox,
add_good_toast_button,
- add_error_toast_button
+ add_error_toast_button,
+ test_confirm_modal_button
]
}
};