feat(ui): tasks island with rows, chips, add-task, selection

TaskRowView with status chip (EqStatus converter + parameter),
StrikeIfTrue, NotNullToBool converters. TasksIslandView with header,
add-task TextBox (Enter=AddCommand), ItemsControl + flat Button for
selection. Converters registered in App.axaml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-20 10:24:36 +02:00
parent 4f41b084fa
commit f94bb35db7
7 changed files with 169 additions and 2 deletions

View File

@@ -0,0 +1,26 @@
using System.Globalization;
using Avalonia.Data.Converters;
using ClaudeDo.Data.Models;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.Converters;
/// <summary>
/// Returns true when the bound TaskStatus equals the ConverterParameter string.
/// Usage: Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
/// </summary>
public class EqStatusConverter : IValueConverter
{
public static EqStatusConverter Instance { get; } = new();
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is TaskStatus status && parameter is string name &&
Enum.TryParse<TaskStatus>(name, ignoreCase: true, out var target))
return status == target;
return false;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

View File

@@ -0,0 +1,15 @@
using System.Globalization;
using Avalonia.Data.Converters;
namespace ClaudeDo.Ui.Converters;
public class NotNullToBoolConverter : IValueConverter
{
public static NotNullToBoolConverter Instance { get; } = new();
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is not null;
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

View File

@@ -0,0 +1,16 @@
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace ClaudeDo.Ui.Converters;
public class StrikeIfTrueConverter : IValueConverter
{
public static StrikeIfTrueConverter Instance { get; } = new();
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is true ? TextDecorations.Strikethrough : null;
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

View File

@@ -0,0 +1,57 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.TaskRowView"
x:DataType="vm:TaskRowViewModel">
<Border Classes="task-row" Classes.selected="{Binding IsSelected}">
<Grid ColumnDefinitions="36,*,32" Margin="10,10">
<!-- Done toggle -->
<Button Grid.Column="0" Classes="check-btn" VerticalAlignment="Center"
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleDoneCommand}"
CommandParameter="{Binding}">
<Ellipse Width="18" Height="18" Classes="task-check"
Classes.done="{Binding Done}"/>
</Button>
<!-- Title + chip row + live tail -->
<StackPanel Grid.Column="1" Spacing="6" VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" FontSize="14"
Foreground="{DynamicResource TextBrush}"
TextDecorations="{Binding Done, Converter={StaticResource StrikeIfTrue}}"/>
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="chip"
Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
Classes.review="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Done}"
Classes.error="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Failed}"
Classes.queued="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Queued}">
<TextBlock Text="{Binding Status}" FontSize="10"
FontFamily="{DynamicResource MonoFamily}" Margin="6,2"/>
</Border>
<Border Classes="chip">
<TextBlock Text="{Binding ListName}" FontSize="10" Margin="6,2"
Foreground="{DynamicResource TextDimBrush}"/>
</Border>
<Border Classes="chip" IsVisible="{Binding Branch, Converter={StaticResource NotNullToBool}}">
<TextBlock Text="{Binding Branch}" FontFamily="{DynamicResource MonoFamily}"
FontSize="10" Margin="6,2"/>
</Border>
<Border Classes="chip" IsVisible="{Binding DiffStat, Converter={StaticResource NotNullToBool}}">
<TextBlock Text="{Binding DiffStat}" FontFamily="{DynamicResource MonoFamily}"
FontSize="10" Margin="6,2"/>
</Border>
</StackPanel>
<TextBlock Text="{Binding LiveTail}" FontFamily="{DynamicResource MonoFamily}"
FontSize="11" Foreground="{DynamicResource TextMuteBrush}"
TextTrimming="CharacterEllipsis" MaxLines="1"
IsVisible="{Binding LiveTail, Converter={StaticResource NotNullToBool}}"/>
</StackPanel>
<!-- Star toggle -->
<Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center"
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleStarCommand}"
CommandParameter="{Binding}">
<PathIcon Width="14" Height="14"/>
</Button>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace ClaudeDo.Ui.Views.Islands;
public partial class TaskRowView : UserControl
{
public TaskRowView() { InitializeComponent(); }
}

View File

@@ -1,8 +1,47 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.TasksIslandView"
x:DataType="vm:TasksIslandViewModel">
<TextBlock Margin="14" Text="Tasks (placeholder)"
Foreground="{DynamicResource TextDimBrush}"/>
<DockPanel LastChildFill="True">
<!-- Header -->
<Border DockPanel.Dock="Top" Classes="island-header">
<StackPanel Margin="18,14" Spacing="4">
<TextBlock Classes="eyebrow" Text="{Binding HeaderEyebrow}"/>
<TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="24"
FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}"
Text="{Binding HeaderTitle}"/>
<TextBlock FontFamily="{DynamicResource MonoFamily}" FontSize="11"
Foreground="{DynamicResource TextMuteBrush}" Text="{Binding Subtitle}"/>
</StackPanel>
</Border>
<!-- Add-task row -->
<Border DockPanel.Dock="Top" Margin="18,8,18,4">
<TextBox Watermark="Add a task…" Text="{Binding NewTaskTitle, Mode=TwoWay}">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddCommand}"/>
</TextBox.KeyBindings>
</TextBox>
</Border>
<!-- Task list -->
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Items}" Margin="10,4">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:TaskRowViewModel">
<Button Classes="flat" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].DataContext.SelectCommand}"
CommandParameter="{Binding}">
<islands:TaskRowView/>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</UserControl>