Scaffold AvParser: Avalonia 12 shell with adaptive layout

Greenfield skeleton for a parser desktop app. The domain is deliberately a
placeholder — IParser<TIn,TOut> plus two sample parsers — so the shell is
runnable and verifiable end to end before real logic lands.

Layers run one way: Core (no Avalonia, no IO) <- Infrastructure <- UI <- Desktop.
UI is a class library rather than the exe so headless tests build real views
without dragging in Program.cs, Serilog or the container.

Adaptive layout is built from what Avalonia actually offers, since it has no
AdaptiveTrigger or media queries: ResponsiveLayout observes Visual.Bounds and
projects a breakpoint onto both an attached property and :compact/:medium/
:expanded pseudoclasses, with 24px hysteresis so dragging a window edge cannot
make the layout flap. Pane state lives in the view model because a style setter
loses to a local value permanently; styles own only the visual variance.

Stack notes worth remembering: Avalonia.ReactiveUI is deprecated in favour of
ReactiveUI.Avalonia, and ReactiveUI 24 runs on the Primitives engine (RxVoid,
ISequencer, Signal<T>) and no longer self-initialises. Avalonia.Headless.XUnit
12.x requires xUnit v3. InvariantGlobalization must stay false or Semi.Avalonia
throws in its static constructor.

102 tests across three projects, including headless guards for the two failures
that are otherwise completely silent: a stylesheet whose selectors match nothing,
and a light palette too low-contrast for cards to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-08-13 16:07:08 +03:00
co-authored by Claude Opus 5
commit 3db9d4dfc6
94 changed files with 5966 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
<rxui:ReactiveUserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:rxui="clr-namespace:ReactiveUI.Avalonia;assembly=ReactiveUI.Avalonia"
xmlns:vm="clr-namespace:AvParser.UI.ViewModels"
xmlns:conv="clr-namespace:AvParser.UI.Converters"
xmlns:r="clr-namespace:AvParser.UI.Responsive"
x:TypeArguments="vm:ShellViewModel"
x:Class="AvParser.UI.Views.ShellView"
x:DataType="vm:ShellViewModel"
Classes="shell"
r:ResponsiveLayout.IsEnabled="True"
>
<SplitView x:Name="NavPane" DisplayMode="{Binding PaneDisplayMode}" IsPaneOpen="{Binding IsPaneOpen, Mode=TwoWay}">
<!-- ===== Navigation pane ===== -->
<SplitView.Pane>
<DockPanel LastChildFill="True">
<Border x:Name="PaneHeader" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="10">
<PathIcon
Classes="glyph"
Data="{DynamicResource IconSparkle}"
Foreground="{DynamicResource AppAccentBrush}"
VerticalAlignment="Center"
/>
<TextBlock x:Name="PaneTitle" Classes="subtitle" Text="AvParser" VerticalAlignment="Center" />
</StackPanel>
</Border>
<ListBox
x:Name="NavList"
Classes="nav"
ItemsSource="{Binding Pages}"
SelectedItem="{Binding SelectedPage, Mode=TwoWay}"
>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:PageViewModel">
<StackPanel Orientation="Horizontal" Spacing="12">
<PathIcon
Classes="glyph"
Data="{Binding IconKey, Converter={x:Static conv:AppConverters.IconKeyToGeometry}}"
VerticalAlignment="Center"
/>
<TextBlock Classes="navLabel" Text="{Binding Title}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</SplitView.Pane>
<!-- ===== Content ===== -->
<DockPanel LastChildFill="True">
<Border x:Name="TitleBar" DockPanel.Dock="Top">
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
<Button
x:Name="PaneToggle"
Grid.Column="0"
Classes="icon"
Command="{Binding TogglePaneCommand}"
ToolTip.Tip="Toggle navigation"
Margin="0,0,4,0"
>
<PathIcon Classes="glyph" Data="{DynamicResource IconMenu}" />
</Button>
<Button
x:Name="BackButton"
Grid.Column="1"
Classes="icon"
Command="{Binding GoBackCommand}"
IsVisible="{Binding CanGoBack}"
ToolTip.Tip="Back"
Margin="0,0,8,0"
>
<PathIcon Classes="glyph" Data="{DynamicResource IconBack}" />
</Button>
<TextBlock
x:Name="ShellTitle"
Grid.Column="2"
Classes="title"
Text="{Binding Title}"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
/>
<Button
x:Name="ThemeToggle"
Grid.Column="3"
Classes="icon"
Command="{Binding ToggleThemeCommand}"
ToolTip.Tip="Switch light / dark"
>
<PathIcon
Classes="glyph"
Data="{Binding ThemeIconKey, Converter={x:Static conv:AppConverters.IconKeyToGeometry}}"
/>
</Button>
</Grid>
</Border>
<Border x:Name="PageHost" Background="{DynamicResource AppSurfaceBrush}">
<TransitioningContentControl Content="{Binding CurrentPage}">
<TransitioningContentControl.PageTransition>
<CompositePageTransition>
<CrossFade Duration="0:0:0.18" />
<PageSlide Duration="0:0:0.18" Orientation="Horizontal" SlideInEasing="CubicEaseOut" />
</CompositePageTransition>
</TransitioningContentControl.PageTransition>
</TransitioningContentControl>
</Border>
</DockPanel>
</SplitView>
</rxui:ReactiveUserControl>