Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
name: avalonia-mvvm
description: Guide for using MVVM Community Toolkit with Avalonia in RemoteViewer.Client. Use when working with ViewModels, data binding, commands, or Avalonia UI patterns.
---

Expand All @@ -9,7 +10,7 @@ The RemoteViewer.Client uses **CommunityToolkit.Mvvm v8.4.0** with Avalonia's co
## Core Patterns

### ViewModels
- **Base class**: `ViewModelBase` extends `ObservableObject` from MVVM Toolkit
- **Base class**: `ObservableObject` from CommunityToolkit.Mvvm
- **Location**: Co-located with views in `Views/{Feature}/` directories
- **Creation**: Use `IViewModelFactory` with dependency injection for instantiation
- **Constructor injection**: All dependencies injected via constructor (services, logger, etc.)
Expand Down
86 changes: 86 additions & 0 deletions .claude/skills/design-system/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: design-system
description: RemoteViewer.Client design system with spacing, colors, typography, and components. Use when building UI, styling components, or working with Avalonia XAML.
---

# Design System

The client uses a comprehensive design system with markup extensions, design tokens, and reusable components. All UI should use these tokens instead of hardcoded values.

## Markup Extensions

Located in `Themes/` folder. Add `xmlns:theme="using:RemoteViewer.Client.Themes"` to use.

### Spacing (Margin/Padding)
```xml
Padding="{theme:Spacing MD}" <!-- All sides: 12 -->
Margin="{theme:Spacing X=LG, Y=SM}" <!-- Horizontal: 16, Vertical: 8 -->
Margin="{theme:Spacing Top=XL, Right=MD}" <!-- Individual sides -->
```

Values: `None=0, XXS=2, XS=4, SM=8, MD=12, LG=16, XL=24, XXL=32`

The same extension works for `Spacing` (double) and `GridLength` properties:
```xml
<StackPanel Spacing="{theme:Spacing SM}"> <!-- 8px between items -->
<ColumnDefinition Width="{theme:Spacing SM}"/> <!-- 8px spacer column -->
```

## Design Tokens

### Colors (use DynamicResource for theme support)
- **Muted text**: `SystemControlDisabledBaseMediumLowBrush` (Avalonia built-in)
- **Accent**: `AccentButtonBackground`, `AccentButtonForeground` (Avalonia built-in)
- **Surfaces**: `SurfaceElevatedBrush`, `SurfaceOverlayBrush`, `CardBackgroundBrush`
- **Semantic**: `SuccessBrush`, `ErrorBrush`, `WarningBrush`

## Typography Classes

Apply via `Classes="class-name"` on TextBlock:

- **Headings**: `h1` (22px Bold), `h2` (15px SemiBold), `h3` (13px SemiBold)
- **Small text**: `m1` (12px), `m2` (10px)
- **Special**: `credential` (18px Bold monospace)
- **Color modifier**: `muted`

## Button Styles

All buttons automatically get `CornerRadius="6"` from the base style.

```xml
<Button> <!-- Default button (uses Avalonia defaults) -->
<Button Classes="accent"> <!-- Accent/primary button (Avalonia built-in) -->
<Button Classes="icon-button"> <!-- 32x32 transparent icon button for toolbars -->
```

## Card Component (Controls/Card.axaml)

A flexible card container with size options.

```xml
<controls:Card> <!-- Default: 8px radius, 14px padding -->
<controls:Card Size="Large"> <!-- 12px radius, 16px padding -->
<controls:Card Size="XLarge"> <!-- 20px radius, 32x28 padding (for overlays) -->
<controls:Card Size="ListItem"><!-- Uses SurfaceOverlayBrush background -->
```

## Icon Component (Controls/Icon.axaml)

A unified icon control with optional badge mode.

```xml
<controls:Icon Kind="Check" Size="MD"/> <!-- 24px icon -->
<controls:Icon Kind="Check" Size="LG" ShowAsBadge="True"/> <!-- Icon in circular badge -->
```

**Properties:**
- `Kind`: MaterialIconKind enum value
- `Size`: XXS (12px), XS (16px), SM (20px), MD (24px), LG (32px), XL (40px), XXL (48px)
- `ShowAsBadge`: When true, displays icon in a circular background
- `BadgeBackground`: Custom badge background brush (defaults to `BadgeBackgroundBrush`)

## Components

- **Card**: Flexible container with size options (`Controls/Card.axaml`)
- **Icon**: Unified icon with size presets and optional badge mode (`Controls/Icon.axaml`)
- **DialogHeader**: Standardized dialog header with icon, title, subtitle (`Controls/DialogHeader.axaml`)
61 changes: 11 additions & 50 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,24 @@
# Project Overview
# RemoteViewer

RemoteViewer is a .NET 10.0 remote desktop viewer application for Windows. It consists of three main components:
A .NET 10 remote desktop viewer for Windows with three components:

