feat(lists): selectable list/folder colors with accent-aware swatches and custom picker
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user