diff --git a/src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml b/src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml
new file mode 100644
index 0000000..f20d154
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml.cs b/src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml.cs
new file mode 100644
index 0000000..dc6e59f
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml.cs
@@ -0,0 +1,423 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace ClaudeDo.Ui.Views.Controls;
+
+public partial class ThemedDatePicker : UserControl
+{
+ public static readonly StyledProperty SelectedDateProperty =
+ AvaloniaProperty.Register(
+ nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty ShowTimeProperty =
+ AvaloniaProperty.Register(nameof(ShowTime), false);
+
+ public static readonly StyledProperty IsRangeProperty =
+ AvaloniaProperty.Register(nameof(IsRange), false);
+
+ public static readonly StyledProperty StartDateProperty =
+ AvaloniaProperty.Register(
+ nameof(StartDate), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty EndDateProperty =
+ AvaloniaProperty.Register(
+ nameof(EndDate), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty WatermarkProperty =
+ AvaloniaProperty.Register(nameof(Watermark), "Pick a date");
+
+ public static readonly StyledProperty DisplayTextProperty =
+ AvaloniaProperty.Register(nameof(DisplayText));
+
+ public static readonly StyledProperty DisplayForegroundProperty =
+ AvaloniaProperty.Register(nameof(DisplayForeground));
+
+ public static readonly StyledProperty TimeTextProperty =
+ AvaloniaProperty.Register(
+ nameof(TimeText), "09:00",
+ defaultBindingMode: BindingMode.TwoWay);
+
+ public DateTime? SelectedDate
+ {
+ get => GetValue(SelectedDateProperty);
+ set => SetValue(SelectedDateProperty, value);
+ }
+
+ public bool ShowTime
+ {
+ get => GetValue(ShowTimeProperty);
+ set => SetValue(ShowTimeProperty, value);
+ }
+
+ public bool IsRange
+ {
+ get => GetValue(IsRangeProperty);
+ set => SetValue(IsRangeProperty, value);
+ }
+
+ public DateTime? StartDate
+ {
+ get => GetValue(StartDateProperty);
+ set => SetValue(StartDateProperty, value);
+ }
+
+ public DateTime? EndDate
+ {
+ get => GetValue(EndDateProperty);
+ set => SetValue(EndDateProperty, value);
+ }
+
+ public string? Watermark
+ {
+ get => GetValue(WatermarkProperty);
+ set => SetValue(WatermarkProperty, value);
+ }
+
+ public string? DisplayText
+ {
+ get => GetValue(DisplayTextProperty);
+ set => SetValue(DisplayTextProperty, value);
+ }
+
+ public IBrush? DisplayForeground
+ {
+ get => GetValue(DisplayForegroundProperty);
+ set => SetValue(DisplayForegroundProperty, value);
+ }
+
+ public string? TimeText
+ {
+ get => GetValue(TimeTextProperty);
+ set => SetValue(TimeTextProperty, value);
+ }
+
+ private static readonly string[] TimeFormats = { @"h\:mm", @"hh\:mm" };
+
+ private DateTime _displayMonth;
+ private bool _suppressTimeSync;
+
+ public ThemedDatePicker()
+ {
+ InitializeComponent();
+ _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
+ BuildWeekdayHeaders();
+ BuildDayGrid();
+ UpdateDisplayText();
+ UpdateTimeRowVisibility();
+
+ PickerPopup.Opened += OnPopupOpened;
+ }
+
+ private void UpdateTimeRowVisibility()
+ {
+ if (TimeRow is null) return;
+ TimeRow.IsVisible = ShowTime && !IsRange;
+ }
+
+ private void OnPopupOpened(object? sender, EventArgs e)
+ {
+ var seed = AnchorDate() ?? DateTime.Today;
+ _displayMonth = new DateTime(seed.Year, seed.Month, 1);
+ BuildDayGrid();
+ }
+
+ private DateTime? AnchorDate() =>
+ IsRange ? (StartDate ?? EndDate) : SelectedDate;
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ if (change.Property == SelectedDateProperty)
+ {
+ UpdateDisplayText();
+ SyncTimeFromSelected();
+ BuildDayGrid();
+ }
+ else if (change.Property == StartDateProperty || change.Property == EndDateProperty)
+ {
+ UpdateDisplayText();
+ BuildDayGrid();
+ }
+ else if (change.Property == ShowTimeProperty || change.Property == WatermarkProperty
+ || change.Property == IsRangeProperty)
+ {
+ UpdateDisplayText();
+ BuildDayGrid();
+ UpdateTimeRowVisibility();
+ }
+ else if (change.Property == TimeTextProperty)
+ {
+ ApplyTimeTextToSelected();
+ }
+ }
+
+ private void UpdateDisplayText()
+ {
+ if (IsRange)
+ {
+ var (s, end) = NormalizeRange(StartDate?.Date, EndDate?.Date);
+ if (s is null && end is null)
+ {
+ DisplayText = Watermark ?? "Pick a range";
+ DisplayForeground = TryGetBrush("TextDimBrush");
+ return;
+ }
+ if (s is not null && end is null)
+ {
+ DisplayText = $"{s.Value:MMM d} – select end";
+ DisplayForeground = TryGetBrush("TextBrush");
+ return;
+ }
+ // both set
+ var sd = s!.Value;
+ var ed = end!.Value;
+ DisplayText = sd.Year == ed.Year
+ ? $"{sd:MMM d} – {ed:MMM d, yyyy}"
+ : $"{sd:MMM d, yyyy} – {ed:MMM d, yyyy}";
+ DisplayForeground = TryGetBrush("TextBrush");
+ return;
+ }
+
+ if (SelectedDate is null)
+ {
+ DisplayText = Watermark ?? "Pick a date";
+ DisplayForeground = TryGetBrush("TextDimBrush");
+ return;
+ }
+ var d = SelectedDate.Value;
+ DisplayText = ShowTime
+ ? d.ToString("MMM d, yyyy · HH:mm", CultureInfo.CurrentCulture)
+ : d.ToString("MMM d, yyyy", CultureInfo.CurrentCulture);
+ DisplayForeground = TryGetBrush("TextBrush");
+ }
+
+ private static (DateTime? Start, DateTime? End) NormalizeRange(DateTime? a, DateTime? b)
+ {
+ if (a is null && b is null) return (null, null);
+ if (a is null) return (b, b);
+ if (b is null) return (a, null);
+ return a.Value <= b.Value ? (a, b) : (b, a);
+ }
+
+ private IBrush? TryGetBrush(string key)
+ {
+ if (this.TryFindResource(key, out var v) && v is IBrush b) return b;
+ return null;
+ }
+
+ private void SyncTimeFromSelected()
+ {
+ if (SelectedDate is null) return;
+ _suppressTimeSync = true;
+ TimeText = SelectedDate.Value.ToString("HH:mm");
+ _suppressTimeSync = false;
+ }
+
+ private void ApplyTimeTextToSelected()
+ {
+ if (_suppressTimeSync || !ShowTime || IsRange || SelectedDate is null) return;
+ if (TryParseTime(TimeText, out var t))
+ {
+ var d = SelectedDate.Value.Date + t;
+ if (d != SelectedDate) SelectedDate = d;
+ }
+ }
+
+ private static bool TryParseTime(string? text, out TimeSpan ts)
+ {
+ ts = default;
+ if (string.IsNullOrWhiteSpace(text)) return false;
+ return TimeSpan.TryParseExact(text, TimeFormats, CultureInfo.InvariantCulture, out ts);
+ }
+
+ private void BuildWeekdayHeaders()
+ {
+ if (WeekdayHeaders is null) return;
+ WeekdayHeaders.Children.Clear();
+ var firstDow = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
+ var names = CultureInfo.CurrentCulture.DateTimeFormat.AbbreviatedDayNames;
+ for (int i = 0; i < 7; i++)
+ {
+ var dow = (DayOfWeek)(((int)firstDow + i) % 7);
+ var name = names[(int)dow];
+ if (name.Length > 3) name = name.Substring(0, 3);
+ WeekdayHeaders.Children.Add(new TextBlock { Text = name, Classes = { "weekday" } });
+ }
+ }
+
+ private void BuildDayGrid()
+ {
+ if (DayGrid is null || MonthHeader is null) return;
+ DayGrid.Children.Clear();
+ MonthHeader.Text = _displayMonth.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
+
+ var firstDow = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
+ var offset = ((int)_displayMonth.DayOfWeek - (int)firstDow + 7) % 7;
+ var start = _displayMonth.AddDays(-offset);
+
+ var today = DateTime.Today;
+ var sel = SelectedDate?.Date;
+ var (rs, re) = NormalizeRange(StartDate?.Date, EndDate?.Date);
+ var rangeFill = TryGetBrush("AccentSoftBrush");
+
+ for (int i = 0; i < 42; i++)
+ {
+ var day = start.AddDays(i);
+ var cell = new Grid();
+
+ if (IsRange && rs.HasValue && re.HasValue && rs.Value != re.Value
+ && day >= rs.Value && day <= re.Value)
+ {
+ Thickness margin;
+ if (day == rs.Value)
+ margin = new Thickness(19, 0, 0, 0);
+ else if (day == re.Value)
+ margin = new Thickness(0, 0, 19, 0);
+ else
+ margin = default;
+
+ cell.Children.Add(new Border
+ {
+ Background = rangeFill,
+ Margin = margin,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ VerticalAlignment = VerticalAlignment.Stretch
+ });
+ }
+
+ var btn = new Button
+ {
+ Content = day.Day.ToString(CultureInfo.CurrentCulture),
+ Classes = { "day" },
+ Tag = day
+ };
+ if (day.Month != _displayMonth.Month) btn.Classes.Add("outside");
+ if (day == today) btn.Classes.Add("today");
+
+ var isSelected = IsRange
+ ? (rs.HasValue && day == rs.Value) || (re.HasValue && day == re.Value)
+ : sel.HasValue && day == sel.Value;
+ if (isSelected) btn.Classes.Add("selected");
+
+ btn.Click += OnDayClick;
+ cell.Children.Add(btn);
+ DayGrid.Children.Add(cell);
+ }
+ }
+
+ private void OnDayClick(object? sender, RoutedEventArgs e)
+ {
+ if (sender is not Button { Tag: DateTime day }) return;
+
+ if (IsRange)
+ {
+ // State A: nothing or both set → start a fresh range
+ // State B: only start set → complete (or restart) range
+ if (StartDate is null || EndDate is not null)
+ {
+ StartDate = day.Date;
+ EndDate = null;
+ }
+ else
+ {
+ var s = StartDate.Value.Date;
+ if (day.Date < s)
+ {
+ StartDate = day.Date;
+ EndDate = s;
+ }
+ else
+ {
+ EndDate = day.Date;
+ }
+ }
+ BuildDayGrid();
+ return;
+ }
+
+ var time = TimeSpan.Zero;
+ if (ShowTime)
+ {
+ if (TryParseTime(TimeText, out var parsed)) time = parsed;
+ else if (SelectedDate is { } cur) time = cur.TimeOfDay;
+ }
+ else if (SelectedDate is { } cur) time = cur.TimeOfDay;
+
+ SelectedDate = day.Date + time;
+ if (!ShowTime) PickerPopup.IsOpen = false;
+ }
+
+ private void OnPrevMonthClick(object? sender, RoutedEventArgs e)
+ {
+ _displayMonth = _displayMonth.AddMonths(-1);
+ BuildDayGrid();
+ }
+
+ private void OnNextMonthClick(object? sender, RoutedEventArgs e)
+ {
+ _displayMonth = _displayMonth.AddMonths(1);
+ BuildDayGrid();
+ }
+
+ private void OnTodayClick(object? sender, RoutedEventArgs e) => SetQuickDate(DateTime.Today);
+
+ private void OnTomorrowClick(object? sender, RoutedEventArgs e) => SetQuickDate(DateTime.Today.AddDays(1));
+
+ private void OnNextMondayClick(object? sender, RoutedEventArgs e)
+ {
+ var today = DateTime.Today;
+ int delta = ((int)DayOfWeek.Monday - (int)today.DayOfWeek + 7) % 7;
+ if (delta == 0) delta = 7;
+ SetQuickDate(today.AddDays(delta));
+ }
+
+ private void OnClearClick(object? sender, RoutedEventArgs e)
+ {
+ if (IsRange)
+ {
+ StartDate = null;
+ EndDate = null;
+ }
+ else
+ {
+ SelectedDate = null;
+ }
+ PickerPopup.IsOpen = false;
+ }
+
+ private void SetQuickDate(DateTime date)
+ {
+ _displayMonth = new DateTime(date.Year, date.Month, 1);
+
+ if (IsRange)
+ {
+ StartDate = date.Date;
+ EndDate = date.Date;
+ BuildDayGrid();
+ PickerPopup.IsOpen = false;
+ return;
+ }
+
+ var time = TimeSpan.Zero;
+ if (ShowTime)
+ {
+ if (!TryParseTime(TimeText, out time))
+ time = SelectedDate?.TimeOfDay ?? new TimeSpan(9, 0, 0);
+ }
+ SelectedDate = date.Date + time;
+ BuildDayGrid();
+ if (!ShowTime) PickerPopup.IsOpen = false;
+ }
+
+ private void OnDoneClick(object? sender, RoutedEventArgs e)
+ {
+ ApplyTimeTextToSelected();
+ PickerPopup.IsOpen = false;
+ }
+}
diff --git a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
index 916de42..c68cf1d 100644
--- a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
+++ b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
@@ -3,6 +3,7 @@
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:settings="using:ClaudeDo.Ui.ViewModels.Modals.Settings"
xmlns:conv="using:ClaudeDo.Ui.Converters"
+ xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
x:DataType="vm:SettingsModalViewModel"
Title="Settings"
@@ -225,23 +226,23 @@
-
+
-
-
-
+
-
-
-