11 Commits

Author SHA1 Message Date
Connor Johnstone
d089f1545b Fixing the gitea error
Some checks failed
Build and Push Docker Image / docker (push) Failing after 22s
2025-09-02 12:34:47 -04:00
Connor Johnstone
7b06fef6c3 Revert "Fix Gitea action Docker build tag error"
This reverts commit 7be9f5a869.
2025-09-02 12:33:17 -04:00
Connor Johnstone
7be9f5a869 Fix Gitea action Docker build tag error
- Add fallback registry to prevent invalid tag format
- Make Docker login conditional on secrets being present
- Make push conditional on registry being configured
- Rename Docker image from calendar to runway

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 12:32:34 -04:00
Connor Johnstone
a7ebbe0635 Add application screenshot to README
Shows Runway's week view with events, all-day events, and dark theme

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 11:23:13 -04:00
Connor Johnstone
3662f117f5 Fix overlapping events to only split columns for overlapping event groups
Implemented clustering algorithm in calculate_event_layout that:
- Only creates column splits for events that actually overlap
- Non-overlapping events maintain full width display
- Uses greedy column assignment for overlapping groups
- Preserves proper column indices for each event

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 11:20:37 -04:00
Connor Johnstone
0899a84b42 Fix all-day events: validation and proper header positioning
Backend fixes:
- Fix all-day event creation validation error
- Allow same start/end date for all-day events (single-day events)
- Maintain strict validation for timed events (end must be after start)

Frontend improvements:
- Move all-day events from time grid to day headers
- Add dedicated all-day events container that stacks vertically
- Filter all-day events out of main time-based events area
- Add proper CSS styling for all-day event display and interaction
- Maintain event click handling and color themes

All-day events now appear in the correct location at the top of each
day column and properly stack when multiple events exist.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 11:13:54 -04:00
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
10 changed files with 308 additions and 41 deletions

View File

@@ -28,7 +28,7 @@ jobs:
context: . context: .
push: true push: true
tags: | tags: |
${{ secrets.DOCKER_REGISTRY }}/calendar:latest ${{ secrets.DOCKER_REGISTRY }}/connor/calendar:latest
${{ secrets.DOCKER_REGISTRY }}/calendar:${{ github.sha }} ${{ secrets.DOCKER_REGISTRY }}/connor/calendar:${{ github.sha }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

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 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) # 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 # Copy actual source code and build the frontend application
RUN rm -rf frontend RUN rm -rf frontend
@@ -55,7 +55,7 @@ COPY calendar-models ./calendar-models
# Create empty frontend directory to satisfy workspace # Create empty frontend directory to satisfy workspace
RUN mkdir -p frontend/src && \ 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 echo 'fn main() {}' > frontend/src/main.rs
# Create dummy backend source to build dependencies first # Create dummy backend source to build dependencies first

View File

