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
65 changes: 65 additions & 0 deletions examples/Avalonia.Preview/App.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Themes.Fluent;
using AvaloniaDemo.Components.UI;

namespace AvaloniaPreview;

public class App : Application
{
// The frame around the preview.
public static Border? Host { get; private set; }

public override void Initialize()
{
Styles.Add(new FluentTheme());
RequestedThemeVariant = Program.Theme;
ShellTheme.EnsureInitialized();
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is ISingleViewApplicationLifetime single)
{
Host = new Border { Padding = new Thickness(24), Child = Load(Program.Component) };
Host.Token(Border.BackgroundProperty, ShellToken.Background);
single.MainView = Host;
}

base.OnFrameworkInitializationCompleted();
}

// A preview is Previews/<name>.axaml: the markup a docs page shows, in a panel that places it.
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The previews are in this assembly, which the project file keeps whole.")]
private static Control Load(string name)
{
if (name.Length > 0 && name.All(c => char.IsAsciiLetterOrDigit(c) || c == '-'))
{
try
{
return (Control)AvaloniaXamlLoader.Load(new Uri($"avares://AvaloniaPreview/Previews/{name}.axaml"));
}
catch (Exception)
{
// No such file: say so below.
}
}

var message = new TextBlock
{
Text = name.Length == 0 ? "Add ?c=<name> to the address to pick a preview." : $"There is no preview named \"{name}\".",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
};
message.Token(TextBlock.ForegroundProperty, ShellToken.MutedForeground);
return message;
}
}
40 changes: 40 additions & 0 deletions examples/Avalonia.Preview/AvaloniaPreview.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
<!-- The Avalonia components in the browser, one per URL, for the live previews on the docs site.
Needs the wasm-tools workload: dotnet workload install wasm-tools -->
<PropertyGroup>
<TargetFramework>net10.0-browser</TargetFramework>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<RootNamespace>AvaloniaPreview</RootNamespace>
<!-- No ICU data: it is the largest single download after the runtime. -->
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

<!-- The components are the demo's own files, so a preview can never drift from them. -->
<ItemGroup>
<Compile Include="..\Avalonia.Demo\Components\UI\**\*.cs" LinkBase="Components\UI" />
</ItemGroup>

<!-- Previews are loaded by name at run time, which the trimmer can't follow. -->
<ItemGroup>
<TrimmerRootAssembly Include="AvaloniaPreview" />
</ItemGroup>

