style(ui): task row chip set, selected/done states, live tail
- Expose IsOverdue, Tags, StepsCount/StepsCompleted, DiffAdditions/Deletions on TaskRowViewModel; parse DiffStat into numeric add/del. - Rebuild TaskRowView with explicit chip set: status, list-with-dot, branch (GitBranch icon + mono), diff (+N moss / −M blood), per-tag chips. - Selected row shows 2px accent left bar + AccentSoft background. - Done rows dim to 0.55 opacity with faint title plus strikethrough. - Live-tail row: mono 11px ellipsized text + slim 3px moss progress bar, visible only when Status=Running and HasLiveTail. - task-row Border now transitions Background and Margin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.Generic;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
@@ -18,6 +18,25 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string? _branch;
|
[ObservableProperty] private string? _branch;
|
||||||
[ObservableProperty] private string? _diffStat;
|
[ObservableProperty] private string? _diffStat;
|
||||||
[ObservableProperty] private string? _liveTail;
|
[ObservableProperty] private string? _liveTail;
|
||||||
|
[ObservableProperty] private DateTime? _scheduledFor;
|
||||||
|
[ObservableProperty] private int _diffAdditions;
|
||||||
|
[ObservableProperty] private int _diffDeletions;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
|
||||||
|
public int StepsCount { get; init; }
|
||||||
|
public int StepsCompleted { get; init; }
|
||||||
|
|
||||||
|
public bool HasBranch => !string.IsNullOrWhiteSpace(Branch);
|
||||||
|
public bool HasDiff => DiffAdditions > 0 || DiffDeletions > 0;
|
||||||
|
public bool HasTags => Tags.Count > 0;
|
||||||
|
public bool HasSteps => StepsCount > 0;
|
||||||
|
public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done;
|
||||||
|
public bool IsRunning => Status == TaskStatus.Running;
|
||||||
|
public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail);
|
||||||
|
|
||||||
|
public string DiffAdditionsText => $"+{DiffAdditions}";
|
||||||
|
public string DiffDeletionsText => $"−{DiffDeletions}";
|
||||||
|
public string StepsText => $"{StepsCompleted}/{StepsCount} steps";
|
||||||
|
|
||||||
public string StatusChipClass => Status switch
|
public string StatusChipClass => Status switch
|
||||||
{
|
{
|
||||||
@@ -28,18 +47,51 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
_ => "idle",
|
_ => "idle",
|
||||||
};
|
};
|
||||||
|
|
||||||
partial void OnStatusChanged(TaskStatus value) => OnPropertyChanged(nameof(StatusChipClass));
|
partial void OnStatusChanged(TaskStatus value)
|
||||||
|
|
||||||
public static TaskRowViewModel FromEntity(TaskEntity t) => new()
|
|
||||||
{
|
{
|
||||||
Id = t.Id,
|
OnPropertyChanged(nameof(StatusChipClass));
|
||||||
Title = t.Title,
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
ListName = t.List?.Name ?? "",
|
OnPropertyChanged(nameof(HasLiveTail));
|
||||||
Done = t.Status == TaskStatus.Done,
|
}
|
||||||
IsStarred = t.IsStarred,
|
|
||||||
IsMyDay = t.IsMyDay,
|
partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch));
|
||||||
Status = t.Status,
|
partial void OnLiveTailChanged(string? value) => OnPropertyChanged(nameof(HasLiveTail));
|
||||||
Branch = t.Worktree?.BranchName,
|
partial void OnDoneChanged(bool value) => OnPropertyChanged(nameof(IsOverdue));
|
||||||
DiffStat = t.Worktree?.DiffStat,
|
partial void OnScheduledForChanged(DateTime? value) => OnPropertyChanged(nameof(IsOverdue));
|
||||||
};
|
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
|
||||||
|
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
|
||||||
|
|
||||||
|
public static TaskRowViewModel FromEntity(TaskEntity t)
|
||||||
|
{
|
||||||
|
var (add, del) = ParseDiffStat(t.Worktree?.DiffStat);
|
||||||
|
return new TaskRowViewModel
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
Title = t.Title,
|
||||||
|
ListName = t.List?.Name ?? "",
|
||||||
|
Done = t.Status == TaskStatus.Done,
|
||||||
|
IsStarred = t.IsStarred,
|
||||||
|
IsMyDay = t.IsMyDay,
|
||||||
|
Status = t.Status,
|
||||||
|
Branch = t.Worktree?.BranchName,
|
||||||
|
DiffStat = t.Worktree?.DiffStat,
|
||||||
|
ScheduledFor = t.ScheduledFor,
|
||||||
|
DiffAdditions = add,
|
||||||
|
DiffDeletions = del,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
||||||
|
private static (int add, int del) ParseDiffStat(string? s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(s)) return (0, 0);
|
||||||
|
int add = 0, del = 0;
|
||||||
|
var parts = s.Split(new[] { ' ', ',', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var p in parts)
|
||||||
|
{
|
||||||
|
if (p.Length > 1 && p[0] == '+' && int.TryParse(p.AsSpan(1), out var a)) add = a;
|
||||||
|
else if (p.Length > 1 && (p[0] == '-' || p[0] == '\u2212') && int.TryParse(p.AsSpan(1), out var d)) del = d;
|
||||||
|
}
|
||||||
|
return (add, del);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,18 @@
|
|||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
||||||
x:Class="ClaudeDo.Ui.Views.Islands.TaskRowView"
|
x:Class="ClaudeDo.Ui.Views.Islands.TaskRowView"
|
||||||
x:DataType="vm:TaskRowViewModel">
|
x:DataType="vm:TaskRowViewModel">
|
||||||
<Border Classes="task-row" Classes.selected="{Binding IsSelected}">
|
<Border Classes="task-row"
|
||||||
<Grid ColumnDefinitions="36,*,32" Margin="10,10">
|
Classes.selected="{Binding IsSelected}"
|
||||||
|
Classes.done="{Binding Done}">
|
||||||
|
<Grid ColumnDefinitions="4,32,*,32" Margin="6,8,10,8">
|
||||||
|
|
||||||
|
<!-- Left accent bar (visible when selected) -->
|
||||||
|
<Border Grid.Column="0" Classes="task-row-accent"
|
||||||
|
IsVisible="{Binding IsSelected}"/>
|
||||||
|
|
||||||
<!-- Done toggle -->
|
<!-- Done toggle -->
|
||||||
<Button Grid.Column="0" Classes="check-btn" VerticalAlignment="Center"
|
<Button Grid.Column="1" Classes="check-btn" VerticalAlignment="Top"
|
||||||
|
Margin="0,2,0,0"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleDoneCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleDoneCommand}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
<Ellipse Width="18" Height="18" Classes="task-check"
|
<Ellipse Width="18" Height="18" Classes="task-check"
|
||||||
@@ -14,43 +22,91 @@
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Title + chip row + live tail -->
|
<!-- Title + chip row + live tail -->
|
||||||
<StackPanel Grid.Column="1" Spacing="6" VerticalAlignment="Center">
|
<StackPanel Grid.Column="2" Spacing="6" VerticalAlignment="Center">
|
||||||
<TextBlock Text="{Binding Title}" FontSize="14"
|
<TextBlock Classes="task-title"
|
||||||
|
Text="{Binding Title}" FontSize="14"
|
||||||
Foreground="{DynamicResource TextBrush}"
|
Foreground="{DynamicResource TextBrush}"
|
||||||
TextDecorations="{Binding Done, Converter={StaticResource StrikeIfTrue}}"/>
|
TextDecorations="{Binding Done, Converter={StaticResource StrikeIfTrue}}"/>
|
||||||
|
|
||||||
|
<!-- Chip row -->
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
|
||||||
|
<!-- Status chip -->
|
||||||
<Border Classes="chip"
|
<Border Classes="chip"
|
||||||
Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
|
Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
|
||||||
Classes.review="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Done}"
|
Classes.review="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Done}"
|
||||||
Classes.error="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Failed}"
|
Classes.error="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Failed}"
|
||||||
Classes.queued="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Queued}">
|
Classes.queued="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Queued}">
|
||||||
<TextBlock Text="{Binding Status}" FontSize="10"
|
<TextBlock Text="{Binding Status}"/>
|
||||||
FontFamily="{DynamicResource MonoFamily}" Margin="6,2"/>
|
|
||||||
</Border>
|
</Border>
|
||||||
<Border Classes="chip">
|
|
||||||
<TextBlock Text="{Binding ListName}" FontSize="10" Margin="6,2"
|
<!-- List chip with dot -->
|
||||||
Foreground="{DynamicResource TextDimBrush}"/>
|
<Border Classes="chip chip-list">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
|
||||||
|
<Ellipse Width="6" Height="6"
|
||||||
|
Fill="{DynamicResource MossBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{Binding ListName}"/>
|
||||||
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<Border Classes="chip" IsVisible="{Binding Branch, Converter={StaticResource NotNullToBool}}">
|
|
||||||
<TextBlock Text="{Binding Branch}" FontFamily="{DynamicResource MonoFamily}"
|
<!-- Branch chip -->
|
||||||
FontSize="10" Margin="6,2"/>
|
<Border Classes="chip chip-branch" IsVisible="{Binding HasBranch}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||||
|
<PathIcon Width="10" Height="10"
|
||||||
|
Data="{StaticResource Icon.GitBranch}"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"/>
|
||||||
|
<TextBlock Text="{Binding Branch}"/>
|
||||||
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<Border Classes="chip" IsVisible="{Binding DiffStat, Converter={StaticResource NotNullToBool}}">
|
|
||||||
<TextBlock Text="{Binding DiffStat}" FontFamily="{DynamicResource MonoFamily}"
|
<!-- Diff chip -->
|
||||||
FontSize="10" Margin="6,2"/>
|
<Border Classes="chip chip-diff" IsVisible="{Binding HasDiff}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="diff-add" Text="{Binding DiffAdditionsText}"/>
|
||||||
|
<TextBlock Classes="diff-del" Text="{Binding DiffDeletionsText}"/>
|
||||||
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Tag chips -->
|
||||||
|
<ItemsControl ItemsSource="{Binding Tags}" IsVisible="{Binding HasTags}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Classes="chip chip-tag">
|
||||||
|
<TextBlock Text="{Binding}"/>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Text="{Binding LiveTail}" FontFamily="{DynamicResource MonoFamily}"
|
|
||||||
FontSize="11" Foreground="{DynamicResource TextMuteBrush}"
|
<!-- Live-tail row (visible when running + has tail) -->
|
||||||
TextTrimming="CharacterEllipsis" MaxLines="1"
|
<Border Classes="task-live-tail" IsVisible="{Binding HasLiveTail}">
|
||||||
IsVisible="{Binding LiveTail, Converter={StaticResource NotNullToBool}}"/>
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Text="{Binding LiveTail}"
|
||||||
|
TextTrimming="CharacterEllipsis" MaxLines="1"/>
|
||||||
|
<Grid Height="3" HorizontalAlignment="Stretch">
|
||||||
|
<Rectangle Fill="{DynamicResource Surface3Brush}"
|
||||||
|
HorizontalAlignment="Stretch" RadiusX="1.5" RadiusY="1.5"/>
|
||||||
|
<Rectangle Fill="{DynamicResource MossBrush}"
|
||||||
|
HorizontalAlignment="Left" Width="60" RadiusX="1.5" RadiusY="1.5"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Star toggle -->
|
<!-- Star toggle -->
|
||||||
<Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center"
|
<Button Grid.Column="3" Classes="icon-btn star-btn"
|
||||||
|
Classes.on="{Binding IsStarred}"
|
||||||
|
VerticalAlignment="Top" Margin="0,2,0,0"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleStarCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleStarCommand}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
<PathIcon Width="14" Height="14"/>
|
<PathIcon Width="14" Height="14" Data="{StaticResource Icon.Star}"/>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
Reference in New Issue
Block a user