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
34 changes: 34 additions & 0 deletions docs/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,40 @@ this, showing one category of components at a time.

---

## Blocks

Blocks are ready-made screens built from the components, numbered like ShellUI's `dashboard-01`.
`add` writes one file you own to `Components/Blocks/` (namespace `<App>.Components.Blocks`, next to
`Components/UI` or `Components/Native`) and installs the components it uses. Each block is a
plain view in C#: use it as a page's content and edit it like any other code.

| Block | What it is | Uses |
|---|---|---|
| `bottom-nav-01` | App shell: tab bar with a raised Pay button in the middle and an avatar tab (a wallet app) | bottom-nav, avatar |
| `bottom-nav-02` | App shell: Material 3 tab bar, a pill behind the active icon | bottom-nav |
| `bottom-nav-03` | App shell: floating tab bar over the page that hides while it scrolls; only the active tab is labelled | bottom-nav |
| `bottom-nav-04` | App shell: compact capsule; the active tab widens into a pill with its label | bottom-nav |
| `bottom-nav-05` | App shell: the active tab rises into a circle over the bar; the bar hides on scroll | bottom-nav |
| `edge-menu-01` | A page with a solid dock of tools on its right edge that dims the page while open | edge-menu |

```bash
shellui-native add bottom-nav-01
```

```csharp
// MAUI (Avalonia: the same, with Control instead of View)
var shell = new BottomNav01();
shell.Pages["home"] = new HomeView(); // one view per tab Value
shell.Pages["cards"] = new CardsView();
shell.ActionClicked += (_, _) => StartPayment();
Content = shell;
```

