feat(ui): searchable Material Design icon library for lists and folders
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user