@@ -1,13 +1,22 @@
# Modern CalDAV Web Client # Runway
## _Passive infrastructure for life's coordination_
![Runway Screenshot](sample.png)
>[!WARNING] >[!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. >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 ## Features
@@ -63,7 +72,7 @@ While there are many excellent self-hosted CalDAV server implementations (Nextcl
### Docker Deployment (Recommended) ### 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**: 1. **Clone the repository**:
```bash ```bash
@@ -162,7 +171,7 @@ calendar/
This client is designed to work with any RFC-compliant CalDAV server: This client is designed to work with any RFC-compliant CalDAV server:
- **Baikal** - ✅ Fully tested with complete event and recurrence support - **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 - **Radicale** - 🚧 Planned lightweight CalDAV server support
- **Apple Calendar Server** - 🚧 Planned standards-compliant operation - **Apple Calendar Server** - 🚧 Planned standards-compliant operation
- **Google Calendar** - 🚧 Planned CalDAV API compatibility - **Google Calendar** - 🚧 Planned CalDAV API compatibility

View File

@@ -417,12 +417,20 @@ pub async fn create_event(
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
// Validate that end is after start // Validate that end is after start (allow equal times for all-day events)
if request.all_day {
if end_datetime < start_datetime {
return Err(ApiError::BadRequest(
"End date must be on or after start date for all-day events".to_string(),
));
}
} else {
if end_datetime <= start_datetime { if end_datetime <= start_datetime {
return Err(ApiError::BadRequest( return Err(ApiError::BadRequest(
"End date/time must be after start date/time".to_string(), "End date/time must be after start date/time".to_string(),
)); ));
} }
}
// Generate a unique UID for the event // Generate a unique UID for the event
let uid = format!( let uid = format!(
@@ -707,12 +715,20 @@ pub async fn update_event(
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
// Validate that end is after start // Validate that end is after start (allow equal times for all-day events)
if request.all_day {
if end_datetime < start_datetime {
return Err(ApiError::BadRequest(
"End date must be on or after start date for all-day events".to_string(),
));
}
} else {
if end_datetime <= start_datetime { if end_datetime <= start_datetime {
return Err(ApiError::BadRequest( return Err(ApiError::BadRequest(
"End date/time must be after start date/time".to_string(), "End date/time must be after start date/time".to_string(),
)); ));
} }
}
// Update event properties // Update event properties
event.dtstart = start_datetime; event.dtstart = start_datetime;

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "calendar-app" name = "runway"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View File

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

View File

@@ -158,7 +158,7 @@ pub fn sidebar(props: &SidebarProps) -> Html {
html! { html! {
<aside class="app-sidebar"> <aside class="app-sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<h1>{"Calendar App"}</h1> <h1>{"Runway"}</h1>
{ {
if let Some(ref info) = props.user_info { if let Some(ref info) = props.user_info {
html! { html! {
@@ -219,7 +219,6 @@ pub fn sidebar(props: &SidebarProps) -> Html {
</div> </div>
<div class="theme-selector"> <div class="theme-selector">
<label>{"Theme:"}</label>
<select class="theme-selector-dropdown" onchange={on_theme_change}> <select class="theme-selector-dropdown" onchange={on_theme_change}>
<option value="default" selected={matches!(props.current_theme, Theme::Default)}>{"Default"}</option> <option value="default" selected={matches!(props.current_theme, Theme::Default)}>{"Default"}</option>
<option value="ocean" selected={matches!(props.current_theme, Theme::Ocean)}>{"Ocean"}</option> <option value="ocean" selected={matches!(props.current_theme, Theme::Ocean)}>{"Ocean"}</option>
@@ -233,7 +232,6 @@ pub fn sidebar(props: &SidebarProps) -> Html {
</div> </div>
<div class="style-selector"> <div class="style-selector">
<label>{"Style:"}</label>
<select class="style-selector-dropdown" onchange={on_style_change}> <select class="style-selector-dropdown" onchange={on_style_change}>
<option value="default" selected={matches!(props.current_style, Style::Default)}>{"Default"}</option> <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="google" selected={matches!(props.current_style, Style::Google)}>{"Google Calendar"}</option>

View File

@@ -319,12 +319,53 @@ pub fn week_view(props: &WeekViewProps) -> Html {
week_days.iter().map(|date| { week_days.iter().map(|date| {
let is_today = *date == props.today; let is_today = *date == props.today;
let weekday_name = get_weekday_name(date.weekday()); let weekday_name = get_weekday_name(date.weekday());
let day_events = props.events.get(date).cloned().unwrap_or_default();
// Filter for all-day events only
let all_day_events: Vec<_> = day_events.iter().filter(|event| event.all_day).collect();
html! { html! {
<div class={classes!("week-day-header", if is_today { Some("today") } else { None })}> <div class={classes!("week-day-header", if is_today { Some("today") } else { None })}>
<div class="day-header-content">
<div class="weekday-name">{weekday_name}</div> <div class="weekday-name">{weekday_name}</div>
<div class="day-number">{date.day()}</div> <div class="day-number">{date.day()}</div>
</div> </div>
// All-day events section
{if !all_day_events.is_empty() {
html! {
<div class="all-day-events">
{
all_day_events.iter().map(|event| {
let event_color = get_event_color(event);
let onclick = {
let on_event_click = props.on_event_click.clone();
let event = (*event).clone();
Callback::from(move |e: MouseEvent| {
e.stop_propagation();
on_event_click.emit(event.clone());
})
};
html! {
<div
class="all-day-event"
style={format!("background-color: {}", event_color)}
{onclick}
>
<span class="all-day-event-title">
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
</span>
</div>
}
}).collect::<Html>()
}
</div>
}
} else {
html! {}
}}
</div>
} }
}).collect::<Html>() }).collect::<Html>()
} }
@@ -353,6 +394,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
week_days.iter().enumerate().map(|(_column_index, date)| { week_days.iter().enumerate().map(|(_column_index, date)| {
let is_today = *date == props.today; let is_today = *date == props.today;
let day_events = props.events.get(date).cloned().unwrap_or_default(); let day_events = props.events.get(date).cloned().unwrap_or_default();
let event_layouts = calculate_event_layout(&day_events, *date);
// Drag event handlers // Drag event handlers
let drag_state_clone = drag_state.clone(); let drag_state_clone = drag_state.clone();
@@ -398,6 +440,13 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let drag_state = drag_state_clone.clone(); let drag_state = drag_state_clone.clone();
let time_increment = props.time_increment; let time_increment = props.time_increment;
Callback::from(move |e: MouseEvent| { 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 let Some(mut current_drag) = (*drag_state).clone() {
if current_drag.is_dragging { if current_drag.is_dragging {
// Use layer_y for consistent coordinate calculation // Use layer_y for consistent coordinate calculation
@@ -567,9 +616,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! { html! {
<div <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} {onmousedown}
{onmousemove} {onmousemove}
{onmouseup} {onmouseup}
@@ -589,11 +649,16 @@ pub fn week_view(props: &WeekViewProps) -> Html {
// Events positioned absolutely based on their actual times // Events positioned absolutely based on their actual times
<div class="events-container"> <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); let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date);
// Skip all-day events (they're rendered in the header)
if is_all_day {
return None;
}
// Skip events that don't belong on this date or have invalid positioning // Skip events that don't belong on this date or have invalid positioning
if start_pixels == 0.0 && duration_pixels == 0.0 && !is_all_day { if start_pixels == 0.0 && duration_pixels == 0.0 {
return None; return None;
} }
@@ -782,12 +847,28 @@ pub fn week_view(props: &WeekViewProps) -> Html {
if is_refreshing { Some("refreshing") } else { None }, if is_refreshing { Some("refreshing") } else { None },
if is_all_day { Some("all-day") } else { None } if is_all_day { Some("all-day") } else { None }
)} )}
style={format!( style={
"background-color: {}; top: {}px; height: {}px;", 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, event_color,
start_pixels, start_pixels,
duration_pixels duration_pixels,
)} left_offset,
column_width
)
}
{onclick} {onclick}
{oncontextmenu} {oncontextmenu}
onmousedown={onmousedown_event} onmousedown={onmousedown_event}
@@ -835,7 +916,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
// Temporary event box during drag // Temporary event box during drag
{ {
if let Some(drag) = (*drag_state).clone() { 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 { match &drag.drag_type {
DragType::CreateEvent => { DragType::CreateEvent => {
let start_y = drag.start_y.min(drag.current_y); let start_y = drag.start_y.min(drag.current_y);
@@ -1065,3 +1146,111 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool)
(start_pixels, duration_pixels, false) // is_all_day = false (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 and sort events that should appear on this date
let mut day_events: Vec<_> = events.iter()
.enumerate()
.filter_map(|(idx, event)| {
let (_, _, _) = calculate_event_position(event, date);
let local_start = event.dtstart.with_timezone(&Local);
let event_date = local_start.date_naive();
if event_date == date ||
(event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20) {
Some((idx, event))
} else {
None
}
})
.collect();
// Sort by start time
day_events.sort_by_key(|(_, event)| event.dtstart.with_timezone(&Local).naive_local());
// For each event, find all events it overlaps with
let mut event_columns = vec![(0, 1); events.len()]; // (column_idx, total_columns)
for i in 0..day_events.len() {
let (orig_idx_i, event_i) = day_events[i];
// Find all events that overlap with this event
let mut overlapping_events = vec![i];
for j in 0..day_events.len() {
if i != j {
let (_, event_j) = day_events[j];
if events_overlap(event_i, event_j) {
overlapping_events.push(j);
}
}
}
// If this event doesn't overlap with anything, it gets full width
if overlapping_events.len() == 1 {
event_columns[orig_idx_i] = (0, 1);
} else {
// This event overlaps - we need to calculate column layout
// Sort the overlapping group by start time
overlapping_events.sort_by_key(|&idx| day_events[idx].1.dtstart.with_timezone(&Local).naive_local());
// Assign columns using a greedy algorithm
let mut columns: Vec<Vec<usize>> = Vec::new();
for &event_idx in &overlapping_events {
let (orig_idx, event) = day_events[event_idx];
// Find the first column where this event doesn't overlap with existing events
let mut placed = false;
for (col_idx, column) in columns.iter_mut().enumerate() {
let can_place = column.iter().all(|&existing_idx| {
let (_, existing_event) = day_events[existing_idx];
!events_overlap(event, existing_event)
});
if can_place {
column.push(event_idx);
event_columns[orig_idx] = (col_idx, columns.len());
placed = true;
break;
}
}
if !placed {
// Create new column
columns.push(vec![event_idx]);
event_columns[orig_idx] = (columns.len() - 1, columns.len());
}
}
// Update total_columns for all events in this overlapping group
let total_columns = columns.len();
for &event_idx in &overlapping_events {
let (orig_idx, _) = day_events[event_idx];
event_columns[orig_idx].1 = total_columns;
}
}
}
event_columns
}

View File

@@ -650,11 +650,14 @@ body {
} }
.week-day-header { .week-day-header {
padding: 1rem; padding: 0.5rem;
text-align: center; text-align: center;
border-right: 1px solid var(--time-label-border, #e9ecef); border-right: 1px solid var(--time-label-border, #e9ecef);
background: var(--weekday-header-bg, #f8f9fa); background: var(--weekday-header-bg, #f8f9fa);
color: var(--weekday-header-text, inherit); color: var(--weekday-header-text, inherit);
min-height: 70px; /* Ensure space for all-day events */
display: flex;
flex-direction: column;
} }
.week-day-header.today { .week-day-header.today {
@@ -680,6 +683,45 @@ body {
color: var(--calendar-today-text, #1976d2); color: var(--calendar-today-text, #1976d2);
} }
/* All-day events in header */
.day-header-content {
flex-shrink: 0;
}
.all-day-events {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 0.5rem;
min-height: 0;
}
.all-day-event {
background: #3B82F6;
color: white;
border-radius: 4px;
padding: 2px 6px;
font-size: 0.75rem;
text-align: left;
cursor: pointer;
border: 1px solid rgba(255,255,255,0.2);
min-height: 18px;
display: flex;
align-items: center;
}
.all-day-event:hover {
filter: brightness(1.1);
}
.all-day-event-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
/* Week Content */ /* Week Content */
.week-content { .week-content {
flex: 1; flex: 1;
@@ -782,8 +824,7 @@ body {
/* Week Events */ /* Week Events */
.week-event { .week-event {
position: absolute !important; position: absolute !important;
left: 4px; /* left and width are now set inline for overlap handling */
right: 4px;
min-height: 20px; min-height: 20px;
background: #3B82F6; background: #3B82F6;
color: white; color: white;
@@ -803,6 +844,20 @@ body {
white-space: nowrap; 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 { .week-event:hover {
filter: brightness(1.1); filter: brightness(1.1);
z-index: 4; z-index: 4;

BIN
sample.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB