diff --git a/crates/cdk-ldk-node/src/web/handlers/channels.rs b/crates/cdk-ldk-node/src/web/handlers/channels.rs index dcbe010f..654a29e1 100644 --- a/crates/cdk-ldk-node/src/web/handlers/channels.rs +++ b/crates/cdk-ldk-node/src/web/handlers/channels.rs @@ -15,7 +15,8 @@ use serde::Deserialize; use crate::web::handlers::utils::deserialize_optional_u64; use crate::web::handlers::AppState; use crate::web::templates::{ - error_message, form_card, format_sats_as_btc, info_card, layout, success_message, + error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status, + success_message, }; #[derive(Deserialize)] @@ -43,7 +44,7 @@ pub async fn channels_page(State(_state): State) -> Result) -> Result, StatusCode> { +pub async fn open_channel_page(State(state): State) -> Result, StatusCode> { let content = form_card( "Open New Channel", html! { @@ -68,14 +69,18 @@ pub async fn open_channel_page(State(_state): State) -> Result) -> Result, St } }; - Ok(Html(layout("Dashboard", content).into_string())) + let is_running = is_node_running(&state.node.inner); + Ok(Html( + layout_with_status("Dashboard", content, is_running).into_string(), + )) } diff --git a/crates/cdk-ldk-node/src/web/handlers/invoices.rs b/crates/cdk-ldk-node/src/web/handlers/invoices.rs index 7d181d54..7691e383 100644 --- a/crates/cdk-ldk-node/src/web/handlers/invoices.rs +++ b/crates/cdk-ldk-node/src/web/handlers/invoices.rs @@ -10,7 +10,8 @@ use serde::Deserialize; use crate::web::handlers::utils::{deserialize_optional_f64, deserialize_optional_u32}; use crate::web::handlers::AppState; use crate::web::templates::{ - error_message, form_card, format_sats_as_btc, info_card, layout, success_message, + error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status, + success_message, }; #[derive(Deserialize)] @@ -30,7 +31,7 @@ pub struct CreateBolt12Form { expiry_seconds: Option, } -pub async fn invoices_page(State(_state): State) -> Result, StatusCode> { +pub async fn invoices_page(State(state): State) -> Result, StatusCode> { let content = html! { h2 style="text-align: center; margin-bottom: 3rem;" { "Invoices" } div class="grid" { @@ -78,7 +79,10 @@ pub async fn invoices_page(State(_state): State) -> Result) -> Result, StatusCode> { let balances = state.node.inner.list_balances(); @@ -216,5 +216,8 @@ pub async fn balance_page(State(state): State) -> Result, } }; - Ok(Html(layout("Lightning", content).into_string())) + let is_running = is_node_running(&state.node.inner); + Ok(Html( + layout_with_status("Lightning", content, is_running).into_string(), + )) } diff --git a/crates/cdk-ldk-node/src/web/handlers/onchain.rs b/crates/cdk-ldk-node/src/web/handlers/onchain.rs index 1658d0e0..0a296faf 100644 --- a/crates/cdk-ldk-node/src/web/handlers/onchain.rs +++ b/crates/cdk-ldk-node/src/web/handlers/onchain.rs @@ -13,7 +13,8 @@ use serde::{Deserialize, Serialize}; use crate::web::handlers::utils::deserialize_optional_u64; use crate::web::handlers::AppState; use crate::web::templates::{ - error_message, form_card, format_sats_as_btc, info_card, layout, success_message, + error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status, + success_message, }; #[derive(Deserialize, Serialize)] @@ -32,54 +33,8 @@ pub struct ConfirmOnchainForm { confirmed: Option, } -pub async fn get_new_address(State(state): State) -> Result, StatusCode> { - let address_result = state.node.inner.onchain_payment().new_address(); - - let content = match address_result { - Ok(address) => { - html! { - (success_message(&format!("New address generated: {address}"))) - div class="card" { - h2 { "Bitcoin Address" } - div class="info-item" { - span class="info-label" { "Address:" } - span class="info-value" style="font-family: monospace; font-size: 0.9rem;" { (address.to_string()) } - } - } - div class="card" { - a href="/onchain" { button { "← Back to On-chain" } } - " " - a href="/onchain/new-address" { button { "Generate Another Address" } } - } - } - } - Err(e) => { - html! { - (error_message(&format!("Failed to generate address: {e}"))) - div class="card" { - a href="/onchain" { button { "← Back to On-chain" } } - } - } - } - }; - - Ok(Html(layout("New Address", content).into_string())) -} - -pub async fn onchain_page( - State(state): State, - query: Query>, -) -> Result, StatusCode> { - let balances = state.node.inner.list_balances(); - let action = query - .get("action") - .map(|s| s.as_str()) - .unwrap_or("overview"); - - let mut content = html! { - h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" } - - // Quick Actions section - individual cards +fn quick_actions_section() -> maud::Markup { + html! { div class="card" style="margin-bottom: 2rem;" { h2 { "Quick Actions" } div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" { @@ -102,6 +57,64 @@ pub async fn onchain_page( } } } + } +} + +pub async fn get_new_address(State(state): State) -> Result, StatusCode> { + let address_result = state.node.inner.onchain_payment().new_address(); + + let content = match address_result { + Ok(address) => { + html! { + div class="card" { + h2 { "Bitcoin Address" } + div class="address-display" { + div class="address-container" { + span class="address-text" { (address.to_string()) } + } + } + } + div class="card" { + div style="display: flex; justify-content: space-between; gap: 1rem;" { + a href="/onchain" { button class="button-secondary" { "Back" } } + form method="post" action="/onchain/new-address" style="display: inline;" { + button class="button-primary" type="submit" { "Generate Another Address" } + } + } + } + } + } + Err(e) => { + html! { + (error_message(&format!("Failed to generate address: {e}"))) + div class="card" { + a href="/onchain" { button class="button-primary" { "← Back to On-chain" } } + } + } + } + }; + + let is_running = is_node_running(&state.node.inner); + Ok(Html( + layout_with_status("New Address", content, is_running).into_string(), + )) +} + +pub async fn onchain_page( + State(state): State, + query: Query>, +) -> Result, StatusCode> { + let balances = state.node.inner.list_balances(); + let action = query + .get("action") + .map(|s| s.as_str()) + .unwrap_or("overview"); + + let mut content = html! { + h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" } + + // Quick Actions section - only show on overview + (quick_actions_section()) // On-chain Balance as metric cards div class="card" { @@ -124,30 +137,6 @@ pub async fn onchain_page( content = html! { h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" } - // Quick Actions section - individual cards - div class="card" style="margin-bottom: 2rem;" { - h2 { "Quick Actions" } - div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" { - // Receive Bitcoin Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." } - a href="/onchain?action=receive" style="text-decoration: none;" { - button class="button-outline" { "Receive Bitcoin" } - } - } - - // Send Bitcoin Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." } - a href="/onchain?action=send" style="text-decoration: none;" { - button class="button-outline" { "Send Bitcoin" } - } - } - } - } - // Send form above balance (form_card( "Send On-chain Payment", @@ -193,30 +182,6 @@ pub async fn onchain_page( content = html! { h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" } - // Quick Actions section - individual cards - div class="card" style="margin-bottom: 2rem;" { - h2 { "Quick Actions" } - div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" { - // Receive Bitcoin Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." } - a href="/onchain?action=receive" style="text-decoration: none;" { - button class="button-outline" { "Receive Bitcoin" } - } - } - - // Send Bitcoin Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." } - a href="/onchain?action=send" style="text-decoration: none;" { - button class="button-outline" { "Send Bitcoin" } - } - } - } - } - // Generate address form above balance (form_card( "Generate New Address", @@ -252,7 +217,10 @@ pub async fn onchain_page( } } - Ok(Html(layout("On-chain", content).into_string())) + let is_running = is_node_running(&state.node.inner); + Ok(Html( + layout_with_status("On-chain", content, is_running).into_string(), + )) } pub async fn post_send_onchain( @@ -294,7 +262,7 @@ pub async fn onchain_confirm_page( .status(StatusCode::BAD_REQUEST) .header("content-type", "text/html") .body(Body::from( - layout("Send On-chain Error", content).into_string(), + layout_with_status("Send On-chain Error", content, true).into_string(), )) .unwrap()); } @@ -320,7 +288,7 @@ pub async fn onchain_confirm_page( .status(StatusCode::BAD_REQUEST) .header("content-type", "text/html") .body(Body::from( - layout("Send On-chain Error", content).into_string(), + layout_with_status("Send On-chain Error", content, true).into_string(), )) .unwrap()); } @@ -345,46 +313,46 @@ pub async fn onchain_confirm_page( let content = html! { h2 style="text-align: center; margin-bottom: 3rem;" { "Confirm On-chain Transaction" } - div class="card" style="border: 2px solid hsl(var(--primary)); background-color: hsl(var(--primary) / 0.05);" { - h2 { "⚠️ Transaction Confirmation" } - p style="color: hsl(var(--muted-foreground)); margin-bottom: 1.5rem;" { - "Please review the transaction details carefully before proceeding. This action cannot be undone." - } - } - - (info_card( - "Transaction Details", - vec![ - ("Recipient Address", form.address.clone()), - ("Amount to Send", if is_send_all { - format!("{} (All available funds)", format_sats_as_btc(amount_to_send)) - } else { - format_sats_as_btc(amount_to_send) - }), - ("Current Spendable Balance", format_sats_as_btc(spendable_balance)), - ] - )) - @if is_send_all { - div class="card" style="border: 1px solid hsl(32.6 75.4% 55.1%); background-color: hsl(32.6 75.4% 55.1% / 0.1);" { - h3 style="color: hsl(32.6 75.4% 55.1%);" { "Send All Notice" } - p style="color: hsl(32.6 75.4% 55.1%);" { - "This transaction will send all available funds to the recipient address. " - "Network fees will be deducted from the total amount automatically." + div class="card send-all-notice" { + h3 { "Send All Notice" } + p { + "This transaction will send all available funds to the recipient address. Network fees will be deducted from the total amount automatically." } } } + // Transaction Details Card div class="card" { - div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" { + h2 { "Transaction Details" } + div class="transaction-details" { + div class="detail-row" { + span class="detail-label" { "Recipient Address:" } + span class="detail-value" { (form.address.clone()) } + } + div class="detail-row" { + span class="detail-label" { "Amount to Send:" } + span class="detail-value-amount" { + (if is_send_all { + format!("{} (All available funds)", format_sats_as_btc(amount_to_send)) + } else { + format_sats_as_btc(amount_to_send) + }) + } + } + div class="detail-row" { + span class="detail-label" { "Current Spendable Balance:" } + span class="detail-value-amount" { (format_sats_as_btc(spendable_balance)) } + } + } + + div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid hsl(var(--border));" { a href="/onchain?action=send" { button type="button" class="button-secondary" { "Cancel" } } - div style="display: flex; gap: 0.5rem;" { - a href=(confirmation_url) { - button class="button-primary" { - "✓ Confirm & Send Transaction" - } + a href=(confirmation_url) { + button class="button-primary" { + "Confirm" } } } @@ -394,7 +362,7 @@ pub async fn onchain_confirm_page( Ok(Response::builder() .header("content-type", "text/html") .body(Body::from( - layout("Confirm Transaction", content).into_string(), + layout_with_status("Confirm Transaction", content, true).into_string(), )) .unwrap()) } @@ -427,7 +395,7 @@ async fn execute_onchain_transaction( .status(StatusCode::BAD_REQUEST) .header("content-type", "text/html") .body(Body::from( - layout("Send On-chain Error", content).into_string(), + layout_with_status("Send On-chain Error", content, true).into_string(), )) .unwrap()); } @@ -506,7 +474,7 @@ async fn execute_onchain_transaction( Ok(Response::builder() .header("content-type", "text/html") .body(Body::from( - layout("Send On-chain Result", content).into_string(), + layout_with_status("Send On-chain Result", content, true).into_string(), )) .unwrap()) } diff --git a/crates/cdk-ldk-node/src/web/handlers/payments.rs b/crates/cdk-ldk-node/src/web/handlers/payments.rs index 343c7999..6d057022 100644 --- a/crates/cdk-ldk-node/src/web/handlers/payments.rs +++ b/crates/cdk-ldk-node/src/web/handlers/payments.rs @@ -15,8 +15,8 @@ use serde::Deserialize; use crate::web::handlers::utils::{deserialize_optional_u64, get_paginated_payments_streaming}; use crate::web::handlers::AppState; use crate::web::templates::{ - error_message, form_card, format_msats_as_btc, format_sats_as_btc, info_card, layout, - payment_list_item, success_message, + error_message, form_card, format_msats_as_btc, format_sats_as_btc, info_card, is_node_running, + layout_with_status, payment_list_item, success_message, }; #[derive(Deserialize)] @@ -236,12 +236,13 @@ pub async fn payments_page( } }; - Ok(Html(layout("Payment History", content).into_string())) + let is_running = is_node_running(&state.node.inner); + Ok(Html( + layout_with_status("Payment History", content, is_running).into_string(), + )) } -pub async fn send_payments_page( - State(_state): State, -) -> Result, StatusCode> { +pub async fn send_payments_page(State(state): State) -> Result, StatusCode> { let content = html! { h2 style="text-align: center; margin-bottom: 3rem;" { "Send Payment" } div class="grid" { @@ -280,13 +281,12 @@ pub async fn send_payments_page( )) } - div class="card" { - h3 { "Payment History" } - a href="/payments" { button { "View All Payments" } } - } }; - Ok(Html(layout("Send Payments", content).into_string())) + let is_running = is_node_running(&state.node.inner); + Ok(Html( + layout_with_status("Send Payments", content, is_running).into_string(), + )) } pub async fn post_pay_bolt11( @@ -306,7 +306,9 @@ pub async fn post_pay_bolt11( return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .header("content-type", "text/html") - .body(Body::from(layout("Payment Error", content).into_string())) + .body(Body::from( + layout_with_status("Payment Error", content, true).into_string(), + )) .unwrap()); } }; @@ -348,7 +350,9 @@ pub async fn post_pay_bolt11( return Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header("content-type", "text/html") - .body(Body::from(layout("Payment Error", content).into_string())) + .body(Body::from( + layout_with_status("Payment Error", content, true).into_string(), + )) .unwrap()); } }; @@ -433,7 +437,9 @@ pub async fn post_pay_bolt11( Ok(Response::builder() .header("content-type", "text/html") - .body(Body::from(layout("Payment Result", content).into_string())) + .body(Body::from( + layout_with_status("Payment Result", content, true).into_string(), + )) .unwrap()) } @@ -454,7 +460,9 @@ pub async fn post_pay_bolt12( return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .header("content-type", "text/html") - .body(Body::from(layout("Payment Error", content).into_string())) + .body(Body::from( + layout_with_status("Payment Error", content, true).into_string(), + )) .unwrap()); } }; @@ -486,7 +494,9 @@ pub async fn post_pay_bolt12( return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .header("content-type", "text/html") - .body(Body::from(layout("Payment Error", content).into_string())) + .body(Body::from( + layout_with_status("Payment Error", content, true).into_string(), + )) .unwrap()); } }; @@ -518,7 +528,9 @@ pub async fn post_pay_bolt12( return Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header("content-type", "text/html") - .body(Body::from(layout("Payment Error", content).into_string())) + .body(Body::from( + layout_with_status("Payment Error", content, true).into_string(), + )) .unwrap()); } }; @@ -610,6 +622,8 @@ pub async fn post_pay_bolt12( Ok(Response::builder() .header("content-type", "text/html") - .body(Body::from(layout("Payment Result", content).into_string())) + .body(Body::from( + layout_with_status("Payment Result", content, true).into_string(), + )) .unwrap()) } diff --git a/crates/cdk-ldk-node/src/web/templates/layout.rs b/crates/cdk-ldk-node/src/web/templates/layout.rs index 4fd25ef8..76cc167d 100644 --- a/crates/cdk-ldk-node/src/web/templates/layout.rs +++ b/crates/cdk-ldk-node/src/web/templates/layout.rs @@ -1,6 +1,12 @@ +use ldk_node::Node; use maud::{html, Markup, DOCTYPE}; -pub fn layout(title: &str, content: Markup) -> Markup { +/// Helper function to check if the node is running +pub fn is_node_running(node: &Node) -> bool { + node.status().is_running +} + +pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Markup { html! { (DOCTYPE) html lang="en" { @@ -252,12 +258,21 @@ pub fn layout(title: &str, content: Markup) -> Markup { box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); } + .status-indicator.status-inactive { + background-color: #ef4444; + box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2); + } + .status-text { font-size: 0.875rem; font-weight: 500; color: #10b981; } + .status-text.status-inactive { + color: #ef4444; + } + .node-title { font-size: 1.875rem; font-weight: 600; @@ -281,38 +296,104 @@ pub fn layout(title: &str, content: Markup) -> Markup { /* Responsive header */ @media (max-width: 768px) { + header { + height: 180px; /* Slightly taller for better mobile layout */ + padding: 1rem 0; + } + + header .container { + padding: 0 1rem; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + .header-content { flex-direction: column; gap: 1rem; text-align: center; + width: 100%; + justify-content: center; } .header-left { flex-direction: column; text-align: center; + align-items: center; + gap: 0.75rem; + } + + .header-avatar { + width: 64px; + height: 64px; + padding: 0.5rem; + } + + .header-avatar-image { + width: 40px; + height: 40px; } .node-title { font-size: 1.5rem; } + + .node-subtitle { + font-size: 0.8125rem; + text-align: center; + } + + .node-status { + justify-content: center; + } } + @media (max-width: 480px) { + header { + height: 160px; + } + + .header-avatar { + width: 56px; + height: 56px; + padding: 0.375rem; + } + + .header-avatar-image { + width: 36px; + height: 36px; + } + + .node-title { + font-size: 1.25rem; + } + + .node-subtitle { + font-size: 0.75rem; + } + } + + /* Dark mode navigation styles */ + @media (prefers-color-scheme: dark) { nav a { color: var(--text-muted) !important; } nav a:hover { color: var(--text-secondary) !important; - background-color: rgba(255, 255, 255, 0.05) !important; + background-color: rgba(255, 255, 255, 0.08) !important; + transform: translateY(-1px) !important; } nav a.active { color: var(--text-primary) !important; - background-color: rgba(255, 255, 255, 0.08) !important; + background-color: rgba(255, 255, 255, 0.1) !important; } nav a.active:hover { - background-color: rgba(255, 255, 255, 0.1) !important; + background-color: rgba(255, 255, 255, 0.12) !important; + transform: translateY(-1px) !important; } } @@ -409,23 +490,6 @@ pub fn layout(title: &str, content: Markup) -> Markup { line-height: 1.6; } - @media (max-width: 768px) { - header { - height: 150px; /* Smaller height on mobile */ - } - - header .container { - padding: 0 1rem; - } - - h1 { - font-size: 2.25rem; - } - - .subtitle { - font-size: 1.1rem; - } - } /* Card fade-in animation */ @keyframes fade-in { @@ -491,6 +555,15 @@ pub fn layout(title: &str, content: Markup) -> Markup { background-color: hsl(var(--muted)); } + /* Light mode navigation hover states */ + @media (prefers-color-scheme: light) { + nav a:hover { + color: hsl(var(--foreground)); + background-color: hsl(var(--muted) / 0.8); + transform: translateY(-1px); + } + } + nav a.active { color: hsl(var(--primary-foreground)); background-color: hsl(var(--primary)); @@ -587,6 +660,20 @@ pub fn layout(title: &str, content: Markup) -> Markup { width: 100%; } + /* Dark mode input field improvements */ + @media (prefers-color-scheme: dark) { + input, textarea, select { + background-color: hsl(0 0% 8%); + border: 1px solid hsl(0 0% 20%); + color: hsl(var(--foreground)); + } + + input:focus, textarea:focus, select:focus { + background-color: hsl(0 0% 10%); + border-color: hsl(var(--ring)); + } + } + input:focus, textarea:focus, select:focus { outline: 2px solid transparent; outline-offset: 2px; @@ -599,6 +686,74 @@ pub fn layout(title: &str, content: Markup) -> Markup { opacity: 0.5; } + /* Subtle pagination dropdown styling */ + .per-page-selector { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 1rem 0 0 0; + padding: 0; + background-color: transparent; + border: none; + border-radius: 0; + font-size: 0.875rem; + } + + .per-page-selector label { + color: hsl(var(--muted-foreground)); + font-weight: 500; + } + + .per-page-selector select { + background-color: transparent; + border: 1px solid hsl(var(--muted)); + border-radius: calc(var(--radius) - 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + min-width: 50px; + cursor: pointer; + transition: all 0.2s ease; + flex: none; + width: auto; + } + + .per-page-selector select:hover { + border-color: hsl(var(--ring)); + background-color: hsl(var(--muted) / 0.5); + } + + .per-page-selector select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + border-color: hsl(var(--ring)); + box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2); + } + + .per-page-selector span { + color: hsl(var(--muted-foreground)); + font-weight: 500; + } + + /* Form actions layout */ + .form-actions { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid hsl(var(--border)); + } + + .form-actions .button-secondary { + order: 1; + } + + .form-actions .button-primary { + order: 2; + } + button { display: inline-flex; align-items: center; @@ -1082,6 +1237,14 @@ pub fn layout(title: &str, content: Markup) -> Markup { margin-bottom: 1.5rem; } + /* Dark mode payment card improvements - match other cards */ + @media (prefers-color-scheme: dark) { + .payment-item { + background-color: rgba(255, 255, 255, 0.03) !important; + border: none !important; + } + } + .payment-header { display: flex; justify-content: space-between; @@ -1266,6 +1429,39 @@ pub fn layout(title: &str, content: Markup) -> Markup { border: 1px solid hsl(32 95% 44% / 0.2); } + /* Dark mode payment type badge improvements */ + @media (prefers-color-scheme: dark) { + .payment-type-onchain { + background-color: hsl(32 95% 60% / 0.15); + color: hsl(32 95% 70%); + border: 1px solid hsl(32 95% 60% / 0.3); + } + + .payment-type-bolt11 { + background-color: hsl(217 91% 70% / 0.15); + color: hsl(217 91% 80%); + border: 1px solid hsl(217 91% 70% / 0.3); + } + + .payment-type-bolt12 { + background-color: hsl(262 83% 70% / 0.15); + color: hsl(262 83% 80%); + border: 1px solid hsl(262 83% 70% / 0.3); + } + + .payment-type-spontaneous { + background-color: hsl(142.1 70.6% 60% / 0.15); + color: hsl(142.1 70.6% 75%); + border: 1px solid hsl(142.1 70.6% 60% / 0.3); + } + + .payment-type-bolt11-jit { + background-color: hsl(199 89% 65% / 0.15); + color: hsl(199 89% 80%); + border: 1px solid hsl(199 89% 65% / 0.3); + } + } + .payment-type-spontaneous { background-color: hsl(142.1 70.6% 45.3% / 0.1); color: hsl(142.1 70.6% 45.3%); @@ -1635,6 +1831,147 @@ pub fn layout(title: &str, content: Markup) -> Markup { fill: rgba(156, 163, 175, 0.2); } } + + /* Address display styling */ + .address-display { + margin: 1.5rem 0; + } + + .address-container { + padding: 1rem 0; + } + + .address-text { + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; + font-size: 1.25rem; + font-weight: 500; + color: hsl(var(--foreground)); + word-break: break-all; + overflow-wrap: break-word; + hyphens: auto; + flex: 1; + min-width: 0; + line-height: 1.4; + background-color: transparent; + border: none; + padding: 0; + } + + + /* Dark mode address styling */ + @media (prefers-color-scheme: dark) { + .address-text { + color: var(--text-primary) !important; + } + } + + /* Responsive address display */ + @media (max-width: 640px) { + .address-text { + font-size: 1.125rem; + text-align: center; + } + } + + /* Transaction confirmation styling */ + .transaction-details { + margin-top: 1rem; + } + + .transaction-details .detail-row { + display: flex; + align-items: baseline; + margin-bottom: 1rem; + gap: 1rem; + padding: 0.75rem 0; + border-bottom: 1px solid hsl(var(--border)); + } + + .transaction-details .detail-row:last-child { + border-bottom: none; + margin-bottom: 0; + } + + .transaction-details .detail-label { + font-weight: 500; + color: hsl(var(--muted-foreground)); + font-size: 0.875rem; + min-width: 180px; + flex-shrink: 0; + } + + .transaction-details .detail-value { + color: hsl(var(--foreground)); + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; + font-size: 0.875rem; + word-break: break-all; + flex: 1; + min-width: 0; + } + + .transaction-details .detail-value-amount { + color: hsl(var(--foreground)); + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; + font-size: 1rem; + font-weight: 600; + flex: 1; + min-width: 0; + } + + .send-all-notice { + border: 1px solid hsl(32.6 75.4% 55.1%); + background-color: hsl(32.6 75.4% 55.1% / 0.1); + } + + .send-all-notice h3 { + color: hsl(32.6 75.4% 55.1%); + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.5rem; + } + + .send-all-notice p { + color: hsl(32.6 75.4% 55.1%); + font-size: 0.875rem; + line-height: 1.4; + margin: 0; + } + + /* Dark mode transaction styling */ + @media (prefers-color-scheme: dark) { + .transaction-details .detail-label { + color: var(--text-muted) !important; + } + + .transaction-details .detail-value, + .transaction-details .detail-value-amount { + color: var(--text-primary) !important; + } + + .send-all-notice { + background-color: hsl(32.6 75.4% 55.1% / 0.15) !important; + border-color: hsl(32.6 75.4% 55.1% / 0.3) !important; + } + } + + /* Responsive transaction details */ + @media (max-width: 640px) { + .transaction-details .detail-row { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .transaction-details .detail-label { + min-width: auto; + font-size: 0.8125rem; + } + + .transaction-details .detail-value, + .transaction-details .detail-value-amount { + font-size: 0.875rem; + } + } " } } @@ -1648,8 +1985,13 @@ pub fn layout(title: &str, content: Markup) -> Markup { } div class="node-info" { div class="node-status" { - span class="status-indicator status-running" {} - span class="status-text" { "Running" } + @if is_running { + span class="status-indicator" {} + span class="status-text" { "Running" } + } @else { + span class="status-indicator status-inactive" {} + span class="status-text status-inactive" { "Inactive" } + } } h1 class="node-title" { "CDK LDK Node" } span class="node-subtitle" { "Cashu Mint & Lightning Network Node Management" } diff --git a/crates/cdk-ldk-node/static/images/bg-dark.jpg b/crates/cdk-ldk-node/static/images/bg-dark.jpg index d96f3453..6fa6745a 100644 Binary files a/crates/cdk-ldk-node/static/images/bg-dark.jpg and b/crates/cdk-ldk-node/static/images/bg-dark.jpg differ diff --git a/crates/cdk-ldk-node/static/images/bg.jpg b/crates/cdk-ldk-node/static/images/bg.jpg index 0c165114..e8e5a502 100644 Binary files a/crates/cdk-ldk-node/static/images/bg.jpg and b/crates/cdk-ldk-node/static/images/bg.jpg differ