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
16 changes: 13 additions & 3 deletions docs/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,16 @@ 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.
Ported so far: `shell`, `icon`, `theme-toggle`, P0 (`button`, `input`, `label`, `checkbox`,
`switch`, `card` and its parts, `separator`, `badge`, `progress`, `alert`) and P1 (`dialog`,
`drawer`, `sheet`, `dropdown`, `popover` and their parts). 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
Avalonia's placement and flip above the trigger when there is no room below, and a click outside
closes them. Escape closes the overlay on top. Keyboard focus is not trapped inside an open
dialog yet: Tab can still reach controls behind it.

Setup after `shellui-native init`: call `Components.UI.ShellTheme.EnsureInitialized();` in
`App.Initialize` after `AvaloniaXamlLoader.Load(this);`, and add
Expand All @@ -85,6 +92,9 @@ Same names, properties and variants as MAUI, except:
| 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 |
| Overlay children | XAML children | XAML children, collected in `Items` (`Dialog`, `DialogContent`, `Dropdown`, …) |
| Dropdown / Popover | `ToggleAsync()` / `CloseAsync()` kept for older code | `Toggle()` / `Close()` only |
| Flipped popup gap | 4px above the trigger | Flush with the trigger (Avalonia doesn't mirror the offset) |

---

Expand Down
6 changes: 5 additions & 1 deletion docs/DEVELOPMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,11 @@ Cross-desktop (Windows + macOS + Linux) from one XAML codebase.
- [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)
- [x] P1: dialog, drawer, sheet, dropdown, popover and their parts, on Avalonia's `Popup` in the
overlay layer (shared hosts in `shell`: `ShellOverlayHost`, `ShellPopoverHost`,
`ShellTriggerView`, `ShellDismiss` for Escape). Each installs alone into a fresh
`dotnet new avalonia.app`; checked in the demo: opening from triggers, placement and flip,
Escape, picking an item, `DialogClose`, light and dark. Open: focus trapping in dialogs
- [ ] 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 and P0 components; not on NuGet yet | .NET 10.0 (Avalonia 12) |
| Avalonia UI | In progress (Phase 2): theme, icons, P0 and P1 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
7 changes: 5 additions & 2 deletions examples/Avalonia.Demo/Components/UI/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace AvaloniaDemo.Components.UI;
// Button with ShellUI variants, sizes, optional icon and loading state.
// Sizes to its content like an inline-flex button; set HorizontalAlignment="Stretch" for a block button.
// Usage: <ui:Button Text="Save" Icon="Check" Variant="Outline" Clicked="OnSave" />
public class Button : Border
public class Button : Border, IShellFocusable
{
public static readonly StyledProperty<ButtonVariant> VariantProperty =
AvaloniaProperty.Register<Button, ButtonVariant>(nameof(Variant));
Expand Down Expand Up @@ -125,7 +125,10 @@ public Button()
// Clicks in code, as a pointer click or Enter/Space would.
public void Press()
{
if (CanClick) Clicked?.Invoke(this, EventArgs.Empty);
if (!CanClick) return;
Clicked?.Invoke(this, EventArgs.Empty);
// Inside a DialogTrigger, DropdownTrigger, ... the click also activates the trigger.
ShellTriggerView.ActivateAncestor(this);
}

protected override void OnPointerEntered(PointerEventArgs e)
Expand Down
11 changes: 11 additions & 0 deletions examples/Avalonia.Demo/Components/UI/Dialog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace AvaloniaDemo.Components.UI;

// Modal dialog. Declare it anywhere; the content covers the window while open:
// <ui:Dialog x:Name="ConfirmDialog">
// <ui:DialogTrigger><ui:Button Text="Open" /></ui:DialogTrigger> (optional)
// <ui:DialogContent>...</ui:DialogContent>
// </ui:Dialog>
// Open from code with ConfirmDialog.SetOpen(true).
public class Dialog : ShellOverlayHost
{
}
7 changes: 7 additions & 0 deletions examples/Avalonia.Demo/Components/UI/DialogClose.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace AvaloniaDemo.Components.UI;

// Closes the enclosing Dialog. Wrap a Button: <ui:DialogClose><ui:Button Text="Cancel" /></ui:DialogClose>
public class DialogClose : ShellTriggerView
{
protected override void OnActivated() => this.FindParentOfType<Dialog>()?.SetOpen(false);
}
106 changes: 106 additions & 0 deletions examples/Avalonia.Demo/Components/UI/DialogContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Animation.Easings;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Metadata;

namespace AvaloniaDemo.Components.UI;

// Dialog panel — backdrop + centered box: max-w-lg rounded-lg border bg-background p-6
// shadow-lg, close button top-right; fades and zooms in from 95%. XAML children go to Items.
public class DialogContent : Border, IShellOverlayContent
{
public static readonly StyledProperty<bool> ShowCloseButtonProperty =
AvaloniaProperty.Register<DialogContent, bool>(nameof(ShowCloseButton), true);

private readonly Border _backdrop;
private readonly Border _box;
private readonly Border _close;
private readonly StackPanel _body = new() { Spacing = 16 };

static DialogContent()
{
ShowCloseButtonProperty.Changed.AddClassHandler<DialogContent>((c, e) => c._close.IsVisible = (bool)e.NewValue!);
}

public bool ShowCloseButton
{
get => GetValue(ShowCloseButtonProperty);
set => SetValue(ShowCloseButtonProperty, value);
}

[Content]
public Controls Items => _body.Children;

public DialogContent()
{
_backdrop = new Border();
_backdrop.Token(BackgroundProperty, ShellToken.Overlay);
_backdrop.PointerPressed += (_, e) =>
{
this.FindParentOfType<Dialog>()?.SetOpen(false);
e.Handled = true;
};

_close = new Border
{
Child = new Icon { Kind = IconName.X, Size = 16, Token = ShellToken.MutedForeground },
Width = 24,
Height = 24,
CornerRadius = new CornerRadius(ShellTheme.RadiusSm),
Background = Brushes.Transparent,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, -8, -8, 0),
Cursor = new Cursor(StandardCursorType.Hand)
};
AutomationProperties.SetName(_close, "Close");
_close.PointerEntered += (_, _) => _close.Token(BackgroundProperty, ShellToken.Accent);
_close.PointerExited += (_, _) => { _close.ClearToken(BackgroundProperty); _close.Background = Brushes.Transparent; };
_close.PointerReleased += (_, e) =>
{
this.FindParentOfType<Dialog>()?.SetOpen(false);
e.Handled = true;
};

_box = new Border
{
Child = new Panel { Children = { _body, _close } },
Padding = new Thickness(24),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(ShellTheme.RadiusLg),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
BoxShadow = new BoxShadows(new BoxShadow { OffsetY = 10, Blur = 24, Color = ShellTheme.Shadow(0.2) })
};
_box.Token(BackgroundProperty, ShellToken.Background).Token(BorderBrushProperty, ShellToken.Border);
// Clicks on the box must not reach the backdrop behind it.
_box.PointerPressed += (_, e) => e.Handled = true;

Child = new Panel { Children = { _backdrop, _box } };
SizeChanged += (_, e) => _box.Width = Math.Max(0, Math.Min(512, e.NewSize.Width - 32));
}

public async Task AnimateAsync(bool open)
{
if (open)
{
ShellMotion.Set(_backdrop, 0, "scale(1)");
ShellMotion.Set(_box, 0, "scale(0.95)");
await Task.WhenAll(
ShellMotion.To(_backdrop, 1, "scale(1)", 150, new CubicEaseOut()),
ShellMotion.To(_box, 1, "scale(1)", 150, new CubicEaseOut()));
}
else
{
await Task.WhenAll(
ShellMotion.To(_backdrop, 0, "scale(1)", 120, new CubicEaseIn()),
ShellMotion.To(_box, 0, "scale(0.95)", 120, new CubicEaseIn()));
}
}
}
15 changes: 15 additions & 0 deletions examples/Avalonia.Demo/Components/UI/DialogDescription.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Avalonia.Controls;
using Avalonia.Media;

namespace AvaloniaDemo.Components.UI;

// text-sm text-muted-foreground
public class DialogDescription : TextBlock
{
public DialogDescription()
{
FontSize = 14;
TextWrapping = TextWrapping.Wrap;
this.Token(ForegroundProperty, ShellToken.MutedForeground);
}
}
17 changes: 17 additions & 0 deletions examples/Avalonia.Demo/Components/UI/DialogFooter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;

namespace AvaloniaDemo.Components.UI;

// Actions row — flex justify-end gap-2.
public class DialogFooter : StackPanel
{
public DialogFooter()
{
Orientation = Orientation.Horizontal;
Spacing = 8;
HorizontalAlignment = HorizontalAlignment.Right;
Margin = new Thickness(0, 8, 0, 0);
}
}
14 changes: 14 additions & 0 deletions examples/Avalonia.Demo/Components/UI/DialogHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;

namespace AvaloniaDemo.Components.UI;

// Title + description stack — flex flex-col space-y-1.5. Leaves room for the close button.
public class DialogHeader : StackPanel
{
public DialogHeader()
{
Spacing = 6;
Margin = new Thickness(0, 0, 24, 0);
}
}
16 changes: 16 additions & 0 deletions examples/Avalonia.Demo/Components/UI/DialogTitle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Avalonia.Controls;
using Avalonia.Media;

namespace AvaloniaDemo.Components.UI;

// text-lg font-semibold leading-none
public class DialogTitle : TextBlock
{
public DialogTitle()
{
FontSize = 18;
FontWeight = FontWeight.SemiBold;
TextWrapping = TextWrapping.Wrap;
this.Token(ForegroundProperty, ShellToken.Foreground);
}
}
7 changes: 7 additions & 0 deletions examples/Avalonia.Demo/Components/UI/DialogTrigger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace AvaloniaDemo.Components.UI;

// Opens the enclosing Dialog. Usage: <ui:DialogTrigger><ui:Button Text="Open" /></ui:DialogTrigger>
public class DialogTrigger : ShellTriggerView
{
protected override void OnActivated() => this.FindParentOfType<Dialog>()?.SetOpen(true);
}
19 changes: 19 additions & 0 deletions examples/Avalonia.Demo/Components/UI/Drawer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Avalonia;

namespace AvaloniaDemo.Components.UI;

// Edge drawer (defaults to bottom, like vaul). Declare it anywhere:
// <ui:Drawer x:Name="MyDrawer"><ui:DrawerContent>...</ui:DrawerContent></ui:Drawer>
public class Drawer : ShellOverlayHost
{
public static readonly StyledProperty<DrawerSide> SideProperty =
AvaloniaProperty.Register<Drawer, DrawerSide>(nameof(Side), DrawerSide.Bottom);

public DrawerSide Side
{
get => GetValue(SideProperty);
set => SetValue(SideProperty, value);
}
}

public enum DrawerSide { Left, Right, Top, Bottom }
Loading
Loading