feat(claude-do): merge Diff viewer file tree: folder/file-type icons and change-kin

ClaudeDo-Task: c5106d39c4e849a6ad0027f77fad742c
This commit is contained in:
mika kuns
2026-10-02 10:33:02 +02:00
4 changed files with 119 additions and 5 deletions
+30
View File
@@ -292,6 +292,23 @@
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
</Style> </Style>
<!-- diff file tree: folders stay neutral, file icons take the change-kind colour via Tag (A/D/M/R) -->
<Style Selector="PathIcon.tree-icon">
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
</Style>
<Style Selector="PathIcon.tree-icon[Tag=A]">
<Setter Property="Foreground" Value="{StaticResource MossBrush}" />
</Style>
<Style Selector="PathIcon.tree-icon[Tag=D]">
<Setter Property="Foreground" Value="{StaticResource BloodBrush}" />
</Style>
<Style Selector="PathIcon.tree-icon[Tag=M]">
<Setter Property="Foreground" Value="{StaticResource PeatBrush}" />
</Style>
<Style Selector="PathIcon.tree-icon[Tag=R]">
<Setter Property="Foreground" Value="{StaticResource SageBrush}" />
</Style>
<!-- hover --> <!-- hover -->
<Style Selector=".icon-btn:pointerover PathIcon, .new-list-btn:pointerover PathIcon"> <Style Selector=".icon-btn:pointerover PathIcon, .new-list-btn:pointerover PathIcon">
<Setter Property="Foreground" Value="{StaticResource TextBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextBrush}" />
@@ -1071,6 +1088,19 @@
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" /> <Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style> </Style>
<!-- diff file tree names: change-kind colour; must stay after TextBlock.meta -->
<Style Selector="TextBlock.tree-name[Tag=A]">
<Setter Property="Foreground" Value="{StaticResource MossBrush}" />
</Style>
<Style Selector="TextBlock.tree-name[Tag=D]">
<Setter Property="Foreground" Value="{StaticResource BloodBrush}" />
</Style>
<Style Selector="TextBlock.tree-name[Tag=M]">
<Setter Property="Foreground" Value="{StaticResource PeatBrush}" />
</Style>
<Style Selector="TextBlock.tree-name[Tag=R]">
<Setter Property="Foreground" Value="{StaticResource SageBrush}" />
</Style>
<!-- Default body / list values / descriptions --> <!-- Default body / list values / descriptions -->
<Style Selector="TextBlock.body"> <Style Selector="TextBlock.body">
@@ -1,5 +1,6 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Material.Icons;
namespace ClaudeDo.Ui.ViewModels.Modals; namespace ClaudeDo.Ui.ViewModels.Modals;
@@ -63,7 +64,14 @@ public sealed partial class DiffTreeNodeViewModel : ViewModelBase
public string RelativePath { get; init; } = ""; public string RelativePath { get; init; } = "";
public DiffFileViewModel? File { get; init; } public DiffFileViewModel? File { get; init; }
public ObservableCollection<DiffTreeNodeViewModel> Children { get; } = new(); public ObservableCollection<DiffTreeNodeViewModel> Children { get; } = new();
[ObservableProperty] private bool _isExpanded = true; [ObservableProperty]
[NotifyPropertyChangedFor(nameof(IconKey))]
private bool _isExpanded = true;
/// <summary>The row's <c>mdi:</c> icon key: open/closed folder, or the file-type icon.</summary>
public string IconKey => IsDirectory
? (IsExpanded ? "mdi:FolderOpen" : "mdi:Folder")
: DiffFileIcons.ForPath(Name);
public string? StatusCode => File?.StatusCode; public string? StatusCode => File?.StatusCode;
public bool ShowStats => File is { IsBinary: false, IsPureRename: false }; public bool ShowStats => File is { IsBinary: false, IsPureRename: false };
@@ -71,6 +79,36 @@ public sealed partial class DiffTreeNodeViewModel : ViewModelBase
public int Deletions => File?.Deletions ?? 0; public int Deletions => File?.Deletions ?? 0;
} }
/// <summary>Maps a file name's extension to an <c>mdi:</c> icon key for the diff file tree.</summary>
public static class DiffFileIcons
{
private const MaterialIconKind Fallback = MaterialIconKind.FileOutline;
private static readonly Dictionary<string, MaterialIconKind> ByExtension = new(StringComparer.OrdinalIgnoreCase)
{
[".cs"] = MaterialIconKind.LanguageCsharp,
[".axaml"] = MaterialIconKind.CodeTags,
[".xaml"] = MaterialIconKind.CodeTags,
[".xml"] = MaterialIconKind.CodeTags,
[".json"] = MaterialIconKind.CodeJson,
[".md"] = MaterialIconKind.LanguageMarkdownOutline,
[".csproj"] = MaterialIconKind.FileCogOutline,
[".slnx"] = MaterialIconKind.FileCogOutline,
[".ps1"] = MaterialIconKind.Powershell,
[".sh"] = MaterialIconKind.Console,
[".png"] = MaterialIconKind.FileImageOutline,
[".jpg"] = MaterialIconKind.FileImageOutline,
[".jpeg"] = MaterialIconKind.FileImageOutline,
[".gif"] = MaterialIconKind.FileImageOutline,
[".svg"] = MaterialIconKind.FileImageOutline,
[".ico"] = MaterialIconKind.FileImageOutline,
};
/// <summary>Icon key for a file name or path; unknown or missing extensions fall back to a generic file.</summary>
public static string ForPath(string path) =>
"mdi:" + (ByExtension.TryGetValue(System.IO.Path.GetExtension(path), out var kind) ? kind : Fallback);
}
/// Builds a folder-grouped tree from a flat list of parsed diff files. /// Builds a folder-grouped tree from a flat list of parsed diff files.
public static class DiffTree public static class DiffTree
{ {
@@ -88,17 +88,20 @@
<TreeView.ItemTemplate> <TreeView.ItemTemplate>
<TreeDataTemplate DataType="vm:DiffTreeNodeViewModel" ItemsSource="{Binding Children}"> <TreeDataTemplate DataType="vm:DiffTreeNodeViewModel" ItemsSource="{Binding Children}">
<Border Background="Transparent" Tapped="OnNodeTapped" Cursor="Hand" Padding="0,2"> <Border Background="Transparent" Tapped="OnNodeTapped" Cursor="Hand" Padding="0,2">
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,Auto,*,Auto">
<Border Grid.Column="0" Tag="{Binding StatusCode}" CornerRadius="3" Padding="4,0" Margin="0,0,6,0" <PathIcon Grid.Column="0" Classes="tree-icon" Width="14" Height="14" Margin="0,0,6,0"
Tag="{Binding StatusCode}"
Data="{Binding IconKey, Converter={StaticResource IconKey}}"/>
<Border Grid.Column="1" Tag="{Binding StatusCode}" CornerRadius="3" Padding="4,0" Margin="0,0,6,0"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding StatusCode, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"> IsVisible="{Binding StatusCode, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding StatusCode}" <TextBlock Text="{Binding StatusCode}"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeEyebrow}" FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/> Foreground="{DynamicResource TextBrush}"/>
</Border> </Border>
<TextBlock Grid.Column="1" Classes="meta" Text="{Binding Name}" <TextBlock Grid.Column="2" Classes="meta tree-name" Text="{Binding Name}" Tag="{Binding StatusCode}"
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/> VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center" <StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center"
Margin="0,0,8,0" Margin="0,0,8,0"
IsVisible="{Binding ShowStats}"> IsVisible="{Binding ShowStats}">
<TextBlock Foreground="{DynamicResource MossBrightBrush}" FontSize="{StaticResource FontSizeEyebrow}" <TextBlock Foreground="{DynamicResource MossBrightBrush}" FontSize="{StaticResource FontSizeEyebrow}"
@@ -0,0 +1,43 @@
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Tests;
public class DiffFileIconsTests
{
[Theory]
[InlineData("src/Foo.cs", "mdi:LanguageCsharp")]
[InlineData("Views/A.axaml", "mdi:CodeTags")]
[InlineData("a.xaml", "mdi:CodeTags")]
[InlineData("a.xml", "mdi:CodeTags")]
[InlineData("package.json", "mdi:CodeJson")]
[InlineData("README.md", "mdi:LanguageMarkdownOutline")]
[InlineData("X.csproj", "mdi:FileCogOutline")]
[InlineData("ClaudeDo.slnx", "mdi:FileCogOutline")]
[InlineData("run.ps1", "mdi:Powershell")]
[InlineData("build.sh", "mdi:Console")]
[InlineData("logo.png", "mdi:FileImageOutline")]
public void ForPath_MapsKnownExtensions(string path, string expected)
=> Assert.Equal(expected, DiffFileIcons.ForPath(path));
[Theory]
[InlineData("Foo.CS")]
[InlineData("A.AXAML")]
public void ForPath_IsCaseInsensitive(string path)
=> Assert.NotEqual("mdi:FileOutline", DiffFileIcons.ForPath(path));
[Theory]
[InlineData("Dockerfile")]
[InlineData("notes.unknownext")]
[InlineData("")]
public void ForPath_FallsBackToGenericFile(string path)
=> Assert.Equal("mdi:FileOutline", DiffFileIcons.ForPath(path));
[Fact]
public void FolderIcon_FollowsIsExpanded()
{
var dir = new DiffTreeNodeViewModel { Name = "src", IsDirectory = true };
Assert.Equal("mdi:FolderOpen", dir.IconKey);
dir.IsExpanded = false;
Assert.Equal("mdi:Folder", dir.IconKey);
}
}