4 Commits

Author SHA1 Message Date
Connor Johnstone
c0bdd3d8c2 Add theme-aware styling for 15-minute time grid lines
All checks were successful
Build and Push Docker Image / docker (push) Successful in 30s
Add --calendar-border-light CSS variables to all 8 color themes for proper
15-minute grid line styling. Previously used hard-coded fallback (#f8f8f8)
which was too bright for dark mode and inconsistent with theme colors.

- Dark mode: Use subtle #2a2a2a instead of bright #f8f8f8
- All themes: Theme-appropriate very light border colors
- Better visual integration with each color scheme
- Consistent dotted 15-minute grid lines across all themes

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 15:42:29 -04:00
Connor Johnstone
2b98c4d229 Hide event time display for single-slot events
Hide time display for events with duration <= 30px (single time slots)
to maximize space for event titles in compact event boxes.

- Single-slot events show title only for better readability
- Applies to both 15-minute and 30-minute time increment modes
- Consistent behavior across static events and drag previews
- Improves UX for short duration events where time display crowds the title

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 15:39:15 -04:00
Connor Johnstone
ceae654a39 Implement dynamic 15-minute time grid density and remove final boundary
- Scale time grid height dynamically based on time increment (1530px/2970px)
- Add quarter-mode CSS classes for 15-minute blocks (30px each, same as 30-min blocks)
- Update pixel-to-time conversion functions with 2px:1min scaling in 15-min mode
- Generate correct number of time slots (4 per hour in 15-min mode)
- Remove unnecessary final boundary time label and related CSS
- Fix CSS grid layout by removing malformed CSS syntax
- All time-related containers scale properly between 30-minute and 15-minute modes

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 15:35:50 -04:00
Connor Johnstone
fb28fa95c9 Fix recurring events from previous years not showing up
Extend recurring event expansion date range from 30 days to 100 years in the past.
This ensures yearly recurring events created many years ago (birthdays, anniversaries,
historical dates) properly generate their current year occurrences.

The backend correctly includes old recurring events that could have occurrences in
the requested month, but the frontend was only expanding occurrences within a
30-day historical window, missing events from previous years.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 13:19:17 -04:00
4 changed files with 139 additions and 67 deletions

View File

@@ -95,8 +95,8 @@ pub fn week_view(props: &WeekViewProps) -> Html {
"#3B82F6".to_string()
};
// Generate time labels - 24 hours plus the final midnight boundary
let mut time_labels: Vec<String> = (0..24)
// Generate time labels - 24 hours
let time_labels: Vec<String> = (0..24)
.map(|hour| {
if hour == 0 {
"12 AM".to_string()
@@ -110,9 +110,6 @@ pub fn week_view(props: &WeekViewProps) -> Html {
})
.collect();
// Add the final midnight boundary to show where the day ends
time_labels.push("12 AM".to_string());
// Handlers for recurring event modification modal
let on_recurring_choice = {
let pending_recurring_edit = pending_recurring_edit.clone();
@@ -388,14 +385,17 @@ pub fn week_view(props: &WeekViewProps) -> Html {
// Scrollable content area with time grid
<div class="week-content">
<div class="time-grid">
<div class={classes!("time-grid", if props.time_increment == 15 { Some("quarter-mode") } else { None })}>
// Time labels
<div class="time-labels">
<div class={classes!("time-labels", if props.time_increment == 15 { Some("quarter-mode") } else { None })}>
{
time_labels.iter().enumerate().map(|(index, time)| {
let is_final = index == time_labels.len() - 1;
time_labels.iter().map(|time| {
let is_quarter_mode = props.time_increment == 15;
html! {
<div class={classes!("time-label", if is_final { Some("final-boundary") } else { None })}>
<div class={classes!(
"time-label",
if is_quarter_mode { Some("quarter-mode") } else { None }
)}>
{time}
</div>
}
@@ -404,12 +404,12 @@ pub fn week_view(props: &WeekViewProps) -> Html {
</div>
// Day columns
<div class="week-days-grid">
<div class={classes!("week-days-grid", if props.time_increment == 15 { Some("quarter-mode") } else { None })}>
{
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);
let event_layouts = calculate_event_layout(&day_events, *date, props.time_increment);
// Drag event handlers
let drag_state_clone = drag_state.clone();
@@ -500,8 +500,8 @@ pub fn week_view(props: &WeekViewProps) -> Html {
match &current_drag.drag_type {
DragType::CreateEvent => {
// Calculate start and end times
let start_time = pixels_to_time(current_drag.start_y);
let end_time = pixels_to_time(current_drag.current_y);
let start_time = pixels_to_time(current_drag.start_y, time_increment);
let end_time = pixels_to_time(current_drag.current_y, time_increment);
// Ensure start is before end
let (actual_start, actual_end) = if start_time <= end_time {
@@ -529,7 +529,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let unsnapped_position = current_drag.current_y - current_drag.offset_y;
// Snap the final position to maintain time increment alignment
let event_top_position = snap_to_increment(unsnapped_position, time_increment);
let new_start_time = pixels_to_time(event_top_position);
let new_start_time = pixels_to_time(event_top_position, time_increment);
// Calculate duration from original event
let original_duration = if let Some(end) = event.dtend {
@@ -558,7 +558,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
},
DragType::ResizeEventStart(event) => {
// Calculate new start time based on drag position
let new_start_time = pixels_to_time(current_drag.current_y);
let new_start_time = pixels_to_time(current_drag.current_y, time_increment);
// Keep the original end time
let original_end = if let Some(end) = event.dtend {
@@ -594,7 +594,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
},
DragType::ResizeEventEnd(event) => {
// Calculate new end time based on drag position
let new_end_time = pixels_to_time(current_drag.current_y);
let new_end_time = pixels_to_time(current_drag.current_y, time_increment);
// Keep the original start time
let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local();
@@ -643,7 +643,8 @@ pub fn week_view(props: &WeekViewProps) -> Html {
class={classes!(
"week-day-column",
if is_today { Some("today") } else { None },
if is_creating_event { Some("creating-event") } else { None }
if is_creating_event { Some("creating-event") } else { None },
if props.time_increment == 15 { Some("quarter-mode") } else { None }
)}
{onmousedown}
{onmousemove}
@@ -652,10 +653,21 @@ pub fn week_view(props: &WeekViewProps) -> Html {
// Time slot backgrounds - 24 hour slots to represent full day
{
(0..24).map(|_hour| {
let slots_per_hour = 60 / props.time_increment;
html! {
<div class="time-slot">
<div class="time-slot-half"></div>
<div class="time-slot-half"></div>
<div class={classes!("time-slot", if props.time_increment == 15 { Some("quarter-mode") } else { None })}>
{
(0..slots_per_hour).map(|_slot| {
let slot_class = if props.time_increment == 15 {
"time-slot-quarter"
} else {
"time-slot-half"
};
html! {
<div class={slot_class}></div>
}
}).collect::<Html>()
}
</div>
}
}).collect::<Html>()
@@ -665,7 +677,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
<div class="events-container">
{
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, props.time_increment);
// Skip all-day events (they're rendered in the header)
if is_all_day {
@@ -693,7 +705,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let drag_state = drag_state.clone();
let event_for_drag = event.clone();
let date_for_drag = *date;
let _time_increment = props.time_increment;
let time_increment = props.time_increment;
Callback::from(move |e: MouseEvent| {
e.stop_propagation(); // Prevent drag-to-create from starting on event clicks
@@ -707,7 +719,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let click_y_relative = if click_y_relative > 0.0 { click_y_relative } else { e.offset_y() as f64 };
// Get event's current position in day column coordinates
let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag);
let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag, time_increment);
let event_start_pixels = event_start_pixels as f64;
// Convert click position to day column coordinates
@@ -903,7 +915,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
// Event content
<div class="event-content">
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
{if !is_all_day {
{if !is_all_day && duration_pixels > 30.0 {
html! { <div class="event-time">{time_display}</div> }
} else {
html! {}
@@ -939,8 +951,8 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let height = (drag.current_y - drag.start_y).abs().max(20.0);
// Convert pixels to times for display
let start_time = pixels_to_time(start_y);
let end_time = pixels_to_time(end_y);
let start_time = pixels_to_time(start_y, props.time_increment);
let end_time = pixels_to_time(end_y, props.time_increment);
html! {
<div
@@ -956,7 +968,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let unsnapped_position = drag.current_y - drag.offset_y;
// Snap the final position to maintain time increment alignment
let preview_position = snap_to_increment(unsnapped_position, props.time_increment);
let new_start_time = pixels_to_time(preview_position);
let new_start_time = pixels_to_time(preview_position, props.time_increment);
let original_duration = if let Some(end) = event.dtend {
end.signed_duration_since(event.dtstart)
} else {
@@ -973,13 +985,17 @@ pub fn week_view(props: &WeekViewProps) -> Html {
style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", preview_position, duration_pixels, event_color)}
>
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
<div class="event-time">{format!("{} - {}", new_start_time.format("%I:%M %p"), new_end_time.format("%I:%M %p"))}</div>
{if duration_pixels > 30.0 {
html! { <div class="event-time">{format!("{} - {}", new_start_time.format("%I:%M %p"), new_end_time.format("%I:%M %p"))}</div> }
} else {
html! {}
}}
</div>
}
},
DragType::ResizeEventStart(event) => {
// Show the event being resized from the start
let new_start_time = pixels_to_time(drag.current_y);
let new_start_time = pixels_to_time(drag.current_y, props.time_increment);
let original_end = if let Some(end) = event.dtend {
end.with_timezone(&chrono::Local).naive_local()
} else {
@@ -987,7 +1003,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
};
// Calculate positions for the preview
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date);
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment);
let original_duration = original_end.signed_duration_since(event.dtstart.with_timezone(&chrono::Local).naive_local());
let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32);
@@ -1002,17 +1018,21 @@ pub fn week_view(props: &WeekViewProps) -> Html {
style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", new_start_pixels, new_height, event_color)}
>
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
<div class="event-time">{format!("{} - {}", new_start_time.format("%I:%M %p"), original_end.time().format("%I:%M %p"))}</div>
{if new_height > 30.0 {
html! { <div class="event-time">{format!("{} - {}", new_start_time.format("%I:%M %p"), original_end.time().format("%I:%M %p"))}</div> }
} else {
html! {}
}}
</div>
}
},
DragType::ResizeEventEnd(event) => {
// Show the event being resized from the end
let new_end_time = pixels_to_time(drag.current_y);
let new_end_time = pixels_to_time(drag.current_y, props.time_increment);
let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local();
// Calculate positions for the preview
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date);
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment);
let new_end_pixels = drag.current_y;
let new_height = (new_end_pixels - original_start_pixels as f64).max(20.0);
@@ -1025,7 +1045,11 @@ pub fn week_view(props: &WeekViewProps) -> Html {
style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", original_start_pixels, new_height, event_color)}
>
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
<div class="event-time">{format!("{} - {}", original_start.time().format("%I:%M %p"), new_end_time.format("%I:%M %p"))}</div>
{if new_height > 30.0 {
html! { <div class="event-time">{format!("{} - {}", original_start.time().format("%I:%M %p"), new_end_time.format("%I:%M %p"))}</div> }
} else {
html! {}
}}
</div>
}
}
@@ -1089,22 +1113,25 @@ fn get_weekday_name(weekday: Weekday) -> &'static str {
}
// Calculate the pixel position of an event based on its time
// Each hour is 60px, so we convert time to pixels
// Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes)
// Snap pixel position based on time increment and grid scaling
// In 30-minute mode: 60px per hour (1px = 1 minute)
// In 15-minute mode: 120px per hour (2px = 1 minute)
fn snap_to_increment(pixels: f64, increment: u32) -> f64 {
let increment_px = increment as f64; // Convert to pixels (1px = 1 minute)
let pixels_per_minute = if increment == 15 { 2.0 } else { 1.0 };
let increment_px = increment as f64 * pixels_per_minute;
(pixels / increment_px).round() * increment_px
}
// Convert pixel position to time (inverse of time to pixels)
fn pixels_to_time(pixels: f64) -> NaiveTime {
// Since 60px = 1 hour, pixels directly represent minutes
let total_minutes = pixels; // 1px = 1 minute
fn pixels_to_time(pixels: f64, time_increment: u32) -> NaiveTime {
let pixels_per_minute = if time_increment == 15 { 2.0 } else { 1.0 };
let total_minutes = pixels / pixels_per_minute;
let hours = (total_minutes / 60.0) as u32;
let minutes = (total_minutes % 60.0) as u32;
// Handle midnight boundary - if we're at exactly 1440 pixels (24:00), return midnight
if total_minutes >= 1440.0 {
// Handle midnight boundary - check against scaled boundary
let max_pixels = 1440.0 * pixels_per_minute; // 24 hours in pixels
if pixels >= max_pixels {
return NaiveTime::from_hms_opt(0, 0, 0).unwrap();
}
@@ -1115,7 +1142,7 @@ fn pixels_to_time(pixels: f64) -> NaiveTime {
NaiveTime::from_hms_opt(hours, minutes, 0).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
}
fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool) {
fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32) -> (f32, f32, bool) {
// Convert UTC times to local time for display
let local_start = event.dtstart.with_timezone(&Local);
let event_date = local_start.date_naive();
@@ -1138,7 +1165,8 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool)
// Calculate start position in pixels from midnight
let start_hour = local_start.hour() as f32;
let start_minute = local_start.minute() as f32;
let start_pixels = (start_hour + start_minute / 60.0) * 60.0; // 60px per hour
let pixels_per_hour = if time_increment == 15 { 120.0 } else { 60.0 };
let start_pixels = (start_hour + start_minute / 60.0) * pixels_per_hour;
// Calculate duration and height
let duration_pixels = if let Some(end) = event.dtend {
@@ -1147,16 +1175,17 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool)
// Handle events that span multiple days by capping at midnight
if end_date > date {
// Event continues past midnight, cap at 24:00 (1440px)
1440.0 - start_pixels
// Event continues past midnight, cap at 24:00
let max_pixels = 24.0 * pixels_per_hour;
max_pixels - start_pixels
} else {
let end_hour = local_end.hour() as f32;
let end_minute = local_end.minute() as f32;
let end_pixels = (end_hour + end_minute / 60.0) * 60.0;
let end_pixels = (end_hour + end_minute / 60.0) * pixels_per_hour;
(end_pixels - start_pixels).max(20.0) // Minimum 20px height
}
} else {
60.0 // Default 1 hour if no end time
pixels_per_hour // Default 1 hour if no end time
};
(start_pixels, duration_pixels, false) // is_all_day = false
@@ -1183,13 +1212,13 @@ fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool {
}
// Calculate layout columns for overlapping events
fn calculate_event_layout(events: &[VEvent], date: NaiveDate) -> Vec<(usize, usize)> {
fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u32) -> 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 (_, _, _) = calculate_event_position(event, date, time_increment);
let local_start = event.dtstart.with_timezone(&Local);
let event_date = local_start.date_naive();
if event_date == date ||

View File

@@ -271,8 +271,8 @@ impl CalendarService {
pub fn expand_recurring_events(events: Vec<VEvent>) -> Vec<VEvent> {
let mut expanded_events = Vec::new();
let today = chrono::Utc::now().date_naive();
let start_range = today - Duration::days(30); // Show past 30 days
let end_range = today + Duration::days(365); // Show next 365 days
let start_range = today - Duration::days(36500); // Show past 100 years (to catch any historical yearly events)
let end_range = today + Duration::days(36500); // Show next 100 years
for event in events {
if let Some(ref rrule) = event.rrule {

View File

@@ -733,7 +733,11 @@ body {
.time-grid {
display: grid;
grid-template-columns: 80px 1fr;
min-height: 1530px;
min-height: 1530px; /* 30-minute mode */
}
.time-grid.quarter-mode {
min-height: 2970px; /* 15-minute mode */
}
/* Time Labels */
@@ -743,9 +747,15 @@ body {
position: sticky;
left: 0;
z-index: 5;
min-height: 1440px; /* Match the time slots height */
min-height: 1530px; /* 30-minute mode */
}
/* Scale time labels container for 15-minute mode */
.time-labels.quarter-mode {
min-height: 2970px; /* 15-minute mode */
}
/* Default time label height for 30-minute mode */
.time-label {
height: 60px;
display: flex;
@@ -758,24 +768,31 @@ body {
font-weight: 500;
}
.time-label.final-boundary {
height: 60px; /* Keep same height but this marks the end boundary */
border-bottom: 2px solid #e9ecef; /* Stronger border to show day end */
color: #999; /* Lighter color to indicate it's the boundary */
font-size: 0.7rem;
/* Time label height for 15-minute mode - double height */
.time-label.quarter-mode {
height: 120px;
}
/* Week Days Grid */
.week-days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
min-height: 1440px; /* Ensure grid is tall enough for 24 time slots */
min-height: 1530px; /* 30-minute mode */
}
.week-days-grid.quarter-mode {
min-height: 2970px; /* 15-minute mode */
}
.week-day-column {
position: relative;
border-right: 1px solid var(--time-label-border, #e9ecef);
min-height: 1440px; /* 24 time slots × 60px = 1440px total */
min-height: 1530px; /* 30-minute mode */
}
.week-day-column.quarter-mode {
min-height: 2970px; /* 15-minute mode */
}
.week-day-column:last-child {
@@ -788,12 +805,16 @@ body {
/* Time Slots */
.time-slot {
height: 60px;
height: 60px; /* 30-minute mode: 2 slots × 30px = 60px */
border-bottom: 1px solid var(--calendar-border, #f0f0f0);
position: relative;
pointer-events: none; /* Don't capture mouse events */
}
.time-slot.quarter-mode {
height: 120px; /* 15-minute mode: 4 slots × 30px = 120px */
}
.time-slot-half {
height: 30px;
border-bottom: 1px dotted var(--calendar-border, #f5f5f5);
@@ -804,13 +825,17 @@ body {
border-bottom: none;
}
.time-slot.boundary-slot {
height: 60px; /* Match the final time label height */
border-bottom: 2px solid #e9ecef; /* Strong border to match final boundary */
background: rgba(0,0,0,0.02); /* Slightly different background to indicate boundary */
.time-slot-quarter {
height: 30px;
border-bottom: 1px dotted var(--calendar-border-light, #f8f8f8);
pointer-events: none; /* Don't capture mouse events */
}
.time-slot-quarter:last-child {
border-bottom: none;
}
/* Events Container */
.events-container {
position: absolute;
@@ -3242,6 +3267,7 @@ body {
--accent-color: #667eea;
--calendar-bg: white;
--calendar-border: #f0f0f0;
--calendar-border-light: #f8f8f8;
--calendar-day-bg: white;
--calendar-day-hover: #f8f9ff;
--calendar-day-prev-next: #fafafa;
@@ -3275,6 +3301,7 @@ body {
--accent-color: #2196F3;
--calendar-bg: #ffffff;
--calendar-border: #bbdefb;
--calendar-border-light: #e3f2fd;
--calendar-day-bg: #ffffff;
--calendar-day-hover: #e1f5fe;
--calendar-day-prev-next: #f3f8ff;
@@ -3317,6 +3344,7 @@ body {
--accent-color: #4CAF50;
--calendar-bg: #ffffff;
--calendar-border: #c8e6c9;
--calendar-border-light: #e8f5e8;
--calendar-day-bg: #ffffff;
--calendar-day-hover: #f1f8e9;
--calendar-day-prev-next: #f9fbe7;
@@ -3359,6 +3387,7 @@ body {
--accent-color: #FF9800;
--calendar-bg: #ffffff;
--calendar-border: #ffe0b2;
--calendar-border-light: #fff3e0;
--calendar-day-bg: #ffffff;
--calendar-day-hover: #fff8e1;
--calendar-day-prev-next: #fffde7;
@@ -3401,6 +3430,7 @@ body {
--accent-color: #9C27B0;
--calendar-bg: #ffffff;
--calendar-border: #ce93d8;
--calendar-border-light: #f3e5f5;
--calendar-day-bg: #ffffff;
--calendar-day-hover: #f8e9fc;
--calendar-day-prev-next: #fce4ec;
@@ -3443,6 +3473,7 @@ body {
--accent-color: #666666;
--calendar-bg: #1f1f1f;
--calendar-border: #333333;
--calendar-border-light: #2a2a2a;
--calendar-day-bg: #1f1f1f;
--calendar-day-hover: #2a2a2a;
--calendar-day-prev-next: #1a1a1a;
@@ -3495,6 +3526,7 @@ body {
--accent-color: #E91E63;
--calendar-bg: #ffffff;
--calendar-border: #f8bbd9;
--calendar-border-light: #fce4ec;
--calendar-day-bg: #ffffff;
--calendar-day-hover: #fdf2f8;
--calendar-day-prev-next: #fef7ff;
@@ -3537,6 +3569,7 @@ body {
--accent-color: #26A69A;
--calendar-bg: #ffffff;
--calendar-border: #b2dfdb;
--calendar-border-light: #e0f2f1;
--calendar-day-bg: #ffffff;
--calendar-day-hover: #f0fdfc;
--calendar-day-prev-next: #f7ffff;

View File

@@ -648,6 +648,16 @@ body {
border-bottom: none;
}
.time-slot-quarter {
height: 30px;
border-bottom: 1px dotted var(--calendar-border-light, #f8f8f8);
pointer-events: none; /* Don't capture mouse events */
}
.time-slot-quarter:last-child {
border-bottom: none;
}
.time-slot.boundary-slot {
height: 60px; /* Match the final time label height */
border-bottom: 2px solid #e9ecef; /* Strong border to match final boundary */