feat(lists): assign a colour to a sidebar folder
Tints the folder name and its guide line; picked from a swatch row in the folder settings.
This commit is contained in:
@@ -9,6 +9,16 @@ namespace ClaudeDo.Ui.Converters;
|
||||
// instantiate a generic FuncValueConverter as a resource; they stay one per type so the
|
||||
// x:Key names in App.axaml keep resolving. Anything with real logic gets its own file.
|
||||
|
||||
/// <summary>"#RRGGBB" to a brush; anything else falls back to the app resource named by the parameter.</summary>
|
||||
public sealed class HexBrushConverter : OneWayConverter
|
||||
{
|
||||
public static HexBrushConverter Instance { get; } = new();
|
||||
public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> value is string s && Color.TryParse(s, out var c)
|
||||
? new SolidColorBrush(c)
|
||||
: Avalonia.Application.Current?.TryGetResource(parameter as string ?? "", null, out var res) == true ? res : null;
|
||||
}
|
||||
|
||||
public sealed class UpperCaseConverter : OneWayConverter
|
||||
{
|
||||
public static UpperCaseConverter Instance { get; } = new();
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed partial class ListFolderNavItemViewModel : ViewModelBase
|
||||
public string? ParentFolderId { get; set; }
|
||||
[ObservableProperty] private string _name = "";
|
||||
[ObservableProperty] private string _iconKey = "Folder";
|
||||
/// <summary>Hex colour for name + guide line; null = theme default.</summary>
|
||||
[ObservableProperty] private string? _color;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowCount))]
|
||||
private bool _isCollapsed;
|
||||
|
||||
@@ -352,6 +352,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
Id = f.Id,
|
||||
Name = f.Name,
|
||||
IconKey = f.Icon ?? ListFolderIcon,
|
||||
Color = f.Color,
|
||||
IsCollapsed = f.IsCollapsed,
|
||||
ParentFolderId = f.ParentFolderId,
|
||||
};
|
||||
@@ -758,7 +759,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
if (folder is null || Dialogs is null || _services is null) return;
|
||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||
vm.BeginLoadFolder(folder.Id, folder.Name, folder.IconKey);
|
||||
vm.BeginLoadFolder(folder.Id, folder.Name, folder.IconKey, folder.Color);
|
||||
await Dialogs.ShowListSettingsAsync(vm);
|
||||
// Rename, re-icon and delete all change the folder header (and delete releases its lists
|
||||
// back to the top level) — one reload covers every outcome.
|
||||
|
||||
@@ -115,6 +115,13 @@ 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>Selected folder colour, "" for none.</summary>
|
||||
[ObservableProperty] private string _folderColor = "";
|
||||
|
||||
/// <summary>Default icon for folders, kept apart from <see cref="DefaultIcon"/> so lists never look like folders.</summary>
|
||||
public const string DefaultFolderIcon = "Folder";
|
||||
|
||||
@@ -229,8 +236,9 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
||||
/// Opens the modal on an existing folder: name + icon only, no list config to load and
|
||||
/// nothing to fetch from the worker, so this needs no async counterpart.
|
||||
/// </summary>
|
||||
public void BeginLoadFolder(string folderId, string name, string? icon)
|
||||
public void BeginLoadFolder(string folderId, string name, string? icon, string? color = null)
|
||||
{
|
||||
FolderColor = color ?? "";
|
||||
_folderId = folderId;
|
||||
IsFolder = true;
|
||||
CanToggleFolder = false;
|
||||
@@ -375,6 +383,8 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
|
||||
private static string? NullIfEmpty(string s) => s.Length == 0 ? null : s;
|
||||
|
||||
private async Task SaveFolderAsync()
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name;
|
||||
@@ -388,6 +398,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Name = name,
|
||||
Icon = Icon,
|
||||
Color = NullIfEmpty(FolderColor),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
// The create flow pre-creates a list row for this modal to save against; a folder
|
||||
@@ -398,7 +409,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
||||
}
|
||||
else
|
||||
{
|
||||
await lists.RenameFolderAsync(_folderId, name, Icon);
|
||||
await lists.RenameFolderAsync(_folderId, name, Icon, NullIfEmpty(FolderColor));
|
||||
}
|
||||
|
||||
Saved = true;
|
||||
|
||||
@@ -153,7 +153,8 @@
|
||||
<TextBlock Grid.Column="2" Classes="list-label"
|
||||
Text="{Binding Name}" ToolTip.Tip="{Binding Name}"
|
||||
VerticalAlignment="Center" Margin="8,0" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
|
||||
Foreground="{Binding Color, Converter={x:Static converters:HexBrushConverter.Instance}, ConverterParameter=TextDimBrush}"
|
||||
FontSize="{StaticResource FontSizeBody}"/>
|
||||
<TextBlock Grid.Column="3" Classes="list-count"
|
||||
Text="{Binding Count}" IsVisible="{Binding ShowCount}"/>
|
||||
</Grid>
|
||||
@@ -163,7 +164,8 @@
|
||||
glyph); children start past it so their highlight never touches the line. -->
|
||||
<Panel IsVisible="{Binding !IsCollapsed}">
|
||||
<Border Width="1" HorizontalAlignment="Left" Margin="8,0,0,4"
|
||||
Background="{DynamicResource LineBrush}" IsHitTestVisible="False"/>
|
||||
Background="{Binding Color, Converter={x:Static converters:HexBrushConverter.Instance}, ConverterParameter=LineBrush}"
|
||||
IsHitTestVisible="False"/>
|
||||
<ItemsControl ItemsSource="{Binding Children}" Margin="14,0,0,0"/>
|
||||
</Panel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
|
||||
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||
xmlns:conv="using:ClaudeDo.Ui.Converters"
|
||||
x:Class="ClaudeDo.Ui.Views.Modals.ListSettingsModalView"
|
||||
x:DataType="vm:ListSettingsModalViewModel"
|
||||
Title="{loc:Tr modals.listSettings.title}"
|
||||
@@ -88,6 +89,30 @@
|
||||
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4" IsVisible="{Binding IsFolder}">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.folderColor}"/>
|
||||
<ListBox ItemsSource="{x:Static vm:ListSettingsModalViewModel.FolderColorOptions}"
|
||||
SelectedItem="{Binding FolderColor, Mode=TwoWay}"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
HorizontalAlignment="Left">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<!-- "" renders as an empty ring: the "no colour" swatch. -->
|
||||
<Border Width="18" Height="18" Margin="3" CornerRadius="9"
|
||||
BorderThickness="1" BorderBrush="{DynamicResource LineBrightBrush}"
|
||||
Background="{Binding Converter={x:Static conv:HexBrushConverter.Instance}, ConverterParameter=Surface2Brush}"/>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<TextBlock Text="{loc:Tr modals.listSettings.folderColorHint}"
|
||||
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.workingDirectory}"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
|
||||
Reference in New Issue
Block a user