Restyle the Avalonia server host and add a single-file publish workflow (#18)

Give the host a cohesive vault-terminal look: header wordmark with a live
status pill that glows green while running, sectioned configuration card with
per-field labels, a terminal-style server log, richer player rows (id, name,
address, packet counts), and a bold Start/Stop control bar. All existing
bindings are unchanged.

Add publish-host.yml to build self-contained, single-file win-x64 and
linux-x64 binaries on the self-hosted runner and upload them as artifacts,
and document the untrimmed publish commands in the host README.
This commit is contained in:
Nomads_Reach
2026-08-16 18:47:14 -04:00
committed by GitHub
parent 2f6100907f
commit ea275210cd
4 changed files with 437 additions and 82 deletions
+51
View File
@@ -0,0 +1,51 @@
name: Publish Host (Avalonia)
# Produces self-contained, single-file Server Host binaries for Windows and
# Linux. No .NET runtime is required on the target machine. Runs on demand and
# on version tags; each build is uploaded as a workflow artifact.
on:
workflow_dispatch:
push:
tags:
- "host-v*"
jobs:
publish:
name: Publish ${{ matrix.rid }}
runs-on: [self-hosted, Linux, X64]
strategy:
fail-fast: false
matrix:
rid: [win-x64, linux-x64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
# The runner user cannot write to system /usr/share/dotnet; install the
# pinned SDK into a runner-writable, cached path instead.
env:
DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet
with:
dotnet-version: "8.0.x"
- name: Publish single-file self-contained
# Untrimmed on purpose: the host uses reflection-based Avalonia bindings,
# which the trimmer would strip.
run: >
dotnet publish host/CommonwealthOnline.Host.csproj
-c Release
-r ${{ matrix.rid }}
--self-contained true
-p:PublishSingleFile=true
-p:IncludeNativeLibrariesForSelfExtract=true
-p:DebugType=none
-o out/${{ matrix.rid }}
--nologo
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: CommonwealthOnline.Host-${{ matrix.rid }}
path: out/${{ matrix.rid }}/
if-no-files-found: error
+195 -7
View File
@@ -3,23 +3,211 @@
x:Class="CommonwealthOnline.Host.App" x:Class="CommonwealthOnline.Host.App"
RequestedThemeVariant="Dark"> RequestedThemeVariant="Dark">
<Application.Styles>
<FluentTheme />
</Application.Styles>
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<Color x:Key="CoBackground">#0E120B</Color>
<Color x:Key="CoSurface">#171C10</Color> <!-- ============ Palette ============ -->
<Color x:Key="CoBackground">#0A0E07</Color>
<Color x:Key="CoBackgroundTop">#12180C</Color>
<Color x:Key="CoSurface">#151B0F</Color>
<Color x:Key="CoSurface2">#1C2314</Color>
<Color x:Key="CoSurface3">#232C18</Color>
<Color x:Key="CoBorder">#2C3720</Color>
<Color x:Key="CoBorderBright">#43532F</Color>
<Color x:Key="CoAccent">#E6D28C</Color> <Color x:Key="CoAccent">#E6D28C</Color>
<Color x:Key="CoAccentBright">#F6ECBE</Color>
<Color x:Key="CoText">#D8D2BE</Color> <Color x:Key="CoText">#D8D2BE</Color>
<Color x:Key="CoDanger">#C24B4B</Color> <Color x:Key="CoMuted">#8A9068</Color>
<Color x:Key="CoGreen">#8BD05C</Color>
<Color x:Key="CoGreenDim">#3E5C28</Color>
<Color x:Key="CoDanger">#C85450</Color>
<Color x:Key="CoDangerDim">#5A2A28</Color>
<SolidColorBrush x:Key="CoBackgroundBrush" Color="{StaticResource CoBackground}" /> <SolidColorBrush x:Key="CoBackgroundBrush" Color="{StaticResource CoBackground}" />
<SolidColorBrush x:Key="CoSurfaceBrush" Color="{StaticResource CoSurface}" /> <SolidColorBrush x:Key="CoSurfaceBrush" Color="{StaticResource CoSurface}" />
<SolidColorBrush x:Key="CoSurface2Brush" Color="{StaticResource CoSurface2}" />
<SolidColorBrush x:Key="CoSurface3Brush" Color="{StaticResource CoSurface3}" />
<SolidColorBrush x:Key="CoBorderBrush" Color="{StaticResource CoBorder}" />
<SolidColorBrush x:Key="CoBorderBrightBrush" Color="{StaticResource CoBorderBright}" />
<SolidColorBrush x:Key="CoAccentBrush" Color="{StaticResource CoAccent}" /> <SolidColorBrush x:Key="CoAccentBrush" Color="{StaticResource CoAccent}" />
<SolidColorBrush x:Key="CoAccentBrightBrush" Color="{StaticResource CoAccentBright}" />
<SolidColorBrush x:Key="CoTextBrush" Color="{StaticResource CoText}" /> <SolidColorBrush x:Key="CoTextBrush" Color="{StaticResource CoText}" />
<SolidColorBrush x:Key="CoMutedBrush" Color="{StaticResource CoMuted}" />
<SolidColorBrush x:Key="CoGreenBrush" Color="{StaticResource CoGreen}" />
<SolidColorBrush x:Key="CoDangerBrush" Color="{StaticResource CoDanger}" /> <SolidColorBrush x:Key="CoDangerBrush" Color="{StaticResource CoDanger}" />
<!-- ============ Gradients ============ -->
<LinearGradientBrush x:Key="CoWindowBrush" StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="{StaticResource CoBackgroundTop}" />
<GradientStop Offset="1" Color="{StaticResource CoBackground}" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="CoBarBrush" StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#1B2212" />
<GradientStop Offset="1" Color="#12180C" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="CoAccentButtonBrush" StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#F0DC9C" />
<GradientStop Offset="1" Color="#D8C079" />
</LinearGradientBrush>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
<Application.Styles>
<FluentTheme />
<!-- Section header label -->
<Style Selector="TextBlock.section">
<Setter Property="Foreground" Value="{StaticResource CoAccentBrush}" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="FontSize" Value="12" />
<Setter Property="LetterSpacing" Value="2" />
</Style>
<!-- Small muted field label -->
<Style Selector="TextBlock.fieldlabel">
<Setter Property="Foreground" Value="{StaticResource CoMutedBrush}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="LetterSpacing" Value="1" />
<Setter Property="Margin" Value="2,0,0,3" />
</Style>
<!-- Text inputs -->
<Style Selector="TextBox">
<Setter Property="Background" Value="{StaticResource CoSurface2Brush}" />
<Setter Property="Foreground" Value="{StaticResource CoTextBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CoBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="CaretBrush" Value="{StaticResource CoAccentBrush}" />
<Setter Property="SelectionBrush" Value="{StaticResource CoGreenDim}" />
<Setter Property="Padding" Value="9,7" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{StaticResource CoBorderBrightBrush}" />
</Style>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{StaticResource CoAccentBrush}" />
</Style>
<!-- Combo -->
<Style Selector="ComboBox">
<Setter Property="Background" Value="{StaticResource CoSurface2Brush}" />
<Setter Property="Foreground" Value="{StaticResource CoTextBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CoBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Padding" Value="9,6" />
<Setter Property="FontSize" Value="13" />
</Style>
<!-- Checkbox -->
<Style Selector="CheckBox">
<Setter Property="Foreground" Value="{StaticResource CoTextBrush}" />
<Setter Property="FontSize" Value="13" />
</Style>
<!-- Base button -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource CoSurface3Brush}" />
<Setter Property="Foreground" Value="{StaticResource CoTextBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CoBorderBrightBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Padding" Value="14,7" />
<Setter Property="FontSize" Value="13" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.12" />
</Transitions>
</Setter>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CoBorderBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CoAccentBrush}" />
</Style>
<Style Selector="Button:disabled">
<Setter Property="Opacity" Value="0.4" />
</Style>
<!-- Accent (primary) button -->
<Style Selector="Button.accent">
<Setter Property="Background" Value="{StaticResource CoAccentButtonBrush}" />
<Setter Property="Foreground" Value="#0C1006" />
<Setter Property="BorderBrush" Value="{StaticResource CoAccentBrightBrush}" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="Button.accent:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CoAccentBrightBrush}" />
<Setter Property="Foreground" Value="#0C1006" />
</Style>
<!-- Danger button -->
<Style Selector="Button.danger">
<Setter Property="Background" Value="#3A1E1D" />
<Setter Property="Foreground" Value="{StaticResource CoDangerBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CoDangerBrush}" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#552726" />
<Setter Property="Foreground" Value="#F0B4B2" />
</Style>
<!-- Ghost / outline button -->
<Style Selector="Button.ghost">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource CoAccentBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CoBorderBrightBrush}" />
</Style>
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CoSurface2Brush}" />
<Setter Property="BorderBrush" Value="{StaticResource CoAccentBrush}" />
</Style>
<!-- Status dot -->
<Style Selector="Ellipse.dot">
<Setter Property="Fill" Value="{StaticResource CoDangerBrush}" />
</Style>
<Style Selector="Ellipse.dot.on">
<Setter Property="Fill" Value="{StaticResource CoGreenBrush}" />
</Style>
<!-- Status pill: glow green when running, quiet red when stopped -->
<Style Selector="Border.pill">
<Setter Property="BorderBrush" Value="{StaticResource CoBorderBrightBrush}" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="BorderBrush" Duration="0:0:0.2" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.pill.on">
<Setter Property="BorderBrush" Value="{StaticResource CoGreenBrush}" />
<Setter Property="BoxShadow" Value="0 0 16 0 #558BD05C" />
</Style>
<!-- Cards lift off the background -->
<Style Selector="Border.card">
<Setter Property="BoxShadow" Value="0 3 14 0 #4D000000" />
</Style>
<!-- ListBox rows -->
<Style Selector="ListBox">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="6,4" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CoSurface3Brush}" />
</Style>
</Application.Styles>
</Application> </Application>
+14 -2
View File
@@ -25,7 +25,19 @@ server — a published `CommonwealthOnline.Server` executable, the framework
## Publish ## Publish
Self-contained, single-file builds that need no .NET runtime on the target.
The host uses reflection-based Avalonia bindings, so publish **untrimmed**.
``` ```
dotnet publish host/CommonwealthOnline.Host.csproj -c Release -r win-x64 --self-contained false dotnet publish host/CommonwealthOnline.Host.csproj -c Release -r win-x64 \
dotnet publish host/CommonwealthOnline.Host.csproj -c Release -r linux-x64 --self-contained false --self-contained true -p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true -o out/win-x64
dotnet publish host/CommonwealthOnline.Host.csproj -c Release -r linux-x64 \
--self-contained true -p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true -o out/linux-x64
``` ```
CI builds both runtimes and uploads them as artifacts — run the
**Publish Host (Avalonia)** workflow (`.github/workflows/publish-host.yml`)
manually, or push a `host-v*` tag.
+177 -73
View File
@@ -3,102 +3,206 @@
xmlns:vm="clr-namespace:CommonwealthOnline.Host.ViewModels" xmlns:vm="clr-namespace:CommonwealthOnline.Host.ViewModels"
x:Class="CommonwealthOnline.Host.Views.MainWindow" x:Class="CommonwealthOnline.Host.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Width="960" Height="700" Width="1040" Height="740"
MinWidth="820" MinHeight="600" MinWidth="900" MinHeight="640"
Title="Commonwealth Online — Server Host" Title="Commonwealth Online — Server Host"
Background="{StaticResource CoBackgroundBrush}"> Background="{StaticResource CoWindowBrush}">
<Grid RowDefinitions="Auto,*,Auto" Margin="18"> <Grid RowDefinitions="Auto,*,Auto">
<Image Grid.Row="0" Source="/Assets/logo.png" Height="84" <!-- ============ Header ============ -->
HorizontalAlignment="Left" Margin="0,0,0,14" /> <Border Grid.Row="0" Background="{StaticResource CoBarBrush}"
BorderBrush="{StaticResource CoBorderBrush}" BorderThickness="0,0,0,1"
Padding="20,12">
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<Grid Grid.Row="1" ColumnDefinitions="330,*"> <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
<Image Source="/Assets/logo.png" Height="42" VerticalAlignment="Center" />
<StackPanel VerticalAlignment="Center" Spacing="1">
<TextBlock Text="COMMONWEALTH ONLINE" Foreground="{StaticResource CoAccentBrush}"
FontWeight="Bold" FontSize="16" LetterSpacing="3" />
<TextBlock Text="DEDICATED SERVER HOST" Foreground="{StaticResource CoMutedBrush}"
FontSize="10" LetterSpacing="4" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="0" Spacing="9" Margin="0,0,18,0"> <!-- Live status pill -->
<TextBlock Text="SERVER" Foreground="{StaticResource CoAccentBrush}" <Border Grid.Column="2" Classes="pill" Classes.on="{Binding IsRunning}"
FontWeight="Bold" FontSize="13" /> Background="{StaticResource CoSurface2Brush}"
<TextBox Watermark="Server name" Text="{Binding ServerName}" /> BorderThickness="1"
<Grid ColumnDefinitions="*,*"> CornerRadius="14" Padding="14,7" VerticalAlignment="Center">
<TextBox Grid.Column="0" Watermark="Host" Text="{Binding Host}" Margin="0,0,4,0" /> <StackPanel Orientation="Horizontal" Spacing="9" VerticalAlignment="Center">
<TextBox Grid.Column="1" Watermark="Port" Text="{Binding Port}" Margin="4,0,0,0" /> <Ellipse Classes="dot" Classes.on="{Binding IsRunning}"
</Grid> Width="10" Height="10" VerticalAlignment="Center" />
<Grid ColumnDefinitions="*,*"> <TextBlock Text="{Binding StatusText}" Foreground="{StaticResource CoTextBrush}"
<TextBox Grid.Column="0" Watermark="Max players" Text="{Binding MaxPlayers}" Margin="0,0,4,0" /> FontWeight="SemiBold" FontSize="13" LetterSpacing="1"
<TextBox Grid.Column="1" Watermark="Admin port" Text="{Binding AdminPort}" Margin="4,0,0,0" /> VerticalAlignment="Center" />
</Grid> </StackPanel>
<TextBlock Text="LOG VERBOSITY" Foreground="{StaticResource CoAccentBrush}" </Border>
FontWeight="Bold" FontSize="12" Margin="0,4,0,0" /> </Grid>
<ComboBox HorizontalAlignment="Stretch" </Border>
ItemsSource="{Binding VerbosityOptions}"
SelectedItem="{Binding LogVerbosity}" />
<CheckBox Content="Enable GNS transport" IsChecked="{Binding EnableGnsTransport}"
Foreground="{StaticResource CoTextBrush}" />
<Button Content="Save config" Command="{Binding SaveCommand}"
HorizontalAlignment="Stretch" />
</StackPanel>
<!-- ============ Body ============ -->
<Grid Grid.Row="1" ColumnDefinitions="352,*" Margin="20,18,20,14">
<!-- Config card -->
<Border Grid.Column="0" Classes="card" Background="{StaticResource CoSurfaceBrush}"
BorderBrush="{StaticResource CoBorderBrush}" BorderThickness="1"
CornerRadius="8" Padding="18" Margin="0,0,18,0">
<StackPanel Spacing="12">
<TextBlock Classes="section" Text="CONFIGURATION" />
<StackPanel Spacing="4">
<TextBlock Classes="fieldlabel" Text="SERVER NAME" />
<TextBox Watermark="My Commonwealth server" Text="{Binding ServerName}" />
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="4" Margin="0,0,8,0">
<TextBlock Classes="fieldlabel" Text="HOST ADDRESS" />
<TextBox Watermark="0.0.0.0" Text="{Binding Host}" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="4" Width="96">
<TextBlock Classes="fieldlabel" Text="PORT" />
<TextBox Watermark="7777" Text="{Binding Port}" />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="4" Margin="0,0,8,0">
<TextBlock Classes="fieldlabel" Text="MAX PLAYERS" />
<TextBox Watermark="16" Text="{Binding MaxPlayers}" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="4" Width="96">
<TextBlock Classes="fieldlabel" Text="ADMIN PORT" />
<TextBox Watermark="7779" Text="{Binding AdminPort}" />
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Classes="fieldlabel" Text="LOG VERBOSITY" />
<ComboBox HorizontalAlignment="Stretch"
ItemsSource="{Binding VerbosityOptions}"
SelectedItem="{Binding LogVerbosity}" />
</StackPanel>
<Border Background="{StaticResource CoSurface2Brush}"
BorderBrush="{StaticResource CoBorderBrush}" BorderThickness="1"
CornerRadius="5" Padding="10,8" Margin="0,2,0,0">
<CheckBox Content="Enable GNS transport (UDP)"
IsChecked="{Binding EnableGnsTransport}" />
</Border>
<Button Classes="ghost" Content="Save configuration"
Command="{Binding SaveCommand}"
HorizontalAlignment="Stretch" Margin="0,4,0,0" />
</StackPanel>
</Border>
<!-- Right column: log + players -->
<Grid Grid.Column="1" RowDefinitions="*,Auto"> <Grid Grid.Column="1" RowDefinitions="*,Auto">
<Border Grid.Row="0" Background="{StaticResource CoSurfaceBrush}" <!-- Server log -->
CornerRadius="4" Padding="10"> <Border Grid.Row="0" Classes="card" Background="{StaticResource CoSurfaceBrush}"
<ScrollViewer> BorderBrush="{StaticResource CoBorderBrush}" BorderThickness="1"
<ItemsControl ItemsSource="{Binding Log}"> CornerRadius="8" Padding="0">
<ItemsControl.ItemTemplate> <Grid RowDefinitions="Auto,*">
<DataTemplate> <Border Grid.Row="0" Background="{StaticResource CoSurface2Brush}"
<TextBlock Text="{Binding}" Foreground="{StaticResource CoTextBrush}" BorderBrush="{StaticResource CoBorderBrush}" BorderThickness="0,0,0,1"
FontFamily="Cascadia Mono,Consolas,monospace" FontSize="12" CornerRadius="8,8,0,0" Padding="14,9">
TextWrapping="Wrap" /> <StackPanel Orientation="Horizontal" Spacing="9">
</DataTemplate> <Ellipse Classes="dot" Classes.on="{Binding IsRunning}"
</ItemsControl.ItemTemplate> Width="9" Height="9" VerticalAlignment="Center" />
</ItemsControl> <TextBlock Classes="section" Text="SERVER LOG" VerticalAlignment="Center" />
</ScrollViewer> </StackPanel>
</Border>
<ScrollViewer Grid.Row="1" Padding="14,10" x:Name="LogScroll">
<ItemsControl ItemsSource="{Binding Log}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Foreground="{StaticResource CoTextBrush}"
FontFamily="Cascadia Mono,Consolas,Menlo,monospace" FontSize="12"
TextWrapping="Wrap" Margin="0,0,0,1" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Border> </Border>
<Border Grid.Row="1" Background="{StaticResource CoSurfaceBrush}" <!-- Players -->
CornerRadius="4" Padding="10" Margin="0,10,0,0" Height="188"> <Border Grid.Row="1" Classes="card" Background="{StaticResource CoSurfaceBrush}"
BorderBrush="{StaticResource CoBorderBrush}" BorderThickness="1"
CornerRadius="8" Padding="0" Margin="0,14,0,0" Height="210">
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">
<DockPanel Grid.Row="0" Margin="0,0,0,6"> <Border Grid.Row="0" Background="{StaticResource CoSurface2Brush}"
<TextBlock DockPanel.Dock="Left" Text="PLAYERS" BorderBrush="{StaticResource CoBorderBrush}" BorderThickness="0,0,0,1"
Foreground="{StaticResource CoAccentBrush}" FontWeight="Bold" FontSize="12" /> CornerRadius="8,8,0,0" Padding="14,9">
<TextBlock DockPanel.Dock="Right" Text="{Binding StatsText}" <DockPanel>
Foreground="{StaticResource CoTextBrush}" FontSize="11" <TextBlock DockPanel.Dock="Left" Classes="section" Text="PLAYERS"
HorizontalAlignment="Right" /> VerticalAlignment="Center" />
</DockPanel> <TextBlock DockPanel.Dock="Right" Text="{Binding StatsText}"
<ListBox Grid.Row="1" Background="Transparent" Foreground="{StaticResource CoMutedBrush}" FontSize="11"
FontFamily="Cascadia Mono,Consolas,monospace"
HorizontalAlignment="Right" VerticalAlignment="Center" />
</DockPanel>
</Border>
<ListBox Grid.Row="1" Margin="6,4"
ItemsSource="{Binding Players}" SelectedItem="{Binding SelectedPlayer}"> ItemsSource="{Binding Players}" SelectedItem="{Binding SelectedPlayer}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate x:DataType="vm:PlayerRow">
<TextBlock Text="{Binding Display}" Foreground="{StaticResource CoTextBrush}" <Grid ColumnDefinitions="52,*,Auto" >
FontFamily="Cascadia Mono,Consolas,monospace" FontSize="12" /> <TextBlock Grid.Column="0" Text="{Binding PlayerId, StringFormat='#{0}'}"
Foreground="{StaticResource CoAccentBrush}" FontWeight="Bold"
FontFamily="Cascadia Mono,Consolas,monospace" FontSize="12"
VerticalAlignment="Center" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="0">
<TextBlock Text="{Binding Label}" Foreground="{StaticResource CoTextBrush}"
FontSize="13" />
<TextBlock Text="{Binding Address}" Foreground="{StaticResource CoMutedBrush}"
FontFamily="Cascadia Mono,Consolas,monospace" FontSize="10" />
</StackPanel>
<TextBlock Grid.Column="2"
Text="{Binding PacketsReceived, StringFormat='↓{0}'}"
Foreground="{StaticResource CoMutedBrush}"
FontFamily="Cascadia Mono,Consolas,monospace" FontSize="11"
VerticalAlignment="Center" />
</Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8" Margin="0,6,0,0">
<Button Content="Kick" Command="{Binding KickCommand}" Padding="18,4" /> <StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8"
<Button Content="Ban" Command="{Binding BanCommand}" Padding="18,4" HorizontalAlignment="Right" Margin="10,0,10,10">
Background="{StaticResource CoDangerBrush}" Foreground="White" /> <Button Content="Kick" Command="{Binding KickCommand}" Padding="22,5" />
<Button Classes="danger" Content="Ban" Command="{Binding BanCommand}" Padding="22,5" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
</Grid> </Grid>
<Grid Grid.Row="2" ColumnDefinitions="Auto,*,Auto,Auto" Margin="0,14,0,0"> <!-- ============ Control bar ============ -->
<TextBlock Grid.Column="0" Text="Status:" Foreground="{StaticResource CoTextBrush}" <Border Grid.Row="2" Background="{StaticResource CoBarBrush}"
VerticalAlignment="Center" Margin="0,0,6,0" /> BorderBrush="{StaticResource CoBorderBrush}" BorderThickness="0,1,0,0"
<TextBlock Grid.Column="1" Text="{Binding StatusText}" Padding="20,14">
Foreground="{StaticResource CoAccentBrush}" FontWeight="Bold" <Grid ColumnDefinitions="*,Auto,Auto">
VerticalAlignment="Center" /> <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Button Grid.Column="2" Content="Start" Command="{Binding StartCommand}" <TextBlock Text="" Foreground="{StaticResource CoMutedBrush}" FontSize="10"
Margin="0,0,8,0" Padding="26,7" VerticalAlignment="Center" />
Background="{StaticResource CoAccentBrush}" Foreground="#0E120B" FontWeight="Bold" /> <TextBlock Text="Local admin channel · 127.0.0.1"
<Button Grid.Column="3" Content="Stop" Command="{Binding StopCommand}" Foreground="{StaticResource CoMutedBrush}" FontSize="11"
Padding="26,7" FontFamily="Cascadia Mono,Consolas,monospace" VerticalAlignment="Center" />
Background="{StaticResource CoDangerBrush}" Foreground="White" FontWeight="Bold" /> </StackPanel>
</Grid> <Button Grid.Column="1" Classes="accent" Content="▶ START SERVER"
Command="{Binding StartCommand}" Margin="0,0,10,0" Padding="26,9"
FontSize="14" />
<Button Grid.Column="2" Classes="danger" Content="■ STOP"
Command="{Binding StopCommand}" Padding="26,9" FontSize="14" />
</Grid>
</Border>
</Grid> </Grid>
</Window> </Window>