feat(lists): selectable list/folder colors with accent-aware swatches and custom picker
This commit is contained in:
@@ -38,8 +38,16 @@ public sealed partial class ListNavItemViewModel : ViewModelBase
|
||||
/// <summary>No icon picked — the row shows a coloured dot instead of a glyph.</summary>
|
||||
public bool IsDot => IconKey is null or ListsIslandViewModel.ListIcon;
|
||||
|
||||
/// <summary>Picked colour (swatch key or "#RRGGBB"); null = the hash-derived identity colour.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(DotColor))]
|
||||
private string? _color;
|
||||
|
||||
/// <summary>Identity colour of the dot, stable per list id across restarts.</summary>
|
||||
public string DotColorKey => $"Identity{ConPtyPaneViewModel.StableIndex(Id, 6) + 1}";
|
||||
|
||||
/// <summary>What the dot renders: the picked colour, else <see cref="DotColorKey"/>.</summary>
|
||||
public string DotColor => string.IsNullOrEmpty(Color) ? DotColorKey : Color;
|
||||
// The sidebar folder this row sits in (ListEntity.FolderId); null = top level.
|
||||
[ObservableProperty] private string? _folderId;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||
vm.BeginLoad(rawId, row.Name, row.WorkingDir, row.DefaultCommitType, row.IsManual, row.FindingsTracked, row.IconKey);
|
||||
vm.BeginLoad(rawId, row.Name, row.WorkingDir, row.DefaultCommitType, row.IsManual, row.FindingsTracked, row.IconKey, color: row.Color);
|
||||
await Dialogs.ShowListSettingsAsync(vm);
|
||||
if (vm.Deleted) await LoadAsync();
|
||||
else await RefreshRowAsync(row.Id);
|
||||
@@ -373,6 +373,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
Name = l.Name,
|
||||
Kind = ListKind.User,
|
||||
IconKey = l.Icon ?? ListIcon,
|
||||
Color = l.Color,
|
||||
WorkingDir = l.WorkingDir,
|
||||
DefaultCommitType = l.DefaultCommitType,
|
||||
IsManual = l.IsManual,
|
||||
@@ -492,7 +493,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||
// canToggleFolder: only the create flow offers "make this a folder instead".
|
||||
vm.BeginLoad(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType, entity.IsManual, entity.FindingsTracked, entity.Icon, canToggleFolder: true);
|
||||
vm.BeginLoad(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType, entity.IsManual, entity.FindingsTracked, entity.Icon, canToggleFolder: true, color: entity.Color);
|
||||
await Dialogs.ShowListSettingsAsync(vm);
|
||||
if (!vm.Saved && !vm.Deleted)
|
||||
{
|
||||
@@ -845,6 +846,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
row.IsManual = entity.IsManual;
|
||||
row.FindingsTracked = entity.FindingsTracked;
|
||||
row.IconKey = entity.Icon ?? ListIcon;
|
||||
row.Color = entity.Color;
|
||||
row.HasTicketProject = hasTicketProject;
|
||||
RecomputeHasNoLinkedRepo();
|
||||
}
|
||||
@@ -891,6 +893,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
Name = entity.Name,
|
||||
Kind = ListKind.User,
|
||||
IconKey = entity.Icon ?? ListIcon,
|
||||
Color = entity.Color,
|
||||
WorkingDir = entity.WorkingDir,
|
||||
DefaultCommitType = entity.DefaultCommitType,
|
||||
IsManual = entity.IsManual,
|
||||
|
||||
@@ -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