feat(ui): Empty States für Detail-Pane und Task-Liste (UX-Audit #4)
Zentrierter, gedimmter Platzhalter (Mission-Control-Muster) im Detail-Pane bei fehlender Selektion (nicht in Notes-/Prep-Modus) und in der Task-Liste bei 0 sichtbaren Tasks; User-Listen ohne WorkingDir bekommen zusätzlich den Repo-Verknüpfungs-Hinweis.
This commit is contained in:
@@ -44,8 +44,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
|
||||
public bool IsTaskDetailVisible => !IsNotesMode && !IsPrepMode;
|
||||
|
||||
partial void OnIsNotesModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||
partial void OnIsPrepModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||
/// <summary>Centered placeholder shown when no task is selected — hidden in Notes/Prep mode,
|
||||
/// where <see cref="IsTaskDetailVisible"/> is already false.</summary>
|
||||
public bool IsEmptyStateVisible => Task is null && IsTaskDetailVisible;
|
||||
|
||||
partial void OnIsNotesModeChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||
OnPropertyChanged(nameof(IsEmptyStateVisible));
|
||||
}
|
||||
|
||||
partial void OnIsPrepModeChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||
OnPropertyChanged(nameof(IsEmptyStateVisible));
|
||||
}
|
||||
|
||||
public NotesEditorViewModel Notes { get; private set; } = null!;
|
||||
|
||||
@@ -55,6 +68,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
[NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
|
||||
[NotifyPropertyChangedFor(nameof(TaskIdBadge))]
|
||||
[NotifyPropertyChangedFor(nameof(IsEmptyStateVisible))]
|
||||
private TaskRowViewModel? _task;
|
||||
|
||||
// Editable fields
|
||||
|
||||
@@ -104,16 +104,38 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
[ObservableProperty] private string _statusPill = "";
|
||||
[ObservableProperty] private bool _hasStatusPill;
|
||||
[ObservableProperty] private bool _isShowingCompleted = true;
|
||||
[ObservableProperty] private bool _hasOverdue;
|
||||
[ObservableProperty] private bool _hasOpen;
|
||||
[ObservableProperty] private bool _hasCompleted;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsTasksEmptyHintVisible))]
|
||||
[NotifyPropertyChangedFor(nameof(IsTasksEmptyRepoHintVisible))]
|
||||
private bool _hasOverdue;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsTasksEmptyHintVisible))]
|
||||
[NotifyPropertyChangedFor(nameof(IsTasksEmptyRepoHintVisible))]
|
||||
private bool _hasOpen;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsTasksEmptyHintVisible))]
|
||||
[NotifyPropertyChangedFor(nameof(IsTasksEmptyRepoHintVisible))]
|
||||
private bool _hasCompleted;
|
||||
[ObservableProperty] private bool _showOpenLabel;
|
||||
[ObservableProperty] private string _completedHeader = "";
|
||||
[ObservableProperty] private bool _showNotesRow;
|
||||
[ObservableProperty] private bool _isMyDayList;
|
||||
[ObservableProperty] private bool _isLetClaudeVisible;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsTasksEmptyRepoHintVisible))]
|
||||
private bool _isLetClaudeVisible;
|
||||
[ObservableProperty] private bool _isQuickClaudeVisible;
|
||||
|
||||
/// <summary>No visible tasks below the add-task row — every item lands in one of
|
||||
/// Overdue/Open/Completed, so all-false here always means the list has zero items.</summary>
|
||||
public bool IsTasksEmptyHintVisible => !HasOverdue && !HasOpen && !HasCompleted;
|
||||
|
||||
/// <summary>Extra empty-state line for a User list with no linked working dir, where the
|
||||
/// execution features (queue, Let Claude handle it, Quick session) are unavailable. Derived
|
||||
/// separately from <see cref="IsLetClaudeVisible"/> so Smart/Virtual lists — which also have
|
||||
/// no working dir but aren't missing a repo link — don't pick it up.</summary>
|
||||
public bool IsTasksEmptyRepoHintVisible =>
|
||||
IsTasksEmptyHintVisible && _currentList?.Kind == ListKind.User && !IsLetClaudeVisible;
|
||||
|
||||
// Shared by QueuePlanningSubtasksAsync and FinalizePlanningSessionAsync — both are triggered
|
||||
// from a context menu that closes the instant a click lands, so there is no per-row surface
|
||||
// left standing to anchor an indicator to. The island header is the nearest surface still
|
||||
|
||||
@@ -146,6 +146,12 @@
|
||||
DragCompleted="OnSplitterDragCompleted"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Empty state: no task selected, outside Notes/Prep mode -->
|
||||
<TextBlock IsVisible="{Binding IsEmptyStateVisible}"
|
||||
Text="{loc:Tr details.emptyState}"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
|
||||
<!-- Notes mode -->
|
||||
<Panel IsVisible="{Binding IsNotesMode}">
|
||||
<islands:NotesEditorView DataContext="{Binding Notes}"/>
|
||||
|
||||
@@ -97,60 +97,76 @@
|
||||
Inset lives on the panel, NOT on the ListBox: Avalonia 12 leaves ScrollViewer.Padding
|
||||
out of the Extent, so at max scroll the last row would sit below the viewport and stay
|
||||
unreachable. -->
|
||||
<ListBox x:Name="RowsListBox"
|
||||
ItemsSource="{Binding Rows}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel Margin="10,4"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<!-- SelectedItem is never bound — selection is handled entirely by the inner row Button's
|
||||
SelectCommand, mirroring the pre-virtualization ItemsControl. Avalonia's ListBox has
|
||||
no "no selection" mode, so neutralize the app-wide selected/pointerover
|
||||
ContentPresenter overlay (App.axaml) instead of letting it double up with
|
||||
TaskRowView's own hover/selected style. -->
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="MinHeight" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Focusable" Value="{Binding Converter={x:Static uiconverters:NotHeaderRowConverter.Instance}}"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate DataType="vm:HeaderRow">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="14,14,14,6">
|
||||
<TextBlock Grid.Column="0" Classes="eyebrow section-label"
|
||||
Classes.overdue="{Binding IsOverdue}"
|
||||
Text="{Binding Label}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Classes="icon-btn" IsVisible="{Binding HasAction}"
|
||||
Command="{Binding ActionCommand}"
|
||||
ToolTip.Tip="{loc:Tr tasks.clearCompletedTip}"
|
||||
VerticalAlignment="Center">
|
||||
<PathIcon Data="{StaticResource Icon.Trash}" Width="13" Height="13"
|
||||
Foreground="{DynamicResource BloodBrush}"/>
|
||||
<Panel>
|
||||
<ListBox x:Name="RowsListBox"
|
||||
ItemsSource="{Binding Rows}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel Margin="10,4"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<!-- SelectedItem is never bound — selection is handled entirely by the inner row Button's
|
||||
SelectCommand, mirroring the pre-virtualization ItemsControl. Avalonia's ListBox has
|
||||
no "no selection" mode, so neutralize the app-wide selected/pointerover
|
||||
ContentPresenter overlay (App.axaml) instead of letting it double up with
|
||||
TaskRowView's own hover/selected style. -->
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="MinHeight" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Focusable" Value="{Binding Converter={x:Static uiconverters:NotHeaderRowConverter.Instance}}"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate DataType="vm:HeaderRow">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="14,14,14,6">
|
||||
<TextBlock Grid.Column="0" Classes="eyebrow section-label"
|
||||
Classes.overdue="{Binding IsOverdue}"
|
||||
Text="{Binding Label}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Classes="icon-btn" IsVisible="{Binding HasAction}"
|
||||
Command="{Binding ActionCommand}"
|
||||
ToolTip.Tip="{loc:Tr tasks.clearCompletedTip}"
|
||||
VerticalAlignment="Center">
|
||||
<PathIcon Data="{StaticResource Icon.Trash}" Width="13" Height="13"
|
||||
Foreground="{DynamicResource BloodBrush}"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vm:TaskRowViewModel">
|
||||
<Button Classes="flat" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Command="{Binding $parent[ListBox].((vm:TasksIslandViewModel)DataContext).SelectCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<islands:TaskRowView/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vm:TaskRowViewModel">
|
||||
<Button Classes="flat" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Command="{Binding $parent[ListBox].((vm:TasksIslandViewModel)DataContext).SelectCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<islands:TaskRowView/>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
|
||||
<!-- Empty state: shown below the add-task row once the list has zero visible tasks. -->
|
||||
<StackPanel IsVisible="{Binding IsTasksEmptyHintVisible}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
MaxWidth="260" Spacing="6" IsHitTestVisible="False">
|
||||
<TextBlock Text="{loc:Tr tasks.emptyHint}"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
TextAlignment="Center" TextWrapping="Wrap"/>
|
||||
<TextBlock IsVisible="{Binding IsTasksEmptyRepoHintVisible}"
|
||||
Classes="meta"
|
||||
Text="{loc:Tr tasks.emptyHintNoWorkingDir}"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
TextAlignment="Center" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user