<!-- The docs site keeps a preview per example in its own repository and builds this project with
-p:PreviewsDir=<that folder>. The files in Previews are samples for working here alone. -->
<ItemGroup Condition="'$(PreviewsDir)' != ''">
<AvaloniaXaml Remove="Previews\**" />
<AvaloniaXaml Include="$(PreviewsDir)\**\*.axaml" Link="Previews/%(Filename)%(Extension)" />
<Compile Include="$(PreviewsDir)\**\*.cs" Link="Previews/%(Filename)%(Extension)" />
<!-- Pictures the previews show, addressed as /Assets/<file>. -->
<AvaloniaResource Include="$(PreviewsDir)\assets\**" Link="Assets/%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia.Browser" Version="12.1.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.3" />
</ItemGroup>
</Project>
10 changes: 10 additions & 0 deletions examples/Avalonia.Preview/Previews/button.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<WrapPanel xmlns="https://github.com/avaloniaui"
xmlns:ui="using:AvaloniaDemo.Components.UI"
ItemSpacing="8" LineSpacing="8" HorizontalAlignment="Center" VerticalAlignment="Center">
<ui:Button Text="Default" />
<ui:Button Text="Secondary" Variant="Secondary" />
<ui:Button Text="Outline" Variant="Outline" />
<ui:Button Text="Ghost" Variant="Ghost" />
<ui:Button Text="Destructive" Variant="Destructive" />
<ui:Button Text="Link" Variant="Link" />
</WrapPanel>
23 changes: 23 additions & 0 deletions examples/Avalonia.Preview/Previews/dialog.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Panel xmlns="https://github.com/avaloniaui"
xmlns:ui="using:AvaloniaDemo.Components.UI"
HorizontalAlignment="Center" VerticalAlignment="Center">
<ui:Dialog>
<ui:DialogTrigger>
<ui:Button Text="Edit profile" Variant="Outline" />
</ui:DialogTrigger>
<ui:DialogContent>
<ui:DialogHeader>
<ui:DialogTitle Text="Edit profile" />
<ui:DialogDescription Text="Make changes to your profile here. Click save when you're done." />
</ui:DialogHeader>
<StackPanel Spacing="8">
<ui:ShellLabel Text="Name" Size="Sm" Weight="Medium" />
<ui:Input Text="Shewart Shepherd" />
</StackPanel>
<ui:DialogFooter>
<ui:DialogClose><ui:Button Text="Cancel" Variant="Outline" /></ui:DialogClose>
<ui:DialogClose><ui:Button Text="Save changes" /></ui:DialogClose>
</ui:DialogFooter>
</ui:DialogContent>
</ui:Dialog>
</Panel>
6 changes: 6 additions & 0 deletions examples/Avalonia.Preview/Previews/switch.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<StackPanel xmlns="https://github.com/avaloniaui"
xmlns:ui="using:AvaloniaDemo.Components.UI"
Spacing="12" HorizontalAlignment="Center" VerticalAlignment="Center">
<ui:Switch Label="Release emails" IsToggled="True" />
<ui:Switch Label="Airplane mode" />
</StackPanel>
22 changes: 22 additions & 0 deletions examples/Avalonia.Preview/Previews/tabs.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Panel xmlns="https://github.com/avaloniaui"
xmlns:ui="using:AvaloniaDemo.Components.UI"
MaxWidth="420" VerticalAlignment="Center">
<ui:Tabs Value="account">
<ui:TabsList>
<ui:TabsTrigger Value="account" Text="Account" />
<ui:TabsTrigger Value="password" Text="Password" />
</ui:TabsList>
<ui:TabsContent Value="account">
<ui:Card>
<ui:CardHeader Title="Account" Description="Make changes to your account here." />
<ui:CardContent><ui:Input Text="Shewart Shepherd" /></ui:CardContent>
</ui:Card>
</ui:TabsContent>
<ui:TabsContent Value="password">
<ui:Card>
<ui:CardHeader Title="Password" Description="Change your password here." />
<ui:CardContent><ui:Input Placeholder="New password" IsPassword="True" /></ui:CardContent>
</ui:Card>
</ui:TabsContent>
</ui:Tabs>
</Panel>
71 changes: 71 additions & 0 deletions examples/Avalonia.Preview/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Globalization;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser;
using Avalonia.Styling;
using Avalonia.Threading;
using AvaloniaDemo.Components.UI;

[assembly: SupportedOSPlatform("browser")]

namespace AvaloniaPreview;

internal static partial class Program
{
// The preview to show, from ?c=button.
public static string Component { get; private set; } = "";

// From ?theme=dark or ?theme=light; anything else follows the browser.
public static ThemeVariant Theme { get; private set; } = ThemeVariant.Default;

// main.js passes the page's address as the first argument.
private static Task Main(string[] args)
{
var query = args.Length > 0 && Uri.TryCreate(args[0], UriKind.Absolute, out var uri) ? uri.Query : "";
Component = Read(query, "c");
Theme = ParseTheme(Read(query, "theme"));

// Without ICU data the only culture is the invariant one, whose currency sign is a
// placeholder. Money in the previews reads as dollars instead.
var culture = (CultureInfo)CultureInfo.InvariantCulture.Clone();
culture.NumberFormat.CurrencySymbol = "$";
CultureInfo.CurrentCulture = CultureInfo.DefaultThreadCurrentCulture = culture;
return BuildAvaloniaApp().WithInterFont().StartBrowserAppAsync("out");
}

public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>();

// Called by main.js when the page hosting the frame changes its theme.
[JSExport]
internal static void SetTheme(string theme)
{
Theme = ParseTheme(theme);
if (Application.Current is null) return;
Dispatcher.UIThread.Post(() => ShellTheme.SetTheme(Theme));
}

// The height the preview asks for, with the frame's padding, so a page can size its frame.
[JSExport]
internal static double ContentHeight() =>
App.Host?.Child is { } preview ? Math.Ceiling(preview.DesiredSize.Height + App.Host.Padding.Top + App.Host.Padding.Bottom) : 0;

private static ThemeVariant ParseTheme(string theme) => theme switch
{
"dark" => ThemeVariant.Dark,
"light" => ThemeVariant.Light,
_ => ThemeVariant.Default,
};

private static string Read(string query, string key)
{
foreach (var pair in query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var parts = pair.Split('=', 2);
if (parts[0] == key) return parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : "";
}
return "";
}
}
62 changes: 62 additions & 0 deletions examples/Avalonia.Preview/wwwroot/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* The colours match ShellTheme's Background, Border and MutedForeground. */
:root {
color-scheme: light dark;
--background: #fcfcfc;
--border: #e4e4e4;
--muted-foreground: #737373;
}

