From db2764c56627cf8f1b7cfcac8f2a9aa16c18bd72 Mon Sep 17 00:00:00 2001 From: Erik <78821053+swedishfrenchpress@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:36:04 +0200 Subject: [PATCH] Redesign Lightning invoice creation and display with better UX and status handling (#1184) --------- Co-authored-by: thesimplekid --- .../src/web/handlers/dashboard.rs | 113 ++- .../cdk-ldk-node/src/web/handlers/invoices.rs | 199 +++-- .../src/web/handlers/lightning.rs | 163 ++-- .../cdk-ldk-node/src/web/handlers/onchain.rs | 86 +- .../cdk-ldk-node/src/web/handlers/payments.rs | 142 +++- .../src/web/templates/components.rs | 65 +- .../cdk-ldk-node/src/web/templates/layout.rs | 787 ++++++++++++++++-- .../src/web/templates/payments.rs | 1 + 8 files changed, 1190 insertions(+), 366 deletions(-) diff --git a/crates/cdk-ldk-node/src/web/handlers/dashboard.rs b/crates/cdk-ldk-node/src/web/handlers/dashboard.rs index 0b1471a5..a24cab16 100644 --- a/crates/cdk-ldk-node/src/web/handlers/dashboard.rs +++ b/crates/cdk-ldk-node/src/web/handlers/dashboard.rs @@ -140,8 +140,8 @@ pub async fn dashboard(State(state): State) -> Result, St // Balance Summary as metric cards div class="card" { - h2 { "Balance Summary" } - div class="metrics-container" { + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Balance Summary" } + div class="metrics-container" style="margin-top: 1.5rem;" { div class="metric-card" { div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) } div class="metric-label" { "Lightning Balance" } @@ -209,8 +209,8 @@ pub async fn dashboard(State(state): State) -> Result, St // Right side - Connections metrics aside class="node-metrics" { div class="card" { - h3 { "Connections" } - div class="metrics-container" { + h3 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Connections" } + div class="metrics-container" style="margin-top: 1.5rem;" { div class="metric-card" { div class="metric-value" { (format!("{}/{}", num_connected_peers, num_peers)) } div class="metric-label" { "Connected Peers" } @@ -224,52 +224,75 @@ pub async fn dashboard(State(state): State) -> Result, St } } - // Lightning Network Activity as metric cards + // Activity Sections - Side by Side Layout div class="card" { - h2 { "Lightning Network Activity" } - div class="metrics-container" { - div class="metric-card" { - div class="metric-value" { (format_sats_as_btc(metrics.lightning_inflow_24h)) } - div class="metric-label" { "24h LN Inflow" } + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; margin-bottom: 0;" { "Activity Overview" } + + div class="activity-grid" { + // Lightning Network Activity + div class="activity-section" { + div class="activity-header" { + div class="activity-icon-box" { + svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" { + path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" {} + } + } + h3 class="activity-title" { "Lightning Network Activity" } + } + + div class="activity-metrics" { + div class="activity-metric-card" { + div class="activity-metric-label" { "24h Inflow" } + div class="activity-metric-value" { (format_sats_as_btc(metrics.lightning_inflow_24h)) } + } + div class="activity-metric-card" { + div class="activity-metric-label" { "24h Outflow" } + div class="activity-metric-value" { (format_sats_as_btc(metrics.lightning_outflow_24h)) } + } + div class="activity-metric-card" { + div class="activity-metric-label" { "All-time Inflow" } + div class="activity-metric-value" { (format_sats_as_btc(metrics.lightning_inflow_all_time)) } + } + div class="activity-metric-card" { + div class="activity-metric-label" { "All-time Outflow" } + div class="activity-metric-value" { (format_sats_as_btc(metrics.lightning_outflow_all_time)) } + } + } } - div class="metric-card" { - div class="metric-value" { (format_sats_as_btc(metrics.lightning_outflow_24h)) } - div class="metric-label" { "24h LN Outflow" } - } - div class="metric-card" { - div class="metric-value" { (format_sats_as_btc(metrics.lightning_inflow_all_time)) } - div class="metric-label" { "All-time LN Inflow" } - } - div class="metric-card" { - div class="metric-value" { (format_sats_as_btc(metrics.lightning_outflow_all_time)) } - div class="metric-label" { "All-time LN Outflow" } + + // On-chain Activity + div class="activity-section" { + div class="activity-header" { + div class="activity-icon-box" { + svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" { + path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" {} + path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" {} + } + } + h3 class="activity-title" { "On-chain Activity" } + } + + div class="activity-metrics" { + div class="activity-metric-card" { + div class="activity-metric-label" { "24h Inflow" } + div class="activity-metric-value" { (format_sats_as_btc(metrics.onchain_inflow_24h)) } + } + div class="activity-metric-card" { + div class="activity-metric-label" { "24h Outflow" } + div class="activity-metric-value" { (format_sats_as_btc(metrics.onchain_outflow_24h)) } + } + div class="activity-metric-card" { + div class="activity-metric-label" { "All-time Inflow" } + div class="activity-metric-value" { (format_sats_as_btc(metrics.onchain_inflow_all_time)) } + } + div class="activity-metric-card" { + div class="activity-metric-label" { "All-time Outflow" } + div class="activity-metric-value" { (format_sats_as_btc(metrics.onchain_outflow_all_time)) } + } + } } } } - - // On-chain Activity as metric cards - div class="card" { - h2 { "On-chain Activity" } - div class="metrics-container" { - div class="metric-card" { - div class="metric-value" { (format_sats_as_btc(metrics.onchain_inflow_24h)) } - div class="metric-label" { "24h On-chain Inflow" } - } - div class="metric-card" { - div class="metric-value" { (format_sats_as_btc(metrics.onchain_outflow_24h)) } - div class="metric-label" { "24h On-chain Outflow" } - } - div class="metric-card" { - div class="metric-value" { (format_sats_as_btc(metrics.onchain_inflow_all_time)) } - div class="metric-label" { "All-time On-chain Inflow" } - } - div class="metric-card" { - div class="metric-value" { (format_sats_as_btc(metrics.onchain_outflow_all_time)) } - div class="metric-label" { "All-time On-chain Outflow" } - } - } - - } }; let is_running = is_node_running(&state.node.inner); diff --git a/crates/cdk-ldk-node/src/web/handlers/invoices.rs b/crates/cdk-ldk-node/src/web/handlers/invoices.rs index 7691e383..931eb5c6 100644 --- a/crates/cdk-ldk-node/src/web/handlers/invoices.rs +++ b/crates/cdk-ldk-node/src/web/handlers/invoices.rs @@ -10,7 +10,7 @@ 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, is_node_running, layout_with_status, + error_message, format_sats_as_btc, invoice_display_card, is_node_running, layout_with_status, success_message, }; @@ -34,54 +34,101 @@ pub struct CreateBolt12Form { 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" { - (form_card( - "Create BOLT11 Invoice", - html! { - form method="post" action="/invoices/bolt11" { - div class="form-group" { - label for="amount_btc" { "Amount" } - input type="number" id="amount_btc" name="amount_btc" required placeholder="₿0" step="0.00000001" {} - } - div class="form-group" { - label for="description" { "Description (optional)" } - input type="text" id="description" name="description" placeholder="Payment for..." {} - } - div class="form-group" { - label for="expiry_seconds" { "Expiry (seconds, optional)" } - input type="number" id="expiry_seconds" name="expiry_seconds" placeholder="3600" {} - } - button type="submit" { "Create BOLT11 Invoice" } - } - } - )) - (form_card( - "Create BOLT12 Offer", - html! { - form method="post" action="/invoices/bolt12" { - div class="form-group" { - label for="amount_btc" { "Amount (optional for variable amount)" } - input type="number" id="amount_btc" name="amount_btc" placeholder="₿0" step="0.00000001" {} - } - div class="form-group" { - label for="description" { "Description (optional)" } - input type="text" id="description" name="description" placeholder="Payment for..." {} - } - div class="form-group" { - label for="expiry_seconds" { "Expiry (seconds, optional)" } - input type="number" id="expiry_seconds" name="expiry_seconds" placeholder="3600" {} - } - button type="submit" { "Create BOLT12 Offer" } + div class="card" { + // Tab navigation + div class="payment-tabs" style="display: flex; gap: 0.5rem; margin-bottom: 1.5rem; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 0;" { + button type="button" class="payment-tab active" onclick="switchInvoiceTab('bolt11')" data-tab="bolt11" { + "BOLT11 Invoice" + } + button type="button" class="payment-tab" onclick="switchInvoiceTab('bolt12')" data-tab="bolt12" { + "BOLT12 Offer" + } + } + + // BOLT11 tab content + div id="bolt11-content" class="tab-content active" { + form method="post" action="/invoices/bolt11" { + div class="form-group" { + label for="amount_btc_bolt11" { "Amount" } + input type="number" id="amount_btc_bolt11" name="amount_btc" required placeholder="₿0" step="0.00000001" {} + } + div class="form-group" { + label for="description_bolt11" { "Description (optional)" } + input type="text" id="description_bolt11" name="description" placeholder="Payment for..." {} + } + div class="form-group" { + label for="expiry_seconds_bolt11" { "Expiry (seconds, optional)" } + input type="number" id="expiry_seconds_bolt11" name="expiry_seconds" placeholder="3600" {} + } + div class="form-actions" { + a href="/balance" { button type="button" class="button-secondary" { "Cancel" } } + button type="submit" class="button-primary" { "Create BOLT11 Invoice" } } } - )) + } + + // BOLT12 tab content + div id="bolt12-content" class="tab-content" { + form method="post" action="/invoices/bolt12" { + div class="form-group" { + label for="amount_btc_bolt12" { "Amount (optional for variable amount)" } + input type="number" id="amount_btc_bolt12" name="amount_btc" placeholder="₿0" step="0.00000001" {} + p style="font-size: 0.8125rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem;" { + "Leave empty for variable amount offers, specify amount for fixed offers" + } + } + div class="form-group" { + label for="description_bolt12" { "Description (optional)" } + input type="text" id="description_bolt12" name="description" placeholder="Payment for..." {} + } + div class="form-group" { + label for="expiry_seconds_bolt12" { "Expiry (seconds, optional)" } + input type="number" id="expiry_seconds_bolt12" name="expiry_seconds" placeholder="3600" {} + } + div class="form-actions" { + a href="/balance" { button type="button" class="button-secondary" { "Cancel" } } + button type="submit" class="button-primary" { "Create BOLT12 Offer" } + } + } + } + } + + // Tab switching script + script type="text/javascript" { + (maud::PreEscaped(r#" + function switchInvoiceTab(tabName) { + console.log('Switching to invoice tab:', tabName); + + // Hide all tab contents + const contents = document.querySelectorAll('.tab-content'); + contents.forEach(content => content.classList.remove('active')); + + // Remove active class from all tabs + const tabs = document.querySelectorAll('.payment-tab'); + tabs.forEach(tab => tab.classList.remove('active')); + + // Show selected tab content + const tabContent = document.getElementById(tabName + '-content'); + if (tabContent) { + tabContent.classList.add('active'); + console.log('Activated invoice tab content:', tabName); + } + + // Add active class to selected tab + const tabButton = document.querySelector('[data-tab="' + tabName + '"]'); + if (tabButton) { + tabButton.classList.add('active'); + console.log('Activated invoice tab button:', tabName); + } + } + "#)) } }; let is_running = is_node_running(&state.node.inner); Ok(Html( - layout_with_status("Create Invoices", content, is_running).into_string(), + layout_with_status("s", content, is_running).into_string(), )) } @@ -126,7 +173,7 @@ pub async fn post_create_bolt11( .status(StatusCode::BAD_REQUEST) .header("content-type", "text/html") .body(Body::from( - layout_with_status("Create Invoice Error", content, true).into_string(), + layout_with_status(" Error", content, true).into_string(), )) .unwrap()); } @@ -161,32 +208,25 @@ pub async fn post_create_bolt11( description_text.clone() }; + let invoice_details = vec![ + ("Payment Hash", invoice.payment_hash().to_string()), + ("Amount", format_sats_as_btc(form.amount_btc)), + ("Description", description_display), + ( + "Expires At", + format!("{}", current_time + expiry_seconds as u64), + ), + ]; + html! { (success_message("BOLT11 Invoice created successfully!")) - (info_card( - "Invoice Details", - vec![ - ("Payment Hash", invoice.payment_hash().to_string()), - ("Amount", format_sats_as_btc(form.amount_btc)), - ("Description", description_display), - ("Expires At", format!("{}", current_time + expiry_seconds as u64)), - ] - )) - div class="card" { - h3 { "Invoice (copy this to share)" } - textarea readonly style="width: 100%; height: 150px; font-family: monospace; font-size: 0.8rem;" { - (invoice.to_string()) - } - } - div class="card" { - a href="/invoices" { button { "← Create Another Invoice" } } - } + (invoice_display_card(&invoice.to_string(), &format_sats_as_btc(form.amount_btc), invoice_details, "/invoices")) } } Err(e) => { tracing::error!("Web interface: Failed to create BOLT11 invoice: {}", e); html! { - (error_message(&format!("Failed to create invoice: {e}"))) + (error_message(&format!("Failed to : {e}"))) div class="card" { a href="/invoices" { button { "← Try Again" } } } @@ -210,15 +250,15 @@ pub async fn post_create_bolt12( let description_text = form.description.unwrap_or_else(|| "".to_string()); tracing::info!( - "Web interface: Creating BOLT12 offer for amount={:?} btc, description={:?}, expiry={}s", + "Web interface: Creating BOLT12 offer for amount={:?} sats, description={:?}, expiry={}s", form.amount_btc, description_text, expiry_seconds ); let offer_result = if let Some(amount_btc) = form.amount_btc { - // Convert Bitcoin to millisatoshis (1 BTC = 100,000,000,000 msats) - let amount_msats = (amount_btc * 100_000_000_000.0) as u64; + // Convert satoshis to millisatoshis (1 sat = 1,000 msats) + let amount_msats = (amount_btc * 1_000.0) as u64; state.node.inner.bolt12_payment().receive( amount_msats, &description_text, @@ -246,7 +286,7 @@ pub async fn post_create_bolt12( let amount_display = form .amount_btc - .map(|a| format_sats_as_btc((a * 100_000_000.0) as u64)) + .map(|a| format_sats_as_btc(a as u64)) .unwrap_or_else(|| "Variable amount".to_string()); let description_display = if description_text.is_empty() { @@ -255,26 +295,19 @@ pub async fn post_create_bolt12( description_text }; + let offer_details = vec![ + ("Offer ID", offer.id().to_string()), + ("Amount", amount_display.clone()), + ("Description", description_display), + ( + "Expires At", + format!("{}", current_time + expiry_seconds as u64), + ), + ]; + html! { (success_message("BOLT12 Offer created successfully!")) - (info_card( - "Offer Details", - vec![ - ("Offer ID", offer.id().to_string()), - ("Amount", amount_display), - ("Description", description_display), - ("Expires At", format!("{}", current_time + expiry_seconds as u64)), - ] - )) - div class="card" { - h3 { "Offer (copy this to share)" } - textarea readonly style="width: 100%; height: 150px; font-family: monospace; font-size: 0.8rem;" { - (offer.to_string()) - } - } - div class="card" { - a href="/invoices" { button { "← Create Another Offer" } } - } + (invoice_display_card(&offer.to_string(), &amount_display, offer_details, "/invoices")) } } Err(e) => { diff --git a/crates/cdk-ldk-node/src/web/handlers/lightning.rs b/crates/cdk-ldk-node/src/web/handlers/lightning.rs index a5a0b9e7..2782cb9d 100644 --- a/crates/cdk-ldk-node/src/web/handlers/lightning.rs +++ b/crates/cdk-ldk-node/src/web/handlers/lightning.rs @@ -26,43 +26,33 @@ pub async fn balance_page(State(state): State) -> Result, html! { h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" } - // 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;" { - // Open Channel Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning Network channel to connect with another node." } - a href="/channels/open" style="text-decoration: none;" { - button class="button-outline" { "Open Channel" } - } - } - - // Create Invoice Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments from other users or services." } - a href="/invoices" style="text-decoration: none;" { - button class="button-outline" { "Create Invoice" } - } - } - - // Make Payment Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices. BOLT 11 & 12 supported." } - a href="/invoices" style="text-decoration: none;" { - button class="button-outline" { "Make Payment" } - } + // Inactive channels warning (only show if > 0) + @if num_inactive_channels > 0 { + div class="card" style="background-color: #fef3c7; border: 1px solid #f59e0b; margin-bottom: 2rem;" { + h3 style="color: #92400e; margin-bottom: 0.5rem;" { "⚠️ Inactive Channels Detected" } + p style="color: #78350f; margin: 0;" { + "You have " (num_inactive_channels) " inactive channel(s). This may indicate a connectivity issue that requires attention." } } } - // Balance Information as metric cards + // Balance Information with action buttons in header div class="card" { - h2 { "Balance Information" } - div class="metrics-container" { + div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "Balance Information" } + div style="display: flex; gap: 0.5rem;" { + a href="/payments/send" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" } + } + a href="/invoices" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" } + } + a href="/channels/open" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Open Channel" } + } + } + } + div class="metrics-container" style="margin-top: 1.5rem;" { div class="metric-card" { div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) } div class="metric-label" { "Lightning Balance" } @@ -75,9 +65,11 @@ pub async fn balance_page(State(state): State) -> Result, div class="metric-value" { (format!("{}", num_active_channels)) } div class="metric-label" { "Active Channels" } } - div class="metric-card" { - div class="metric-value" { (format!("{}", num_inactive_channels)) } - div class="metric-label" { "Inactive Channels" } + @if num_inactive_channels > 0 { + div class="metric-card" { + div class="metric-value" style="color: #f59e0b;" { (format!("{}", num_inactive_channels)) } + div class="metric-label" { "Inactive Channels" } + } } } } @@ -90,43 +82,33 @@ pub async fn balance_page(State(state): State) -> Result, html! { h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" } - // 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;" { - // Open Channel Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning channel by connecting with another node." } - a href="/channels/open" style="text-decoration: none;" { - button class="button-outline" { "Open Channel" } - } - } - - // Create Invoice Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments." } - a href="/invoices" style="text-decoration: none;" { - button class="button-outline" { "Create Invoice" } - } - } - - // Make Payment Card - div class="quick-action-card" { - h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" } - p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices." } - a href="/payments/send" style="text-decoration: none;" { - button class="button-outline" { "Make Payment" } - } + // Inactive channels warning (only show if > 0) + @if num_inactive_channels > 0 { + div class="card" style="background-color: #fef3c7; border: 1px solid #f59e0b; margin-bottom: 2rem;" { + h3 style="color: #92400e; margin-bottom: 0.5rem;" { "⚠️ Inactive Channels Detected" } + p style="color: #78350f; margin: 0;" { + "You have " (num_inactive_channels) " inactive channel(s). This may indicate a connectivity issue that requires attention." } } } - // Balance Information as metric cards + // Balance Information with action buttons in header div class="card" { - h2 { "Balance Information" } - div class="metrics-container" { + div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "Balance Information" } + div style="display: flex; gap: 0.5rem;" { + a href="/payments/send" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" } + } + a href="/invoices" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" } + } + a href="/channels/open" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Open Channel" } + } + } + } + div class="metrics-container" style="margin-top: 1.5rem;" { div class="metric-card" { div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) } div class="metric-label" { "Lightning Balance" } @@ -139,47 +121,48 @@ pub async fn balance_page(State(state): State) -> Result, div class="metric-value" { (format!("{}", num_active_channels)) } div class="metric-label" { "Active Channels" } } - div class="metric-card" { - div class="metric-value" { (format!("{}", num_inactive_channels)) } - div class="metric-label" { "Inactive Channels" } + @if num_inactive_channels > 0 { + div class="metric-card" { + div class="metric-value" style="color: #f59e0b;" { (format!("{}", num_inactive_channels)) } + div class="metric-label" { "Inactive Channels" } + } } } } // Channel Details header (outside card) - h2 class="section-header" { "Channel Details" } + h2 class="section-header" style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5;" { "Channel Details" } - // Channels list - @for (index, channel) in channels.iter().enumerate() { + // Channels list + @for (index, channel) in channels.iter().enumerate() { @let node_id = channel.counterparty_node_id.to_string(); @let channel_number = index + 1; div class="channel-box" { - // Channel number as prominent header - div class="channel-alias" { (format!("Channel {}", channel_number)) } + // Channel header with number on left and status badge on right + div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 1.5rem;" { + div class="channel-alias" style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { (format!("Channel {}", channel_number)) } + @if channel.is_usable { + span class="status-badge status-active" { "Active" } + } @else { + span class="status-badge status-inactive" { "Inactive" } + } + } - // Channel details in left-aligned format + // Channel details - ordered by label length div class="channel-details" { + div class="detail-row" { + span class="detail-label" { "Node ID" } + span class="detail-value" { (node_id) } + } div class="detail-row" { span class="detail-label" { "Channel ID" } - span class="detail-value-amount" { (channel.channel_id.to_string()) } + span class="detail-value" { (channel.channel_id.to_string()) } } @if let Some(short_channel_id) = channel.short_channel_id { div class="detail-row" { span class="detail-label" { "Short Channel ID" } - span class="detail-value-amount" { (short_channel_id.to_string()) } - } - } - div class="detail-row" { - span class="detail-label" { "Node ID" } - span class="detail-value-amount" { (node_id) } - } - div class="detail-row" { - span class="detail-label" { "Status" } - @if channel.is_usable { - span class="status-badge status-active" { "Active" } - } @else { - span class="status-badge status-inactive" { "Inactive" } + span class="detail-value" { (short_channel_id.to_string()) } } } } diff --git a/crates/cdk-ldk-node/src/web/handlers/onchain.rs b/crates/cdk-ldk-node/src/web/handlers/onchain.rs index 0a296faf..2282001e 100644 --- a/crates/cdk-ldk-node/src/web/handlers/onchain.rs +++ b/crates/cdk-ldk-node/src/web/handlers/onchain.rs @@ -33,33 +33,6 @@ pub struct ConfirmOnchainForm { confirmed: Option, } -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;" { - // 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" } - } - } - } - } - } -} - pub async fn get_new_address(State(state): State) -> Result, StatusCode> { let address_result = state.node.inner.onchain_payment().new_address(); @@ -67,8 +40,8 @@ pub async fn get_new_address(State(state): State) -> Result { html! { div class="card" { - h2 { "Bitcoin Address" } - div class="address-display" { + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Bitcoin Address" } + div class="address-display" style="margin-top: 1.5rem;" { div class="address-container" { span class="address-text" { (address.to_string()) } } @@ -113,13 +86,20 @@ pub async fn onchain_page( 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 + // On-chain Balance with action buttons in header div class="card" { - h2 { "On-chain Balance" } - div class="metrics-container" { + div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "On-chain Balance" } + div style="display: flex; gap: 0.5rem;" { + a href="/onchain?action=send" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" } + } + a href="/onchain?action=receive" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" } + } + } + } + div class="metrics-container" style="margin-top: 1.5rem;" { div class="metric-card" { div class="metric-value" { (format_sats_as_btc(balances.total_onchain_balance_sats)) } div class="metric-label" { "Total Balance" } @@ -162,10 +142,20 @@ pub async fn onchain_page( } )) - // On-chain Balance as metric cards + // On-chain Balance with action buttons in header div class="card" { - h2 { "On-chain Balance" } - div class="metrics-container" { + div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "On-chain Balance" } + div style="display: flex; gap: 0.5rem;" { + a href="/onchain?action=send" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" } + } + a href="/onchain?action=receive" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" } + } + } + } + div class="metrics-container" style="margin-top: 1.5rem;" { div class="metric-card" { div class="metric-value" { (format_sats_as_btc(balances.total_onchain_balance_sats)) } div class="metric-label" { "Total Balance" } @@ -196,10 +186,20 @@ pub async fn onchain_page( } )) - // On-chain Balance as metric cards + // On-chain Balance with action buttons in header div class="card" { - h2 { "On-chain Balance" } - div class="metrics-container" { + div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "On-chain Balance" } + div style="display: flex; gap: 0.5rem;" { + a href="/onchain?action=send" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" } + } + a href="/onchain?action=receive" style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" } + } + } + } + div class="metrics-container" style="margin-top: 1.5rem;" { div class="metric-card" { div class="metric-value" { (format_sats_as_btc(balances.total_onchain_balance_sats)) } div class="metric-label" { "Total Balance" } @@ -324,8 +324,8 @@ pub async fn onchain_confirm_page( // Transaction Details Card div class="card" { - h2 { "Transaction Details" } - div class="transaction-details" { + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Transaction Details" } + div class="transaction-details" style="margin-top: 1.5rem;" { div class="detail-row" { span class="detail-label" { "Recipient Address:" } span class="detail-value" { (form.address.clone()) } diff --git a/crates/cdk-ldk-node/src/web/handlers/payments.rs b/crates/cdk-ldk-node/src/web/handlers/payments.rs index 6d057022..3bcfbc43 100644 --- a/crates/cdk-ldk-node/src/web/handlers/payments.rs +++ b/crates/cdk-ldk-node/src/web/handlers/payments.rs @@ -15,7 +15,7 @@ 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, is_node_running, + error_message, format_msats_as_btc, format_sats_as_btc, info_card, is_node_running, layout_with_status, payment_list_item, success_message, }; @@ -86,7 +86,7 @@ pub async fn payments_page( div class="card" { div class="payment-list-header" { div { - h2 { "Payment History" } + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Payment History" } @if total_count > 0 { p style="margin: 0.25rem 0 0 0; color: #666; font-size: 0.9rem;" { "Showing " (start_index + 1) " to " (end_index) " of " (total_count) " payments" @@ -116,14 +116,6 @@ pub async fn payments_page( PaymentDirection::Outbound => "Outbound", }; - @let status_str = match payment.status { - PaymentStatus::Pending => "Pending", - PaymentStatus::Succeeded => "Succeeded", - PaymentStatus::Failed => "Failed", - }; - - @let amount_str = payment.amount_msat.map(format_msats_as_btc).unwrap_or_else(|| "Unknown".to_string()); - @let (payment_hash, description, payment_type, preimage) = match &payment.kind { PaymentKind::Bolt11 { hash, preimage, .. } => { (Some(hash.to_string()), None::, "BOLT11", preimage.map(|p| p.to_string())) @@ -147,6 +139,27 @@ pub async fn payments_page( }, }; + @let status_str = { + // Helper function to determine invoice status + fn get_invoice_status(status: PaymentStatus, direction: PaymentDirection, payment_type: &str) -> &'static str { + match status { + PaymentStatus::Succeeded => "Succeeded", + PaymentStatus::Failed => "Failed", + PaymentStatus::Pending => { + // For inbound BOLT11 payments, show "Unpaid" instead of "Pending" + if direction == PaymentDirection::Inbound && payment_type == "BOLT11" { + "Unpaid" + } else { + "Pending" + } + } + } + } + get_invoice_status(payment.status, payment.direction, payment_type) + }; + + @let amount_str = payment.amount_msat.map(format_msats_as_btc).unwrap_or_else(|| "Unknown".to_string()); + (payment_list_item( &payment.id.to_string(), direction_str, @@ -245,42 +258,91 @@ pub async fn payments_page( 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" { - (form_card( - "Pay BOLT11 Invoice", - html! { - form method="post" action="/payments/bolt11" { - div class="form-group" { - label for="invoice" { "BOLT11 Invoice" } - textarea id="invoice" name="invoice" required placeholder="lnbc..." style="height: 120px;" {} - } - div class="form-group" { - label for="amount_btc" { "Amount Override (optional)" } - input type="number" id="amount_btc" name="amount_btc" placeholder="Leave empty to use invoice amount" step="1" {} - } - button type="submit" { "Pay BOLT11 Invoice" } - } - } - )) - (form_card( - "Pay BOLT12 Offer", - html! { - form method="post" action="/payments/bolt12" { - div class="form-group" { - label for="offer" { "BOLT12 Offer" } - textarea id="offer" name="offer" required placeholder="lno..." style="height: 120px;" {} + div class="card" { + // Tab navigation + div class="payment-tabs" style="display: flex; gap: 0.5rem; margin-bottom: 1.5rem; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 0;" { + button type="button" class="payment-tab active" onclick="switchTab('bolt11')" data-tab="bolt11" { + "BOLT11 Invoice" + } + button type="button" class="payment-tab" onclick="switchTab('bolt12')" data-tab="bolt12" { + "BOLT12 Offer" + } + } + + // BOLT11 tab content + div id="bolt11-content" class="tab-content active" { + form method="post" action="/payments/bolt11" { + div class="form-group" { + label for="invoice" { "BOLT11 Invoice" } + textarea id="invoice" name="invoice" required placeholder="lnbc..." rows="4" {} + } + div class="form-group" { + label for="amount_btc_bolt11" { "Amount Override (optional)" } + input type="number" id="amount_btc_bolt11" name="amount_btc" placeholder="Leave empty to use invoice amount" step="1" {} + p style="font-size: 0.8125rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem;" { + "Only specify an amount if you want to override the invoice amount" } - div class="form-group" { - label for="amount_btc" { "Amount (required for variable amount offers)" } - input type="number" id="amount_btc" name="amount_btc" placeholder="Required for variable amount offers, ignored for fixed amount offers" step="1" {} - } - button type="submit" { "Pay BOLT12 Offer" } + } + div class="form-actions" { + a href="/balance" { button type="button" class="button-secondary" { "Cancel" } } + button type="submit" class="button-primary" { "Pay Invoice" } } } - )) + } + + // BOLT12 tab content + div id="bolt12-content" class="tab-content" { + form method="post" action="/payments/bolt12" { + div class="form-group" { + label for="offer" { "BOLT12 Offer" } + textarea id="offer" name="offer" required placeholder="lno..." rows="4" {} + } + div class="form-group" { + label for="amount_btc_bolt12" { "Amount" } + input type="number" id="amount_btc_bolt12" name="amount_btc" placeholder="Amount in satoshis" step="1" {} + p style="font-size: 0.8125rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem;" { + "Required for variable amount offers, ignored for fixed amount offers" + } + } + div class="form-actions" { + a href="/balance" { button type="button" class="button-secondary" { "Cancel" } } + button type="submit" class="button-primary" { "Pay Offer" } + } + } + } } + // Tab switching script + script type="text/javascript" { + (maud::PreEscaped(r#" + function switchTab(tabName) { + console.log('Switching to tab:', tabName); + + // Hide all tab contents + const contents = document.querySelectorAll('.tab-content'); + contents.forEach(content => content.classList.remove('active')); + + // Remove active class from all tabs + const tabs = document.querySelectorAll('.payment-tab'); + tabs.forEach(tab => tab.classList.remove('active')); + + // Show selected tab content + const tabContent = document.getElementById(tabName + '-content'); + if (tabContent) { + tabContent.classList.add('active'); + console.log('Activated tab content:', tabName); + } + + // Add active class to selected tab + const tabButton = document.querySelector('[data-tab="' + tabName + '"]'); + if (tabButton) { + tabButton.classList.add('active'); + console.log('Activated tab button:', tabName); + } + } + "#)) + } }; let is_running = is_node_running(&state.node.inner); diff --git a/crates/cdk-ldk-node/src/web/templates/components.rs b/crates/cdk-ldk-node/src/web/templates/components.rs index bbada534..6df325e6 100644 --- a/crates/cdk-ldk-node/src/web/templates/components.rs +++ b/crates/cdk-ldk-node/src/web/templates/components.rs @@ -3,11 +3,13 @@ use maud::{html, Markup}; pub fn info_card(title: &str, items: Vec<(&str, String)>) -> Markup { html! { div class="card" { - h2 { (title) } - @for (label, value) in items { - div class="info-item" { - span class="info-label" { (label) ":" } - span class="info-value" { (value) } + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { (title) } + div style="margin-top: 1.5rem;" { + @for (label, value) in items { + div class="info-item" { + span class="info-label" { (label) ":" } + span class="info-value" { (value) } + } } } } @@ -17,8 +19,10 @@ pub fn info_card(title: &str, items: Vec<(&str, String)>) -> Markup { pub fn form_card(title: &str, form_content: Markup) -> Markup { html! { div class="card" { - h2 { (title) } - (form_content) + h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { (title) } + div style="margin-top: 1.5rem;" { + (form_content) + } } } } @@ -34,3 +38,50 @@ pub fn error_message(message: &str) -> Markup { div class="error" { (message) } } } + +pub fn invoice_display_card( + invoice_text: &str, + amount: &str, + details: Vec<(&str, String)>, + back_url: &str, +) -> Markup { + html! { + div class="card" { + div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 1.5rem;" { + h3 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "Invoice Details" } + } + + // Amount highlight section at the top + div class="invoice-amount-section" { + div class="invoice-amount-label" { "Amount" } + div class="invoice-amount-value" { (amount) } + } + + // Invoice display section - under the amount + div class="invoice-display-section" { + div class="invoice-label" { "Invoice" } + div class="invoice-display-container" { + textarea readonly class="invoice-textarea" { (invoice_text) } + } + } + + // Invoice details section - after the invoice with increased spacing + div class="invoice-details-section" style="margin-top: 2.5rem;" { + h4 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0 0 1rem 0;" { "Details" } + @for (label, value) in details { + div class="info-item" { + span class="info-label" { (label) ":" } + span class="info-value" { (value) } + } + } + } + + // Back button at bottom left - no border lines + div style="margin-top: 2rem;" { + a href=(back_url) style="text-decoration: none;" { + button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Back" } + } + } + } + } +} diff --git a/crates/cdk-ldk-node/src/web/templates/layout.rs b/crates/cdk-ldk-node/src/web/templates/layout.rs index 76cc167d..9e191a4b 100644 --- a/crates/cdk-ldk-node/src/web/templates/layout.rs +++ b/crates/cdk-ldk-node/src/web/templates/layout.rs @@ -39,7 +39,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; - --radius: 0.5rem; + --radius: 0; /* Typography scale */ --fs-title: 1.25rem; @@ -148,11 +148,16 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .detail-label { - color: var(--text-muted) !important; + color: hsl(var(--foreground)) !important; + opacity: 0.5 !important; } .detail-value, .detail-value-amount { - color: var(--text-secondary) !important; + color: hsl(var(--foreground)) !important; + } + + .info-value { + color: var(--text-primary) !important; } .metric-label, .balance-label { @@ -177,7 +182,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .quick-action-card { background-color: rgba(255, 255, 255, 0.03) !important; border: none !important; - border-radius: 0.75rem !important; + border-radius: 0 !important; padding: 1.5rem !important; } @@ -219,7 +224,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar flex-shrink: 0; background-color: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); - border-radius: var(--radius); + border-radius: 0; padding: 0.75rem; display: flex; align-items: center; @@ -231,7 +236,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .header-avatar-image { width: 48px; height: 48px; - border-radius: calc(var(--radius) - 2px); + border-radius: 0; object-fit: cover; display: block; } @@ -282,9 +287,11 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .node-subtitle { - font-size: 0.875rem; - color: var(--header-subtitle); + font-size: 0.75rem; + color: var(--text-muted); font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; } .header-right { @@ -340,7 +347,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .node-subtitle { - font-size: 0.8125rem; + font-size: 0.6875rem; text-align: center; } @@ -436,10 +443,12 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar /* Hero section styling */ header { position: relative; - background-image: url('/static/images/bg.jpg?v=3'); - background-size: cover; - background-position: center; - background-repeat: no-repeat; + background-color: hsl(var(--background)); + background-image: + linear-gradient(hsl(var(--border)) 1px, transparent 1px), + linear-gradient(90deg, hsl(var(--border)) 1px, transparent 1px); + background-size: 40px 40px; + background-position: -1px -1px; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 2rem; text-align: left; @@ -450,10 +459,34 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar justify-content: flex-start; } - /* Dark mode header background - using different image */ + /* Subtle diamond gradient fade on edges */ + header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + linear-gradient(90deg, hsl(var(--background)) 0%, transparent 15%, transparent 85%, hsl(var(--background)) 100%), + linear-gradient(180deg, hsl(var(--background)) 0%, transparent 15%, transparent 85%, hsl(var(--background)) 100%); + pointer-events: none; + z-index: 1; + } + + /* Dark mode header background - subtle grid with darker theme */ @media (prefers-color-scheme: dark) { header { - background-image: url('/static/images/bg-dark.jpg?v=3'); + background-color: rgb(18, 19, 21); + background-image: + linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px); + } + + header::before { + background: + linear-gradient(90deg, rgb(18, 19, 21) 0%, transparent 15%, transparent 85%, rgb(18, 19, 21) 100%), + linear-gradient(180deg, rgb(18, 19, 21) 0%, transparent 15%, transparent 85%, rgb(18, 19, 21) 100%); } } @@ -501,6 +534,37 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar animation: fade-in 0.3s ease-out; } + /* Corner embellishments for angular design */ + .card::before, + .card::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border: 1px solid hsl(var(--border)); + } + + .card::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .card::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + @media (prefers-color-scheme: dark) { + .card::before, + .card::after { + border-color: rgba(255, 255, 255, 0.2); + } + } + /* Modern Navigation Bar Styling */ nav { background-color: hsl(var(--card)); @@ -544,7 +608,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar font-weight: 600; color: hsl(var(--muted-foreground)); padding: 1rem 1.5rem; - border-radius: calc(var(--radius) - 2px); + border-radius: 0; transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); position: relative; min-height: 3rem; @@ -575,12 +639,13 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .card { + position: relative; background-color: hsl(var(--card)); border: 1px solid hsl(var(--border)); - border-radius: var(--radius); + border-radius: 0; padding: 1.5rem; margin-bottom: 1.5rem; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + box-shadow: none; } /* Metric cards styling - matching balance-item style */ @@ -592,15 +657,46 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .metric-card { + position: relative; flex: 1; min-width: 200px; text-align: center; padding: 1rem; background-color: hsl(var(--muted) / 0.3); - border-radius: calc(var(--radius) - 2px); + border-radius: 0; border: 1px solid hsl(var(--border)); } + .metric-card::before, + .metric-card::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + border: 1px solid hsl(var(--border)); + } + + .metric-card::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .metric-card::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + @media (prefers-color-scheme: dark) { + .metric-card::before, + .metric-card::after { + border-color: rgba(255, 255, 255, 0.2); + } + } + .metric-value { font-size: 1.5rem; font-weight: 600; @@ -651,7 +747,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar flex: 1; background-color: hsl(var(--background)); border: 1px solid hsl(var(--input)); - border-radius: calc(var(--radius) - 2px); + border-radius: 0; padding: 0.5rem 0.75rem; font-size: 0.875rem; line-height: 1.25; @@ -672,6 +768,10 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar background-color: hsl(0 0% 10%); border-color: hsl(var(--ring)); } + + textarea { + color: var(--text-primary) !important; + } } input:focus, textarea:focus, select:focus { @@ -707,7 +807,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .per-page-selector select { background-color: transparent; border: 1px solid hsl(var(--muted)); - border-radius: calc(var(--radius) - 2px); + border-radius: 0; padding: 0.25rem 0.5rem; font-size: 0.875rem; color: hsl(var(--muted-foreground)); @@ -759,7 +859,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar align-items: center; justify-content: center; white-space: nowrap; - border-radius: calc(var(--radius) - 2px); + border-radius: 0; font-size: 0.875rem; font-weight: 600; transition: all 150ms ease-in-out; @@ -820,14 +920,14 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .button-sm { height: 2rem; - border-radius: calc(var(--radius) - 4px); + border-radius: 0; padding: 0 0.75rem; font-size: 0.75rem; } .button-lg { height: 2.75rem; - border-radius: var(--radius); + border-radius: 0; padding: 0 2rem; font-size: 1rem; } @@ -932,7 +1032,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border: 1px solid hsl(var(--border)); - border-radius: calc(var(--radius) - 4px); + border-radius: 0; padding: 0.25rem 0.5rem; cursor: pointer; font-size: 0.75rem; @@ -949,6 +1049,186 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar border-color: hsl(var(--border)); } + /* Invoice details section */ + .invoice-details-section { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid hsl(var(--border)); + } + + /* Invoice amount section - prominent display */ + .invoice-amount-section { + text-align: center; + margin-bottom: 2rem; + padding: 1.5rem; + background-color: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + position: relative; + } + + .invoice-amount-section::before, + .invoice-amount-section::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border: 1px solid hsl(var(--border)); + } + + .invoice-amount-section::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .invoice-amount-section::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + .invoice-amount-label { + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--muted-foreground)); + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .invoice-amount-value { + font-size: 2rem; + font-weight: 700; + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; + color: hsl(var(--foreground)); + line-height: 1.2; + } + + /* Invoice display section */ + .invoice-display-section { + margin-top: 1rem; + } + + .invoice-label { + font-size: 0.875rem; + font-weight: 600; + color: hsl(var(--foreground)); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.75rem; + } + + .invoice-display-container { + background-color: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0; + padding: 1rem; + position: relative; + } + + .invoice-display-container::before, + .invoice-display-container::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + border: 1px solid hsl(var(--border)); + } + + .invoice-display-container::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .invoice-display-container::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + .invoice-textarea { + width: 100%; + background-color: transparent !important; + border: none !important; + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace !important; + font-size: 0.875rem !important; + color: var(--fg-primary) !important; + padding: 0 !important; + margin: 0 !important; + outline: none !important; + word-break: break-all; + overflow-wrap: break-word; + hyphens: auto; + line-height: 1.5; + text-align: left; + resize: none; + min-height: 100px; + height: auto; + overflow: visible; + } + + .invoice-textarea:focus { + box-shadow: none !important; + border: none !important; + } + + /* Dark mode invoice display styling */ + @media (prefers-color-scheme: dark) { + .invoice-amount-section { + background-color: rgba(255, 255, 255, 0.03) !important; + border: none !important; + } + + .invoice-amount-section::before, + .invoice-amount-section::after { + border-color: rgba(255, 255, 255, 0.2); + } + + .invoice-amount-label { + color: var(--text-muted) !important; + } + + .invoice-amount-value { + color: var(--text-primary) !important; + } + + .invoice-label { + color: var(--text-primary) !important; + } + + .invoice-display-container { + background-color: rgba(255, 255, 255, 0.03) !important; + border: none !important; + } + + .invoice-display-container::before, + .invoice-display-container::after { + border-color: rgba(255, 255, 255, 0.2); + } + + .invoice-textarea { + color: var(--text-primary) !important; + } + } + + /* Responsive invoice display */ + @media (max-width: 640px) { + .invoice-amount-value { + font-size: 1.5rem; + } + + .invoice-textarea { + font-size: 0.75rem !important; + line-height: 1.4; + min-height: 80px; + } + } + .balance-item, .balance-item-container { padding: 1.25rem 0; @@ -998,7 +1278,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .alert { border: 1px solid hsl(var(--border)); - border-radius: var(--radius); + border-radius: 0; padding: 1rem; margin-bottom: 1rem; } @@ -1027,7 +1307,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar background-color: hsl(142.1 70.6% 45.3% / 0.1); color: hsl(142.1 76.2% 36.3%); border: 1px solid hsl(142.1 76.2% 36.3%); - border-radius: var(--radius); + border-radius: 0; padding: 1rem; margin-bottom: 1rem; } @@ -1037,7 +1317,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar background-color: hsl(var(--destructive) / 0.1); color: hsl(var(--destructive)); border: 1px solid hsl(var(--destructive)); - border-radius: var(--radius); + border-radius: 0; padding: 1rem; margin-bottom: 1rem; } @@ -1110,13 +1390,44 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .channel-box { + position: relative; background-color: hsl(var(--card)); border: 1px solid hsl(var(--border)); - border-radius: var(--radius); + border-radius: 0; padding: 1.5rem; margin-bottom: 1.5rem; } + .channel-box::before, + .channel-box::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border: 1px solid hsl(var(--border)); + } + + .channel-box::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .channel-box::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + @media (prefers-color-scheme: dark) { + .channel-box::before, + .channel-box::after { + border-color: rgba(255, 255, 255, 0.2); + } + } + .section-header { font-size: 1.25rem; font-weight: 700; @@ -1139,9 +1450,10 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .detail-row { display: flex; - align-items: baseline; - margin-bottom: 0.75rem; - gap: 1rem; + align-items: center; + margin-bottom: 1rem; + gap: 1.5rem; + padding: 0.75rem 0; } .detail-row:last-child { @@ -1150,27 +1462,36 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .detail-label { font-weight: 500; - color: hsl(var(--muted-foreground)); - font-size: 0.875rem; - min-width: 120px; + color: hsl(var(--foreground)); + opacity: 0.5; + font-size: 0.8125rem; + min-width: 140px; flex-shrink: 0; + letter-spacing: 0.025em; + text-transform: uppercase; + text-align: right; } .detail-value { color: hsl(var(--foreground)); font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; font-size: 0.875rem; + font-weight: 400; word-break: break-all; flex: 1; min-width: 0; + letter-spacing: -0.01em; + line-height: 1.5; } .detail-value-amount { color: hsl(var(--foreground)); - font-size: 0.875rem; + font-size: 0.9375rem; + font-weight: 500; word-break: break-all; flex: 1; min-width: 0; + letter-spacing: 0; } .channel-actions { @@ -1212,13 +1533,44 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .balance-item { + position: relative; text-align: center; padding: 1rem; background-color: hsl(var(--muted) / 0.3); - border-radius: calc(var(--radius) - 2px); + border-radius: 0; border: 1px solid hsl(var(--border)); } + .balance-item::before, + .balance-item::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + border: 1px solid hsl(var(--border)); + } + + .balance-item::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .balance-item::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + @media (prefers-color-scheme: dark) { + .balance-item::before, + .balance-item::after { + border-color: rgba(255, 255, 255, 0.2); + } + } + .balance-amount { font-weight: 600; font-size: 1.125rem; @@ -1230,13 +1582,44 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .payment-item { + position: relative; background-color: hsl(var(--card)); border: 1px solid hsl(var(--border)); - border-radius: var(--radius); + border-radius: 0; padding: 1.5rem; margin-bottom: 1.5rem; } + .payment-item::before, + .payment-item::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border: 1px solid hsl(var(--border)); + } + + .payment-item::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .payment-item::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + @media (prefers-color-scheme: dark) { + .payment-item::before, + .payment-item::after { + border-color: rgba(255, 255, 255, 0.2); + } + } + /* Dark mode payment card improvements - match other cards */ @media (prefers-color-scheme: dark) { .payment-item { @@ -1307,18 +1690,23 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .payment-label { - font-weight: 500; - color: hsl(var(--muted-foreground)); - font-size: 0.875rem; + font-weight: 400; + color: var(--text-muted); + font-size: 0.75rem; flex-shrink: 0; + letter-spacing: 0.05em; + text-transform: uppercase; } .payment-value { - color: hsl(var(--foreground)); + color: var(--text-tertiary); font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; - font-size: 0.875rem; + font-size: 0.8125rem; + font-weight: 300; word-break: break-all; min-width: 0; + letter-spacing: -0.02em; + line-height: 1.7; } .payment-list-header { @@ -1353,7 +1741,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar padding: 0.5rem 1rem; border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); - border-radius: calc(var(--radius) - 2px); + border-radius: 0; text-decoration: none; color: hsl(var(--muted-foreground)); font-size: 0.875rem; @@ -1500,7 +1888,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar align-items: center; justify-content: center; white-space: nowrap; - border-radius: calc(var(--radius) - 2px); + border-radius: 0; font-size: 0.875rem; font-weight: 600; transition: all 150ms ease-in-out; @@ -1613,18 +2001,49 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .node-info-main-container { + position: relative; flex: 1; display: flex; flex-direction: column; gap: 1rem; background-color: hsl(var(--card)); border: 1px solid hsl(var(--border)); - border-radius: var(--radius); + border-radius: 0; padding: 1.5rem; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + box-shadow: none; height: 100%; } + .node-info-main-container::before, + .node-info-main-container::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border: 1px solid hsl(var(--border)); + } + + .node-info-main-container::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .node-info-main-container::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + @media (prefers-color-scheme: dark) { + .node-info-main-container::before, + .node-info-main-container::after { + border-color: rgba(255, 255, 255, 0.2); + } + } + .node-info-left { display: flex; align-items: center; @@ -1636,7 +2055,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar flex-shrink: 0; background-color: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); - border-radius: var(--radius); + border-radius: 0; padding: 0.75rem; display: flex; align-items: center; @@ -1648,7 +2067,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .avatar-image { width: 48px; height: 48px; - border-radius: calc(var(--radius) - 2px); + border-radius: 0; object-fit: cover; display: block; } @@ -1670,8 +2089,11 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .node-address { - font-size: 0.875rem; - color: var(--fg-muted); + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; margin: 0; line-height: var(--lh-normal); } @@ -1679,7 +2101,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .node-content-box { background-color: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); - border-radius: var(--radius); + border-radius: 0; min-height: 200px; padding: 1rem; display: flex; @@ -1807,6 +2229,167 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } } + /* Activity Grid Layout - Side by Side */ + .activity-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + margin-top: 1.5rem; + } + + .activity-section { + padding: 2rem 1.5rem; + border-right: 1px solid hsl(var(--border)); + border-top: 1px solid hsl(var(--border)); + } + + .activity-section:last-child { + border-right: none; + } + + .activity-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 2rem; + padding-bottom: 0; + border-bottom: none; + } + + .activity-icon-box { + flex-shrink: 0; + background-color: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + } + + .activity-icon-box svg { + color: hsl(var(--foreground)); + } + + .activity-title { + font-size: 1rem; + font-weight: 400; + color: hsl(var(--foreground)); + margin: 0; + text-transform: none; + letter-spacing: normal; + } + + .activity-metrics { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .activity-metric-card { + position: relative; + text-align: left; + padding: 1rem; + background-color: hsl(var(--muted) / 0.3); + border-radius: 0; + border: 1px solid hsl(var(--border)); + } + + .activity-metric-card::before, + .activity-metric-card::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + border: 1px solid hsl(var(--border)); + } + + .activity-metric-card::before { + top: -1px; + left: -1px; + border-right: none; + border-bottom: none; + } + + .activity-metric-card::after { + bottom: -1px; + right: -1px; + border-left: none; + border-top: none; + } + + .activity-metric-label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.875rem; + font-weight: 400; + color: hsl(var(--muted-foreground)); + text-transform: none; + letter-spacing: normal; + } + + .activity-metric-value { + display: block; + font-size: 1.5rem; + font-weight: 600; + color: hsl(var(--foreground)); + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; + line-height: 1.2; + } + + /* Dark mode activity styling */ + @media (prefers-color-scheme: dark) { + .activity-icon-box { + background-color: rgba(255, 255, 255, 0.03) !important; + border: none !important; + } + + .activity-icon-box svg { + color: var(--text-primary); + } + + .activity-title { + color: var(--text-primary); + } + + .activity-metric-card { + background-color: rgba(255, 255, 255, 0.03) !important; + border: none !important; + } + + .activity-metric-card::before, + .activity-metric-card::after { + border-color: rgba(255, 255, 255, 0.2); + } + + .activity-metric-label { + color: var(--text-muted) !important; + } + + .activity-metric-value { + color: var(--text-primary) !important; + } + } + + /* Responsive activity grid */ + @media (max-width: 768px) { + .activity-grid { + grid-template-columns: 1fr; + } + + .activity-section { + border-right: none; + border-bottom: 1px solid hsl(var(--border)); + padding: 1.5rem 1rem; + } + + .activity-section:last-child { + border-bottom: none; + } + } + /* Responsive typography adjustments */ @media (max-width: 640px) { :root { @@ -1816,6 +2399,87 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar .node-name { font-size: 0.875rem; } + + .activity-metric-value { + font-size: 1.25rem; + } + } + + /* Payment tabs styling */ + .payment-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid hsl(var(--border)); + } + + .payment-tab { + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; + padding: 0.75rem 1.5rem; + border: none; + border-bottom: 2px solid transparent; + background-color: transparent; + border-radius: 0; + text-decoration: none; + color: hsl(var(--muted-foreground)); + font-size: 0.9375rem; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; + position: relative; + margin-bottom: -1px; + } + + .payment-tab:hover { + color: hsl(var(--foreground)); + background-color: hsl(var(--muted) / 0.5); + } + + .payment-tab.active { + color: hsl(var(--foreground)); + border-bottom-color: hsl(var(--foreground)); + background-color: transparent; + } + + /* Dark mode tab styling */ + @media (prefers-color-scheme: dark) { + .payment-tab { + color: var(--text-muted); + } + + .payment-tab:hover { + color: var(--text-secondary); + background-color: rgba(255, 255, 255, 0.05); + } + + .payment-tab.active { + color: var(--text-primary); + border-bottom-color: var(--text-primary); + } + } + + /* Tab content */ + .tab-content { + display: none; + animation: fade-in 0.2s ease-out; + } + + .tab-content.active { + display: block; + } + + @keyframes fade-in { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } } @media (max-width: 480px) { @@ -1893,27 +2557,33 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } .transaction-details .detail-label { - font-weight: 500; - color: hsl(var(--muted-foreground)); - font-size: 0.875rem; + font-weight: 400; + color: var(--text-muted); + font-size: 0.75rem; min-width: 180px; flex-shrink: 0; + letter-spacing: 0.05em; + text-transform: uppercase; } .transaction-details .detail-value { - color: hsl(var(--foreground)); + color: var(--text-tertiary); font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; - font-size: 0.875rem; + font-size: 0.8125rem; + font-weight: 300; word-break: break-all; flex: 1; min-width: 0; + letter-spacing: -0.02em; + line-height: 1.7; } .transaction-details .detail-value-amount { - color: hsl(var(--foreground)); + color: var(--text-secondary); font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; font-size: 1rem; font-weight: 600; + letter-spacing: 0; flex: 1; min-width: 0; } @@ -2056,6 +2726,7 @@ pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Mar } + } } } diff --git a/crates/cdk-ldk-node/src/web/templates/payments.rs b/crates/cdk-ldk-node/src/web/templates/payments.rs index 479d0764..50c518c4 100644 --- a/crates/cdk-ldk-node/src/web/templates/payments.rs +++ b/crates/cdk-ldk-node/src/web/templates/payments.rs @@ -18,6 +18,7 @@ pub fn payment_list_item( "Succeeded" => "status-active", "Failed" => "status-inactive", "Pending" => "status-pending", + "Unpaid" => "status-pending", // Use pending styling for unpaid _ => "status-badge", };