feat(ui): searchable Material Design icon library for lists and folders

This commit is contained in:
mika kuns
2026-09-30 09:52:34 +02:00
parent 5da8036b9a
commit f2ff865dca
7 changed files with 120 additions and 5 deletions
@@ -2,12 +2,14 @@ using System.Collections.ObjectModel;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Converters;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Agent;
using ClaudeDo.Ui.ViewModels.Modals.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Material.Icons;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.ViewModels.Modals;
@@ -132,7 +134,66 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
"Box", "Truck", "MapPin", "Camera", "Image", "Barcode", "Skull",
];
public ObservableCollection<string> Icons { get; } = new(IconOptions);
/// <summary>Cap on search hits so the picker's WrapPanel stays responsive.</summary>
public const int MaxIconResults = 200;
// One entry per distinct MDI icon; the enum carries aliases, all of which stay searchable.
private static readonly Lazy<(string Key, string Names)[]> MdiIndex = new(() =>
Enum.GetNames<MaterialIconKind>()
.GroupBy(Enum.Parse<MaterialIconKind>)
.Select(g => (IconKeyConverter.MdiPrefix + g.First(), string.Join(' ', g)))
.ToArray());
/// <summary>Picker items: the curated set, or MDI search hits once the search has 2+ characters.</summary>
[ObservableProperty] private IReadOnlyList<string> _icons = IconOptions;
/// <summary>Free-text filter over the MDI icon names (English).</summary>
[ObservableProperty] private string _iconSearch = "";
/// <summary>True when the search hit more than <see cref="MaxIconResults"/> icons.</summary>
[ObservableProperty] private bool _iconResultsTruncated;
// Separate from Icon: swapping Icons makes the ListBox push null here, which must not wipe the choice.
[ObservableProperty] private string? _selectedIconOption = DefaultIcon;
/// <summary>True for a curated key or an existing "mdi:" kind.</summary>
public static bool IsKnownIcon(string? key) =>
key is not null && (IconOptions.Contains(key)
|| (key.StartsWith(IconKeyConverter.MdiPrefix, StringComparison.Ordinal)
&& Enum.TryParse<MaterialIconKind>(key[IconKeyConverter.MdiPrefix.Length..], out _)));
partial void OnIconSearchChanged(string value)
{
var q = value.Trim();
if (q.Length < 2)
{
IconResultsTruncated = false;
// Keep a chosen MDI icon visible (and selected) in the curated view.
Icons = IconOptions.Contains(Icon) ? IconOptions : [Icon, .. IconOptions];
}
else
{
var hits = MdiIndex.Value
.Where(e => e.Names.Contains(q, StringComparison.OrdinalIgnoreCase))
.Select(e => e.Key)
.Take(MaxIconResults + 1)
.ToList();
IconResultsTruncated = hits.Count > MaxIconResults;
Icons = hits.Take(MaxIconResults).ToList();
}
SelectedIconOption = Icons.Contains(Icon) ? Icon : null;
}
partial void OnSelectedIconOptionChanged(string? value)
{
if (value is not null) Icon = value;
}
partial void OnIconChanged(string value)
{
if (!Icons.Contains(value)) OnIconSearchChanged(IconSearch);
else SelectedIconOption = value;
}
// The shared agent-config editor (Model / MaxTurns / SystemPrompt / AgentFile),
// scoped to this list (list → global inheritance).
@@ -174,7 +235,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
IsFolder = true;
CanToggleFolder = false;
Name = name;
Icon = IconOptions.Contains(icon) ? icon! : DefaultFolderIcon;
Icon = IsKnownIcon(icon) ? icon! : DefaultFolderIcon;
}
public async Task LoadAsync(
@@ -198,7 +259,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
FindingsTracked = findingsTracked;
// An unknown/legacy key would leave the picker with no selection and silently reset the
// icon on the next save — fall back to the default instead.
Icon = IconOptions.Contains(icon) ? icon! : DefaultIcon;
Icon = IsKnownIcon(icon) ? icon! : DefaultIcon;
WorkingDir = workingDir ?? "";
DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? CommitTypeRegistry.DefaultType : defaultCommitType;