feat(ui): UX redesign — Microsoft To Do style with Rider island layout
- Inline task creation (Enter to add, Escape to cancel) - Editable detail pane with auto-save on focus-lost - Tag chips with inline add/remove - Circular checkboxes with status colors - Keyboard shortcuts (Ctrl+N, Ctrl+L, Ctrl+R, Delete, Space, F2) - Rider-style island layout with rounded panels - Warm Charcoal theme with Forest Teal accent - Reactive proportional column sizing - Completed task strikethrough and dimming Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,14 +2,49 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ClaudeDo.App.App"
|
||||
xmlns:local="using:ClaudeDo.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
RequestedThemeVariant="Dark">
|
||||
|
||||
<Application.Resources>
|
||||
<!-- Accent: Forest Teal -->
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#3d9474"/>
|
||||
<SolidColorBrush x:Key="AccentLightBrush" Color="#6bb89e"/>
|
||||
<SolidColorBrush x:Key="AccentSubtleBrush" Color="#1A3D9474"/>
|
||||
<SolidColorBrush x:Key="AccentSelectedBrush" Color="#263D9474"/>
|
||||
|
||||
<!-- Text -->
|
||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#f1f5f9"/>
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#c8d0dc"/>
|
||||
<SolidColorBrush x:Key="TextMutedBrush" Color="#8892a2"/>
|
||||
<SolidColorBrush x:Key="TextDimBrush" Color="#6b7688"/>
|
||||
|
||||
<!-- Borders & Backgrounds -->
|
||||
<SolidColorBrush x:Key="BorderSubtleBrush" Color="#3a3f46"/>
|
||||
<SolidColorBrush x:Key="WindowBgBrush" Color="#1c1e21"/>
|
||||
<SolidColorBrush x:Key="IslandBgBrush" Color="#272a2e"/>
|
||||
<SolidColorBrush x:Key="SidebarBgBrush" Color="#272a2e"/>
|
||||
<SolidColorBrush x:Key="ContentBgBrush" Color="#272a2e"/>
|
||||
|
||||
<!-- Status colors (for checkboxes) -->
|
||||
<SolidColorBrush x:Key="StatusGrayBrush" Color="#475569"/>
|
||||
<SolidColorBrush x:Key="StatusOrangeBrush" Color="#e67e22"/>
|
||||
<SolidColorBrush x:Key="StatusGreenBrush" Color="#3d9474"/>
|
||||
<SolidColorBrush x:Key="StatusRedBrush" Color="#ef4444"/>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="#333d9474"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="#1A3D9474"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="#403D9474"/>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -60,7 +60,13 @@ sealed class Program
|
||||
sc.AddTransient<ListEditorViewModel>();
|
||||
sc.AddTransient<TaskEditorViewModel>();
|
||||
sc.AddSingleton<StatusBarViewModel>();
|
||||
sc.AddSingleton<TaskDetailViewModel>();
|
||||
sc.AddSingleton<TaskDetailViewModel>(sp => new TaskDetailViewModel(
|
||||
sp.GetRequiredService<TaskRepository>(),
|
||||
sp.GetRequiredService<WorktreeRepository>(),
|
||||
sp.GetRequiredService<ListRepository>(),
|
||||
sp.GetRequiredService<GitService>(),
|
||||
sp.GetRequiredService<WorkerClient>(),
|
||||
sp.GetRequiredService<TagRepository>()));
|
||||
sc.AddSingleton<TaskListViewModel>(sp =>
|
||||
{
|
||||
var taskRepo = sp.GetRequiredService<TaskRepository>();
|
||||
|
||||
30
src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs
Normal file
30
src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace ClaudeDo.Ui.Converters;
|
||||
|
||||
public sealed class CheckboxBorderConverter : IValueConverter
|
||||
{
|
||||
public static readonly CheckboxBorderConverter Instance = new();
|
||||
|
||||
private static readonly ISolidColorBrush Gray = new SolidColorBrush(Color.Parse("#475569"));
|
||||
private static readonly ISolidColorBrush Orange = new SolidColorBrush(Color.Parse("#e67e22"));
|
||||
private static readonly ISolidColorBrush Green = new SolidColorBrush(Color.Parse("#3d9474"));
|
||||
private static readonly ISolidColorBrush Red = new SolidColorBrush(Color.Parse("#ef4444"));
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return value?.ToString()?.ToLowerInvariant() switch
|
||||
{
|
||||
"running" => Orange,
|
||||
"done" => Green,
|
||||
"failed" => Red,
|
||||
_ => Gray, // manual, queued
|
||||
};
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using Avalonia.Media;
|
||||
using ClaudeDo.Data.Models;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
@@ -9,6 +11,17 @@ public partial class ListItemViewModel : ViewModelBase
|
||||
[ObservableProperty] private string? _workingDir;
|
||||
[ObservableProperty] private string _defaultCommitType;
|
||||
|
||||
private static readonly IBrush[] DotPalette =
|
||||
[
|
||||
new SolidColorBrush(Color.Parse("#3d9474")), // green
|
||||
new SolidColorBrush(Color.Parse("#5571a1")), // blue
|
||||
new SolidColorBrush(Color.Parse("#d4964a")), // amber
|
||||
new SolidColorBrush(Color.Parse("#7c6aad")), // purple
|
||||
new SolidColorBrush(Color.Parse("#c25d6a")), // rose
|
||||
];
|
||||
|
||||
public IBrush DotBrush => DotPalette[Math.Abs(Id.GetHashCode()) % DotPalette.Length];
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public ListItemViewModel(ListEntity entity)
|
||||
|
||||
@@ -41,6 +41,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
StatusBar = statusBar;
|
||||
|
||||
TaskList.SelectedTaskChanged += OnSelectedTaskChanged;
|
||||
TaskDetail.TaskChanged += taskId => _ = TaskList.RefreshSingleAsync(taskId);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
|
||||
@@ -16,12 +16,18 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
private readonly ListRepository _listRepo;
|
||||
private readonly GitService _git;
|
||||
private readonly WorkerClient _worker;
|
||||
private readonly TagRepository _tagRepo;
|
||||
|
||||
[ObservableProperty] private string _title = "";
|
||||
[ObservableProperty] private string? _description;
|
||||
[ObservableProperty] private string? _result;
|
||||
[ObservableProperty] private string? _logPath;
|
||||
[ObservableProperty] private string _statusText = "";
|
||||
[ObservableProperty] private string _statusChoice = "Manual";
|
||||
[ObservableProperty] private string _commitType = "chore";
|
||||
|
||||
public static string[] StatusChoices { get; } = ["Manual", "Queued", "Running", "Done", "Failed"];
|
||||
public static string[] CommitTypes { get; } = ["feat", "fix", "refactor", "docs", "test", "chore", "ci", "perf", "style", "build"];
|
||||
|
||||
// Worktree
|
||||
[ObservableProperty] private bool _hasWorktree;
|
||||
@@ -32,19 +38,25 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
|
||||
// Live stream
|
||||
public ObservableCollection<string> LiveLines { get; } = new();
|
||||
public ObservableCollection<TagEntity> Tags { get; } = new();
|
||||
[ObservableProperty] private string _newTagInput = "";
|
||||
|
||||
private string? _taskId;
|
||||
private string? _listId;
|
||||
private bool _isLoading;
|
||||
private const int MaxLiveLines = 500;
|
||||
|
||||
public event Action<string>? TaskChanged;
|
||||
|
||||
public TaskDetailViewModel(TaskRepository taskRepo, WorktreeRepository worktreeRepo,
|
||||
ListRepository listRepo, GitService git, WorkerClient worker)
|
||||
ListRepository listRepo, GitService git, WorkerClient worker, TagRepository tagRepo)
|
||||
{
|
||||
_taskRepo = taskRepo;
|
||||
_worktreeRepo = worktreeRepo;
|
||||
_listRepo = listRepo;
|
||||
_git = git;
|
||||
_worker = worker;
|
||||
_tagRepo = tagRepo;
|
||||
|
||||
worker.TaskMessageEvent += OnTaskMessage;
|
||||
worker.WorktreeUpdatedEvent += OnWorktreeUpdated;
|
||||
@@ -59,16 +71,77 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
var task = await _taskRepo.GetByIdAsync(taskId);
|
||||
if (task is null) return;
|
||||
|
||||
_listId = task.ListId;
|
||||
Title = task.Title;
|
||||
Description = task.Description;
|
||||
Result = task.Result;
|
||||
LogPath = task.LogPath;
|
||||
StatusText = task.Status.ToString().ToLowerInvariant();
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
_listId = task.ListId;
|
||||
Title = task.Title;
|
||||
Description = task.Description;
|
||||
Result = task.Result;
|
||||
LogPath = task.LogPath;
|
||||
StatusText = task.Status.ToString().ToLowerInvariant();
|
||||
StatusChoice = task.Status.ToString();
|
||||
CommitType = task.CommitType;
|
||||
|
||||
Tags.Clear();
|
||||
var tags = await _taskRepo.GetTagsAsync(taskId);
|
||||
foreach (var tag in tags)
|
||||
Tags.Add(tag);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
await LoadWorktreeAsync(taskId);
|
||||
}
|
||||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
if (_isLoading || _taskId is null) return;
|
||||
|
||||
var entity = await _taskRepo.GetByIdAsync(_taskId);
|
||||
if (entity is null) return;
|
||||
|
||||
entity.Title = Title;
|
||||
entity.Description = Description;
|
||||
entity.CommitType = CommitType;
|
||||
|
||||
if (Enum.TryParse<Data.Models.TaskStatus>(StatusChoice, true, out var status))
|
||||
entity.Status = status;
|
||||
|
||||
await _taskRepo.UpdateAsync(entity);
|
||||
StatusText = entity.Status.ToString().ToLowerInvariant();
|
||||
TaskChanged?.Invoke(_taskId);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddTag()
|
||||
{
|
||||
var name = NewTagInput.Trim();
|
||||
if (string.IsNullOrEmpty(name) || _taskId is null) return;
|
||||
|
||||
var tagId = await _tagRepo.GetOrCreateAsync(name);
|
||||
await _taskRepo.AddTagAsync(_taskId, tagId);
|
||||
|
||||
Tags.Clear();
|
||||
var tags = await _taskRepo.GetTagsAsync(_taskId);
|
||||
foreach (var tag in tags)
|
||||
Tags.Add(tag);
|
||||
|
||||
NewTagInput = "";
|
||||
TaskChanged?.Invoke(_taskId);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RemoveTag(TagEntity tag)
|
||||
{
|
||||
if (_taskId is null) return;
|
||||
await _taskRepo.RemoveTagAsync(_taskId, tag.Id);
|
||||
Tags.Remove(tag);
|
||||
TaskChanged?.Invoke(_taskId);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_taskId = null;
|
||||
@@ -80,6 +153,10 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
StatusText = "";
|
||||
HasWorktree = false;
|
||||
LiveLines.Clear();
|
||||
Tags.Clear();
|
||||
NewTagInput = "";
|
||||
StatusChoice = "Manual";
|
||||
CommitType = "chore";
|
||||
}
|
||||
|
||||
private async Task LoadWorktreeAsync(string taskId)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Avalonia.Media;
|
||||
using ClaudeDo.Data.Models;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -20,9 +21,10 @@ public partial class TaskItemViewModel : ViewModelBase
|
||||
|
||||
private readonly Func<string, Task>? _runNow;
|
||||
private readonly Func<bool> _canRunNow;
|
||||
private readonly Func<string, Task>? _toggleDone;
|
||||
|
||||
public TaskItemViewModel(TaskEntity entity, IReadOnlyList<TagEntity> tags,
|
||||
Func<string, Task>? runNow, Func<bool> canRunNow)
|
||||
Func<string, Task>? runNow, Func<bool> canRunNow, Func<string, Task>? toggleDone = null)
|
||||
{
|
||||
Entity = entity;
|
||||
Id = entity.Id;
|
||||
@@ -35,8 +37,23 @@ public partial class TaskItemViewModel : ViewModelBase
|
||||
_description = entity.Description;
|
||||
_runNow = runNow;
|
||||
_canRunNow = canRunNow;
|
||||
_toggleDone = toggleDone;
|
||||
}
|
||||
|
||||
public bool IsDone => Status == TaskStatus.Done;
|
||||
public bool IsRunning => Status == TaskStatus.Running;
|
||||
public bool CanToggleDone => Status != TaskStatus.Running && Status != TaskStatus.Failed;
|
||||
|
||||
public TextDecorationCollection? TitleDecorations => IsDone
|
||||
? TextDecorations.Strikethrough
|
||||
: null;
|
||||
|
||||
public IBrush TitleForeground => IsDone
|
||||
? new SolidColorBrush(Color.Parse("#5a6578"))
|
||||
: new SolidColorBrush(Color.Parse("#e2e8f0"));
|
||||
|
||||
public double RowOpacity => IsDone ? 0.6 : 1.0;
|
||||
|
||||
public void Refresh(TaskEntity entity, IReadOnlyList<TagEntity> tags)
|
||||
{
|
||||
Entity = entity;
|
||||
@@ -47,6 +64,13 @@ public partial class TaskItemViewModel : ViewModelBase
|
||||
CommitType = entity.CommitType;
|
||||
Description = entity.Description;
|
||||
RunNowCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(IsDone));
|
||||
OnPropertyChanged(nameof(IsRunning));
|
||||
OnPropertyChanged(nameof(CanToggleDone));
|
||||
OnPropertyChanged(nameof(TitleDecorations));
|
||||
OnPropertyChanged(nameof(TitleForeground));
|
||||
OnPropertyChanged(nameof(RowOpacity));
|
||||
ToggleDoneCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanRunNow))]
|
||||
@@ -58,4 +82,11 @@ public partial class TaskItemViewModel : ViewModelBase
|
||||
|
||||
private bool CanRunNow() =>
|
||||
_canRunNow() && Status == TaskStatus.Queued;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanToggleDone))]
|
||||
private async Task ToggleDone()
|
||||
{
|
||||
if (_toggleDone is not null)
|
||||
await _toggleDone(Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private TaskItemViewModel? _selectedTask;
|
||||
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(AddTaskCommand))] private string? _currentListId;
|
||||
[ObservableProperty] private string _listName = "Tasks";
|
||||
[ObservableProperty] private string _inlineAddTitle = "";
|
||||
|
||||
public event Action<TaskItemViewModel?>? SelectedTaskChanged;
|
||||
|
||||
@@ -61,6 +63,16 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
Tasks.Clear();
|
||||
SelectedTask = null;
|
||||
|
||||
if (listId is not null)
|
||||
{
|
||||
var list = await _listRepo.GetByIdAsync(listId);
|
||||
ListName = list?.Name ?? "Tasks";
|
||||
}
|
||||
else
|
||||
{
|
||||
ListName = "Tasks";
|
||||
}
|
||||
|
||||
if (listId is null) return;
|
||||
|
||||
try
|
||||
@@ -69,7 +81,7 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
foreach (var e in entities)
|
||||
{
|
||||
var tags = await _taskRepo.GetEffectiveTagsAsync(e.Id);
|
||||
Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected));
|
||||
Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -80,6 +92,40 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
private bool CanAddTask() => CurrentListId is not null;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanAddTask))]
|
||||
private async Task InlineAdd()
|
||||
{
|
||||
var title = InlineAddTitle.Trim();
|
||||
if (string.IsNullOrEmpty(title) || CurrentListId is null) return;
|
||||
|
||||
var list = await _listRepo.GetByIdAsync(CurrentListId);
|
||||
var defaultCommitType = list?.DefaultCommitType ?? "chore";
|
||||
|
||||
var entity = new TaskEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
ListId = CurrentListId,
|
||||
Title = title,
|
||||
Status = TaskStatus.Manual,
|
||||
CommitType = defaultCommitType,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await _taskRepo.AddAsync(entity);
|
||||
var tags = await _taskRepo.GetEffectiveTagsAsync(entity.Id);
|
||||
var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync);
|
||||
Tasks.Add(vm);
|
||||
SelectedTask = vm;
|
||||
InlineAddTitle = "";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_showMessage($"Error creating task: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanAddTask))]
|
||||
private async Task AddTask()
|
||||
{
|
||||
@@ -108,7 +154,7 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
var tags = await _taskRepo.GetEffectiveTagsAsync(saved.Id);
|
||||
Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected));
|
||||
Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
|
||||
|
||||
// Auto wake-queue if agent+queued
|
||||
if (saved.Status == TaskStatus.Queued &&
|
||||
@@ -206,6 +252,19 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleDoneAsync(string taskId)
|
||||
{
|
||||
var entity = await _taskRepo.GetByIdAsync(taskId);
|
||||
if (entity is null) return;
|
||||
|
||||
entity.Status = entity.Status == TaskStatus.Done ? TaskStatus.Manual : TaskStatus.Done;
|
||||
if (entity.Status == TaskStatus.Done)
|
||||
entity.FinishedAt = DateTime.UtcNow;
|
||||
|
||||
await _taskRepo.UpdateAsync(entity);
|
||||
await RefreshSingleAsync(taskId);
|
||||
}
|
||||
|
||||
private async void OnTaskUpdated(string taskId)
|
||||
{
|
||||
if (CurrentListId is null) return;
|
||||
|
||||
@@ -7,60 +7,97 @@
|
||||
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="700"
|
||||
x:Class="ClaudeDo.Ui.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Title="ClaudeDo" Width="1200" Height="700"
|
||||
MinWidth="800" MinHeight="500">
|
||||
Title="ClaudeDo"
|
||||
MinWidth="800" MinHeight="500"
|
||||
KeyDown="OnGlobalKeyDown">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Status Bar at bottom -->
|
||||
<DockPanel Background="{StaticResource WindowBgBrush}">
|
||||
<v:StatusBarView DockPanel.Dock="Bottom" DataContext="{Binding StatusBar}" />
|
||||
|
||||
<!-- Main 3-column layout -->
|
||||
<Grid ColumnDefinitions="220,Auto,*,Auto,350">
|
||||
<!-- Lists pane -->
|
||||
<DockPanel Grid.Column="0">
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="4">
|
||||
<TextBlock Text="Lists" FontWeight="Bold" FontSize="14" VerticalAlignment="Center" Margin="4,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="4" Margin="4">
|
||||
<Button Content="+" Command="{Binding AddListCommand}" ToolTip.Tip="Add List" MinWidth="30"/>
|
||||
<Button Content="E" Command="{Binding EditListCommand}" ToolTip.Tip="Edit List" MinWidth="30"/>
|
||||
<Button Content="-" Command="{Binding DeleteListCommand}" ToolTip.Tip="Delete List" MinWidth="30"/>
|
||||
</StackPanel>
|
||||
<ListBox ItemsSource="{Binding Lists}"
|
||||
SelectedItem="{Binding SelectedList}"
|
||||
Margin="4">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ListItemViewModel">
|
||||
<StackPanel Margin="4,2" Background="Transparent" HorizontalAlignment="Stretch" DoubleTapped="OnListItemDoubleTapped" PointerPressed="OnListItemPointerPressed">
|
||||
<StackPanel.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuItem Header="Edit"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).EditListCommand}"/>
|
||||
<MenuItem Header="Delete"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DeleteListCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="New Task"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).TaskList.AddTaskCommand}"/>
|
||||
</MenuFlyout>
|
||||
</StackPanel.ContextFlyout>
|
||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding WorkingDir}" FontSize="10" Foreground="Gray"
|
||||
IsVisible="{Binding WorkingDir, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
<Grid ColumnDefinitions="1*,2*,1.5*" Margin="8,8,8,0">
|
||||
|
||||
<GridSplitter Grid.Column="1" Width="4" ResizeDirection="Columns"/>
|
||||
<!-- Lists island -->
|
||||
<Border Grid.Column="0" CornerRadius="12" Background="{StaticResource IslandBgBrush}"
|
||||
MinWidth="180" MaxWidth="320" Margin="0,0,4,8" ClipToBounds="True">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top"
|
||||
Text="Lists" FontWeight="SemiBold" FontSize="13"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="16,14,16,10"/>
|
||||
|
||||
<!-- Tasks pane -->
|
||||
<v:TaskListView Grid.Column="2" DataContext="{Binding TaskList}" />
|
||||
<Border DockPanel.Dock="Bottom" Padding="8,8"
|
||||
BorderThickness="0,1,0,0" BorderBrush="{StaticResource BorderSubtleBrush}">
|
||||
<Button Content="+ New List"
|
||||
Command="{Binding AddListCommand}"
|
||||
Background="Transparent"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
BorderThickness="0"
|
||||
Padding="12,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
FontSize="13"
|
||||
Cursor="Hand"/>
|
||||
</Border>
|
||||
|
||||
<GridSplitter Grid.Column="3" Width="4" ResizeDirection="Columns"/>
|
||||
<ListBox x:Name="ListsBox"
|
||||
ItemsSource="{Binding Lists}"
|
||||
SelectedItem="{Binding SelectedList}"
|
||||
Background="Transparent"
|
||||
Margin="4,0">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ListItemViewModel">
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="8,6"
|
||||
Background="Transparent"
|
||||
DoubleTapped="OnListItemDoubleTapped"
|
||||
PointerPressed="OnListItemPointerPressed">
|
||||
<Grid.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuItem Header="Edit"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).EditListCommand}"/>
|
||||
<MenuItem Header="Delete"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DeleteListCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="New Task"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).TaskList.AddTaskCommand}"/>
|
||||
</MenuFlyout>
|
||||
</Grid.ContextFlyout>
|
||||
|
||||
<!-- Detail pane -->
|
||||
<v:TaskDetailView Grid.Column="4" DataContext="{Binding TaskDetail}" />
|
||||
<Ellipse Grid.Column="0" Width="8" Height="8"
|
||||
Fill="{Binding DotBrush}"
|
||||
VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Medium"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<TextBlock Text="{Binding WorkingDir}" FontSize="10"
|
||||
Foreground="{StaticResource TextDimBrush}"
|
||||
IsVisible="{Binding WorkingDir, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Tasks island -->
|
||||
<Border Grid.Column="1" CornerRadius="12" Background="{StaticResource IslandBgBrush}"
|
||||
Margin="4,0,4,8" ClipToBounds="True">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top"
|
||||
Text="{Binding TaskList.ListName, FallbackValue='Tasks'}"
|
||||
FontWeight="SemiBold" FontSize="16"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="16,14,16,10"/>
|
||||
<v:TaskListView DataContext="{Binding TaskList}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Detail island -->
|
||||
<Border Grid.Column="2" CornerRadius="12" Background="{StaticResource IslandBgBrush}"
|
||||
MinWidth="280" MaxWidth="500" Margin="4,0,0,8" ClipToBounds="True">
|
||||
<v:TaskDetailView DataContext="{Binding TaskDetail}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
|
||||
namespace ClaudeDo.Ui.Views;
|
||||
@@ -18,24 +22,54 @@ public partial class MainWindow : Window
|
||||
await vm.InitializeAsync();
|
||||
}
|
||||
|
||||
private void OnListItemDoubleTapped(object? sender, TappedEventArgs e)
|
||||
private void OnGlobalKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (sender is Control c && c.DataContext is ListItemViewModel
|
||||
&& DataContext is MainWindowViewModel vm)
|
||||
if (DataContext is not MainWindowViewModel vm) return;
|
||||
|
||||
var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
|
||||
var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
|
||||
|
||||
if (ctrl && shift && e.Key == Key.N)
|
||||
{
|
||||
vm.EditListCommand.Execute(null);
|
||||
vm.AddListCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (ctrl && e.Key == Key.N)
|
||||
{
|
||||
var taskListView = this.GetVisualDescendants().OfType<TaskListView>().FirstOrDefault();
|
||||
taskListView?.FocusInlineAdd();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (ctrl && e.Key == Key.L)
|
||||
{
|
||||
this.FindControl<ListBox>("ListsBox")?.Focus();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (ctrl && e.Key == Key.R)
|
||||
{
|
||||
if (vm.TaskList.SelectedTask is { } task)
|
||||
{
|
||||
task.RunNowCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnListItemDoubleTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
vm.EditListCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void OnListItemPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(null).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonPressed
|
||||
&& sender is Control c && c.DataContext is ListItemViewModel item
|
||||
var props = e.GetCurrentPoint(this).Properties;
|
||||
if (!props.IsRightButtonPressed) return;
|
||||
|
||||
if (sender is Grid { DataContext: ListItemViewModel item }
|
||||
&& DataContext is MainWindowViewModel vm)
|
||||
{
|
||||
vm.SelectedList = item;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,80 +2,148 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
||||
xmlns:conv="using:ClaudeDo.Ui.Converters"
|
||||
xmlns:m="using:ClaudeDo.Data.Models"
|
||||
x:Class="ClaudeDo.Ui.Views.TaskDetailView"
|
||||
x:DataType="vm:TaskDetailViewModel">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8" Spacing="8"
|
||||
<StackPanel Margin="12" Spacing="8"
|
||||
IsVisible="{Binding Title, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
|
||||
<!-- Header -->
|
||||
<TextBlock Text="{Binding Title}" FontWeight="Bold" FontSize="16"/>
|
||||
<Border CornerRadius="3" Padding="6,2" HorizontalAlignment="Left"
|
||||
Background="{Binding StatusText, Converter={x:Static conv:StatusColorConverter.Instance}}">
|
||||
<TextBlock Text="{Binding StatusText}" Foreground="White" FontSize="11"/>
|
||||
</Border>
|
||||
<!-- Title (large, editable) -->
|
||||
<TextBox x:Name="TitleBox"
|
||||
Text="{Binding Title}"
|
||||
FontWeight="Bold" FontSize="16"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
BorderThickness="0" Background="Transparent"
|
||||
Padding="0,4"
|
||||
LostFocus="OnFieldLostFocus"/>
|
||||
|
||||
<!-- Description -->
|
||||
<TextBlock Text="Description" FontWeight="SemiBold" Margin="0,8,0,2"/>
|
||||
<!-- TODO: Markdown rendering -->
|
||||
<TextBox Text="{Binding Description, Mode=OneWay}" IsReadOnly="True"
|
||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
|
||||
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<TextBlock Text="(no description)" Foreground="Gray" FontStyle="Italic"
|
||||
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
|
||||
<!-- Status + Commit Type row -->
|
||||
<Grid ColumnDefinitions="*,16,*" Margin="0,4,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Status" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<ComboBox ItemsSource="{Binding StatusChoices}"
|
||||
SelectedItem="{Binding StatusChoice}"
|
||||
MinWidth="100"
|
||||
LostFocus="OnFieldLostFocus"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Commit Type" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<ComboBox ItemsSource="{Binding CommitTypes}"
|
||||
SelectedItem="{Binding CommitType}"
|
||||
MinWidth="100"
|
||||
LostFocus="OnFieldLostFocus"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Result -->
|
||||
<TextBlock Text="Result" FontWeight="SemiBold" Margin="0,8,0,2"/>
|
||||
<!-- TODO: Markdown rendering -->
|
||||
<TextBox Text="{Binding Result, Mode=OneWay}" IsReadOnly="True"
|
||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="80"
|
||||
IsVisible="{Binding Result, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<TextBlock Text="(no result yet)" Foreground="Gray" FontStyle="Italic"
|
||||
IsVisible="{Binding Result, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
|
||||
|
||||
<!-- Log path -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="4"
|
||||
IsVisible="{Binding LogPath, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="Log:" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding LogPath}" FontSize="11" Foreground="Gray" VerticalAlignment="Center"/>
|
||||
<!-- Tags -->
|
||||
<StackPanel Spacing="4" Margin="0,8,0,0">
|
||||
<TextBlock Text="Tags" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<ItemsControl ItemsSource="{Binding Tags}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="m:TagEntity">
|
||||
<Border CornerRadius="10" Padding="8,3" Margin="0,0,4,4"
|
||||
Background="{StaticResource AccentSubtleBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBlock Text="{Binding Name}" FontSize="12"
|
||||
Foreground="{StaticResource AccentLightBrush}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Content="x" FontSize="10" Padding="2,0"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Cursor="Hand"
|
||||
Command="{Binding $parent[UserControl].((vm:TaskDetailViewModel)DataContext).RemoveTagCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBox Text="{Binding NewTagInput}"
|
||||
PlaceholderText="Add tag..."
|
||||
Width="100" FontSize="12"
|
||||
BorderThickness="0" Background="Transparent"
|
||||
Padding="4,3"
|
||||
KeyDown="OnTagInputKeyDown"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Live stream -->
|
||||
<TextBlock Text="Live Output" FontWeight="SemiBold" Margin="0,8,0,2"/>
|
||||
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="4"
|
||||
MaxHeight="200">
|
||||
<!-- Description (editable) -->
|
||||
<TextBlock Text="Description" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
|
||||
<TextBox Text="{Binding Description}"
|
||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
PlaceholderText="Add a description..."
|
||||
LostFocus="OnFieldLostFocus"/>
|
||||
|
||||
<!-- === READ-ONLY ZONE === -->
|
||||
|
||||
<TextBlock Text="Result" FontWeight="SemiBold" FontSize="12"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,12,0,2"/>
|
||||
<TextBox Text="{Binding Result, Mode=OneWay}" IsReadOnly="True"
|
||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
|
||||
IsVisible="{Binding Result, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<TextBlock Text="(no result yet)" Foreground="{StaticResource TextMutedBrush}" FontStyle="Italic"
|
||||
IsVisible="{Binding Result, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="4"
|
||||
IsVisible="{Binding LogPath, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="Log:" FontWeight="SemiBold" FontSize="12"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding LogPath}" FontSize="11"
|
||||
Foreground="{StaticResource TextDimBrush}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Live Output" FontWeight="SemiBold" FontSize="12"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
|
||||
<Border BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="6" MaxHeight="200">
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{Binding LiveLines}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" FontFamily="Consolas,Courier New,monospace"
|
||||
FontSize="11" TextWrapping="NoWrap"/>
|
||||
FontSize="11" TextWrapping="NoWrap"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Worktree section -->
|
||||
<Border IsVisible="{Binding HasWorktree}" BorderBrush="CornflowerBlue"
|
||||
BorderThickness="1" CornerRadius="5" Padding="8" Margin="0,8,0,0">
|
||||
<Border IsVisible="{Binding HasWorktree}" BorderBrush="{StaticResource AccentBrush}"
|
||||
BorderThickness="1" CornerRadius="8" Padding="10" Margin="0,8,0,0">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Worktree" FontWeight="Bold" FontSize="14"/>
|
||||
<TextBlock Text="Worktree" FontWeight="Bold" FontSize="14"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Branch:" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding BranchName}" FontFamily="Consolas,Courier New,monospace"/>
|
||||
<TextBlock Text="Branch:" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<TextBlock Text="{Binding BranchName}" FontFamily="Consolas,Courier New,monospace"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="State:" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding WorktreeState}"/>
|
||||
<TextBlock Text="State:" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<TextBlock Text="{Binding WorktreeState}"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="Diff Stat:" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
IsVisible="{Binding DiffStat, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<TextBox Text="{Binding DiffStat, Mode=OneWay}" IsReadOnly="True"
|
||||
AcceptsReturn="True" FontFamily="Consolas,Courier New,monospace" FontSize="11"
|
||||
IsVisible="{Binding DiffStat, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
|
||||
<!-- Worktree actions -->
|
||||
<WrapPanel Orientation="Horizontal" Margin="0,4,0,0">
|
||||
<Button Content="Open Worktree" Command="{Binding OpenWorktreeCommand}" Margin="0,0,4,4"/>
|
||||
<Button Content="Show Diff" Command="{Binding ShowDiffCommand}" Margin="0,0,4,4"/>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
|
||||
namespace ClaudeDo.Ui.Views;
|
||||
|
||||
@@ -8,4 +11,24 @@ public partial class TaskDetailView : UserControl
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnFieldLostFocus(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskDetailViewModel vm)
|
||||
await vm.SaveAsync();
|
||||
}
|
||||
|
||||
private void OnTagInputKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter && DataContext is TaskDetailViewModel vm)
|
||||
{
|
||||
vm.AddTagCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void FocusTitle()
|
||||
{
|
||||
this.FindControl<TextBox>("TitleBox")?.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,36 @@
|
||||
x:DataType="vm:TaskListViewModel"
|
||||
x:Name="Root">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="4">
|
||||
<TextBlock Text="Tasks" FontWeight="Bold" FontSize="14" VerticalAlignment="Center" Margin="4,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="4" Margin="4">
|
||||
<Button Content="+ Task" Command="{Binding AddTaskCommand}" MinWidth="60"/>
|
||||
<Button Content="Edit" Command="{Binding EditTaskCommand}" MinWidth="50"/>
|
||||
<Button Content="Delete" Command="{Binding DeleteTaskCommand}" MinWidth="50"/>
|
||||
</StackPanel>
|
||||
<ListBox ItemsSource="{Binding Tasks}"
|
||||
<!-- Inline add field at bottom -->
|
||||
<Border DockPanel.Dock="Bottom" Padding="8,8"
|
||||
BorderThickness="0,1,0,0" BorderBrush="{StaticResource BorderSubtleBrush}">
|
||||
<TextBox x:Name="InlineAddBox"
|
||||
Text="{Binding InlineAddTitle, Mode=TwoWay}"
|
||||
PlaceholderText="+ Add a task..."
|
||||
BorderThickness="1"
|
||||
BorderBrush="{StaticResource BorderSubtleBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="10,8"
|
||||
FontSize="13"
|
||||
KeyDown="OnInlineAddKeyDown"
|
||||
GotFocus="OnInlineAddGotFocus"
|
||||
LostFocus="OnInlineAddLostFocus"/>
|
||||
</Border>
|
||||
|
||||
<!-- Task list -->
|
||||
<ListBox x:Name="TaskListBox"
|
||||
ItemsSource="{Binding Tasks}"
|
||||
SelectedItem="{Binding SelectedTask}"
|
||||
Margin="4">
|
||||
Background="Transparent"
|
||||
Margin="4,0"
|
||||
KeyDown="OnTaskListKeyDown">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TaskItemViewModel">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="4,2" Background="Transparent" HorizontalAlignment="Stretch" DoubleTapped="OnTaskItemDoubleTapped" PointerPressed="OnTaskItemPointerPressed">
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="4,4"
|
||||
Background="Transparent"
|
||||
Opacity="{Binding RowOpacity}"
|
||||
DoubleTapped="OnTaskItemDoubleTapped"
|
||||
PointerPressed="OnTaskItemPointerPressed">
|
||||
<Grid.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuItem Header="Edit"
|
||||
@@ -31,19 +47,52 @@
|
||||
Command="{Binding RunNowCommand}"/>
|
||||
</MenuFlyout>
|
||||
</Grid.ContextFlyout>
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="{Binding Title}" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding TagsText}" FontSize="10" Foreground="Gray"
|
||||
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" CornerRadius="3" Padding="6,2" Margin="4,0"
|
||||
Background="{Binding StatusText, Converter={x:Static conv:StatusColorConverter.Instance}}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding StatusText}" Foreground="White" FontSize="11"/>
|
||||
|
||||
<!-- Circular checkbox -->
|
||||
<Border Grid.Column="0" Width="22" Height="22"
|
||||
CornerRadius="11"
|
||||
BorderThickness="2"
|
||||
BorderBrush="{Binding StatusText, Converter={x:Static conv:CheckboxBorderConverter.Instance}}"
|
||||
Background="Transparent"
|
||||
VerticalAlignment="Center" Margin="0,0,10,0"
|
||||
Cursor="Hand"
|
||||
PointerPressed="OnCheckboxPressed">
|
||||
<Panel>
|
||||
<!-- Checkmark for done -->
|
||||
<Canvas Width="12" Height="12"
|
||||
IsVisible="{Binding IsDone}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Path Stroke="{StaticResource StatusGreenBrush}" StrokeThickness="2"
|
||||
Data="M 1,6 L 4.5,9.5 L 11,3"/>
|
||||
</Canvas>
|
||||
<!-- Running dot -->
|
||||
<Ellipse Width="8" Height="8"
|
||||
Fill="{StaticResource StatusOrangeBrush}"
|
||||
IsVisible="{Binding IsRunning}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Panel>
|
||||
</Border>
|
||||
<Button Grid.Column="2" Content="Run" Command="{Binding RunNowCommand}"
|
||||
Margin="4,0,0,0" VerticalAlignment="Center" Padding="8,2"
|
||||
FontSize="11"/>
|
||||
|
||||
<!-- Task content -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Title}" FontWeight="Medium"
|
||||
Foreground="{Binding TitleForeground}"
|
||||
TextDecorations="{Binding TitleDecorations}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock FontSize="11"
|
||||
Foreground="{StaticResource TextDimBrush}"
|
||||
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0} · {1}">
|
||||
<Binding Path="TagsText"/>
|
||||
<Binding Path="StatusText"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding StatusText}" FontSize="11"
|
||||
Foreground="{StaticResource TextDimBrush}"
|
||||
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
|
||||
namespace ClaudeDo.Ui.Views;
|
||||
@@ -11,24 +13,92 @@ public partial class TaskListView : UserControl
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnTaskItemDoubleTapped(object? sender, TappedEventArgs e)
|
||||
private void OnInlineAddKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (sender is Control c && c.DataContext is TaskItemViewModel
|
||||
&& DataContext is TaskListViewModel vm)
|
||||
if (DataContext is not TaskListViewModel vm) return;
|
||||
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
vm.EditTaskCommand.Execute(null);
|
||||
vm.InlineAddCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape)
|
||||
{
|
||||
vm.InlineAddTitle = "";
|
||||
this.FindControl<ListBox>("TaskListBox")?.Focus();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTaskItemPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
private void OnInlineAddGotFocus(object? sender, FocusChangedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(null).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonPressed
|
||||
&& sender is Control c && c.DataContext is TaskItemViewModel item
|
||||
&& DataContext is TaskListViewModel vm)
|
||||
if (sender is TextBox tb)
|
||||
tb.BorderBrush = Avalonia.Application.Current?.FindResource("AccentBrush") as Avalonia.Media.IBrush;
|
||||
}
|
||||
|
||||
private void OnInlineAddLostFocus(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox tb)
|
||||
tb.BorderBrush = Avalonia.Application.Current?.FindResource("BorderSubtleBrush") as Avalonia.Media.IBrush;
|
||||
}
|
||||
|
||||
private void OnTaskListKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (DataContext is not TaskListViewModel vm || vm.SelectedTask is null) return;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
vm.SelectedTask = item;
|
||||
case Key.Delete:
|
||||
vm.DeleteTaskCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Space:
|
||||
if (vm.SelectedTask.CanToggleDone)
|
||||
{
|
||||
vm.SelectedTask.ToggleDoneCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.Enter:
|
||||
case Key.F2:
|
||||
var detailView = this.GetVisualAncestors().OfType<Window>().FirstOrDefault()
|
||||
?.GetVisualDescendants().OfType<TaskDetailView>().FirstOrDefault();
|
||||
detailView?.FocusTitle();
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCheckboxPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is not Border { DataContext: TaskItemViewModel task }) return;
|
||||
if (task.CanToggleDone)
|
||||
{
|
||||
task.ToggleDoneCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTaskItemDoubleTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskListViewModel vm)
|
||||
vm.EditTaskCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void OnTaskItemPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
var props = e.GetCurrentPoint(this).Properties;
|
||||
if (!props.IsRightButtonPressed) return;
|
||||
|
||||
if (sender is Grid { DataContext: TaskItemViewModel item }
|
||||
&& DataContext is TaskListViewModel vm)
|
||||
{
|
||||
vm.SelectedTask = item;
|
||||
}
|
||||
}
|
||||
|
||||
public void FocusInlineAdd()
|
||||
{
|
||||
this.FindControl<TextBox>("InlineAddBox")?.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user