6 Commits

Author SHA1 Message Date
Connor Johnstone
933d7a8c1b Fix calendar visibility preservation during event updates
All checks were successful
Build and Push Docker Image / docker (push) Successful in 1m5s
Prevent hidden calendars from becoming visible when events are modified via drag-and-drop or other update operations. The refresh mechanism was overwriting frontend visibility state with fresh server data that doesn't include visibility settings.

Changes:
- Preserve existing calendar visibility and color settings during refresh_calendar_data
- Maintain smart fallback for saved colors on new calendars
- Ensure calendar visibility state persists across event modifications

This fixes the issue where users would hide calendars, then see them reappear after dragging events.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 11:20:43 -04:00
Connor Johnstone
c938f25951 Fix recurring event series modification and UI issues
Backend fixes:
- Fix "this event only" EXDATE handling - ensure proper timezone conversion for exception dates
- Remove debug logging for cleaner production output

Frontend fixes:
- Add EXDATE timezone conversion in convert_utc_to_local function
- Fix event duplication when viewing weeks across month boundaries with deduplication logic
- Update CSS theme colors for context menus, recurrence options, and recurring edit modals

These changes ensure RFC 5545 compliance for recurring event exceptions and improve the user experience across different themes and calendar views.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 10:22:02 -04:00
Connor Johnstone
c612f567b4 Fix calendar layout positioning and overflow handling
- Add position relative and height 100% to calendar component for proper layout
- Add overflow hidden to week events overlay to prevent content spillover

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 15:30:59 -04:00
Connor Johnstone
b5b53bb23a Fix theme-independent login styling and improve calendar responsiveness
- Remove theme reset on logout to preserve user theme preferences
- Implement hardcoded login page colors that override all theme styles
- Add comprehensive overrides for Google theme affecting login forms
- Optimize month view to show minimum required weeks (4-6) instead of fixed 6
- Implement dynamic calendar grid height calculations for better responsive fit
- Add calendar header to print preview with updated height calculations
- Update responsive breakpoints with proper header height variables

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 12:24:55 -04:00
Connor Johnstone
7e058ba972 Implement comprehensive responsive design improvements for sidebar and calendar views
- Add full responsive sidebar support for screens 600px+ height with three breakpoints (900px, 750px, 650px)
- Implement consistent spacing for all sidebar controls (view-selector, theme-selector, style-selector, add-calendar-button)
- Add calendar header compactness scaling based on screen height (padding, font sizes, min-heights)
- Implement width-based responsive event text sizing for better space utilization
- Fix login page theme inheritance issue by resetting theme to default on logout
- Remove problematic position:relative style from external calendar items that caused color picker interference
- Standardize external-calendar-list margin to 2px across all breakpoints
- Add proper overflow handling and minimum heights to ensure all sidebar components remain visible
- Scale event titles and times progressively smaller on narrower viewports (1200px, 900px, 600px breakpoints)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 18:04:02 -04:00
Connor Johnstone
1f86ea9f71 Enhance styling system with new themes and fix modal theming consistency
- Add 4 new dark themes: Midnight, Charcoal, Nord, Dracula with complete CSS variable definitions
- Create Apple Calendar style with glassmorphism effects and theme-aware design
- Fix Google Calendar style to be theme-aware instead of using hardcoded colors
- Replace hardcoded colors in modal CSS with theme variables for consistent theming
- Add data-style attribute support to document root for style-specific CSS selectors
- Update sidebar dropdowns to include all new theme and style options

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 23:36:47 -04:00
12 changed files with 2252 additions and 669 deletions

View File

@@ -465,13 +465,10 @@ pub async fn update_event_series(
};
// Update the event on the CalDAV server using the original event's href
println!("📤 Updating event on CalDAV server...");
let event_href = existing_event
.href
.as_ref()
.ok_or_else(|| ApiError::Internal("Event missing href for update".to_string()))?;
println!("📤 Using event href: {}", event_href);
println!("📤 Calendar path: {}", calendar_path);
match client
.update_event(&calendar_path, &updated_event, event_href)
@@ -1026,7 +1023,7 @@ async fn update_single_occurrence(
println!("✅ Created exception event successfully");
// Return the original series (now with EXDATE) - main handler will update it on CalDAV
// Return the modified existing event with EXDATE for the main handler to update on CalDAV
Ok((existing_event.clone(), 1)) // 1 occurrence modified (via exception)
}

View File

