style(ui): lists icons, section headers, active state
- Add central icon library (Icon.Sun/Activity/Star/Calendar/Eye/Inbox/Folder/Search/Plus/MoreHorizontal/GitBranch/Copy/Trash/Sort/X/Check) to IslandStyles.axaml - Add list-section-label, search-wrap, kbd, new-list-btn, avatar-circle styles - Add UpperCaseConverter, IconKeyConverter, DotBrushConverter; register in App.axaml - Expose SmartLists / UserLists filtered collections from ListsIslandViewModel - Add DotColorKey (Moss/Peat/Sage rotation) and UserInitials/UserName/MachineName props Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@
|
||||
<converters:NotNullToBoolConverter x:Key="NotNullToBool"/>
|
||||
<converters:StrikeIfTrueConverter x:Key="StrikeIfTrue"/>
|
||||
<converters:EqStatusConverter x:Key="EqStatus"/>
|
||||
<converters:UpperCaseConverter x:Key="UpperCase"/>
|
||||
<converters:IconKeyConverter x:Key="IconKey"/>
|
||||
<converters:DotBrushConverter x:Key="DotBrush"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
24
src/ClaudeDo.Ui/Converters/DotBrushConverter.cs
Normal file
24
src/ClaudeDo.Ui/Converters/DotBrushConverter.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace ClaudeDo.Ui.Converters;
|
||||
|
||||
public class DotBrushConverter : IValueConverter
|
||||
{
|
||||
public static DotBrushConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
var key = value?.ToString();
|
||||
if (string.IsNullOrEmpty(key)) key = "Moss";
|
||||
var resourceKey = $"{key}Brush";
|
||||
if (Application.Current?.TryGetResource(resourceKey, null, out var res) == true)
|
||||
return res as IBrush;
|
||||
return Brushes.Transparent;
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
28
src/ClaudeDo.Ui/Converters/IconKeyConverter.cs
Normal file
28
src/ClaudeDo.Ui/Converters/IconKeyConverter.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace ClaudeDo.Ui.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts an icon key string (e.g. "Sun") to the matching StreamGeometry resource "Icon.Sun".
|
||||
/// </summary>
|
||||
public class IconKeyConverter : IValueConverter
|
||||
{
|
||||
public static IconKeyConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not string key || string.IsNullOrEmpty(key))
|
||||
return null;
|
||||
|
||||
var resourceKey = $"Icon.{key}";
|
||||
if (Application.Current?.TryGetResource(resourceKey, null, out var res) == true)
|
||||
return res as StreamGeometry;
|
||||
return null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
15
src/ClaudeDo.Ui/Converters/UpperCaseConverter.cs
Normal file
15
src/ClaudeDo.Ui/Converters/UpperCaseConverter.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace ClaudeDo.Ui.Converters;
|
||||
|
||||
public class UpperCaseConverter : IValueConverter
|
||||
{
|
||||
public static UpperCaseConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> value?.ToString()?.ToUpperInvariant();
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
@@ -24,6 +24,60 @@
|
||||
<StreamGeometry x:Key="Icon.WinClose">M5 5l14 14M19 5L5 19</StreamGeometry>
|
||||
<!-- Brand check glyph (checkbox-style tick inside a rounded square) -->
|
||||
<StreamGeometry x:Key="Icon.BrandCheck">M3 3 h18 v18 h-18 z M6 12 l4 4 8-8</StreamGeometry>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Icons — central icon library (Phase B) -->
|
||||
<!-- All d-strings sourced from icons.jsx -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- Icon.Sun -->
|
||||
<StreamGeometry x:Key="Icon.Sun">M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M12 3v2M12 19v2M3 12h2M19 12h2M5.5 5.5l1.4 1.4M17.1 17.1l1.4 1.4M5.5 18.5l1.4-1.4M17.1 6.9l1.4-1.4</StreamGeometry>
|
||||
|
||||
<!-- Icon.Activity (pulse waveform) -->
|
||||
<StreamGeometry x:Key="Icon.Activity">M3 12h4l2-6 4 12 2-8 2 2h4</StreamGeometry>
|
||||
|
||||
<!-- Icon.Star -->
|
||||
<StreamGeometry x:Key="Icon.Star">M12 3.5l2.6 5.3 5.8.8-4.2 4.1 1 5.8-5.2-2.7-5.2 2.7 1-5.8-4.2-4.1 5.8-.8z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Calendar -->
|
||||
<StreamGeometry x:Key="Icon.Calendar">M3.5 5a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2v15a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2z M3.5 10h17M8 3v4M16 3v4</StreamGeometry>
|
||||
|
||||
<!-- Icon.Eye -->
|
||||
<StreamGeometry x:Key="Icon.Eye">M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Inbox -->
|
||||
<StreamGeometry x:Key="Icon.Inbox">M3 13h5l1 2h6l1-2h5M3 13l3-8h12l3 8v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Folder -->
|
||||
<StreamGeometry x:Key="Icon.Folder">M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Search -->
|
||||
<StreamGeometry x:Key="Icon.Search">M11 4a7 7 0 1 0 0 14A7 7 0 0 0 11 4z M20 20l-3.5-3.5</StreamGeometry>
|
||||
|
||||
<!-- Icon.Plus -->
|
||||
<StreamGeometry x:Key="Icon.Plus">M12 5v14M5 12h14</StreamGeometry>
|
||||
|
||||
<!-- Icon.MoreHorizontal (three filled dots) — uses fill so rendered via PathIcon with fill brush -->
|
||||
<StreamGeometry x:Key="Icon.MoreHorizontal">M5 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0 M12 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0 M19 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0</StreamGeometry>
|
||||
|
||||
<!-- Icon.GitBranch -->
|
||||
<StreamGeometry x:Key="Icon.GitBranch">M6 3a2 2 0 1 0 0 4 2 2 0 0 0 0-4z M6 19a2 2 0 1 0 0 4 2 2 0 0 0 0-4z M18 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4z M6 7v10M6 13c0-4 12-2 12-4</StreamGeometry>
|
||||
|
||||
<!-- Icon.Copy -->
|
||||
<StreamGeometry x:Key="Icon.Copy">M8 8h12a1.5 1.5 0 0 1 1.5 1.5v12A1.5 1.5 0 0 1 20 23H8a1.5 1.5 0 0 1-1.5-1.5v-12A1.5 1.5 0 0 1 8 8z M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3</StreamGeometry>
|
||||
|
||||
<!-- Icon.Trash -->
|
||||
<StreamGeometry x:Key="Icon.Trash">M4 7h16M10 11v6M14 11v6M5 7l1 13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1l1-13M9 7V4h6v3</StreamGeometry>
|
||||
|
||||
<!-- Icon.Sort -->
|
||||
<StreamGeometry x:Key="Icon.Sort">M7 4v16M7 20l-3-3M7 4l-3 3M14 8h7M14 12h5M14 16h3</StreamGeometry>
|
||||
|
||||
<!-- Icon.X -->
|
||||
<StreamGeometry x:Key="Icon.X">M6 6l12 12M18 6L6 18</StreamGeometry>
|
||||
|
||||
<!-- Icon.Check -->
|
||||
<StreamGeometry x:Key="Icon.Check">M4 12l5 5 11-11</StreamGeometry>
|
||||
|
||||
</Styles.Resources>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
@@ -346,6 +400,7 @@
|
||||
<Setter Property="Padding" Value="10,7" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="Border.list-item:pointerover">
|
||||
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
||||
@@ -353,5 +408,95 @@
|
||||
<Style Selector="Border.list-item.active">
|
||||
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
||||
</Style>
|
||||
<!-- Active item text / icon colors -->
|
||||
<Style Selector="Border.list-item.active TextBlock.list-label">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.list-item.active PathIcon.list-icon">
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- LIST SECTION HEADER -->
|
||||
<!-- ============================================================ -->
|
||||
<Style Selector="TextBlock.list-section-label">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="9" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
|
||||
<Setter Property="Margin" Value="10,10,10,4" />
|
||||
<Setter Property="LetterSpacing" Value="1.2" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- SEARCH BOX WRAPPER -->
|
||||
<!-- ============================================================ -->
|
||||
<Style Selector="Border.search-wrap">
|
||||
<Setter Property="Background" Value="{StaticResource DeepBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Padding" Value="8,0" />
|
||||
</Style>
|
||||
<Style Selector="Border.search-wrap:focus-within">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Style>
|
||||
<!-- Borderless TextBox inside search-wrap -->
|
||||
<Style Selector="Border.search-wrap TextBox.search-inner">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="4,7" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.search-wrap TextBox.search-inner /template/ Border#PART_BorderElement">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="BoxShadow" Value="none" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- KBD CHIP -->
|
||||
<!-- ============================================================ -->
|
||||
<Style Selector="Border.kbd">
|
||||
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Padding" Value="6,2" />
|
||||
</Style>
|
||||
<Style Selector="Border.kbd > TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- NEW LIST BUTTON -->
|
||||
<!-- ============================================================ -->
|
||||
<Style Selector="Button.new-list-btn">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
<Style Selector="Button.new-list-btn:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- FOOTER PROFILE ROW -->
|
||||
<!-- ============================================================ -->
|
||||
<Style Selector="Border.avatar-circle">
|
||||
<Setter Property="Width" Value="28" />
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Background" Value="{StaticResource AccentDimBrush}" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
|
||||
@@ -10,4 +10,5 @@ public sealed partial class ListNavItemViewModel : ViewModelBase
|
||||
[ObservableProperty] private int _count;
|
||||
[ObservableProperty] private bool _isActive;
|
||||
public string? IconKey { get; init; }
|
||||
public string? DotColorKey { get; init; }
|
||||
}
|
||||
|
||||
@@ -18,30 +18,63 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
|
||||
public void RequestFocusSearch() => FocusSearchRequested?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
public ObservableCollection<ListNavItemViewModel> Items { get; } = new();
|
||||
public ObservableCollection<ListNavItemViewModel> SmartLists { get; } = new();
|
||||
public ObservableCollection<ListNavItemViewModel> UserLists { get; } = new();
|
||||
|
||||
[ObservableProperty] private string _searchText = "";
|
||||
[ObservableProperty] private ListNavItemViewModel? _selectedList;
|
||||
|
||||
public string UserName { get; } = Environment.UserName;
|
||||
public string MachineName { get; } = Environment.MachineName;
|
||||
public string UserInitials { get; }
|
||||
|
||||
public ListsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
var parts = Environment.UserName.Split('.', '_', '-', ' ');
|
||||
UserInitials = parts.Length >= 2
|
||||
? $"{parts[0][0]}{parts[1][0]}".ToUpperInvariant()
|
||||
: Environment.UserName.Length >= 2
|
||||
? Environment.UserName[..2].ToUpperInvariant()
|
||||
: Environment.UserName.ToUpperInvariant();
|
||||
}
|
||||
|
||||
public async Task LoadAsync(CancellationToken ct = default)
|
||||
{
|
||||
Items.Clear();
|
||||
Items.Add(new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" });
|
||||
Items.Add(new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" });
|
||||
Items.Add(new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" });
|
||||
Items.Add(new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Pulse" });
|
||||
Items.Add(new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" });
|
||||
SmartLists.Clear();
|
||||
UserLists.Clear();
|
||||
|
||||
var smart = new[]
|
||||
{
|
||||
new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" },
|
||||
new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" },
|
||||
new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" },
|
||||
new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Activity" },
|
||||
new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" },
|
||||
};
|
||||
foreach (var s in smart) { Items.Add(s); SmartLists.Add(s); }
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var lists = new ListRepository(ctx);
|
||||
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
||||
var dotColors = new[] { "Moss", "Peat", "Sage" };
|
||||
int idx = 0;
|
||||
foreach (var l in await lists.GetAllAsync(ct))
|
||||
if (!seedNames.Contains(l.Name))
|
||||
Items.Add(new ListNavItemViewModel { Id = $"user:{l.Id}", Name = l.Name, Kind = ListKind.User, IconKey = "Folder" });
|
||||
{
|
||||
if (seedNames.Contains(l.Name)) continue;
|
||||
var item = new ListNavItemViewModel
|
||||
{
|
||||
Id = $"user:{l.Id}",
|
||||
Name = l.Name,
|
||||
Kind = ListKind.User,
|
||||
IconKey = "Folder",
|
||||
DotColorKey = dotColors[idx % dotColors.Length],
|
||||
};
|
||||
Items.Add(item);
|
||||
UserLists.Add(item);
|
||||
idx++;
|
||||
}
|
||||
|
||||
await RefreshCountsAsync(ct);
|
||||
SelectedList = Items.FirstOrDefault();
|
||||
|
||||
Reference in New Issue
Block a user