From c323953f8c146b7a570711a4807623ca7fb4d598 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 19:21:51 +0200 Subject: [PATCH] feat(ui): add DescriptionStepsCard detail component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standalone UserControl combining Description + Steps into one card with a top-right toggle. Description view shows raw editor or composed MarkdownView (title + description + open steps). Steps view has an add-step input and subtask rows with inline editing and check circles. Adds Icon.Text geometry to IslandStyles for the steps→description toggle. Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Ui/Design/IslandStyles.axaml | 3 + .../Detail/DescriptionStepsCardViewModel.cs | 133 ++++++++++++++ .../Islands/Detail/DescriptionStepsCard.axaml | 167 ++++++++++++++++++ .../Detail/DescriptionStepsCard.axaml.cs | 35 ++++ 4 files changed, 338 insertions(+) create mode 100644 src/ClaudeDo.Ui/ViewModels/Islands/Detail/DescriptionStepsCardViewModel.cs create mode 100644 src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml create mode 100644 src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml.cs diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml index ab1ae34..76cc491 100644 --- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml +++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml @@ -85,6 +85,9 @@ M13 4 H20 V11 H18 V7.4 L11.4 14 L10 12.6 L16.6 6 H13 Z M4 6 H10 V8 H6 V18 H16 V14 H18 V20 H4 Z + + M4 6 H20 V8 H4 Z M4 11 H20 V13 H4 Z M4 16 H14 V18 H4 Z + F0 M12 3 L22 20 H2 Z M11 9 H13 V14 H11 Z M11 16 H13 V18 H11 Z diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/Detail/DescriptionStepsCardViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/Detail/DescriptionStepsCardViewModel.cs new file mode 100644 index 0000000..dab0050 --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Islands/Detail/DescriptionStepsCardViewModel.cs @@ -0,0 +1,133 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Text; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace ClaudeDo.Ui.ViewModels.Islands.Detail; + +public partial class SubtaskRowSampleViewModel : ObservableObject +{ + [ObservableProperty] string _title = ""; + [ObservableProperty] bool _done; + [ObservableProperty] bool _isEditing; + + [RelayCommand] void ToggleDone() => Done = !Done; + [RelayCommand] void BeginEdit() => IsEditing = true; + [RelayCommand] void CommitEdit() => IsEditing = false; +} + +public partial class DescriptionStepsCardViewModel : ClaudeDo.Ui.ViewModels.ViewModelBase +{ + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsDescriptionView))] + bool _isStepsView; + + public bool IsDescriptionView => !IsStepsView; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ComposedPreview))] + string _title = ""; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ComposedPreview))] + string _editableDescription = ""; + + [ObservableProperty] bool _isEditingDescription; + [ObservableProperty] string _newSubtaskTitle = ""; + + public ObservableCollection Subtasks { get; } = new(); + + public string ComposedPreview => BuildComposedPreview(); + + [RelayCommand] void ToggleCardView() => IsStepsView = !IsStepsView; + [RelayCommand] void ToggleEditDescription() => IsEditingDescription = !IsEditingDescription; + + [RelayCommand] + void AddSubtask() + { + if (string.IsNullOrWhiteSpace(NewSubtaskTitle)) return; + var row = new SubtaskRowSampleViewModel { Title = NewSubtaskTitle.Trim() }; + row.PropertyChanged += OnRowPropertyChanged; + Subtasks.Add(row); + NewSubtaskTitle = ""; + OnPropertyChanged(nameof(ComposedPreview)); + } + + [RelayCommand] + void ToggleSubtaskDone(SubtaskRowSampleViewModel row) + { + row.Done = !row.Done; + } + + [RelayCommand] + void CommitSubtaskEdit(SubtaskRowSampleViewModel row) + { + row.IsEditing = false; + } + + private void OnRowPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(SubtaskRowSampleViewModel.Done) + or nameof(SubtaskRowSampleViewModel.Title)) + OnPropertyChanged(nameof(ComposedPreview)); + } + + private void OnSubtasksChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems is not null) + foreach (SubtaskRowSampleViewModel row in e.NewItems) + row.PropertyChanged += OnRowPropertyChanged; + if (e.OldItems is not null) + foreach (SubtaskRowSampleViewModel row in e.OldItems) + row.PropertyChanged -= OnRowPropertyChanged; + OnPropertyChanged(nameof(ComposedPreview)); + } + + private string BuildComposedPreview() + { + var sb = new StringBuilder(); + sb.AppendLine(Title); + + if (!string.IsNullOrWhiteSpace(EditableDescription)) + { + sb.AppendLine(); + sb.AppendLine(EditableDescription.TrimEnd()); + } + + var openSteps = Subtasks.Where(s => !s.Done).ToList(); + if (openSteps.Count > 0) + { + sb.AppendLine(); + sb.AppendLine("## Sub-Tasks"); + foreach (var step in openSteps) + sb.AppendLine($"- [ ] {step.Title}"); + } + + return sb.ToString().TrimEnd(); + } + + public DescriptionStepsCardViewModel() + { + Subtasks.CollectionChanged += OnSubtasksChanged; + + _title = "Refactor diff viewer"; + _editableDescription = + "Split the current monolithic diff renderer into smaller, testable components.\n\n" + + "The goal is to improve readability and allow unit testing of each parsing stage independently."; + + var samples = new[] + { + new SubtaskRowSampleViewModel { Title = "Extract DiffParser into its own class", Done = true }, + new SubtaskRowSampleViewModel { Title = "Add unit tests for DiffParser", Done = true }, + new SubtaskRowSampleViewModel { Title = "Wire new parser into DiffViewerViewModel" }, + new SubtaskRowSampleViewModel { Title = "Update snapshot tests" }, + }; + foreach (var row in samples) + { + row.PropertyChanged += OnRowPropertyChanged; + Subtasks.Add(row); + } + } +} diff --git a/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml b/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml new file mode 100644 index 0000000..be69475 --- /dev/null +++ b/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml.cs new file mode 100644 index 0000000..db581db --- /dev/null +++ b/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using ClaudeDo.Ui.ViewModels.Islands.Detail; + +namespace ClaudeDo.Ui.Views.Islands.Detail; + +public partial class DescriptionStepsCard : UserControl +{ + public DescriptionStepsCard() + { + InitializeComponent(); + } + + private async void OnCopyClick(object? sender, RoutedEventArgs e) + { + if (DataContext is not DescriptionStepsCardViewModel vm) return; + var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; + if (clipboard is null) return; + await clipboard.SetTextAsync(vm.ComposedPreview); + } + + private void OnSubtaskTitleTapped(object? sender, TappedEventArgs e) + { + if (sender is TextBlock { DataContext: SubtaskRowSampleViewModel row }) + row.IsEditing = true; + } + + private void OnSubtaskEditLostFocus(object? sender, RoutedEventArgs e) + { + if (sender is TextBox { DataContext: SubtaskRowSampleViewModel row }) + row.IsEditing = false; + } +}