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
13 changes: 10 additions & 3 deletions docs/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ Ported so far: `shell`, `icon`, `theme-toggle`, P0 (`button`, `input`, `label`,
`switch`, `card` and its parts, `separator`, `badge`, `progress`, `alert`), P1 (`dialog`,
`drawer`, `sheet`, `dropdown`, `popover` and their parts), P2 (`textarea`, `select`, `slider`,
`radio-group`, `calendar`, `date-picker`, `time-picker`), P3 (`collapsible`, `accordion`,
`tabs`, `breadcrumb` and their parts, `skeleton`, `scroll-area`) and P4 (`spinner`, `tooltip`,
`toast`, `alert-dialog`, `hover-card` and its parts). Not yet published to NuGet. In an
Avalonia project, `shellui-native list` shows only these.
`tabs`, `breadcrumb` and their parts, `skeleton`, `scroll-area`), P4 (`spinner`, `tooltip`,
`toast`, `alert-dialog`, `hover-card` and its parts), P5 (`avatar`, `toggle`, `callout`,
`table`, `empty-state`, `pagination`) and P6 (`context-menu`, `carousel`, `stepper`). Not yet
published to NuGet. In an Avalonia project, `shellui-native list` shows only these.

Overlays use Avalonia's `Popup` in the window's overlay layer: the content stays in the logical
tree (theme, DataContext and `FindParentOfType` work as declared), dropdowns and popovers get
Expand Down Expand Up @@ -107,6 +108,12 @@ Same names, properties and variants as MAUI, except:
| Toaster | Declared on a page | Declared in a window; `Toast.Show` without one uses the active window |
| HoverCard on touch | Tap toggles | Touch, pen and Enter / Space toggle; a mouse click acts on the wrapped control only |
| AlertDialog extra content | One `Body` view | Any XAML children, stacked between the description and the buttons |
| Avatar `Source` | `ImageSource` (`Source="profile.png"`) | `IImage`, an Avalonia resource path (`Source="/Assets/profile.png"`) |
| TableRow clicks | `Tapped` event on the row | Avalonia's own `Tapped` event (`TappedEventArgs`) |
| ContextMenu | Right-click, or long-press on touch | Also the Menu key / Shift+F10 while focus is inside the trigger; a right-click while it is open moves it; items are tab stops; children are collected in `Items` |
| Carousel | Swipe, arrows, dots | Drag with the mouse or touch; the arrows are tab stops and Left / Right move while one has focus; with `AspectRatio="0"` set `Height` |
| Stepper | Shows the active step's view | Every step stays in the tree and only the active one is visible; reached steps are tab stops (Enter / Space) |
| Pagination | Labels hidden on phones | `ShowLabels` defaults to true; every page and arrow is a tab stop (Enter / Space) |

---

