10 Commits

Author SHA1 Message Date
Connor Johnstone
85d23b0347 Rebrand application from 'Calendar App' to 'Runway'
Some checks failed
Build and Push Docker Image / docker (push) Failing after 26s
- Update project name in Cargo.toml from calendar-app to runway
- Change HTML title and sidebar header to 'Runway'
- Complete README rewrite with new branding and philosophy
- Add 'The Name' section explaining runway metaphor as passive infrastructure
- Update Dockerfile build references to use new binary name
- Maintain all technical documentation with new branding context

The name 'Runway' embodies passive infrastructure that enables coordination
without getting in the way - like airport runways that provide essential
structure for planes but stay invisible during flight.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 11:05:21 -04:00
Connor Johnstone
13db4abc0f Remove labels from theme and style pickers in sidebar
- Remove "Theme:" label from theme selector dropdown
- Remove "Style:" label from style selector dropdown
- Create cleaner, more minimal sidebar UI

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 10:57:26 -04:00
Connor Johnstone
57e434e4ff Fix drag interaction issues in week view
- Fix drag-to-create being blocked by existing events
- Add creating-event CSS class that disables pointer events on existing events
- Fix single clicks creating temporary event boxes
- Add mouse button state check to prevent post-mouseup movement being treated as drag
- Ensure temp event boxes only appear during actual drag operations (has_moved=true)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 10:55:39 -04:00
Connor Johnstone
7c2901f453 Implement side-by-side rendering for overlapping events in week view
- Add overlap detection algorithm to identify overlapping events
- Implement layout calculation to arrange events in columns
- Update event positioning to use dynamic left/width instead of fixed right
- Events now render side-by-side when they overlap in time
- Maintains proper spacing and margins for all event arrangements

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 10:44:32 -04:00
6c67444b19 Merge pull request 'Fixes for the time grid for late night events' (#8) from bugfix/week-view-time-grid into main
Some checks failed
Build and Push Docker Image / docker (push) Failing after 13m50s
Reviewed-on: #8
2025-09-02 10:39:23 -04:00
Connor Johnstone
970b0a07da Fix compiler warnings
- Remove unused import in auth handler
- Remove unused PreferencesService export
- Fix unused mutable variable in preferences
- Remove unused display_name method from Style enum
- Add dead_code attributes for future preferences service

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 10:38:13 -04:00
Connor Johnstone
e2e5813b54 Fix week view time grid display and late night event rendering
- Remove unnecessary boundary slot from week view time grid
- Adjust container heights to display full 11 PM - midnight time slot
- Fix timezone issue preventing events at 8 PM or later from rendering
- Update date matching logic to handle UTC-4 timezone offset correctly

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 10:36:26 -04:00
Connor Johnstone
73567c185c Implement comprehensive style system with Google Calendar theme
This commit adds a complete style system alongside the existing theme system, allowing users to switch between different UI styles while maintaining theme color variations.

**Core Features:**
- Style enum (Default, Google Calendar) separate from Theme enum
- Hot-swappable stylesheets with dynamic loading
- Style preference persistence (localStorage + database)
- Style picker UI in sidebar below theme picker

**Frontend Implementation:**
- Add Style enum to sidebar.rs with value/display methods
- Implement dynamic stylesheet loading in app.rs
- Add style picker dropdown with proper styling
- Handle style state management and persistence
- Add web-sys features for HtmlLinkElement support

**Backend Integration:**
- Add calendar_style column to user_preferences table
- Update all database operations (insert/update/select)
- Extend API models for style preference
- Add migration for existing users

**Google Calendar Style:**
- Clean Material Design-inspired interface
- White sidebar with proper contrast
- Enhanced calendar grid with subtle shadows
- Improved event styling with hover effects
- Google Sans typography throughout
- Professional color scheme and spacing

**Technical Details:**
- Trunk asset management for stylesheet copying
- High CSS specificity to override theme styles
- Modular CSS architecture for easy extensibility
- Comprehensive text contrast fixes
- Enhanced calendar cells and navigation

Users can now choose between the original gradient design (Default) and a clean Google Calendar-inspired interface (Google Calendar), with full preference persistence across sessions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-01 20:08:05 -04:00
0587762bbb Merge pull request 'Fixed some auth stuff and deployment stuff' (#7) from bugfix/sqlite-auth-docker-deployment into main
Some checks failed
Build and Push Docker Image / docker (push) Failing after 2s
Reviewed-on: #7
2025-09-01 19:29:17 -04:00
4d2aad404b Merge pull request 'Improved auth system' (#6) from feature/sqlite-auth-system into main
Some checks failed
Build and Push Docker Image / docker (push) Failing after 2s
Reviewed-on: #6
2025-09-01 19:03:30 -04:00
23 changed files with 8176 additions and 42 deletions

View File

@@ -38,4 +38,4 @@ calendar.db
**/tests/
# Migrations (not needed for builds)
migrations/
migrations/

View File

@@ -29,7 +29,7 @@ RUN mkdir -p frontend/src && \
echo "pub fn add(a: usize, b: usize) -> usize { a + b }" > calendar-models/src/lib.rs
# Build dependencies (this layer will be cached unless dependencies change)
RUN cargo build --release --target wasm32-unknown-unknown --bin calendar-app
RUN cargo build --release --target wasm32-unknown-unknown --bin runway
# Copy actual source code and build the frontend application
RUN rm -rf frontend
@@ -55,7 +55,7 @@ COPY calendar-models ./calendar-models
# Create empty frontend directory to satisfy workspace
RUN mkdir -p frontend/src && \
printf '[package]\nname = "calendar-app"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\n' > frontend/Cargo.toml && \
printf '[package]\nname = "runway"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\n' > frontend/Cargo.toml && \
echo 'fn main() {}' > frontend/src/main.rs
# Create dummy backend source to build dependencies first

View File

@@ -1,13 +1,20 @@
# Modern CalDAV Web Client
# Runway
## _Passive infrastructure for life's coordination_
>[!WARNING]
>This project was entirely vibe coded. It's my first attempt vibe coding anything, but I just sat down one day and realized this was something I've wanted to build for many years, but would just take way too long. With AI, I've been able to lay out the majority of the app in one long weekend. So proceed at your own risk, but I actually think the app is pretty solid.
A full-stack calendar application built with Rust, featuring a modern web interface for CalDAV calendar management.
A modern CalDAV web client built with Rust WebAssembly.
## Motivation
## The Name
While there are many excellent self-hosted CalDAV server implementations (Nextcloud, Radicale, Baikal, etc.), the web client ecosystem remains limited. Existing solutions like [AgenDAV](https://github.com/agendav/agendav) often suffer from outdated interfaces, bugs, and poor user experience. This project aims to provide a modern, fast, and reliable web interface for CalDAV servers.
Runway embodies the concept of **passive infrastructure** — unobtrusive systems that enable better coordination without getting in the way. Planes can fly and do lots of cool things, but without runways, they can't take off or land. Similarly, calendars and scheduling tools are essential for organizing our lives, but they should not dominate our attention.
The best infrastructure is invisible when working, essential when needed, and enables rather than constrains.
## Why Runway?
While there are many excellent self-hosted CalDAV server implementations (Nextcloud, Radicale, Baikal, etc.), the web client ecosystem remains limited. Existing solutions like [AgenDAV](https://github.com/agendav/agendav) often suffer from outdated interfaces, bugs, and poor user experience. Runway provides a modern, fast, and reliable web interface for CalDAV servers — infrastructure that just works.
## Features
@@ -63,7 +70,7 @@ While there are many excellent self-hosted CalDAV server implementations (Nextcl
### Docker Deployment (Recommended)
The easiest way to run the calendar is using Docker Compose:
The easiest way to run Runway is using Docker Compose:
1. **Clone the repository**:
```bash
@@ -162,7 +169,7 @@ calendar/
This client is designed to work with any RFC-compliant CalDAV server:
- **Baikal** - ✅ Fully tested with complete event and recurrence support
- **Nextcloud** - 🚧 Planned compatibility with calendar app
- **Nextcloud** - 🚧 Planned compatibility with Nextcloud calendar
- **Radicale** - 🚧 Planned lightweight CalDAV server support
- **Apple Calendar Server** - 🚧 Planned standards-compliant operation
- **Google Calendar** - 🚧 Planned CalDAV API compatibility

View File

@@ -0,0 +1,2 @@
-- Add calendar style preference to user preferences
ALTER TABLE user_preferences ADD COLUMN calendar_style TEXT DEFAULT 'default';

View File

@@ -91,6 +91,7 @@ impl AuthService {
calendar_time_increment: preferences.calendar_time_increment,
calendar_view_mode: preferences.calendar_view_mode,
calendar_theme: preferences.calendar_theme,
calendar_style: preferences.calendar_style,
calendar_colors: preferences.calendar_colors,
},
})

View File

@@ -93,6 +93,7 @@ pub struct UserPreferences {
pub calendar_time_increment: Option<i32>,
pub calendar_view_mode: Option<String>,
pub calendar_theme: Option<String>,
pub calendar_style: Option<String>,
pub calendar_colors: Option<String>, // JSON string
pub updated_at: DateTime<Utc>,
}
@@ -106,6 +107,7 @@ impl UserPreferences {
calendar_time_increment: Some(15),
calendar_view_mode: Some("month".to_string()),
calendar_theme: Some("light".to_string()),
calendar_style: Some("default".to_string()),
calendar_colors: None,
updated_at: Utc::now(),
}
@@ -264,14 +266,15 @@ impl<'a> PreferencesRepository<'a> {
sqlx::query(
"INSERT INTO user_preferences
(user_id, calendar_selected_date, calendar_time_increment,
calendar_view_mode, calendar_theme, calendar_colors, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)",
calendar_view_mode, calendar_theme, calendar_style, calendar_colors, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(&prefs.user_id)
.bind(&prefs.calendar_selected_date)
.bind(&prefs.calendar_time_increment)
.bind(&prefs.calendar_view_mode)
.bind(&prefs.calendar_theme)
.bind(&prefs.calendar_style)
.bind(&prefs.calendar_colors)
.bind(&prefs.updated_at)
.execute(self.db.pool())
@@ -286,7 +289,7 @@ impl<'a> PreferencesRepository<'a> {
sqlx::query(
"UPDATE user_preferences
SET calendar_selected_date = ?, calendar_time_increment = ?,
calendar_view_mode = ?, calendar_theme = ?,
calendar_view_mode = ?, calendar_theme = ?, calendar_style = ?,
calendar_colors = ?, updated_at = ?
WHERE user_id = ?",
)
@@ -294,6 +297,7 @@ impl<'a> PreferencesRepository<'a> {
.bind(&prefs.calendar_time_increment)
.bind(&prefs.calendar_view_mode)
.bind(&prefs.calendar_theme)
.bind(&prefs.calendar_style)
.bind(&prefs.calendar_colors)
.bind(Utc::now())
.bind(&prefs.user_id)

View File

@@ -2,7 +2,6 @@ use axum::{extract::State, http::HeaderMap, response::Json};
use std::sync::Arc;
use crate::calendar::CalDAVClient;
use crate::config::CalDAVConfig;
use crate::{
models::{ApiError, AuthResponse, CalDAVLoginRequest, CalendarInfo, UserInfo},
AppState,

View File

@@ -38,6 +38,7 @@ pub async fn get_preferences(
calendar_time_increment: preferences.calendar_time_increment,
calendar_view_mode: preferences.calendar_view_mode,
calendar_theme: preferences.calendar_theme,
calendar_style: preferences.calendar_style,
calendar_colors: preferences.calendar_colors,
}))
}
@@ -78,6 +79,9 @@ pub async fn update_preferences(
if request.calendar_theme.is_some() {
preferences.calendar_theme = request.calendar_theme;
}
if request.calendar_style.is_some() {
preferences.calendar_style = request.calendar_style;
}
if request.calendar_colors.is_some() {
preferences.calendar_colors = request.calendar_colors;
}
@@ -94,6 +98,7 @@ pub async fn update_preferences(
calendar_time_increment: preferences.calendar_time_increment,
calendar_view_mode: preferences.calendar_view_mode,
calendar_theme: preferences.calendar_theme,
calendar_style: preferences.calendar_style,
calendar_colors: preferences.calendar_colors,
}),
))

View File

@@ -28,6 +28,7 @@ pub struct UserPreferencesResponse {
pub calendar_time_increment: Option<i32>,
pub calendar_view_mode: Option<String>,
pub calendar_theme: Option<String>,
pub calendar_style: Option<String>,
pub calendar_colors: Option<String>,
}
@@ -37,6 +38,7 @@ pub struct UpdatePreferencesRequest {
pub calendar_time_increment: Option<i32>,
pub calendar_view_mode: Option<String>,
pub calendar_theme: Option<String>,
pub calendar_style: Option<String>,
pub calendar_colors: Option<String>,
}

Binary file not shown.

View File

@@ -1,17 +1,16 @@
services:
calendar-backend:
build: .
env_file:
- .env
ports:
- "3000:3000"
volumes:
- ./data/site_dist:/srv/www
- ./data/db:/db
calendar-frontend:
image: caddy
env_file:
- .env
environment:
- BACKEND_API_URL=http://localhost:3000/api
ports:
- "80:80"
- "443:443"

View File

@@ -1,5 +1,5 @@
[package]
name = "calendar-app"
name = "runway"
version = "0.1.0"
edition = "2021"
@@ -13,6 +13,8 @@ web-sys = { version = "0.3", features = [
"HtmlSelectElement",
"HtmlInputElement",
"HtmlTextAreaElement",
"HtmlLinkElement",
"HtmlHeadElement",
"Event",
"MouseEvent",
"InputEvent",

View File

@@ -2,10 +2,11 @@
<html>
<head>
<meta charset="utf-8" />
<title>Calendar App</title>
<title>Runway</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base data-trunk-public-url />
<link data-trunk rel="css" href="styles.css">
<link data-trunk rel="copy-file" href="styles/google.css">
</head>
<body>
<script>

View File

@@ -3,10 +3,12 @@ use crate::components::{
EditAction, EventClass, EventContextMenu, EventCreationData, EventStatus, RecurrenceType,
ReminderType, RouteHandler, Sidebar, Theme, ViewMode,
};
use crate::components::sidebar::{Style};
use crate::models::ical::VEvent;
use crate::services::{calendar_service::UserInfo, CalendarService};
use chrono::NaiveDate;
use gloo_storage::{LocalStorage, Storage};
use wasm_bindgen::JsCast;
use web_sys::MouseEvent;
use yew::prelude::*;
use yew_router::prelude::*;
@@ -96,6 +98,16 @@ pub fn App() -> Html {
}
});
// Style state - load from localStorage if available
let current_style = use_state(|| {
// Try to load saved style from localStorage
if let Ok(saved_style) = LocalStorage::get::<String>("calendar_style") {
Style::from_value(&saved_style)
} else {
Style::Default // Default style
}
});
let available_colors = use_state(|| get_theme_event_colors());
let on_login = {
@@ -152,6 +164,42 @@ pub fn App() -> Html {
})
};
let on_style_change = {
let current_style = current_style.clone();
Callback::from(move |new_style: Style| {
// Save style to localStorage
let _ = LocalStorage::set("calendar_style", new_style.value());
// Hot-swap stylesheet
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
// Remove existing style link if it exists
if let Some(existing_link) = document.get_element_by_id("dynamic-style") {
existing_link.remove();
}
// Create and append new stylesheet link only if style has a path
if let Some(stylesheet_path) = new_style.stylesheet_path() {
if let Ok(link) = document.create_element("link") {
let link = link.dyn_into::<web_sys::HtmlLinkElement>().unwrap();
link.set_id("dynamic-style");
link.set_rel("stylesheet");
link.set_href(stylesheet_path);
if let Some(head) = document.head() {
let _ = head.append_child(&link);
}
}
}
// If stylesheet_path is None (Default style), just removing the dynamic link is enough
}
}
// Update state
current_style.set(new_style);
})
};
// Apply initial theme on mount
{
let current_theme = current_theme.clone();
@@ -165,6 +213,32 @@ pub fn App() -> Html {
});
}
// Apply initial style on mount
{
let current_style = current_style.clone();
use_effect_with((), move |_| {
let style = (*current_style).clone();
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
// 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") {
let link = link.dyn_into::<web_sys::HtmlLinkElement>().unwrap();
link.set_id("dynamic-style");
link.set_rel("stylesheet");
link.set_href(stylesheet_path);
if let Some(head) = document.head() {
let _ = head.append_child(&link);
}
}
}
// If initial style is Default (None), no additional stylesheet needed
}
}
});
}
// Fetch user info when token is available
{
let user_info = user_info.clone();
@@ -718,6 +792,8 @@ pub fn App() -> Html {
on_view_change={on_view_change}
current_theme={(*current_theme).clone()}
on_theme_change={on_theme_change}
current_style={(*current_style).clone()}
on_style_change={on_style_change}
/>
<main class="app-main">
<RouteHandler

View File

@@ -32,6 +32,12 @@ pub enum Theme {
Mint,
}
#[derive(Clone, PartialEq)]
pub enum Style {
Default,
Google,
}
impl Theme {
pub fn value(&self) -> &'static str {
match self {
@@ -60,6 +66,30 @@ impl Theme {
}
}
impl Style {
pub fn value(&self) -> &'static str {
match self {
Style::Default => "default",
Style::Google => "google",
}
}
pub fn from_value(value: &str) -> Self {
match value {
"google" => Style::Google,
_ => Style::Default,
}
}
pub fn stylesheet_path(&self) -> Option<&'static str> {
match self {
Style::Default => None, // No additional stylesheet needed - uses base styles.css
Style::Google => Some("google.css"), // Trunk copies to root level
}
}
}
impl Default for ViewMode {
fn default() -> Self {
ViewMode::Month
@@ -80,6 +110,8 @@ pub struct SidebarProps {
pub on_view_change: Callback<ViewMode>,
pub current_theme: Theme,
pub on_theme_change: Callback<Theme>,
pub current_style: Style,
pub on_style_change: Callback<Style>,
}
#[function_component(Sidebar)]
@@ -111,10 +143,22 @@ pub fn sidebar(props: &SidebarProps) -> Html {
})
};
let on_style_change = {
let on_style_change = props.on_style_change.clone();
Callback::from(move |e: Event| {
let target = e.target_dyn_into::<HtmlSelectElement>();
if let Some(select) = target {
let value = select.value();
let new_style = Style::from_value(&value);
on_style_change.emit(new_style);
}
})
};
html! {
<aside class="app-sidebar">
<div class="sidebar-header">
<h1>{"Calendar App"}</h1>
<h1>{"Runway"}</h1>
{
if let Some(ref info) = props.user_info {
html! {
@@ -187,6 +231,13 @@ pub fn sidebar(props: &SidebarProps) -> Html {
</select>
</div>
<div class="style-selector">
<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>
</select>
</div>
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
</div>
</aside>

View File

@@ -353,6 +353,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
week_days.iter().enumerate().map(|(_column_index, date)| {
let is_today = *date == props.today;
let day_events = props.events.get(date).cloned().unwrap_or_default();
let event_layouts = calculate_event_layout(&day_events, *date);
// Drag event handlers
let drag_state_clone = drag_state.clone();
@@ -398,6 +399,13 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let drag_state = drag_state_clone.clone();
let time_increment = props.time_increment;
Callback::from(move |e: MouseEvent| {
// Only process mouse move if a button is still pressed
if e.buttons() == 0 {
// No mouse button pressed, clear drag state
drag_state.set(None);
return;
}
if let Some(mut current_drag) = (*drag_state).clone() {
if current_drag.is_dragging {
// Use layer_y for consistent coordinate calculation
@@ -567,9 +575,20 @@ pub fn week_view(props: &WeekViewProps) -> Html {
})
};
// Check if currently dragging to create an event
let is_creating_event = if let Some(drag) = (*drag_state).clone() {
matches!(drag.drag_type, DragType::CreateEvent) && drag.is_dragging
} else {
false
};
html! {
<div
class={classes!("week-day-column", if is_today { Some("today") } else { None })}
class={classes!(
"week-day-column",
if is_today { Some("today") } else { None },
if is_creating_event { Some("creating-event") } else { None }
)}
{onmousedown}
{onmousemove}
{onmouseup}
@@ -585,16 +604,11 @@ pub fn week_view(props: &WeekViewProps) -> Html {
}
}).collect::<Html>()
}
// Final boundary slot to complete the 24-hour visual grid - make it interactive like other slots
<div class="time-slot boundary-slot">
<div class="time-slot-half"></div>
<div class="time-slot-half"></div>
</div>
// Events positioned absolutely based on their actual times
<div class="events-container">
{
day_events.iter().filter_map(|event| {
day_events.iter().enumerate().filter_map(|(event_idx, event)| {
let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date);
// Skip events that don't belong on this date or have invalid positioning
@@ -787,12 +801,28 @@ pub fn week_view(props: &WeekViewProps) -> Html {
if is_refreshing { Some("refreshing") } else { None },
if is_all_day { Some("all-day") } else { None }
)}
style={format!(
"background-color: {}; top: {}px; height: {}px;",
event_color,
start_pixels,
duration_pixels
)}
style={
let (column_idx, total_columns) = event_layouts[event_idx];
let column_width = if total_columns > 1 {
format!("calc((100% - 8px) / {})", total_columns) // Account for 4px margins on each side
} else {
"calc(100% - 8px)".to_string()
};
let left_offset = if total_columns > 1 {
format!("calc(4px + {} * (100% - 8px) / {})", column_idx, total_columns)
} else {
"4px".to_string()
};
format!(
"background-color: {}; top: {}px; height: {}px; left: {}; width: {}; right: auto;",
event_color,
start_pixels,
duration_pixels,
left_offset,
column_width
)
}
{onclick}
{oncontextmenu}
onmousedown={onmousedown_event}
@@ -840,7 +870,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
// Temporary event box during drag
{
if let Some(drag) = (*drag_state).clone() {
if drag.is_dragging && drag.start_date == *date {
if drag.is_dragging && drag.has_moved && drag.start_date == *date {
match &drag.drag_type {
DragType::CreateEvent => {
let start_y = drag.start_y.min(drag.current_y);
@@ -1029,8 +1059,13 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool)
let local_start = event.dtstart.with_timezone(&Local);
let event_date = local_start.date_naive();
// Only position events that are on this specific date
if event_date != date {
// Position events based on when they appear in local time, not their original date
// For timezone issues: an event created at 10 PM Sunday might be stored as Monday UTC
// but should still display on Sunday's column since that's when the user sees it
let should_display_here = event_date == date ||
(event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20);
if !should_display_here {
return (0.0, 0.0, false); // Event not on this date
}
@@ -1065,3 +1100,82 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool)
(start_pixels, duration_pixels, false) // is_all_day = false
}
// Check if two events overlap in time
fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool {
let start1 = event1.dtstart.with_timezone(&Local).naive_local();
let end1 = if let Some(end) = event1.dtend {
end.with_timezone(&Local).naive_local()
} else {
start1 + chrono::Duration::hours(1) // Default 1 hour duration
};
let start2 = event2.dtstart.with_timezone(&Local).naive_local();
let end2 = if let Some(end) = event2.dtend {
end.with_timezone(&Local).naive_local()
} else {
start2 + chrono::Duration::hours(1) // Default 1 hour duration
};
// Events overlap if one starts before the other ends
start1 < end2 && start2 < end1
}
// Calculate layout columns for overlapping events
fn calculate_event_layout(events: &[VEvent], date: NaiveDate) -> Vec<(usize, usize)> {
// Filter events that should appear on this date and sort by start time
let mut day_events: Vec<_> = events.iter()
.filter(|event| {
let (_, _, _) = calculate_event_position(event, date);
// Only include events that would be positioned (non-zero dimensions or all-day)
let local_start = event.dtstart.with_timezone(&Local);
let event_date = local_start.date_naive();
event_date == date ||
(event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20)
})
.collect();
// Sort by start time
day_events.sort_by_key(|event| event.dtstart.with_timezone(&Local).naive_local());
// Calculate layout: (column_index, total_columns)
let mut layout = Vec::with_capacity(day_events.len());
let mut columns: Vec<Vec<&VEvent>> = Vec::new();
for event in &day_events {
// Find the first column where this event doesn't overlap with any existing event
let mut placed = false;
for (col_idx, column) in columns.iter_mut().enumerate() {
if !column.iter().any(|existing_event| events_overlap(event, existing_event)) {
column.push(event);
layout.push((col_idx, 0)); // total_columns will be set later
placed = true;
break;
}
}
if !placed {
// Create new column
columns.push(vec![event]);
layout.push((columns.len() - 1, 0)); // total_columns will be set later
}
}
// Update total_columns for all events
let total_columns = columns.len();
for (_, total_cols) in layout.iter_mut() {
*total_cols = total_columns;
}
// Create result mapping original events to their layout
let mut result = Vec::with_capacity(events.len());
for event in events {
if let Some(pos) = day_events.iter().position(|e| e.uid == event.uid) {
result.push(layout[pos]);
} else {
result.push((0, 1)); // Default: single column
}
}
result
}

View File

@@ -2,4 +2,3 @@ pub mod calendar_service;
pub mod preferences;
pub use calendar_service::CalendarService;
pub use preferences::PreferencesService;

View File

@@ -15,6 +15,7 @@ pub struct UserPreferences {
}
#[derive(Debug, Serialize)]
#[allow(dead_code)]
pub struct UpdatePreferencesRequest {
pub calendar_selected_date: Option<String>,
pub calendar_time_increment: Option<i32>,
@@ -23,10 +24,12 @@ pub struct UpdatePreferencesRequest {
pub calendar_colors: Option<String>,
}
#[allow(dead_code)]
pub struct PreferencesService {
base_url: String,
}
#[allow(dead_code)]
impl PreferencesService {
pub fn new() -> Self {
let base_url = option_env!("BACKEND_API_URL")
@@ -147,7 +150,7 @@ impl PreferencesService {
let session_token = LocalStorage::get::<String>("session_token")
.map_err(|_| "No session token found".to_string())?;
let mut request = UpdatePreferencesRequest {
let request = UpdatePreferencesRequest {
calendar_selected_date: LocalStorage::get::<String>("calendar_selected_date").ok(),
calendar_time_increment: LocalStorage::get::<u32>("calendar_time_increment").ok().map(|i| i as i32),
calendar_view_mode: LocalStorage::get::<String>("calendar_view_mode").ok(),

View File

@@ -1,9 +1,120 @@
/* Base Styles - Always Loaded */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* CSS Variables for Style System */
--border-radius-small: 4px;
--border-radius-medium: 8px;
--border-radius-large: 12px;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--shadow-sm: 0 1px 3px 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);
--border-light: 1px solid #e9ecef;
--border-medium: 1px solid #dee2e6;
--transition-fast: 0.15s ease;
--transition-normal: 0.2s ease;
--transition-slow: 0.3s ease;
}
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;
}
/* Theme Data Attributes for Color Schemes */
[data-theme="default"] {
--primary-color: #667eea;
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
[data-theme="ocean"] {
--primary-color: #006994;
--primary-gradient: linear-gradient(135deg, #006994 0%, #0891b2 100%);
}
[data-theme="forest"] {
--primary-color: #065f46;
--primary-gradient: linear-gradient(135deg, #065f46 0%, #047857 100%);
}
[data-theme="sunset"] {
--primary-color: #ea580c;
--primary-gradient: linear-gradient(135deg, #ea580c 0%, #dc2626 100%);
}
[data-theme="purple"] {
--primary-color: #7c3aed;
--primary-gradient: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
}
[data-theme="dark"] {
--primary-color: #374151;
--primary-gradient: linear-gradient(135deg, #374151 0%, #1f2937 100%);
}
[data-theme="rose"] {
--primary-color: #e11d48;
--primary-gradient: linear-gradient(135deg, #e11d48 0%, #f43f5e 100%);
}
[data-theme="mint"] {
--primary-color: #10b981;
--primary-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
}* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
@@ -574,12 +685,13 @@ body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0; /* Allow flex item to shrink below content size */
}
.time-grid {
display: grid;
grid-template-columns: 80px 1fr;
min-height: 100%;
min-height: 1530px;
}
/* Time Labels */
@@ -589,6 +701,7 @@ body {
position: sticky;
left: 0;
z-index: 5;
min-height: 1440px; /* Match the time slots height */
}
.time-label {
@@ -614,12 +727,13 @@ body {
.week-days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
min-height: 1440px; /* Ensure grid is tall enough for 24 time slots */
}
.week-day-column {
position: relative;
border-right: 1px solid var(--time-label-border, #e9ecef);
min-height: 1500px; /* 25 time labels × 60px = 1500px total */
min-height: 1440px; /* 24 time slots × 60px = 1440px total */
}
.week-day-column:last-child {
@@ -668,8 +782,7 @@ body {
/* Week Events */
.week-event {
position: absolute !important;
left: 4px;
right: 4px;
/* left and width are now set inline for overlap handling */
min-height: 20px;
background: #3B82F6;
color: white;
@@ -689,6 +802,20 @@ body {
white-space: nowrap;
}
/* Disable pointer events on existing events when creating a new event */
.week-day-column.creating-event .week-event {
pointer-events: none;
opacity: 0.6; /* Visual feedback that events are not interactive */
}
.week-day-column.creating-event .week-event .event-content {
pointer-events: none;
}
.week-day-column.creating-event .week-event .resize-handle {
pointer-events: none;
}
.week-event:hover {
filter: brightness(1.1);
z-index: 4;
@@ -3014,6 +3141,50 @@ body {
padding: 0.5rem;
}
/* Style Selector Styles */
.style-selector {
margin-bottom: 1rem;
}
.style-selector label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(255, 255, 255, 0.8);
}
.style-selector-dropdown {
width: 100%;
padding: 0.5rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: white;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
}
.style-selector-dropdown:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.style-selector-dropdown:focus {
outline: none;
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
}
.style-selector-dropdown option {
background: #333;
color: white;
padding: 0.5rem;
}
/* Theme Definitions */
:root {
/* Default Theme */

3501
frontend/styles.css.backup Normal file

File diff suppressed because it is too large Load Diff

51
frontend/styles/base.css Normal file
View File

@@ -0,0 +1,51 @@
/* 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;
}

3501
frontend/styles/default.css Normal file

File diff suppressed because it is too large Load Diff

645
frontend/styles/google.css Normal file
View File

@@ -0,0 +1,645 @@
/* Google Calendar-inspired styles */
/* Override CSS Variables for Google Calendar Style */
:root {
/* Google-style spacing */
--spacing-xs: 2px;
--spacing-sm: 4px;
--spacing-md: 8px;
--spacing-lg: 12px;
--spacing-xl: 16px;
/* Google-style borders and radius */
--border-radius-small: 2px;
--border-radius-medium: 4px;
--border-radius-large: 8px;
--border-light: 1px solid #dadce0;
--border-medium: 1px solid #dadce0;
/* Google-style shadows */
--shadow-sm: 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15);
--shadow-md: 0 1px 3px 0 rgba(60,64,67,.3), 0 4px 8px 3px rgba(60,64,67,.15);
--shadow-lg: 0 4px 6px 0 rgba(60,64,67,.3), 0 8px 25px 5px rgba(60,64,67,.15);
}
/* Google-style sidebar - override all theme variants */
body .app-sidebar,
[data-theme] .app-sidebar,
[data-theme="default"] .app-sidebar,
[data-theme="ocean"] .app-sidebar,
[data-theme="forest"] .app-sidebar,
[data-theme="sunset"] .app-sidebar,
[data-theme="purple"] .app-sidebar,
[data-theme="dark"] .app-sidebar,
[data-theme="rose"] .app-sidebar,
[data-theme="mint"] .app-sidebar {
background: #ffffff !important;
border-right: 1px solid #dadce0 !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
box-shadow: 2px 0 8px rgba(60,64,67,.1) !important;
}
body .sidebar-header,
[data-theme] .sidebar-header {
background: transparent !important;
border-bottom: 1px solid #dadce0 !important;
}
body .sidebar-header h1,
[data-theme] .sidebar-header h1 {
font-size: 20px !important;
font-weight: 500 !important;
color: #3c4043 !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
}
body .user-info,
[data-theme] .user-info {
color: #3c4043 !important;
}
body .user-info .username,
[data-theme] .user-info .username {
font-weight: 500 !important;
color: #3c4043 !important;
}
body .user-info .server-url,
[data-theme] .user-info .server-url {
color: #5f6368 !important;
}
/* Google-style buttons */
.create-calendar-button {
background: #1a73e8 !important;
color: white !important;
border: none !important;
border-radius: 4px !important;
padding: 8px 16px !important;
font-weight: 500 !important;
font-size: 14px !important;
cursor: pointer !important;
box-shadow: 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15) !important;
transition: box-shadow 0.2s ease !important;
}
.create-calendar-button:hover {
box-shadow: 0 1px 3px 0 rgba(60,64,67,.3), 0 4px 8px 3px rgba(60,64,67,.15) !important;
}
.logout-button {
background: transparent !important;
color: #1a73e8 !important;
border: 1px solid #dadce0 !important;
border-radius: 4px !important;
padding: 8px 16px !important;
font-weight: 500 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: background-color 0.2s ease !important;
}
.logout-button:hover {
background: #f8f9fa !important;
}
/* Google-style navigation and sidebar text */
body .sidebar-nav .nav-link,
[data-theme] .sidebar-nav .nav-link {
color: #3c4043 !important;
text-decoration: none !important;
}
body .sidebar-nav .nav-link:hover,
[data-theme] .sidebar-nav .nav-link:hover {
color: #1a73e8 !important;
background: #f1f3f4 !important;
}
/* Calendar list styling */
body .calendar-list h3,
[data-theme] .calendar-list h3 {
color: #3c4043 !important;
}
body .calendar-list .calendar-name,
[data-theme] .calendar-list .calendar-name {
color: #3c4043 !important;
}
body .no-calendars,
[data-theme] .no-calendars {
color: #5f6368 !important;
}
/* Form labels and text */
body .sidebar-footer label,
[data-theme] .sidebar-footer label,
body .view-selector label,
[data-theme] .view-selector label,
body .theme-selector label,
[data-theme] .theme-selector label,
body .style-selector label,
[data-theme] .style-selector label {
color: #3c4043 !important;
}
/* Google-style selectors */
body .view-selector-dropdown,
body .theme-selector-dropdown,
body .style-selector-dropdown,
[data-theme] .view-selector-dropdown,
[data-theme] .theme-selector-dropdown,
[data-theme] .style-selector-dropdown {
border: 1px solid #dadce0 !important;
border-radius: 4px !important;
padding: 8px !important;
font-size: 14px !important;
color: #3c4043 !important;
background: white !important;
font-family: inherit !important;
}
.view-selector-dropdown:focus,
.theme-selector-dropdown:focus,
.style-selector-dropdown:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px rgba(26,115,232,.2);
}
/* Google-style calendar list items */
.calendar-list h3 {
font-size: 14px;
font-weight: 500;
color: #3c4043;
margin-bottom: 8px;
}
.calendar-list ul {
list-style: none;
}
.calendar-list .calendar-item {
padding: 4px 0;
border-radius: 4px;
transition: background-color 0.15s ease;
}
.calendar-list .calendar-item:hover {
background-color: #f1f3f4;
}
.calendar-list .calendar-name {
color: #3c4043;
font-size: 14px;
font-weight: 400;
}
/* Google-style main content area */
body .app-main,
[data-theme] .app-main {
background: #ffffff !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
color: #3c4043 !important;
}
/* Calendar header elements */
body .calendar-header,
[data-theme] .calendar-header {
background: #f8f9fa !important;
color: #3c4043 !important;
}
body .calendar-header h2,
body .calendar-header h3,
body .month-header,
body .week-header,
[data-theme] .calendar-header h2,
[data-theme] .calendar-header h3,
[data-theme] .month-header,
[data-theme] .week-header {
color: #3c4043 !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
}
/* Month name and title - aggressive override */
body h1,
body h2,
body h3,
body .month-title,
body .calendar-title,
body .current-month,
body .month-year,
body .header-title,
[data-theme] h1,
[data-theme] h2,
[data-theme] h3,
[data-theme] .month-title,
[data-theme] .calendar-title,
[data-theme] .current-month,
[data-theme] .month-year,
[data-theme] .header-title {
color: #3c4043 !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
}
/* Navigation arrows and buttons - aggressive override */
body button,
body .nav-button,
body .calendar-nav-button,
body .prev-button,
body .next-button,
body .arrow-button,
body .navigation-arrow,
body [class*="arrow"],
body [class*="nav"],
body [class*="button"],
[data-theme] button,
[data-theme] .nav-button,
[data-theme] .calendar-nav-button,
[data-theme] .prev-button,
[data-theme] .next-button,
[data-theme] .arrow-button,
[data-theme] .navigation-arrow,
[data-theme] [class*="arrow"],
[data-theme] [class*="nav"],
[data-theme] [class*="button"] {
color: #3c4043 !important;
background: #f8f9fa !important;
border: 1px solid #dadce0 !important;
border-radius: 4px !important;
}
body button:hover,
body .nav-button:hover,
body .calendar-nav-button:hover,
body .prev-button:hover,
body .next-button:hover,
body .arrow-button:hover,
[data-theme] button:hover,
[data-theme] .nav-button:hover,
[data-theme] .calendar-nav-button:hover,
[data-theme] .prev-button:hover,
[data-theme] .next-button:hover,
[data-theme] .arrow-button:hover {
background: #e8f0fe !important;
color: #1a73e8 !important;
border-color: #1a73e8 !important;
}
/* Calendar controls and date display */
body .calendar-controls,
body .current-date,
body .date-display,
[data-theme] .calendar-controls,
[data-theme] .current-date,
[data-theme] .date-display {
color: #3c4043 !important;
}
/* Ultimate nuclear approach - override EVERYTHING */
html body .app-main,
html body .app-main *,
html body .main-content,
html body .main-content *,
html body .calendar-container,
html body .calendar-container *,
html [data-theme] .app-main,
html [data-theme] .app-main *,
html [data-theme] .main-content,
html [data-theme] .main-content *,
html [data-theme] .calendar-container,
html [data-theme] .calendar-container *,
html [data-theme="default"] .app-main *,
html [data-theme="ocean"] .app-main *,
html [data-theme="forest"] .app-main *,
html [data-theme="sunset"] .app-main *,
html [data-theme="purple"] .app-main *,
html [data-theme="dark"] .app-main *,
html [data-theme="rose"] .app-main *,
html [data-theme="mint"] .app-main * {
color: #3c4043 !important;
text-shadow: none !important;
}
/* Force all text elements */
html body .app-main h1,
html body .app-main h2,
html body .app-main h3,
html body .app-main h4,
html body .app-main h5,
html body .app-main h6,
html body .app-main p,
html body .app-main span,
html body .app-main div,
html body .app-main button,
html [data-theme] .app-main h1,
html [data-theme] .app-main h2,
html [data-theme] .app-main h3,
html [data-theme] .app-main h4,
html [data-theme] .app-main h5,
html [data-theme] .app-main h6,
html [data-theme] .app-main p,
html [data-theme] .app-main span,
html [data-theme] .app-main div,
html [data-theme] .app-main button {
color: #3c4043 !important;
}
/* Exception for buttons - make them stand out */
body .app-main button,
body .main-content button,
[data-theme] .app-main button,
[data-theme] .main-content button {
color: #3c4043 !important;
background: #f8f9fa !important;
border: 1px solid #dadce0 !important;
}
/* Google-style calendar grid - more aggressive styling */
html body .calendar-grid,
html [data-theme] .calendar-grid,
body .calendar-container,
[data-theme] .calendar-container {
border: 1px solid #dadce0 !important;
border-radius: 8px !important;
overflow: hidden !important;
background: white !important;
box-shadow: 0 1px 3px 0 rgba(60,64,67,.3), 0 4px 8px 3px rgba(60,64,67,.15) !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
}
html body .calendar-header,
html [data-theme] .calendar-header {
background: #f8f9fa !important;
border-bottom: 1px solid #dadce0 !important;
padding: 16px !important;
}
html body .month-header,
html body .week-header,
html [data-theme] .month-header,
html [data-theme] .week-header {
font-size: 22px !important;
font-weight: 400 !important;
color: #3c4043 !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
}
/* Google-style calendar cells - complete overhaul */
html body .calendar-day,
html [data-theme] .calendar-day,
body .day-cell,
[data-theme] .day-cell {
border: 1px solid #e8eaed !important;
background: white !important;
transition: background-color 0.15s ease !important;
padding: 8px !important;
min-height: 120px !important;
position: relative !important;
}
html body .calendar-day:hover,
html [data-theme] .calendar-day:hover,
body .day-cell:hover,
[data-theme] .day-cell:hover {
background: #f8f9fa !important;
box-shadow: inset 0 0 0 1px #dadce0 !important;
}
html body .calendar-day.today,
html [data-theme] .calendar-day.today,
body .day-cell.today,
[data-theme] .day-cell.today {
background: #e8f0fe !important;
border-color: #1a73e8 !important;
}
html body .calendar-day.other-month,
html [data-theme] .calendar-day.other-month,
body .day-cell.other-month,
[data-theme] .day-cell.other-month {
background: #fafafa !important;
color: #9aa0a6 !important;
}
html body .day-number,
html [data-theme] .day-number,
body .date-number,
[data-theme] .date-number {
font-size: 14px !important;
font-weight: 500 !important;
color: #3c4043 !important;
margin-bottom: 4px !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
}
/* Day headers (Mon, Tue, Wed, etc.) */
html body .day-header,
html [data-theme] .day-header,
body .weekday-header,
[data-theme] .weekday-header {
background: #f8f9fa !important;
color: #5f6368 !important;
font-size: 12px !important;
font-weight: 500 !important;
text-transform: uppercase !important;
letter-spacing: 0.8px !important;
padding: 8px !important;
border-bottom: 1px solid #dadce0 !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
}
/* Google Calendar-style events - complete redesign */
html body .app-main .event,
html [data-theme] .app-main .event,
html body .calendar-container .event,
html [data-theme] .calendar-container .event,
body .event,
[data-theme] .event {
border-radius: 4px !important;
padding: 2px 8px !important;
font-size: 11px !important;
font-weight: 400 !important;
margin: 1px 0 2px 0 !important;
cursor: pointer !important;
border: none !important;
color: white !important;
font-family: 'Google Sans', 'Roboto', sans-serif !important;
box-shadow: 0 1px 3px rgba(60,64,67,.3) !important;
transition: transform 0.1s ease, box-shadow 0.1s ease !important;
display: block !important;
text-overflow: ellipsis !important;
overflow: hidden !important;
white-space: nowrap !important;
line-height: 1.3 !important;
}
html body .app-main .event *,
html [data-theme] .app-main .event *,
html body .calendar-container .event *,
html [data-theme] .calendar-container .event *,
body .event *,
[data-theme] .event * {
color: white !important;
font-family: inherit !important;
}
html body .app-main .event:hover,
html [data-theme] .app-main .event:hover,
body .event:hover,
[data-theme] .event:hover {
transform: translateY(-1px) !important;
box-shadow: 0 2px 8px rgba(60,64,67,.4) !important;
}
/* All-day events styling */
html body .event.all-day,
html [data-theme] .event.all-day {
border-radius: 12px !important;
padding: 4px 12px !important;
font-weight: 500 !important;
margin: 2px 0 !important;
}
/* Event time display */
html body .event-time,
html [data-theme] .event-time {
opacity: 0.9 !important;
font-size: 10px !important;
margin-right: 4px !important;
}
/* Week view events */
html body .week-view .event,
html [data-theme] .week-view .event {
border-left: 3px solid rgba(255,255,255,0.8) !important;
border-radius: 0 4px 4px 0 !important;
padding-left: 6px !important;
}
/* Calendar table structure */
html body .calendar-table,
html [data-theme] .calendar-table,
body table,
[data-theme] table {
border-collapse: separate !important;
border-spacing: 0 !important;
width: 100% !important;
background: white !important;
}
html body .calendar-table td,
html [data-theme] .calendar-table td,
body table td,
[data-theme] table td {
vertical-align: top !important;
border: 1px solid #e8eaed !important;
background: white !important;
}
/* Month/Week view toggle */
html body .view-toggle,
html [data-theme] .view-toggle {
display: flex !important;
gap: 4px !important;
background: #f1f3f4 !important;
border-radius: 6px !important;
padding: 2px !important;
}
html body .view-toggle button,
html [data-theme] .view-toggle button {
padding: 6px 12px !important;
border: none !important;
background: transparent !important;
color: #5f6368 !important;
border-radius: 4px !important;
font-size: 13px !important;
font-weight: 500 !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
}
html body .view-toggle button.active,
html [data-theme] .view-toggle button.active {
background: white !important;
color: #1a73e8 !important;
box-shadow: 0 1px 3px rgba(60,64,67,.3) !important;
}
/* Today button */
html body .today-button,
html [data-theme] .today-button {
background: white !important;
border: 1px solid #dadce0 !important;
color: #1a73e8 !important;
padding: 8px 16px !important;
border-radius: 4px !important;
font-weight: 500 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
}
html body .today-button:hover,
html [data-theme] .today-button:hover {
background: #f8f9fa !important;
border-color: #1a73e8 !important;
}
/* Google-style modals */
.modal-overlay {
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: var(--shadow-lg);
border: none;
}
.modal h2 {
font-size: 20px;
font-weight: 500;
color: #3c4043;
font-family: 'Google Sans', sans-serif;
}
/* Google-style form inputs */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="url"],
input[type="date"],
input[type="time"],
textarea,
select {
border: 1px solid #dadce0;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
color: #3c4043;
background: white;
font-family: inherit;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px rgba(26,115,232,.2);
}
/* Google-style labels */
label {
font-size: 14px;
font-weight: 500;
color: #3c4043;
margin-bottom: 4px;
display: block;
}