feat(details): segmented Description/Steps/Files header

Replace the static DETAILS label and its dead space with a segment switcher; the card body now shows one section at a time. Step/file counts sit in the tab labels, the edit/preview toggle is scoped to Description, and drag-and-drop or add jumps to the Files tab. Tab labels localized (en/de).
This commit is contained in:
Mika Kuns
2026-06-23 08:34:03 +02:00
parent 637886f33a
commit 9301bbc81a
4 changed files with 171 additions and 130 deletions

View File

@@ -192,6 +192,11 @@
"overLimitError": "Konnte {0} nicht hinzufügen: {1}", "overLimitError": "Konnte {0} nicht hinzufügen: {1}",
"invalidNameError": "Konnte {0} nicht hinzufügen: {1}", "invalidNameError": "Konnte {0} nicht hinzufügen: {1}",
"selectIdleTask": "Zuerst eine inaktive Aufgabe auswählen" "selectIdleTask": "Zuerst eine inaktive Aufgabe auswählen"
},
"sections": {
"description": "Beschreibung",
"steps": "Schritte",
"files": "Dateien"
} }
}, },
"agent": { "agent": {

View File

@@ -192,6 +192,11 @@
"overLimitError": "Could not add {0}: {1}", "overLimitError": "Could not add {0}: {1}",
"invalidNameError": "Could not add {0}: {1}", "invalidNameError": "Could not add {0}: {1}",
"selectIdleTask": "Select an idle task first" "selectIdleTask": "Select an idle task first"
},
"sections": {
"description": "Description",
"steps": "Steps",
"files": "Files"
} }
}, },
"agent": { "agent": {

View File

@@ -117,23 +117,30 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
[RelayCommand] [RelayCommand]
private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded; private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded;
[ObservableProperty] private bool _isStepsExpanded; // Which section of the details card is shown (header acts as a segment switcher).
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsDescriptionSection))]
[NotifyPropertyChangedFor(nameof(IsStepsSection))]
[NotifyPropertyChangedFor(nameof(IsFilesSection))]
private string _detailSection = "description";
public bool IsDescriptionSection => DetailSection == "description";
public bool IsStepsSection => DetailSection == "steps";
public bool IsFilesSection => DetailSection == "files";
[RelayCommand] [RelayCommand]
private void ToggleStepsExpanded() => IsStepsExpanded = !IsStepsExpanded; private void SelectDetailSection(string? section) => DetailSection = section ?? "description";
public int TotalStepCount => Subtasks.Count; public int TotalStepCount => Subtasks.Count;
public int OpenStepCount => Subtasks.Count(s => !s.Done); public int DoneStepCount => Subtasks.Count(s => s.Done);
public string StepsSummary => public string StepsBadge => TotalStepCount > 0 ? $"{DoneStepCount}/{TotalStepCount}" : "";
TotalStepCount == 0 ? "no steps yet" public string FilesBadge => Attachments.Count > 0 ? Attachments.Count.ToString() : "";
: OpenStepCount == 0 ? $"all done · {TotalStepCount} total"
: $"{OpenStepCount} open · {TotalStepCount} total";
private void NotifyStepsChanged() private void NotifyStepsChanged()
{ {
OnPropertyChanged(nameof(TotalStepCount)); OnPropertyChanged(nameof(TotalStepCount));
OnPropertyChanged(nameof(OpenStepCount)); OnPropertyChanged(nameof(DoneStepCount));
OnPropertyChanged(nameof(StepsSummary)); OnPropertyChanged(nameof(StepsBadge));
OnPropertyChanged(nameof(ComposedPreview)); OnPropertyChanged(nameof(ComposedPreview));
} }
@@ -425,6 +432,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
Notes = new NotesEditorViewModel(_notesApi); Notes = new NotesEditorViewModel(_notesApi);
Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged(); Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged();
Subtasks.CollectionChanged += (_, _) => Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count); Subtasks.CollectionChanged += (_, _) => Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
Attachments.CollectionChanged += (_, _) => OnPropertyChanged(nameof(FilesBadge));
AgentSettings.PropertyChanged += (_, e) => AgentSettings.PropertyChanged += (_, e) =>
{ {
@@ -1233,6 +1241,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
public async System.Threading.Tasks.Task AddFilesAsync(IReadOnlyList<(string FileName, Stream Content)> files) public async System.Threading.Tasks.Task AddFilesAsync(IReadOnlyList<(string FileName, Stream Content)> files)
{ {
DetailSection = "files";
if (Task is null || Task.IsRunning) if (Task is null || Task.IsRunning)
{ {
DropStatus = Loc.T("details.attachments.selectIdleTask"); DropStatus = Loc.T("details.attachments.selectIdleTask");

View File

@@ -6,17 +6,70 @@
x:Class="ClaudeDo.Ui.Views.Islands.Detail.DescriptionStepsCard" x:Class="ClaudeDo.Ui.Views.Islands.Detail.DescriptionStepsCard"
x:DataType="vm:DetailsIslandViewModel"> x:DataType="vm:DetailsIslandViewModel">
<UserControl.Styles>
<!-- Segment switcher in the card header (mirrors the WorkConsole tab look) -->
<Style Selector="Button.seg-btn">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="8,3" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Button.seg-btn:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
<Setter Property="TextElement.Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
<Style Selector="Button.seg-btn.active /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Surface3Brush}" />
<Setter Property="TextElement.Foreground" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="TextBlock.seg-count">
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
</Style>
</UserControl.Styles>
<Border Classes="island" <Border Classes="island"
Background="{DynamicResource Surface2Brush}" Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource LineBrush}"> BorderBrush="{DynamicResource LineBrush}">
<DockPanel> <DockPanel>
<!-- Header: DETAILS · copy · preview/edit --> <!-- Header: segment switcher (Description · Steps · Files) + copy + edit -->
<Border DockPanel.Dock="Top" Classes="island-header"> <Border DockPanel.Dock="Top" Classes="island-header">
<Grid ColumnDefinitions="Auto,*,Auto,Auto" VerticalAlignment="Center"> <Grid ColumnDefinitions="Auto,*,Auto,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Classes="section-label" Text="DETAILS" <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="2">
VerticalAlignment="Center"/> <Button Classes="seg-btn"
Classes.active="{Binding IsDescriptionSection}"
Command="{Binding SelectDetailSectionCommand}"
CommandParameter="description"
Content="{loc:Tr details.sections.description}"/>
<Button Classes="seg-btn"
Classes.active="{Binding IsStepsSection}"
Command="{Binding SelectDetailSectionCommand}"
CommandParameter="steps">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="{loc:Tr details.sections.steps}" VerticalAlignment="Center"/>
<TextBlock Classes="seg-count" Text="{Binding StepsBadge}"
VerticalAlignment="Center"
IsVisible="{Binding StepsBadge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</Button>
<Button Classes="seg-btn"
Classes.active="{Binding IsFilesSection}"
Command="{Binding SelectDetailSectionCommand}"
CommandParameter="files">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="{loc:Tr details.sections.files}" VerticalAlignment="Center"/>
<TextBlock Classes="seg-count" Text="{Binding FilesBadge}"
VerticalAlignment="Center"
IsVisible="{Binding FilesBadge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</Button>
</StackPanel>
<!-- Copy formatted --> <!-- Copy formatted -->
<Button Grid.Column="2" <Button Grid.Column="2"
@@ -27,10 +80,11 @@
<PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/> <PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/>
</Button> </Button>
<!-- Preview/Edit toggle --> <!-- Preview/Edit toggle (Description section only) -->
<Button Grid.Column="3" <Button Grid.Column="3"
Classes="btn" Classes="btn"
Padding="8,3" Padding="8,3"
IsVisible="{Binding IsDescriptionSection}"
ToolTip.Tip="{loc:Tr details.toggleEditPreviewTip}" ToolTip.Tip="{loc:Tr details.toggleEditPreviewTip}"
Command="{Binding ToggleEditDescriptionCommand}"> Command="{Binding ToggleEditDescriptionCommand}">
<Panel> <Panel>
@@ -42,132 +96,101 @@
</Grid> </Grid>
</Border> </Border>
<!-- Body (scrolls inside the card so the card fills its row to the divider) --> <!-- Body: only the active section is shown -->
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="14" Spacing="10"> <Panel Margin="14">
<!-- Description (always visible) --> <!-- Description -->
<Panel> <Panel IsVisible="{Binding IsDescriptionSection}">
<!-- Edit mode: raw TextBox --> <!-- Edit mode: raw TextBox -->
<TextBox Text="{Binding EditableDescription, Mode=TwoWay}" <TextBox Text="{Binding EditableDescription, Mode=TwoWay}"
AcceptsReturn="True" AcceptsReturn="True"
TextWrapping="Wrap" TextWrapping="Wrap"
MinHeight="80" MinHeight="80"
MaxHeight="320" MaxHeight="320"
Padding="8" Padding="8"
FontFamily="{DynamicResource MonoFont}" FontFamily="{DynamicResource MonoFont}"
FontSize="{StaticResource FontSizeBody}" FontSize="{StaticResource FontSizeBody}"
Background="{DynamicResource Surface3Brush}" Background="{DynamicResource Surface3Brush}"
BorderBrush="{DynamicResource LineBrush}" BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="8" CornerRadius="8"
IsVisible="{Binding IsEditingDescription}"/> IsVisible="{Binding IsEditingDescription}"/>
<!-- Preview mode: rendered composed text (title + description + open steps) --> <!-- Preview mode: rendered composed text (title + description + open steps) -->
<ctl:MarkdownView Markdown="{Binding ComposedPreview}" <ctl:MarkdownView Markdown="{Binding ComposedPreview}"
IsVisible="{Binding !IsEditingDescription}"/> IsVisible="{Binding !IsEditingDescription}"/>
</Panel> </Panel>
<!-- Steps: always-visible summary strip; expand to manage --> <!-- Steps -->
<Border BorderBrush="{DynamicResource LineBrush}" <StackPanel IsVisible="{Binding IsStepsSection}" Spacing="6">
BorderThickness="0,1,0,0" <TextBox Text="{Binding NewSubtaskTitle, Mode=TwoWay}"
Padding="0,8,0,0"> PlaceholderText="Add step…"
<StackPanel Spacing="6"> Padding="8"
Background="{DynamicResource Surface3Brush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1"
CornerRadius="8">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddSubtaskCommand}"/>
</TextBox.KeyBindings>
</TextBox>
<!-- Summary header (click to expand/collapse) --> <!-- Subtask rows -->
<Button Classes="flat" Cursor="Hand" <ItemsControl ItemsSource="{Binding Subtasks}">
HorizontalAlignment="Stretch" <ItemsControl.ItemTemplate>
HorizontalContentAlignment="Stretch" <DataTemplate DataType="vm:SubtaskRowViewModel">
Command="{Binding ToggleStepsExpandedCommand}"> <Border Classes="subtask-row" Classes.done="{Binding Done}">
<Grid ColumnDefinitions="Auto,Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*">
<Panel Grid.Column="0" Width="12" Margin="0,0,6,0" VerticalAlignment="Center">
<TextBlock Classes="meta" Text="▸" IsVisible="{Binding !IsStepsExpanded}"/>
<TextBlock Classes="meta" Text="▾" IsVisible="{Binding IsStepsExpanded}"/>
</Panel>
<TextBlock Grid.Column="1" Classes="section-label" Text="STEPS"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="3" Classes="meta" Text="{Binding StepsSummary}"
Foreground="{DynamicResource TextMuteBrush}"
VerticalAlignment="Center"/>
</Grid>
</Button>
<!-- Expanded: add-step input + step rows --> <!-- Check circle -->
<StackPanel IsVisible="{Binding IsStepsExpanded}" Spacing="6"> <Button Grid.Column="0"
<TextBox Text="{Binding NewSubtaskTitle, Mode=TwoWay}" Classes="flat"
PlaceholderText="Add step…" Padding="0"
Padding="8" Margin="0,0,8,0"
Background="{DynamicResource Surface3Brush}" VerticalAlignment="Center"
BorderBrush="{DynamicResource LineBrush}" Command="{Binding $parent[ItemsControl].((vm:DetailsIslandViewModel)DataContext).ToggleSubtaskDoneCommand}"
BorderThickness="1" CommandParameter="{Binding}">
CornerRadius="8"> <Ellipse Classes="task-check"
<TextBox.KeyBindings> Classes.done="{Binding Done}"
<KeyBinding Gesture="Enter" Command="{Binding AddSubtaskCommand}"/> Width="16" Height="16"
</TextBox.KeyBindings> Cursor="Hand"/>
</TextBox> </Button>
<!-- Subtask rows --> <!-- Title / edit -->
<ItemsControl ItemsSource="{Binding Subtasks}"> <Panel Grid.Column="1" VerticalAlignment="Center">
<ItemsControl.ItemTemplate> <TextBlock Classes="subtask-title"
<DataTemplate DataType="vm:SubtaskRowViewModel"> Text="{Binding Title}"
<Border Classes="subtask-row" Classes.done="{Binding Done}"> IsVisible="{Binding !IsEditing}"
<Grid ColumnDefinitions="Auto,*"> FontSize="{StaticResource FontSizeBody}"
Foreground="{DynamicResource TextDimBrush}"
<!-- Check circle --> VerticalAlignment="Center"
<Button Grid.Column="0" TextWrapping="Wrap"
Classes="flat" Cursor="Ibeam"
Padding="0" Tapped="OnSubtaskTitleTapped"/>
Margin="0,0,8,0" <TextBox Classes="subtask-edit"
VerticalAlignment="Center" Text="{Binding Title, Mode=TwoWay}"
Command="{Binding $parent[ItemsControl].((vm:DetailsIslandViewModel)DataContext).ToggleSubtaskDoneCommand}" IsVisible="{Binding IsEditing}"
CommandParameter="{Binding}">
<Ellipse Classes="task-check"
Classes.done="{Binding Done}"
Width="16" Height="16"
Cursor="Hand"/>
</Button>
<!-- Title / edit -->
<Panel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Classes="subtask-title"
Text="{Binding Title}"
IsVisible="{Binding !IsEditing}"
FontSize="{StaticResource FontSizeBody}" FontSize="{StaticResource FontSizeBody}"
Foreground="{DynamicResource TextDimBrush}" AcceptsReturn="False"
VerticalAlignment="Center"
TextWrapping="Wrap" TextWrapping="Wrap"
Cursor="Ibeam" LostFocus="OnSubtaskEditLostFocus">
Tapped="OnSubtaskTitleTapped"/> <TextBox.KeyBindings>
<TextBox Classes="subtask-edit" <KeyBinding Gesture="Enter"
Text="{Binding Title, Mode=TwoWay}" Command="{Binding $parent[ItemsControl].((vm:DetailsIslandViewModel)DataContext).CommitSubtaskEditCommand}"
IsVisible="{Binding IsEditing}" CommandParameter="{Binding}"/>
FontSize="{StaticResource FontSizeBody}" </TextBox.KeyBindings>
AcceptsReturn="False" </TextBox>
TextWrapping="Wrap" </Panel>
LostFocus="OnSubtaskEditLostFocus">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter"
Command="{Binding $parent[ItemsControl].((vm:DetailsIslandViewModel)DataContext).CommitSubtaskEditCommand}"
CommandParameter="{Binding}"/>
</TextBox.KeyBindings>
</TextBox>
</Panel>
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</StackPanel>
</StackPanel> </StackPanel>
</Border>
<!-- Attachments section -->
<Border BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0"
Padding="0,8,0,0">
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="{loc:Tr details.attachments.sectionLabel}"/>
<!-- Files -->
<StackPanel IsVisible="{Binding IsFilesSection}" Spacing="6">
<!-- Attachment rows --> <!-- Attachment rows -->
<ItemsControl ItemsSource="{Binding Attachments}"> <ItemsControl ItemsSource="{Binding Attachments}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
@@ -211,9 +234,8 @@
Foreground="{DynamicResource TextMuteBrush}" Foreground="{DynamicResource TextMuteBrush}"
TextWrapping="Wrap"/> TextWrapping="Wrap"/>
</StackPanel> </StackPanel>
</Border>
</StackPanel> </Panel>
</ScrollViewer> </ScrollViewer>
</DockPanel> </DockPanel>
</Border> </Border>