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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,22 @@ jobs:

- name: Build MAUI Demo (Windows)
run: dotnet build examples/MAUI.Demo/MAUI.Demo.csproj --configuration Release --framework net10.0-windows10.0.19041.0

# Avalonia is the cross-desktop target, so its demo builds on all three desktop OSes
build-avalonia:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x

- name: Build Avalonia Demo
run: dotnet build examples/Avalonia.Demo/AvaloniaDemo.csproj --configuration Release
1 change: 1 addition & 0 deletions ShellUI.Native.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<File Path="docs/README.md" />
</Folder>
<Folder Name="/examples/">
<Project Path="examples/Avalonia.Demo/AvaloniaDemo.csproj" />
<File Path="examples/README.md" />
<Project Path="examples/MAUI.Demo/MAUI.Demo.csproj">
<Deploy />
Expand Down
40 changes: 24 additions & 16 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,33 +168,41 @@ public partial class Button : ContentView
}
```

## Component Pattern (Avalonia, planned)
## Component Pattern (Avalonia)

Avalonia components will follow the `StyledProperty`/`TemplatedControl` pattern instead of
MAUI's `BindableProperty`/`ContentView` — the same property-driven visual-state approach, just
Avalonia's equivalent APIs:
Same model as MAUI: one C# file per component, built in code from Avalonia primitives
(`Border`, `Panel`, `TextBlock`, a custom `Render` for icons), with no `.axaml`. The CLI keeps
installing one file per component, and nothing has to be registered in `App.axaml`.

- **Properties** are `StyledProperty`s; class handlers or `AffectsRender` react to changes.
- **Colors** bind to the same tokens with `control.Token(property, ShellToken.X)`, a
resource-observable binding. `ShellTheme` publishes the light and dark palettes as theme
dictionaries, so Avalonia swaps them when `RequestedThemeVariant` changes.
- **Names match MAUI** where Avalonia allows it. The one difference so far: `Icon` uses `Kind`
for the icon, because every Avalonia control already has a `Name`.
- **Usings are explicit.** `dotnet new avalonia.app` has implicit usings off, so each component
carries `using System;` and the rest; the demo turns them off too, so it catches a missing one.

```csharp
public partial class Button : TemplatedControl
public class Badge : Border
{
public static readonly StyledProperty<ButtonVariant> VariantProperty =
AvaloniaProperty.Register<Button, ButtonVariant>(nameof(Variant), ButtonVariant.Default);
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<Badge, string?>(nameof(Text));

public ButtonVariant Variant
{
get => GetValue(VariantProperty);
set => SetValue(VariantProperty, value);
}
private readonly TextBlock _label = new() { FontSize = 12 };

static Button()
public Badge()
{
VariantProperty.Changed.AddClassHandler<Button>((button, _) => button.UpdateVisualState());
this.Token(BackgroundProperty, ShellToken.Primary);
Child = _label.Token(TextBlock.ForegroundProperty, ShellToken.PrimaryForeground);
}
}
```

Not implemented yet — this illustrates the target shape once Template System v2 lands and
`ShellUI.Native.Avalonia` is scaffolded.
Components are developed in `examples/Avalonia.Demo/Components/UI/` and copied into the
templates' `[NativePlatform.Avalonia]` entries by `scripts/sync-templates.py`. A component gets
an Avalonia entry only after its MAUI one exists, and only if every dependency has one too
(checked by `TemplateContentTests`).

## Versioning Strategy

Expand Down
23 changes: 23 additions & 0 deletions docs/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ your background so the splash-to-app transition doesn't flash the template's pur

---

## Avalonia (in progress)

Ported so far: `shell`, `icon`, `theme-toggle` and P0 (`button`, `input`, `label`, `checkbox`,
`switch`, `card` and its parts, `separator`, `badge`, `progress`, `alert`). Not yet published to
NuGet. In an Avalonia project, `shellui-native list` shows only these.

Setup after `shellui-native init`: call `Components.UI.ShellTheme.EnsureInitialized();` in
`App.Initialize` after `AvaloniaXamlLoader.Load(this);`, and add
`xmlns:ui="using:YourApp.Components.UI"` to your XAML. Switch theme with
`ShellTheme.SetTheme(ThemeVariant.Dark)` or `ShellTheme.ToggleTheme()`.

Same names, properties and variants as MAUI, except:

| | MAUI | Avalonia |
|---|---|---|
| Icon | `<ui:Icon Name="Search" />` | `<ui:Icon Kind="Search" />` (every Avalonia control already has a `Name`) |
| Input length limit | `MaxLength`, default `int.MaxValue` | `MaxLength`, `0` (default) means no limit |
| Card children | XAML children | XAML children, collected in `Card.Items` |
| Block button | `HorizontalOptions="Fill"` | `HorizontalAlignment="Stretch"` |
| Focus ring | Platform focus visuals | shadcn's ring, for keyboard focus only |

---

## Icon

Stroke icons from [ShellIcons](../../../icons/shell-icons) (Lucide 0.475.0 — the set ShellUI
Expand Down
27 changes: 21 additions & 6 deletions docs/DEVELOPMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,13 +441,28 @@ Cross-desktop (Windows + macOS + Linux) from one XAML codebase.

### Deliverables

- [ ] New `src/ShellUI.Native.Avalonia/` reference project (Avalonia 12.x)
- [ ] Avalonia design-token `ResourceDictionary` mirroring [`StyleTemplates.cs`](../src/ShellUI.Native.Templates/StyleTemplates.cs)
- [ ] Add Avalonia content to each template (P0 + P1 first), using the
`TemplatedControl`/`StyledProperty` pattern documented in
- [x] `examples/Avalonia.Demo/` (Avalonia 12.1.3, .NET 10, implicit usings off like the
Avalonia template); components are developed there, as in the MAUI demo. A separate
`src/ShellUI.Native.Avalonia/` project isn't needed
- [x] Theme tokens: `ShellTheme` in the Avalonia `shell` publishes both palettes as theme
dictionaries (no XAML resource file to install)
- [x] Foundation templates with Avalonia content: `shell`, `icon` (same 110 icons, `Kind`
instead of `Name`), `theme-toggle`; pattern in
[ARCHITECTURE.md § Component Pattern (Avalonia)](./ARCHITECTURE.md)
- [ ] `examples/Avalonia.Demo/` reference app mirroring the MAUI demo
- [ ] CI job that builds the Avalonia demo on all three OSes (`ubuntu-latest`, `windows-latest`, `macos-latest`)
- [x] `scripts/sync-templates.py` writes the `[NativePlatform.Avalonia]` entries;
`scripts/generate-icons.py` writes both `Icon.cs` files
- [x] CLI: `init` in an Avalonia project installs the Avalonia `Shell.cs` and prints Avalonia next
steps; `list` shows only components with a template for the project's platform
- [x] CI job that builds the Avalonia demo on `ubuntu-latest`, `windows-latest` and `macos-latest`
- [x] P0: button (+ button-variants), input, label, checkbox, switch, card family, separator, badge,
progress, alert. Each installs alone into a fresh `dotnet new avalonia.app` and builds with 0
warnings; checked in the demo in light and dark, with the keyboard focus ring and loading state
- [ ] P1: dialog, drawer, sheet, dropdown, popover (Avalonia's overlay layer and popups)
- [ ] Replace the generated `Icon.cs` with the `ShellIcons.Maui` / `ShellIcons.Avalonia` packages
once they are on NuGet. Their names already match (`IconName`, `Icon`, `Kind` on Avalonia), so
components change little: token tinting binds the package's `Color` (MAUI) or `Foreground`
(Avalonia). The CLI needs NuGet dependencies first (`add icon` runs `dotnet add package`), and
`scripts/generate-icons.py` goes away

### Order of implementation

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ shellui-native list
| Platform | Status | .NET Version |
|----------|--------|--------------|
| .NET MAUI | Available (`0.1.0-alpha.1`) — Android, iOS, Mac Catalyst, Windows | .NET 10.0 |
| Avalonia UI | Planned (Phase 2) | .NET 10.0 (Avalonia 12) |
| Avalonia UI | In progress (Phase 2): theme, icons and P0 components; not on NuGet yet | .NET 10.0 (Avalonia 12) |
| WinUI 3 | Conditional (Phase 3) | .NET 10.0 |

WPF is intentionally not on this list — it's recognized for project detection only, not an
Expand Down
10 changes: 10 additions & 0 deletions examples/Avalonia.Demo/App.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AvaloniaDemo.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>
26 changes: 26 additions & 0 deletions examples/Avalonia.Demo/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using AvaloniaDemo.Components.UI;

namespace AvaloniaDemo;

public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
// Publish the ShellUI theme tokens before the first window resolves {DynamicResource ShellUI*}.
ShellTheme.EnsureInitialized();
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}

base.OnFrameworkInitializationCompleted();
}
}
21 changes: 21 additions & 0 deletions examples/Avalonia.Demo/AvaloniaDemo.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<!-- Off, as in `dotnet new avalonia.app`, so a component missing a using fails here first. -->
<ImplicitUsings>disable</ImplicitUsings>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="12.1.3" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.3" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
98 changes: 98 additions & 0 deletions examples/Avalonia.Demo/Components/UI/Alert.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;

namespace AvaloniaDemo.Components.UI;

// Callout with icon, title and message — rounded-lg border p-4, icon + text tinted per variant.
public class Alert : Border
{
public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<Alert, string?>(nameof(Title));

public static readonly StyledProperty<string?> MessageProperty =
AvaloniaProperty.Register<Alert, string?>(nameof(Message));

public static readonly StyledProperty<AlertVariant> VariantProperty =
AvaloniaProperty.Register<Alert, AlertVariant>(nameof(Variant));

private readonly Icon _icon;
private readonly TextBlock _titleLabel;
private readonly TextBlock _messageLabel;

static Alert()
{
TitleProperty.Changed.AddClassHandler<Alert>((a, _) => a.UpdateVisualState());
MessageProperty.Changed.AddClassHandler<Alert>((a, _) => a.UpdateVisualState());
VariantProperty.Changed.AddClassHandler<Alert>((a, _) => a.UpdateVisualState());
}

public string? Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}

public string? Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}

public AlertVariant Variant
{
get => GetValue(VariantProperty);
set => SetValue(VariantProperty, value);
}

public Alert()
{
_icon = new Icon { Size = 16, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 2, 0, 0) };
_titleLabel = new TextBlock { FontSize = 14, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap };
_messageLabel = new TextBlock { FontSize = 14, TextWrapping = TextWrapping.Wrap };

var text = new StackPanel { Spacing = 4, Children = { _titleLabel, _messageLabel } };
Grid.SetColumn(text, 1);
Child = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*"), ColumnSpacing = 12, Children = { _icon, text } };
Padding = new Thickness(16, 12);
BorderThickness = new Thickness(1);
CornerRadius = new CornerRadius(ShellTheme.RadiusLg);
this.Token(BackgroundProperty, ShellToken.Card);
UpdateVisualState();
}

private void UpdateVisualState()
{
var (icon, accent) = Variant switch
{
AlertVariant.Destructive => (IconName.CircleAlert, ShellToken.Destructive),
AlertVariant.Success => (IconName.CircleCheck, ShellToken.Success),
AlertVariant.Warning => (IconName.TriangleAlert, ShellToken.Warning),
AlertVariant.Info => (IconName.Info, ShellToken.Info),
_ => (IconName.Info, ShellToken.Foreground)
};

_icon.Kind = icon;
_icon.Token = accent;
_titleLabel.Text = Title ?? string.Empty;
_titleLabel.IsVisible = !string.IsNullOrEmpty(Title);
_messageLabel.Text = Message ?? string.Empty;
_messageLabel.IsVisible = !string.IsNullOrEmpty(Message);

// Tinted variants color the title; the message stays readable in the muted foreground.
_titleLabel.Token(TextBlock.ForegroundProperty, accent);
_messageLabel.Token(TextBlock.ForegroundProperty,
Variant == AlertVariant.Default ? ShellToken.MutedForeground : ShellToken.Foreground);
this.Token(BorderBrushProperty, Variant == AlertVariant.Default ? ShellToken.Border : accent);
}
}

public enum AlertVariant
{
Default,
Destructive,
Success,
Warning,
Info
}
Loading
Loading