Compare commits
	
		
			9 Commits
		
	
	
		
			d930468748
			...
			print-prev
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ca1ca0c3b1 | ||
|   | 64dbf65beb | ||
|   | 96585440d1 | ||
|   | a297d38276 | ||
|   | 4fdaa9931d | ||
|   | c6c7b38bef | ||
|   | 78db2cc00f | ||
|   | 73d191c5ca | ||
|   | 4cbc495c48 | 
| @@ -30,6 +30,8 @@ web-sys = { version = "0.3", features = [ | |||||||
|     "RequestMode", |     "RequestMode", | ||||||
|     "Response", |     "Response", | ||||||
|     "CssStyleDeclaration", |     "CssStyleDeclaration", | ||||||
|  |     "MediaQueryList", | ||||||
|  |     "MediaQueryListEvent", | ||||||
| ] } | ] } | ||||||
| wasm-bindgen = "0.2" | wasm-bindgen = "0.2" | ||||||
| js-sys = "0.3" | js-sys = "0.3" | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ dist = "dist" | |||||||
| BACKEND_API_URL = "http://localhost:3000/api" | BACKEND_API_URL = "http://localhost:3000/api" | ||||||
|  |  | ||||||
| [watch] | [watch] | ||||||
| watch = ["src", "Cargo.toml", "../calendar-models/src", "styles.css", "index.html"] | watch = ["src", "Cargo.toml", "../calendar-models/src", "styles.css", "print-preview.css", "index.html"] | ||||||
| ignore = ["../backend/", "../target/"] | ignore = ["../backend/", "../target/"] | ||||||
|  |  | ||||||
| [serve] | [serve] | ||||||
|   | |||||||
| @@ -6,8 +6,10 @@ | |||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> |     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|     <base data-trunk-public-url /> |     <base data-trunk-public-url /> | ||||||
|     <link data-trunk rel="css" href="styles.css"> |     <link data-trunk rel="css" href="styles.css"> | ||||||
|  |     <link data-trunk rel="css" href="print-preview.css"> | ||||||
|     <link data-trunk rel="copy-file" href="styles/google.css"> |     <link data-trunk rel="copy-file" href="styles/google.css"> | ||||||
|     <link data-trunk rel="icon" href="favicon.ico"> |     <link data-trunk rel="icon" href="favicon.ico"> | ||||||
|  |     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|     <script> |     <script> | ||||||
|   | |||||||
							
								
								
									
										1215
									
								
								frontend/print-preview.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1215
									
								
								frontend/print-preview.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1702,6 +1702,9 @@ pub fn App() -> Html { | |||||||
|                     on_close={on_mobile_warning_close} |                     on_close={on_mobile_warning_close} | ||||||
|                 /> |                 /> | ||||||
|             </div> |             </div> | ||||||
|  |              | ||||||
|  |             // Hidden print copy that gets shown only during printing | ||||||
|  |             <div id="print-preview-copy" class="print-preview-paper" style="display: none;"></div> | ||||||
|         </BrowserRouter> |         </BrowserRouter> | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| use crate::components::{ | use crate::components::{ | ||||||
|     CalendarHeader, CreateEventModal, EventCreationData, EventModal, MonthView, ViewMode, WeekView, |     CalendarHeader, CreateEventModal, EventCreationData, EventModal, MonthView, PrintPreviewModal, ViewMode, WeekView, | ||||||
| }; | }; | ||||||
| use crate::models::ical::VEvent; | use crate::models::ical::VEvent; | ||||||
| use crate::services::{calendar_service::{UserInfo, ExternalCalendar}, CalendarService}; | use crate::services::{calendar_service::{UserInfo, ExternalCalendar}, CalendarService}; | ||||||
| @@ -389,6 +389,15 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     // Handle print calendar preview | ||||||
|  |     let show_print_preview = use_state(|| false); | ||||||
|  |     let on_print = { | ||||||
|  |         let show_print_preview = show_print_preview.clone(); | ||||||
|  |         Callback::from(move |_: MouseEvent| { | ||||||
|  |             show_print_preview.set(true); | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     // Handle drag-to-create event |     // Handle drag-to-create event | ||||||
|     let on_create_event = { |     let on_create_event = { | ||||||
|         let show_create_modal = show_create_modal.clone(); |         let show_create_modal = show_create_modal.clone(); | ||||||
| @@ -457,6 +466,7 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|                 on_today={on_today} |                 on_today={on_today} | ||||||
|                 time_increment={Some(*time_increment)} |                 time_increment={Some(*time_increment)} | ||||||
|                 on_time_increment_toggle={Some(on_time_increment_toggle)} |                 on_time_increment_toggle={Some(on_time_increment_toggle)} | ||||||
|  |                 on_print={Some(on_print)} | ||||||
|             /> |             /> | ||||||
|  |  | ||||||
|             { |             { | ||||||
| @@ -563,6 +573,32 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|                     }) |                     }) | ||||||
|                 }} |                 }} | ||||||
|             /> |             /> | ||||||
|  |  | ||||||
|  |             // Print preview modal | ||||||
|  |             { | ||||||
|  |                 if *show_print_preview { | ||||||
|  |                     html! { | ||||||
|  |                         <PrintPreviewModal | ||||||
|  |                             on_close={{ | ||||||
|  |                                 let show_print_preview = show_print_preview.clone(); | ||||||
|  |                                 Callback::from(move |_| { | ||||||
|  |                                     show_print_preview.set(false); | ||||||
|  |                                 }) | ||||||
|  |                             }} | ||||||
|  |                             view_mode={props.view.clone()} | ||||||
|  |                             current_date={*current_date} | ||||||
|  |                             selected_date={*selected_date} | ||||||
|  |                             events={(*events).clone()} | ||||||
|  |                             user_info={props.user_info.clone()} | ||||||
|  |                             external_calendars={props.external_calendars.clone()} | ||||||
|  |                             time_increment={*time_increment} | ||||||
|  |                             today={today} | ||||||
|  |                         /> | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     html! {} | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         </div> |         </div> | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ pub struct CalendarHeaderProps { | |||||||
|     pub time_increment: Option<u32>, |     pub time_increment: Option<u32>, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     pub on_time_increment_toggle: Option<Callback<MouseEvent>>, |     pub on_time_increment_toggle: Option<Callback<MouseEvent>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_print: Option<Callback<MouseEvent>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[function_component(CalendarHeader)] | #[function_component(CalendarHeader)] | ||||||
| @@ -39,6 +41,17 @@ pub fn calendar_header(props: &CalendarHeaderProps) -> Html { | |||||||
|                         html! {} |                         html! {} | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |                 { | ||||||
|  |                     if let Some(print_callback) = &props.on_print { | ||||||
|  |                         html! { | ||||||
|  |                             <button class="print-button" onclick={print_callback.clone()} title="Print Calendar"> | ||||||
|  |                                 <i class="fas fa-print"></i> | ||||||
|  |                             </button> | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         html! {} | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             </div> |             </div> | ||||||
|             <h2 class="month-year">{title}</h2> |             <h2 class="month-year">{title}</h2> | ||||||
|             <div class="header-right"> |             <div class="header-right"> | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ pub mod external_calendar_modal; | |||||||
| pub mod login; | pub mod login; | ||||||
| pub mod mobile_warning_modal; | pub mod mobile_warning_modal; | ||||||
| pub mod month_view; | pub mod month_view; | ||||||
|  | pub mod print_preview_modal; | ||||||
| pub mod recurring_edit_modal; | pub mod recurring_edit_modal; | ||||||
| pub mod route_handler; | pub mod route_handler; | ||||||
| pub mod sidebar; | pub mod sidebar; | ||||||
| @@ -32,6 +33,7 @@ pub use event_modal::EventModal; | |||||||
| pub use login::Login; | pub use login::Login; | ||||||
| pub use mobile_warning_modal::MobileWarningModal; | pub use mobile_warning_modal::MobileWarningModal; | ||||||
| pub use month_view::MonthView; | pub use month_view::MonthView; | ||||||
|  | pub use print_preview_modal::PrintPreviewModal; | ||||||
| pub use recurring_edit_modal::{RecurringEditAction, RecurringEditModal}; | pub use recurring_edit_modal::{RecurringEditAction, RecurringEditModal}; | ||||||
| pub use route_handler::RouteHandler; | pub use route_handler::RouteHandler; | ||||||
| pub use sidebar::{Sidebar, Theme, ViewMode}; | pub use sidebar::{Sidebar, Theme, ViewMode}; | ||||||
|   | |||||||
							
								
								
									
										362
									
								
								frontend/src/components/print_preview_modal.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								frontend/src/components/print_preview_modal.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,362 @@ | |||||||
|  | use crate::components::{ViewMode, WeekView, MonthView}; | ||||||
|  | use crate::models::ical::VEvent; | ||||||
|  | use crate::services::calendar_service::{UserInfo, ExternalCalendar}; | ||||||
|  | use chrono::NaiveDate; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use wasm_bindgen::{closure::Closure, JsCast}; | ||||||
|  | use yew::prelude::*; | ||||||
|  |  | ||||||
|  | #[derive(Properties, PartialEq)] | ||||||
|  | pub struct PrintPreviewModalProps { | ||||||
|  |     pub on_close: Callback<()>, | ||||||
|  |     pub view_mode: ViewMode, | ||||||
|  |     pub current_date: NaiveDate, | ||||||
|  |     pub selected_date: NaiveDate, | ||||||
|  |     pub events: HashMap<NaiveDate, Vec<VEvent>>, | ||||||
|  |     pub user_info: Option<UserInfo>, | ||||||
|  |     pub external_calendars: Vec<ExternalCalendar>, | ||||||
|  |     pub time_increment: u32, | ||||||
|  |     pub today: NaiveDate, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[function_component] | ||||||
|  | pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html { | ||||||
|  |     let start_hour = use_state(|| 6u32); | ||||||
|  |     let end_hour = use_state(|| 22u32); | ||||||
|  |     let zoom_level = use_state(|| 0.4f64); // Default 40% zoom | ||||||
|  |  | ||||||
|  |     let close_modal = { | ||||||
|  |         let on_close = props.on_close.clone(); | ||||||
|  |         Callback::from(move |_| { | ||||||
|  |             on_close.emit(()); | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let backdrop_click = { | ||||||
|  |         let on_close = props.on_close.clone(); | ||||||
|  |         Callback::from(move |e: MouseEvent| { | ||||||
|  |             if e.target() == e.current_target() { | ||||||
|  |                 on_close.emit(()); | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let on_start_hour_change = { | ||||||
|  |         let start_hour = start_hour.clone(); | ||||||
|  |         let end_hour = end_hour.clone(); | ||||||
|  |         Callback::from(move |e: Event| { | ||||||
|  |             let target = e.target_dyn_into::<web_sys::HtmlSelectElement>(); | ||||||
|  |             if let Some(select) = target { | ||||||
|  |                 if let Ok(hour) = select.value().parse::<u32>() { | ||||||
|  |                     if hour < *end_hour { | ||||||
|  |                         start_hour.set(hour); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let on_end_hour_change = { | ||||||
|  |         let start_hour = start_hour.clone(); | ||||||
|  |         let end_hour = end_hour.clone(); | ||||||
|  |         Callback::from(move |e: Event| { | ||||||
|  |             let target = e.target_dyn_into::<web_sys::HtmlSelectElement>(); | ||||||
|  |             if let Some(select) = target { | ||||||
|  |                 if let Ok(hour) = select.value().parse::<u32>() { | ||||||
|  |                     if hour > *start_hour && hour <= 24 { | ||||||
|  |                         end_hour.set(hour); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     let format_hour = |hour: u32| -> String { | ||||||
|  |         if hour == 0 { | ||||||
|  |             "12 AM".to_string() | ||||||
|  |         } else if hour < 12 { | ||||||
|  |             format!("{} AM", hour) | ||||||
|  |         } else if hour == 12 { | ||||||
|  |             "12 PM".to_string() | ||||||
|  |         } else { | ||||||
|  |             format!("{} PM", hour - 12) | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Calculate dynamic base unit for print preview | ||||||
|  |     let calculate_print_dimensions = |start_hour: u32, end_hour: u32, time_increment: u32| -> (f64, f64, f64) { | ||||||
|  |         let visible_hours = (end_hour - start_hour) as f64; | ||||||
|  |         let slots_per_hour = if time_increment == 15 { 4.0 } else { 2.0 }; | ||||||
|  |         let header_height = 50.0; // Fixed week header height in print preview | ||||||
|  |         let header_border = 2.0;   // Week header bottom border (2px solid) | ||||||
|  |         let container_spacing = 8.0; // Additional container spacing/margins | ||||||
|  |         let total_overhead = header_height + header_border + container_spacing; | ||||||
|  |         let available_height = 720.0 - total_overhead; // Available for time content | ||||||
|  |         let base_unit = available_height / (visible_hours * slots_per_hour); | ||||||
|  |         let pixels_per_hour = base_unit * slots_per_hour; | ||||||
|  |          | ||||||
|  |         (base_unit, pixels_per_hour, available_height) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Calculate print dimensions for the current hour range | ||||||
|  |     let (base_unit, pixels_per_hour, _available_height) = calculate_print_dimensions(*start_hour, *end_hour, props.time_increment); | ||||||
|  |  | ||||||
|  |     // Effect to update print copy whenever modal renders or content changes | ||||||
|  |     { | ||||||
|  |         let start_hour = *start_hour; | ||||||
|  |         let end_hour = *end_hour; | ||||||
|  |         let time_increment = props.time_increment; | ||||||
|  |         let original_base_unit = base_unit; | ||||||
|  |         use_effect(move || { | ||||||
|  |             if let Some(window) = web_sys::window() { | ||||||
|  |                 if let Some(document) = window.document() { | ||||||
|  |                     // Set CSS variables on document root | ||||||
|  |                     if let Some(document_element) = document.document_element() { | ||||||
|  |                         if let Some(html_element) = document_element.dyn_ref::<web_sys::HtmlElement>() { | ||||||
|  |                             let style = html_element.style(); | ||||||
|  |                             let _ = style.set_property("--print-start-hour", &start_hour.to_string()); | ||||||
|  |                             let _ = style.set_property("--print-end-hour", &end_hour.to_string()); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     // Copy content from print-preview-content to the hidden print-preview-copy div | ||||||
|  |                     let copy_content = move || { | ||||||
|  |                         if let Some(preview_content) = document.query_selector(".print-preview-content").ok().flatten() { | ||||||
|  |                             if let Some(print_copy) = document.get_element_by_id("print-preview-copy") { | ||||||
|  |                                 // Clone the preview content | ||||||
|  |                                 if let Some(content_clone) = preview_content.clone_node_with_deep(true).ok() { | ||||||
|  |                                     // Clear the print copy div and add the cloned content | ||||||
|  |                                     print_copy.set_inner_html(""); | ||||||
|  |                                     let _ = print_copy.append_child(&content_clone); | ||||||
|  |                                      | ||||||
|  |                                     // Get the actual rendered height of the print copy div and recalculate base-unit | ||||||
|  |                                     if let Some(print_copy_html) = print_copy.dyn_ref::<web_sys::HtmlElement>() { | ||||||
|  |                                         // Temporarily make visible to measure height, then hide again | ||||||
|  |                                         let original_display = print_copy_html.style().get_property_value("display").unwrap_or_default(); | ||||||
|  |                                         let _ = print_copy_html.style().set_property("display", "block"); | ||||||
|  |                                         let _ = print_copy_html.style().set_property("visibility", "hidden"); | ||||||
|  |                                         let _ = print_copy_html.style().set_property("position", "absolute"); | ||||||
|  |                                         let _ = print_copy_html.style().set_property("top", "-9999px"); | ||||||
|  |                                          | ||||||
|  |                                         // Now measure the height | ||||||
|  |                                         let actual_height = print_copy_html.client_height() as f64; | ||||||
|  |                                          | ||||||
|  |                                         // Restore original display | ||||||
|  |                                         let _ = print_copy_html.style().set_property("display", &original_display); | ||||||
|  |                                         let _ = print_copy_html.style().remove_property("visibility"); | ||||||
|  |                                         let _ = print_copy_html.style().remove_property("position"); | ||||||
|  |                                         let _ = print_copy_html.style().remove_property("top"); | ||||||
|  |                                          | ||||||
|  |                                         // Recalculate base-unit and pixels-per-hour based on actual height | ||||||
|  |                                         let visible_hours = (end_hour - start_hour) as f64; | ||||||
|  |                                         let slots_per_hour = if time_increment == 15 { 4.0 } else { 2.0 }; | ||||||
|  |                                         let header_height = 50.0; | ||||||
|  |                                         let header_border = 2.0; | ||||||
|  |                                         let container_spacing = 8.0; | ||||||
|  |                                         let total_overhead = header_height + header_border + container_spacing; | ||||||
|  |                                         let available_height = actual_height - total_overhead; | ||||||
|  |                                         let actual_base_unit = available_height / (visible_hours * slots_per_hour); | ||||||
|  |                                         let actual_pixels_per_hour = actual_base_unit * slots_per_hour; | ||||||
|  |                                          | ||||||
|  |                                          | ||||||
|  |                                         // Set CSS variables with recalculated values | ||||||
|  |                                         let style = print_copy_html.style(); | ||||||
|  |                                         let _ = style.set_property("--print-base-unit", &format!("{:.2}", actual_base_unit)); | ||||||
|  |                                         let _ = style.set_property("--print-pixels-per-hour", &format!("{:.2}", actual_pixels_per_hour)); | ||||||
|  |                                         let _ = style.set_property("--print-start-hour", &start_hour.to_string()); | ||||||
|  |                                         let _ = style.set_property("--print-end-hour", &end_hour.to_string()); | ||||||
|  |                                          | ||||||
|  |                                         // Copy data attributes | ||||||
|  |                                         let _ = print_copy.set_attribute("data-start-hour", &start_hour.to_string()); | ||||||
|  |                                         let _ = print_copy.set_attribute("data-end-hour", &end_hour.to_string()); | ||||||
|  |                                          | ||||||
|  |                                         // Recalculate event positions using the new base-unit | ||||||
|  |                                         let events = print_copy.query_selector_all(".week-event").unwrap(); | ||||||
|  |                                         let scale_factor = actual_base_unit / original_base_unit; | ||||||
|  |                                          | ||||||
|  |                                         for i in 0..events.length() { | ||||||
|  |                                             if let Some(event_element) = events.get(i) { | ||||||
|  |                                                 if let Some(event_html) = event_element.dyn_ref::<web_sys::HtmlElement>() { | ||||||
|  |                                                     let event_style = event_html.style(); | ||||||
|  |                                                      | ||||||
|  |                                                     // Get current positioning values and recalculate | ||||||
|  |                                                     if let Ok(current_top) = event_style.get_property_value("top") { | ||||||
|  |                                                         if current_top.ends_with("px") { | ||||||
|  |                                                             if let Ok(top_px) = current_top[..current_top.len()-2].parse::<f64>() { | ||||||
|  |                                                                 let new_top = top_px * scale_factor; | ||||||
|  |                                                                 let _ = event_style.set_property("top", &format!("{:.2}px", new_top)); | ||||||
|  |                                                             } | ||||||
|  |                                                         } | ||||||
|  |                                                     } | ||||||
|  |                                                      | ||||||
|  |                                                     if let Ok(current_height) = event_style.get_property_value("height") { | ||||||
|  |                                                         if current_height.ends_with("px") { | ||||||
|  |                                                             if let Ok(height_px) = current_height[..current_height.len()-2].parse::<f64>() { | ||||||
|  |                                                                 let new_height = height_px * scale_factor; | ||||||
|  |                                                                 let _ = event_style.set_property("height", &format!("{:.2}px", new_height)); | ||||||
|  |                                                             } | ||||||
|  |                                                         } | ||||||
|  |                                                     } | ||||||
|  |                                                 } | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|  |                                          | ||||||
|  |                                         web_sys::console::log_1(&format!("Height: {:.2}, Original base-unit: {:.2}, New base-unit: {:.2}, Scale factor: {:.2}",  | ||||||
|  |                                             actual_height, original_base_unit, actual_base_unit, scale_factor).into()); | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     }; | ||||||
|  |  | ||||||
|  |                     // Copy content immediately | ||||||
|  |                     copy_content(); | ||||||
|  |  | ||||||
|  |                     // Also set up a small delay to catch any async rendering | ||||||
|  |                     let copy_callback = Closure::wrap(Box::new(copy_content) as Box<dyn FnMut()>); | ||||||
|  |                     let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0( | ||||||
|  |                         copy_callback.as_ref().unchecked_ref(), | ||||||
|  |                         100 | ||||||
|  |                     ); | ||||||
|  |                     copy_callback.forget(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             || () | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     let on_print = { | ||||||
|  |         Callback::from(move |_: MouseEvent| { | ||||||
|  |             if let Some(window) = web_sys::window() { | ||||||
|  |                 // Print copy is already updated by the use_effect, just trigger print | ||||||
|  |                 let _ = window.print(); | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     html! { | ||||||
|  |         <div class="modal-backdrop print-preview-modal-backdrop" onclick={backdrop_click}> | ||||||
|  |             <div class="modal-content print-preview-modal"> | ||||||
|  |                 <div class="modal-header"> | ||||||
|  |                     <h3>{"Print Preview"}</h3> | ||||||
|  |                     <button class="modal-close" onclick={close_modal.clone()}>{"×"}</button> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="modal-body print-preview-body"> | ||||||
|  |                     <div class="print-preview-controls"> | ||||||
|  |                         { | ||||||
|  |                             if props.view_mode == ViewMode::Week { | ||||||
|  |                                 html! { | ||||||
|  |                                     <> | ||||||
|  |                                         <div class="control-group"> | ||||||
|  |                                             <label for="start-hour">{"Start Hour:"}</label> | ||||||
|  |                                             <select id="start-hour" onchange={on_start_hour_change}> | ||||||
|  |                                                 { | ||||||
|  |                                                     (0..24).map(|hour| { | ||||||
|  |                                                         html! { | ||||||
|  |                                                             <option value={hour.to_string()} selected={hour == *start_hour}> | ||||||
|  |                                                                 {format_hour(hour)} | ||||||
|  |                                                             </option> | ||||||
|  |                                                         } | ||||||
|  |                                                     }).collect::<Html>() | ||||||
|  |                                                 } | ||||||
|  |                                             </select> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="control-group"> | ||||||
|  |                                             <label for="end-hour">{"End Hour:"}</label> | ||||||
|  |                                             <select id="end-hour" onchange={on_end_hour_change}> | ||||||
|  |                                                 { | ||||||
|  |                                                     (1..=24).map(|hour| { | ||||||
|  |                                                         html! { | ||||||
|  |                                                             <option value={hour.to_string()} selected={hour == *end_hour}> | ||||||
|  |                                                                 {if hour == 24 { "12 AM".to_string() } else { format_hour(hour) }} | ||||||
|  |                                                             </option> | ||||||
|  |                                                         } | ||||||
|  |                                                     }).collect::<Html>() | ||||||
|  |                                                 } | ||||||
|  |                                             </select> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="hour-range-info"> | ||||||
|  |                                             {format!("Will print from {} to {}",  | ||||||
|  |                                                 format_hour(*start_hour),  | ||||||
|  |                                                 if *end_hour == 24 { "12 AM".to_string() } else { format_hour(*end_hour) } | ||||||
|  |                                             )} | ||||||
|  |                                         </div> | ||||||
|  |                                     </> | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 html! { | ||||||
|  |                                     <div class="month-info"> | ||||||
|  |                                         {"Will print entire month view"} | ||||||
|  |                                     </div> | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         <div class="zoom-display-info"> | ||||||
|  |                             <label>{"Zoom: "}</label> | ||||||
|  |                             <span>{format!("{}%", (*zoom_level * 100.0) as i32)}</span> | ||||||
|  |                             <span class="zoom-hint">{"(scroll to zoom)"}</span> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="preview-actions"> | ||||||
|  |                             <button class="btn-primary" onclick={on_print}>{"Print"}</button> | ||||||
|  |                             <button class="btn-secondary" onclick={close_modal}>{"Cancel"}</button> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="print-preview-display" onwheel={{ | ||||||
|  |                         let zoom_level = zoom_level.clone(); | ||||||
|  |                         Callback::from(move |e: WheelEvent| { | ||||||
|  |                             e.prevent_default(); // Prevent page scroll | ||||||
|  |                             let delta_y = e.delta_y(); | ||||||
|  |                             let zoom_change = if delta_y < 0.0 { 1.1 } else { 1.0 / 1.1 }; | ||||||
|  |                             let new_zoom = (*zoom_level * zoom_change).clamp(0.2, 1.5); | ||||||
|  |                             zoom_level.set(new_zoom); | ||||||
|  |                         }) | ||||||
|  |                     }}> | ||||||
|  |                         <div class="print-preview-paper"  | ||||||
|  |                                      data-start-hour={start_hour.to_string()}  | ||||||
|  |                                      data-end-hour={end_hour.to_string()} | ||||||
|  |                                      style={format!( | ||||||
|  |                                          "--print-start-hour: {}; --print-end-hour: {}; --print-base-unit: {:.2}; --print-pixels-per-hour: {:.2}; transform: scale({}); transform-origin: top center;",  | ||||||
|  |                                          *start_hour, *end_hour, base_unit, pixels_per_hour, *zoom_level | ||||||
|  |                                      )}> | ||||||
|  |                             <div class="print-preview-content"> | ||||||
|  |                                 { | ||||||
|  |                                     match props.view_mode { | ||||||
|  |                                         ViewMode::Week => html! { | ||||||
|  |                                             <WeekView | ||||||
|  |                                                 key={format!("week-preview-{}-{}", *start_hour, *end_hour)} | ||||||
|  |                                                 current_date={props.current_date} | ||||||
|  |                                                 today={props.today} | ||||||
|  |                                                 events={props.events.clone()} | ||||||
|  |                                                 on_event_click={Callback::noop()} | ||||||
|  |                                                 user_info={props.user_info.clone()} | ||||||
|  |                                                 external_calendars={props.external_calendars.clone()} | ||||||
|  |                                                 time_increment={props.time_increment} | ||||||
|  |                                                 print_mode={true} | ||||||
|  |                                                 print_pixels_per_hour={Some(pixels_per_hour)} | ||||||
|  |                                                 print_start_hour={Some(*start_hour)} | ||||||
|  |                                             /> | ||||||
|  |                                         }, | ||||||
|  |                                         ViewMode::Month => html! { | ||||||
|  |                                             <MonthView | ||||||
|  |                                                 key={format!("month-preview-{}-{}", *start_hour, *end_hour)} | ||||||
|  |                                                 current_month={props.current_date} | ||||||
|  |                                                 selected_date={Some(props.selected_date)} | ||||||
|  |                                                 today={props.today} | ||||||
|  |                                                 events={props.events.clone()} | ||||||
|  |                                                 on_day_select={None::<Callback<NaiveDate>>} | ||||||
|  |                                                 on_event_click={Callback::noop()} | ||||||
|  |                                                 user_info={props.user_info.clone()} | ||||||
|  |                                                 external_calendars={props.external_calendars.clone()} | ||||||
|  |                                             /> | ||||||
|  |                                         }, | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -350,9 +350,9 @@ pub fn sidebar(props: &SidebarProps) -> Html { | |||||||
|                                                         > |                                                         > | ||||||
|                                                             { |                                                             { | ||||||
|                                                                 if props.refreshing_calendar_id == Some(cal.id) { |                                                                 if props.refreshing_calendar_id == Some(cal.id) { | ||||||
|                                                                     "⏳" // Loading spinner |                                                                     html! { <i class="fas fa-spinner fa-spin"></i> } | ||||||
|                                                                 } else { |                                                                 } else { | ||||||
|                                                                     "🔄" // Normal refresh icon |                                                                     html! { <i class="fas fa-sync-alt"></i> } | ||||||
|                                                                 } |                                                                 } | ||||||
|                                                             } |                                                             } | ||||||
|                                                         </button> |                                                         </button> | ||||||
|   | |||||||
| @@ -42,6 +42,12 @@ pub struct WeekViewProps { | |||||||
|     pub context_menus_open: bool, |     pub context_menus_open: bool, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     pub time_increment: u32, |     pub time_increment: u32, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub print_mode: bool, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub print_pixels_per_hour: Option<f64>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub print_start_hour: Option<u32>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Clone, PartialEq)] | #[derive(Clone, PartialEq)] | ||||||
| @@ -438,13 +444,13 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                     // Time labels |                     // Time labels | ||||||
|                     <div class={classes!("time-labels", if props.time_increment == 15 { Some("quarter-mode") } else { None })}> |                     <div class={classes!("time-labels", if props.time_increment == 15 { Some("quarter-mode") } else { None })}> | ||||||
|                         { |                         { | ||||||
|                             time_labels.iter().map(|time| { |                             time_labels.iter().enumerate().map(|(hour, time)| { | ||||||
|                                 let is_quarter_mode = props.time_increment == 15; |                                 let is_quarter_mode = props.time_increment == 15; | ||||||
|                                 html! { |                                 html! { | ||||||
|                                     <div class={classes!( |                                     <div class={classes!( | ||||||
|                                         "time-label", |                                         "time-label", | ||||||
|                                         if is_quarter_mode { Some("quarter-mode") } else { None } |                                         if is_quarter_mode { Some("quarter-mode") } else { None } | ||||||
|                                     )}> |                                     )} data-hour={hour.to_string()}> | ||||||
|                                         {time} |                                         {time} | ||||||
|                                     </div> |                                     </div> | ||||||
|                                 } |                                 } | ||||||
| @@ -701,10 +707,10 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                     > |                                     > | ||||||
|                                         // Time slot backgrounds - 24 hour slots to represent full day |                                         // Time slot backgrounds - 24 hour slots to represent full day | ||||||
|                                         { |                                         { | ||||||
|                                             (0..24).map(|_hour| { |                                             (0..24).map(|hour| { | ||||||
|                                                 let slots_per_hour = 60 / props.time_increment; |                                                 let slots_per_hour = 60 / props.time_increment; | ||||||
|                                                 html! { |                                                 html! { | ||||||
|                                                     <div class={classes!("time-slot", if props.time_increment == 15 { Some("quarter-mode") } else { None })}> |                                                     <div class={classes!("time-slot", if props.time_increment == 15 { Some("quarter-mode") } else { None })} data-hour={hour.to_string()}> | ||||||
|                                                         { |                                                         { | ||||||
|                                                             (0..slots_per_hour).map(|_slot| { |                                                             (0..slots_per_hour).map(|_slot| { | ||||||
|                                                                 let slot_class = if props.time_increment == 15 { |                                                                 let slot_class = if props.time_increment == 15 { | ||||||
| @@ -726,7 +732,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                         <div class="events-container"> |                                         <div class="events-container"> | ||||||
|                                             { |                                             { | ||||||
|                                                 day_events.iter().enumerate().filter_map(|(event_idx, event)| { |                                                 day_events.iter().enumerate().filter_map(|(event_idx, event)| { | ||||||
|                                                     let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date, props.time_increment); |                                                     let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour); | ||||||
|  |  | ||||||
|                                                     // Skip all-day events (they're rendered in the header) |                                                     // Skip all-day events (they're rendered in the header) | ||||||
|                                                     if is_all_day { |                                                     if is_all_day { | ||||||
| @@ -755,6 +761,8 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                         let event_for_drag = event.clone(); |                                                         let event_for_drag = event.clone(); | ||||||
|                                                         let date_for_drag = *date; |                                                         let date_for_drag = *date; | ||||||
|                                                         let time_increment = props.time_increment; |                                                         let time_increment = props.time_increment; | ||||||
|  |                                                         let print_pixels_per_hour = props.print_pixels_per_hour; | ||||||
|  |                                                         let print_start_hour = props.print_start_hour; | ||||||
|                                                         Callback::from(move |e: MouseEvent| { |                                                         Callback::from(move |e: MouseEvent| { | ||||||
|                                                             e.stop_propagation(); // Prevent drag-to-create from starting on event clicks |                                                             e.stop_propagation(); // Prevent drag-to-create from starting on event clicks | ||||||
|  |  | ||||||
| @@ -768,7 +776,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                             let click_y_relative = if click_y_relative > 0.0 { click_y_relative } else { e.offset_y() as f64 }; |                                                             let click_y_relative = if click_y_relative > 0.0 { click_y_relative } else { e.offset_y() as f64 }; | ||||||
|  |  | ||||||
|                                                             // Get event's current position in day column coordinates |                                                             // Get event's current position in day column coordinates | ||||||
|                                                             let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag, time_increment); |                                                             let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag, time_increment, print_pixels_per_hour, print_start_hour); | ||||||
|                                                             let event_start_pixels = event_start_pixels as f64; |                                                             let event_start_pixels = event_start_pixels as f64; | ||||||
|  |  | ||||||
|                                                             // Convert click position to day column coordinates |                                                             // Convert click position to day column coordinates | ||||||
| @@ -1054,7 +1062,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                             }; |                                                             }; | ||||||
|  |  | ||||||
|                                                             // Calculate positions for the preview |                                                             // Calculate positions for the preview | ||||||
|                                                             let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment); |                                                             let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour); | ||||||
|                                                             let original_duration = original_end.signed_duration_since(event.dtstart.with_timezone(&chrono::Local).naive_local()); |                                                             let original_duration = original_end.signed_duration_since(event.dtstart.with_timezone(&chrono::Local).naive_local()); | ||||||
|                                                             let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32); |                                                             let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32); | ||||||
|  |  | ||||||
| @@ -1084,7 +1092,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                             let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local(); |                                                             let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local(); | ||||||
|  |  | ||||||
|                                                             // Calculate positions for the preview |                                                             // Calculate positions for the preview | ||||||
|                                                             let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment); |                                                             let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour); | ||||||
|  |  | ||||||
|                                                             let new_end_pixels = drag.current_y; |                                                             let new_end_pixels = drag.current_y; | ||||||
|                                                             let new_height = (new_end_pixels - original_start_pixels as f64).max(20.0); |                                                             let new_height = (new_end_pixels - original_start_pixels as f64).max(20.0); | ||||||
| @@ -1218,7 +1226,7 @@ fn pixels_to_time(pixels: f64, time_increment: u32) -> NaiveTime { | |||||||
|     NaiveTime::from_hms_opt(hours, minutes, 0).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) |     NaiveTime::from_hms_opt(hours, minutes, 0).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) | ||||||
| } | } | ||||||
|  |  | ||||||
| fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32) -> (f32, f32, bool) { | fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32, print_pixels_per_hour: Option<f64>, print_start_hour: Option<u32>) -> (f32, f32, bool) { | ||||||
|     // Convert UTC times to local time for display |     // Convert UTC times to local time for display | ||||||
|     let local_start = event.dtstart.with_timezone(&Local); |     let local_start = event.dtstart.with_timezone(&Local); | ||||||
|     let event_date = local_start.date_naive(); |     let event_date = local_start.date_naive(); | ||||||
| @@ -1238,11 +1246,23 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32 | |||||||
|         return (0.0, 30.0, true); // Position at top, 30px height, is_all_day = true |         return (0.0, 30.0, true); // Position at top, 30px height, is_all_day = true | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Calculate start position in pixels from midnight |     // Calculate start position in pixels | ||||||
|     let start_hour = local_start.hour() as f32; |     let start_hour = local_start.hour() as f32; | ||||||
|     let start_minute = local_start.minute() as f32; |     let start_minute = local_start.minute() as f32; | ||||||
|     let pixels_per_hour = if time_increment == 15 { 120.0 } else { 60.0 }; |     let pixels_per_hour = if let Some(print_pph) = print_pixels_per_hour { | ||||||
|     let start_pixels = (start_hour + start_minute / 60.0) * pixels_per_hour; |         print_pph as f32 // Use the dynamic print mode calculation | ||||||
|  |     } else { | ||||||
|  |         if time_increment == 15 { 120.0 } else { 60.0 } // Default values | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     // In print mode, offset by the start hour to show relative position within visible range | ||||||
|  |     let hour_offset = if let Some(print_start) = print_start_hour { | ||||||
|  |         print_start as f32 | ||||||
|  |     } else { | ||||||
|  |         0.0 // No offset for normal view (starts at midnight) | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     let start_pixels = ((start_hour + start_minute / 60.0) - hour_offset) * pixels_per_hour; | ||||||
|  |  | ||||||
|     // Calculate duration and height |     // Calculate duration and height | ||||||
|     let duration_pixels = if let Some(end) = event.dtend { |     let duration_pixels = if let Some(end) = event.dtend { | ||||||
| @@ -1251,19 +1271,19 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32 | |||||||
|  |  | ||||||
|         // Handle events that span multiple days by capping at midnight |         // Handle events that span multiple days by capping at midnight | ||||||
|         if end_date > date { |         if end_date > date { | ||||||
|             // Event continues past midnight, cap at 24:00  |             // Event continues past midnight, cap at end of visible range | ||||||
|             let max_pixels = 24.0 * pixels_per_hour; |             let max_hour = if let Some(_print_start) = print_start_hour { 24.0 } else { 24.0 }; | ||||||
|             max_pixels - start_pixels |             let max_pixels = (max_hour - hour_offset) * pixels_per_hour; | ||||||
|  |             (max_pixels - start_pixels).max(20.0) | ||||||
|         } else { |         } else { | ||||||
|             let end_hour = local_end.hour() as f32; |             let end_hour = local_end.hour() as f32; | ||||||
|             let end_minute = local_end.minute() as f32; |             let end_minute = local_end.minute() as f32; | ||||||
|             let end_pixels = (end_hour + end_minute / 60.0) * pixels_per_hour; |             let end_pixels = ((end_hour + end_minute / 60.0) - hour_offset) * pixels_per_hour; | ||||||
|             (end_pixels - start_pixels).max(20.0) // Minimum 20px height |             (end_pixels - start_pixels).max(20.0) // Minimum 20px height | ||||||
|         } |         } | ||||||
|     } else { |     } else { | ||||||
|         pixels_per_hour // Default 1 hour if no end time |         pixels_per_hour // Default 1 hour if no end time | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     (start_pixels, duration_pixels, false) // is_all_day = false |     (start_pixels, duration_pixels, false) // is_all_day = false | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -1304,7 +1324,7 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3 | |||||||
|                 return None; |                 return None; | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             let (_, _, _) = calculate_event_position(event, date, time_increment); |             let (_, _, _) = calculate_event_position(event, date, time_increment, None, None); | ||||||
|             let local_start = event.dtstart.with_timezone(&Local); |             let local_start = event.dtstart.with_timezone(&Local); | ||||||
|             let event_date = local_start.date_naive(); |             let event_date = local_start.date_naive(); | ||||||
|             if event_date == date ||  |             if event_date == date ||  | ||||||
|   | |||||||
| @@ -18,19 +18,10 @@ | |||||||
|     --shadow-sm: 0 1px 3px rgba(0,0,0,0.1); |     --shadow-sm: 0 1px 3px rgba(0,0,0,0.1); | ||||||
|     --shadow-md: 0 4px 6px rgba(0,0,0,0.1); |     --shadow-md: 0 4px 6px rgba(0,0,0,0.1); | ||||||
|     --shadow-lg: 0 8px 25px rgba(0,0,0,0.15); |     --shadow-lg: 0 8px 25px rgba(0,0,0,0.15); | ||||||
|     --border-light: 1px solid #e9ecef; |  | ||||||
|     --border-medium: 1px solid #dee2e6; |  | ||||||
|     --transition-fast: 0.15s ease; |     --transition-fast: 0.15s ease; | ||||||
|     --transition-normal: 0.2s ease; |     --transition-normal: 0.2s ease; | ||||||
|     --transition-slow: 0.3s ease; |     --transition-slow: 0.3s ease; | ||||||
|      |      | ||||||
|     /* Common Glass/Glassmorphism Effects */ |  | ||||||
|     --glass-bg: var(--glass-bg); |  | ||||||
|     --glass-bg-light: var(--glass-bg-light); |  | ||||||
|     --glass-bg-lighter: var(--glass-bg-lighter); |  | ||||||
|     --glass-border: 1px solid var(--glass-bg-light); |  | ||||||
|     --glass-border-light: 1px solid var(--glass-bg-lighter); |  | ||||||
|      |  | ||||||
|     /* Standard Control Dimensions */ |     /* Standard Control Dimensions */ | ||||||
|     --control-height: 40px; |     --control-height: 40px; | ||||||
|     --control-padding: 0.875rem; |     --control-padding: 0.875rem; | ||||||
| @@ -39,12 +30,54 @@ | |||||||
|      |      | ||||||
|     /* Common Transition */ |     /* Common Transition */ | ||||||
|     --standard-transition: all 0.2s ease; |     --standard-transition: all 0.2s ease; | ||||||
|  |      | ||||||
|  |     /* Default Light Theme Colors */ | ||||||
|  |     --background-primary: #f8f9fa; | ||||||
|  |     --background-secondary: #ffffff; | ||||||
|  |     --background-tertiary: #f1f3f4; | ||||||
|  |     --text-primary: #333333; | ||||||
|  |     --text-secondary: #6c757d; | ||||||
|  |     --text-inverse: #ffffff; | ||||||
|  |     --border-primary: #e9ecef; | ||||||
|  |     --border-secondary: #dee2e6; | ||||||
|  |     --border-light: #f8f9fa; | ||||||
|  |     --error-color: #dc3545; | ||||||
|  |     --success-color: #28a745; | ||||||
|  |     --warning-color: #ffc107; | ||||||
|  |     --info-color: #17a2b8; | ||||||
|  |      | ||||||
|  |     /* Modal Colors */ | ||||||
|  |     --modal-background: #ffffff; | ||||||
|  |     --modal-text: #333333; | ||||||
|  |     --modal-header-background: #ffffff; | ||||||
|  |     --modal-header-border: #e5e7eb; | ||||||
|  |      | ||||||
|  |     /* Button Colors */ | ||||||
|  |     --button-primary-bg: #667eea; | ||||||
|  |     --button-primary-text: #ffffff; | ||||||
|  |     --button-secondary-bg: #6c757d; | ||||||
|  |     --button-secondary-text: #ffffff; | ||||||
|  |     --button-danger-bg: #dc3545; | ||||||
|  |     --button-danger-text: #ffffff; | ||||||
|  |      | ||||||
|  |     /* Input Colors */ | ||||||
|  |     --input-background: #ffffff; | ||||||
|  |     --input-border: #ced4da; | ||||||
|  |     --input-border-focus: #80bdff; | ||||||
|  |     --input-text: #495057; | ||||||
|  |      | ||||||
|  |     /* Glass/Glassmorphism Effects */ | ||||||
|  |     --glass-bg: rgba(255, 255, 255, 0.1); | ||||||
|  |     --glass-bg-light: rgba(255, 255, 255, 0.2); | ||||||
|  |     --glass-bg-lighter: rgba(255, 255, 255, 0.3); | ||||||
|  |     --glass-border: 1px solid rgba(255, 255, 255, 0.2); | ||||||
|  |     --glass-border-light: 1px solid rgba(255, 255, 255, 0.3); | ||||||
| } | } | ||||||
|  |  | ||||||
| body { | body { | ||||||
|     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||||
|     background-color: #f8f9fa; |     background-color: var(--background-primary); | ||||||
|     color: #333; |     color: var(--text-primary); | ||||||
|     line-height: 1.6; |     line-height: 1.6; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -115,6 +148,44 @@ input, select, textarea, button { | |||||||
| [data-theme="dark"] { | [data-theme="dark"] { | ||||||
|     --primary-color: #374151; |     --primary-color: #374151; | ||||||
|     --primary-gradient: linear-gradient(135deg, #374151 0%, #1f2937 100%); |     --primary-gradient: linear-gradient(135deg, #374151 0%, #1f2937 100%); | ||||||
|  |      | ||||||
|  |     /* Dark Theme Overrides */ | ||||||
|  |     --background-primary: #111827; | ||||||
|  |     --background-secondary: #1f2937; | ||||||
|  |     --background-tertiary: #374151; | ||||||
|  |     --text-primary: #f9fafb; | ||||||
|  |     --text-secondary: #d1d5db; | ||||||
|  |     --text-inverse: #111827; | ||||||
|  |     --border-primary: #374151; | ||||||
|  |     --border-secondary: #4b5563; | ||||||
|  |     --border-light: #6b7280; | ||||||
|  |      | ||||||
|  |     /* Modal Colors - Dark */ | ||||||
|  |     --modal-background: #1f2937; | ||||||
|  |     --modal-text: #f3f4f6; | ||||||
|  |     --modal-header-background: #1f2937; | ||||||
|  |     --modal-header-border: #374151; | ||||||
|  |      | ||||||
|  |     /* Button Colors - Dark */ | ||||||
|  |     --button-primary-bg: #4f46e5; | ||||||
|  |     --button-primary-text: #ffffff; | ||||||
|  |     --button-secondary-bg: #4b5563; | ||||||
|  |     --button-secondary-text: #ffffff; | ||||||
|  |     --button-danger-bg: #dc2626; | ||||||
|  |     --button-danger-text: #ffffff; | ||||||
|  |      | ||||||
|  |     /* Input Colors - Dark */ | ||||||
|  |     --input-background: #374151; | ||||||
|  |     --input-border: #4b5563; | ||||||
|  |     --input-border-focus: #6366f1; | ||||||
|  |     --input-text: #f9fafb; | ||||||
|  |      | ||||||
|  |     /* Glass Effects - Dark */ | ||||||
|  |     --glass-bg: rgba(0, 0, 0, 0.2); | ||||||
|  |     --glass-bg-light: rgba(0, 0, 0, 0.3); | ||||||
|  |     --glass-bg-lighter: rgba(0, 0, 0, 0.4); | ||||||
|  |     --glass-border: 1px solid rgba(255, 255, 255, 0.1); | ||||||
|  |     --glass-border-light: 1px solid rgba(255, 255, 255, 0.2); | ||||||
| } | } | ||||||
|  |  | ||||||
| [data-theme="rose"] { | [data-theme="rose"] { | ||||||
| @@ -133,8 +204,8 @@ input, select, textarea, button { | |||||||
|  |  | ||||||
| body { | body { | ||||||
|     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||||
|     background-color: #f8f9fa; |     background-color: var(--background-primary); | ||||||
|     color: #333; |     color: var(--text-primary); | ||||||
|     line-height: 1.6; |     line-height: 1.6; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -637,6 +708,25 @@ body { | |||||||
|     background: rgba(255,255,255,0.3); |     background: rgba(255,255,255,0.3); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .print-button { | ||||||
|  |     background: rgba(255,255,255,0.2); | ||||||
|  |     border: none; | ||||||
|  |     color: white; | ||||||
|  |     font-size: 1rem; | ||||||
|  |     width: 40px; | ||||||
|  |     height: 40px; | ||||||
|  |     border-radius: 50%; | ||||||
|  |     cursor: pointer; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     transition: background 0.2s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .print-button:hover { | ||||||
|  |     background: rgba(255,255,255,0.3); | ||||||
|  | } | ||||||
|  |  | ||||||
| .calendar-grid { | .calendar-grid { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-columns: repeat(7, 1fr); |     grid-template-columns: repeat(7, 1fr); | ||||||
| @@ -3684,10 +3774,37 @@ body { | |||||||
| } | } | ||||||
|  |  | ||||||
| .external-calendar-info input[type="checkbox"] { | .external-calendar-info input[type="checkbox"] { | ||||||
|  |     appearance: none; | ||||||
|  |     -webkit-appearance: none; | ||||||
|  |     -moz-appearance: none; | ||||||
|     width: 16px; |     width: 16px; | ||||||
|     height: 16px; |     height: 16px; | ||||||
|     accent-color: rgba(255, 255, 255, 0.8); |     background: transparent; | ||||||
|  |     border: 2px solid rgba(255, 255, 255, 0.3); | ||||||
|  |     border-radius: 3px; | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|  |     position: relative; | ||||||
|  |     transition: all 0.2s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .external-calendar-info input[type="checkbox"]:hover { | ||||||
|  |     border-color: rgba(255, 255, 255, 0.5); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .external-calendar-info input[type="checkbox"]:checked { | ||||||
|  |     border-color: rgba(255, 255, 255, 0.6); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .external-calendar-info input[type="checkbox"]:checked::after { | ||||||
|  |     content: "✓"; | ||||||
|  |     position: absolute; | ||||||
|  |     top: 45%; | ||||||
|  |     left: 50%; | ||||||
|  |     transform: translate(-50%, -50%); | ||||||
|  |     color: rgba(255, 255, 255, 0.9); | ||||||
|  |     font-size: 12px; | ||||||
|  |     font-weight: bold; | ||||||
|  |     line-height: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| .external-calendar-color { | .external-calendar-color { | ||||||
| @@ -3762,10 +3879,37 @@ body { | |||||||
| } | } | ||||||
|  |  | ||||||
| .calendar-info input[type="checkbox"] { | .calendar-info input[type="checkbox"] { | ||||||
|  |     appearance: none; | ||||||
|  |     -webkit-appearance: none; | ||||||
|  |     -moz-appearance: none; | ||||||
|     width: 16px; |     width: 16px; | ||||||
|     height: 16px; |     height: 16px; | ||||||
|     accent-color: rgba(255, 255, 255, 0.8); |     background: transparent; | ||||||
|  |     border: 2px solid rgba(255, 255, 255, 0.3); | ||||||
|  |     border-radius: 3px; | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|  |     position: relative; | ||||||
|  |     transition: all 0.2s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .calendar-info input[type="checkbox"]:hover { | ||||||
|  |     border-color: rgba(255, 255, 255, 0.5); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .calendar-info input[type="checkbox"]:checked { | ||||||
|  |     border-color: rgba(255, 255, 255, 0.6); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .calendar-info input[type="checkbox"]:checked::after { | ||||||
|  |     content: "✓"; | ||||||
|  |     position: absolute; | ||||||
|  |     top: 45%; | ||||||
|  |     left: 50%; | ||||||
|  |     transform: translate(-50%, -50%); | ||||||
|  |     color: rgba(255, 255, 255, 0.9); | ||||||
|  |     font-size: 12px; | ||||||
|  |     font-weight: bold; | ||||||
|  |     line-height: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Create External Calendar Button */ | /* Create External Calendar Button */ | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,51 +0,0 @@ | |||||||
| /* Base Styles - Always Loaded */ |  | ||||||
| * { |  | ||||||
|     margin: 0; |  | ||||||
|     padding: 0; |  | ||||||
|     box-sizing: border-box; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| body { |  | ||||||
|     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |  | ||||||
|     background-color: #f8f9fa; |  | ||||||
|     color: #333; |  | ||||||
|     line-height: 1.6; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .app { |  | ||||||
|     min-height: 100vh; |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: row; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .login-layout { |  | ||||||
|     min-height: 100vh; |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: column; |  | ||||||
|     width: 100%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /* Base Layout */ |  | ||||||
| .main-content { |  | ||||||
|     flex: 1; |  | ||||||
|     margin-left: 280px; |  | ||||||
|     overflow-x: hidden; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /* Basic Form Elements */ |  | ||||||
| input, select, textarea, button { |  | ||||||
|     font-family: inherit; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /* Utility Classes */ |  | ||||||
| .loading { |  | ||||||
|     opacity: 0.7; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .error { |  | ||||||
|     color: #dc3545; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .success { |  | ||||||
|     color: #28a745; |  | ||||||
| } |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user