The bottom-nav blocks expose `Pages` (a view per tab `Value`), `Nav` (the `BottomNav`, to change
items or the selection; call `Show(value)` after setting `Nav.SelectedValue` from code) and, in
01, `ActionClicked`. `edge-menu-01` exposes `Page` (the view under the menu), `Menu` and
`ToolPicked` (the picked tile's `Value`).

## Adding Components

```bash
Expand Down
1 change: 1 addition & 0 deletions docs/COMPONENTS_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ Icons: `python scripts/generate-icons.py` regenerates `Icon.cs` from the ShellIc
| aspect-ratio ✅ | — | Done 2026-10-05 |
| wrap-layout ✅ | — | Done 2026-10-05 — wrapping layout used by tag-input and the demo's button rows |
| theme-toggle ✅ | P7 | Done — `ThemeToggle` component |
| **blocks** ✅ | — | Done 2026-10-16, MAUI and Avalonia — numbered presets like ShellUI's `dashboard-01`, installed to `Components/Blocks`: `bottom-nav-01`…`05`, `edge-menu-01` |
| **edge-menu** ✅ | — | Done 2026-10-16, MAUI and Avalonia — native only: a handle on a screen edge slides out a rail of tool tiles; Default / Solid, left or right, optional dimming |
| **bottom-nav** ✅ | — | Done 2026-10-16, MAUI and Avalonia — native only (ShellUI for Blazor has none): Default, Pill, Floating, Lifted and Expanding styles, badges and dots, a raised action button, avatar items, hide on scroll, the safe area under the bar |

Expand Down
65 changes: 65 additions & 0 deletions examples/Avalonia.Demo/Components/Blocks/BottomNav01.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;

using AvaloniaDemo.Components.UI;

namespace AvaloniaDemo.Components.Blocks;

// bottom-nav-01 — an app shell with a tab bar along the bottom, a raised Pay button in the middle
// and the user's avatar on the last tab, like a wallet app.
// Use it as a page's content (Content = new BottomNav01();), put your views in Pages, one per
// tab Value, and change the items to your own. Handle ActionClicked for the round button.
public class BottomNav01 : UserControl
{
// One view per tab, keyed by the item's Value. Replace the placeholders with your pages.
public Dictionary<string, Control> Pages { get; } = new()
{
["home"] = Placeholder("Home"),
["cards"] = Placeholder("Cards"),
["alerts"] = Placeholder("Alerts"),
["me"] = Placeholder("Me"),
};

public BottomNav Nav { get; }

// The round button in the middle (an action, never the selected tab).
public event EventHandler? ActionClicked;

private readonly ContentControl _page = new();

public BottomNav01()
{
var action = new BottomNavItem { Icon = IconName.Plus, Text = "Pay", Prominent = true };
action.Clicked += (_, _) => ActionClicked?.Invoke(this, EventArgs.Empty);
Nav = new BottomNav { ActiveToken = ShellToken.Primary, SelectedValue = "home" };
Nav.Items.Add(new BottomNavItem { Value = "home", Icon = IconName.House, Text = "Home" });
Nav.Items.Add(new BottomNavItem { Value = "cards", Icon = IconName.CreditCard, Text = "Cards" });
Nav.Items.Add(action);
Nav.Items.Add(new BottomNavItem { Value = "alerts", Icon = IconName.Bell, Text = "Alerts", Badge = "3" });
Nav.Items.Add(new BottomNavItem { Value = "me", IconContent = new Avatar { Size = AvatarSize.Sm, Fallback = "ME" }, Text = "Me", Dot = true });
Nav.SelectedValueChanged += (_, value) => Show(value);
Content = new Grid { RowDefinitions = new RowDefinitions("*,Auto"), Children = { _page, Nav } };
Grid.SetRow(Nav, 1);
this.Token(BackgroundProperty, ShellToken.Background);
Show(Nav.SelectedValue);
}

// Shows the page for a tab Value; call it after setting Nav.SelectedValue from code.
public void Show(string? value)
{
_page.Content = value != null && Pages.TryGetValue(value, out var page) ? page : null;
}

private static Control Placeholder(string title)
{
var heading = new TextBlock { Text = title, FontSize = 22, FontWeight = FontWeight.Bold };
heading.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
var stack = new StackPanel { Margin = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } };
for (var i = 0; i < 12; i++)
stack.Children.Add(new Border { Height = 56, CornerRadius = new CornerRadius(12) }.Token(Border.BackgroundProperty, ShellToken.Muted));
return new ScrollViewer { Content = stack };
}
}
58 changes: 58 additions & 0 deletions examples/Avalonia.Demo/Components/Blocks/BottomNav02.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;

using AvaloniaDemo.Components.UI;

namespace AvaloniaDemo.Components.Blocks;

// bottom-nav-02 — an app shell with a Material 3 tab bar: a pill grows behind the active icon.
// Use it as a page's content (Content = new BottomNav02();), put your views in Pages, one per
// tab Value, and change the items to your own.
public class BottomNav02 : UserControl
{
// One view per tab, keyed by the item's Value. Replace the placeholders with your pages.
public Dictionary<string, Control> Pages { get; } = new()
{
["home"] = Placeholder("Home"),
["search"] = Placeholder("Search"),
["saved"] = Placeholder("Saved"),
["settings"] = Placeholder("Settings"),
};

public BottomNav Nav { get; }

private readonly ContentControl _page = new();

public BottomNav02()
{
Nav = new BottomNav { Variant = BottomNavVariant.Pill, SelectedValue = "home" };
Nav.Items.Add(new BottomNavItem { Value = "home", Icon = IconName.House, Text = "Home" });
Nav.Items.Add(new BottomNavItem { Value = "search", Icon = IconName.Search, Text = "Search" });
Nav.Items.Add(new BottomNavItem { Value = "saved", Icon = IconName.Heart, Text = "Saved" });
Nav.Items.Add(new BottomNavItem { Value = "settings", Icon = IconName.Settings, Text = "Settings" });
Nav.SelectedValueChanged += (_, value) => Show(value);
Content = new Grid { RowDefinitions = new RowDefinitions("*,Auto"), Children = { _page, Nav } };
Grid.SetRow(Nav, 1);
this.Token(BackgroundProperty, ShellToken.Background);
Show(Nav.SelectedValue);
}

// Shows the page for a tab Value; call it after setting Nav.SelectedValue from code.
public void Show(string? value)
{
_page.Content = value != null && Pages.TryGetValue(value, out var page) ? page : null;
}

private static Control Placeholder(string title)
{
var heading = new TextBlock { Text = title, FontSize = 22, FontWeight = FontWeight.Bold };
heading.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
var stack = new StackPanel { Margin = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } };
for (var i = 0; i < 12; i++)
stack.Children.Add(new Border { Height = 56, CornerRadius = new CornerRadius(12) }.Token(Border.BackgroundProperty, ShellToken.Muted));
return new ScrollViewer { Content = stack };
}
}
61 changes: 61 additions & 0 deletions examples/Avalonia.Demo/Components/Blocks/BottomNav03.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;

using AvaloniaDemo.Components.UI;

namespace AvaloniaDemo.Components.Blocks;

// bottom-nav-03 — an app shell with a floating tab bar over the page that slides away while
// the page scrolls down; only the active tab shows its label.
// Use it as a page's content (Content = new BottomNav03();), put your views in Pages, one per
// tab Value, and change the items to your own.
public class BottomNav03 : UserControl
{
// One view per tab, keyed by the item's Value. Replace the placeholders with your pages.
public Dictionary<string, Control> Pages { get; } = new()
{
["home"] = Placeholder("Home"),
["inbox"] = Placeholder("Inbox"),
["messages"] = Placeholder("Messages"),
["profile"] = Placeholder("Profile"),
};

public BottomNav Nav { get; }

private readonly ContentControl _page = new();

public BottomNav03()
{
Nav = new BottomNav { Variant = BottomNavVariant.Floating, Labels = BottomNavLabels.Active, SelectedValue = "home" };
Nav.Items.Add(new BottomNavItem { Value = "home", Icon = IconName.House, Text = "Home" });
Nav.Items.Add(new BottomNavItem { Value = "inbox", Icon = IconName.Inbox, Text = "Inbox", Badge = "3" });
Nav.Items.Add(new BottomNavItem { Value = "messages", Icon = IconName.MessageCircle, Text = "Messages", Dot = true });
Nav.Items.Add(new BottomNavItem { Value = "profile", Icon = IconName.User, Text = "Profile" });
Nav.SelectedValueChanged += (_, value) => Show(value);
// The bar floats over the page, so the page can use the space it leaves when it hides.
Content = new Grid { Children = { _page, Nav } };
this.Token(BackgroundProperty, ShellToken.Background);
Show(Nav.SelectedValue);
}

// Shows the page for a tab Value; call it after setting Nav.SelectedValue from code.
public void Show(string? value)
{
_page.Content = value != null && Pages.TryGetValue(value, out var page) ? page : null;
Nav.ScrollSource = _page.Content as ScrollViewer;
Nav.IsHidden = false;
}

private static Control Placeholder(string title)
{
var heading = new TextBlock { Text = title, FontSize = 22, FontWeight = FontWeight.Bold };
heading.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
var stack = new StackPanel { Margin = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } };
for (var i = 0; i < 12; i++)
stack.Children.Add(new Border { Height = 56, CornerRadius = new CornerRadius(12) }.Token(Border.BackgroundProperty, ShellToken.Muted));
return new ScrollViewer { Content = stack };
}
}
61 changes: 61 additions & 0 deletions examples/Avalonia.Demo/Components/Blocks/BottomNav04.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;

using AvaloniaDemo.Components.UI;

namespace AvaloniaDemo.Components.Blocks;

// bottom-nav-04 — an app shell with a compact capsule of icons; the active one widens into a pill
// with its label.
// Use it as a page's content (Content = new BottomNav04();), put your views in Pages, one per
// tab Value, and change the items to your own.
public class BottomNav04 : UserControl
{
// One view per tab, keyed by the item's Value. Replace the placeholders with your pages.
public Dictionary<string, Control> Pages { get; } = new()
{
["home"] = Placeholder("Home"),
["search"] = Placeholder("Search"),
["saved"] = Placeholder("Saved"),
["profile"] = Placeholder("Profile"),
};

public BottomNav Nav { get; }

private readonly ContentControl _page = new();

public BottomNav04()
{
Nav = new BottomNav { Variant = BottomNavVariant.Expanding, SelectedValue = "home" };
Nav.Items.Add(new BottomNavItem { Value = "home", Icon = IconName.House, Text = "Home" });
Nav.Items.Add(new BottomNavItem { Value = "search", Icon = IconName.Search, Text = "Search" });
Nav.Items.Add(new BottomNavItem { Value = "saved", Icon = IconName.Heart, Text = "Saved" });
Nav.Items.Add(new BottomNavItem { Value = "profile", Icon = IconName.User, Text = "Profile" });
Nav.SelectedValueChanged += (_, value) => Show(value);
// The bar floats over the page, so the page can use the space it leaves when it hides.
Content = new Grid { Children = { _page, Nav } };
this.Token(BackgroundProperty, ShellToken.Background);
Show(Nav.SelectedValue);
}

// Shows the page for a tab Value; call it after setting Nav.SelectedValue from code.
public void Show(string? value)
{
_page.Content = value != null && Pages.TryGetValue(value, out var page) ? page : null;
Nav.ScrollSource = _page.Content as ScrollViewer;
Nav.IsHidden = false;
}

private static Control Placeholder(string title)
{
var heading = new TextBlock { Text = title, FontSize = 22, FontWeight = FontWeight.Bold };
heading.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
var stack = new StackPanel { Margin = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } };
for (var i = 0; i < 12; i++)
stack.Children.Add(new Border { Height = 56, CornerRadius = new CornerRadius(12) }.Token(Border.BackgroundProperty, ShellToken.Muted));
return new ScrollViewer { Content = stack };
}
}
61 changes: 61 additions & 0 deletions examples/Avalonia.Demo/Components/Blocks/BottomNav05.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;

using AvaloniaDemo.Components.UI;

namespace AvaloniaDemo.Components.Blocks;

// bottom-nav-05 — an app shell whose active tab rises into a circle over the bar; the bar slides
// away while the page scrolls down.
// Use it as a page's content (Content = new BottomNav05();), put your views in Pages, one per
// tab Value, and change the items to your own.
public class BottomNav05 : UserControl
{
// One view per tab, keyed by the item's Value. Replace the placeholders with your pages.
public Dictionary<string, Control> Pages { get; } = new()
{
["home"] = Placeholder("Home"),
["search"] = Placeholder("Search"),
["alerts"] = Placeholder("Notifications"),
["profile"] = Placeholder("Profile"),
};

public BottomNav Nav { get; }

private readonly ContentControl _page = new();

public BottomNav05()
{
Nav = new BottomNav { Variant = BottomNavVariant.Lifted, SelectedValue = "home" };
Nav.Items.Add(new BottomNavItem { Value = "home", Icon = IconName.House, Text = "Home" });
Nav.Items.Add(new BottomNavItem { Value = "search", Icon = IconName.Search, Text = "Search" });
Nav.Items.Add(new BottomNavItem { Value = "alerts", Icon = IconName.Bell, Text = "Notifications", Badge = "12" });
Nav.Items.Add(new BottomNavItem { Value = "profile", Icon = IconName.User, Text = "Profile" });
Nav.SelectedValueChanged += (_, value) => Show(value);
// The bar floats over the page, so the page can use the space it leaves when it hides.
Content = new Grid { Children = { _page, Nav } };
this.Token(BackgroundProperty, ShellToken.Background);
Show(Nav.SelectedValue);
}

// Shows the page for a tab Value; call it after setting Nav.SelectedValue from code.
public void Show(string? value)
{
_page.Content = value != null && Pages.TryGetValue(value, out var page) ? page : null;
Nav.ScrollSource = _page.Content as ScrollViewer;
Nav.IsHidden = false;
}

private static Control Placeholder(string title)
{
var heading = new TextBlock { Text = title, FontSize = 22, FontWeight = FontWeight.Bold };
heading.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
var stack = new StackPanel { Margin = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } };
for (var i = 0; i < 12; i++)
stack.Children.Add(new Border { Height = 56, CornerRadius = new CornerRadius(12) }.Token(Border.BackgroundProperty, ShellToken.Muted));
return new ScrollViewer { Content = stack };
}
}
Loading
Loading