feat(lists): selectable list/folder colors with accent-aware swatches and custom picker

This commit is contained in:
mika kuns
2026-10-01 10:29:53 +02:00
parent d316f3c6a5
commit a6926f53b5
26 changed files with 1529 additions and 45 deletions
@@ -115,12 +115,42 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
public const string DefaultIcon = "Dot";
/// <summary>Folder colour swatches; "" = no colour (theme default).</summary>
public static readonly string[] FolderColorOptions =
["", "#D4A574", "#C87060", "#D9C27A", "#8B9D7A", "#6FB3A8", "#7FA7C9", "#A38BC9"];
/// <summary>Picker swatches: "" (default) followed by the accent-aware palette keys.</summary>
public static readonly string[] SwatchOptions = ["", .. AccentPresets.SwatchKeys];
/// <summary>Selected folder colour, "" for none.</summary>
[ObservableProperty] private string _folderColor = "";
/// <summary>Stored colour: "" (default), a swatch key, or a literal "#RRGGBB". Shared by lists and folders.</summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCustomColor))]
private string _selectedColor = "";
/// <summary>True when <see cref="SelectedColor"/> is a custom hex rather than a swatch.</summary>
public bool IsCustomColor => SelectedColor.StartsWith('#');
// Separate from SelectedColor: the ListBox pushes null when a custom hex (not in SwatchOptions) is current.
[ObservableProperty] private string? _selectedSwatch = "";
/// <summary>Bound to the custom ColorPicker; writes back as "#RRGGBB".</summary>
[ObservableProperty] private Avalonia.Media.Color _customColor = Avalonia.Media.Color.Parse(AccentPresets.Default.Accent);
private bool _syncingColor;
partial void OnSelectedColorChanged(string value)
{
_syncingColor = true;
SelectedSwatch = SwatchOptions.Contains(value) ? value : null;
if (IsCustomColor && Avalonia.Media.Color.TryParse(value, out var c)) CustomColor = c;
_syncingColor = false;
}
partial void OnSelectedSwatchChanged(string? value)
{
if (!_syncingColor && value is not null) SelectedColor = value;
}
partial void OnCustomColorChanged(Avalonia.Media.Color value)
{
if (!_syncingColor) SelectedColor = $"#{value.R:X2}{value.G:X2}{value.B:X2}";
}
/// <summary>Default icon for folders, kept apart from <see cref="DefaultIcon"/> so lists never look like folders.</summary>
public const string DefaultFolderIcon = "Folder";
@@ -226,10 +256,11 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
bool isManual = false,
bool findingsTracked = false,
string? icon = null,
bool canToggleFolder = false)
bool canToggleFolder = false,
string? color = null)
{
CanToggleFolder = canToggleFolder;
_ = LoadAsync(listId, name, workingDir, defaultCommitType, isManual, findingsTracked, icon);
_ = LoadAsync(listId, name, workingDir, defaultCommitType, isManual, findingsTracked, icon, color);
}
/// <summary>
@@ -238,7 +269,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
/// </summary>
public void BeginLoadFolder(string folderId, string name, string? icon, string? color = null)
{
FolderColor = color ?? "";
SelectedColor = color ?? "";
_folderId = folderId;
IsFolder = true;
CanToggleFolder = false;
@@ -254,6 +285,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
bool isManual = false,
bool findingsTracked = false,
string? icon = null,
string? color = null,
CancellationToken ct = default)
{
ListConfigDto? cfg = null;
@@ -268,6 +300,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
// 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 = IsKnownIcon(icon) ? icon! : DefaultIcon;
SelectedColor = color ?? "";
WorkingDir = workingDir ?? "";
DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? CommitTypeRegistry.DefaultType : defaultCommitType;
@@ -364,7 +397,8 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
DefaultCommitType,
IsManual,
FindingsTracked,
Icon));
Icon,
NullIfEmpty(SelectedColor)));
// Tri-state: dropdown never shown (no base URL), or ticket projects failed/never loaded
// (e.g. an unreachable ticket system) -> null, leave the stored link alone. Dropdown shown
@@ -398,7 +432,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
Id = Guid.NewGuid().ToString("N"),
Name = name,
Icon = Icon,
Color = NullIfEmpty(FolderColor),
Color = NullIfEmpty(SelectedColor),
CreatedAt = DateTime.UtcNow,
});
// The create flow pre-creates a list row for this modal to save against; a folder
@@ -409,7 +443,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
}
else
{
await lists.RenameFolderAsync(_folderId, name, Icon, NullIfEmpty(FolderColor));
await lists.RenameFolderAsync(_folderId, name, Icon, NullIfEmpty(SelectedColor));
}
Saved = true;