Files
ClaudeDo/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml
T

322 lines
18 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:converters="using:ClaudeDo.Ui.Converters"
xmlns:loc="using:ClaudeDo.Ui.Localization"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
x:DataType="vm:ListsIslandViewModel">
<!-- Implicit, type-matched templates: the sidebar's top level is one mixed ItemsControl over
TopLevel (folders and folder-less lists interleaved), and each folder's children resolve the
list template the same way. -->
<!-- Glyph alignment is a style, not a local value, so the nested class can override it. -->
<UserControl.Styles>
<Style Selector="Panel.list-glyph > :is(Control)">
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<Style Selector="Panel.list-glyph.nested > :is(Control)">
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</UserControl.Styles>
<UserControl.DataTemplates>
<!-- One user-list row. Shared by the top level and by each folder's children, so the
row (context menu, drag handlers, drop indicators) exists once. -->
<DataTemplate DataType="vm:ListNavItemViewModel">
<Grid RowDefinitions="Auto,Auto,Auto">
<!-- Above-row drop indicator -->
<Border Grid.Row="0" Height="2" VerticalAlignment="Center" Margin="4,0"
Background="{DynamicResource MossBrush}" CornerRadius="1"
IsVisible="{Binding DropHintAbove}"/>
<Border Grid.Row="1" Classes="list-item tree-row" Classes.active="{Binding IsActive}"
Classes.drop-target="{Binding IsTaskDropTarget}"
Tapped="OnItemTapped"
DragDrop.AllowDrop="True"
DragDrop.DragOver="OnListDragOver"
DragDrop.Drop="OnListDrop">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="{loc:Tr lists.contextWorktrees}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenWorktreesOverviewCommand}"
CommandParameter="{Binding}"/>
<Separator IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<MenuItem Header="{loc:Tr lists.contextOpenExplorer}"
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInExplorerCommand}"
CommandParameter="{Binding}"/>
<MenuItem Header="{loc:Tr lists.contextOpenFindings}"
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenFindingsCommand}"
CommandParameter="{Binding}"/>
<MenuItem Header="{loc:Tr lists.contextOpenTerminal}"
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInTerminalCommand}"
CommandParameter="{Binding}"/>
<MenuItem Header="{loc:Tr lists.contextImportTickets}"
IsVisible="{Binding HasTicketProject}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).ImportTicketsCommand}"
CommandParameter="{Binding}"/>
<Separator/>
<MenuItem Header="{loc:Tr lists.contextRemoveFromFolder}"
IsVisible="{Binding FolderId, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).RemoveFromFolderCommand}"
CommandParameter="{Binding}"/>
<MenuItem Header="{loc:Tr lists.contextSettings}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenListSettingsCommand}"
CommandParameter="{Binding}"/>
</ContextMenu>
</Border.ContextMenu>
<!-- Inside a folder: a spacer for the chevron slot, so list and subfolder icons line up.
At the top level: no spacer, glyph left-aligned to where a folder's chevron starts. -->
<Grid ColumnDefinitions="Auto,20,*,Auto">
<!-- Left accent bar for active state -->
<Border Grid.Column="0" Grid.ColumnSpan="4"
Background="Transparent"
CornerRadius="8" IsHitTestVisible="False"
IsVisible="{Binding IsActive}">
<Border Width="2" Height="16"
Background="{DynamicResource AccentBrush}"
CornerRadius="1"
HorizontalAlignment="Left" VerticalAlignment="Center"
Margin="-2,0,0,0"/>
</Border>
<!-- Icon (list-settings → Icon tab; "Folder" when unset) -->
<Border Grid.Column="0" Width="12"
IsVisible="{Binding FolderId, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Panel Grid.Column="1" Classes="list-glyph"
Classes.nested="{Binding FolderId, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<PathIcon Classes="list-icon"
Width="14" Height="14" IsVisible="{Binding !IsDot}"
Data="{Binding IconKey, Converter={StaticResource IconKey}}"
VerticalAlignment="Center"/>
<Ellipse Width="8" Height="8" IsVisible="{Binding IsDot}"
Fill="{Binding DotColor, Converter={x:Static converters:ColorBrushConverter.Instance}, ConverterParameter=MossBrush}"
VerticalAlignment="Center"/>
</Panel>
<!-- Name -->
<TextBlock Grid.Column="2" Classes="list-label"
Text="{Binding Name}" ToolTip.Tip="{Binding Name}"
VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
<!-- Count -->
<TextBlock Grid.Column="3" Classes="list-count"
Text="{Binding Count}" IsVisible="{Binding HasCount}"/>
</Grid>
</Border>
<!-- Below-row drop indicator (last item only) -->
<Border Grid.Row="2" Height="2" VerticalAlignment="Center" Margin="4,0"
Background="{DynamicResource MossBrush}" CornerRadius="1"
IsVisible="{Binding DropHintBelow}"/>
</Grid>
</DataTemplate>
<!-- One folder: header (never selectable — tapping folds it) plus its child lists. The two
drop indicators sit around the whole block, so "below this folder" doesn't draw a line
between the header and its children. -->
<DataTemplate DataType="vm:ListFolderNavItemViewModel">
<Grid RowDefinitions="Auto,Auto,Auto">
<Border Grid.Row="0" Height="2" VerticalAlignment="Center" Margin="4,0"
Background="{DynamicResource MossBrush}" CornerRadius="1"
IsVisible="{Binding DropHintAbove}"/>
<StackPanel Grid.Row="1">
<Border Classes="list-item tree-row"
Classes.drop-target="{Binding IsDropTarget}"
Tapped="OnFolderTapped"
DragDrop.AllowDrop="True"
DragDrop.DragOver="OnFolderDragOver"
DragDrop.Drop="OnFolderDrop">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="{loc:Tr lists.contextFolderSettings}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenFolderSettingsCommand}"
CommandParameter="{Binding}"/>
</ContextMenu>
</Border.ContextMenu>
<Grid ColumnDefinitions="12,20,*,Auto">
<PathIcon Grid.Column="0" Classes="list-icon" Foreground="{Binding Color, Converter={x:Static converters:ColorBrushConverter.Instance}, ConverterParameter=TextMuteBrush}"
Width="9" Height="9"
Data="{Binding Source='mdi:ChevronRight', Converter={StaticResource IconKey}}"
IsVisible="{Binding IsCollapsed}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<PathIcon Grid.Column="0" Classes="list-icon" Foreground="{Binding Color, Converter={x:Static converters:ColorBrushConverter.Instance}, ConverterParameter=TextMuteBrush}"
Width="9" Height="9"
Data="{Binding Source='mdi:ChevronDown', Converter={StaticResource IconKey}}"
IsVisible="{Binding !IsCollapsed}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<PathIcon Grid.Column="1" Classes="list-icon" Foreground="{Binding Color, Converter={x:Static converters:ColorBrushConverter.Instance}, ConverterParameter=TextMuteBrush}"
Width="14" Height="14"
Data="{Binding IconKey, Converter={StaticResource IconKey}}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Classes="list-label"
Text="{Binding Name}" ToolTip.Tip="{Binding Name}"
VerticalAlignment="Center" Margin="8,0" FontWeight="SemiBold"
Foreground="{Binding Color, Converter={x:Static converters:ColorBrushConverter.Instance}, ConverterParameter=TextDimBrush}"
FontSize="{StaticResource FontSizeBody}"/>
<TextBlock Grid.Column="3" Classes="list-count"
Text="{Binding Count}" IsVisible="{Binding ShowCount}"/>
</Grid>
</Border>
<!-- Guide line drops from under the chevron's centre (tree-row padding 4 + half the 9px
glyph); children start past it so their highlight never touches the line. -->
<ctl:ExpandPanel IsExpanded="{Binding !IsCollapsed}">
<Panel>
<Border Width="1" HorizontalAlignment="Left" Margin="8,0,0,4"
Background="{Binding Color, Converter={x:Static converters:ColorBrushConverter.Instance}, ConverterParameter=LineBrush}"
IsHitTestVisible="False"/>
<ItemsControl ItemsSource="{Binding Children}" Margin="14,0,0,0"/>
</Panel>
</ctl:ExpandPanel>
</StackPanel>
<Border Grid.Row="2" Height="2" VerticalAlignment="Center" Margin="4,0"
Background="{DynamicResource MossBrush}" CornerRadius="1"
IsVisible="{Binding DropHintBelow}"/>
</Grid>
</DataTemplate>
</UserControl.DataTemplates>
<DockPanel LastChildFill="True">
<!-- ── Header ── -->
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions><RowDefinition Height="Auto" SharedSizeGroup="IslandHeader"/></Grid.RowDefinitions>
<Border Classes="island-header">
<StackPanel Spacing="4">
<TextBlock Classes="heading" Text="{loc:Tr lists.heading}"/>
<!-- Search row -->
<Border Classes="search-wrap" Margin="0,8,0,12">
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<PathIcon Grid.Column="0" Width="14" Height="14"
Data="{Binding Source='mdi:Magnify', Converter={StaticResource IconKey}}"
Foreground="{DynamicResource TextFaintBrush}"
Margin="2,0,0,0"/>
<TextBox Grid.Column="1" x:Name="SearchBox" Classes="search-inner"
PlaceholderText="{loc:Tr lists.searchPlaceholder}"
Text="{Binding SearchText, Mode=TwoWay}"/>
<Border Grid.Column="2" Classes="kbd" Margin="0,0,2,0">
<TextBlock Text="{loc:Tr lists.searchKbd}"/>
</Border>
</Grid>
</Border>
</StackPanel>
</Border>
</Grid>
<!-- ── No-repo banner — visible until any User list has a linked working dir; no
dismiss, no persisted state, comes back if the last repo link is removed. ── -->
<Border DockPanel.Dock="Top"
IsVisible="{Binding HasNoLinkedRepo}"
Margin="12,0,12,12"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="14,10">
<StackPanel Spacing="8">
<TextBlock Classes="body" Text="{loc:Tr lists.noRepoBannerText}" TextWrapping="Wrap"/>
<Button Classes="btn" HorizontalAlignment="Right"
Content="{loc:Tr lists.noRepoBannerButton}"
Command="{Binding OpenRepoImportCommand}"/>
</StackPanel>
</Border>
<!-- ── Footer ── -->
<Border DockPanel.Dock="Bottom"
BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0"
Padding="12,10">
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<!-- Avatar circle -->
<Border Grid.Column="0" Classes="avatar-circle"
VerticalAlignment="Center">
<TextBlock Classes="eyebrow"
Text="{Binding UserInitials}"
FontWeight="SemiBold"
Foreground="{DynamicResource DeepBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<!-- Name + machine -->
<StackPanel Grid.Column="1" Margin="8,0" Spacing="1" VerticalAlignment="Center">
<TextBlock Classes="title" Text="{Binding UserName}"/>
<TextBlock Classes="meta" Text="{Binding MachineNameLocal}"/>
</StackPanel>
<!-- Settings button -->
<Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center"
Command="{Binding OpenSettingsCommand}"
ToolTip.Tip="{loc:Tr lists.settingsTip}">
<PathIcon Data="{Binding Source='mdi:Cog', Converter={StaticResource IconKey}}" Width="15" Height="15"/>
</Button>
</Grid>
</Border>
<!-- ── Scrollable body ── -->
<ScrollViewer x:Name="BodyScroll" DragDrop.AllowDrop="True">
<StackPanel Margin="6,0,6,4">
<!-- SMART LISTS section -->
<TextBlock Classes="section-label" Text="{loc:Tr lists.smartListsLabel}" Margin="10,10,10,4"/>
<ItemsControl ItemsSource="{Binding SmartLists}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ListNavItemViewModel">
<Border Classes="list-item" Classes.active="{Binding IsActive}"
Tapped="OnItemTapped">
<Grid ColumnDefinitions="20,*,Auto">
<!-- Left accent bar for active state -->
<Border Grid.Column="0" Grid.ColumnSpan="3"
Background="Transparent"
CornerRadius="8" IsHitTestVisible="False"
IsVisible="{Binding IsActive}">
<Border Width="2" Height="16"
Background="{DynamicResource AccentBrush}"
CornerRadius="1"
HorizontalAlignment="Left" VerticalAlignment="Center"
Margin="-8,0,0,0"/>
</Border>
<!-- Icon -->
<PathIcon Grid.Column="0" Classes="list-icon"
Width="14" Height="14"
Data="{Binding IconKey, Converter={StaticResource IconKey}}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Name -->
<TextBlock Grid.Column="1" Classes="list-label"
Text="{Binding Name}"
VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
<!-- Count -->
<TextBlock Grid.Column="2" Classes="list-count"
Text="{Binding Count}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- MY LISTS section — add-list + repo-import sit next to the label -->
<Grid ColumnDefinitions="*,Auto,Auto" Margin="10,10,6,4" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Classes="section-label"
Text="{loc:Tr lists.myListsLabel}" VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn"
Command="{Binding CreateListCommand}"
ToolTip.Tip="{loc:Tr lists.newList}">
<PathIcon Data="{Binding Source='mdi:Plus', Converter={StaticResource IconKey}}" Width="13" Height="13"/>
</Button>
<Button Grid.Column="2" Classes="icon-btn"
Command="{Binding OpenRepoImportCommand}"
ToolTip.Tip="{loc:Tr lists.addReposTip}">
<PathIcon Data="{Binding Source='mdi:FolderOutline', Converter={StaticResource IconKey}}" Width="14" Height="14"/>
</Button>
</Grid>
<!-- Folders and folder-less lists in one sequence, so either can be dragged above the
other; each item picks its template from UserControl.DataTemplates by type. -->
<ItemsControl ItemsSource="{Binding TopLevel}"/>
</StackPanel>
</ScrollViewer>
</DockPanel>
</UserControl>