Implement interactive calendar color picker

Backend enhancements:
- Add calendar_path field to CalendarEvent for color mapping
- Generate consistent colors for calendars using path-based hashing
- Update CalDAV parsing to associate events with their calendar paths
- Add 16-color palette with hash-based assignment algorithm

Frontend features:
- Interactive color picker with 4x4 grid of selectable colors
- Click color swatches to open dropdown with all available colors
- Instant color changes for both sidebar and calendar events
- Persistent color preferences using local storage
- Enhanced UX with hover effects and visual feedback

Styling improvements:
- Larger 16px color swatches for better clickability
- Professional color picker dropdown with smooth animations
- Dynamic event coloring based on calendar assignment
- Improved contrast with text shadows and borders
- Click-outside-to-close functionality for better UX

Users can now personalize their calendar organization with custom colors
that persist across sessions and immediately update throughout the app.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-28 20:14:56 -04:00
parent 5d519fd875
commit f94d057f81
7 changed files with 255 additions and 16 deletions

View File

@@ -61,6 +61,9 @@ pub struct CalendarEvent {
/// URL/href of this event on the CalDAV server
pub href: Option<String>,
/// Calendar path this event belongs to
pub calendar_path: Option<String>,
}
/// Event status enumeration
@@ -182,11 +185,11 @@ impl CalDAVClient {
}
let body = response.text().await.map_err(CalDAVError::RequestError)?;
self.parse_calendar_response(&body)
self.parse_calendar_response(&body, calendar_path)
}
/// Parse CalDAV XML response containing calendar data
fn parse_calendar_response(&self, xml_response: &str) -> Result<Vec<CalendarEvent>, CalDAVError> {
fn parse_calendar_response(&self, xml_response: &str, calendar_path: &str) -> Result<Vec<CalendarEvent>, CalDAVError> {
let mut events = Vec::new();
// Extract calendar data from XML response
@@ -198,6 +201,7 @@ impl CalDAVClient {
for mut event in parsed_events {
event.etag = calendar_data.etag.clone();
event.href = calendar_data.href.clone();
event.calendar_path = Some(calendar_path.to_string());
events.push(event);
}
}
@@ -377,6 +381,7 @@ impl CalDAVClient {
reminders: self.parse_alarms(&event)?,
etag: None, // Set by caller
href: None, // Set by caller
calendar_path: None, // Set by caller
})
}

View File

@@ -145,8 +145,9 @@ pub async fn get_user_info(
None
} else {
Some(CalendarInfo {
path,
path: path.clone(),
display_name,
color: generate_calendar_color(&path),
})
}
}).collect();
@@ -158,6 +159,39 @@ pub async fn get_user_info(
}))
}
// Helper function to generate a consistent color for a calendar based on its path
fn generate_calendar_color(path: &str) -> String {
// Predefined set of attractive, accessible colors for calendars
let colors = [
"#3B82F6", // Blue
"#10B981", // Emerald
"#F59E0B", // Amber
"#EF4444", // Red
"#8B5CF6", // Violet
"#06B6D4", // Cyan
"#84CC16", // Lime
"#F97316", // Orange
"#EC4899", // Pink
"#6366F1", // Indigo
"#14B8A6", // Teal
"#F3B806", // Yellow
"#8B5A2B", // Brown
"#6B7280", // Gray
"#DC2626", // Red-600
"#7C3AED", // Violet-600
];
// Create a simple hash from the path to ensure consistent color assignment
let mut hash: u32 = 0;
for byte in path.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(byte as u32);
}
// Use the hash to select a color from our palette
let color_index = (hash as usize) % colors.len();
colors[color_index].to_string()
}
// Helper function to extract a readable calendar name from path
fn extract_calendar_name(path: &str) -> String {
// Extract the last meaningful part of the path

View File

@@ -31,6 +31,7 @@ pub struct UserInfo {
pub struct CalendarInfo {
pub path: String,
pub display_name: String,
pub color: String,
}
// Error handling