From 7250bb854f87060020a7a788cd471b5cb9a51f85 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Fri, 16 Oct 2026 16:30:59 +0200 Subject: [PATCH 1/2] feat(cli): blocks, numbered presets like ShellUI's dashboard-01 Blocks are ready-made screens built from components. `add bottom-nav-01` writes one file the user owns to Components/Blocks (namespace .Components.Blocks, next to Components/UI or Components/Native) and installs the components it uses. - bottom-nav-01: app shell, tab bar with a raised Pay button and an avatar tab (wallet app) - bottom-nav-02: Material 3 tab bar - bottom-nav-03: floating tab bar that hides on scroll, active label only - bottom-nav-04: compact capsule, the active tab widens with its label - bottom-nav-05: the active tab rises into a circle; hides on scroll - edge-menu-01: a page with a solid tool dock on its right edge The bottom-nav blocks expose Pages (one view per tab Value), Nav and Show(value); 01 adds ActionClicked; edge-menu-01 exposes Page, Menu and ToolPicked. Code-built views (no XAML), MAUI and Avalonia. - ComponentCategory.Block; block templates install to ../Blocks/.cs - sync-templates.py reads Components/Blocks in both demos - Both demos show every block in a phone frame - BlockTests (registry, install path, namespaces with Components/UI and Components/Native); the headless smoke test renders the blocks - Docs: COMPONENTS.md "Blocks", CLI README, roadmap --- docs/COMPONENTS.md | 34 ++++ docs/COMPONENTS_ROADMAP.md | 1 + .../Components/Blocks/BottomNav01.cs | 65 ++++++++ .../Components/Blocks/BottomNav02.cs | 58 +++++++ .../Components/Blocks/BottomNav03.cs | 61 +++++++ .../Components/Blocks/BottomNav04.cs | 61 +++++++ .../Components/Blocks/BottomNav05.cs | 61 +++++++ .../Components/Blocks/EdgeMenu01.cs | 53 ++++++ examples/Avalonia.Demo/MainWindow.axaml | 42 +++++ .../Components/Blocks/BottomNav01.cs | 63 ++++++++ .../Components/Blocks/BottomNav02.cs | 56 +++++++ .../Components/Blocks/BottomNav03.cs | 59 +++++++ .../Components/Blocks/BottomNav04.cs | 59 +++++++ .../Components/Blocks/BottomNav05.cs | 59 +++++++ .../MAUI.Demo/Components/Blocks/EdgeMenu01.cs | 51 ++++++ examples/MAUI.Demo/MainPage.xaml | 43 +++++ scripts/sync-templates.py | 36 ++++- src/ShellUI.Native.CLI/README.md | 2 + .../Models/ComponentCategory.cs | 5 +- .../ComponentRegistry.cs | 10 +- .../Templates/BottomNav01Template.cs | 151 ++++++++++++++++++ .../Templates/BottomNav02Template.cs | 137 ++++++++++++++++ .../Templates/BottomNav03Template.cs | 143 +++++++++++++++++ .../Templates/BottomNav04Template.cs | 143 +++++++++++++++++ .../Templates/BottomNav05Template.cs | 143 +++++++++++++++++ .../Templates/EdgeMenu01Template.cs | 127 +++++++++++++++ .../ComponentSmokeTests.cs | 8 +- .../ShellUI.Native.Avalonia.Tests.csproj | 3 +- tests/ShellUI.Native.Tests/BlockTests.cs | 54 +++++++ .../TemplateContentTests.cs | 5 +- 30 files changed, 1780 insertions(+), 13 deletions(-) create mode 100644 examples/Avalonia.Demo/Components/Blocks/BottomNav01.cs create mode 100644 examples/Avalonia.Demo/Components/Blocks/BottomNav02.cs create mode 100644 examples/Avalonia.Demo/Components/Blocks/BottomNav03.cs create mode 100644 examples/Avalonia.Demo/Components/Blocks/BottomNav04.cs create mode 100644 examples/Avalonia.Demo/Components/Blocks/BottomNav05.cs create mode 100644 examples/Avalonia.Demo/Components/Blocks/EdgeMenu01.cs create mode 100644 examples/MAUI.Demo/Components/Blocks/BottomNav01.cs create mode 100644 examples/MAUI.Demo/Components/Blocks/BottomNav02.cs create mode 100644 examples/MAUI.Demo/Components/Blocks/BottomNav03.cs create mode 100644 examples/MAUI.Demo/Components/Blocks/BottomNav04.cs create mode 100644 examples/MAUI.Demo/Components/Blocks/BottomNav05.cs create mode 100644 examples/MAUI.Demo/Components/Blocks/EdgeMenu01.cs create mode 100644 src/ShellUI.Native.Templates/Templates/BottomNav01Template.cs create mode 100644 src/ShellUI.Native.Templates/Templates/BottomNav02Template.cs create mode 100644 src/ShellUI.Native.Templates/Templates/BottomNav03Template.cs create mode 100644 src/ShellUI.Native.Templates/Templates/BottomNav04Template.cs create mode 100644 src/ShellUI.Native.Templates/Templates/BottomNav05Template.cs create mode 100644 src/ShellUI.Native.Templates/Templates/EdgeMenu01Template.cs create mode 100644 tests/ShellUI.Native.Tests/BlockTests.cs diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index ced7ff7..532c06d 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -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 `.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 diff --git a/docs/COMPONENTS_ROADMAP.md b/docs/COMPONENTS_ROADMAP.md index 8003c19..11796d3 100644 --- a/docs/COMPONENTS_ROADMAP.md +++ b/docs/COMPONENTS_ROADMAP.md @@ -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 | diff --git a/examples/Avalonia.Demo/Components/Blocks/BottomNav01.cs b/examples/Avalonia.Demo/Components/Blocks/BottomNav01.cs new file mode 100644 index 0000000..32308b9 --- /dev/null +++ b/examples/Avalonia.Demo/Components/Blocks/BottomNav01.cs @@ -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 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 }; + } +} diff --git a/examples/Avalonia.Demo/Components/Blocks/BottomNav02.cs b/examples/Avalonia.Demo/Components/Blocks/BottomNav02.cs new file mode 100644 index 0000000..b78272a --- /dev/null +++ b/examples/Avalonia.Demo/Components/Blocks/BottomNav02.cs @@ -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 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 }; + } +} diff --git a/examples/Avalonia.Demo/Components/Blocks/BottomNav03.cs b/examples/Avalonia.Demo/Components/Blocks/BottomNav03.cs new file mode 100644 index 0000000..2451c10 --- /dev/null +++ b/examples/Avalonia.Demo/Components/Blocks/BottomNav03.cs @@ -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 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 }; + } +} diff --git a/examples/Avalonia.Demo/Components/Blocks/BottomNav04.cs b/examples/Avalonia.Demo/Components/Blocks/BottomNav04.cs new file mode 100644 index 0000000..17d0c26 --- /dev/null +++ b/examples/Avalonia.Demo/Components/Blocks/BottomNav04.cs @@ -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 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 }; + } +} diff --git a/examples/Avalonia.Demo/Components/Blocks/BottomNav05.cs b/examples/Avalonia.Demo/Components/Blocks/BottomNav05.cs new file mode 100644 index 0000000..572dc4a --- /dev/null +++ b/examples/Avalonia.Demo/Components/Blocks/BottomNav05.cs @@ -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 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 }; + } +} diff --git a/examples/Avalonia.Demo/Components/Blocks/EdgeMenu01.cs b/examples/Avalonia.Demo/Components/Blocks/EdgeMenu01.cs new file mode 100644 index 0000000..5ba1f32 --- /dev/null +++ b/examples/Avalonia.Demo/Components/Blocks/EdgeMenu01.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +using AvaloniaDemo.Components.UI; + +namespace AvaloniaDemo.Components.Blocks; + +// edge-menu-01 — a page with a dock of tools on its right edge: a solid rail that dims the page +// while open. Put your view in Page, change the tiles to your tools and handle ToolPicked. +public class EdgeMenu01 : UserControl +{ + public EdgeMenu Menu { get; } + + // The page under the menu. + public Control? Page + { + get => _page.Content as Control; + set => _page.Content = value; + } + + // The picked tile's Value. + public event EventHandler? ToolPicked; + + private readonly ContentControl _page = new(); + + public EdgeMenu01() + { + Menu = new EdgeMenu { Variant = EdgeMenuVariant.Solid, Dim = true, Position = 0.45 }; + foreach (var (icon, text) in new[] { (IconName.Settings, "Settings"), (IconName.FileText, "Notes"), (IconName.Calendar, "Calendar"), (IconName.Image, "Images"), (IconName.Camera, "Camera") }) + { + var value = text.ToLowerInvariant(); + var tile = new EdgeMenuItem { Icon = icon, Text = text, Value = value }; + tile.Clicked += (_, _) => ToolPicked?.Invoke(this, value); + Menu.Items.Add(tile); + } + Content = new Grid { Children = { _page, Menu } }; + this.Token(BackgroundProperty, ShellToken.Background); + Page = Placeholder("Notes"); + } + + 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 }; + } +} diff --git a/examples/Avalonia.Demo/MainWindow.axaml b/examples/Avalonia.Demo/MainWindow.axaml index 2d08f32..d488209 100644 --- a/examples/Avalonia.Demo/MainWindow.axaml +++ b/examples/Avalonia.Demo/MainWindow.axaml @@ -1,6 +1,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/MAUI.Demo/Components/Blocks/BottomNav01.cs b/examples/MAUI.Demo/Components/Blocks/BottomNav01.cs new file mode 100644 index 0000000..577eb58 --- /dev/null +++ b/examples/MAUI.Demo/Components/Blocks/BottomNav01.cs @@ -0,0 +1,63 @@ +using MAUI.Demo.Components.UI; + +namespace MAUI.Demo.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 partial class BottomNav01 : ContentView +{ + // One view per tab, keyed by the item's Value. Replace the placeholders with your pages. + public Dictionary 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 ContentView _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 RowDefinitionCollection(new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto)), Children = { _page, Nav } }; + Grid.SetRow(Nav, 1); + this.Token(BackgroundColorProperty, 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 View Placeholder(string title) + { + var heading = new Label { Text = title, FontSize = 22, FontAttributes = FontAttributes.Bold }; + heading.Token(Label.TextColorProperty, ShellToken.Foreground); + var stack = new VerticalStackLayout { Padding = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } }; + for (var i = 0; i < 12; i++) + { + var row = new BoxView { HeightRequest = 56, CornerRadius = 12 }; + row.Token(BoxView.ColorProperty, ShellToken.Muted); + stack.Children.Add(row); + } + return new ScrollView { Content = stack }; + } +} diff --git a/examples/MAUI.Demo/Components/Blocks/BottomNav02.cs b/examples/MAUI.Demo/Components/Blocks/BottomNav02.cs new file mode 100644 index 0000000..9f028ec --- /dev/null +++ b/examples/MAUI.Demo/Components/Blocks/BottomNav02.cs @@ -0,0 +1,56 @@ +using MAUI.Demo.Components.UI; + +namespace MAUI.Demo.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 partial class BottomNav02 : ContentView +{ + // One view per tab, keyed by the item's Value. Replace the placeholders with your pages. + public Dictionary Pages { get; } = new() + { + ["home"] = Placeholder("Home"), + ["search"] = Placeholder("Search"), + ["saved"] = Placeholder("Saved"), + ["settings"] = Placeholder("Settings"), + }; + + public BottomNav Nav { get; } + + private readonly ContentView _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 RowDefinitionCollection(new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto)), Children = { _page, Nav } }; + Grid.SetRow(Nav, 1); + this.Token(BackgroundColorProperty, 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 View Placeholder(string title) + { + var heading = new Label { Text = title, FontSize = 22, FontAttributes = FontAttributes.Bold }; + heading.Token(Label.TextColorProperty, ShellToken.Foreground); + var stack = new VerticalStackLayout { Padding = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } }; + for (var i = 0; i < 12; i++) + { + var row = new BoxView { HeightRequest = 56, CornerRadius = 12 }; + row.Token(BoxView.ColorProperty, ShellToken.Muted); + stack.Children.Add(row); + } + return new ScrollView { Content = stack }; + } +} diff --git a/examples/MAUI.Demo/Components/Blocks/BottomNav03.cs b/examples/MAUI.Demo/Components/Blocks/BottomNav03.cs new file mode 100644 index 0000000..e28348c --- /dev/null +++ b/examples/MAUI.Demo/Components/Blocks/BottomNav03.cs @@ -0,0 +1,59 @@ +using MAUI.Demo.Components.UI; + +namespace MAUI.Demo.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 partial class BottomNav03 : ContentView +{ + // One view per tab, keyed by the item's Value. Replace the placeholders with your pages. + public Dictionary Pages { get; } = new() + { + ["home"] = Placeholder("Home"), + ["inbox"] = Placeholder("Inbox"), + ["messages"] = Placeholder("Messages"), + ["profile"] = Placeholder("Profile"), + }; + + public BottomNav Nav { get; } + + private readonly ContentView _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(BackgroundColorProperty, 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 ScrollView; + Nav.IsHidden = false; + } + + private static View Placeholder(string title) + { + var heading = new Label { Text = title, FontSize = 22, FontAttributes = FontAttributes.Bold }; + heading.Token(Label.TextColorProperty, ShellToken.Foreground); + var stack = new VerticalStackLayout { Padding = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } }; + for (var i = 0; i < 12; i++) + { + var row = new BoxView { HeightRequest = 56, CornerRadius = 12 }; + row.Token(BoxView.ColorProperty, ShellToken.Muted); + stack.Children.Add(row); + } + return new ScrollView { Content = stack }; + } +} diff --git a/examples/MAUI.Demo/Components/Blocks/BottomNav04.cs b/examples/MAUI.Demo/Components/Blocks/BottomNav04.cs new file mode 100644 index 0000000..9b10c2b --- /dev/null +++ b/examples/MAUI.Demo/Components/Blocks/BottomNav04.cs @@ -0,0 +1,59 @@ +using MAUI.Demo.Components.UI; + +namespace MAUI.Demo.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 partial class BottomNav04 : ContentView +{ + // One view per tab, keyed by the item's Value. Replace the placeholders with your pages. + public Dictionary Pages { get; } = new() + { + ["home"] = Placeholder("Home"), + ["search"] = Placeholder("Search"), + ["saved"] = Placeholder("Saved"), + ["profile"] = Placeholder("Profile"), + }; + + public BottomNav Nav { get; } + + private readonly ContentView _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(BackgroundColorProperty, 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 ScrollView; + Nav.IsHidden = false; + } + + private static View Placeholder(string title) + { + var heading = new Label { Text = title, FontSize = 22, FontAttributes = FontAttributes.Bold }; + heading.Token(Label.TextColorProperty, ShellToken.Foreground); + var stack = new VerticalStackLayout { Padding = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } }; + for (var i = 0; i < 12; i++) + { + var row = new BoxView { HeightRequest = 56, CornerRadius = 12 }; + row.Token(BoxView.ColorProperty, ShellToken.Muted); + stack.Children.Add(row); + } + return new ScrollView { Content = stack }; + } +} diff --git a/examples/MAUI.Demo/Components/Blocks/BottomNav05.cs b/examples/MAUI.Demo/Components/Blocks/BottomNav05.cs new file mode 100644 index 0000000..256c90c --- /dev/null +++ b/examples/MAUI.Demo/Components/Blocks/BottomNav05.cs @@ -0,0 +1,59 @@ +using MAUI.Demo.Components.UI; + +namespace MAUI.Demo.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 partial class BottomNav05 : ContentView +{ + // One view per tab, keyed by the item's Value. Replace the placeholders with your pages. + public Dictionary Pages { get; } = new() + { + ["home"] = Placeholder("Home"), + ["search"] = Placeholder("Search"), + ["alerts"] = Placeholder("Notifications"), + ["profile"] = Placeholder("Profile"), + }; + + public BottomNav Nav { get; } + + private readonly ContentView _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(BackgroundColorProperty, 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 ScrollView; + Nav.IsHidden = false; + } + + private static View Placeholder(string title) + { + var heading = new Label { Text = title, FontSize = 22, FontAttributes = FontAttributes.Bold }; + heading.Token(Label.TextColorProperty, ShellToken.Foreground); + var stack = new VerticalStackLayout { Padding = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } }; + for (var i = 0; i < 12; i++) + { + var row = new BoxView { HeightRequest = 56, CornerRadius = 12 }; + row.Token(BoxView.ColorProperty, ShellToken.Muted); + stack.Children.Add(row); + } + return new ScrollView { Content = stack }; + } +} diff --git a/examples/MAUI.Demo/Components/Blocks/EdgeMenu01.cs b/examples/MAUI.Demo/Components/Blocks/EdgeMenu01.cs new file mode 100644 index 0000000..79cf3ed --- /dev/null +++ b/examples/MAUI.Demo/Components/Blocks/EdgeMenu01.cs @@ -0,0 +1,51 @@ +using MAUI.Demo.Components.UI; + +namespace MAUI.Demo.Components.Blocks; + +// edge-menu-01 — a page with a dock of tools on its right edge: a solid rail that dims the page +// while open. Put your view in Page, change the tiles to your tools and handle ToolPicked. +public partial class EdgeMenu01 : ContentView +{ + public EdgeMenu Menu { get; } + + // The page under the menu. + public View? Page + { + get => _page.Content as View; + set => _page.Content = value; + } + + // The picked tile's Value. + public event EventHandler? ToolPicked; + + private readonly ContentView _page = new(); + + public EdgeMenu01() + { + Menu = new EdgeMenu { Variant = EdgeMenuVariant.Solid, Dim = true, Position = 0.45 }; + foreach (var (icon, text) in new[] { (IconName.Settings, "Settings"), (IconName.FileText, "Notes"), (IconName.Calendar, "Calendar"), (IconName.Image, "Images"), (IconName.Camera, "Camera") }) + { + var value = text.ToLowerInvariant(); + var tile = new EdgeMenuItem { Icon = icon, Text = text, Value = value }; + tile.Clicked += (_, _) => ToolPicked?.Invoke(this, value); + Menu.Items.Add(tile); + } + Content = new Grid { Children = { _page, Menu } }; + this.Token(BackgroundColorProperty, ShellToken.Background); + Page = Placeholder("Notes"); + } + + private static View Placeholder(string title) + { + var heading = new Label { Text = title, FontSize = 22, FontAttributes = FontAttributes.Bold }; + heading.Token(Label.TextColorProperty, ShellToken.Foreground); + var stack = new VerticalStackLayout { Padding = new Thickness(16, 16, 16, 120), Spacing = 12, Children = { heading } }; + for (var i = 0; i < 12; i++) + { + var row = new BoxView { HeightRequest = 56, CornerRadius = 12 }; + row.Token(BoxView.ColorProperty, ShellToken.Muted); + stack.Children.Add(row); + } + return new ScrollView { Content = stack }; + } +} diff --git a/examples/MAUI.Demo/MainPage.xaml b/examples/MAUI.Demo/MainPage.xaml index 55757b8..a5db875 100644 --- a/examples/MAUI.Demo/MainPage.xaml +++ b/examples/MAUI.Demo/MainPage.xaml @@ -2,6 +2,7 @@ + +