- **RemoteViewer.Client**: Avalonia 11.3-based desktop UI application with MVVM architecture
- **RemoteViewer.Server**: ASP.NET Core SignalR hub server for connection routing
- **RemoteViewer.Client**: Avalonia 11.3 desktop UI with MVVM architecture
- **RemoteViewer.Server**: ASP.NET Core SignalR hub for connection routing
- **RemoteViewer.Shared**: Shared models, protocol messages, and utilities

# Build Commands
## Build & Run

```bash
dotnet build # Build entire solution
dotnet build -c Release # Release build
dotnet run --project src/RemoteViewer.Server # Run server
dotnet run --project src/RemoteViewer.Client # Run client
dotnet test # Run all tests (TUnit)
```

# Testing
## Code Conventions

Uses TUnit testing framework. Four test projects exist:
- **Logging**: Source-generated using `[LoggerMessage]` attributes, preferable in the same file where they are used
- **Error handling**: Nullable enum returns for expected failures (e.g., `Task<TryConnectError?>`); exceptions only for unexpected errors
- **P/Invoke**: CsWin32 source-generated bindings with SafeHandle wrappers
- **No XML docs**: Code should be self-explanatory; add comments only where necessary

```bash
dotnet test # Run all tests
```

- **RemoteViewer.Client.Tests**: Unit tests for client-side logic (e.g., CredentialParser)
- **RemoteViewer.Server.Tests**: Unit tests for server services and protocol messages
- **RemoteViewer.Shared.Tests**: Unit tests for shared utilities and models
- **RemoteViewer.IntegrationTests**: End-to-end SignalR hub integration tests

# Code Conventions

## Logging
- Source-generated logging with `[LoggerMessage]` attributes in separate `*Logs.cs` files
- Structured logging with named parameters for context

## Error Handling
- Nullable enum return types for expected failures (e.g., `Task<TryConnectError?>`)
- Exceptions only for unexpected/startup errors; graceful `false`/`null` returns otherwise

## P/Invoke
- CsWin32 source-generated bindings with SafeHandle wrappers
- `Marshal.GetLastWin32Error()` for error logging

## Documentation
- No XML docs (`///`) - code should be self-explanatory
- You can add comments if necessary for clarity

# MVVM Architecture (Client)

The RemoteViewer.Client uses **CommunityToolkit.Mvvm v8.4.0** with Avalonia 11.3's compiled bindings.

For detailed MVVM patterns, examples, and best practices, see the `avalonia-mvvm` skill (.claude/skills/avalonia-mvvm.md).

## Quick Reference

- **ViewModels**: Inherit from `ViewModelBase`, use `IViewModelFactory` for creation
- **Properties**: `[ObservableProperty]` on private fields generates public properties
- **Commands**: `[RelayCommand]` on private methods generates `IRelayCommand` properties
- **Dependencies**: `[NotifyPropertyChangedFor]` and `[NotifyCanExecuteChangedFor]`
- **Bindings**: Use `x:DataType` in XAML for compiled bindings
- **DI**: Services registered in `ServiceRegistration.cs`
- **Threading**: Use `IDispatcher` for UI updates from background threads
55 changes: 9 additions & 46 deletions src/RemoteViewer.Client/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,10 @@

<Application.Resources>
<ResourceDictionary>
<!-- Accent Colors -->
<SolidColorBrush x:Key="AccentBrush" Color="#0078D4"/>
<SolidColorBrush x:Key="WarningBrush" Color="#FFA500"/>

<!-- Semantic Status Colors -->
<SolidColorBrush x:Key="SuccessBrush" Color="#4CAF50"/>
<SolidColorBrush x:Key="ErrorBrush" Color="#F44336"/>
<SolidColorBrush x:Key="InfoBrush" Color="#2196F3"/>
<SolidColorBrush x:Key="WarningBackgroundBrush" Color="#FFF3CD"/>
<SolidColorBrush x:Key="WarningForegroundBrush" Color="#856404"/>

<!-- Overlay Colors -->
<SolidColorBrush x:Key="OverlayDimBrush" Color="#B0000000"/>
<SolidColorBrush x:Key="OverlayButtonBrush" Color="#80000000"/>

<!-- Toast Accent Colors (shared across themes) -->
<SolidColorBrush x:Key="ToastSuccessAccentBrush" Color="#22C55E"/>
<SolidColorBrush x:Key="ToastErrorAccentBrush" Color="#EF4444"/>
<SolidColorBrush x:Key="ToastInfoAccentBrush" Color="#3B82F6"/>
<SolidColorBrush x:Key="ToastTransferAccentBrush" Color="#3B82F6"/>

