(this Element element) where T : Element
+{
+ var parent = element.Parent;
+ while (parent != null)
+ {
+ if (parent is T match) return match;
+ parent = parent.Parent;
+ }
+ return null;
+}
+```
+
+### DRY / SOLID
+- **Single template per component** — one `.cs` file per logical component
+- **Shared enums/variants** — `ButtonVariants`, `AlertVariant`, etc. in `Variants/` folder
+- **Design tokens** — use `ShellTheme` or shared color constants
+- **Composition over configuration** — prefer Trigger+Content over `IsModal`/`RenderMode` flags
+- **Minimal surface** — only expose properties that map to real use cases
+
+### Clean API (per ShellUI)
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Order of Implementation (Recommended)
+
+1. **dialog** + dialog-trigger, dialog-content, dialog-header, dialog-footer, dialog-title, dialog-close
+2. **drawer** + drawer-trigger, drawer-content
+3. **tabs** + tabs-list, tabs-trigger, tabs-content
+4. **textarea**
+5. **select** + select-trigger, select-content, select-item
+6. **dropdown** + dropdown-trigger, dropdown-content, dropdown-item
+7. **slider**
+8. **skeleton**
+9. **accordion** + accordion-item, accordion-trigger, accordion-content
+10. **popover** + popover-trigger, popover-content
diff --git a/docs/PLAN.md b/docs/PLAN.md
index a0789d9..7ec579b 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -2,9 +2,9 @@
## Overview
-**ShellUI Native** is the native cross-platform extension of ShellUI, bringing the same design system and component philosophy to MAUI, WinUI, WPF, and other native platforms. Inspired by shadcn/ui's approach, ShellUI Native provides copy-and-own components for native desktop and mobile development.
+**ShellUI Native** is the native cross-platform extension of [ShellUI](https://shellui.dev/), bringing the same design system and component philosophy to MAUI, Avalonia, and other native platforms. Inspired by [shadcn/ui](https://ui.shadcn.com/)'s approach, ShellUI Native provides copy-and-own components for native desktop and mobile development.
-**ShellUI (Blazor) Reference:** This plan builds upon the successful ShellUI Blazor implementation, adapting its patterns and architecture for native platforms while maintaining design consistency.
+**[ShellUI (Blazor)](https://shellui.dev/) Reference:** This plan builds upon the successful ShellUI Blazor implementation, adapting its patterns and architecture for native platforms while maintaining design consistency.
## Vision
@@ -14,20 +14,43 @@ ShellUI Native brings the same beautiful, accessible, and customizable component
## Supported Platforms
+> **2026-07-04 roadmap revision:** Avalonia UI replaces WPF as the Phase 2 target. WPF and WinUI
+> are both Windows-only, and MAUI's desktop story (Mac Catalyst) still leaves Linux uncovered.
+> Avalonia uses a WPF-like XAML/ResourceDictionary model but compiles to Windows, macOS, *and*
+> Linux (plus experimental iOS/Android/WASM) from one codebase — implementing it once buys
+> cross-desktop coverage that WPF alone never would. WPF project detection is kept (existing
+> WPF apps still need to be recognized), but WPF is no longer an active component-authoring
+> target. See [DEVELOPMENT_PLAN.md](./DEVELOPMENT_PLAN.md) for the branch/phase breakdown.
+
### Phase 1: MAUI (Primary Focus)
-- **Target:** .NET MAUI (iOS, Android, Windows, macOS)
+- **Target:** .NET MAUI (iOS, Android, Windows, macOS) on .NET 10
- **Components:** 50+ native controls
-- **Status:** In Development
-
-### Phase 2: WinUI 3
-- **Target:** Windows 11+ native apps
+- **Status:** In Development — foundational set (~40 components) landed on
+ `feat/initial-foundation`; overlay + form completeness tracked in
+ [COMPONENTS_ROADMAP.md](./COMPONENTS_ROADMAP.md).
+
+### Phase 2: Avalonia UI
+- **Target:** Windows, macOS, Linux (desktop-first; mobile/WASM heads are stretch goals)
+- **Components:** 50+ controls, mirrored 1:1 with the MAUI set once that set stabilizes
+- **Status:** Planned — starts after MAUI hits P0+P1 parity and Template System v2 lands.
+ Supersedes the former WPF phase.
+- **Why now and not later:** the template/pattern work only needs to be proven once (on MAUI);
+ porting to Avalonia is mechanical translation of the same component contracts, and doing it
+ before the template system calcifies around MAUI-only assumptions is cheaper than retrofitting
+ it later. See "Template System v2" below — a prerequisite, not optional cleanup.
+
+### Phase 3: WinUI 3 (Conditional)
+- **Target:** Windows 11+ native apps (Fluent Design)
- **Components:** 40+ WinUI controls
-- **Status:** Planned Q2 2026
+- **Status:** Deprioritized — only pursued if there's a concrete need for Windows-11-specific
+ Fluent styling that Avalonia's Fluent theme can't satisfy. Otherwise this phase is dropped
+ and that effort folds into Avalonia.
-### Phase 3: WPF (.NET 8+)
-- **Target:** Modern WPF applications
-- **Components:** 35+ WPF controls
-- **Status:** Planned Q3 2026
+### WPF (.NET 8+) — Detection Only
+- **Target:** N/A — not an active build target
+- **Status:** `ProjectDetector` still recognizes existing WPF projects (via SDK/`UseWPF`) so
+ the CLI can report "not yet supported" instead of misbehaving, but no WPF component templates
+ are planned. Users on WPF should be pointed at Avalonia once it ships.
## Architecture
@@ -36,13 +59,29 @@ ShellUI Native brings the same beautiful, accessible, and customizable component
```
ShellUI.Native/
├── ShellUI.Native.Core/ # Shared abstractions
-├── ShellUI.Native.MAUI/ # MAUI implementation
-├── ShellUI.Native.WinUI/ # WinUI 3 implementation
-├── ShellUI.Native.WPF/ # WPF implementation
+├── ShellUI.Native.MAUI/ # MAUI reference implementation (Phase 1)
+├── ShellUI.Native.Avalonia/ # Avalonia implementation (Phase 2, planned)
+├── ShellUI.Native.WinUI/ # WinUI 3 implementation (Phase 3, conditional)
├── ShellUI.Native.CLI/ # Native CLI tool
-└── ShellUI.Native.Templates/ # Code templates
+└── ShellUI.Native.Templates/ # Code templates (per-platform, see Template System v2)
```
+Note: `ShellUI.Native.WPF/` has been removed from the planned package structure — see the
+roadmap revision above.
+
+### Template System v2 (prerequisite for Phase 2)
+
+Today [`ShellUI.Native.Templates`](../src/ShellUI.Native.Templates) stores one hardcoded C#
+string per component (e.g. [`ButtonTemplate.cs`](../src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs)),
+written directly against MAUI's `ContentView`/`BindableProperty` APIs, and `ComponentRegistry`
+maps a component name straight to that single string. There is no platform dimension anywhere
+in the lookup path. Before Avalonia templates can be added, the registry needs to key content
+by `(componentName, NativePlatform)` — e.g. `ButtonTemplate.Maui` / `ButtonTemplate.Avalonia` —
+so `ComponentRegistry.GetComponentContent` can select the right implementation for
+`config.TargetPlatform`. Until this lands, `ComponentInstaller` warns and installs MAUI code
+regardless of detected platform (see
+[`ComponentInstaller.cs`](../src/ShellUI.Native.CLI/Services/ComponentInstaller.cs)).
+
### Core Design Principles
1. **Platform Agnostic APIs** - Same component interfaces across platforms
@@ -179,14 +218,16 @@ dotnet run
## Technical Specifications
### .NET Version Support
-- **MAUI:** .NET 8.0+
-- **WinUI:** .NET 8.0+ (WinUI 3)
-- **WPF:** .NET 8.0+
+- **MAUI:** .NET 10.0 (unified across all libraries — see [global.json](../global.json))
+- **Avalonia:** .NET 10.0 (Avalonia 11+)
+- **WinUI:** .NET 10.0 (WinUI 3) — conditional phase
### Platform Requirements
-- **MAUI:** iOS 14+, Android API 21+, Windows 10 1903+
-- **WinUI:** Windows 11 (recommended), Windows 10 1903+
-- **WPF:** Windows 7 SP1+
+- **MAUI:** iOS 15+, Android API 21+, Windows 10 1809+, macOS 12+ (Mac Catalyst)
+- **Avalonia:** Windows 10 1809+, macOS 10.14+, most current Linux distros (glibc 2.27+/GTK),
+ with experimental iOS/Android/WASM heads
+- **WinUI:** Windows 11 (recommended), Windows 10 1809+ — conditional phase
+- **WPF:** Windows 7 SP1+ — existing apps only; not an active template target
### Dependencies
- **Minimal:** Only .NET runtime
@@ -194,7 +235,7 @@ dotnet run
## Related Projects
-- [ShellUI (Blazor)](https://github.com/shellui-dev/shellui) - Blazor component library
+- [ShellUI (Blazor)](https://shellui.dev/) - Blazor component library
- [shadcn/ui](https://ui.shadcn.com/) - The original inspiration
## License
diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md
index 158bccf..3564771 100644
--- a/docs/QUICKSTART.md
+++ b/docs/QUICKSTART.md
@@ -4,8 +4,8 @@ Get started with ShellUI Native in under 5 minutes.
## Prerequisites
-- .NET 8.0 SDK or later
-- A MAUI, WinUI, or WPF project
+- .NET 10.0 SDK (see [global.json](../global.json))
+- A MAUI project today; Avalonia support is landing in Phase 2 (see [DEVELOPMENT_PLAN.md](./DEVELOPMENT_PLAN.md))
## Installation
@@ -28,12 +28,15 @@ shellui-native init --yes
```
This will:
-- Detect your project type (MAUI, WinUI, or WPF)
+- Detect your project type (MAUI, Avalonia, WinUI, or WPF — see [ARCHITECTURE.md § Platform Detection](./ARCHITECTURE.md#platform-detection))
- Create the `Components/UI/` folder
- Install the `Shell.cs` utility
- Create `shellui-native.json` configuration
- Generate theme resources (MAUI)
+> If the CLI detects Avalonia/WinUI/WPF, `shellui-native add` will print a warning and still
+> install MAUI-flavored code, since per-platform templates are the Phase 1b / Phase 2 workstream.
+
### 4. Add Components
```bash
diff --git a/docs/README.md b/docs/README.md
index ab52168..c52b888 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,21 +1,35 @@
-# ShellUI Native
+
-> One design system, every platform
+```
+███████╗██╗ ██╗███████╗██╗ ██╗ ██╗ ██╗██╗ ███╗ ██╗ █████╗ ████████╗██╗██╗ ██╗███████╗
+██╔════╝██║ ██║██╔════╝██║ ██║ ██║ ██║██║ ████╗ ██║██╔══██╗╚══██╔══╝██║██║ ██║██╔════╝
+███████╗███████║█████╗ ██║ ██║ ██║ ██║██║ ██████╗ ██╔██╗ ██║███████║ ██║ ██║██║ ██║█████╗
+╚════██║██╔══██║██╔══╝ ██║ ██║ ██║ ██║██║ ╚═════╝ ██║╚██╗██║██╔══██║ ██║ ██║╚██╗ ██╔╝██╔══╝
+███████║██║ ██║███████╗███████╗███████╗╚██████╔╝██║ ██║ ╚████║██║ ██║ ██║ ██║ ╚████╔╝ ███████╗
+╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝
+```
+
+**One design system, every platform**
-ShellUI Native is the native cross-platform extension of ShellUI, bringing the same design system and component philosophy to MAUI, WinUI, WPF, and other native platforms. Inspired by shadcn/ui's approach, ShellUI Native provides copy-and-own components for native desktop and mobile development.
+[](https://www.nuget.org/packages/ShellUI.Native.CLI)
+[](../LICENSE)
+
+
+
+ShellUI Native is the native cross-platform extension of [ShellUI](https://shellui.dev/), bringing the same design system and component philosophy to MAUI, Avalonia, and other native platforms. Inspired by [shadcn/ui](https://ui.shadcn.com/)'s approach, ShellUI Native provides copy-and-own components for native desktop and mobile development.
## Features
- **Copy & Own** - Components are copied to your project, giving you full control
- **Platform Native** - Pure native controls, no web views, no CSS overhead
- **Lightweight & Fast** - No Tailwind/CSS processing, native styling baked in
-- **Consistent Design** - Same design tokens as ShellUI Blazor
+- **Consistent Design** - Same design tokens as [ShellUI Blazor](https://shellui.dev/)
- **CLI-First** - Simple command-line interface for adding components
- **Composable** - Build complex UIs from simple components
## No Tailwind Required!
-Unlike ShellUI Blazor, **ShellUI Native does not use Tailwind CSS**. Native platforms (MAUI/WinUI/WPF) use XAML styles and ResourceDictionaries, not CSS. The design tokens (colors, spacing, radii) are identical to ShellUI's Tailwind theme but implemented as native styles - making your app **lightweight and fast** with zero CSS processing overhead.
+Unlike [ShellUI Blazor](https://shellui.dev/), **ShellUI Native does not use Tailwind CSS**. Native platforms (MAUI/Avalonia/WinUI) use XAML styles and ResourceDictionaries, not CSS. The design tokens (colors, spacing, radii) are identical to ShellUI's Tailwind theme but implemented as native styles - making your app **lightweight and fast** with zero CSS processing overhead.
## Quick Start
@@ -23,7 +37,7 @@ Unlike ShellUI Blazor, **ShellUI Native does not use Tailwind CSS**. Native plat
# Install the CLI tool
dotnet tool install -g ShellUI.Native.CLI
-# Initialize in your MAUI/WinUI/WPF project
+# Initialize in your MAUI or Avalonia project
shellui-native init --yes
# Add components
@@ -37,9 +51,12 @@ shellui-native list
| Platform | Status | .NET Version |
|----------|--------|--------------|
-| .NET MAUI | Active | .NET 8.0+ (including 9, 10, etc.) |
-| WinUI 3 | Planned | .NET 8.0+ |
-| WPF | Planned | .NET 8.0+ |
+| .NET MAUI | Active (Phase 1) | .NET 10.0 |
+| Avalonia UI | Planned (Phase 2) | .NET 10.0 (Avalonia 11+) |
+| WinUI 3 | Conditional (Phase 3) | .NET 10.0 |
+
+WPF is intentionally not on this list — it's recognized for project detection only, not an
+active component target. See [PLAN.md](./PLAN.md) and [DEVELOPMENT_PLAN.md](./DEVELOPMENT_PLAN.md) for the rationale.
## Blazor MAUI Hybrid Projects
@@ -57,7 +74,7 @@ For **Blazor Hybrid** apps, you can use both libraries:
└─────────────────────────────────────────┘
```
-- **ShellUI (Blazor)** - For components inside the BlazorWebView (HTML/CSS/Razor)
+- **[ShellUI (Blazor)](https://shellui.dev/)** - For components inside the BlazorWebView (HTML/CSS/Razor)
- **ShellUI Native** - For native MAUI controls outside the WebView
They're separate rendering contexts with no conflict. Install what you need based on your app architecture.
@@ -66,8 +83,10 @@ They're separate rendering contexts with no conflict. Install what you need base
- [Getting Started](./QUICKSTART.md)
- [Component List](./COMPONENTS.md)
-- [Theming](./THEMING.md)
+- [Components Roadmap](./COMPONENTS_ROADMAP.md) — prioritized P0–P7 backlog
+- [Development Plan](./DEVELOPMENT_PLAN.md) — branch strategy & phase breakdown
- [Architecture](./ARCHITECTURE.md)
+- [Plan](./PLAN.md) — high-level strategy
## Example Usage
diff --git a/examples/MAUI.Demo/App.xaml b/examples/MAUI.Demo/App.xaml
index 6f2f8b5..da92b41 100644
--- a/examples/MAUI.Demo/App.xaml
+++ b/examples/MAUI.Demo/App.xaml
@@ -1,4 +1,4 @@
-
+
-
diff --git a/examples/MAUI.Demo/App.xaml.cs b/examples/MAUI.Demo/App.xaml.cs
index 9fd151a..cc5cce1 100644
--- a/examples/MAUI.Demo/App.xaml.cs
+++ b/examples/MAUI.Demo/App.xaml.cs
@@ -1,10 +1,16 @@
+using Microsoft.Extensions.DependencyInjection;
+
namespace MAUI.Demo;
public partial class App : Application
{
- public App()
- {
- InitializeComponent();
- MainPage = new AppShell();
- }
-}
+ public App()
+ {
+ InitializeComponent();
+ }
+
+ protected override Window CreateWindow(IActivationState? activationState)
+ {
+ return new Window(new AppShell());
+ }
+}
\ No newline at end of file
diff --git a/examples/MAUI.Demo/AppShell.xaml b/examples/MAUI.Demo/AppShell.xaml
index 2869f59..207a447 100644
--- a/examples/MAUI.Demo/AppShell.xaml
+++ b/examples/MAUI.Demo/AppShell.xaml
@@ -1,25 +1,14 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/examples/MAUI.Demo/AppShell.xaml.cs b/examples/MAUI.Demo/AppShell.xaml.cs
index d839509..32fdb08 100644
--- a/examples/MAUI.Demo/AppShell.xaml.cs
+++ b/examples/MAUI.Demo/AppShell.xaml.cs
@@ -1,9 +1,9 @@
-namespace MAUI.Demo;
+namespace MAUI.Demo;
public partial class AppShell : Shell
{
- public AppShell()
- {
- InitializeComponent();
- }
+ public AppShell()
+ {
+ InitializeComponent();
+ }
}
diff --git a/examples/MAUI.Demo/Components/UI/Alert.cs b/examples/MAUI.Demo/Components/UI/Alert.cs
new file mode 100644
index 0000000..72959c2
--- /dev/null
+++ b/examples/MAUI.Demo/Components/UI/Alert.cs
@@ -0,0 +1,159 @@
+using Microsoft.Maui.Controls.Shapes;
+
+namespace MAUI.Demo.Components.UI;
+
+// Alert component for notifications and feedback
+public partial class Alert : ContentView
+{
+ public static readonly BindableProperty TitleProperty =
+ BindableProperty.Create(nameof(Title), typeof(string), typeof(Alert),
+ string.Empty, propertyChanged: OnTitleChanged);
+
+ public static readonly BindableProperty MessageProperty =
+ BindableProperty.Create(nameof(Message), typeof(string), typeof(Alert),
+ string.Empty, propertyChanged: OnMessageChanged);
+
+ public static readonly BindableProperty VariantProperty =
+ BindableProperty.Create(nameof(Variant), typeof(AlertVariant), typeof(Alert),
+ AlertVariant.Default, propertyChanged: OnVisualPropertyChanged);
+
+ private readonly Border _container;
+ private readonly Label _titleLabel;
+ private readonly Label _messageLabel;
+ private readonly VerticalStackLayout _contentStack;
+
+ public string Title
+ {
+ get => (string)GetValue(TitleProperty);
+ set => SetValue(TitleProperty, value);
+ }
+
+ public string Message
+ {
+ get => (string)GetValue(MessageProperty);
+ set => SetValue(MessageProperty, value);
+ }
+
+ public AlertVariant Variant
+ {
+ get => (AlertVariant)GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public Alert()
+ {
+ _titleLabel = new Label
+ {
+ FontSize = 16,
+ FontAttributes = FontAttributes.Bold,
+ Margin = new Thickness(0, 0, 0, 4)
+ };
+
+ _messageLabel = new Label
+ {
+ FontSize = 14
+ };
+
+ _contentStack = new VerticalStackLayout
+ {
+ Spacing = 0,
+ Padding = new Thickness(16),
+ Children = { _titleLabel, _messageLabel }
+ };
+
+ _container = new Border
+ {
+ Content = _contentStack,
+ Padding = new Thickness(0),
+ StrokeThickness = 1,
+ StrokeShape = new RoundRectangle { CornerRadius = 6 }
+ };
+
+ Content = _container;
+ UpdateVisualState();
+ }
+
+ private static void OnTitleChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Alert alert)
+ {
+ var text = newValue as string ?? string.Empty;
+ alert._titleLabel.Text = text;
+ alert._titleLabel.IsVisible = !string.IsNullOrEmpty(text);
+ }
+ }
+
+ private static void OnMessageChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Alert alert)
+ {
+ var text = newValue as string ?? string.Empty;
+ alert._messageLabel.Text = text;
+ alert._messageLabel.IsVisible = !string.IsNullOrEmpty(text);
+ }
+ }
+
+ private static void OnVisualPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Alert alert)
+ alert.UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ // Design tokens matching ShellUI theme - variant color schemes
+ var (bg, fg, border, titleColor) = Variant switch
+ {
+ AlertVariant.Default => (
+ Color.FromArgb("#F3F4F6"),
+ Color.FromArgb("#374151"),
+ Color.FromArgb("#E5E7EB"),
+ Color.FromArgb("#1F2937")
+ ),
+ AlertVariant.Destructive => (
+ Color.FromArgb("#FEF2F2"),
+ Color.FromArgb("#991B1B"),
+ Color.FromArgb("#FECACA"),
+ Color.FromArgb("#DC2626")
+ ),
+ AlertVariant.Success => (
+ Color.FromArgb("#F0FDF4"),
+ Color.FromArgb("#166534"),
+ Color.FromArgb("#BBF7D0"),
+ Color.FromArgb("#22C55E")
+ ),
+ AlertVariant.Warning => (
+ Color.FromArgb("#FFFBEB"),
+ Color.FromArgb("#92400E"),
+ Color.FromArgb("#FDE68A"),
+ Color.FromArgb("#F59E0B")
+ ),
+ AlertVariant.Info => (
+ Color.FromArgb("#EFF6FF"),
+ Color.FromArgb("#1E40AF"),
+ Color.FromArgb("#BFDBFE"),
+ Color.FromArgb("#3B82F6")
+ ),
+ _ => (
+ Color.FromArgb("#F3F4F6"),
+ Color.FromArgb("#374151"),
+ Color.FromArgb("#E5E7EB"),
+ Color.FromArgb("#1F2937")
+ )
+ };
+
+ _container.BackgroundColor = bg;
+ _container.Stroke = border;
+ _titleLabel.TextColor = titleColor;
+ _messageLabel.TextColor = fg;
+ }
+}
+
+public enum AlertVariant
+{
+ Default,
+ Destructive,
+ Success,
+ Warning,
+ Info
+}
diff --git a/examples/MAUI.Demo/Components/UI/Badge.cs b/examples/MAUI.Demo/Components/UI/Badge.cs
new file mode 100644
index 0000000..85add8f
--- /dev/null
+++ b/examples/MAUI.Demo/Components/UI/Badge.cs
@@ -0,0 +1,126 @@
+using Microsoft.Maui.Controls.Shapes;
+
+namespace MAUI.Demo.Components.UI;
+
+// Badge status indicator component - theme-aware
+public partial class Badge : ContentView
+{
+ public static readonly BindableProperty TextProperty =
+ BindableProperty.Create(nameof(Text), typeof(string), typeof(Badge),
+ string.Empty, propertyChanged: OnTextChanged);
+
+ public static readonly BindableProperty VariantProperty =
+ BindableProperty.Create(nameof(Variant), typeof(BadgeVariant), typeof(Badge),
+ BadgeVariant.Default, propertyChanged: OnVisualPropertyChanged);
+
+ private readonly Border _border;
+ private readonly Label _label;
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public BadgeVariant Variant
+ {
+ get => (BadgeVariant)GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public Badge()
+ {
+ _label = new Label
+ {
+ FontSize = 12,
+ FontAttributes = FontAttributes.Bold,
+ VerticalOptions = LayoutOptions.Center,
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalTextAlignment = TextAlignment.Center,
+ HorizontalTextAlignment = TextAlignment.Center
+ };
+
+ _border = new Border
+ {
+ Content = _label,
+ Padding = new Thickness(12, 4), // More padding
+ StrokeThickness = 1,
+ VerticalOptions = LayoutOptions.Center,
+ HorizontalOptions = LayoutOptions.Start
+ };
+
+ Content = _border;
+
+ // Listen for theme changes
+ if (Application.Current != null)
+ {
+ Application.Current.RequestedThemeChanged += (s, e) => UpdateVisualState();
+ }
+
+ UpdateVisualState();
+ }
+
+ private static void OnTextChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Badge badge)
+ badge._label.Text = newValue as string ?? string.Empty;
+ }
+
+ private static void OnVisualPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Badge badge)
+ badge.UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ var isDark = ShellTheme.IsDarkMode;
+
+ // Theme-aware color schemes for each variant
+ var (bg, fg, border) = Variant switch
+ {
+ BadgeVariant.Default => (
+ isDark ? Color.FromArgb("#F8FAFC") : Color.FromArgb("#0F172A"),
+ isDark ? Color.FromArgb("#0F172A") : Color.FromArgb("#F8FAFC"),
+ isDark ? Color.FromArgb("#F8FAFC") : Color.FromArgb("#0F172A")
+ ),
+ BadgeVariant.Secondary => (
+ isDark ? Color.FromArgb("#334155") : Color.FromArgb("#F1F5F9"),
+ isDark ? Color.FromArgb("#F8FAFC") : Color.FromArgb("#0F172A"),
+ isDark ? Color.FromArgb("#475569") : Color.FromArgb("#E2E8F0")
+ ),
+ BadgeVariant.Destructive => (
+ Color.FromArgb("#EF4444"),
+ Colors.White,
+ Color.FromArgb("#EF4444")
+ ),
+ BadgeVariant.Outline => (
+ Colors.Transparent,
+ isDark ? Color.FromArgb("#F8FAFC") : Color.FromArgb("#0F172A"),
+ isDark ? Color.FromArgb("#475569") : Color.FromArgb("#E2E8F0")
+ ),
+ BadgeVariant.Success => (
+ Color.FromArgb("#22C55E"),
+ Colors.White,
+ Color.FromArgb("#22C55E")
+ ),
+ BadgeVariant.Warning => (
+ Color.FromArgb("#F59E0B"),
+ Colors.White,
+ Color.FromArgb("#F59E0B")
+ ),
+ _ => (
+ isDark ? Color.FromArgb("#F8FAFC") : Color.FromArgb("#0F172A"),
+ isDark ? Color.FromArgb("#0F172A") : Color.FromArgb("#F8FAFC"),
+ isDark ? Color.FromArgb("#F8FAFC") : Color.FromArgb("#0F172A")
+ )
+ };
+
+ _border.BackgroundColor = bg;
+ _border.Stroke = border;
+ _border.StrokeShape = new RoundRectangle { CornerRadius = 9999 }; // Full rounded (pill shape)
+ _label.TextColor = fg;
+ }
+}
+
+public enum BadgeVariant { Default, Secondary, Destructive, Outline, Success, Warning }
diff --git a/examples/MAUI.Demo/Components/UI/Button.cs b/examples/MAUI.Demo/Components/UI/Button.cs
index c663d4f..4002af1 100644
--- a/examples/MAUI.Demo/Components/UI/Button.cs
+++ b/examples/MAUI.Demo/Components/UI/Button.cs
@@ -1,30 +1,43 @@
+using Microsoft.Maui.Controls.Shapes;
+using MauiIcons.Core;
+using MauiIcons.Fluent;
using MAUI.Demo.Components.UI.Variants;
namespace MAUI.Demo.Components.UI;
-// Interactive button with variant and size support
+// Interactive button with variant, size, icon support, and click animations
public partial class Button : ContentView
{
public static readonly BindableProperty VariantProperty =
- BindableProperty.Create(nameof(Variant), typeof(ButtonVariant), typeof(Button),
+ BindableProperty.Create(nameof(Variant), typeof(ButtonVariant), typeof(Button),
ButtonVariant.Default, propertyChanged: OnVisualPropertyChanged);
public static readonly BindableProperty SizeProperty =
- BindableProperty.Create(nameof(Size), typeof(ButtonSize), typeof(Button),
+ BindableProperty.Create(nameof(Size), typeof(ButtonSize), typeof(Button),
ButtonSize.Default, propertyChanged: OnVisualPropertyChanged);
public static readonly BindableProperty IsLoadingProperty =
- BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(Button),
+ BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(Button),
false, propertyChanged: OnVisualPropertyChanged);
public static readonly BindableProperty TextProperty =
- BindableProperty.Create(nameof(Text), typeof(string), typeof(Button),
+ BindableProperty.Create(nameof(Text), typeof(string), typeof(Button),
string.Empty, propertyChanged: OnTextChanged);
- private readonly Border _border;
+ public static readonly BindableProperty IconProperty =
+ BindableProperty.Create(nameof(Icon), typeof(FluentIcons?), typeof(Button),
+ null, propertyChanged: OnIconChanged);
+
+ public static readonly BindableProperty IconPositionProperty =
+ BindableProperty.Create(nameof(IconPosition), typeof(IconPosition), typeof(Button),
+ IconPosition.Left, propertyChanged: OnIconChanged);
+
private readonly ActivityIndicator _loadingIndicator;
private readonly Label _textLabel;
+ private readonly MauiIcon _iconView;
+ private readonly Border _border;
private readonly HorizontalStackLayout _contentLayout;
+ private ButtonStyle _currentStyle = new();
public ButtonVariant Variant
{
@@ -50,6 +63,18 @@ public string Text
set => SetValue(TextProperty, value);
}
+ public FluentIcons? Icon
+ {
+ get => (FluentIcons?)GetValue(IconProperty);
+ set => SetValue(IconProperty, value);
+ }
+
+ public IconPosition IconPosition
+ {
+ get => (IconPosition)GetValue(IconPositionProperty);
+ set => SetValue(IconPositionProperty, value);
+ }
+
public event EventHandler? Clicked;
public Button()
@@ -63,6 +88,14 @@ public Button()
Margin = new Thickness(0, 0, 8, 0)
};
+ _iconView = new MauiIcon
+ {
+ IconSize = 18,
+ IsVisible = false,
+ VerticalOptions = LayoutOptions.Center,
+ HorizontalOptions = LayoutOptions.Center
+ };
+
_textLabel = new Label
{
VerticalOptions = LayoutOptions.Center,
@@ -75,7 +108,8 @@ public Button()
{
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
- Children = { _loadingIndicator, _textLabel }
+ Spacing = 8,
+ Children = { _loadingIndicator, _iconView, _textLabel }
};
_border = new Border
@@ -84,19 +118,63 @@ public Button()
StrokeThickness = 0
};
- // Add tap gesture
+ // Add pointer events for hover/press states
+ var pointerGesture = new PointerGestureRecognizer();
+ pointerGesture.PointerEntered += OnPointerEntered;
+ pointerGesture.PointerExited += OnPointerExited;
+ pointerGesture.PointerPressed += OnPointerPressed;
+ pointerGesture.PointerReleased += OnPointerReleased;
+ _border.GestureRecognizers.Add(pointerGesture);
+
+ // Add tap gesture for click handling
var tapGesture = new TapGestureRecognizer();
tapGesture.Tapped += OnTapped;
_border.GestureRecognizers.Add(tapGesture);
Content = _border;
+
+ // Listen for theme changes
+ if (Application.Current != null)
+ {
+ Application.Current.RequestedThemeChanged += (s, e) => UpdateVisualState();
+ }
+
UpdateVisualState();
}
- private void OnTapped(object? sender, TappedEventArgs e)
+ private void OnPointerEntered(object? sender, PointerEventArgs e)
{
if (!IsLoading && IsEnabled)
{
+ _border.Opacity = 0.9;
+ }
+ }
+
+ private void OnPointerExited(object? sender, PointerEventArgs e)
+ {
+ _border.Opacity = 1.0;
+ _border.Scale = 1.0;
+ }
+
+ private async void OnPointerPressed(object? sender, PointerEventArgs e)
+ {
+ if (!IsLoading && IsEnabled)
+ {
+ await _border.ScaleToAsync(0.96, 50, Easing.CubicOut);
+ }
+ }
+
+ private async void OnPointerReleased(object? sender, PointerEventArgs e)
+ {
+ await _border.ScaleToAsync(1.0, 100, Easing.CubicOut);
+ }
+
+ private async void OnTapped(object? sender, TappedEventArgs e)
+ {
+ if (!IsLoading && IsEnabled)
+ {
+ await _border.ScaleToAsync(0.95, 50, Easing.CubicOut);
+ await _border.ScaleToAsync(1.0, 100, Easing.CubicOut);
Clicked?.Invoke(this, EventArgs.Empty);
}
}
@@ -113,26 +191,68 @@ private static void OnTextChanged(BindableObject bindable, object oldValue, obje
button._textLabel.Text = newValue as string ?? string.Empty;
}
- private void UpdateVisualState()
+ private static void OnIconChanged(BindableObject bindable, object oldValue, object newValue)
{
- var style = ButtonVariants.GetStyle(Variant, Size);
+ if (bindable is Button button)
+ button.UpdateIconState();
+ }
- _border.BackgroundColor = style.BackgroundColor;
- _border.Stroke = style.BorderColor;
- _border.StrokeThickness = style.BorderThickness;
- _border.StrokeShape = new RoundRectangle { CornerRadius = style.CornerRadius };
- _border.Padding = style.Padding;
- _border.HeightRequest = style.Height;
- _border.MinimumWidthRequest = style.MinWidth;
+ private void UpdateIconState()
+ {
+ var hasIcon = Icon.HasValue;
+ _iconView.IsVisible = hasIcon && !IsLoading;
+
+ if (hasIcon)
+ {
+ _iconView.Icon = Icon!.Value;
+
+ // Reorder children based on icon position
+ _contentLayout.Children.Clear();
+ _contentLayout.Children.Add(_loadingIndicator);
+
+ if (IconPosition == IconPosition.Left)
+ {
+ _contentLayout.Children.Add(_iconView);
+ _contentLayout.Children.Add(_textLabel);
+ }
+ else
+ {
+ _contentLayout.Children.Add(_textLabel);
+ _contentLayout.Children.Add(_iconView);
+ }
+ }
+ }
- _textLabel.TextColor = style.TextColor;
- _textLabel.FontSize = style.FontSize;
+ private void UpdateVisualState()
+ {
+ _currentStyle = ButtonVariants.GetStyle(Variant, Size);
+
+ _border.BackgroundColor = _currentStyle.BackgroundColor;
+ _border.Stroke = _currentStyle.BorderColor;
+ _border.StrokeThickness = _currentStyle.BorderThickness;
+ _border.StrokeShape = new RoundRectangle { CornerRadius = _currentStyle.CornerRadius };
+ _border.Padding = _currentStyle.Padding;
+ _border.HeightRequest = _currentStyle.Height;
+ _border.MinimumWidthRequest = _currentStyle.MinWidth;
+
+ _textLabel.TextColor = _currentStyle.TextColor;
+ _textLabel.FontSize = _currentStyle.FontSize;
_textLabel.FontAttributes = FontAttributes.Bold;
- _loadingIndicator.Color = style.TextColor;
+ // Icon color matches text color
+ _iconView.IconColor = _currentStyle.TextColor;
+ _iconView.IconSize = _currentStyle.FontSize + 2;
+
+ _loadingIndicator.Color = _currentStyle.TextColor;
_loadingIndicator.IsVisible = IsLoading;
_loadingIndicator.IsRunning = IsLoading;
+
+ // Hide icon when loading
+ if (Icon.HasValue)
+ _iconView.IsVisible = !IsLoading;
Opacity = IsEnabled ? 1.0 : 0.5;
}
}
+
+public enum IconPosition { Left, Right }
diff --git a/examples/MAUI.Demo/Components/UI/Card.cs b/examples/MAUI.Demo/Components/UI/Card.cs
new file mode 100644
index 0000000..85174ae
--- /dev/null
+++ b/examples/MAUI.Demo/Components/UI/Card.cs
@@ -0,0 +1,178 @@
+using Microsoft.Maui.Controls.Shapes;
+
+namespace MAUI.Demo.Components.UI;
+
+// Card - A container component with header, content, footer slots
+// Usage:
+[ContentProperty(nameof(CardContent))]
+public partial class Card : ContentView
+{
+ public static readonly BindableProperty VariantProperty =
+ BindableProperty.Create(nameof(Variant), typeof(CardVariant), typeof(Card),
+ CardVariant.Default, propertyChanged: OnVisualChanged);
+
+ public static readonly BindableProperty IsPressableProperty =
+ BindableProperty.Create(nameof(IsPressable), typeof(bool), typeof(Card), false);
+
+ private readonly Border _border;
+ private readonly VerticalStackLayout _container;
+
+ public CardVariant Variant
+ {
+ get => (CardVariant)GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public bool IsPressable
+ {
+ get => (bool)GetValue(IsPressableProperty);
+ set => SetValue(IsPressableProperty, value);
+ }
+
+ public event EventHandler? Clicked;
+
+ public IList CardContent => _container.Children;
+
+ public Card()
+ {
+ _container = new VerticalStackLayout { Spacing = 0 };
+
+ _border = new Border
+ {
+ Content = _container,
+ Padding = 0
+ };
+
+ var tap = new TapGestureRecognizer();
+ tap.Tapped += (s, e) => { if (IsPressable) Clicked?.Invoke(this, EventArgs.Empty); };
+ _border.GestureRecognizers.Add(tap);
+
+ Content = _border;
+
+ if (Application.Current != null)
+ Application.Current.RequestedThemeChanged += (s, e) => UpdateVisuals();
+
+ UpdateVisuals();
+ }
+
+ private static void OnVisualChanged(BindableObject b, object o, object n) => (b as Card)?.UpdateVisuals();
+
+ private void UpdateVisuals()
+ {
+ _border.BackgroundColor = ShellTheme.BackgroundCard;
+ _border.StrokeShape = new RoundRectangle { CornerRadius = 8 };
+
+ switch (Variant)
+ {
+ case CardVariant.Default:
+ _border.Stroke = ShellTheme.Border;
+ _border.StrokeThickness = 1;
+ _border.Shadow = new Shadow { Opacity = 0 };
+ break;
+ case CardVariant.Elevated:
+ _border.Stroke = Colors.Transparent;
+ _border.StrokeThickness = 0;
+ _border.Shadow = new Shadow
+ {
+ Brush = new SolidColorBrush(Color.FromArgb("#30000000")),
+ Offset = new Point(0, 4),
+ Radius = 12,
+ Opacity = ShellTheme.IsDarkMode ? 0.4f : 0.15f
+ };
+ break;
+ }
+ }
+}
+
+// Standalone CardHeader - use ui:CardHeader to avoid XAML parsing "Card.Header" as property
+public class CardHeader : ContentView
+{
+ public static readonly BindableProperty TitleProperty =
+ BindableProperty.Create(nameof(Title), typeof(string), typeof(CardHeader), "", propertyChanged: OnTextChanged);
+
+ public static readonly BindableProperty DescriptionProperty =
+ BindableProperty.Create(nameof(Description), typeof(string), typeof(CardHeader), "", propertyChanged: OnTextChanged);
+
+ private readonly Label _title;
+ private readonly Label _description;
+
+ public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
+ public string Description { get => (string)GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); }
+
+ public CardHeader()
+ {
+ _title = new Label { FontSize = 18, FontAttributes = FontAttributes.Bold };
+ _description = new Label { FontSize = 14, IsVisible = false };
+
+ Content = new VerticalStackLayout
+ {
+ Spacing = 4,
+ Padding = new Thickness(16, 16, 16, 8),
+ Children = { _title, _description }
+ };
+
+ if (Application.Current != null)
+ Application.Current.RequestedThemeChanged += (s, e) => UpdateColors();
+ UpdateColors();
+ }
+
+ private static void OnTextChanged(BindableObject b, object o, object n)
+ {
+ if (b is CardHeader h)
+ {
+ h._title.Text = h.Title;
+ h._description.Text = h.Description;
+ h._description.IsVisible = !string.IsNullOrEmpty(h.Description);
+ }
+ }
+
+ private void UpdateColors()
+ {
+ _title.TextColor = ShellTheme.Foreground;
+ _description.TextColor = ShellTheme.ForegroundMuted;
+ }
+}
+
+// Standalone CardContent - body area (inherits ContentProperty from ContentView)
+public class CardContent : ContentView
+{
+ public CardContent()
+ {
+ Padding = new Thickness(16, 8);
+ }
+}
+
+// Standalone CardFooter - footer area with action buttons
+[ContentProperty(nameof(FooterContent))]
+public class CardFooter : ContentView
+{
+ private readonly BoxView _separator;
+ private readonly HorizontalStackLayout _content;
+
+ public IList FooterContent => _content.Children;
+
+ public CardFooter()
+ {
+ _separator = new BoxView { HeightRequest = 1, HorizontalOptions = LayoutOptions.Fill };
+ _content = new HorizontalStackLayout
+ {
+ Spacing = 8,
+ Padding = new Thickness(16, 12),
+ HorizontalOptions = LayoutOptions.End
+ };
+
+ Content = new VerticalStackLayout
+ {
+ Spacing = 0,
+ Children = { _separator, _content }
+ };
+
+ if (Application.Current != null)
+ Application.Current.RequestedThemeChanged += (s, e) => UpdateColors();
+ UpdateColors();
+ }
+
+ private void UpdateColors() => _separator.BackgroundColor = ShellTheme.Border;
+}
+
+public enum CardVariant { Default, Elevated }
diff --git a/examples/MAUI.Demo/Components/UI/Checkbox.cs b/examples/MAUI.Demo/Components/UI/Checkbox.cs
new file mode 100644
index 0000000..a834e4e
--- /dev/null
+++ b/examples/MAUI.Demo/Components/UI/Checkbox.cs
@@ -0,0 +1,149 @@
+using Microsoft.Maui.Controls.Shapes;
+
+namespace MAUI.Demo.Components.UI;
+
+// Checkbox component with label support
+public partial class Checkbox : ContentView
+{
+ public static readonly BindableProperty IsCheckedProperty =
+ BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(Checkbox),
+ false, BindingMode.TwoWay, propertyChanged: OnIsCheckedChanged);
+
+ public static readonly BindableProperty LabelProperty =
+ BindableProperty.Create(nameof(Label), typeof(string), typeof(Checkbox),
+ string.Empty, propertyChanged: OnLabelChanged);
+
+ public static readonly BindableProperty HasErrorProperty =
+ BindableProperty.Create(nameof(HasError), typeof(bool), typeof(Checkbox),
+ false, propertyChanged: OnVisualPropertyChanged);
+
+ public static new readonly BindableProperty IsEnabledProperty =
+ BindableProperty.Create(nameof(IsEnabled), typeof(bool), typeof(Checkbox),
+ true, propertyChanged: OnIsEnabledChanged);
+
+ private readonly Border _checkboxBorder;
+ private readonly Label _checkmark;
+ private readonly Label _label;
+ private readonly TapGestureRecognizer _tapGesture;
+
+ public bool IsChecked
+ {
+ get => (bool)GetValue(IsCheckedProperty);
+ set => SetValue(IsCheckedProperty, value);
+ }
+
+ public string Label
+ {
+ get => (string)GetValue(LabelProperty);
+ set => SetValue(LabelProperty, value);
+ }
+
+ public bool HasError
+ {
+ get => (bool)GetValue(HasErrorProperty);
+ set => SetValue(HasErrorProperty, value);
+ }
+
+ public new bool IsEnabled
+ {
+ get => (bool)GetValue(IsEnabledProperty);
+ set => SetValue(IsEnabledProperty, value);
+ }
+
+ public event EventHandler? CheckedChanged;
+
+ public Checkbox()
+ {
+ _checkmark = new Label
+ {
+ Text = "✓",
+ FontSize = 14,
+ TextColor = Colors.White,
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center,
+ IsVisible = false
+ };
+
+ _checkboxBorder = new Border
+ {
+ Content = _checkmark,
+ WidthRequest = 20,
+ HeightRequest = 20,
+ StrokeThickness = 2,
+ StrokeShape = new RoundRectangle { CornerRadius = 4 }
+ };
+
+ _label = new Label
+ {
+ FontSize = 14,
+ VerticalOptions = LayoutOptions.Center,
+ Margin = new Thickness(8, 0, 0, 0)
+ };
+
+ _tapGesture = new TapGestureRecognizer();
+ _tapGesture.Tapped += OnTapped;
+
+ var container = new HorizontalStackLayout
+ {
+ Spacing = 0,
+ Children = { _checkboxBorder, _label }
+ };
+
+ container.GestureRecognizers.Add(_tapGesture);
+ Content = container;
+
+ UpdateVisualState();
+ }
+
+ private void OnTapped(object? sender, EventArgs e)
+ {
+ if (IsEnabled)
+ IsChecked = !IsChecked;
+ }
+
+ private static void OnIsCheckedChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Checkbox checkbox)
+ {
+ checkbox.UpdateVisualState();
+ checkbox.CheckedChanged?.Invoke(checkbox, (bool)newValue);
+ }
+ }
+
+ private static void OnLabelChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Checkbox checkbox)
+ checkbox._label.Text = newValue as string ?? string.Empty;
+ }
+
+ private static void OnIsEnabledChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Checkbox checkbox)
+ checkbox.UpdateVisualState();
+ }
+
+ private static void OnVisualPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Checkbox checkbox)
+ checkbox.UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ // Design tokens matching ShellUI theme
+ var borderColor = HasError
+ ? Color.FromArgb("#EF4444")
+ : (IsChecked ? Color.FromArgb("#2563EB") : Color.FromArgb("#E5E7EB"));
+
+ var backgroundColor = IsChecked
+ ? Color.FromArgb("#2563EB")
+ : Colors.Transparent;
+
+ _checkboxBorder.BackgroundColor = backgroundColor;
+ _checkboxBorder.Stroke = borderColor;
+ _checkmark.IsVisible = IsChecked;
+ _label.TextColor = Color.FromArgb("#1F2937");
+ _label.Opacity = IsEnabled ? 1.0 : 0.5;
+ _checkboxBorder.Opacity = IsEnabled ? 1.0 : 0.5;
+ }
+}
diff --git a/examples/MAUI.Demo/Components/UI/DatePicker.cs b/examples/MAUI.Demo/Components/UI/DatePicker.cs
new file mode 100644
index 0000000..65faec9
--- /dev/null
+++ b/examples/MAUI.Demo/Components/UI/DatePicker.cs
@@ -0,0 +1,73 @@
+using Microsoft.Maui.Controls.Shapes;
+
+namespace MAUI.Demo.Components.UI;
+
+public partial class DatePicker : ContentView
+{
+ public static readonly BindableProperty DateProperty =
+ BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(DatePicker),
+ DateTime.Today, BindingMode.TwoWay, propertyChanged: OnDateChanged);
+
+ public static readonly BindableProperty MinimumDateProperty =
+ BindableProperty.Create(nameof(MinimumDate), typeof(DateTime?), typeof(DatePicker), null);
+
+ public static readonly BindableProperty MaximumDateProperty =
+ BindableProperty.Create(nameof(MaximumDate), typeof(DateTime?), typeof(DatePicker), null);
+
+ public DateTime Date
+ {
+ get => (DateTime)GetValue(DateProperty);
+ set => SetValue(DateProperty, value);
+ }
+
+ public DateTime? MinimumDate
+ {
+ get => (DateTime?)GetValue(MinimumDateProperty);
+ set => SetValue(MinimumDateProperty, value);
+ }
+
+ public DateTime? MaximumDate
+ {
+ get => (DateTime?)GetValue(MaximumDateProperty);
+ set => SetValue(MaximumDateProperty, value);
+ }
+
+ public event EventHandler? DateChanged;
+
+ private readonly Microsoft.Maui.Controls.DatePicker _nativePicker;
+ private readonly Border _border;
+
+ public DatePicker()
+ {
+ _nativePicker = new Microsoft.Maui.Controls.DatePicker();
+ _nativePicker.DateSelected += (s, e) =>
+ {
+ var newDate = e.NewDate ?? DateTime.Today;
+ Date = newDate;
+ DateChanged?.Invoke(this, new DateChangedEventArgs(e.OldDate, newDate));
+ };
+ _border = new Border
+ {
+ Content = _nativePicker,
+ Padding = new Thickness(12, 0),
+ HeightRequest = 40,
+ StrokeThickness = 1,
+ StrokeShape = new RoundRectangle { CornerRadius = 6 }
+ };
+ Content = _border;
+ }
+
+ protected override void OnPropertyChanged(string? propertyName = null)
+ {
+ base.OnPropertyChanged(propertyName);
+ if (propertyName == DateProperty.PropertyName) _nativePicker.Date = Date;
+ else if (propertyName == MinimumDateProperty.PropertyName) _nativePicker.MinimumDate = MinimumDate ?? DateTime.MinValue;
+ else if (propertyName == MaximumDateProperty.PropertyName) _nativePicker.MaximumDate = MaximumDate ?? DateTime.MaxValue;
+ }
+
+ private static void OnDateChanged(BindableObject b, object o, object n)
+ {
+ if (b is DatePicker dp && n is DateTime dt && dp._nativePicker.Date != dt)
+ dp._nativePicker.Date = dt;
+ }
+}
diff --git a/examples/MAUI.Demo/Components/UI/Dialog.cs b/examples/MAUI.Demo/Components/UI/Dialog.cs
new file mode 100644
index 0000000..cfa233d
--- /dev/null
+++ b/examples/MAUI.Demo/Components/UI/Dialog.cs
@@ -0,0 +1,59 @@
+namespace MAUI.Demo.Components.UI;
+
+// Modal dialog - compositional. Usage:
+public partial class Dialog : Grid
+{
+ public static readonly BindableProperty OpenProperty =
+ BindableProperty.Create(nameof(Open), typeof(bool), typeof(Dialog), false,
+ propertyChanged: (b, o, n) => (b as Dialog)?.OnOpenChanged());
+
+ public bool Open
+ {
+ get => (bool)GetValue(OpenProperty);
+ set => SetValue(OpenProperty, value);
+ }
+
+ public event EventHandler? OpenChanged;
+
+ private readonly VerticalStackLayout _triggerContainer;
+ private readonly Grid _overlayLayer;
+
+ public Dialog()
+ {
+ RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+ _triggerContainer = new VerticalStackLayout { Spacing = 0, VerticalOptions = LayoutOptions.Start };
+ _overlayLayer = new Grid { IsVisible = false, ZIndex = 1000 };
+ Children.Add(_triggerContainer);
+ Children.Add(_overlayLayer);
+ Grid.SetRow(_triggerContainer, 0);
+ Grid.SetRow(_overlayLayer, 0);
+ }
+
+ protected override void OnChildAdded(Element child)
+ {
+ base.OnChildAdded(child);
+ if (child == _triggerContainer || child == _overlayLayer) return;
+ Dispatcher.Dispatch(() =>
+ {
+ if (child is IView view)
+ {
+ Children.Remove(view);
+ if (child is DialogTrigger dt)
+ _triggerContainer.Children.Add(dt);
+ else if (child is DialogContent dc)
+ _overlayLayer.Children.Add(dc);
+ }
+ });
+ }
+
+ public void SetOpen(bool value)
+ {
+ if (Open != value) { Open = value; OnOpenChanged(); }
+ }
+
+ private void OnOpenChanged()
+ {
+ _overlayLayer.IsVisible = Open;
+ OpenChanged?.Invoke(this, Open);
+ }
+}
diff --git a/examples/MAUI.Demo/Components/UI/DialogClose.cs b/examples/MAUI.Demo/Components/UI/DialogClose.cs
new file mode 100644
index 0000000..c1a88d6
--- /dev/null
+++ b/examples/MAUI.Demo/Components/UI/DialogClose.cs
@@ -0,0 +1,11 @@
+namespace MAUI.Demo.Components.UI;
+
+public partial class DialogClose : ContentView
+{
+ public DialogClose()
+ {
+ var tap = new TapGestureRecognizer();
+ tap.Tapped += (s, e) => this.FindParentOfType