Add mini-calendar sidebar to Calendar TUI
- Add MonthCalendar widget as a collapsible sidebar (toggle with 's') - Sidebar syncs with main week grid (week highlight, selected date) - Click dates in sidebar to navigate week grid to that date - Click month navigation arrows to change displayed month - Add goto_date() method to WeekGrid for date navigation
This commit is contained in:
@@ -18,6 +18,7 @@ from textual.reactive import reactive
|
||||
|
||||
from src.calendar.backend import CalendarBackend, Event
|
||||
from src.calendar.widgets.WeekGrid import WeekGrid
|
||||
from src.calendar.widgets.MonthCalendar import MonthCalendar
|
||||
from src.calendar.widgets.AddEventForm import EventFormData
|
||||
from src.utils.shared_config import get_theme_name
|
||||
|
||||
@@ -51,6 +52,26 @@ class CalendarApp(App):
|
||||
Screen {
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
layout: horizontal;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 26;
|
||||
border-right: solid $surface-darken-1;
|
||||
background: $surface;
|
||||
padding: 1 0;
|
||||
}
|
||||
|
||||
#sidebar.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar-calendar {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#week-grid {
|
||||
height: 1fr;
|
||||
@@ -98,6 +119,7 @@ class CalendarApp(App):
|
||||
Binding("L", "next_week", "Next Week", show=True),
|
||||
Binding("g", "goto_today", "Today", show=True),
|
||||
Binding("w", "toggle_weekends", "Weekends", show=True),
|
||||
Binding("s", "toggle_sidebar", "Sidebar", show=True),
|
||||
Binding("r", "refresh", "Refresh", show=True),
|
||||
Binding("enter", "view_event", "View", show=True),
|
||||
Binding("a", "add_event", "Add", show=True),
|
||||
@@ -106,6 +128,7 @@ class CalendarApp(App):
|
||||
|
||||
# Reactive attributes
|
||||
include_weekends: reactive[bool] = reactive(True)
|
||||
show_sidebar: reactive[bool] = reactive(True)
|
||||
|
||||
# Instance attributes
|
||||
backend: Optional[CalendarBackend]
|
||||
@@ -124,7 +147,10 @@ class CalendarApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create the app layout."""
|
||||
yield Header()
|
||||
yield WeekGrid(id="week-grid")
|
||||
with Horizontal(id="main-content"):
|
||||
with Vertical(id="sidebar"):
|
||||
yield MonthCalendar(id="sidebar-calendar")
|
||||
yield WeekGrid(id="week-grid")
|
||||
yield Static(id="event-detail", classes="hidden")
|
||||
yield CalendarStatusBar(id="status-bar")
|
||||
yield Footer()
|
||||
@@ -136,10 +162,23 @@ class CalendarApp(App):
|
||||
# Load events for current week
|
||||
self.load_events()
|
||||
|
||||
# Sync sidebar calendar with current week
|
||||
self._sync_sidebar_calendar()
|
||||
|
||||
# Update status bar and title
|
||||
self._update_status()
|
||||
self._update_title()
|
||||
|
||||
def _sync_sidebar_calendar(self) -> None:
|
||||
"""Sync the sidebar calendar with the main week grid."""
|
||||
try:
|
||||
grid = self.query_one("#week-grid", WeekGrid)
|
||||
calendar = self.query_one("#sidebar-calendar", MonthCalendar)
|
||||
calendar.update_week(grid.week_start)
|
||||
calendar.update_selected(grid.get_cursor_date())
|
||||
except Exception:
|
||||
pass # Sidebar might not exist yet
|
||||
|
||||
def load_events(self) -> None:
|
||||
"""Load events from backend for the current week."""
|
||||
if not self.backend:
|
||||
@@ -255,11 +294,22 @@ class CalendarApp(App):
|
||||
def on_week_grid_week_changed(self, message: WeekGrid.WeekChanged) -> None:
|
||||
"""Handle week change - reload events."""
|
||||
self.load_events()
|
||||
self._sync_sidebar_calendar()
|
||||
|
||||
def on_week_grid_event_selected(self, message: WeekGrid.EventSelected) -> None:
|
||||
"""Handle event selection."""
|
||||
self._update_event_detail(message.event)
|
||||
|
||||
# Handle MonthCalendar messages
|
||||
def on_month_calendar_date_selected(
|
||||
self, message: MonthCalendar.DateSelected
|
||||
) -> None:
|
||||
"""Handle date selection from sidebar calendar."""
|
||||
grid = self.query_one("#week-grid", WeekGrid)
|
||||
grid.goto_date(message.date)
|
||||
self.load_events()
|
||||
self._sync_sidebar_calendar()
|
||||
|
||||
# Navigation actions (forwarded to grid)
|
||||
def action_cursor_down(self) -> None:
|
||||
"""Move cursor down."""
|
||||
@@ -311,6 +361,15 @@ class CalendarApp(App):
|
||||
mode = "7 days" if self.include_weekends else "5 days (weekdays)"
|
||||
self.notify(f"Showing {mode}")
|
||||
|
||||
def action_toggle_sidebar(self) -> None:
|
||||
"""Toggle sidebar visibility."""
|
||||
self.show_sidebar = not self.show_sidebar
|
||||
sidebar = self.query_one("#sidebar", Vertical)
|
||||
if self.show_sidebar:
|
||||
sidebar.remove_class("hidden")
|
||||
else:
|
||||
sidebar.add_class("hidden")
|
||||
|
||||
def action_refresh(self) -> None:
|
||||
"""Refresh events from backend."""
|
||||
self.load_events()
|
||||
@@ -383,6 +442,7 @@ Keybindings:
|
||||
H/L - Previous/Next week
|
||||
g - Go to today
|
||||
w - Toggle weekends (5/7 days)
|
||||
s - Toggle sidebar
|
||||
Enter - View event details
|
||||
a - Add new event
|
||||
r - Refresh
|
||||
|
||||
@@ -240,3 +240,41 @@ class MonthCalendar(Widget):
|
||||
self.display_month = date(year, month, 1)
|
||||
self.post_message(self.MonthChanged(self.display_month))
|
||||
self.refresh()
|
||||
|
||||
def on_click(self, event) -> None:
|
||||
"""Handle mouse clicks on the calendar."""
|
||||
# Row 0 is the month header (< Month Year >)
|
||||
# Row 1 is day names (Mo Tu We ...)
|
||||
# Row 2+ are the week rows
|
||||
y = event.y
|
||||
x = event.x
|
||||
|
||||
if y == 0:
|
||||
# Month header - check for navigation arrows
|
||||
if x < 2:
|
||||
self.prev_month()
|
||||
elif x >= self.size.width - 2:
|
||||
self.next_month()
|
||||
return
|
||||
|
||||
if y == 1:
|
||||
# Day names row - ignore
|
||||
return
|
||||
|
||||
# Week row - calculate which date was clicked
|
||||
week_idx = y - 2
|
||||
weeks = self._weeks
|
||||
if week_idx < 0 or week_idx >= len(weeks):
|
||||
return
|
||||
|
||||
week = weeks[week_idx]
|
||||
# Each day takes 3 characters ("DD "), so find which day column
|
||||
day_col = x // 3
|
||||
if day_col < 0 or day_col >= 7:
|
||||
return
|
||||
|
||||
clicked_date = week[day_col]
|
||||
if clicked_date is not None:
|
||||
self.selected_date = clicked_date
|
||||
self.post_message(self.DateSelected(clicked_date))
|
||||
self.refresh()
|
||||
|
||||
@@ -761,3 +761,20 @@ class WeekGrid(Vertical):
|
||||
event = self.get_event_at_cursor()
|
||||
if event:
|
||||
self.post_message(self.EventSelected(event))
|
||||
|
||||
def goto_date(self, target_date: date) -> None:
|
||||
"""Navigate to a specific date.
|
||||
|
||||
Sets the week to contain the target date and places cursor on that day.
|
||||
"""
|
||||
# Get the week start for the target date
|
||||
week_start_date = get_week_start_for_date(target_date)
|
||||
|
||||
if self.week_start != week_start_date:
|
||||
self.week_start = week_start_date
|
||||
|
||||
# Set cursor column to the target date
|
||||
col = get_day_column_for_date(target_date, self.week_start)
|
||||
if not self.include_weekends and col >= 5:
|
||||
col = 4 # Last weekday if weekend
|
||||
self.cursor_col = col
|
||||
|
||||
Reference in New Issue
Block a user