style(ui): Material Design icons for the tasks island header bar

This commit is contained in:
mika kuns
2026-10-01 16:17:30 +02:00
parent 34c71bbe02
commit cbab4da54c
3 changed files with 8 additions and 16 deletions
+1 -1
View File
@@ -130,7 +130,7 @@ snaps `CanResize="True"` windows, which is the opt-in), and it insets itself by
## Gotchas ## Gotchas
- **Never set `Foreground`/`Stroke` on an icon inline.** A local value outranks every style, so the button's hover/checked/disabled states can't reach the icon and it looks dead on hover — this is what broke the icons that were "fixed" to the right colour inline. Colour is picked with a class; the single `ICON PAINTING` block in `IslandStyles.axaml` owns all states (`.danger`, `.star-btn.on`, `.active`, `:checked`, `:disabled`). Avalonia has no specificity — that block's **declaration order** is the mechanism, so add new variants in the right section, not at the end. - **Never set `Foreground`/`Stroke` on an icon inline.** A local value outranks every style, so the button's hover/checked/disabled states can't reach the icon and it looks dead on hover — this is what broke the icons that were "fixed" to the right colour inline. Colour is picked with a class; the single `ICON PAINTING` block in `IslandStyles.axaml` owns all states (`.danger`, `.star-btn.on`, `.active`, `:checked`, `:disabled`). Avalonia has no specificity — that block's **declaration order** is the mechanism, so add new variants in the right section, not at the end.
- **`PathIcon` *fills* its geometry.** Line-art/stroke icons must be authored as filled geometry or rendered as `<Path Classes="icon-stroke">` inside a `Viewbox` (e.g. `Icon.PlanDay`, `Icon.GitBranch`). A pure stroke path in a `PathIcon` is **invisible**. `Path.icon-stroke` takes the same colour classes/states as `PathIcon`. - **`PathIcon` *fills* its geometry.** Line-art/stroke icons must be authored as filled geometry or rendered as `<Path Classes="icon-stroke">` inside a `Viewbox` (e.g. `Icon.Refine`, `Icon.GitBranch`). For new icons prefer Material Design Icons (`mdi:Name` via the `IconKey` converter) — they are filled and work in a plain `PathIcon`. A pure stroke path in a `PathIcon` is **invisible**. `Path.icon-stroke` takes the same colour classes/states as `PathIcon`.
- **`NumericUpDown.Value` is `decimal?` and goes null while the box is empty** — i.e. every time the user clears a value to type a new one. Bound TwoWay to a non-nullable `int`/`decimal`, that null throws `InvalidCastException`. Either bind a `decimal?` property (as `AgentConfigEditorViewModel.MaxTurns` does) or add `Converter={StaticResource KeepLastNumber}`, which drops the null via `BindingOperations.DoNothing`. - **`NumericUpDown.Value` is `decimal?` and goes null while the box is empty** — i.e. every time the user clears a value to type a new one. Bound TwoWay to a non-nullable `int`/`decimal`, that null throws `InvalidCastException`. Either bind a `decimal?` property (as `AgentConfigEditorViewModel.MaxTurns` does) or add `Converter={StaticResource KeepLastNumber}`, which drops the null via `BindingOperations.DoNothing`.
- **`ScrollViewer.Padding` is excluded from the Extent (Avalonia 12).** At max scroll the content still sticks out below the viewport by the padding height, so the last line(s) are clipped and unreachable — even after `ScrollToEnd()`. Put the inset on the *content* (`Margin` on the inner `ItemsControl`/`StackPanel`) instead. Verified headlessly: 12,8,12,4 on the viewer → last item 20px below the viewport; same inset as content margin → fully visible. **`ListBox` does *not* have this trap** — its Fluent control-theme template forwards `Padding` onto `PART_ItemsPresenter`'s margin (content side), not the raw `ScrollViewer.Padding`; verified headlessly that `ListBox.Padding="10,4"` and a `Margin="10,4"` on the `ItemsPanelTemplate` panel produce an *identical* Extent and leave the last row fully visible at max scroll. Still fine to put the inset on the panel for consistency with the other scrollables above, just don't assume it's fixing a real bug there. - **`ScrollViewer.Padding` is excluded from the Extent (Avalonia 12).** At max scroll the content still sticks out below the viewport by the padding height, so the last line(s) are clipped and unreachable — even after `ScrollToEnd()`. Put the inset on the *content* (`Margin` on the inner `ItemsControl`/`StackPanel`) instead. Verified headlessly: 12,8,12,4 on the viewer → last item 20px below the viewport; same inset as content margin → fully visible. **`ListBox` does *not* have this trap** — its Fluent control-theme template forwards `Padding` onto `PART_ItemsPresenter`'s margin (content side), not the raw `ScrollViewer.Padding`; verified headlessly that `ListBox.Padding="10,4"` and a `Margin="10,4"` on the `ItemsPanelTemplate` panel produce an *identical* Extent and leave the last row fully visible at max scroll. Still fine to put the inset on the panel for consistency with the other scrollables above, just don't assume it's fixing a real bug there.
- **Re-templating a `TabControl` must keep `PART_ItemsPresenter`.** `TabControl.SelectedContent` is resolved via `ContainerFromIndex`, and containers are only realized by the `ItemsPresenter`. A template that renders just `PART_SelectedContentHost` (to hide the tab strip) leaves `SelectedContent` permanently `null` → an empty body. Keep the presenter with `IsVisible="False"`; that still realizes containers (verified headlessly, Avalonia 12.0.4). This broke the whole Settings modal body in 56c3719e. - **Re-templating a `TabControl` must keep `PART_ItemsPresenter`.** `TabControl.SelectedContent` is resolved via `ContainerFromIndex`, and containers are only realized by the `ItemsPresenter`. A template that renders just `PART_SelectedContentHost` (to hide the tab strip) leaves `SelectedContent` permanently `null` → an empty body. Keep the presenter with `IsVisible="False"`; that still realizes containers (verified headlessly, Avalonia 12.0.4). This broke the whole Settings modal body in 56c3719e.
@@ -82,9 +82,6 @@
<!-- Icon.Broom (filled: handle + binding band + flared bristles) --> <!-- Icon.Broom (filled: handle + binding band + flared bristles) -->
<StreamGeometry x:Key="Icon.Broom">M11 3 H13 V10 H11 Z M8.5 10 H15.5 V12 H8.5 Z M9 12 H15 L17 21 H7 Z</StreamGeometry> <StreamGeometry x:Key="Icon.Broom">M11 3 H13 V10 H11 Z M8.5 10 H15.5 V12 H8.5 Z M9 12 H15 L17 21 H7 Z</StreamGeometry>
<!-- Icon.PlanDay (stroke-rendered via Path.icon-stroke — sun over horizon) -->
<StreamGeometry x:Key="Icon.PlanDay">M3,20 L21,20 M8.4,11 a3.6,3.6 0 1,0 7.2,0 a3.6,3.6 0 1,0 -7.2,0 M12,4.5 L12,3 M6,11 L4.5,11 M18,11 L19.5,11 M7.5,6.5 L6.4,5.4 M16.5,6.5 L17.6,5.4</StreamGeometry>
<!-- Icon.Refine (stroke-rendered via Path.icon-stroke — list lines + two sparkles) --> <!-- Icon.Refine (stroke-rendered via Path.icon-stroke — list lines + two sparkles) -->
<StreamGeometry x:Key="Icon.Refine">M3,6 L13,6 M3,11 L11,11 M3,16 L9,16 M18.5,3 L19.28,5.22 L21.5,6 L19.28,6.78 L18.5,9 L17.72,6.78 L15.5,6 L17.72,5.22 Z M19.5,14.9 L19.85,16.15 L21.1,16.5 L19.85,16.85 L19.5,18.1 L19.15,16.85 L17.9,16.5 L19.15,16.15 Z</StreamGeometry> <StreamGeometry x:Key="Icon.Refine">M3,6 L13,6 M3,11 L11,11 M3,16 L9,16 M18.5,3 L19.28,5.22 L21.5,6 L19.28,6.78 L18.5,9 L17.72,6.78 L15.5,6 L17.72,5.22 Z M19.5,14.9 L19.85,16.15 L21.1,16.5 L19.85,16.85 L19.5,18.1 L19.15,16.85 L17.9,16.5 L19.15,16.15 Z</StreamGeometry>
@@ -36,36 +36,31 @@
IsVisible="{Binding !IsNotesList}" IsVisible="{Binding !IsNotesList}"
Command="{Binding ToggleShowCompletedCommand}" Command="{Binding ToggleShowCompletedCommand}"
ToolTip.Tip="{loc:Tr tasks.showCompletedTip}"> ToolTip.Tip="{loc:Tr tasks.showCompletedTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Eye}"/> <PathIcon Width="16" Height="16" Data="{Binding Source='mdi:CheckAll', Converter={StaticResource IconKey}}"/>
</Button> </Button>
<Button Classes="icon-btn" IsVisible="{Binding IsLetClaudeVisible}" <Button Classes="icon-btn" IsVisible="{Binding IsLetClaudeVisible}"
Command="{Binding LetClaudeHandleCommand}" ToolTip.Tip="{loc:Tr tasks.letClaudeTip}"> Command="{Binding LetClaudeHandleCommand}" ToolTip.Tip="{loc:Tr tasks.letClaudeTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.AgentSuggested}"/> <PathIcon Width="16" Height="16" Data="{Binding Source='mdi:RobotOutline', Converter={StaticResource IconKey}}"/>
</Button> </Button>
<Button Classes="icon-btn" IsVisible="{Binding IsMergeReviewsVisible}" <Button Classes="icon-btn" IsVisible="{Binding IsMergeReviewsVisible}"
Command="{Binding MergeReviewsCommand}" ToolTip.Tip="{loc:Tr tasks.mergeReviewsTip}"> Command="{Binding MergeReviewsCommand}" ToolTip.Tip="{loc:Tr tasks.mergeReviewsTip}">
<!-- Icon.GitBranch is line-art: stroke-rendered, PathIcon would fill the lines away --> <PathIcon Width="16" Height="16" Data="{Binding Source='mdi:SourceMerge', Converter={StaticResource IconKey}}"/>
<Viewbox Width="16" Height="16">
<Path Classes="icon-stroke" Data="{StaticResource Icon.GitBranch}"/>
</Viewbox>
</Button> </Button>
<Button Classes="icon-btn" IsVisible="{Binding IsQuickClaudeVisible}" <Button Classes="icon-btn" IsVisible="{Binding IsQuickClaudeVisible}"
Command="{Binding OpenQuickClaudeSessionCommand}" ToolTip.Tip="{loc:Tr tasks.quickClaudeTip}"> Command="{Binding OpenQuickClaudeSessionCommand}" ToolTip.Tip="{loc:Tr tasks.quickClaudeTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.ArrowOut}"/> <PathIcon Width="16" Height="16" Data="{Binding Source='mdi:ConsoleLine', Converter={StaticResource IconKey}}"/>
</Button> </Button>
<Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}" <Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}"
Command="{Binding ClearDayCommand}" ToolTip.Tip="{loc:Tr tasks.clearDayTip}"> Command="{Binding ClearDayCommand}" ToolTip.Tip="{loc:Tr tasks.clearDayTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Broom}"/> <PathIcon Width="16" Height="16" Data="{Binding Source='mdi:Broom', Converter={StaticResource IconKey}}"/>
</Button> </Button>
<Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}" <Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}"
Command="{Binding ShowPrepLogCommand}" ToolTip.Tip="{loc:Tr tasks.planMyDayTip}"> Command="{Binding ShowPrepLogCommand}" ToolTip.Tip="{loc:Tr tasks.planMyDayTip}">
<Viewbox Width="18" Height="18"> <PathIcon Width="16" Height="16" Data="{Binding Source='mdi:WeatherSunsetUp', Converter={StaticResource IconKey}}"/>
<Path Classes="icon-stroke" Data="{StaticResource Icon.PlanDay}"/>
</Viewbox>
</Button> </Button>
<Button Classes="icon-btn" IsVisible="{Binding !IsNotesList}" <Button Classes="icon-btn" IsVisible="{Binding !IsNotesList}"
Command="{Binding OpenListSettingsCommand}" ToolTip.Tip="{loc:Tr tasks.listSettingsTip}"> Command="{Binding OpenListSettingsCommand}" ToolTip.Tip="{loc:Tr tasks.listSettingsTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Settings}"/> <PathIcon Width="16" Height="16" Data="{Binding Source='mdi:CogOutline', Converter={StaticResource IconKey}}"/>
</Button> </Button>
</StackPanel> </StackPanel>
</Grid> </Grid>