@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--background: #0a0a0a;
--border: #2a2a2a;
--muted-foreground: #a3a3a3;
}
}

:root[data-theme="dark"] {
--background: #0a0a0a;
--border: #2a2a2a;
--muted-foreground: #a3a3a3;
}

html, body {
height: 100%;
margin: 0;
overflow: hidden;
background: var(--background);
}

#out {
position: relative;
height: 100%;
}

.avalonia-splash {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--background);
transition: opacity 150ms ease-out;
}

/* Avalonia adds this class when the first frame is ready. */
.avalonia-splash.splash-close {
opacity: 0;
pointer-events: none;
}

.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border);
border-top-color: var(--muted-foreground);
border-radius: 50%;
animation: spin 700ms linear infinite;
}

@keyframes spin {
to { transform: rotate(360deg); }
}
22 changes: 22 additions & 0 deletions examples/Avalonia.Preview/wwwroot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ShellUI Native preview</title>
<script>
// Before first paint, so the loading screen is in the reader's theme.
const theme = new URLSearchParams(location.search).get('theme');
if (theme === 'dark' || theme === 'light') document.documentElement.dataset.theme = theme;
</script>
<link rel="stylesheet" href="./app.css">
</head>
<body>
<div id="out">
<div class="avalonia-splash">
<div class="spinner" role="status" aria-label="Loading the preview"></div>
</div>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions examples/Avalonia.Preview/wwwroot/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { dotnet } from './_framework/dotnet.js';

const runtime = await dotnet.create();
const config = runtime.getConfig();
const preview = (await runtime.getAssemblyExports(config.mainAssemblyName)).AvaloniaPreview.Program;

// For the page that hosts the frame, and for tools that measure a preview.
globalThis.shelluiPreview = {
setTheme: (theme) => preview.SetTheme(String(theme)),
contentHeight: () => preview.ContentHeight(),
};

// ShellDocs posts the reader's theme to its preview frames when it changes.
window.addEventListener('message', (event) => {
if (event.data?.type === 'shelldocs-theme') preview.SetTheme(String(event.data.theme));
});

await runtime.runMain(config.mainAssemblyName, [globalThis.location.href]);
performance.mark('preview-started');
19 changes: 19 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This folder contains example projects demonstrating ShellUI Native components.
|---------|----------|-------------|
| [MAUI.Demo](./MAUI.Demo/) | .NET MAUI | Complete MAUI app showcasing all components |
| [Avalonia.Demo](./Avalonia.Demo/) | Avalonia 12 | Avalonia components as they are ported (Phase 2) |
| [Avalonia.Preview](./Avalonia.Preview/) | Avalonia 12 on WebAssembly | One component per URL, for the live previews on the docs site |
| WinUI.Demo | WinUI 3 | *(Coming Soon)* |
| WPF.Demo | WPF | *(Coming Soon)* |

Expand Down Expand Up @@ -38,6 +39,24 @@ dotnet run --project examples/Avalonia.Demo/AvaloniaDemo.csproj

Runs on Windows, macOS and Linux.

### Avalonia Preview

The Avalonia components running in the browser. It compiles the demo's own component files, and
shows the preview named in the address: `?c=button`, with `&theme=dark` or `&theme=light`.

```bash
dotnet workload install wasm-tools
dotnet run --project examples/Avalonia.Preview/AvaloniaPreview.csproj
dotnet publish examples/Avalonia.Preview/AvaloniaPreview.csproj -c Release
```

A preview is one file, `Previews/<name>.axaml`: the markup the docs page shows, inside a panel that
places it. The ones here are samples. The docs site keeps a preview for every example in its own
repository and builds this project with `-p:PreviewsDir=<that folder>` (`.axaml` files, their
`.axaml.cs` files, and pictures under `assets/`, addressed as `/Assets/<file>`).

The project is not in the solution, so building the solution doesn't need the workload.

## Purpose

These examples serve as:
Expand Down
Loading