<ResourceDictionary.ThemeDictionaries>
<!-- Dark Theme -->
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ToastBackgroundBrush" Color="#F01E1E1E"/>
<SolidColorBrush x:Key="ToastForegroundBrush" Color="#FFFFFF"/>
<SolidColorBrush x:Key="ToastTransferBackgroundBrush" Color="#F01E1E1E"/>
<SolidColorBrush x:Key="ToastIconBadgeBrush" Color="#18FFFFFF"/>
<SolidColorBrush x:Key="ToastProgressBackgroundBrush" Color="#40FFFFFF"/>
<SolidColorBrush x:Key="ToastSecondaryTextBrush" Color="#E0E0E0"/>
<SolidColorBrush x:Key="ToastMutedTextBrush" Color="#A0A0A0"/>
<SolidColorBrush x:Key="ToastCloseButtonHoverBrush" Color="#20FFFFFF"/>
</ResourceDictionary>

<!-- Light Theme -->
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ToastBackgroundBrush" Color="#F5FFFFFF"/>
<SolidColorBrush x:Key="ToastForegroundBrush" Color="#1F2937"/>
<SolidColorBrush x:Key="ToastTransferBackgroundBrush" Color="#F5FFFFFF"/>
<SolidColorBrush x:Key="ToastIconBadgeBrush" Color="#18000000"/>
<SolidColorBrush x:Key="ToastProgressBackgroundBrush" Color="#20000000"/>
<SolidColorBrush x:Key="ToastSecondaryTextBrush" Color="#4B5563"/>
<SolidColorBrush x:Key="ToastMutedTextBrush" Color="#6B7280"/>
<SolidColorBrush x:Key="ToastCloseButtonHoverBrush" Color="#10000000"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<ResourceDictionary.MergedDictionaries>
<!-- Design System - Colors and Tokens -->
<ResourceInclude Source="/Themes/Colors.axaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

Expand All @@ -64,5 +22,10 @@
<Application.Styles>
<FluentTheme />
<mi:MaterialIconStyles />

<!-- Design System - Component Styles -->
<StyleInclude Source="/Themes/Typography.axaml"/>
<StyleInclude Source="/Themes/ButtonStyles.axaml"/>

</Application.Styles>
</Application>
41 changes: 41 additions & 0 deletions src/RemoteViewer.Client/Controls/Card.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<ContentControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:RemoteViewer.Client.Controls"
x:Class="RemoteViewer.Client.Controls.Card"
x:DataType="controls:Card">

<ContentControl.Styles>
<!-- Base card template -->
<Style Selector="controls|Card">
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="CardBorder"
Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
CornerRadius="8"
Padding="14">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
</Border>
</ControlTemplate>
</Setter>
</Style>

<!-- Size: Large -->
<Style Selector="controls|Card[Size=Large] /template/ Border#CardBorder">
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="16"/>
</Style>

<!-- Size: XLarge (for overlays) -->
<Style Selector="controls|Card[Size=XLarge] /template/ Border#CardBorder">
<Setter Property="CornerRadius" Value="20"/>
<Setter Property="Padding" Value="32,28"/>
</Style>

<!-- Size: ListItem -->
<Style Selector="controls|Card[Size=ListItem] /template/ Border#CardBorder">
<Setter Property="Background" Value="{DynamicResource SurfaceOverlayBrush}"/>
</Style>
</ContentControl.Styles>

</ContentControl>
29 changes: 29 additions & 0 deletions src/RemoteViewer.Client/Controls/Card.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Avalonia;
using Avalonia.Controls;

namespace RemoteViewer.Client.Controls;

public enum CardSize
{
Default,
Large,
XLarge,
ListItem
}

public partial class Card : ContentControl
{
public static readonly StyledProperty<CardSize> SizeProperty =
AvaloniaProperty.Register<Card, CardSize>(nameof(Size), CardSize.Default);

public CardSize Size
{
get => this.GetValue(SizeProperty);
set => this.SetValue(SizeProperty, value);
}

public Card()
{
this.InitializeComponent();
}
}
31 changes: 31 additions & 0 deletions src/RemoteViewer.Client/Controls/DialogHeader.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:RemoteViewer.Client.Controls"
xmlns:theme="using:RemoteViewer.Client.Themes"
x:Class="RemoteViewer.Client.Controls.DialogHeader"
x:DataType="controls:DialogHeader"
x:Name="Root">

<Grid ColumnDefinitions="Auto,*">
<!-- Icon Badge -->
<controls:Icon Grid.Column="0"
Kind="{Binding Icon, ElementName=Root}"
Size="{Binding IconSize, ElementName=Root}"
Foreground="{Binding IconForeground, ElementName=Root}"
ShowAsBadge="True"
BadgeBackground="{Binding IconBackground, ElementName=Root}"
Margin="{theme:Spacing Right=MD}"
VerticalAlignment="Center"/>

<!-- Title and Subtitle -->
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Spacing="{theme:Spacing XS}">
<TextBlock Text="{Binding Title, ElementName=Root}"
Classes="h2"/>
<TextBlock Text="{Binding Subtitle, ElementName=Root}"
Classes="m1"
IsVisible="{Binding Subtitle, ElementName=Root, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</Grid>
</UserControl>
Loading