@@ -8,6 +8,7 @@
<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/apple.css">
<link data-trunk rel="copy-file" href="service-worker.js">
<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" />

View File

@@ -212,6 +212,7 @@
display: none !important;
}
/* Remove today highlighting in preview */
.print-preview-paper .calendar-day.today,
.print-preview-paper .week-day-header.today,

View File

@@ -295,7 +295,21 @@ pub fn App() -> Html {
if !password.is_empty() {
match calendar_service.fetch_user_info(&token, &password).await {
Ok(mut info) => {
// Apply saved colors
// Preserve existing calendar settings (colors and visibility) from current state
if let Some(current_info) = (*user_info).clone() {
for current_cal in &current_info.calendars {
for cal in &mut info.calendars {
if cal.path == current_cal.path {
// Preserve visibility setting
cal.is_visible = current_cal.is_visible;
// Preserve color setting
cal.color = current_cal.color.clone();
}
}
}
}
// Apply saved colors as fallback for new calendars
if let Ok(saved_colors_json) =
LocalStorage::get::<String>("calendar_colors")
{
@@ -304,13 +318,15 @@ pub fn App() -> Html {
{
for saved_cal in &saved_info.calendars {
for cal in &mut info.calendars {
if cal.path == saved_cal.path {
if cal.path == saved_cal.path && cal.color == "#3B82F6" {
// Only apply saved color if it's still the default
cal.color = saved_cal.color.clone();
}
}
}
}
}
// Add timestamp to force re-render
info.last_updated = (js_sys::Date::now() / 1000.0) as u64;
user_info.set(Some(info));
@@ -497,6 +513,11 @@ pub fn App() -> Html {
// Hot-swap stylesheet
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
// Set data-style attribute on document root
if let Some(root) = document.document_element() {
let _ = root.set_attribute("data-style", new_style.value());
}
// Remove existing style link if it exists
if let Some(existing_link) = document.get_element_by_id("dynamic-style") {
existing_link.remove();
@@ -544,6 +565,11 @@ pub fn App() -> Html {
let style = (*current_style).clone();
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
// Set data-style attribute on document root
if let Some(root) = document.document_element() {
let _ = root.set_attribute("data-style", style.value());
}
// Create and append stylesheet link for initial style only if it has a path
if let Some(stylesheet_path) = style.stylesheet_path() {
if let Ok(link) = document.create_element("link") {
@@ -563,6 +589,7 @@ pub fn App() -> Html {
});
}
// Fetch user info when token is available
{
let user_info = user_info.clone();

View File

@@ -182,6 +182,20 @@ pub fn Calendar(props: &CalendarProps) -> Html {
}
}
// Deduplicate events that may appear in multiple month fetches
// This happens when a recurring event spans across month boundaries
all_events.sort_by(|a, b| {
// Sort by UID first, then by start time
match a.uid.cmp(&b.uid) {
std::cmp::Ordering::Equal => a.dtstart.cmp(&b.dtstart),
other => other,
}
});
all_events.dedup_by(|a, b| {
// Remove duplicates with same UID and start time
a.uid == b.uid && a.dtstart == b.dtstart
});
// Process the combined events
match Ok(all_events) as Result<Vec<VEvent>, String>
{

View File

@@ -114,8 +114,13 @@ pub fn month_view(props: &MonthViewProps) -> Html {
};
let weeks_needed = calculate_minimum_weeks_needed(first_weekday, days_in_month);
// Use calculated weeks with height-based container sizing for proper fit
let dynamic_style = format!("grid-template-rows: var(--weekday-header-height, 50px) repeat({}, 1fr);", weeks_needed);
html! {
<div class="calendar-grid">
<div class="calendar-grid" style={dynamic_style}>
// Weekday headers
<div class="weekday-header">{"Sun"}</div>
<div class="weekday-header">{"Mon"}</div>
@@ -238,13 +243,27 @@ pub fn month_view(props: &MonthViewProps) -> Html {
}).collect::<Html>()
}
{ render_next_month_days(days_from_prev_month.len(), days_in_month) }
{ render_next_month_days(days_from_prev_month.len(), days_in_month, calculate_minimum_weeks_needed(first_weekday, days_in_month)) }
</div>
}
}
fn render_next_month_days(prev_days_count: usize, current_days_count: u32) -> Html {
let total_slots = 42; // 6 rows x 7 days
fn calculate_minimum_weeks_needed(first_weekday: Weekday, days_in_month: u32) -> u32 {
let days_before = match first_weekday {
Weekday::Sun => 0,
Weekday::Mon => 1,
Weekday::Tue => 2,
Weekday::Wed => 3,
Weekday::Thu => 4,
Weekday::Fri => 5,
Weekday::Sat => 6,
};
let total_days_needed = days_before + days_in_month;
(total_days_needed + 6) / 7 // Round up to get number of weeks
}
fn render_next_month_days(prev_days_count: usize, current_days_count: u32, weeks_needed: u32) -> Html {
let total_slots = (weeks_needed * 7) as usize; // Dynamic based on weeks needed
let used_slots = prev_days_count + current_days_count as usize;
let remaining_slots = if used_slots < total_slots {
total_slots - used_slots

View File

@@ -1,9 +1,10 @@
use crate::components::{ViewMode, WeekView, MonthView};
use crate::components::{ViewMode, WeekView, MonthView, CalendarHeader};
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 web_sys::MouseEvent;
use yew::prelude::*;
#[derive(Properties, PartialEq)]
@@ -88,10 +89,11 @@ pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
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 calendar_header_height = 80.0; // Calendar header height in print preview
let week_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 total_overhead = calendar_header_height + week_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;
@@ -151,10 +153,11 @@ pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
// 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 calendar_header_height = 80.0; // Calendar header height
let week_header_height = 50.0; // Week header height
let header_border = 2.0;
let container_spacing = 8.0;
let total_overhead = header_height + header_border + container_spacing;
let total_overhead = calendar_header_height + week_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;
@@ -320,38 +323,50 @@ pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
*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 class={classes!("calendar", match props.view_mode { ViewMode::Week => Some("week-view"), _ => None })}>
<CalendarHeader
current_date={props.current_date}
view_mode={props.view_mode.clone()}
on_prev={Callback::from(|_: MouseEvent| {})}
on_next={Callback::from(|_: MouseEvent| {})}
on_today={Callback::from(|_: MouseEvent| {})}
time_increment={Some(props.time_increment)}
on_time_increment_toggle={None::<Callback<MouseEvent>>}
on_print={None::<Callback<MouseEvent>>}
/>
{
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>

View File

@@ -20,12 +20,17 @@ pub enum Theme {
Dark,
Rose,
Mint,
Midnight,
Charcoal,
Nord,
Dracula,
}
#[derive(Clone, PartialEq)]
pub enum Style {
Default,
Google,
Apple,
}
impl Theme {
@@ -39,6 +44,10 @@ impl Theme {
Theme::Dark => "dark",
Theme::Rose => "rose",
Theme::Mint => "mint",
Theme::Midnight => "midnight",
Theme::Charcoal => "charcoal",
Theme::Nord => "nord",
Theme::Dracula => "dracula",
}
}
@@ -51,6 +60,10 @@ impl Theme {
"dark" => Theme::Dark,
"rose" => Theme::Rose,
"mint" => Theme::Mint,
"midnight" => Theme::Midnight,
"charcoal" => Theme::Charcoal,
"nord" => Theme::Nord,
"dracula" => Theme::Dracula,
_ => Theme::Default,
}
}
@@ -61,12 +74,14 @@ impl Style {
match self {
Style::Default => "default",
Style::Google => "google",
Style::Apple => "apple",
}
}
pub fn from_value(value: &str) -> Self {
match value {
"google" => Style::Google,
"apple" => Style::Apple,
_ => Style::Default,
}
}
@@ -76,6 +91,7 @@ impl Style {
match self {
Style::Default => None, // No additional stylesheet needed - uses base styles.css
Style::Google => Some("google.css"), // Trunk copies to root level
Style::Apple => Some("apple.css"), // Trunk copies to root level
}
}
}
@@ -246,7 +262,7 @@ pub fn sidebar(props: &SidebarProps) -> Html {
};
html! {
<li class="external-calendar-item" style="position: relative;">
<li class="external-calendar-item">
<div
class={if props.color_picker_open.as_ref() == Some(&format!("external_{}", cal.id)) {
"external-calendar-info color-picker-active"
@@ -426,6 +442,10 @@ pub fn sidebar(props: &SidebarProps) -> Html {
<option value="dark" selected={matches!(props.current_theme, Theme::Dark)}>{"Dark"}</option>
<option value="rose" selected={matches!(props.current_theme, Theme::Rose)}>{"Rose"}</option>
<option value="mint" selected={matches!(props.current_theme, Theme::Mint)}>{"Mint"}</option>
<option value="midnight" selected={matches!(props.current_theme, Theme::Midnight)}>{"Midnight"}</option>
<option value="charcoal" selected={matches!(props.current_theme, Theme::Charcoal)}>{"Charcoal"}</option>
<option value="nord" selected={matches!(props.current_theme, Theme::Nord)}>{"Nord"}</option>
<option value="dracula" selected={matches!(props.current_theme, Theme::Dracula)}>{"Dracula"}</option>
</select>
</div>
@@ -433,6 +453,7 @@ pub fn sidebar(props: &SidebarProps) -> Html {
<select class="style-selector-dropdown" onchange={on_style_change}>
<option value="default" selected={matches!(props.current_style, Style::Default)}>{"Default"}</option>
<option value="google" selected={matches!(props.current_style, Style::Google)}>{"Google Calendar"}</option>
<option value="apple" selected={matches!(props.current_style, Style::Apple)}>{"Apple Calendar"}</option>
</select>
</div>

View File

@@ -317,6 +317,11 @@ impl CalendarService {
event.last_modified = Some(modified_utc + chrono::Duration::minutes(-timezone_offset_minutes as i64));
event.last_modified_tzid = None;
}
// Convert EXDATE entries from UTC to local time
event.exdate = event.exdate.into_iter()
.map(|exdate| exdate + chrono::Duration::minutes(-timezone_offset_minutes as i64))
.collect();
}
event
@@ -333,8 +338,6 @@ impl CalendarService {
// Convert UTC events to local time for proper display
let event = Self::convert_utc_to_local(event);
if let Some(ref rrule) = event.rrule {
// Generate occurrences for recurring events using VEvent
let occurrences = Self::generate_occurrences(&event, rrule, start_range, end_range);
expanded_events.extend(occurrences);
@@ -437,25 +440,14 @@ impl CalendarService {
// Check if this occurrence is in the exception dates (EXDATE)
let is_exception = base_event.exdate.iter().any(|exception_date| {
// Compare dates ignoring sub-second precision
let exception_naive = exception_date.and_utc();
let occurrence_naive = occurrence_datetime.and_utc();
// EXDATE from server is in local time, but stored as NaiveDateTime
// We need to compare both as local time (naive datetimes) instead of UTC
let exception_naive = *exception_date;
let occurrence_naive = occurrence_datetime;
// Check if dates match (within a minute to handle minor time differences)
let diff = occurrence_naive - exception_naive;
let matches = diff.num_seconds().abs() < 60;
if matches {
web_sys::console::log_1(
&format!(
"🚫 Excluding occurrence {} due to EXDATE {}",
occurrence_naive, exception_naive
)
.into(),
);
}
matches
diff.num_seconds().abs() < 60
});
if !is_exception {
@@ -632,22 +624,11 @@ impl CalendarService {
// Check if this occurrence is in the exception dates (EXDATE)
let is_exception = base_event.exdate.iter().any(|exception_date| {
let exception_naive = exception_date.and_utc();
let occurrence_naive = occurrence_datetime.and_utc();
// Compare as local time (naive datetimes) instead of UTC
let exception_naive = *exception_date;
let occurrence_naive = occurrence_datetime;
let diff = occurrence_naive - exception_naive;
let matches = diff.num_seconds().abs() < 60;
if matches {
web_sys::console::log_1(
&format!(
"🚫 Excluding occurrence {} due to EXDATE {}",
occurrence_naive, exception_naive
)
.into(),
);
}
matches
diff.num_seconds().abs() < 60
});
if !is_exception {

File diff suppressed because it is too large Load Diff

691
frontend/styles/apple.css Normal file
View File

@@ -0,0 +1,691 @@
/* Apple Calendar-inspired styles */
/* Override CSS Variables for Apple Calendar Style */
:root {
/* Apple-style spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 24px;
/* Apple-style borders and radius */
--border-radius-small: 6px;
--border-radius-medium: 10px;
--border-radius-large: 16px;
/* Apple-style shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
--shadow-md: 0 3px 6px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.12);
--shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.15), 0 3px 6px rgba(0, 0, 0, 0.10);
}
/* Theme-aware Apple style colors - use theme colors but with Apple aesthetic */
[data-style="apple"] {
/* Use theme background and text colors */
--apple-bg-primary: var(--background-secondary);
--apple-bg-secondary: var(--background-primary);
--apple-text-primary: var(--text-primary);
--apple-text-secondary: var(--text-secondary);
--apple-text-tertiary: var(--text-secondary);
--apple-text-inverse: var(--text-inverse);
--apple-border-primary: var(--border-primary);
--apple-border-secondary: var(--border-secondary);
--apple-accent: var(--primary-color);
--apple-hover-bg: var(--background-tertiary);
--apple-today-accent: var(--primary-color);
/* Apple font family */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
/* Theme-specific Apple style adjustments */
[data-style="apple"][data-theme="default"] {
--apple-bg-tertiary: rgba(248, 249, 250, 0.8);
--apple-bg-sidebar: rgba(246, 246, 246, 0.7);
--apple-accent-bg: rgba(102, 126, 234, 0.1);
--apple-today-bg: rgba(102, 126, 234, 0.15);
}
[data-style="apple"][data-theme="ocean"] {
--apple-bg-tertiary: rgba(224, 247, 250, 0.8);
--apple-bg-sidebar: rgba(224, 247, 250, 0.7);
--apple-accent-bg: rgba(0, 105, 148, 0.1);
--apple-today-bg: rgba(0, 105, 148, 0.15);
}
[data-style="apple"][data-theme="forest"] {
--apple-bg-tertiary: rgba(232, 245, 232, 0.8);
--apple-bg-sidebar: rgba(232, 245, 232, 0.7);
--apple-accent-bg: rgba(6, 95, 70, 0.1);
--apple-today-bg: rgba(6, 95, 70, 0.15);
}
[data-style="apple"][data-theme="sunset"] {
--apple-bg-tertiary: rgba(255, 243, 224, 0.8);
--apple-bg-sidebar: rgba(255, 243, 224, 0.7);
--apple-accent-bg: rgba(234, 88, 12, 0.1);
--apple-today-bg: rgba(234, 88, 12, 0.15);
}
[data-style="apple"][data-theme="purple"] {
--apple-bg-tertiary: rgba(243, 229, 245, 0.8);
--apple-bg-sidebar: rgba(243, 229, 245, 0.7);
--apple-accent-bg: rgba(124, 58, 237, 0.1);
--apple-today-bg: rgba(124, 58, 237, 0.15);
}
[data-style="apple"][data-theme="dark"] {
--apple-bg-tertiary: rgba(31, 41, 55, 0.9);
--apple-bg-sidebar: rgba(44, 44, 46, 0.8);
--apple-accent-bg: rgba(55, 65, 81, 0.3);
--apple-today-bg: rgba(55, 65, 81, 0.4);
}
[data-style="apple"][data-theme="rose"] {
--apple-bg-tertiary: rgba(252, 228, 236, 0.8);
--apple-bg-sidebar: rgba(252, 228, 236, 0.7);
--apple-accent-bg: rgba(225, 29, 72, 0.1);
--apple-today-bg: rgba(225, 29, 72, 0.15);
}
[data-style="apple"][data-theme="mint"] {
--apple-bg-tertiary: rgba(224, 242, 241, 0.8);
--apple-bg-sidebar: rgba(224, 242, 241, 0.7);
--apple-accent-bg: rgba(16, 185, 129, 0.1);
--apple-today-bg: rgba(16, 185, 129, 0.15);
}
[data-style="apple"][data-theme="midnight"] {
--apple-bg-tertiary: rgba(21, 27, 38, 0.9);
--apple-bg-sidebar: rgba(21, 27, 38, 0.8);
--apple-accent-bg: rgba(76, 154, 255, 0.15);
--apple-today-bg: rgba(76, 154, 255, 0.2);
}
[data-style="apple"][data-theme="charcoal"] {
--apple-bg-tertiary: rgba(26, 26, 26, 0.9);
--apple-bg-sidebar: rgba(26, 26, 26, 0.8);
--apple-accent-bg: rgba(74, 222, 128, 0.15);
--apple-today-bg: rgba(74, 222, 128, 0.2);
}
[data-style="apple"][data-theme="nord"] {
--apple-bg-tertiary: rgba(59, 66, 82, 0.9);
--apple-bg-sidebar: rgba(59, 66, 82, 0.8);
--apple-accent-bg: rgba(136, 192, 208, 0.15);
--apple-today-bg: rgba(136, 192, 208, 0.2);
}
[data-style="apple"][data-theme="dracula"] {
--apple-bg-tertiary: rgba(68, 71, 90, 0.9);
--apple-bg-sidebar: rgba(68, 71, 90, 0.8);
--apple-accent-bg: rgba(189, 147, 249, 0.15);
--apple-today-bg: rgba(189, 147, 249, 0.2);
}
/* Apple-style body and base styles */
[data-style="apple"] body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--apple-bg-secondary);
color: var(--apple-text-primary);
font-weight: 400;
line-height: 1.47;
letter-spacing: -0.022em;
}
/* Apple-style sidebar with glassmorphism */
[data-style="apple"] .app-sidebar {
background: var(--apple-bg-sidebar);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-right: 1px solid var(--apple-border-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
box-shadow: none;
}
[data-style="apple"] .sidebar-header {
background: transparent;
border-bottom: 1px solid var(--apple-border-primary);
padding: 20px 16px 16px 16px;
}
[data-style="apple"] .sidebar-header h1 {
font-size: 24px;
font-weight: 700;
color: var(--apple-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
letter-spacing: -0.04em;
margin-bottom: 4px;
}
[data-style="apple"] .user-info {
color: var(--apple-text-primary);
font-size: 15px;
line-height: 1.4;
}
[data-style="apple"] .user-info .username {
font-weight: 600;
color: var(--apple-text-primary);
font-size: 16px;
}
[data-style="apple"] .user-info .server-url {
color: var(--apple-text-secondary);
font-size: 13px;
font-weight: 400;
}
/* Apple-style buttons */
[data-style="apple"] .create-calendar-button {
background: var(--apple-accent);
color: var(--apple-text-inverse);
border: none;
border-radius: 8px;
padding: 10px 16px;
font-weight: 600;
font-size: 15px;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
font-family: inherit;
}
[data-style="apple"] .create-calendar-button:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
background: var(--apple-accent);
filter: brightness(1.1);
}
[data-style="apple"] .logout-button {
background: var(--apple-bg-primary);
color: var(--apple-accent);
border: 1px solid var(--apple-border-primary);
border-radius: 8px;
padding: 8px 16px;
font-weight: 500;
font-size: 15px;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
[data-style="apple"] .logout-button:hover {
background: var(--apple-hover-bg);
transform: translateY(-1px);
}
/* Apple-style navigation */
[data-style="apple"] .sidebar-nav .nav-link {
color: var(--apple-text-primary);
text-decoration: none;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.2s ease;
display: block;
font-weight: 500;
font-size: 15px;
}
[data-style="apple"] .sidebar-nav .nav-link:hover {
color: var(--apple-accent);
background: var(--apple-hover-bg);
transform: translateX(2px);
}
/* Apple-style calendar list */
[data-style="apple"] .calendar-list h3 {
color: var(--apple-text-primary);
font-size: 17px;
font-weight: 600;
letter-spacing: -0.024em;
margin-bottom: 12px;
}
[data-style="apple"] .calendar-list .calendar-name {
color: var(--apple-text-primary);
font-size: 15px;
font-weight: 500;
}
[data-style="apple"] .no-calendars {
color: var(--apple-text-secondary);
font-size: 14px;
font-style: italic;
}
/* Apple-style form elements */
[data-style="apple"] .sidebar-footer label,
[data-style="apple"] .view-selector label,
[data-style="apple"] .theme-selector label,
[data-style="apple"] .style-selector label {
color: var(--apple-text-primary);
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
}
[data-style="apple"] .view-selector-dropdown,
[data-style="apple"] .theme-selector-dropdown,
[data-style="apple"] .style-selector-dropdown {
border: 1px solid var(--apple-border-primary);
border-radius: 8px;
padding: 8px 12px;
font-size: 15px;
color: var(--apple-text-primary);
background: var(--apple-bg-primary);
font-family: inherit;
font-weight: 500;
transition: all 0.2s ease;
}
[data-style="apple"] .view-selector-dropdown:focus,
[data-style="apple"] .theme-selector-dropdown:focus,
[data-style="apple"] .style-selector-dropdown:focus {
outline: none;
border-color: var(--apple-accent);
box-shadow: 0 0 0 3px var(--apple-accent-bg);
}
/* Apple-style calendar list items */
[data-style="apple"] .calendar-list .calendar-item {
padding: 6px 8px;
border-radius: 8px;
transition: all 0.2s ease;
margin-bottom: 2px;
}
[data-style="apple"] .calendar-list .calendar-item:hover {
background-color: var(--apple-hover-bg);
transform: translateX(2px);
}
/* Apple-style main content area */
[data-style="apple"] .app-main {
background: var(--apple-bg-secondary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
color: var(--apple-text-primary);
}
/* Apple-style calendar header */
[data-style="apple"] .calendar-header {
background: var(--apple-bg-primary);
color: var(--apple-text-primary);
padding: 20px 24px;
border-radius: 16px 16px 0 0;
box-shadow: var(--shadow-sm);
}
[data-style="apple"] .calendar-header h2,
[data-style="apple"] .calendar-header h3,
[data-style="apple"] .month-header,
[data-style="apple"] .week-header {
color: var(--apple-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-weight: 700;
letter-spacing: -0.04em;
}
/* Apple-style headings */
[data-style="apple"] h1,
[data-style="apple"] h2,
[data-style="apple"] h3,
[data-style="apple"] .month-title,
[data-style="apple"] .calendar-title,
[data-style="apple"] .current-month,
[data-style="apple"] .month-year,
[data-style="apple"] .header-title {
color: var(--apple-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-weight: 700;
letter-spacing: -0.04em;
}
/* Apple-style navigation buttons */
[data-style="apple"] button,
[data-style="apple"] .nav-button,
[data-style="apple"] .calendar-nav-button,
[data-style="apple"] .prev-button,
[data-style="apple"] .next-button,
[data-style="apple"] .arrow-button {
color: var(--apple-text-primary);
background: var(--apple-bg-primary);
border: 1px solid var(--apple-border-primary);
border-radius: 8px;
padding: 8px 12px;
font-weight: 600;
transition: all 0.2s ease;
font-family: inherit;
}
[data-style="apple"] button:hover,
[data-style="apple"] .nav-button:hover,
[data-style="apple"] .calendar-nav-button:hover,
[data-style="apple"] .prev-button:hover,
[data-style="apple"] .next-button:hover,
[data-style="apple"] .arrow-button:hover {
background: var(--apple-accent-bg);
color: var(--apple-accent);
border-color: var(--apple-accent);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
/* Apple-style calendar controls */
[data-style="apple"] .calendar-controls,
[data-style="apple"] .current-date,
[data-style="apple"] .date-display {
color: var(--apple-text-primary);
font-weight: 600;
}
/* Apple-style calendar grid */
[data-style="apple"] .calendar-grid,
[data-style="apple"] .calendar-container {
border: 1px solid var(--apple-border-primary);
border-radius: 16px;
overflow: hidden;
background: var(--apple-bg-primary);
box-shadow: var(--shadow-lg);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
margin: 16px;
}
[data-style="apple"] .month-header,
[data-style="apple"] .week-header {
font-size: 28px;
font-weight: 700;
color: var(--apple-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
letter-spacing: -0.04em;
}
/* Apple-style calendar cells */
[data-style="apple"] .calendar-day,
[data-style="apple"] .day-cell {
border: 1px solid var(--apple-border-secondary);
background: var(--apple-bg-primary);
transition: all 0.3s ease;
padding: 12px;
min-height: 120px;
position: relative;
}
[data-style="apple"] .calendar-day:hover,
[data-style="apple"] .day-cell:hover {
background: var(--apple-hover-bg);
transform: scale(1.02);
box-shadow: var(--shadow-sm);
z-index: 10;
}
[data-style="apple"] .calendar-day.today,
[data-style="apple"] .day-cell.today {
background: var(--apple-today-bg);
border-color: var(--apple-today-accent);
position: relative;
}
[data-style="apple"] .calendar-day.today::before,
[data-style="apple"] .day-cell.today::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--apple-today-accent);
border-radius: 2px 2px 0 0;
}
[data-style="apple"] .calendar-day.other-month,
[data-style="apple"] .day-cell.other-month {
background: var(--apple-bg-secondary);
color: var(--apple-text-secondary);
opacity: 0.6;
}
[data-style="apple"] .day-number,
[data-style="apple"] .date-number {
font-size: 16px;
font-weight: 600;
color: var(--apple-text-primary);
margin-bottom: 6px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
/* Apple-style day headers */
[data-style="apple"] .day-header,
[data-style="apple"] .weekday-header {
background: var(--apple-bg-secondary);
color: var(--apple-text-secondary);
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px;
border-bottom: 1px solid var(--apple-border-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
/* Apple Calendar-style events */
[data-style="apple"] .event {
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
font-weight: 500;
margin: 2px 0;
cursor: pointer;
border: none;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
line-height: 1.3;
position: relative;
}
[data-style="apple"] .event::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: rgba(255, 255, 255, 0.8);
border-radius: 2px 0 0 2px;
}
[data-style="apple"] .event * {
color: white;
font-family: inherit;
}
[data-style="apple"] .event:hover {
transform: translateY(-1px) scale(1.02);
box-shadow: var(--shadow-md);
}
/* All-day events styling */
[data-style="apple"] .event.all-day {
border-radius: 16px;
padding: 6px 12px;
font-weight: 600;
margin: 3px 0;
font-size: 13px;
}
[data-style="apple"] .event.all-day::before {
display: none;
}
/* Event time display */
[data-style="apple"] .event-time {
opacity: 0.9;
font-size: 11px;
margin-right: 4px;
font-weight: 600;
}
/* Calendar table structure */
[data-style="apple"] .calendar-table,
[data-style="apple"] table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
background: var(--apple-bg-primary);
}
[data-style="apple"] .calendar-table td,
[data-style="apple"] table td {
vertical-align: top;
border: 1px solid var(--apple-border-secondary);
background: var(--apple-bg-primary);
}
/* Apple-style view toggle */
[data-style="apple"] .view-toggle {
display: flex;
gap: 0;
background: var(--apple-bg-primary);
border-radius: 10px;
padding: 2px;
box-shadow: var(--shadow-sm);
border: 1px solid var(--apple-border-primary);
}
[data-style="apple"] .view-toggle button {
padding: 8px 16px;
border: none;
background: transparent;
color: var(--apple-text-secondary);
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
[data-style="apple"] .view-toggle button.active {
background: var(--apple-accent);
color: var(--apple-text-inverse);
box-shadow: var(--shadow-sm);
transform: scale(1.02);
}
/* Apple-style today button */
[data-style="apple"] .today-button {
background: var(--apple-accent);
border: none;
color: var(--apple-text-inverse);
padding: 10px 16px;
border-radius: 10px;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
font-family: inherit;
}
[data-style="apple"] .today-button:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
filter: brightness(1.1);
}
/* Apple-style modals */
[data-style="apple"] .modal-overlay {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
[data-style="apple"] .modal-content {
background: var(--apple-bg-primary);
border-radius: 16px;
box-shadow: var(--shadow-lg);
border: 1px solid var(--apple-border-primary);
color: var(--apple-text-primary);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
[data-style="apple"] .modal h2 {
font-size: 22px;
font-weight: 700;
color: var(--apple-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
letter-spacing: -0.04em;
}
/* Apple-style form inputs */
[data-style="apple"] input[type="text"],
[data-style="apple"] input[type="email"],
[data-style="apple"] input[type="password"],
[data-style="apple"] input[type="url"],
[data-style="apple"] input[type="date"],
[data-style="apple"] input[type="time"],
[data-style="apple"] textarea,
[data-style="apple"] select {
border: 1px solid var(--apple-border-primary);
border-radius: 8px;
padding: 10px 12px;
font-size: 15px;
color: var(--apple-text-primary);
background: var(--apple-bg-primary);
font-family: inherit;
font-weight: 500;
transition: all 0.2s ease;
}
[data-style="apple"] input:focus,
[data-style="apple"] textarea:focus,
[data-style="apple"] select:focus {
outline: none;
border-color: var(--apple-accent);
box-shadow: 0 0 0 3px var(--apple-accent-bg);
transform: scale(1.02);
}
/* Apple-style labels */
[data-style="apple"] label {
font-size: 15px;
font-weight: 600;
color: var(--apple-text-primary);
margin-bottom: 6px;
display: block;
letter-spacing: -0.01em;
}
/* Smooth animations and transitions */
[data-style="apple"] * {
transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);
}
/* Custom scrollbar for Apple style */
[data-style="apple"] ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
[data-style="apple"] ::-webkit-scrollbar-track {
background: transparent;
}
[data-style="apple"] ::-webkit-scrollbar-thumb {
background: var(--apple-text-secondary);
border-radius: 4px;
opacity: 0.3;
}
[data-style="apple"] ::-webkit-scrollbar-thumb:hover {
opacity: 0.6;
}

File diff suppressed because it is too large Load Diff