Expand Down
19 changes: 16 additions & 3 deletions docs/DEVELOPMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ main ← Phase 1a (2026-07-05), 1b (PR #2), 1c + 1d (PR #3), 1e (PR #4), brand

| Branch | Base | Status | Purpose |
|--------|------|--------|---------|
| `main` | — | Phase 1a–1e, release `0.1.0-alpha.1`, Phase 2 foundation and P0–P3 merged | 92 registry entries (89 CLI targets, 58 families); Avalonia templates for the foundation and P0–P3 |
| `feat/avalonia-p4-feedback` | `main` | **Active** | Phase 2 P4: Avalonia feedback and overlays |
| `main` | — | Phase 1a–1e, release `0.1.0-alpha.1`, Phase 2 foundation and P0–P4 merged | 92 registry entries (89 CLI targets, 58 families); Avalonia templates for the foundation and P0–P4 |
| `feat/avalonia-p5-data-display` | `main` | Pushed, PR next | Phase 2 P5: Avalonia data display |
| `feat/avalonia-p6-advanced` | `feat/avalonia-p5-data-display` | **Active** | Phase 2 P6: Avalonia context menu, carousel and stepper |

---

Expand Down Expand Up @@ -434,7 +435,7 @@ After merge: tag `v0.1.0-alpha.1` on `main` and push the tag.

---

## Phase 2 — Avalonia (active; one branch per tier, `feat/avalonia-p4-feedback` now)
## Phase 2 — Avalonia (active; one branch per tier, `feat/avalonia-p6-advanced` now)

Cross-desktop (Windows + macOS + Linux) from one XAML codebase.

Expand Down Expand Up @@ -477,6 +478,18 @@ Cross-desktop (Windows + macOS + Linux) from one XAML codebase.
`ShellFloatingHost.ClaimsChild` lets a host place XAML children itself. `ShellLabel` now wraps
like MAUI's Label. Each installs alone into a fresh `dotnet new avalonia.app` and builds with 0
warnings; checked in the demo in light and dark
- [x] P5: avatar, toggle, callout, table (one template: Table, TableHeader, TableRow, TableHead,
TableCell), empty-state, pagination. Each installs alone into a fresh `dotnet new avalonia.app`
and builds with 0 warnings; checked in the demo in light and dark. Both demos now use ShellUI
content: Shewart Shepherd / @shewart, shellui-dev repositories, and the Shell family marks as
avatar images
- [x] P6: context-menu (one template: ContextMenu, ContextMenuTrigger, ContextMenuContent,
ContextMenuItem, ContextMenuSeparator), carousel, stepper (Stepper + StepperStep). The context
menu opens on Avalonia's `ContextRequested` (right-click, Menu key / Shift+F10) and on a touch
hold, at the pointer through `Popup.PlacementRect`; the carousel animates on the window's
animation frames. Each installs alone into a fresh Avalonia app and builds with 0 warnings;
checked in the demo in light and dark with real pointer and key input. resizable, navbar and
sidebar wait for their MAUI versions
- [ ] 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`
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 | In progress (Phase 2): theme, icons, P0–P4 components; not on NuGet yet | .NET 10.0 (Avalonia 12) |
| Avalonia UI | In progress (Phase 2): theme, icons, P0–P5 components and P6's context menu, carousel and stepper; 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
Binary file added examples/Avalonia.Demo/Assets/Brand/shelldocs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/Avalonia.Demo/Assets/Brand/shellui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 105 additions & 0 deletions examples/Avalonia.Demo/Components/UI/Avatar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;

namespace AvaloniaDemo.Components.UI;

// Avatar — rounded-full image over a bg-muted fallback (initials, or a user icon when no
// Fallback is set). The fallback shows wherever the image doesn't cover it.
// Usage: <ui:Avatar Source="/Assets/profile.png" Fallback="SS" Size="Lg" />
public class Avatar : Border
{
public static readonly StyledProperty<IImage?> SourceProperty =
AvaloniaProperty.Register<Avatar, IImage?>(nameof(Source));

public static readonly StyledProperty<string?> FallbackProperty =
AvaloniaProperty.Register<Avatar, string?>(nameof(Fallback));

public static readonly StyledProperty<AvatarSize> SizeProperty =
AvaloniaProperty.Register<Avatar, AvatarSize>(nameof(Size), AvatarSize.Default);

private readonly TextBlock _initials;
private readonly Icon _userIcon;
private readonly Image _image;

static Avatar()
{
SourceProperty.Changed.AddClassHandler<Avatar>((a, _) =>
{
a._image.Source = a.Source;
a._image.IsVisible = a.Source != null;
});
FallbackProperty.Changed.AddClassHandler<Avatar>((a, _) => a.UpdateFallback());
SizeProperty.Changed.AddClassHandler<Avatar>((a, _) => a.UpdateSize());
}

public IImage? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}

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

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

public Avatar()
{
_initials = new TextBlock
{
FontWeight = FontWeight.SemiBold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
_initials.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
_userIcon = new Icon { Kind = IconName.User, Token = ShellToken.MutedForeground, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
_image = new Image { Stretch = Stretch.UniformToFill, IsVisible = false };

Child = new Panel { Children = { _initials, _userIcon, _image } };
// Clips the image to the circle.
ClipToBounds = true;
HorizontalAlignment = HorizontalAlignment.Left;
VerticalAlignment = VerticalAlignment.Center;
this.Token(BackgroundProperty, ShellToken.Muted);
UpdateSize();
UpdateFallback();
}

private void UpdateSize()
{
var size = Size switch
{
AvatarSize.Sm => 32,
AvatarSize.Lg => 48,
AvatarSize.Xl => 64,
_ => 40
};
Width = size;
Height = size;
CornerRadius = new CornerRadius(size / 2.0);
_initials.FontSize = Math.Round(size * 0.36);
_userIcon.Size = size / 2.0;
}

private void UpdateFallback()
{
var hasText = !string.IsNullOrWhiteSpace(Fallback);
_initials.Text = Fallback ?? string.Empty;
_initials.IsVisible = hasText;
_userIcon.IsVisible = !hasText;
AutomationProperties.SetName(this, hasText ? Fallback : "Avatar");
}
}

public enum AvatarSize { Sm, Default, Lg, Xl }
137 changes: 137 additions & 0 deletions examples/Avalonia.Demo/Components/UI/Callout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Metadata;

namespace AvaloniaDemo.Components.UI;

// Highlighted note — rounded-lg border px-4 py-3 on a faint tint of the variant color, with an
// icon, an optional title and text and/or any content.
// <ui:Callout Variant="Tip" Title="Pro tip" Text="Press Ctrl+K to search." />
// <ui:Callout Variant="Warning" Title="Heads up"><TextBlock Text="..." /></ui:Callout>
public class Callout : Border
{
public static readonly StyledProperty<CalloutVariant> VariantProperty =
AvaloniaProperty.Register<Callout, CalloutVariant>(nameof(Variant), CalloutVariant.Info);

public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<Callout, string?>(nameof(Title));

public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<Callout, string?>(nameof(Text));

// None uses the variant's own icon.
public static readonly StyledProperty<IconName> IconProperty =
AvaloniaProperty.Register<Callout, IconName>(nameof(Icon), IconName.None);

public static readonly StyledProperty<Control?> BodyProperty =
AvaloniaProperty.Register<Callout, Control?>(nameof(Body));

private readonly Border _tint;
private readonly Icon _icon;
private readonly TextBlock _title;
private readonly TextBlock _text;
private readonly ContentControl _body = new();

static Callout()
{
VariantProperty.Changed.AddClassHandler<Callout>((c, _) => c.Update());
TitleProperty.Changed.AddClassHandler<Callout>((c, _) => c.Update());
TextProperty.Changed.AddClassHandler<Callout>((c, _) => c.Update());
IconProperty.Changed.AddClassHandler<Callout>((c, _) => c.Update());
BodyProperty.Changed.AddClassHandler<Callout>((c, _) => c.Update());
}

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

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

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

public IconName Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}

[Content]
public Control? Body
{
get => GetValue(BodyProperty);
set => SetValue(BodyProperty, value);
}

public Callout()
{
_tint = new Border { CornerRadius = new CornerRadius(ShellTheme.RadiusLg - 1) };
_icon = new Icon { Size = 16, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 2, 0, 0) };
_title = new TextBlock { FontSize = 14, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap };
_text = new TextBlock { FontSize = 14, LineHeight = 20, TextWrapping = TextWrapping.Wrap };
_text.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);

var text = new StackPanel { Spacing = 6, Children = { _title, _text, _body } };
Grid.SetColumn(text, 1);
var row = new Grid
{
Margin = new Thickness(16, 12),
ColumnSpacing = 12,
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
Children = { _icon, text }
};

Child = new Panel { Children = { _tint, row } };
BorderThickness = new Thickness(1);
CornerRadius = new CornerRadius(ShellTheme.RadiusLg);
this.Token(BorderBrushProperty, ShellToken.Border);
Update();
}

private void Update()
{
var (icon, accent) = Variant switch
{
CalloutVariant.Warning => (IconName.TriangleAlert, ShellToken.Warning),
CalloutVariant.Danger => (IconName.CircleAlert, ShellToken.Destructive),
CalloutVariant.Tip => (IconName.Lightbulb, ShellToken.Success),
CalloutVariant.Default => (IconName.Info, ShellToken.MutedForeground),
_ => (IconName.Info, ShellToken.Info)
};
var neutral = Variant == CalloutVariant.Default;

// bg-<color>/8 for the tinted variants, bg-muted/50 for the neutral one.
_tint.Token(BackgroundProperty, neutral ? ShellToken.Muted : accent);
_tint.Opacity = neutral ? 0.5 : 0.08;

_icon.Kind = Icon == IconName.None ? icon : Icon;
_icon.Token = accent;
_title.Text = Title ?? string.Empty;
_title.IsVisible = !string.IsNullOrEmpty(Title);
_title.Token(TextBlock.ForegroundProperty, neutral ? ShellToken.Foreground : accent);
_text.Text = Text ?? string.Empty;
_text.IsVisible = !string.IsNullOrEmpty(Text);
_body.Content = Body;
_body.IsVisible = Body != null;
}
}

public enum CalloutVariant
{
Info,
Warning,
Danger,
Tip,
Default
}
Loading
Loading