diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1d2850f..a08652b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -66,3 +66,22 @@ jobs:
- name: Build MAUI Demo (Windows)
run: dotnet build examples/MAUI.Demo/MAUI.Demo.csproj --configuration Release --framework net10.0-windows10.0.19041.0
+
+ # Avalonia is the cross-desktop target, so its demo builds on all three desktop OSes
+ build-avalonia:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Build Avalonia Demo
+ run: dotnet build examples/Avalonia.Demo/AvaloniaDemo.csproj --configuration Release
diff --git a/ShellUI.Native.slnx b/ShellUI.Native.slnx
index 0ecb332..d125114 100644
--- a/ShellUI.Native.slnx
+++ b/ShellUI.Native.slnx
@@ -6,6 +6,7 @@
+
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 07dba0c..7c223d9 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -168,33 +168,41 @@ public partial class Button : ContentView
}
```
-## Component Pattern (Avalonia, planned)
+## Component Pattern (Avalonia)
-Avalonia components will follow the `StyledProperty`/`TemplatedControl` pattern instead of
-MAUI's `BindableProperty`/`ContentView` — the same property-driven visual-state approach, just
-Avalonia's equivalent APIs:
+Same model as MAUI: one C# file per component, built in code from Avalonia primitives
+(`Border`, `Panel`, `TextBlock`, a custom `Render` for icons), with no `.axaml`. The CLI keeps
+installing one file per component, and nothing has to be registered in `App.axaml`.
+
+- **Properties** are `StyledProperty`s; class handlers or `AffectsRender` react to changes.
+- **Colors** bind to the same tokens with `control.Token(property, ShellToken.X)`, a
+ resource-observable binding. `ShellTheme` publishes the light and dark palettes as theme
+ dictionaries, so Avalonia swaps them when `RequestedThemeVariant` changes.
+- **Names match MAUI** where Avalonia allows it. The one difference so far: `Icon` uses `Kind`
+ for the icon, because every Avalonia control already has a `Name`.
+- **Usings are explicit.** `dotnet new avalonia.app` has implicit usings off, so each component
+ carries `using System;` and the rest; the demo turns them off too, so it catches a missing one.
```csharp
-public partial class Button : TemplatedControl
+public class Badge : Border
{
- public static readonly StyledProperty VariantProperty =
- AvaloniaProperty.Register(nameof(Variant), ButtonVariant.Default);
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register(nameof(Text));
- public ButtonVariant Variant
- {
- get => GetValue(VariantProperty);
- set => SetValue(VariantProperty, value);
- }
+ private readonly TextBlock _label = new() { FontSize = 12 };
- static Button()
+ public Badge()
{
- VariantProperty.Changed.AddClassHandler((button, _) => button.UpdateVisualState());
+ this.Token(BackgroundProperty, ShellToken.Primary);
+ Child = _label.Token(TextBlock.ForegroundProperty, ShellToken.PrimaryForeground);
}
}
```
-Not implemented yet — this illustrates the target shape once Template System v2 lands and
-`ShellUI.Native.Avalonia` is scaffolded.
+Components are developed in `examples/Avalonia.Demo/Components/UI/` and copied into the
+templates' `[NativePlatform.Avalonia]` entries by `scripts/sync-templates.py`. A component gets
+an Avalonia entry only after its MAUI one exists, and only if every dependency has one too
+(checked by `TemplateContentTests`).
## Versioning Strategy
diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md
index 47a380f..909c74e 100644
--- a/docs/COMPONENTS.md
+++ b/docs/COMPONENTS.md
@@ -65,6 +65,29 @@ your background so the splash-to-app transition doesn't flash the template's pur
---
+## Avalonia (in progress)
+
+Ported so far: `shell`, `icon`, `theme-toggle` and P0 (`button`, `input`, `label`, `checkbox`,
+`switch`, `card` and its parts, `separator`, `badge`, `progress`, `alert`). Not yet published to
+NuGet. In an Avalonia project, `shellui-native list` shows only these.
+
+Setup after `shellui-native init`: call `Components.UI.ShellTheme.EnsureInitialized();` in
+`App.Initialize` after `AvaloniaXamlLoader.Load(this);`, and add
+`xmlns:ui="using:YourApp.Components.UI"` to your XAML. Switch theme with
+`ShellTheme.SetTheme(ThemeVariant.Dark)` or `ShellTheme.ToggleTheme()`.
+
+Same names, properties and variants as MAUI, except:
+
+| | MAUI | Avalonia |
+|---|---|---|
+| Icon | ` ` | ` ` (every Avalonia control already has a `Name`) |
+| Input length limit | `MaxLength`, default `int.MaxValue` | `MaxLength`, `0` (default) means no limit |
+| Card children | XAML children | XAML children, collected in `Card.Items` |
+| Block button | `HorizontalOptions="Fill"` | `HorizontalAlignment="Stretch"` |
+| Focus ring | Platform focus visuals | shadcn's ring, for keyboard focus only |
+
+---
+
## Icon
Stroke icons from [ShellIcons](../../../icons/shell-icons) (Lucide 0.475.0 — the set ShellUI
diff --git a/docs/DEVELOPMENT_PLAN.md b/docs/DEVELOPMENT_PLAN.md
index 4018744..b83956b 100644
--- a/docs/DEVELOPMENT_PLAN.md
+++ b/docs/DEVELOPMENT_PLAN.md
@@ -441,13 +441,28 @@ Cross-desktop (Windows + macOS + Linux) from one XAML codebase.
### Deliverables
-- [ ] New `src/ShellUI.Native.Avalonia/` reference project (Avalonia 12.x)
-- [ ] Avalonia design-token `ResourceDictionary` mirroring [`StyleTemplates.cs`](../src/ShellUI.Native.Templates/StyleTemplates.cs)
-- [ ] Add Avalonia content to each template (P0 + P1 first), using the
- `TemplatedControl`/`StyledProperty` pattern documented in
+- [x] `examples/Avalonia.Demo/` (Avalonia 12.1.3, .NET 10, implicit usings off like the
+ Avalonia template); components are developed there, as in the MAUI demo. A separate
+ `src/ShellUI.Native.Avalonia/` project isn't needed
+- [x] Theme tokens: `ShellTheme` in the Avalonia `shell` publishes both palettes as theme
+ dictionaries (no XAML resource file to install)
+- [x] Foundation templates with Avalonia content: `shell`, `icon` (same 110 icons, `Kind`
+ instead of `Name`), `theme-toggle`; pattern in
[ARCHITECTURE.md § Component Pattern (Avalonia)](./ARCHITECTURE.md)
-- [ ] `examples/Avalonia.Demo/` reference app mirroring the MAUI demo
-- [ ] CI job that builds the Avalonia demo on all three OSes (`ubuntu-latest`, `windows-latest`, `macos-latest`)
+- [x] `scripts/sync-templates.py` writes the `[NativePlatform.Avalonia]` entries;
+ `scripts/generate-icons.py` writes both `Icon.cs` files
+- [x] CLI: `init` in an Avalonia project installs the Avalonia `Shell.cs` and prints Avalonia next
+ steps; `list` shows only components with a template for the project's platform
+- [x] CI job that builds the Avalonia demo on `ubuntu-latest`, `windows-latest` and `macos-latest`
+- [x] P0: button (+ button-variants), input, label, checkbox, switch, card family, separator, badge,
+ progress, alert. Each installs alone into a fresh `dotnet new avalonia.app` and builds with 0
+ warnings; checked in the demo in light and dark, with the keyboard focus ring and loading state
+- [ ] P1: dialog, drawer, sheet, dropdown, popover (Avalonia's overlay layer and popups)
+- [ ] Replace the generated `Icon.cs` with the `ShellIcons.Maui` / `ShellIcons.Avalonia` packages
+ once they are on NuGet. Their names already match (`IconName`, `Icon`, `Kind` on Avalonia), so
+ components change little: token tinting binds the package's `Color` (MAUI) or `Foreground`
+ (Avalonia). The CLI needs NuGet dependencies first (`add icon` runs `dotnet add package`), and
+ `scripts/generate-icons.py` goes away
### Order of implementation
diff --git a/docs/README.md b/docs/README.md
index a094598..0cd04f0 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -54,7 +54,7 @@ shellui-native list
| Platform | Status | .NET Version |
|----------|--------|--------------|
| .NET MAUI | Available (`0.1.0-alpha.1`) — Android, iOS, Mac Catalyst, Windows | .NET 10.0 |
-| Avalonia UI | Planned (Phase 2) | .NET 10.0 (Avalonia 12) |
+| Avalonia UI | In progress (Phase 2): theme, icons and P0 components; not on NuGet yet | .NET 10.0 (Avalonia 12) |
| WinUI 3 | Conditional (Phase 3) | .NET 10.0 |
WPF is intentionally not on this list — it's recognized for project detection only, not an
diff --git a/examples/Avalonia.Demo/App.axaml b/examples/Avalonia.Demo/App.axaml
new file mode 100644
index 0000000..87e3b7b
--- /dev/null
+++ b/examples/Avalonia.Demo/App.axaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/Avalonia.Demo/App.axaml.cs b/examples/Avalonia.Demo/App.axaml.cs
new file mode 100644
index 0000000..0076319
--- /dev/null
+++ b/examples/Avalonia.Demo/App.axaml.cs
@@ -0,0 +1,26 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using AvaloniaDemo.Components.UI;
+
+namespace AvaloniaDemo;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ // Publish the ShellUI theme tokens before the first window resolves {DynamicResource ShellUI*}.
+ ShellTheme.EnsureInitialized();
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/examples/Avalonia.Demo/AvaloniaDemo.csproj b/examples/Avalonia.Demo/AvaloniaDemo.csproj
new file mode 100644
index 0000000..fa533e9
--- /dev/null
+++ b/examples/Avalonia.Demo/AvaloniaDemo.csproj
@@ -0,0 +1,21 @@
+
+
+ WinExe
+ net10.0
+ enable
+
+ disable
+ app.manifest
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
diff --git a/examples/Avalonia.Demo/Components/UI/Alert.cs b/examples/Avalonia.Demo/Components/UI/Alert.cs
new file mode 100644
index 0000000..bbf878d
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Alert.cs
@@ -0,0 +1,98 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Callout with icon, title and message — rounded-lg border p-4, icon + text tinted per variant.
+public class Alert : Border
+{
+ public static readonly StyledProperty TitleProperty =
+ AvaloniaProperty.Register(nameof(Title));
+
+ public static readonly StyledProperty MessageProperty =
+ AvaloniaProperty.Register(nameof(Message));
+
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ private readonly Icon _icon;
+ private readonly TextBlock _titleLabel;
+ private readonly TextBlock _messageLabel;
+
+ static Alert()
+ {
+ TitleProperty.Changed.AddClassHandler((a, _) => a.UpdateVisualState());
+ MessageProperty.Changed.AddClassHandler((a, _) => a.UpdateVisualState());
+ VariantProperty.Changed.AddClassHandler((a, _) => a.UpdateVisualState());
+ }
+
+ public string? Title
+ {
+ get => GetValue(TitleProperty);
+ set => SetValue(TitleProperty, value);
+ }
+
+ public string? Message
+ {
+ get => GetValue(MessageProperty);
+ set => SetValue(MessageProperty, value);
+ }
+
+ public AlertVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public Alert()
+ {
+ _icon = new Icon { Size = 16, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 2, 0, 0) };
+ _titleLabel = new TextBlock { FontSize = 14, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap };
+ _messageLabel = new TextBlock { FontSize = 14, TextWrapping = TextWrapping.Wrap };
+
+ var text = new StackPanel { Spacing = 4, Children = { _titleLabel, _messageLabel } };
+ Grid.SetColumn(text, 1);
+ Child = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*"), ColumnSpacing = 12, Children = { _icon, text } };
+ Padding = new Thickness(16, 12);
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(ShellTheme.RadiusLg);
+ this.Token(BackgroundProperty, ShellToken.Card);
+ UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ var (icon, accent) = Variant switch
+ {
+ AlertVariant.Destructive => (IconName.CircleAlert, ShellToken.Destructive),
+ AlertVariant.Success => (IconName.CircleCheck, ShellToken.Success),
+ AlertVariant.Warning => (IconName.TriangleAlert, ShellToken.Warning),
+ AlertVariant.Info => (IconName.Info, ShellToken.Info),
+ _ => (IconName.Info, ShellToken.Foreground)
+ };
+
+ _icon.Kind = icon;
+ _icon.Token = accent;
+ _titleLabel.Text = Title ?? string.Empty;
+ _titleLabel.IsVisible = !string.IsNullOrEmpty(Title);
+ _messageLabel.Text = Message ?? string.Empty;
+ _messageLabel.IsVisible = !string.IsNullOrEmpty(Message);
+
+ // Tinted variants color the title; the message stays readable in the muted foreground.
+ _titleLabel.Token(TextBlock.ForegroundProperty, accent);
+ _messageLabel.Token(TextBlock.ForegroundProperty,
+ Variant == AlertVariant.Default ? ShellToken.MutedForeground : ShellToken.Foreground);
+ this.Token(BorderBrushProperty, Variant == AlertVariant.Default ? ShellToken.Border : accent);
+ }
+}
+
+public enum AlertVariant
+{
+ Default,
+ Destructive,
+ Success,
+ Warning,
+ Info
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Badge.cs b/examples/Avalonia.Demo/Components/UI/Badge.cs
new file mode 100644
index 0000000..86064c1
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Badge.cs
@@ -0,0 +1,82 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Status pill — rounded-full border px-2.5 py-0.5 text-xs font-semibold.
+public class Badge : Border
+{
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register(nameof(Text));
+
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ private readonly TextBlock _label;
+
+ static Badge()
+ {
+ TextProperty.Changed.AddClassHandler((b, e) => b._label.Text = (string?)e.NewValue ?? string.Empty);
+ VariantProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ }
+
+ public string? Text
+ {
+ get => GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public BadgeVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public Badge()
+ {
+ _label = new TextBlock
+ {
+ FontSize = 12,
+ FontWeight = FontWeight.SemiBold,
+ VerticalAlignment = VerticalAlignment.Center,
+ TextAlignment = TextAlignment.Center
+ };
+
+ Child = _label;
+ Padding = new Thickness(10, 2);
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(999);
+ HorizontalAlignment = HorizontalAlignment.Left;
+ VerticalAlignment = VerticalAlignment.Center;
+ UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ // (background, foreground, border); null background = transparent.
+ var (bg, fg, border) = Variant switch
+ {
+ BadgeVariant.Secondary => ((ShellToken?)ShellToken.Secondary, ShellToken.SecondaryForeground, ShellToken.Secondary),
+ BadgeVariant.Destructive => (ShellToken.Destructive, ShellToken.DestructiveForeground, ShellToken.Destructive),
+ BadgeVariant.Outline => (null, ShellToken.Foreground, ShellToken.Border),
+ BadgeVariant.Success => (ShellToken.Success, ShellToken.SuccessForeground, ShellToken.Success),
+ BadgeVariant.Warning => (ShellToken.Warning, ShellToken.WarningForeground, ShellToken.Warning),
+ BadgeVariant.Info => (ShellToken.Info, ShellToken.InfoForeground, ShellToken.Info),
+ _ => (ShellToken.Primary, ShellToken.PrimaryForeground, ShellToken.Primary)
+ };
+
+ if (bg.HasValue)
+ this.Token(BackgroundProperty, bg.Value);
+ else
+ {
+ this.ClearToken(BackgroundProperty);
+ Background = Brushes.Transparent;
+ }
+ this.Token(BorderBrushProperty, border);
+ _label.Token(TextBlock.ForegroundProperty, fg);
+ }
+}
+
+public enum BadgeVariant { Default, Secondary, Destructive, Outline, Success, Warning, Info }
diff --git a/examples/Avalonia.Demo/Components/UI/Button.cs b/examples/Avalonia.Demo/Components/UI/Button.cs
new file mode 100644
index 0000000..f2c21ca
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Button.cs
@@ -0,0 +1,248 @@
+using System;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Media.Transformation;
+using Avalonia.Threading;
+using AvaloniaDemo.Components.UI.Variants;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Button with ShellUI variants, sizes, optional icon and loading state.
+// Sizes to its content like an inline-flex button; set HorizontalAlignment="Stretch" for a block button.
+// Usage:
+public class Button : Border
+{
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ public static readonly StyledProperty SizeProperty =
+ AvaloniaProperty.Register(nameof(Size));
+
+ public static readonly StyledProperty IsLoadingProperty =
+ AvaloniaProperty.Register(nameof(IsLoading));
+
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register(nameof(Text));
+
+ public static readonly StyledProperty IconProperty =
+ AvaloniaProperty.Register(nameof(Icon));
+
+ public static readonly StyledProperty IconPositionProperty =
+ AvaloniaProperty.Register(nameof(IconPosition));
+
+ private readonly StackPanel _row;
+ private readonly Icon _spinner;
+ private readonly RotateTransform _spin = new();
+ private readonly Icon _icon;
+ private readonly TextBlock _label;
+ private ButtonStyle _style = new();
+ private DispatcherTimer? _spinning;
+ private bool _hovered;
+ private bool _pressed;
+
+ static Button()
+ {
+ VariantProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ SizeProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ IsLoadingProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ TextProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ IconProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ IconPositionProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ IsEnabledProperty.Changed.AddClassHandler((b, _) => b.Opacity = b.IsEnabled ? 1.0 : 0.5);
+ }
+
+ public ButtonVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public ButtonSize Size
+ {
+ get => GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ public bool IsLoading
+ {
+ get => GetValue(IsLoadingProperty);
+ set => SetValue(IsLoadingProperty, value);
+ }
+
+ public string? Text
+ {
+ get => GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public IconName Icon
+ {
+ get => GetValue(IconProperty);
+ set => SetValue(IconProperty, value);
+ }
+
+ public IconPosition IconPosition
+ {
+ get => GetValue(IconPositionProperty);
+ set => SetValue(IconPositionProperty, value);
+ }
+
+ public event EventHandler? Clicked;
+
+ public Button()
+ {
+ _spinner = new Icon { Kind = IconName.LoaderCircle, IsVisible = false, RenderTransform = _spin };
+ _icon = new Icon { IsVisible = false };
+ _label = new TextBlock { VerticalAlignment = VerticalAlignment.Center, FontWeight = FontWeight.Medium };
+ _row = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 8,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+
+ Child = _row;
+ HorizontalAlignment = HorizontalAlignment.Left;
+ VerticalAlignment = VerticalAlignment.Center;
+ Cursor = new Cursor(StandardCursorType.Hand);
+ RenderTransform = TransformOperations.Parse("scale(1)");
+ Transitions = new Transitions
+ {
+ new TransformOperationsTransition { Property = RenderTransformProperty, Duration = TimeSpan.FromMilliseconds(90), Easing = new CubicEaseOut() }
+ };
+ ShellFocus.Ring(this, this);
+ UpdateVisualState();
+ }
+
+ private bool CanClick => IsEffectivelyEnabled && !IsLoading;
+
+ // Clicks in code, as a pointer click or Enter/Space would.
+ public void Press()
+ {
+ if (CanClick) Clicked?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected override void OnPointerEntered(PointerEventArgs e)
+ {
+ base.OnPointerEntered(e);
+ _hovered = true;
+ ApplyHover();
+ }
+
+ protected override void OnPointerExited(PointerEventArgs e)
+ {
+ base.OnPointerExited(e);
+ _hovered = false;
+ ApplyHover();
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ base.OnPointerPressed(e);
+ if (!CanClick || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
+ _pressed = true;
+ RenderTransform = TransformOperations.Parse("scale(0.97)");
+ e.Handled = true;
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (!_pressed) return;
+ _pressed = false;
+ RenderTransform = TransformOperations.Parse("scale(1)");
+ // Releasing outside the button cancels the click, as with native buttons.
+ if (new Rect(Bounds.Size).Contains(e.GetPosition(this))) Press();
+ e.Handled = true;
+ }
+
+ protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ base.OnPointerCaptureLost(e);
+ _pressed = false;
+ RenderTransform = TransformOperations.Parse("scale(1)");
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+ if (e.Key is not (Key.Enter or Key.Space)) return;
+ Press();
+ e.Handled = true;
+ }
+
+ private void UpdateVisualState()
+ {
+ _style = ButtonVariants.GetStyle(Variant, Size);
+
+ Height = _style.Height;
+ Width = _style.Width;
+ Padding = _style.Padding;
+ CornerRadius = new CornerRadius(_style.CornerRadius);
+ BorderThickness = new Thickness(_style.Border.HasValue ? 1 : 0);
+ if (_style.Border.HasValue)
+ this.Token(BorderBrushProperty, _style.Border.Value);
+
+ _label.Text = Text ?? string.Empty;
+ _label.IsVisible = !string.IsNullOrEmpty(Text);
+ _label.FontSize = _style.FontSize;
+ _label.Token(TextBlock.ForegroundProperty, _style.Foreground);
+
+ _icon.Kind = Icon;
+ _icon.Size = _style.IconSize;
+ _icon.Token = _style.Foreground;
+ _icon.IsVisible = Icon != IconName.None && !IsLoading;
+
+ _spinner.Size = _style.IconSize;
+ _spinner.Token = _style.Foreground;
+ _spinner.IsVisible = IsLoading;
+ if (IsLoading) StartSpinner(); else StopSpinner();
+
+ _row.Children.Clear();
+ _row.Children.Add(_spinner);
+ if (IconPosition == IconPosition.Left) { _row.Children.Add(_icon); _row.Children.Add(_label); }
+ else { _row.Children.Add(_label); _row.Children.Add(_icon); }
+
+ ApplyHover();
+ }
+
+ private void ApplyHover()
+ {
+ var hover = _hovered && CanClick;
+ var background = hover && _style.HoverBackground.HasValue ? _style.HoverBackground : _style.Background;
+ if (background.HasValue)
+ this.Token(BackgroundProperty, background.Value);
+ else
+ {
+ // Transparent, not null, so the whole button stays hit-testable.
+ this.ClearToken(BackgroundProperty);
+ Background = Brushes.Transparent;
+ }
+ Opacity = !IsEnabled ? 0.5 : hover ? _style.HoverOpacity : 1.0;
+ _label.TextDecorations = hover && _style.UnderlineOnHover ? TextDecorations.Underline : null;
+ }
+
+ // One turn per 800 ms, advanced once per frame.
+ private void StartSpinner()
+ {
+ if (_spinning != null) return;
+ _spinning = new DispatcherTimer(TimeSpan.FromMilliseconds(16), DispatcherPriority.Render,
+ (_, _) => _spin.Angle = (_spin.Angle + 360 * 16 / 800.0) % 360);
+ _spinning.Start();
+ }
+
+ private void StopSpinner()
+ {
+ _spinning?.Stop();
+ _spinning = null;
+ _spin.Angle = 0;
+ }
+}
+
+public enum IconPosition { Left, Right }
diff --git a/examples/Avalonia.Demo/Components/UI/Card.cs b/examples/Avalonia.Demo/Components/UI/Card.cs
new file mode 100644
index 0000000..8b1c03b
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Card.cs
@@ -0,0 +1,71 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Metadata;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Card container — rounded-lg border bg-card shadow-sm.
+// The parts stack inside; XAML children go to Items.
+// Usage: ... ...
+public class Card : Border
+{
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ public static readonly StyledProperty IsPressableProperty =
+ AvaloniaProperty.Register(nameof(IsPressable));
+
+ private const double Radius = ShellTheme.RadiusLg + 4;
+
+ private readonly StackPanel _items = new();
+
+ static Card()
+ {
+ VariantProperty.Changed.AddClassHandler((c, _) => c.UpdateShadow());
+ IsPressableProperty.Changed.AddClassHandler((c, _) =>
+ c.Cursor = c.IsPressable ? new Cursor(StandardCursorType.Hand) : null);
+ }
+
+ public CardVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public bool IsPressable
+ {
+ get => GetValue(IsPressableProperty);
+ set => SetValue(IsPressableProperty, value);
+ }
+
+ public event EventHandler? Clicked;
+
+ [Content]
+ public Controls Items => _items.Children;
+
+ public Card()
+ {
+ Child = _items;
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(Radius);
+ this.Token(BackgroundProperty, ShellToken.Card).Token(BorderBrushProperty, ShellToken.Border);
+ UpdateShadow();
+ }
+
+ private void UpdateShadow() => BoxShadow = new BoxShadows(Variant == CardVariant.Elevated
+ ? new BoxShadow { OffsetY = 4, Blur = 12, Color = ShellTheme.Shadow(0.10) }
+ : new BoxShadow { OffsetY = 1, Blur = 2, Color = ShellTheme.Shadow(0.05) });
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (!IsPressable || e.InitialPressMouseButton != MouseButton.Left) return;
+ Clicked?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ }
+}
+
+public enum CardVariant { Default, Elevated }
diff --git a/examples/Avalonia.Demo/Components/UI/CardContent.cs b/examples/Avalonia.Demo/Components/UI/CardContent.cs
new file mode 100644
index 0000000..bf05ddd
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/CardContent.cs
@@ -0,0 +1,10 @@
+using Avalonia;
+using Avalonia.Controls;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Card body — p-6 pt-0.
+public class CardContent : Border
+{
+ public CardContent() => Padding = new Thickness(24, 0, 24, 24);
+}
diff --git a/examples/Avalonia.Demo/Components/UI/CardFooter.cs b/examples/Avalonia.Demo/Components/UI/CardFooter.cs
new file mode 100644
index 0000000..e2a088e
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/CardFooter.cs
@@ -0,0 +1,17 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Card footer — flex items-center p-6 pt-0; actions right-aligned.
+public class CardFooter : StackPanel
+{
+ public CardFooter()
+ {
+ Orientation = Orientation.Horizontal;
+ Spacing = 8;
+ HorizontalAlignment = HorizontalAlignment.Right;
+ Margin = new Thickness(24, 0, 24, 24);
+ }
+}
diff --git a/examples/Avalonia.Demo/Components/UI/CardHeader.cs b/examples/Avalonia.Demo/Components/UI/CardHeader.cs
new file mode 100644
index 0000000..c606952
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/CardHeader.cs
@@ -0,0 +1,54 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Card header — p-6 space-y-1.5; title font-semibold, description text-sm text-muted-foreground.
+public class CardHeader : Border
+{
+ public static readonly StyledProperty TitleProperty =
+ AvaloniaProperty.Register(nameof(Title));
+
+ public static readonly StyledProperty DescriptionProperty =
+ AvaloniaProperty.Register(nameof(Description));
+
+ private readonly TextBlock _title;
+ private readonly TextBlock _description;
+
+ static CardHeader()
+ {
+ TitleProperty.Changed.AddClassHandler((h, _) => h.UpdateText());
+ DescriptionProperty.Changed.AddClassHandler((h, _) => h.UpdateText());
+ }
+
+ public string? Title
+ {
+ get => GetValue(TitleProperty);
+ set => SetValue(TitleProperty, value);
+ }
+
+ public string? Description
+ {
+ get => GetValue(DescriptionProperty);
+ set => SetValue(DescriptionProperty, value);
+ }
+
+ public CardHeader()
+ {
+ _title = new TextBlock { FontSize = 18, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap };
+ _title.Token(TextBlock.ForegroundProperty, ShellToken.CardForeground);
+ _description = new TextBlock { FontSize = 14, TextWrapping = TextWrapping.Wrap, IsVisible = false };
+ _description.Token(TextBlock.ForegroundProperty, ShellToken.MutedForeground);
+
+ Child = new StackPanel { Spacing = 6, Children = { _title, _description } };
+ Padding = new Thickness(24, 24, 24, 16);
+ }
+
+ private void UpdateText()
+ {
+ _title.Text = Title ?? string.Empty;
+ _description.Text = Description ?? string.Empty;
+ _description.IsVisible = !string.IsNullOrEmpty(Description);
+ }
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Checkbox.cs b/examples/Avalonia.Demo/Components/UI/Checkbox.cs
new file mode 100644
index 0000000..a4a9ee5
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Checkbox.cs
@@ -0,0 +1,117 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Checkbox — h-4 w-4 rounded-sm border border-primary; checked: bg-primary + check icon.
+// The whole row (box + label) is the hit target.
+public class Checkbox : Border
+{
+ public static readonly StyledProperty IsCheckedProperty =
+ AvaloniaProperty.Register(nameof(IsChecked), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty LabelProperty =
+ AvaloniaProperty.Register(nameof(Label));
+
+ public static readonly StyledProperty HasErrorProperty =
+ AvaloniaProperty.Register(nameof(HasError));
+
+ private readonly Border _box;
+ private readonly Icon _check;
+ private readonly TextBlock _label;
+
+ static Checkbox()
+ {
+ IsCheckedProperty.Changed.AddClassHandler((c, e) =>
+ {
+ c.UpdateVisualState();
+ c.CheckedChanged?.Invoke(c, (bool)e.NewValue!);
+ });
+ LabelProperty.Changed.AddClassHandler((c, _) => c.UpdateLabel());
+ HasErrorProperty.Changed.AddClassHandler((c, _) => c.UpdateVisualState());
+ IsEnabledProperty.Changed.AddClassHandler((c, _) => c.Opacity = c.IsEnabled ? 1.0 : 0.5);
+ }
+
+ public bool IsChecked
+ {
+ get => GetValue(IsCheckedProperty);
+ set => SetValue(IsCheckedProperty, value);
+ }
+
+ public string? Label
+ {
+ get => GetValue(LabelProperty);
+ set => SetValue(LabelProperty, value);
+ }
+
+ public bool HasError
+ {
+ get => GetValue(HasErrorProperty);
+ set => SetValue(HasErrorProperty, value);
+ }
+
+ public event EventHandler? CheckedChanged;
+
+ public Checkbox()
+ {
+ _check = new Icon { Kind = IconName.Check, Size = 12, StrokeWidth = 3, Token = ShellToken.PrimaryForeground };
+ _box = new Border
+ {
+ Child = _check,
+ Width = 16,
+ Height = 16,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(ShellTheme.RadiusSm),
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ _label = new TextBlock { FontSize = 14, VerticalAlignment = VerticalAlignment.Center, IsVisible = false };
+ _label.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
+
+ Child = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, MinHeight = 24, Children = { _box, _label } };
+ Background = Brushes.Transparent;
+ HorizontalAlignment = HorizontalAlignment.Left;
+ Cursor = new Cursor(StandardCursorType.Hand);
+ ShellFocus.Ring(this, _box);
+ UpdateVisualState();
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (e.InitialPressMouseButton != MouseButton.Left || !IsEffectivelyEnabled) return;
+ IsChecked = !IsChecked;
+ e.Handled = true;
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+ if (e.Key != Key.Space) return;
+ IsChecked = !IsChecked;
+ e.Handled = true;
+ }
+
+ private void UpdateLabel()
+ {
+ _label.Text = Label ?? string.Empty;
+ _label.IsVisible = !string.IsNullOrEmpty(Label);
+ }
+
+ private void UpdateVisualState()
+ {
+ _box.Token(BorderBrushProperty, HasError ? ShellToken.Destructive : ShellToken.Primary);
+ if (IsChecked)
+ _box.Token(BackgroundProperty, ShellToken.Primary);
+ else
+ {
+ _box.ClearToken(BackgroundProperty);
+ _box.Background = Brushes.Transparent;
+ }
+ _check.IsVisible = IsChecked;
+ }
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Icon.cs b/examples/Avalonia.Demo/Components/UI/Icon.cs
new file mode 100644
index 0000000..ece85cc
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Icon.cs
@@ -0,0 +1,337 @@
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Stroke icons from ShellIcons (https://github.com/shellui-dev/shell-icons), derived from
+// Lucide 0.475.0 (ISC license) — the icon set ShellUI uses. Drawn as Avalonia geometry: no font or
+// package, crisp at any size, theme-aware. Generated by scripts/generate-icons.py; a curated set
+// of 110 icons — add names there and re-run to include more.
+// Usage: Tint with Token="MutedForeground" or Color="Red".
+// The icon is picked with Kind: every Avalonia control already has a Name.
+public enum IconName
+{
+ None,
+ Check,
+ X,
+ Plus,
+ Minus,
+ Search,
+ Filter,
+ Settings,
+ SlidersHorizontal,
+ Pencil,
+ Trash,
+ Trash2,
+ Copy,
+ Clipboard,
+ Save,
+ Download,
+ Upload,
+ Share,
+ Share2,
+ ExternalLink,
+ Link,
+ RefreshCw,
+ RotateCcw,
+ Undo2,
+ Redo2,
+ LogIn,
+ LogOut,
+ Send,
+ Paperclip,
+ ChevronDown,
+ ChevronUp,
+ ChevronLeft,
+ ChevronRight,
+ ChevronsUpDown,
+ ChevronsLeft,
+ ChevronsRight,
+ ArrowLeft,
+ ArrowRight,
+ ArrowUp,
+ ArrowDown,
+ ArrowUpRight,
+ Menu,
+ Ellipsis,
+ EllipsisVertical,
+ GripVertical,
+ House,
+ LayoutDashboard,
+ PanelLeft,
+ Info,
+ CircleAlert,
+ CircleCheck,
+ CircleX,
+ CircleHelp,
+ TriangleAlert,
+ Ban,
+ Loader,
+ LoaderCircle,
+ Bell,
+ Circle,
+ CirclePlus,
+ Dot,
+ User,
+ Users,
+ UserPlus,
+ Lock,
+ LockOpen,
+ Key,
+ Shield,
+ Eye,
+ EyeOff,
+ Mail,
+ Inbox,
+ Archive,
+ MessageCircle,
+ Phone,
+ Calendar,
+ Clock,
+ Globe,
+ MapPin,
+ Image,
+ File,
+ FileText,
+ Folder,
+ Tag,
+ Bookmark,
+ Heart,
+ Star,
+ ThumbsUp,
+ ShoppingCart,
+ CreditCard,
+ DollarSign,
+ Activity,
+ TrendingUp,
+ ChartColumn,
+ Zap,
+ Sparkles,
+ Command,
+ Terminal,
+ Code,
+ Moon,
+ Sun,
+ Monitor,
+ Smartphone,
+ Laptop,
+ Wifi,
+ Camera,
+ Mic,
+ Play,
+ Pause,
+ Volume2,
+ Lightbulb
+}
+
+public static class IconPaths
+{
+ // Absolute path data on Lucide's 24x24 grid, drawn with a 2px round stroke.
+ public static string Get(IconName name) => name switch
+ {
+ IconName.Check => "M 20 6 L 9 17 L 4 12",
+ IconName.X => "M 18 6 L 6 18 M 6 6 L 18 18",
+ IconName.Plus => "M 5 12 L 19 12 M 12 5 L 12 19",
+ IconName.Minus => "M 5 12 L 19 12",
+ IconName.Search => "M 19 11 C 19 15.418 15.418 19 11 19 C 6.582 19 3 15.418 3 11 C 3 6.582 6.582 3 11 3 C 15.418 3 19 6.582 19 11 Z M 21 21 L 16.7 16.7",
+ IconName.Filter => "M 22 3 L 2 3 L 10 12.46 L 10 19 L 14 21 L 14 12.46 L 22 3 Z",
+ IconName.Settings => "M 12.22 2 L 11.78 2 C 10.675 2 9.78 2.895 9.78 4 L 9.78 4.18 C 9.779 4.894 9.398 5.553 8.78 5.91 L 8.35 6.16 C 7.731 6.517 6.969 6.517 6.35 6.16 L 6.2 6.08 C 5.244 5.529 4.023 5.855 3.47 6.81 L 3.25 7.19 C 2.699 8.146 3.025 9.367 3.98 9.92 L 4.13 10.02 C 4.745 10.375 5.126 11.03 5.13 11.74 L 5.13 12.25 C 5.133 12.967 4.751 13.631 4.13 13.99 L 3.98 14.08 C 3.025 14.633 2.699 15.854 3.25 16.81 L 3.47 17.19 C 4.023 18.145 5.244 18.471 6.2 17.92 L 6.35 17.84 C 6.969 17.483 7.731 17.483 8.35 17.84 L 8.78 18.09 C 9.398 18.447 9.779 19.106 9.78 19.82 L 9.78 20 C 9.78 21.105 10.675 22 11.78 22 L 12.22 22 C 13.325 22 14.22 21.105 14.22 20 L 14.22 19.82 C 14.221 19.106 14.602 18.447 15.22 18.09 L 15.65 17.84 C 16.269 17.483 17.031 17.483 17.65 17.84 L 17.8 17.92 C 18.756 18.471 19.977 18.145 20.53 17.19 L 20.75 16.8 C 21.301 15.844 20.975 14.623 20.02 14.07 L 19.87 13.99 C 19.249 13.631 18.867 12.967 18.87 12.25 L 18.87 11.75 C 18.867 11.033 19.249 10.369 19.87 10.01 L 20.02 9.92 C 20.975 9.367 21.301 8.146 20.75 7.19 L 20.53 6.81 C 19.977 5.855 18.756 5.529 17.8 6.08 L 17.65 6.16 C 17.031 6.517 16.269 6.517 15.65 6.16 L 15.22 5.91 C 14.602 5.553 14.221 4.894 14.22 4.18 L 14.22 4 C 14.22 2.895 13.325 2 12.22 2 Z M 15 12 C 15 13.657 13.657 15 12 15 C 10.343 15 9 13.657 9 12 C 9 10.343 10.343 9 12 9 C 13.657 9 15 10.343 15 12 Z",
+ IconName.SlidersHorizontal => "M 21 4 L 14 4 M 10 4 L 3 4 M 21 12 L 12 12 M 8 12 L 3 12 M 21 20 L 16 20 M 12 20 L 3 20 M 14 2 L 14 6 M 8 10 L 8 14 M 16 18 L 16 22",
+ IconName.Pencil => "M 21.174 6.812 C 22.275 5.711 22.275 3.926 21.174 2.826 C 20.074 1.725 18.289 1.724 17.188 2.825 L 3.842 16.174 C 3.61 16.406 3.438 16.691 3.342 17.004 L 2.021 21.356 C 1.968 21.532 2.017 21.723 2.147 21.853 C 2.277 21.983 2.468 22.031 2.644 21.978 L 6.997 20.658 C 7.31 20.563 7.595 20.392 7.827 20.161 Z M 15 5 L 19 9",
+ IconName.Trash => "M 3 6 L 21 6 M 19 6 L 19 20 C 19 21 18 22 17 22 L 7 22 C 6 22 5 21 5 20 L 5 6 M 8 6 L 8 4 C 8 3 9 2 10 2 L 14 2 C 15 2 16 3 16 4 L 16 6",
+ IconName.Trash2 => "M 3 6 L 21 6 M 19 6 L 19 20 C 19 21 18 22 17 22 L 7 22 C 6 22 5 21 5 20 L 5 6 M 8 6 L 8 4 C 8 3 9 2 10 2 L 14 2 C 15 2 16 3 16 4 L 16 6 M 10 11 L 10 17 M 14 11 L 14 17",
+ IconName.Copy => "M 10 8 L 20 8 C 21.105 8 22 8.895 22 10 L 22 20 C 22 21.105 21.105 22 20 22 L 10 22 C 8.895 22 8 21.105 8 20 L 8 10 C 8 8.895 8.895 8 10 8 Z M 4 16 C 2.9 16 2 15.1 2 14 L 2 4 C 2 2.9 2.9 2 4 2 L 14 2 C 15.1 2 16 2.9 16 4",
+ IconName.Clipboard => "M 9 2 L 15 2 C 15.552 2 16 2.448 16 3 L 16 5 C 16 5.552 15.552 6 15 6 L 9 6 C 8.448 6 8 5.552 8 5 L 8 3 C 8 2.448 8.448 2 9 2 Z M 16 4 L 18 4 C 19.105 4 20 4.895 20 6 L 20 20 C 20 21.105 19.105 22 18 22 L 6 22 C 4.895 22 4 21.105 4 20 L 4 6 C 4 4.895 4.895 4 6 4 L 8 4",
+ IconName.Save => "M 15.2 3 C 15.728 3.008 16.231 3.223 16.6 3.6 L 20.4 7.4 C 20.777 7.769 20.992 8.272 21 8.8 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 5 C 3 3.895 3.895 3 5 3 Z M 17 21 L 17 14 C 17 13.448 16.552 13 16 13 L 8 13 C 7.448 13 7 13.448 7 14 L 7 21 M 7 3 L 7 7 C 7 7.552 7.448 8 8 8 L 15 8",
+ IconName.Download => "M 21 15 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 15 M 7 10 L 12 15 L 17 10 M 12 15 L 12 3",
+ IconName.Upload => "M 21 15 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 15 M 17 8 L 12 3 L 7 8 M 12 3 L 12 15",
+ IconName.Share => "M 4 12 L 4 20 C 4 21.105 4.895 22 6 22 L 18 22 C 19.105 22 20 21.105 20 20 L 20 12 M 16 6 L 12 2 L 8 6 M 12 2 L 12 15",
+ IconName.Share2 => "M 21 5 C 21 6.657 19.657 8 18 8 C 16.343 8 15 6.657 15 5 C 15 3.343 16.343 2 18 2 C 19.657 2 21 3.343 21 5 Z M 9 12 C 9 13.657 7.657 15 6 15 C 4.343 15 3 13.657 3 12 C 3 10.343 4.343 9 6 9 C 7.657 9 9 10.343 9 12 Z M 21 19 C 21 20.657 19.657 22 18 22 C 16.343 22 15 20.657 15 19 C 15 17.343 16.343 16 18 16 C 19.657 16 21 17.343 21 19 Z M 8.59 13.51 L 15.42 17.49 M 15.41 6.51 L 8.59 10.49",
+ IconName.ExternalLink => "M 15 3 L 21 3 L 21 9 M 10 14 L 21 3 M 18 13 L 18 19 C 18 20.105 17.105 21 16 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 8 C 3 6.895 3.895 6 5 6 L 11 6",
+ IconName.Link => "M 10 13 C 10.869 14.162 12.2 14.889 13.647 14.992 C 15.094 15.096 16.514 14.566 17.54 13.54 L 20.54 10.54 C 22.435 8.578 22.408 5.46 20.479 3.531 C 18.55 1.602 15.432 1.575 13.47 3.47 L 11.75 5.18 M 14 11 C 13.131 9.838 11.8 9.111 10.353 9.008 C 8.906 8.904 7.486 9.434 6.46 10.46 L 3.46 13.46 C 1.565 15.422 1.592 18.54 3.521 20.469 C 5.45 22.398 8.568 22.425 10.53 20.53 L 12.24 18.82",
+ IconName.RefreshCw => "M 3 12 C 3 7.029 7.029 3 12 3 C 14.516 3.009 16.931 3.991 18.74 5.74 L 21 8 M 21 3 L 21 8 L 16 8 M 21 12 C 21 16.971 16.971 21 12 21 C 9.484 20.991 7.069 20.009 5.26 18.26 L 3 16 M 8 16 L 3 16 L 3 21",
+ IconName.RotateCcw => "M 3 12 C 3 16.971 7.029 21 12 21 C 16.971 21 21 16.971 21 12 C 21 7.029 16.971 3 12 3 C 9.484 3.009 7.069 3.991 5.26 5.74 L 3 8 M 3 3 L 3 8 L 8 8",
+ IconName.Undo2 => "M 9 14 L 4 9 L 9 4 M 4 9 L 14.5 9 C 17.538 9 20 11.462 20 14.5 C 20 17.538 17.538 20 14.5 20 L 11 20",
+ IconName.Redo2 => "M 15 14 L 20 9 L 15 4 M 20 9 L 9.5 9 C 6.462 9 4 11.462 4 14.5 C 4 17.538 6.462 20 9.5 20 L 13 20",
+ IconName.LogIn => "M 15 3 L 19 3 C 20.105 3 21 3.895 21 5 L 21 19 C 21 20.105 20.105 21 19 21 L 15 21 M 10 17 L 15 12 L 10 7 M 15 12 L 3 12",
+ IconName.LogOut => "M 9 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 5 C 3 3.895 3.895 3 5 3 L 9 3 M 16 17 L 21 12 L 16 7 M 21 12 L 9 12",
+ IconName.Send => "M 14.536 21.686 C 14.614 21.88 14.804 22.005 15.013 22 C 15.222 21.994 15.405 21.86 15.473 21.662 L 21.973 2.662 C 22.038 2.481 21.993 2.279 21.857 2.143 C 21.721 2.007 21.519 1.962 21.338 2.027 L 2.338 8.527 C 2.14 8.595 2.006 8.778 2 8.987 C 1.995 9.196 2.12 9.386 2.314 9.464 L 10.244 12.644 C 10.751 12.847 11.152 13.248 11.356 13.754 Z M 21.854 2.147 L 10.914 13.086",
+ IconName.Paperclip => "M 13.234 20.252 L 21 12.3 M 16 6 L 7.586 14.586 C 6.805 15.367 6.805 16.633 7.586 17.414 C 8.367 18.195 9.633 18.195 10.414 17.414 L 18.828 8.828 C 20.39 7.266 20.39 4.734 18.828 3.172 C 17.266 1.61 14.734 1.61 13.172 3.172 L 4.757 11.757 C 2.414 14.1 2.414 17.9 4.757 20.243 C 7.1 22.586 10.9 22.586 13.243 20.243",
+ IconName.ChevronDown => "M 6 9 L 12 15 L 18 9",
+ IconName.ChevronUp => "M 18 15 L 12 9 L 6 15",
+ IconName.ChevronLeft => "M 15 18 L 9 12 L 15 6",
+ IconName.ChevronRight => "M 9 18 L 15 12 L 9 6",
+ IconName.ChevronsUpDown => "M 7 15 L 12 20 L 17 15 M 7 9 L 12 4 L 17 9",
+ IconName.ChevronsLeft => "M 11 17 L 6 12 L 11 7 M 18 17 L 13 12 L 18 7",
+ IconName.ChevronsRight => "M 6 17 L 11 12 L 6 7 M 13 17 L 18 12 L 13 7",
+ IconName.ArrowLeft => "M 12 19 L 5 12 L 12 5 M 19 12 L 5 12",
+ IconName.ArrowRight => "M 5 12 L 19 12 M 12 5 L 19 12 L 12 19",
+ IconName.ArrowUp => "M 5 12 L 12 5 L 19 12 M 12 19 L 12 5",
+ IconName.ArrowDown => "M 12 5 L 12 19 M 19 12 L 12 19 L 5 12",
+ IconName.ArrowUpRight => "M 7 7 L 17 7 L 17 17 M 7 17 L 17 7",
+ IconName.Menu => "M 4 12 L 20 12 M 4 6 L 20 6 M 4 18 L 20 18",
+ IconName.Ellipsis => "M 13 12 C 13 12.552 12.552 13 12 13 C 11.448 13 11 12.552 11 12 C 11 11.448 11.448 11 12 11 C 12.552 11 13 11.448 13 12 Z M 20 12 C 20 12.552 19.552 13 19 13 C 18.448 13 18 12.552 18 12 C 18 11.448 18.448 11 19 11 C 19.552 11 20 11.448 20 12 Z M 6 12 C 6 12.552 5.552 13 5 13 C 4.448 13 4 12.552 4 12 C 4 11.448 4.448 11 5 11 C 5.552 11 6 11.448 6 12 Z",
+ IconName.EllipsisVertical => "M 13 12 C 13 12.552 12.552 13 12 13 C 11.448 13 11 12.552 11 12 C 11 11.448 11.448 11 12 11 C 12.552 11 13 11.448 13 12 Z M 13 5 C 13 5.552 12.552 6 12 6 C 11.448 6 11 5.552 11 5 C 11 4.448 11.448 4 12 4 C 12.552 4 13 4.448 13 5 Z M 13 19 C 13 19.552 12.552 20 12 20 C 11.448 20 11 19.552 11 19 C 11 18.448 11.448 18 12 18 C 12.552 18 13 18.448 13 19 Z",
+ IconName.GripVertical => "M 10 12 C 10 12.552 9.552 13 9 13 C 8.448 13 8 12.552 8 12 C 8 11.448 8.448 11 9 11 C 9.552 11 10 11.448 10 12 Z M 10 5 C 10 5.552 9.552 6 9 6 C 8.448 6 8 5.552 8 5 C 8 4.448 8.448 4 9 4 C 9.552 4 10 4.448 10 5 Z M 10 19 C 10 19.552 9.552 20 9 20 C 8.448 20 8 19.552 8 19 C 8 18.448 8.448 18 9 18 C 9.552 18 10 18.448 10 19 Z M 16 12 C 16 12.552 15.552 13 15 13 C 14.448 13 14 12.552 14 12 C 14 11.448 14.448 11 15 11 C 15.552 11 16 11.448 16 12 Z M 16 5 C 16 5.552 15.552 6 15 6 C 14.448 6 14 5.552 14 5 C 14 4.448 14.448 4 15 4 C 15.552 4 16 4.448 16 5 Z M 16 19 C 16 19.552 15.552 20 15 20 C 14.448 20 14 19.552 14 19 C 14 18.448 14.448 18 15 18 C 15.552 18 16 18.448 16 19 Z",
+ IconName.House => "M 15 21 L 15 13 C 15 12.448 14.552 12 14 12 L 10 12 C 9.448 12 9 12.448 9 13 L 9 21 M 3 10 C 3 9.411 3.259 8.852 3.709 8.472 L 10.709 2.473 C 11.454 1.843 12.546 1.843 13.291 2.473 L 20.291 8.472 C 20.741 8.852 21 9.411 21 10 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 Z",
+ IconName.LayoutDashboard => "M 4 3 L 9 3 C 9.552 3 10 3.448 10 4 L 10 11 C 10 11.552 9.552 12 9 12 L 4 12 C 3.448 12 3 11.552 3 11 L 3 4 C 3 3.448 3.448 3 4 3 Z M 15 3 L 20 3 C 20.552 3 21 3.448 21 4 L 21 7 C 21 7.552 20.552 8 20 8 L 15 8 C 14.448 8 14 7.552 14 7 L 14 4 C 14 3.448 14.448 3 15 3 Z M 15 12 L 20 12 C 20.552 12 21 12.448 21 13 L 21 20 C 21 20.552 20.552 21 20 21 L 15 21 C 14.448 21 14 20.552 14 20 L 14 13 C 14 12.448 14.448 12 15 12 Z M 4 16 L 9 16 C 9.552 16 10 16.448 10 17 L 10 20 C 10 20.552 9.552 21 9 21 L 4 21 C 3.448 21 3 20.552 3 20 L 3 17 C 3 16.448 3.448 16 4 16 Z",
+ IconName.PanelLeft => "M 5 3 L 19 3 C 20.105 3 21 3.895 21 5 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 5 C 3 3.895 3.895 3 5 3 Z M 9 3 L 9 21",
+ IconName.Info => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 12 16 L 12 12 M 12 8 L 12.01 8",
+ IconName.CircleAlert => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 12 8 L 12 12 M 12 16 L 12.01 16",
+ IconName.CircleCheck => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 9 12 L 11 14 L 15 10",
+ IconName.CircleX => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 15 9 L 9 15 M 9 9 L 15 15",
+ IconName.CircleHelp => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 9.09 9 C 9.575 7.62 10.985 6.791 12.427 7.039 C 13.869 7.286 14.922 8.537 14.92 10 C 14.92 12 11.92 13 11.92 13 M 12 17 L 12.01 17",
+ IconName.TriangleAlert => "M 21.73 18 L 13.73 4 C 13.375 3.373 12.71 2.986 11.99 2.986 C 11.27 2.986 10.605 3.373 10.25 4 L 2.25 18 C 1.891 18.622 1.893 19.388 2.254 20.008 C 2.616 20.628 3.282 21.006 4 21 L 20 21 C 20.714 20.999 21.374 20.618 21.731 19.999 C 22.087 19.38 22.087 18.618 21.73 18 M 12 9 L 12 13 M 12 17 L 12.01 17",
+ IconName.Ban => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 4.9 4.9 L 19.1 19.1",
+ IconName.Loader => "M 12 2 L 12 6 M 16.2 7.8 L 19.1 4.9 M 18 12 L 22 12 M 16.2 16.2 L 19.1 19.1 M 12 18 L 12 22 M 4.9 19.1 L 7.8 16.2 M 2 12 L 6 12 M 4.9 4.9 L 7.8 7.8",
+ IconName.LoaderCircle => "M 21 12 C 21 15.899 18.489 19.354 14.781 20.559 C 11.073 21.764 7.01 20.444 4.719 17.29 C 2.427 14.135 2.427 9.864 4.719 6.709 C 7.011 3.555 11.073 2.235 14.781 3.44",
+ IconName.Bell => "M 10.268 21 C 10.625 21.619 11.286 22 12 22 C 12.714 22 13.375 21.619 13.732 21 M 3.262 15.326 C 2.995 15.619 2.926 16.041 3.086 16.403 C 3.245 16.766 3.604 17 4 17 L 20 17 C 20.396 17 20.755 16.767 20.915 16.405 C 21.075 16.042 21.006 15.62 20.74 15.327 C 19.41 13.956 18 12.499 18 8 C 18 4.686 15.314 2 12 2 C 8.686 2 6 4.686 6 8 C 6 12.499 4.589 13.956 3.262 15.326",
+ IconName.Circle => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z",
+ IconName.CirclePlus => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 8 12 L 16 12 M 12 8 L 12 16",
+ IconName.Dot => "M 13.1 12.1 C 13.1 12.652 12.652 13.1 12.1 13.1 C 11.548 13.1 11.1 12.652 11.1 12.1 C 11.1 11.548 11.548 11.1 12.1 11.1 C 12.652 11.1 13.1 11.548 13.1 12.1 Z",
+ IconName.User => "M 19 21 L 19 19 C 19 16.791 17.209 15 15 15 L 9 15 C 6.791 15 5 16.791 5 19 L 5 21 M 16 7 C 16 9.209 14.209 11 12 11 C 9.791 11 8 9.209 8 7 C 8 4.791 9.791 3 12 3 C 14.209 3 16 4.791 16 7 Z",
+ IconName.Users => "M 16 21 L 16 19 C 16 16.791 14.209 15 12 15 L 6 15 C 3.791 15 2 16.791 2 19 L 2 21 M 13 7 C 13 9.209 11.209 11 9 11 C 6.791 11 5 9.209 5 7 C 5 4.791 6.791 3 9 3 C 11.209 3 13 4.791 13 7 Z M 22 21 L 22 19 C 21.999 17.177 20.765 15.586 19 15.13 M 16 3.13 C 17.77 3.583 19.008 5.178 19.008 7.005 C 19.008 8.832 17.77 10.427 16 10.88",
+ IconName.UserPlus => "M 16 21 L 16 19 C 16 16.791 14.209 15 12 15 L 6 15 C 3.791 15 2 16.791 2 19 L 2 21 M 13 7 C 13 9.209 11.209 11 9 11 C 6.791 11 5 9.209 5 7 C 5 4.791 6.791 3 9 3 C 11.209 3 13 4.791 13 7 Z M 19 8 L 19 14 M 22 11 L 16 11",
+ IconName.Lock => "M 5 11 L 19 11 C 20.105 11 21 11.895 21 13 L 21 20 C 21 21.105 20.105 22 19 22 L 5 22 C 3.895 22 3 21.105 3 20 L 3 13 C 3 11.895 3.895 11 5 11 Z M 7 11 L 7 7 C 7 4.239 9.239 2 12 2 C 14.761 2 17 4.239 17 7 L 17 11",
+ IconName.LockOpen => "M 5 11 L 19 11 C 20.105 11 21 11.895 21 13 L 21 20 C 21 21.105 20.105 22 19 22 L 5 22 C 3.895 22 3 21.105 3 20 L 3 13 C 3 11.895 3.895 11 5 11 Z M 7 11 L 7 7 C 6.997 4.431 8.942 2.278 11.498 2.02 C 14.053 1.762 16.389 3.483 16.9 6",
+ IconName.Key => "M 15.5 7.5 L 17.8 9.8 C 18.189 10.181 18.811 10.181 19.2 9.8 L 21.3 7.7 C 21.681 7.311 21.681 6.689 21.3 6.3 L 19 4 M 21 2 L 11.4 11.6 M 13 15.5 C 13 18.538 10.538 21 7.5 21 C 4.462 21 2 18.538 2 15.5 C 2 12.462 4.462 10 7.5 10 C 10.538 10 13 12.462 13 15.5 Z",
+ IconName.Shield => "M 20 13 C 20 18 16.5 20.5 12.34 21.95 C 12.122 22.024 11.886 22.02 11.67 21.94 C 7.5 20.5 4 18 4 13 L 4 6 C 4 5.448 4.448 5 5 5 C 7 5 9.5 3.8 11.24 2.28 C 11.678 1.906 12.322 1.906 12.76 2.28 C 14.51 3.81 17 5 19 5 C 19.552 5 20 5.448 20 6 Z",
+ IconName.Eye => "M 2.062 12.348 C 1.979 12.123 1.979 11.877 2.062 11.652 C 3.722 7.627 7.646 5.001 12 5.001 C 16.354 5.001 20.278 7.627 21.938 11.652 C 22.021 11.877 22.021 12.123 21.938 12.348 C 20.278 16.373 16.354 18.999 12 18.999 C 7.646 18.999 3.722 16.373 2.062 12.348 M 15 12 C 15 13.657 13.657 15 12 15 C 10.343 15 9 13.657 9 12 C 9 10.343 10.343 9 12 9 C 13.657 9 15 10.343 15 12 Z",
+ IconName.EyeOff => "M 10.733 5.076 C 15.52 4.506 20.101 7.194 21.938 11.651 C 22.021 11.876 22.021 12.122 21.938 12.347 C 21.57 13.238 21.085 14.076 20.494 14.837 M 14.084 14.158 C 12.907 15.295 11.036 15.279 9.879 14.121 C 8.721 12.964 8.705 11.093 9.842 9.916 M 17.479 17.499 C 14.795 19.089 11.553 19.434 8.594 18.446 C 5.635 17.457 3.251 15.232 2.062 12.348 C 1.979 12.123 1.979 11.877 2.062 11.652 C 2.949 9.502 4.509 7.697 6.508 6.509 M 2 2 L 22 22",
+ IconName.Mail => "M 4 4 L 20 4 C 21.105 4 22 4.895 22 6 L 22 18 C 22 19.105 21.105 20 20 20 L 4 20 C 2.895 20 2 19.105 2 18 L 2 6 C 2 4.895 2.895 4 4 4 Z M 22 7 L 13.03 12.7 C 12.4 13.095 11.6 13.095 10.97 12.7 L 2 7",
+ IconName.Inbox => "M 22 12 L 16 12 L 14 15 L 10 15 L 8 12 L 2 12 M 5.45 5.11 L 2 12 L 2 18 C 2 19.105 2.895 20 4 20 L 20 20 C 21.105 20 22 19.105 22 18 L 22 12 L 18.55 5.11 C 18.212 4.43 17.519 4 16.76 4 L 7.24 4 C 6.481 4 5.788 4.43 5.45 5.11 Z",
+ IconName.Archive => "M 3 3 L 21 3 C 21.552 3 22 3.448 22 4 L 22 7 C 22 7.552 21.552 8 21 8 L 3 8 C 2.448 8 2 7.552 2 7 L 2 4 C 2 3.448 2.448 3 3 3 Z M 4 8 L 4 19 C 4 20.105 4.895 21 6 21 L 18 21 C 19.105 21 20 20.105 20 19 L 20 8 M 10 12 L 14 12",
+ IconName.MessageCircle => "M 7.9 20 C 11.843 22.023 16.672 20.874 19.282 17.292 C 21.892 13.71 21.505 8.762 18.372 5.628 C 15.238 2.495 10.29 2.108 6.708 4.718 C 3.126 7.328 1.977 12.157 4 16.1 L 2 22 Z",
+ IconName.Phone => "M 22 16.92 L 22 19.92 C 22.002 20.483 21.767 21.021 21.352 21.402 C 20.937 21.783 20.381 21.971 19.82 21.92 C 16.743 21.586 13.787 20.534 11.19 18.85 C 8.774 17.315 6.725 15.266 5.19 12.85 C 3.5 10.241 2.448 7.271 2.12 4.18 C 2.069 3.621 2.256 3.066 2.635 2.652 C 3.013 2.237 3.549 2.001 4.11 2 L 7.11 2 C 8.114 1.99 8.97 2.726 9.11 3.72 C 9.237 4.68 9.471 5.623 9.81 6.53 C 10.085 7.261 9.909 8.085 9.36 8.64 L 8.09 9.91 C 9.514 12.414 11.586 14.486 14.09 15.91 L 15.36 14.64 C 15.915 14.091 16.739 13.915 17.47 14.19 C 18.377 14.529 19.32 14.763 20.28 14.89 C 21.286 15.032 22.025 15.905 22 16.92 Z",
+ IconName.Calendar => "M 8 2 L 8 6 M 16 2 L 16 6 M 5 4 L 19 4 C 20.105 4 21 4.895 21 6 L 21 20 C 21 21.105 20.105 22 19 22 L 5 22 C 3.895 22 3 21.105 3 20 L 3 6 C 3 4.895 3.895 4 5 4 Z M 3 10 L 21 10",
+ IconName.Clock => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 12 6 L 12 12 L 16 14",
+ IconName.Globe => "M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 12 2 C 6.667 7.6 6.667 16.4 12 22 C 17.333 16.4 17.333 7.6 12 2 M 2 12 L 22 12",
+ IconName.MapPin => "M 20 10 C 20 14.993 14.461 20.193 12.601 21.799 C 12.245 22.067 11.755 22.067 11.399 21.799 C 9.539 20.193 4 14.993 4 10 C 4 5.582 7.582 2 12 2 C 16.418 2 20 5.582 20 10 M 15 10 C 15 11.657 13.657 13 12 13 C 10.343 13 9 11.657 9 10 C 9 8.343 10.343 7 12 7 C 13.657 7 15 8.343 15 10 Z",
+ IconName.Image => "M 5 3 L 19 3 C 20.105 3 21 3.895 21 5 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 5 C 3 3.895 3.895 3 5 3 Z M 11 9 C 11 10.105 10.105 11 9 11 C 7.895 11 7 10.105 7 9 C 7 7.895 7.895 7 9 7 C 10.105 7 11 7.895 11 9 Z M 21 15 L 17.914 11.914 C 17.133 11.133 15.867 11.133 15.086 11.914 L 6 21",
+ IconName.File => "M 15 2 L 6 2 C 4.895 2 4 2.895 4 4 L 4 20 C 4 21.105 4.895 22 6 22 L 18 22 C 19.105 22 20 21.105 20 20 L 20 7 Z M 14 2 L 14 6 C 14 7.105 14.895 8 16 8 L 20 8",
+ IconName.FileText => "M 15 2 L 6 2 C 4.895 2 4 2.895 4 4 L 4 20 C 4 21.105 4.895 22 6 22 L 18 22 C 19.105 22 20 21.105 20 20 L 20 7 Z M 14 2 L 14 6 C 14 7.105 14.895 8 16 8 L 20 8 M 10 9 L 8 9 M 16 13 L 8 13 M 16 17 L 8 17",
+ IconName.Folder => "M 20 20 C 21.105 20 22 19.105 22 18 L 22 8 C 22 6.895 21.105 6 20 6 L 12.1 6 C 11.42 6.007 10.784 5.668 10.41 5.1 L 9.6 3.9 C 9.23 3.338 8.603 3 7.93 3 L 4 3 C 2.895 3 2 3.895 2 5 L 2 18 C 2 19.105 2.895 20 4 20 Z",
+ IconName.Tag => "M 12.586 2.586 C 12.211 2.211 11.702 2 11.172 2 L 4 2 C 2.895 2 2 2.895 2 4 L 2 11.172 C 2 11.702 2.211 12.211 2.586 12.586 L 11.29 21.29 C 12.236 22.23 13.764 22.23 14.71 21.29 L 21.29 14.71 C 22.23 13.764 22.23 12.236 21.29 11.29 Z M 8 7.5 C 8 7.776 7.776 8 7.5 8 C 7.224 8 7 7.776 7 7.5 C 7 7.224 7.224 7 7.5 7 C 7.776 7 8 7.224 8 7.5 Z",
+ IconName.Bookmark => "M 19 21 L 12 17 L 5 21 L 5 5 C 5 3.895 5.895 3 7 3 L 17 3 C 18.105 3 19 3.895 19 5 L 19 21 Z",
+ IconName.Heart => "M 19 14 C 20.49 12.54 22 10.79 22 8.5 C 22 5.462 19.538 3 16.5 3 C 14.74 3 13.5 3.5 12 5 C 10.5 3.5 9.26 3 7.5 3 C 4.462 3 2 5.462 2 8.5 C 2 10.8 3.5 12.55 5 14 L 12 21 Z",
+ IconName.Star => "M 11.525 2.295 C 11.614 2.114 11.798 2 12 2 C 12.202 2 12.386 2.114 12.475 2.295 L 14.785 6.974 C 15.094 7.599 15.69 8.033 16.38 8.134 L 21.546 8.89 C 21.746 8.919 21.912 9.059 21.974 9.251 C 22.036 9.442 21.984 9.653 21.84 9.794 L 18.104 13.432 C 17.604 13.919 17.375 14.622 17.493 15.31 L 18.375 20.45 C 18.41 20.65 18.329 20.852 18.165 20.971 C 18.001 21.09 17.783 21.105 17.604 21.01 L 12.986 18.582 C 12.368 18.258 11.631 18.258 11.013 18.582 L 6.396 21.01 C 6.217 21.105 6 21.089 5.836 20.97 C 5.673 20.851 5.591 20.649 5.626 20.45 L 6.507 15.311 C 6.625 14.622 6.397 13.92 5.896 13.432 L 2.16 9.795 C 2.014 9.654 1.962 9.443 2.024 9.25 C 2.087 9.058 2.253 8.917 2.454 8.889 L 7.619 8.134 C 8.31 8.034 8.907 7.6 9.216 6.974 Z",
+ IconName.ThumbsUp => "M 7 10 L 7 22 M 15 5.88 L 14 10 L 19.83 10 C 20.46 10 21.052 10.296 21.43 10.8 C 21.808 11.304 21.926 11.956 21.75 12.56 L 19.42 20.56 C 19.171 21.413 18.389 22 17.5 22 L 4 22 C 2.895 22 2 21.105 2 20 L 2 12 C 2 10.895 2.895 10 4 10 L 6.76 10 C 7.519 10 8.212 9.57 8.55 8.89 L 12 2 C 12.955 2.012 13.853 2.459 14.437 3.215 C 15.022 3.971 15.229 4.952 15 5.88 Z",
+ IconName.ShoppingCart => "M 9 21 C 9 21.552 8.552 22 8 22 C 7.448 22 7 21.552 7 21 C 7 20.448 7.448 20 8 20 C 8.552 20 9 20.448 9 21 Z M 20 21 C 20 21.552 19.552 22 19 22 C 18.448 22 18 21.552 18 21 C 18 20.448 18.448 20 19 20 C 19.552 20 20 20.448 20 21 Z M 2.05 2.05 L 4.05 2.05 L 6.71 14.47 C 6.911 15.408 7.751 16.071 8.71 16.05 L 18.49 16.05 C 19.428 16.048 20.238 15.396 20.44 14.48 L 22.09 7.05 L 5.12 7.05",
+ IconName.CreditCard => "M 4 5 L 20 5 C 21.105 5 22 5.895 22 7 L 22 17 C 22 18.105 21.105 19 20 19 L 4 19 C 2.895 19 2 18.105 2 17 L 2 7 C 2 5.895 2.895 5 4 5 Z M 2 10 L 22 10",
+ IconName.DollarSign => "M 12 2 L 12 22 M 17 5 L 9.5 5 C 7.567 5 6 6.567 6 8.5 C 6 10.433 7.567 12 9.5 12 L 14.5 12 C 16.433 12 18 13.567 18 15.5 C 18 17.433 16.433 19 14.5 19 L 6 19",
+ IconName.Activity => "M 22 12 L 19.52 12 C 18.622 11.998 17.833 12.595 17.59 13.46 L 15.24 21.82 C 15.209 21.927 15.111 22 15 22 C 14.889 22 14.791 21.927 14.76 21.82 L 9.24 2.18 C 9.209 2.073 9.111 2 9 2 C 8.889 2 8.791 2.073 8.76 2.18 L 6.41 10.54 C 6.168 11.401 5.384 11.997 4.49 12 L 2 12",
+ IconName.TrendingUp => "M 22 7 L 13.5 15.5 L 8.5 10.5 L 2 17 M 16 7 L 22 7 L 22 13",
+ IconName.ChartColumn => "M 3 3 L 3 19 C 3 20.105 3.895 21 5 21 L 21 21 M 18 17 L 18 9 M 13 17 L 13 5 M 8 17 L 8 14",
+ IconName.Zap => "M 4 14 C 3.614 14.001 3.261 13.78 3.095 13.432 C 2.928 13.083 2.977 12.67 3.22 12.37 L 13.12 2.17 C 13.273 1.993 13.528 1.946 13.734 2.057 C 13.94 2.167 14.042 2.405 13.98 2.63 L 12.06 8.65 C 11.945 8.958 11.988 9.302 12.176 9.571 C 12.364 9.841 12.672 10.001 13 10 L 20 10 C 20.386 9.999 20.739 10.22 20.905 10.568 C 21.072 10.917 21.023 11.33 20.78 11.63 L 10.88 21.83 C 10.727 22.007 10.472 22.054 10.266 21.943 C 10.06 21.833 9.958 21.595 10.02 21.37 L 11.94 15.35 C 12.055 15.042 12.012 14.698 11.824 14.429 C 11.636 14.159 11.328 13.999 11 14 Z",
+ IconName.Sparkles => "M 9.937 15.5 C 9.755 14.795 9.205 14.245 8.5 14.063 L 2.365 12.481 C 2.15 12.42 2.002 12.224 2.002 12 C 2.002 11.776 2.15 11.58 2.365 11.519 L 8.5 9.936 C 9.205 9.754 9.755 9.204 9.937 8.5 L 11.519 2.365 C 11.579 2.149 11.776 2 12 2 C 12.225 2 12.422 2.149 12.482 2.365 L 14.063 8.5 C 14.245 9.205 14.795 9.755 15.5 9.937 L 21.635 11.518 C 21.852 11.578 22.002 11.775 22.002 12 C 22.002 12.225 21.852 12.422 21.635 12.482 L 15.5 14.063 C 14.795 14.245 14.245 14.795 14.063 15.5 L 12.481 21.635 C 12.421 21.851 12.224 22 12 22 C 11.775 22 11.578 21.851 11.518 21.635 Z M 20 3 L 20 7 M 22 5 L 18 5 M 4 17 L 4 19 M 5 18 L 3 18",
+ IconName.Command => "M 15 6 L 15 18 C 15 19.657 16.343 21 18 21 C 19.657 21 21 19.657 21 18 C 21 16.343 19.657 15 18 15 L 6 15 C 4.343 15 3 16.343 3 18 C 3 19.657 4.343 21 6 21 C 7.657 21 9 19.657 9 18 L 9 6 C 9 4.343 7.657 3 6 3 C 4.343 3 3 4.343 3 6 C 3 7.657 4.343 9 6 9 L 18 9 C 19.657 9 21 7.657 21 6 C 21 4.343 19.657 3 18 3 C 16.343 3 15 4.343 15 6",
+ IconName.Terminal => "M 4 17 L 10 11 L 4 5 M 12 19 L 20 19",
+ IconName.Code => "M 16 18 L 22 12 L 16 6 M 8 6 L 2 12 L 8 18",
+ IconName.Moon => "M 12 3 C 9.515 5.485 9.515 9.515 12 12 C 14.485 14.485 18.515 14.485 21 12 C 21 16.971 16.971 21 12 21 C 7.029 21 3 16.971 3 12 C 3 7.029 7.029 3 12 3 Z",
+ IconName.Sun => "M 16 12 C 16 14.209 14.209 16 12 16 C 9.791 16 8 14.209 8 12 C 8 9.791 9.791 8 12 8 C 14.209 8 16 9.791 16 12 Z M 12 2 L 12 4 M 12 20 L 12 22 M 4.93 4.93 L 6.34 6.34 M 17.66 17.66 L 19.07 19.07 M 2 12 L 4 12 M 20 12 L 22 12 M 6.34 17.66 L 4.93 19.07 M 19.07 4.93 L 17.66 6.34",
+ IconName.Monitor => "M 4 3 L 20 3 C 21.105 3 22 3.895 22 5 L 22 15 C 22 16.105 21.105 17 20 17 L 4 17 C 2.895 17 2 16.105 2 15 L 2 5 C 2 3.895 2.895 3 4 3 Z M 8 21 L 16 21 M 12 17 L 12 21",
+ IconName.Smartphone => "M 7 2 L 17 2 C 18.105 2 19 2.895 19 4 L 19 20 C 19 21.105 18.105 22 17 22 L 7 22 C 5.895 22 5 21.105 5 20 L 5 4 C 5 2.895 5.895 2 7 2 Z M 12 18 L 12.01 18",
+ IconName.Laptop => "M 20 16 L 20 7 C 20 5.895 19.105 5 18 5 L 6 5 C 4.895 5 4 5.895 4 7 L 4 16 M 20 16 L 4 16 M 20 16 L 21.28 18.55 C 21.437 18.861 21.42 19.231 21.237 19.527 C 21.053 19.823 20.728 20.002 20.38 20 L 3.62 20 C 3.272 20.002 2.947 19.823 2.763 19.527 C 2.58 19.231 2.563 18.861 2.72 18.55 L 4 16",
+ IconName.Wifi => "M 12 20 L 12.01 20 M 2 8.82 C 7.694 3.727 16.306 3.727 22 8.82 M 5 12.859 C 8.888 9.048 15.112 9.048 19 12.859 M 8.5 16.429 C 10.444 14.523 13.556 14.523 15.5 16.429",
+ IconName.Camera => "M 14.5 4 L 9.5 4 L 7 7 L 4 7 C 2.895 7 2 7.895 2 9 L 2 18 C 2 19.105 2.895 20 4 20 L 20 20 C 21.105 20 22 19.105 22 18 L 22 9 C 22 7.895 21.105 7 20 7 L 17 7 L 14.5 4 Z M 15 13 C 15 14.657 13.657 16 12 16 C 10.343 16 9 14.657 9 13 C 9 11.343 10.343 10 12 10 C 13.657 10 15 11.343 15 13 Z",
+ IconName.Mic => "M 12 2 C 10.343 2 9 3.343 9 5 L 9 12 C 9 13.657 10.343 15 12 15 C 13.657 15 15 13.657 15 12 L 15 5 C 15 3.343 13.657 2 12 2 Z M 19 10 L 19 12 C 19 15.866 15.866 19 12 19 C 8.134 19 5 15.866 5 12 L 5 10 M 12 19 L 12 22",
+ IconName.Play => "M 6 3 L 20 12 L 6 21 L 6 3 Z",
+ IconName.Pause => "M 15 4 L 17 4 C 17.552 4 18 4.448 18 5 L 18 19 C 18 19.552 17.552 20 17 20 L 15 20 C 14.448 20 14 19.552 14 19 L 14 5 C 14 4.448 14.448 4 15 4 Z M 7 4 L 9 4 C 9.552 4 10 4.448 10 5 L 10 19 C 10 19.552 9.552 20 9 20 L 7 20 C 6.448 20 6 19.552 6 19 L 6 5 C 6 4.448 6.448 4 7 4 Z",
+ IconName.Volume2 => "M 11 4.702 C 11 4.417 10.828 4.161 10.565 4.052 C 10.301 3.943 9.999 4.003 9.797 4.204 L 6.413 7.587 C 6.149 7.852 5.79 8.001 5.416 8 L 3 8 C 2.448 8 2 8.448 2 9 L 2 15 C 2 15.552 2.448 16 3 16 L 5.416 16 C 5.79 15.999 6.149 16.148 6.413 16.413 L 9.796 19.797 C 9.998 19.999 10.301 20.06 10.565 19.95 C 10.829 19.841 11 19.583 11 19.298 Z M 16 9 C 17.333 10.778 17.333 13.222 16 15 M 19.364 18.364 C 21.052 16.676 22 14.387 22 12 C 22 9.613 21.052 7.324 19.364 5.636",
+ IconName.Lightbulb => "M 15 14 C 15.2 13 15.7 12.3 16.5 11.5 C 17.5 10.6 18 9.3 18 8 C 18 4.686 15.314 2 12 2 C 8.686 2 6 4.686 6 8 C 6 9 6.2 10.2 7.5 11.5 C 8.2 12.2 8.8 13 9 14 M 9 18 L 15 18 M 10 22 L 14 22",
+ _ => string.Empty
+ };
+}
+
+public class Icon : Control
+{
+ public static readonly StyledProperty KindProperty =
+ AvaloniaProperty.Register(nameof(Kind));
+
+ public static readonly StyledProperty SizeProperty =
+ AvaloniaProperty.Register(nameof(Size), 16.0);
+
+ public static readonly StyledProperty StrokeWidthProperty =
+ AvaloniaProperty.Register(nameof(StrokeWidth), 2.0);
+
+ public static readonly StyledProperty TokenProperty =
+ AvaloniaProperty.Register(nameof(Token), ShellToken.Foreground);
+
+ // Explicit color; overrides Token when set.
+ public static readonly StyledProperty ColorProperty =
+ AvaloniaProperty.Register(nameof(Color));
+
+ // The token's brush, kept in sync with the theme by a resource binding.
+ private static readonly StyledProperty TintProperty =
+ AvaloniaProperty.Register("Tint");
+
+ private static readonly Dictionary Geometries = new();
+
+ static Icon()
+ {
+ AffectsMeasure(SizeProperty);
+ AffectsRender(KindProperty, SizeProperty, StrokeWidthProperty, ColorProperty, TintProperty);
+ TokenProperty.Changed.AddClassHandler((icon, _) => icon.Token(TintProperty, icon.Token));
+ IsHitTestVisibleProperty.OverrideDefaultValue(false);
+ HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Center);
+ VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Center);
+ }
+
+ public Icon() => this.Token(TintProperty, Token);
+
+ public IconName Kind
+ {
+ get => GetValue(KindProperty);
+ set => SetValue(KindProperty, value);
+ }
+
+ public double Size
+ {
+ get => GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ public double StrokeWidth
+ {
+ get => GetValue(StrokeWidthProperty);
+ set => SetValue(StrokeWidthProperty, value);
+ }
+
+ public ShellToken Token
+ {
+ get => GetValue(TokenProperty);
+ set => SetValue(TokenProperty, value);
+ }
+
+ public Color? Color
+ {
+ get => GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ protected override Size MeasureOverride(Size availableSize) => new(Size, Size);
+
+ public override void Render(DrawingContext context)
+ {
+ var brush = Color is { } color ? new SolidColorBrush(color) : GetValue(TintProperty);
+ if (GetGeometry(Kind) is not { } geometry || brush is null) return;
+ // Scale the 24x24 grid to Size; the stroke scales with it so icons keep Lucide's weight.
+ var scale = Size / 24.0;
+ using (context.PushTransform(Matrix.CreateScale(scale, scale)))
+ context.DrawGeometry(null, new Pen(brush, StrokeWidth, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), geometry);
+ }
+
+ private static Geometry? GetGeometry(IconName kind)
+ {
+ if (!Geometries.TryGetValue(kind, out var geometry))
+ {
+ var data = IconPaths.Get(kind);
+ geometry = string.IsNullOrEmpty(data) ? null : Geometry.Parse(data);
+ Geometries[kind] = geometry;
+ }
+ return geometry;
+ }
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Input.cs b/examples/Avalonia.Demo/Components/UI/Input.cs
new file mode 100644
index 0000000..38fa8e0
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Input.cs
@@ -0,0 +1,176 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Text input — h-10 rounded-md border border-input px-3 text-sm.
+// Focus: border-ring + soft ring glow. Error: border-destructive.
+// The inner TextBox's Fluent chrome is cleared so this Border is the only frame.
+public class Input : Border
+{
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register (nameof(Text), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty PlaceholderProperty =
+ AvaloniaProperty.Register (nameof(Placeholder));
+
+ public static readonly StyledProperty IsPasswordProperty =
+ AvaloniaProperty.Register (nameof(IsPassword));
+
+ public static readonly StyledProperty HasErrorProperty =
+ AvaloniaProperty.Register (nameof(HasError));
+
+ public static readonly StyledProperty IsReadOnlyProperty =
+ AvaloniaProperty.Register (nameof(IsReadOnly));
+
+ public static readonly StyledProperty MaxLengthProperty =
+ AvaloniaProperty.Register (nameof(MaxLength));
+
+ // Fluent's TextBox template takes its background and border brushes from these resources in
+ // every state (hover, focus, disabled); overriding them on the TextBox clears that chrome.
+ private static readonly string[] ClearedBrushes =
+ {
+ "TextControlBackground", "TextControlBackgroundPointerOver", "TextControlBackgroundFocused", "TextControlBackgroundDisabled",
+ "TextControlBorderBrush", "TextControlBorderBrushPointerOver", "TextControlBorderBrushFocused", "TextControlBorderBrushDisabled",
+ };
+
+ private readonly TextBox _textBox;
+ private bool _isFocused;
+
+ static Input()
+ {
+ TextProperty.Changed.AddClassHandler ((i, e) =>
+ {
+ if (i._textBox.Text != (string?)e.NewValue) i._textBox.Text = (string?)e.NewValue;
+ });
+ PlaceholderProperty.Changed.AddClassHandler ((i, e) => i._textBox.PlaceholderText = (string?)e.NewValue);
+ IsPasswordProperty.Changed.AddClassHandler ((i, e) => i._textBox.PasswordChar = (bool)e.NewValue! ? '•' : default);
+ IsReadOnlyProperty.Changed.AddClassHandler ((i, e) => i._textBox.IsReadOnly = (bool)e.NewValue!);
+ MaxLengthProperty.Changed.AddClassHandler ((i, e) => i._textBox.MaxLength = (int)e.NewValue!);
+ HasErrorProperty.Changed.AddClassHandler ((i, _) => i.UpdateVisualState());
+ IsEnabledProperty.Changed.AddClassHandler ((i, _) => i.Opacity = i.IsEnabled ? 1.0 : 0.5);
+ }
+
+ public string? Text
+ {
+ get => GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public string? Placeholder
+ {
+ get => GetValue(PlaceholderProperty);
+ set => SetValue(PlaceholderProperty, value);
+ }
+
+ public bool IsPassword
+ {
+ get => GetValue(IsPasswordProperty);
+ set => SetValue(IsPasswordProperty, value);
+ }
+
+ public bool HasError
+ {
+ get => GetValue(HasErrorProperty);
+ set => SetValue(HasErrorProperty, value);
+ }
+
+ public bool IsReadOnly
+ {
+ get => GetValue(IsReadOnlyProperty);
+ set => SetValue(IsReadOnlyProperty, value);
+ }
+
+ // 0 means no limit.
+ public int MaxLength
+ {
+ get => GetValue(MaxLengthProperty);
+ set => SetValue(MaxLengthProperty, value);
+ }
+
+ public event EventHandler? TextChanged;
+
+ // Raised on Enter.
+ public event EventHandler? Completed;
+
+ public Input()
+ {
+ _textBox = new TextBox
+ {
+ FontSize = 14,
+ BorderThickness = new Thickness(0),
+ Padding = new Thickness(0),
+ MinHeight = 0,
+ VerticalAlignment = VerticalAlignment.Center,
+ VerticalContentAlignment = VerticalAlignment.Center
+ };
+ foreach (var key in ClearedBrushes)
+ _textBox.Resources[key] = Brushes.Transparent;
+ _textBox.Resources["TextControlBorderThemeThickness"] = new Thickness(0);
+ _textBox.Resources["TextControlBorderThemeThicknessFocused"] = new Thickness(0);
+ _textBox.Resources["TextControlThemePadding"] = new Thickness(0);
+ _textBox.Token(TextBox.ForegroundProperty, ShellToken.Foreground)
+ .Token(TextBox.CaretBrushProperty, ShellToken.Foreground)
+ .Token(TextBox.PlaceholderForegroundProperty, ShellToken.MutedForeground);
+
+ _textBox.TextChanged += (_, e) =>
+ {
+ Text = _textBox.Text;
+ TextChanged?.Invoke(this, e);
+ };
+ _textBox.KeyDown += (_, e) =>
+ {
+ if (e.Key == Key.Enter) Completed?.Invoke(this, EventArgs.Empty);
+ };
+ _textBox.GotFocus += (_, _) => { _isFocused = true; UpdateVisualState(); };
+ _textBox.LostFocus += (_, _) => { _isFocused = false; UpdateVisualState(); };
+
+ Child = _textBox;
+ Height = 40;
+ Padding = new Thickness(12, 0);
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(ShellTheme.RadiusMd);
+ Background = Brushes.Transparent;
+ Cursor = new Cursor(StandardCursorType.Ibeam);
+ UpdateVisualState();
+ }
+
+ // The glow color is a snapshot of a token, so it is repainted on theme changes.
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ ShellTheme.ThemeChanged += OnThemeChanged;
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+ ShellTheme.ThemeChanged -= OnThemeChanged;
+ }
+
+ private void OnThemeChanged(object? sender, EventArgs e) => UpdateVisualState();
+
+ // Clicking the padding focuses the text box too.
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ base.OnPointerPressed(e);
+ if (!_textBox.IsFocused) _textBox.Focus();
+ }
+
+ public new bool Focus(NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None)
+ => _textBox.Focus(method, keyModifiers);
+
+ private void UpdateVisualState()
+ {
+ var stroke = HasError ? ShellToken.Destructive : _isFocused ? ShellToken.Ring : ShellToken.Input;
+ this.Token(BorderBrushProperty, stroke);
+ BoxShadow = _isFocused || HasError
+ ? new BoxShadows(new BoxShadow { Blur = 6, Color = ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring, 0.35) })
+ : default;
+ }
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Label.cs b/examples/Avalonia.Demo/Components/UI/Label.cs
new file mode 100644
index 0000000..4726ebe
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Label.cs
@@ -0,0 +1,82 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Typography label with size / weight / variant — theme-aware.
+// Named ShellLabel because Avalonia already has a Label control.
+public class ShellLabel : TextBlock
+{
+ public static readonly StyledProperty SizeProperty =
+ AvaloniaProperty.Register(nameof(Size), LabelSize.Default);
+
+ public static readonly StyledProperty WeightProperty =
+ AvaloniaProperty.Register(nameof(Weight), LabelWeight.Normal);
+
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ static ShellLabel()
+ {
+ SizeProperty.Changed.AddClassHandler((l, _) => l.UpdateVisualState());
+ WeightProperty.Changed.AddClassHandler((l, _) => l.UpdateVisualState());
+ VariantProperty.Changed.AddClassHandler((l, _) => l.UpdateVisualState());
+ }
+
+ public LabelSize Size
+ {
+ get => GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ public LabelWeight Weight
+ {
+ get => GetValue(WeightProperty);
+ set => SetValue(WeightProperty, value);
+ }
+
+ public LabelVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public ShellLabel() => UpdateVisualState();
+
+ private void UpdateVisualState()
+ {
+ FontSize = Size switch
+ {
+ LabelSize.Xs => 12,
+ LabelSize.Sm => 14,
+ LabelSize.Lg => 18,
+ LabelSize.Xl => 20,
+ LabelSize.Xxl => 24,
+ LabelSize.Xxxl => 30,
+ _ => 16
+ };
+
+ FontWeight = Weight switch
+ {
+ LabelWeight.Light => FontWeight.Light,
+ LabelWeight.Medium => FontWeight.Medium,
+ LabelWeight.Semibold => FontWeight.SemiBold,
+ LabelWeight.Bold => FontWeight.Bold,
+ _ => FontWeight.Normal
+ };
+
+ this.Token(ForegroundProperty, Variant switch
+ {
+ LabelVariant.Muted => ShellToken.MutedForeground,
+ LabelVariant.Destructive => ShellToken.Destructive,
+ LabelVariant.Success => ShellToken.Success,
+ LabelVariant.Warning => ShellToken.Warning,
+ _ => ShellToken.Foreground
+ });
+ }
+}
+
+public enum LabelSize { Xs, Sm, Default, Lg, Xl, Xxl, Xxxl }
+public enum LabelWeight { Light, Normal, Medium, Semibold, Bold }
+public enum LabelVariant { Default, Muted, Destructive, Success, Warning }
diff --git a/examples/Avalonia.Demo/Components/UI/Progress.cs b/examples/Avalonia.Demo/Components/UI/Progress.cs
new file mode 100644
index 0000000..4929188
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Progress.cs
@@ -0,0 +1,119 @@
+using System;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Progress bar — h-2 rounded-full track, fill animates to the new value.
+public class Progress : Grid
+{
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register(nameof(Value));
+
+ public static readonly StyledProperty MaximumProperty =
+ AvaloniaProperty.Register(nameof(Maximum), 100.0);
+
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ public static readonly StyledProperty ShowLabelProperty =
+ AvaloniaProperty.Register(nameof(ShowLabel));
+
+ private const double TrackHeight = 8;
+
+ private readonly Panel _track;
+ private readonly Border _trackFill;
+ private readonly Border _fill;
+ private readonly TextBlock _label;
+ private readonly Transitions _fillTransitions = new()
+ {
+ new DoubleTransition { Property = WidthProperty, Duration = TimeSpan.FromMilliseconds(300), Easing = new CubicEaseOut() }
+ };
+
+ static Progress()
+ {
+ ValueProperty.Changed.AddClassHandler((p, _) => p.UpdateFill(animate: true));
+ MaximumProperty.Changed.AddClassHandler((p, _) => p.UpdateFill(animate: true));
+ VariantProperty.Changed.AddClassHandler((p, _) => p.UpdateVisualState());
+ ShowLabelProperty.Changed.AddClassHandler((p, _) => p.UpdateVisualState());
+ }
+
+ public double Value
+ {
+ get => GetValue(ValueProperty);
+ set => SetValue(ValueProperty, value);
+ }
+
+ public double Maximum
+ {
+ get => GetValue(MaximumProperty);
+ set => SetValue(MaximumProperty, value);
+ }
+
+ public ProgressVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public bool ShowLabel
+ {
+ get => GetValue(ShowLabelProperty);
+ set => SetValue(ShowLabelProperty, value);
+ }
+
+ public double Percentage => Maximum > 0 ? Math.Clamp(Value / Maximum, 0, 1) * 100 : 0;
+
+ public Progress()
+ {
+ // Track tint is the fill color at 20% (bg-primary/20), so it follows the variant.
+ _trackFill = new Border { Opacity = 0.2, CornerRadius = new CornerRadius(TrackHeight / 2) };
+ _fill = new Border { CornerRadius = new CornerRadius(TrackHeight / 2), HorizontalAlignment = HorizontalAlignment.Left, Width = 0 };
+ _track = new Panel { Height = TrackHeight, VerticalAlignment = VerticalAlignment.Center, Children = { _trackFill, _fill } };
+ _track.SizeChanged += (_, _) => UpdateFill(animate: false);
+
+ _label = new TextBlock { FontSize = 12, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(12, 0, 0, 0), IsVisible = false };
+ _label.Token(TextBlock.ForegroundProperty, ShellToken.MutedForeground);
+
+ ColumnDefinitions = new ColumnDefinitions("*,Auto");
+ SetColumn(_label, 1);
+ Children.Add(_track);
+ Children.Add(_label);
+ UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ var token = Variant switch
+ {
+ ProgressVariant.Success => ShellToken.Success,
+ ProgressVariant.Warning => ShellToken.Warning,
+ ProgressVariant.Destructive => ShellToken.Destructive,
+ _ => ShellToken.Primary
+ };
+ _fill.Token(Border.BackgroundProperty, token);
+ _trackFill.Token(Border.BackgroundProperty, token);
+ _label.IsVisible = ShowLabel;
+ UpdateFill(animate: false);
+ }
+
+ private void UpdateFill(bool animate)
+ {
+ _label.Text = $"{Percentage:F0}%";
+ if (_track.Bounds.Width <= 0) return;
+ // Resizes jump to the new width; only value changes animate.
+ _fill.Transitions = animate ? _fillTransitions : null;
+ _fill.Width = _track.Bounds.Width * Percentage / 100;
+ }
+}
+
+public enum ProgressVariant
+{
+ Default,
+ Success,
+ Warning,
+ Destructive
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Separator.cs b/examples/Avalonia.Demo/Components/UI/Separator.cs
new file mode 100644
index 0000000..b230da4
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Separator.cs
@@ -0,0 +1,44 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace AvaloniaDemo.Components.UI;
+
+// 1px divider in the Border token.
+public class Separator : Border
+{
+ public static readonly StyledProperty OrientationProperty =
+ AvaloniaProperty.Register(nameof(Orientation));
+
+ static Separator()
+ {
+ OrientationProperty.Changed.AddClassHandler((s, _) => s.UpdateVisualState());
+ }
+
+ public SeparatorOrientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ public Separator()
+ {
+ this.Token(BackgroundProperty, ShellToken.Border);
+ UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ var horizontal = Orientation == SeparatorOrientation.Horizontal;
+ Height = horizontal ? 1 : double.NaN;
+ Width = horizontal ? double.NaN : 1;
+ HorizontalAlignment = horizontal ? HorizontalAlignment.Stretch : HorizontalAlignment.Center;
+ VerticalAlignment = horizontal ? VerticalAlignment.Center : VerticalAlignment.Stretch;
+ }
+}
+
+public enum SeparatorOrientation
+{
+ Horizontal,
+ Vertical
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Shell.cs b/examples/Avalonia.Demo/Components/UI/Shell.cs
new file mode 100644
index 0000000..a1294aa
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Shell.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Styling;
+
+namespace AvaloniaDemo.Components.UI;
+
+// ShellUI Native core utilities. Installed by `shellui-native init`; every component depends on it.
+public static class Shell
+{
+ // Joins class names, skipping null or empty ones.
+ public static string Cn(params string?[] classes)
+ => string.Join(" ", classes.Where(c => !string.IsNullOrWhiteSpace(c)));
+}
+
+// Design tokens mirroring ShellUI's CSS variables (--background, --primary, ...).
+public enum ShellToken
+{
+ Background, Foreground,
+ Card, CardForeground,
+ Popover, PopoverForeground,
+ Primary, PrimaryForeground,
+ Secondary, SecondaryForeground,
+ Muted, MutedForeground,
+ Accent, AccentForeground,
+ Destructive, DestructiveForeground,
+ Success, SuccessForeground,
+ Warning, WarningForeground,
+ Info, InfoForeground,
+ Border, Input, Ring,
+ Overlay
+}
+
+/* Publishes both palettes as theme dictionaries of the application resources ("ShellUIPrimary" is
+ the Color, "ShellUIPrimaryBrush" the Brush). Avalonia resolves the active variant's palette, so a
+ theme switch repaints without re-publishing. Your own XAML can use them too:
+ Foreground="{DynamicResource ShellUIMutedForegroundBrush}".
+ Customize before the first window opens:
+ ShellTheme.Light[ShellToken.Primary] = Color.Parse("#2563EB");
+ ShellTheme.Apply(); */
+public static class ShellTheme
+{
+ // Based on ShellUI's default neutral theme, softened from pure black/white for native text
+ // rendering. Success/Warning/Info extend it (ShellUI has no semantic status colors).
+ public static Dictionary Light { get; } = new()
+ {
+ [ShellToken.Background] = Color.Parse("#FCFCFC"),
+ [ShellToken.Foreground] = Color.Parse("#0A0A0A"),
+ [ShellToken.Card] = Color.Parse("#FFFFFF"),
+ [ShellToken.CardForeground] = Color.Parse("#0A0A0A"),
+ [ShellToken.Popover] = Color.Parse("#FFFFFF"),
+ [ShellToken.PopoverForeground] = Color.Parse("#0A0A0A"),
+ [ShellToken.Primary] = Color.Parse("#171717"),
+ [ShellToken.PrimaryForeground] = Color.Parse("#FAFAFA"),
+ [ShellToken.Secondary] = Color.Parse("#F0F0F0"),
+ [ShellToken.SecondaryForeground] = Color.Parse("#171717"),
+ [ShellToken.Muted] = Color.Parse("#F5F5F5"),
+ [ShellToken.MutedForeground] = Color.Parse("#737373"),
+ [ShellToken.Accent] = Color.Parse("#F0F0F0"),
+ [ShellToken.AccentForeground] = Color.Parse("#171717"),
+ [ShellToken.Destructive] = Color.Parse("#E54B4F"),
+ [ShellToken.DestructiveForeground] = Color.Parse("#FFFFFF"),
+ [ShellToken.Success] = Color.Parse("#16A34A"),
+ [ShellToken.SuccessForeground] = Color.Parse("#FFFFFF"),
+ [ShellToken.Warning] = Color.Parse("#D97706"),
+ [ShellToken.WarningForeground] = Color.Parse("#FFFFFF"),
+ [ShellToken.Info] = Color.Parse("#2563EB"),
+ [ShellToken.InfoForeground] = Color.Parse("#FFFFFF"),
+ [ShellToken.Border] = Color.Parse("#E4E4E4"),
+ [ShellToken.Input] = Color.Parse("#E4E4E4"),
+ [ShellToken.Ring] = Color.Parse("#A3A3A3"),
+ [ShellToken.Overlay] = Color.Parse("#80000000"),
+ };
+
+ public static Dictionary Dark { get; } = new()
+ {
+ [ShellToken.Background] = Color.Parse("#0A0A0A"),
+ [ShellToken.Foreground] = Color.Parse("#FAFAFA"),
+ [ShellToken.Card] = Color.Parse("#141414"),
+ [ShellToken.CardForeground] = Color.Parse("#FAFAFA"),
+ [ShellToken.Popover] = Color.Parse("#171717"),
+ [ShellToken.PopoverForeground] = Color.Parse("#FAFAFA"),
+ [ShellToken.Primary] = Color.Parse("#FAFAFA"),
+ [ShellToken.PrimaryForeground] = Color.Parse("#171717"),
+ [ShellToken.Secondary] = Color.Parse("#262626"),
+ [ShellToken.SecondaryForeground] = Color.Parse("#FAFAFA"),
+ [ShellToken.Muted] = Color.Parse("#262626"),
+ [ShellToken.MutedForeground] = Color.Parse("#A3A3A3"),
+ [ShellToken.Accent] = Color.Parse("#262626"),
+ [ShellToken.AccentForeground] = Color.Parse("#FAFAFA"),
+ [ShellToken.Destructive] = Color.Parse("#FF5B5B"),
+ [ShellToken.DestructiveForeground] = Color.Parse("#0A0A0A"),
+ [ShellToken.Success] = Color.Parse("#22C55E"),
+ [ShellToken.SuccessForeground] = Color.Parse("#0A0A0A"),
+ [ShellToken.Warning] = Color.Parse("#F59E0B"),
+ [ShellToken.WarningForeground] = Color.Parse("#0A0A0A"),
+ [ShellToken.Info] = Color.Parse("#3B82F6"),
+ [ShellToken.InfoForeground] = Color.Parse("#FFFFFF"),
+ [ShellToken.Border] = Color.Parse("#2A2A2A"),
+ [ShellToken.Input] = Color.Parse("#333333"),
+ [ShellToken.Ring] = Color.Parse("#737373"),
+ [ShellToken.Overlay] = Color.Parse("#B3000000"),
+ };
+
+ // --radius: 0.5rem. Tailwind's rounded-sm / rounded-md / rounded-lg derive from it.
+ public const double RadiusSm = 4;
+ public const double RadiusMd = 6;
+ public const double RadiusLg = 8;
+
+ private static bool _initialized;
+
+ // Raised when a palette is published or the theme variant changes, for anything resources
+ // can't reach (e.g. a BoxShadow).
+ public static event EventHandler? ThemeChanged;
+
+ public static bool IsDarkMode => Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
+
+ public static string ColorKey(ShellToken token) => $"ShellUI{token}";
+ public static string BrushKey(ShellToken token) => $"ShellUI{token}Brush";
+
+ // Current value of a token, for code that can't bind.
+ public static Color Get(ShellToken token) => (IsDarkMode ? Dark : Light)[token];
+
+ // A token at an opacity, for glows and focus rings (Tailwind's ring/35).
+ public static Color Get(ShellToken token, double opacity)
+ {
+ var color = Get(token);
+ return new Color((byte)Math.Round(color.A * opacity), color.R, color.G, color.B);
+ }
+
+ // Black at an opacity, for drop shadows (shadow-sm is black at 5%).
+ public static Color Shadow(double opacity) => new((byte)Math.Round(255 * opacity), 0, 0, 0);
+
+ public static void EnsureInitialized()
+ {
+ if (_initialized) return;
+ var app = Application.Current;
+ if (app is null) return;
+ _initialized = true;
+ app.ActualThemeVariantChanged += (_, _) => ThemeChanged?.Invoke(null, EventArgs.Empty);
+ Apply();
+ }
+
+ // Re-publishes both palettes. Call after editing Light/Dark at runtime.
+ public static void Apply()
+ {
+ var app = Application.Current;
+ if (app is null) return;
+ app.Resources.ThemeDictionaries[ThemeVariant.Light] = Build(Light);
+ app.Resources.ThemeDictionaries[ThemeVariant.Dark] = Build(Dark);
+ ThemeChanged?.Invoke(null, EventArgs.Empty);
+ }
+
+ private static ResourceDictionary Build(Dictionary palette)
+ {
+ var dictionary = new ResourceDictionary();
+ foreach (var (token, color) in palette)
+ {
+ dictionary[ColorKey(token)] = color;
+ dictionary[BrushKey(token)] = new SolidColorBrush(color);
+ }
+ return dictionary;
+ }
+
+ // ThemeVariant.Default follows the OS setting.
+ public static void SetTheme(ThemeVariant theme)
+ {
+ if (Application.Current is not { } app) return;
+ EnsureInitialized();
+ app.RequestedThemeVariant = theme;
+ }
+
+ public static void ToggleTheme() => SetTheme(IsDarkMode ? ThemeVariant.Light : ThemeVariant.Dark);
+}
+
+public static class ShellThemeExtensions
+{
+ // One binding per control and property. A resource binding outlives ClearValue and a later
+ // SetValue, and would repaint the property on the next theme change, so it is disposed instead.
+ private static readonly ConditionalWeakTable> Bindings = new();
+
+ // Binds a Color or Brush property to a theme token, replacing any earlier token binding.
+ // Returns the control for chaining.
+ public static T Token(this T control, AvaloniaProperty property, ShellToken token) where T : StyledElement
+ {
+ ShellTheme.EnsureInitialized();
+ control.ClearToken(property);
+ var key = typeof(IBrush).IsAssignableFrom(property.PropertyType) ? ShellTheme.BrushKey(token) : ShellTheme.ColorKey(token);
+ Bindings.GetOrCreateValue(control)[property] = control.Bind(property, control.GetResourceObservable(key));
+ return control;
+ }
+
+ // Removes the token binding, so a plain value (e.g. Brushes.Transparent) can be set.
+ public static T ClearToken(this T control, AvaloniaProperty property) where T : StyledElement
+ {
+ if (Bindings.TryGetValue(control, out var bindings) && bindings.Remove(property, out var binding))
+ binding.Dispose();
+ return control;
+ }
+}
+
+/* shadcn's focus-visible ring (ring-2 ring-ring ring-offset-2): a 2px Background gap, then a 2px
+ Ring band. Shown for keyboard focus only, so a click doesn't leave a ring behind. Replaces
+ Fluent's focus adorner. */
+public static class ShellFocus
+{
+ private static readonly HashSet Shown = new();
+
+ static ShellFocus() => ShellTheme.ThemeChanged += (_, _) =>
+ {
+ foreach (var border in Shown) Paint(border);
+ };
+
+ public static void Ring(Control control, Border target)
+ {
+ control.Focusable = true;
+ control.FocusAdorner = null;
+ control.GotFocus += (_, e) =>
+ {
+ if (e.NavigationMethod is not (NavigationMethod.Tab or NavigationMethod.Directional)) return;
+ Shown.Add(target);
+ Paint(target);
+ };
+ control.LostFocus += (_, _) =>
+ {
+ Shown.Remove(target);
+ target.BoxShadow = default;
+ };
+ }
+
+ private static void Paint(Border border) => border.BoxShadow = new BoxShadows(
+ new BoxShadow { Spread = 2, Color = ShellTheme.Get(ShellToken.Background) },
+ new[] { new BoxShadow { Spread = 4, Color = ShellTheme.Get(ShellToken.Ring) } });
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Switch.cs b/examples/Avalonia.Demo/Components/UI/Switch.cs
new file mode 100644
index 0000000..5562b96
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Switch.cs
@@ -0,0 +1,125 @@
+using System;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Media.Transformation;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Switch — h-6 w-11 rounded-full track (primary when on, input when off),
+// h-5 w-5 thumb in bg-background that slides translate-x-5.
+public class Switch : Border
+{
+ public static readonly StyledProperty IsToggledProperty =
+ AvaloniaProperty.Register(nameof(IsToggled), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty LabelProperty =
+ AvaloniaProperty.Register(nameof(Label));
+
+ private const double TrackWidth = 44;
+ private const double TrackHeight = 24;
+ private const double Inset = 2;
+ private const double ThumbSize = TrackHeight - Inset * 2; // 20
+ private const double Travel = TrackWidth - Inset * 2 - ThumbSize; // 20
+
+ private readonly Border _track;
+ private readonly Border _thumb;
+ private readonly TextBlock _label;
+
+ static Switch()
+ {
+ IsToggledProperty.Changed.AddClassHandler((s, e) =>
+ {
+ s.UpdateVisualState();
+ s.Toggled?.Invoke(s, (bool)e.NewValue!);
+ });
+ LabelProperty.Changed.AddClassHandler((s, _) => s.UpdateLabel());
+ IsEnabledProperty.Changed.AddClassHandler((s, _) => s.Opacity = s.IsEnabled ? 1.0 : 0.5);
+ }
+
+ public bool IsToggled
+ {
+ get => GetValue(IsToggledProperty);
+ set => SetValue(IsToggledProperty, value);
+ }
+
+ public string? Label
+ {
+ get => GetValue(LabelProperty);
+ set => SetValue(LabelProperty, value);
+ }
+
+ public event EventHandler? Toggled;
+
+ public Switch()
+ {
+ _thumb = new Border
+ {
+ Width = ThumbSize,
+ Height = ThumbSize,
+ CornerRadius = new CornerRadius(ThumbSize / 2),
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Center,
+ BoxShadow = new BoxShadows(new BoxShadow { OffsetY = 1, Blur = 3, Color = ShellTheme.Shadow(0.2) })
+ };
+ _thumb.Token(BackgroundProperty, ShellToken.Background);
+
+ _track = new Border
+ {
+ Child = _thumb,
+ Width = TrackWidth,
+ Height = TrackHeight,
+ Padding = new Thickness(Inset),
+ CornerRadius = new CornerRadius(TrackHeight / 2),
+ VerticalAlignment = VerticalAlignment.Center
+ };
+
+ _label = new TextBlock { FontSize = 14, VerticalAlignment = VerticalAlignment.Center, IsVisible = false };
+ _label.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
+
+ Child = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Children = { _track, _label } };
+ Background = Brushes.Transparent;
+ HorizontalAlignment = HorizontalAlignment.Left;
+ Cursor = new Cursor(StandardCursorType.Hand);
+ ShellFocus.Ring(this, _track);
+ UpdateVisualState();
+ // Added after the first state, so the thumb doesn't slide into place on load.
+ _thumb.Transitions = new Transitions
+ {
+ new TransformOperationsTransition { Property = RenderTransformProperty, Duration = TimeSpan.FromMilliseconds(150), Easing = new CubicEaseOut() }
+ };
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (e.InitialPressMouseButton != MouseButton.Left || !IsEffectivelyEnabled) return;
+ IsToggled = !IsToggled;
+ e.Handled = true;
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+ if (e.Key is not (Key.Space or Key.Enter)) return;
+ IsToggled = !IsToggled;
+ e.Handled = true;
+ }
+
+ private void UpdateLabel()
+ {
+ _label.Text = Label ?? string.Empty;
+ _label.IsVisible = !string.IsNullOrEmpty(Label);
+ }
+
+ private void UpdateVisualState()
+ {
+ _track.Token(BackgroundProperty, IsToggled ? ShellToken.Primary : ShellToken.Input);
+ _thumb.RenderTransform = TransformOperations.Parse($"translateX({(IsToggled ? Travel : 0)}px)");
+ }
+}
diff --git a/examples/Avalonia.Demo/Components/UI/ThemeToggle.cs b/examples/Avalonia.Demo/Components/UI/ThemeToggle.cs
new file mode 100644
index 0000000..c87816e
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/ThemeToggle.cs
@@ -0,0 +1,119 @@
+using System;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+using Avalonia.Automation;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Media.Transformation;
+
+namespace AvaloniaDemo.Components.UI;
+
+// Light/dark mode toggle — a 36x36 outline icon button (shadcn's ModeToggle). The sun and
+// moon cross-fade with a quarter turn. Usage:
+public class ThemeToggle : Border
+{
+ private const double Box = 36;
+
+ private readonly Icon _sun;
+ private readonly Icon _moon;
+
+ public event EventHandler? ThemeChanged;
+
+ public bool IsDarkMode => ShellTheme.IsDarkMode;
+
+ public ThemeToggle()
+ {
+ ShellTheme.EnsureInitialized();
+
+ _sun = new Icon { Kind = IconName.Sun, Size = 16 };
+ _moon = new Icon { Kind = IconName.Moon, Size = 16 };
+
+ Width = Box;
+ Height = Box;
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(ShellTheme.RadiusMd);
+ HorizontalAlignment = HorizontalAlignment.Left;
+ VerticalAlignment = VerticalAlignment.Center;
+ Cursor = new Cursor(StandardCursorType.Hand);
+ Focusable = true;
+ Child = new Panel { Children = { _sun, _moon } };
+ this.Token(BorderBrushProperty, ShellToken.Input);
+ this.Token(BackgroundProperty, ShellToken.Background);
+ AutomationProperties.SetName(this, "Toggle theme");
+
+ ShowIcons(ShellTheme.IsDarkMode);
+ foreach (var icon in new[] { _sun, _moon })
+ {
+ icon.Transitions = new Transitions
+ {
+ new DoubleTransition { Property = OpacityProperty, Duration = TimeSpan.FromMilliseconds(200), Easing = new CubicEaseOut() },
+ new TransformOperationsTransition { Property = RenderTransformProperty, Duration = TimeSpan.FromMilliseconds(200), Easing = new CubicEaseOut() },
+ };
+ }
+ }
+
+ public void Toggle()
+ {
+ ShellTheme.ToggleTheme();
+ ThemeChanged?.Invoke(this, ShellTheme.IsDarkMode);
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ ShellTheme.ThemeChanged += OnThemeChanged;
+ ShowIcons(ShellTheme.IsDarkMode);
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+ ShellTheme.ThemeChanged -= OnThemeChanged;
+ }
+
+ protected override void OnPointerEntered(PointerEventArgs e)
+ {
+ base.OnPointerEntered(e);
+ this.Token(BackgroundProperty, ShellToken.Accent);
+ }
+
+ protected override void OnPointerExited(PointerEventArgs e)
+ {
+ base.OnPointerExited(e);
+ this.Token(BackgroundProperty, ShellToken.Background);
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (e.InitialPressMouseButton == MouseButton.Left)
+ {
+ Toggle();
+ e.Handled = true;
+ }
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+ if (e.Key is Key.Enter or Key.Space)
+ {
+ Toggle();
+ e.Handled = true;
+ }
+ }
+
+ private void OnThemeChanged(object? sender, EventArgs e) => ShowIcons(ShellTheme.IsDarkMode);
+
+ private void ShowIcons(bool dark)
+ {
+ var (show, hide) = dark ? (_moon, _sun) : (_sun, _moon);
+ show.Opacity = 1;
+ show.RenderTransform = TransformOperations.Parse("rotate(0deg) scale(1)");
+ hide.Opacity = 0;
+ hide.RenderTransform = TransformOperations.Parse("rotate(-90deg) scale(0.5)");
+ }
+}
diff --git a/examples/Avalonia.Demo/Components/UI/Variants/ButtonVariants.cs b/examples/Avalonia.Demo/Components/UI/Variants/ButtonVariants.cs
new file mode 100644
index 0000000..4f48e7c
--- /dev/null
+++ b/examples/Avalonia.Demo/Components/UI/Variants/ButtonVariants.cs
@@ -0,0 +1,101 @@
+using Avalonia;
+
+namespace AvaloniaDemo.Components.UI.Variants;
+
+public enum ButtonVariant
+{
+ Default,
+ Destructive,
+ Outline,
+ Secondary,
+ Ghost,
+ Link
+}
+
+public enum ButtonSize
+{
+ Default,
+ Sm,
+ Lg,
+ Icon
+}
+
+// Token-based style for one variant + size. Null background/border = transparent.
+public class ButtonStyle
+{
+ public ShellToken? Background { get; set; }
+ public ShellToken? HoverBackground { get; set; }
+ public ShellToken Foreground { get; set; } = ShellToken.Foreground;
+ public ShellToken? Border { get; set; }
+ public double HoverOpacity { get; set; } = 1.0;
+ public bool UnderlineOnHover { get; set; }
+ public double CornerRadius { get; set; } = ShellTheme.RadiusMd;
+ public Thickness Padding { get; set; } = new(16, 0);
+ public double Height { get; set; } = 40;
+ public double Width { get; set; } = double.NaN;
+ public double FontSize { get; set; } = 14;
+ public double IconSize { get; set; } = 16;
+}
+
+// Mirrors ShellUI's buttonVariants (cva): variant → colors, size → box.
+public static class ButtonVariants
+{
+ public static ButtonStyle GetStyle(ButtonVariant variant = ButtonVariant.Default, ButtonSize size = ButtonSize.Default)
+ {
+ var style = new ButtonStyle();
+
+ switch (variant)
+ {
+ case ButtonVariant.Default: // bg-primary text-primary-foreground hover:bg-primary/90
+ style.Background = ShellToken.Primary;
+ style.Foreground = ShellToken.PrimaryForeground;
+ style.HoverOpacity = 0.9;
+ break;
+ case ButtonVariant.Destructive: // bg-destructive text-destructive-foreground hover:bg-destructive/90
+ style.Background = ShellToken.Destructive;
+ style.Foreground = ShellToken.DestructiveForeground;
+ style.HoverOpacity = 0.9;
+ break;
+ case ButtonVariant.Outline: // border border-input bg-background hover:bg-accent
+ style.Background = ShellToken.Background;
+ style.HoverBackground = ShellToken.Accent;
+ style.Border = ShellToken.Input;
+ style.Foreground = ShellToken.Foreground;
+ break;
+ case ButtonVariant.Secondary: // bg-secondary text-secondary-foreground hover:bg-secondary/80
+ style.Background = ShellToken.Secondary;
+ style.Foreground = ShellToken.SecondaryForeground;
+ style.HoverOpacity = 0.8;
+ break;
+ case ButtonVariant.Ghost: // hover:bg-accent hover:text-accent-foreground
+ style.HoverBackground = ShellToken.Accent;
+ style.Foreground = ShellToken.Foreground;
+ break;
+ case ButtonVariant.Link: // text-primary underline-offset-4 hover:underline
+ style.Foreground = ShellToken.Primary;
+ style.UnderlineOnHover = true;
+ break;
+ }
+
+ switch (size)
+ {
+ case ButtonSize.Sm: // h-9 px-3
+ style.Height = 36;
+ style.Padding = new Thickness(12, 0);
+ style.FontSize = 13;
+ break;
+ case ButtonSize.Lg: // h-11 px-8
+ style.Height = 44;
+ style.Padding = new Thickness(32, 0);
+ style.FontSize = 15;
+ break;
+ case ButtonSize.Icon: // h-10 w-10
+ style.Height = 40;
+ style.Width = 40;
+ style.Padding = new Thickness(0);
+ break;
+ }
+
+ return style;
+ }
+}
diff --git a/examples/Avalonia.Demo/MainWindow.axaml b/examples/Avalonia.Demo/MainWindow.axaml
new file mode 100644
index 0000000..f26baba
--- /dev/null
+++ b/examples/Avalonia.Demo/MainWindow.axaml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Avalonia.Demo/MainWindow.axaml.cs b/examples/Avalonia.Demo/MainWindow.axaml.cs
new file mode 100644
index 0000000..1949666
--- /dev/null
+++ b/examples/Avalonia.Demo/MainWindow.axaml.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using AvaloniaDemo.Components.UI;
+
+namespace AvaloniaDemo;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ var clicks = 0;
+ LoadingButton.Clicked += async (_, _) =>
+ {
+ LoadingButton.IsLoading = true;
+ Progress.Value = (Progress.Value + 20) % 120;
+ await System.Threading.Tasks.Task.Delay(1500);
+ LoadingButton.IsLoading = false;
+ ClickCount.Text = $"Saved {++clicks} time(s).";
+ };
+
+ foreach (var token in Enum.GetValues())
+ {
+ var chip = new Border { Width = 40, Height = 40, CornerRadius = new CornerRadius(ShellTheme.RadiusMd), BorderThickness = new Thickness(1) };
+ chip.Token(Border.BackgroundProperty, token).Token(Border.BorderBrushProperty, ShellToken.Border);
+ var label = new TextBlock { Text = token.ToString(), FontSize = 12, HorizontalAlignment = HorizontalAlignment.Center };
+ label.Token(TextBlock.ForegroundProperty, ShellToken.MutedForeground);
+ Swatches.Children.Add(new StackPanel { Width = 120, Margin = new Thickness(0, 0, 8, 12), Spacing = 4, Children = { chip, label } });
+ }
+
+ foreach (var name in Enum.GetValues().Where(n => n != IconName.None))
+ {
+ var label = new TextBlock { Text = name.ToString(), FontSize = 11, HorizontalAlignment = HorizontalAlignment.Center, TextTrimming = Avalonia.Media.TextTrimming.CharacterEllipsis };
+ label.Token(TextBlock.ForegroundProperty, ShellToken.MutedForeground);
+ Icons.Children.Add(new StackPanel { Width = 96, Margin = new Thickness(0, 0, 4, 12), Spacing = 6, Children = { new Icon { Kind = name, Size = 20 }, label } });
+ }
+ }
+}
diff --git a/examples/Avalonia.Demo/Program.cs b/examples/Avalonia.Demo/Program.cs
new file mode 100644
index 0000000..ea52363
--- /dev/null
+++ b/examples/Avalonia.Demo/Program.cs
@@ -0,0 +1,24 @@
+using Avalonia;
+using System;
+
+namespace AvaloniaDemo;
+
+class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+#if DEBUG
+ .WithDeveloperTools()
+#endif
+ .WithInterFont()
+ .LogToTrace();
+}
diff --git a/examples/Avalonia.Demo/app.manifest b/examples/Avalonia.Demo/app.manifest
new file mode 100644
index 0000000..8c77f6b
--- /dev/null
+++ b/examples/Avalonia.Demo/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/README.md b/examples/README.md
index ca6f06c..96e680e 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -7,6 +7,7 @@ This folder contains example projects demonstrating ShellUI Native components.
| Project | Platform | Description |
|---------|----------|-------------|
| [MAUI.Demo](./MAUI.Demo/) | .NET MAUI | Complete MAUI app showcasing all components |
+| [Avalonia.Demo](./Avalonia.Demo/) | Avalonia 12 | Avalonia components as they are ported (Phase 2) |
| WinUI.Demo | WinUI 3 | *(Coming Soon)* |
| WPF.Demo | WPF | *(Coming Soon)* |
@@ -20,15 +21,23 @@ dotnet restore
dotnet build
# Run on Windows
-dotnet run -f net8.0-windows10.0.19041.0
+dotnet run -f net10.0-windows10.0.19041.0
# Run on Android
-dotnet run -f net8.0-android
+dotnet run -f net10.0-android
# Run on iOS (macOS only)
-dotnet run -f net8.0-ios
+dotnet run -f net10.0-ios
```
+### Avalonia Demo
+
+```bash
+dotnet run --project examples/Avalonia.Demo/AvaloniaDemo.csproj
+```
+
+Runs on Windows, macOS and Linux.
+
## Purpose
These examples serve as:
diff --git a/scripts/generate-icons.py b/scripts/generate-icons.py
index 0719f0e..23534a1 100644
--- a/scripts/generate-icons.py
+++ b/scripts/generate-icons.py
@@ -1,5 +1,6 @@
-"""Generate the MAUI `icon` component (examples/MAUI.Demo/Components/UI/Icon.cs) from the
-ShellIcons catalog (Lucide-derived SVGs).
+"""Generate the `icon` component for MAUI and Avalonia (Components/UI/Icon.cs in each demo) from
+the ShellIcons catalog (Lucide-derived SVGs). The icon list and path data are shared; each file
+keeps its own control class.
python scripts/generate-icons.py [path-to-shell-icons]
@@ -18,7 +19,21 @@
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_CATALOG = os.path.normpath(os.path.join(REPO, "..", "..", "icons", "shell-icons"))
-OUTPUT = os.path.join(REPO, "examples", "MAUI.Demo", "Components", "UI", "Icon.cs")
+# (file, usings and namespace, how it draws, usage comment, first line of the control class to keep)
+OUTPUTS = [
+ (os.path.join(REPO, "examples", "MAUI.Demo", "Components", "UI", "Icon.cs"),
+ ["using Microsoft.Maui.Controls.Shapes;", "", "namespace MAUI.Demo.Components.UI;"],
+ "Drawn with MAUI shapes",
+ ['// Usage: Tint with Token="MutedForeground" or Color="Red".'],
+ "public partial class Icon : ContentView"),
+ (os.path.join(REPO, "examples", "Avalonia.Demo", "Components", "UI", "Icon.cs"),
+ ["using System.Collections.Generic;", "using Avalonia;", "using Avalonia.Controls;", "using Avalonia.Layout;", "using Avalonia.Media;", "",
+ "namespace AvaloniaDemo.Components.UI;"],
+ "Drawn as Avalonia geometry",
+ ['// Usage: Tint with Token="MutedForeground" or Color="Red".',
+ "// The icon is picked with Kind: every Avalonia control already has a Name."],
+ "public class Icon : Control"),
+]
ICONS = [
# actions
@@ -315,18 +330,20 @@ def main():
enum_lines = ",\n".join(f" {p}" for p, _, _ in entries)
cases = "\n".join(f' IconName.{p} => "{d}",' for p, _, d in entries)
- header = open(OUTPUT, encoding="utf-8").read()
- view = header[header.index("public partial class Icon : ContentView"):]
+ for output, prelude, drawn, usage, marker in OUTPUTS:
+ current = open(output, encoding="utf-8").read()
+ write(output, "\n".join(prelude), drawn, "\n".join(usage), current[current.index(marker):],
+ version, len(entries), enum_lines, cases)
- out = f"""using Microsoft.Maui.Controls.Shapes;
-namespace MAUI.Demo.Components.UI;
+def write(output, prelude, drawn, usage, view, version, count, enum_lines, cases):
+ out = f"""{prelude}
// Stroke icons from ShellIcons (https://github.com/shellui-dev/shell-icons), derived from
-// Lucide {version} (ISC license) — the icon set ShellUI uses. Drawn with MAUI shapes: no font or
+// Lucide {version} (ISC license) — the icon set ShellUI uses. {drawn}: no font or
// package, crisp at any size, theme-aware. Generated by scripts/generate-icons.py; a curated set
-// of {len(entries)} icons — add names there and re-run to include more.
-// Usage: Tint with Token="MutedForeground" or Color="Red".
+// of {count} icons — add names there and re-run to include more.
+{usage}
public enum IconName
{{
None,
@@ -344,8 +361,8 @@ def main():
}}
{view}"""
- open(OUTPUT, "w", encoding="utf-8", newline="\n").write(out)
- print(f"wrote {len(entries)} icons (Lucide {version}) to {os.path.relpath(OUTPUT, REPO)}")
+ open(output, "w", encoding="utf-8", newline="\n").write(out)
+ print(f"wrote {count} icons (Lucide {version}) to {os.path.relpath(output, REPO)}")
if __name__ == "__main__":
diff --git a/scripts/sync-templates.py b/scripts/sync-templates.py
index 9218212..2d2b88d 100644
--- a/scripts/sync-templates.py
+++ b/scripts/sync-templates.py
@@ -1,11 +1,11 @@
-"""Sync the CLI's MAUI templates from the working demo copies.
+"""Sync the CLI's templates from the working demo copies.
python scripts/sync-templates.py
-
-The demo (examples/MAUI.Demo/Components/UI) is where components are developed and run;
-this copies each file into its *Template.cs as the [NativePlatform.MAUI] verbatim string,
-restoring the YourProjectNamespace placeholder, and recomputes shared dependencies.
+The demos (examples/MAUI.Demo and examples/Avalonia.Demo, Components/UI) are where components
+are developed and run; this copies each file into its *Template.cs as the platform's verbatim
+string, restoring the YourProjectNamespace placeholder. Dependencies come from the MAUI file;
+an Avalonia file may only use dependencies the template already declares.
"""
import os
import re
@@ -13,11 +13,12 @@
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEMO = os.path.join(ROOT, "examples", "MAUI.Demo", "Components", "UI")
+AVALONIA_DEMO = os.path.join(ROOT, "examples", "Avalonia.Demo", "Components", "UI")
TPL = os.path.join(ROOT, "src", "ShellUI.Native.Templates", "Templates")
# New templates that don't exist yet: name -> (registry key, display, description, category, tags)
NEW = {
- "Icon": ("icon", "Icon", "Lucide stroke icons drawn with MAUI shapes (no font or package)", "Utility",
+ "Icon": ("icon", "Icon", "Lucide stroke icons drawn as native shapes (no font or package)", "Utility",
["icon", "lucide", "svg", "utility"]),
"ThemeToggle": ("theme-toggle", "Theme Toggle", "Light/dark mode toggle button", "Utility",
["theme", "dark-mode", "toggle"]),
@@ -99,14 +100,16 @@
ICON_API = re.compile(r"\bnew Icon\b|\bIconName\b")
EXT_API = re.compile(r"\bFindParentOfType\b|\bFindDescendantsOfType\b|\bAnimateExpandAsync\b")
-MAUI_BLOCK = re.compile(r'(\[NativePlatform\.MAUI\] = @")(.*?)(\n"\n \};)', re.S)
+# A block ends at the closing quote that is followed by the next entry or the end of the dictionary.
+MAUI_BLOCK = re.compile(r'(\[NativePlatform\.MAUI\] = @")(.*?)(\n"(?=,\n|\n \};))', re.S)
+AVALONIA_BLOCK = re.compile(r'(\[NativePlatform\.Avalonia\] = @")(.*?)(\n"(?=,\n|\n \};))', re.S)
DEPS_LINE = re.compile(r'^(\s*)Dependencies = new List(?:\s*\{([^}]*)\})?(?:\(\))?,\s*$', re.M)
-def to_template_content(source: str) -> str:
+def to_template_content(source: str, demo_namespace: str = "MAUI.Demo") -> str:
source = source.replace("\r\n", "\n")
- source = source.replace("MAUI.Demo.Components.UI", "YourProjectNamespace.Components.UI")
- if "MAUI.Demo" in source:
+ source = source.replace(f"{demo_namespace}.Components.UI", "YourProjectNamespace.Components.UI")
+ if demo_namespace in source:
raise SystemExit("demo-specific namespace left in source")
# The closing quote sits on its own line after the block, so drop the trailing newline.
return source.rstrip("\n").replace('"', '""')
@@ -200,9 +203,50 @@ def main() -> None:
open(tpl_path, "w", encoding="utf-8", newline="").write(text)
changed.append(f"UPD {name} deps={deps}")
+ changed += sync_avalonia()
print("\n".join(changed))
print(f"{len(changed)} templates written")
+def sync_avalonia() -> list[str]:
+ changed = []
+ # Walks subfolders too (Variants/ButtonVariants.cs), like the MAUI pass.
+ files = sorted((d, f) for d, _, fs in os.walk(AVALONIA_DEMO) for f in fs if f.endswith(".cs"))
+ for dirpath, file in files:
+ name = file[:-3]
+ source = open(os.path.join(dirpath, file), encoding="utf-8-sig").read()
+ content = to_template_content(source, "AvaloniaDemo")
+ tpl_path = os.path.join(TPL, f"{name}Template.cs")
+ if not os.path.exists(tpl_path):
+ print(f"SKIP (Avalonia, no MAUI template yet): {name}")
+ continue
+
+ raw = open(tpl_path, encoding="utf-8-sig", newline="").read()
+ crlf = "\r\n" in raw
+ text = raw.replace("\r\n", "\n")
+
+ deps_match = DEPS_LINE.search(text)
+ declared = re.findall(r'"([^"]+)"', deps_match.group(2) or "") if deps_match else []
+ missing = [d for d in compute_deps(name, source, []) if d not in declared]
+ if missing:
+ raise SystemExit(f"Avalonia {name} uses {missing}, which the template doesn't declare")
+
+ match = AVALONIA_BLOCK.search(text)
+ if match:
+ text = text[:match.start(2)] + content + text[match.end(2):]
+ else:
+ maui = MAUI_BLOCK.search(text)
+ if not maui:
+ raise SystemExit(f"no MAUI block in {tpl_path}")
+ text = text[:maui.end()] + f',\n [NativePlatform.Avalonia] = @"{content}\n"' + text[maui.end():]
+
+ if crlf:
+ text = text.replace("\n", "\r\n")
+ if text != raw:
+ open(tpl_path, "w", encoding="utf-8", newline="").write(text)
+ changed.append(f"AVA {name}")
+ return changed
+
+
if __name__ == "__main__":
main()
diff --git a/src/ShellUI.Native.CLI/Services/ComponentManager.cs b/src/ShellUI.Native.CLI/Services/ComponentManager.cs
index 4b74496..b72deb2 100644
--- a/src/ShellUI.Native.CLI/Services/ComponentManager.cs
+++ b/src/ShellUI.Native.CLI/Services/ComponentManager.cs
@@ -22,8 +22,10 @@ public static void ListComponents(bool installedOnly, bool availableOnly)
var installedNames = config?.InstalledComponents.Select(c => c.Name).ToHashSet() ?? new HashSet();
// Get available components (excluding non-available utility components)
+ // In an initialized project, list only components with a template for its platform
var components = ComponentRegistry.Components.Values
.Where(c => c.IsAvailable)
+ .Where(c => config == null || ComponentRegistry.SupportsPlatform(c.Name, config.TargetPlatform))
.OrderBy(c => c.Category)
.ThenBy(c => c.Name);
diff --git a/src/ShellUI.Native.CLI/Services/InitService.cs b/src/ShellUI.Native.CLI/Services/InitService.cs
index 08e766d..e978ddc 100644
--- a/src/ShellUI.Native.CLI/Services/InitService.cs
+++ b/src/ShellUI.Native.CLI/Services/InitService.cs
@@ -101,8 +101,17 @@ await LogoLoader.RunAsync("Setting up ShellUI Native...", async status =>
AnsiConsole.MarkupLine("\n[green]✓ ShellUI Native initialized successfully![/]");
AnsiConsole.MarkupLine("\n[blue]Next steps:[/]");
- AnsiConsole.MarkupLine(" [dim]1. Add components:[/] shellui-native add button");
- AnsiConsole.MarkupLine(" [dim]2. Browse all:[/] shellui-native list");
+ if (projectInfo.Platform == NativePlatform.Avalonia)
+ {
+ AnsiConsole.MarkupLine(" [dim]1. Load the theme in App.Initialize, after AvaloniaXamlLoader.Load(this):[/] Components.UI.ShellTheme.EnsureInitialized();");
+ AnsiConsole.MarkupLine($" [dim]2. Use the components in XAML:[/] xmlns:ui=\"using:{Markup.Escape(projectInfo.RootNamespace)}.Components.UI\"");
+ AnsiConsole.MarkupLine(" [dim]3. Add components:[/] shellui-native add theme-toggle");
+ }
+ else
+ {
+ AnsiConsole.MarkupLine(" [dim]1. Add components:[/] shellui-native add button");
+ AnsiConsole.MarkupLine(" [dim]2. Browse all:[/] shellui-native list");
+ }
}
private static async Task InstallShellUtilityAsync(ProjectInfo projectInfo, string componentsPath)
diff --git a/src/ShellUI.Native.CLI/Services/ProjectDetector.cs b/src/ShellUI.Native.CLI/Services/ProjectDetector.cs
index 38339df..b30e9b0 100644
--- a/src/ShellUI.Native.CLI/Services/ProjectDetector.cs
+++ b/src/ShellUI.Native.CLI/Services/ProjectDetector.cs
@@ -78,9 +78,20 @@ internal static NativePlatform DetectPlatform(XDocument doc, string csprojPath)
return NativePlatform.Unknown;
}
- private static string DetectRootNamespace(XDocument doc, string projectName)
+ internal static string DetectRootNamespace(XDocument doc, string projectName)
{
var rootNamespace = doc.Descendants("RootNamespace").FirstOrDefault()?.Value;
- return rootNamespace ?? projectName;
+ return string.IsNullOrWhiteSpace(rootNamespace) ? SafeNamespace(projectName) : rootNamespace.Trim();
+ }
+
+ // What `dotnet new` does with a project name: "my-app" → "my_app", "2fa.app" → "_2fa.app".
+ internal static string SafeNamespace(string name)
+ {
+ var parts = name.Split('.').Select(part =>
+ {
+ var safe = new string(part.Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_').ToArray());
+ return safe.Length == 0 || char.IsDigit(safe[0]) ? "_" + safe : safe;
+ });
+ return string.Join('.', parts);
}
}
diff --git a/src/ShellUI.Native.Templates/Templates/AlertTemplate.cs b/src/ShellUI.Native.Templates/Templates/AlertTemplate.cs
index 9e08f3b..64d4bfb 100644
--- a/src/ShellUI.Native.Templates/Templates/AlertTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/AlertTemplate.cs
@@ -118,6 +118,105 @@ private void UpdateVisualState()
}
}
+public enum AlertVariant
+{
+ Default,
+ Destructive,
+ Success,
+ Warning,
+ Info
+}
+",
+ [NativePlatform.Avalonia] = @"using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Callout with icon, title and message — rounded-lg border p-4, icon + text tinted per variant.
+public class Alert : Border
+{
+ public static readonly StyledProperty TitleProperty =
+ AvaloniaProperty.Register(nameof(Title));
+
+ public static readonly StyledProperty MessageProperty =
+ AvaloniaProperty.Register(nameof(Message));
+
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ private readonly Icon _icon;
+ private readonly TextBlock _titleLabel;
+ private readonly TextBlock _messageLabel;
+
+ static Alert()
+ {
+ TitleProperty.Changed.AddClassHandler((a, _) => a.UpdateVisualState());
+ MessageProperty.Changed.AddClassHandler((a, _) => a.UpdateVisualState());
+ VariantProperty.Changed.AddClassHandler((a, _) => a.UpdateVisualState());
+ }
+
+ public string? Title
+ {
+ get => GetValue(TitleProperty);
+ set => SetValue(TitleProperty, value);
+ }
+
+ public string? Message
+ {
+ get => GetValue(MessageProperty);
+ set => SetValue(MessageProperty, value);
+ }
+
+ public AlertVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public Alert()
+ {
+ _icon = new Icon { Size = 16, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 2, 0, 0) };
+ _titleLabel = new TextBlock { FontSize = 14, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap };
+ _messageLabel = new TextBlock { FontSize = 14, TextWrapping = TextWrapping.Wrap };
+
+ var text = new StackPanel { Spacing = 4, Children = { _titleLabel, _messageLabel } };
+ Grid.SetColumn(text, 1);
+ Child = new Grid { ColumnDefinitions = new ColumnDefinitions(""Auto,*""), ColumnSpacing = 12, Children = { _icon, text } };
+ Padding = new Thickness(16, 12);
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(ShellTheme.RadiusLg);
+ this.Token(BackgroundProperty, ShellToken.Card);
+ UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ var (icon, accent) = Variant switch
+ {
+ AlertVariant.Destructive => (IconName.CircleAlert, ShellToken.Destructive),
+ AlertVariant.Success => (IconName.CircleCheck, ShellToken.Success),
+ AlertVariant.Warning => (IconName.TriangleAlert, ShellToken.Warning),
+ AlertVariant.Info => (IconName.Info, ShellToken.Info),
+ _ => (IconName.Info, ShellToken.Foreground)
+ };
+
+ _icon.Kind = icon;
+ _icon.Token = accent;
+ _titleLabel.Text = Title ?? string.Empty;
+ _titleLabel.IsVisible = !string.IsNullOrEmpty(Title);
+ _messageLabel.Text = Message ?? string.Empty;
+ _messageLabel.IsVisible = !string.IsNullOrEmpty(Message);
+
+ // Tinted variants color the title; the message stays readable in the muted foreground.
+ _titleLabel.Token(TextBlock.ForegroundProperty, accent);
+ _messageLabel.Token(TextBlock.ForegroundProperty,
+ Variant == AlertVariant.Default ? ShellToken.MutedForeground : ShellToken.Foreground);
+ this.Token(BorderBrushProperty, Variant == AlertVariant.Default ? ShellToken.Border : accent);
+ }
+}
+
public enum AlertVariant
{
Default,
diff --git a/src/ShellUI.Native.Templates/Templates/BadgeTemplate.cs b/src/ShellUI.Native.Templates/Templates/BadgeTemplate.cs
index 60a5e87..ff5c788 100644
--- a/src/ShellUI.Native.Templates/Templates/BadgeTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/BadgeTemplate.cs
@@ -108,6 +108,89 @@ private void UpdateVisualState()
}
}
+public enum BadgeVariant { Default, Secondary, Destructive, Outline, Success, Warning, Info }
+",
+ [NativePlatform.Avalonia] = @"using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Status pill — rounded-full border px-2.5 py-0.5 text-xs font-semibold.
+public class Badge : Border
+{
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register(nameof(Text));
+
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ private readonly TextBlock _label;
+
+ static Badge()
+ {
+ TextProperty.Changed.AddClassHandler((b, e) => b._label.Text = (string?)e.NewValue ?? string.Empty);
+ VariantProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ }
+
+ public string? Text
+ {
+ get => GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public BadgeVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public Badge()
+ {
+ _label = new TextBlock
+ {
+ FontSize = 12,
+ FontWeight = FontWeight.SemiBold,
+ VerticalAlignment = VerticalAlignment.Center,
+ TextAlignment = TextAlignment.Center
+ };
+
+ Child = _label;
+ Padding = new Thickness(10, 2);
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(999);
+ HorizontalAlignment = HorizontalAlignment.Left;
+ VerticalAlignment = VerticalAlignment.Center;
+ UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ // (background, foreground, border); null background = transparent.
+ var (bg, fg, border) = Variant switch
+ {
+ BadgeVariant.Secondary => ((ShellToken?)ShellToken.Secondary, ShellToken.SecondaryForeground, ShellToken.Secondary),
+ BadgeVariant.Destructive => (ShellToken.Destructive, ShellToken.DestructiveForeground, ShellToken.Destructive),
+ BadgeVariant.Outline => (null, ShellToken.Foreground, ShellToken.Border),
+ BadgeVariant.Success => (ShellToken.Success, ShellToken.SuccessForeground, ShellToken.Success),
+ BadgeVariant.Warning => (ShellToken.Warning, ShellToken.WarningForeground, ShellToken.Warning),
+ BadgeVariant.Info => (ShellToken.Info, ShellToken.InfoForeground, ShellToken.Info),
+ _ => (ShellToken.Primary, ShellToken.PrimaryForeground, ShellToken.Primary)
+ };
+
+ if (bg.HasValue)
+ this.Token(BackgroundProperty, bg.Value);
+ else
+ {
+ this.ClearToken(BackgroundProperty);
+ Background = Brushes.Transparent;
+ }
+ this.Token(BorderBrushProperty, border);
+ _label.Token(TextBlock.ForegroundProperty, fg);
+ }
+}
+
public enum BadgeVariant { Default, Secondary, Destructive, Outline, Success, Warning, Info }
"
};
diff --git a/src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs b/src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs
index cf41cc2..fba83e4 100644
--- a/src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs
@@ -224,6 +224,255 @@ private void StopSpinner()
}
}
+public enum IconPosition { Left, Right }
+",
+ [NativePlatform.Avalonia] = @"using System;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Media.Transformation;
+using Avalonia.Threading;
+using YourProjectNamespace.Components.UI.Variants;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Button with ShellUI variants, sizes, optional icon and loading state.
+// Sizes to its content like an inline-flex button; set HorizontalAlignment=""Stretch"" for a block button.
+// Usage:
+public class Button : Border
+{
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ public static readonly StyledProperty SizeProperty =
+ AvaloniaProperty.Register(nameof(Size));
+
+ public static readonly StyledProperty IsLoadingProperty =
+ AvaloniaProperty.Register(nameof(IsLoading));
+
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register(nameof(Text));
+
+ public static readonly StyledProperty IconProperty =
+ AvaloniaProperty.Register(nameof(Icon));
+
+ public static readonly StyledProperty IconPositionProperty =
+ AvaloniaProperty.Register(nameof(IconPosition));
+
+ private readonly StackPanel _row;
+ private readonly Icon _spinner;
+ private readonly RotateTransform _spin = new();
+ private readonly Icon _icon;
+ private readonly TextBlock _label;
+ private ButtonStyle _style = new();
+ private DispatcherTimer? _spinning;
+ private bool _hovered;
+ private bool _pressed;
+
+ static Button()
+ {
+ VariantProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ SizeProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ IsLoadingProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ TextProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ IconProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ IconPositionProperty.Changed.AddClassHandler((b, _) => b.UpdateVisualState());
+ IsEnabledProperty.Changed.AddClassHandler((b, _) => b.Opacity = b.IsEnabled ? 1.0 : 0.5);
+ }
+
+ public ButtonVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public ButtonSize Size
+ {
+ get => GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ public bool IsLoading
+ {
+ get => GetValue(IsLoadingProperty);
+ set => SetValue(IsLoadingProperty, value);
+ }
+
+ public string? Text
+ {
+ get => GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public IconName Icon
+ {
+ get => GetValue(IconProperty);
+ set => SetValue(IconProperty, value);
+ }
+
+ public IconPosition IconPosition
+ {
+ get => GetValue(IconPositionProperty);
+ set => SetValue(IconPositionProperty, value);
+ }
+
+ public event EventHandler? Clicked;
+
+ public Button()
+ {
+ _spinner = new Icon { Kind = IconName.LoaderCircle, IsVisible = false, RenderTransform = _spin };
+ _icon = new Icon { IsVisible = false };
+ _label = new TextBlock { VerticalAlignment = VerticalAlignment.Center, FontWeight = FontWeight.Medium };
+ _row = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 8,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+
+ Child = _row;
+ HorizontalAlignment = HorizontalAlignment.Left;
+ VerticalAlignment = VerticalAlignment.Center;
+ Cursor = new Cursor(StandardCursorType.Hand);
+ RenderTransform = TransformOperations.Parse(""scale(1)"");
+ Transitions = new Transitions
+ {
+ new TransformOperationsTransition { Property = RenderTransformProperty, Duration = TimeSpan.FromMilliseconds(90), Easing = new CubicEaseOut() }
+ };
+ ShellFocus.Ring(this, this);
+ UpdateVisualState();
+ }
+
+ private bool CanClick => IsEffectivelyEnabled && !IsLoading;
+
+ // Clicks in code, as a pointer click or Enter/Space would.
+ public void Press()
+ {
+ if (CanClick) Clicked?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected override void OnPointerEntered(PointerEventArgs e)
+ {
+ base.OnPointerEntered(e);
+ _hovered = true;
+ ApplyHover();
+ }
+
+ protected override void OnPointerExited(PointerEventArgs e)
+ {
+ base.OnPointerExited(e);
+ _hovered = false;
+ ApplyHover();
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ base.OnPointerPressed(e);
+ if (!CanClick || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
+ _pressed = true;
+ RenderTransform = TransformOperations.Parse(""scale(0.97)"");
+ e.Handled = true;
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (!_pressed) return;
+ _pressed = false;
+ RenderTransform = TransformOperations.Parse(""scale(1)"");
+ // Releasing outside the button cancels the click, as with native buttons.
+ if (new Rect(Bounds.Size).Contains(e.GetPosition(this))) Press();
+ e.Handled = true;
+ }
+
+ protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ base.OnPointerCaptureLost(e);
+ _pressed = false;
+ RenderTransform = TransformOperations.Parse(""scale(1)"");
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+ if (e.Key is not (Key.Enter or Key.Space)) return;
+ Press();
+ e.Handled = true;
+ }
+
+ private void UpdateVisualState()
+ {
+ _style = ButtonVariants.GetStyle(Variant, Size);
+
+ Height = _style.Height;
+ Width = _style.Width;
+ Padding = _style.Padding;
+ CornerRadius = new CornerRadius(_style.CornerRadius);
+ BorderThickness = new Thickness(_style.Border.HasValue ? 1 : 0);
+ if (_style.Border.HasValue)
+ this.Token(BorderBrushProperty, _style.Border.Value);
+
+ _label.Text = Text ?? string.Empty;
+ _label.IsVisible = !string.IsNullOrEmpty(Text);
+ _label.FontSize = _style.FontSize;
+ _label.Token(TextBlock.ForegroundProperty, _style.Foreground);
+
+ _icon.Kind = Icon;
+ _icon.Size = _style.IconSize;
+ _icon.Token = _style.Foreground;
+ _icon.IsVisible = Icon != IconName.None && !IsLoading;
+
+ _spinner.Size = _style.IconSize;
+ _spinner.Token = _style.Foreground;
+ _spinner.IsVisible = IsLoading;
+ if (IsLoading) StartSpinner(); else StopSpinner();
+
+ _row.Children.Clear();
+ _row.Children.Add(_spinner);
+ if (IconPosition == IconPosition.Left) { _row.Children.Add(_icon); _row.Children.Add(_label); }
+ else { _row.Children.Add(_label); _row.Children.Add(_icon); }
+
+ ApplyHover();
+ }
+
+ private void ApplyHover()
+ {
+ var hover = _hovered && CanClick;
+ var background = hover && _style.HoverBackground.HasValue ? _style.HoverBackground : _style.Background;
+ if (background.HasValue)
+ this.Token(BackgroundProperty, background.Value);
+ else
+ {
+ // Transparent, not null, so the whole button stays hit-testable.
+ this.ClearToken(BackgroundProperty);
+ Background = Brushes.Transparent;
+ }
+ Opacity = !IsEnabled ? 0.5 : hover ? _style.HoverOpacity : 1.0;
+ _label.TextDecorations = hover && _style.UnderlineOnHover ? TextDecorations.Underline : null;
+ }
+
+ // One turn per 800 ms, advanced once per frame.
+ private void StartSpinner()
+ {
+ if (_spinning != null) return;
+ _spinning = new DispatcherTimer(TimeSpan.FromMilliseconds(16), DispatcherPriority.Render,
+ (_, _) => _spin.Angle = (_spin.Angle + 360 * 16 / 800.0) % 360);
+ _spinning.Start();
+ }
+
+ private void StopSpinner()
+ {
+ _spinning?.Stop();
+ _spinning = null;
+ _spin.Angle = 0;
+ }
+}
+
public enum IconPosition { Left, Right }
"
};
diff --git a/src/ShellUI.Native.Templates/Templates/ButtonVariantsTemplate.cs b/src/ShellUI.Native.Templates/Templates/ButtonVariantsTemplate.cs
index 0b3b0ec..586d95a 100644
--- a/src/ShellUI.Native.Templates/Templates/ButtonVariantsTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/ButtonVariantsTemplate.cs
@@ -55,6 +55,108 @@ public class ButtonStyle
public double IconSize { get; set; } = 16;
}
+// Mirrors ShellUI's buttonVariants (cva): variant → colors, size → box.
+public static class ButtonVariants
+{
+ public static ButtonStyle GetStyle(ButtonVariant variant = ButtonVariant.Default, ButtonSize size = ButtonSize.Default)
+ {
+ var style = new ButtonStyle();
+
+ switch (variant)
+ {
+ case ButtonVariant.Default: // bg-primary text-primary-foreground hover:bg-primary/90
+ style.Background = ShellToken.Primary;
+ style.Foreground = ShellToken.PrimaryForeground;
+ style.HoverOpacity = 0.9;
+ break;
+ case ButtonVariant.Destructive: // bg-destructive text-destructive-foreground hover:bg-destructive/90
+ style.Background = ShellToken.Destructive;
+ style.Foreground = ShellToken.DestructiveForeground;
+ style.HoverOpacity = 0.9;
+ break;
+ case ButtonVariant.Outline: // border border-input bg-background hover:bg-accent
+ style.Background = ShellToken.Background;
+ style.HoverBackground = ShellToken.Accent;
+ style.Border = ShellToken.Input;
+ style.Foreground = ShellToken.Foreground;
+ break;
+ case ButtonVariant.Secondary: // bg-secondary text-secondary-foreground hover:bg-secondary/80
+ style.Background = ShellToken.Secondary;
+ style.Foreground = ShellToken.SecondaryForeground;
+ style.HoverOpacity = 0.8;
+ break;
+ case ButtonVariant.Ghost: // hover:bg-accent hover:text-accent-foreground
+ style.HoverBackground = ShellToken.Accent;
+ style.Foreground = ShellToken.Foreground;
+ break;
+ case ButtonVariant.Link: // text-primary underline-offset-4 hover:underline
+ style.Foreground = ShellToken.Primary;
+ style.UnderlineOnHover = true;
+ break;
+ }
+
+ switch (size)
+ {
+ case ButtonSize.Sm: // h-9 px-3
+ style.Height = 36;
+ style.Padding = new Thickness(12, 0);
+ style.FontSize = 13;
+ break;
+ case ButtonSize.Lg: // h-11 px-8
+ style.Height = 44;
+ style.Padding = new Thickness(32, 0);
+ style.FontSize = 15;
+ break;
+ case ButtonSize.Icon: // h-10 w-10
+ style.Height = 40;
+ style.Width = 40;
+ style.Padding = new Thickness(0);
+ break;
+ }
+
+ return style;
+ }
+}
+",
+ [NativePlatform.Avalonia] = @"using Avalonia;
+
+namespace YourProjectNamespace.Components.UI.Variants;
+
+public enum ButtonVariant
+{
+ Default,
+ Destructive,
+ Outline,
+ Secondary,
+ Ghost,
+ Link
+}
+
+public enum ButtonSize
+{
+ Default,
+ Sm,
+ Lg,
+ Icon
+}
+
+// Token-based style for one variant + size. Null background/border = transparent.
+public class ButtonStyle
+{
+ public ShellToken? Background { get; set; }
+ public ShellToken? HoverBackground { get; set; }
+ public ShellToken Foreground { get; set; } = ShellToken.Foreground;
+ public ShellToken? Border { get; set; }
+ public double HoverOpacity { get; set; } = 1.0;
+ public bool UnderlineOnHover { get; set; }
+ public double CornerRadius { get; set; } = ShellTheme.RadiusMd;
+ public Thickness Padding { get; set; } = new(16, 0);
+ public double Height { get; set; } = 40;
+ public double Width { get; set; } = double.NaN;
+ public double FontSize { get; set; } = 14;
+ public double IconSize { get; set; } = 16;
+}
+
// Mirrors ShellUI's buttonVariants (cva): variant → colors, size → box.
public static class ButtonVariants
{
diff --git a/src/ShellUI.Native.Templates/Templates/CardContentTemplate.cs b/src/ShellUI.Native.Templates/Templates/CardContentTemplate.cs
index 9cc933e..f0f44ca 100644
--- a/src/ShellUI.Native.Templates/Templates/CardContentTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/CardContentTemplate.cs
@@ -29,6 +29,17 @@ public CardContent()
Padding = new Thickness(24, 0, 24, 24);
}
}
+",
+ [NativePlatform.Avalonia] = @"using Avalonia;
+using Avalonia.Controls;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Card body — p-6 pt-0.
+public class CardContent : Border
+{
+ public CardContent() => Padding = new Thickness(24, 0, 24, 24);
+}
"
};
}
diff --git a/src/ShellUI.Native.Templates/Templates/CardFooterTemplate.cs b/src/ShellUI.Native.Templates/Templates/CardFooterTemplate.cs
index f329deb..d0c75f1 100644
--- a/src/ShellUI.Native.Templates/Templates/CardFooterTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/CardFooterTemplate.cs
@@ -40,6 +40,24 @@ public CardFooter()
Content = _content;
}
}
+",
+ [NativePlatform.Avalonia] = @"using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Card footer — flex items-center p-6 pt-0; actions right-aligned.
+public class CardFooter : StackPanel
+{
+ public CardFooter()
+ {
+ Orientation = Orientation.Horizontal;
+ Spacing = 8;
+ HorizontalAlignment = HorizontalAlignment.Right;
+ Margin = new Thickness(24, 0, 24, 24);
+ }
+}
"
};
}
diff --git a/src/ShellUI.Native.Templates/Templates/CardHeaderTemplate.cs b/src/ShellUI.Native.Templates/Templates/CardHeaderTemplate.cs
index 0239118..ef38752 100644
--- a/src/ShellUI.Native.Templates/Templates/CardHeaderTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/CardHeaderTemplate.cs
@@ -69,6 +69,61 @@ private void UpdateText()
_description.IsVisible = !string.IsNullOrEmpty(Description);
}
}
+",
+ [NativePlatform.Avalonia] = @"using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Card header — p-6 space-y-1.5; title font-semibold, description text-sm text-muted-foreground.
+public class CardHeader : Border
+{
+ public static readonly StyledProperty TitleProperty =
+ AvaloniaProperty.Register(nameof(Title));
+
+ public static readonly StyledProperty DescriptionProperty =
+ AvaloniaProperty.Register(nameof(Description));
+
+ private readonly TextBlock _title;
+ private readonly TextBlock _description;
+
+ static CardHeader()
+ {
+ TitleProperty.Changed.AddClassHandler((h, _) => h.UpdateText());
+ DescriptionProperty.Changed.AddClassHandler((h, _) => h.UpdateText());
+ }
+
+ public string? Title
+ {
+ get => GetValue(TitleProperty);
+ set => SetValue(TitleProperty, value);
+ }
+
+ public string? Description
+ {
+ get => GetValue(DescriptionProperty);
+ set => SetValue(DescriptionProperty, value);
+ }
+
+ public CardHeader()
+ {
+ _title = new TextBlock { FontSize = 18, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap };
+ _title.Token(TextBlock.ForegroundProperty, ShellToken.CardForeground);
+ _description = new TextBlock { FontSize = 14, TextWrapping = TextWrapping.Wrap, IsVisible = false };
+ _description.Token(TextBlock.ForegroundProperty, ShellToken.MutedForeground);
+
+ Child = new StackPanel { Spacing = 6, Children = { _title, _description } };
+ Padding = new Thickness(24, 24, 24, 16);
+ }
+
+ private void UpdateText()
+ {
+ _title.Text = Title ?? string.Empty;
+ _description.Text = Description ?? string.Empty;
+ _description.IsVisible = !string.IsNullOrEmpty(Description);
+ }
+}
"
};
}
diff --git a/src/ShellUI.Native.Templates/Templates/CardTemplate.cs b/src/ShellUI.Native.Templates/Templates/CardTemplate.cs
index 2b266ab..b7448c2 100644
--- a/src/ShellUI.Native.Templates/Templates/CardTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/CardTemplate.cs
@@ -90,6 +90,78 @@ private void UpdateVisuals()
}
}
+public enum CardVariant { Default, Elevated }
+",
+ [NativePlatform.Avalonia] = @"using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Metadata;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Card container — rounded-lg border bg-card shadow-sm.
+// The parts stack inside; XAML children go to Items.
+// Usage: ... ...
+public class Card : Border
+{
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ public static readonly StyledProperty IsPressableProperty =
+ AvaloniaProperty.Register(nameof(IsPressable));
+
+ private const double Radius = ShellTheme.RadiusLg + 4;
+
+ private readonly StackPanel _items = new();
+
+ static Card()
+ {
+ VariantProperty.Changed.AddClassHandler((c, _) => c.UpdateShadow());
+ IsPressableProperty.Changed.AddClassHandler((c, _) =>
+ c.Cursor = c.IsPressable ? new Cursor(StandardCursorType.Hand) : null);
+ }
+
+ public CardVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public bool IsPressable
+ {
+ get => GetValue(IsPressableProperty);
+ set => SetValue(IsPressableProperty, value);
+ }
+
+ public event EventHandler? Clicked;
+
+ [Content]
+ public Controls Items => _items.Children;
+
+ public Card()
+ {
+ Child = _items;
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(Radius);
+ this.Token(BackgroundProperty, ShellToken.Card).Token(BorderBrushProperty, ShellToken.Border);
+ UpdateShadow();
+ }
+
+ private void UpdateShadow() => BoxShadow = new BoxShadows(Variant == CardVariant.Elevated
+ ? new BoxShadow { OffsetY = 4, Blur = 12, Color = ShellTheme.Shadow(0.10) }
+ : new BoxShadow { OffsetY = 1, Blur = 2, Color = ShellTheme.Shadow(0.05) });
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (!IsPressable || e.InitialPressMouseButton != MouseButton.Left) return;
+ Clicked?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ }
+}
+
public enum CardVariant { Default, Elevated }
"
};
diff --git a/src/ShellUI.Native.Templates/Templates/CheckboxTemplate.cs b/src/ShellUI.Native.Templates/Templates/CheckboxTemplate.cs
index 10083be..c5531a4 100644
--- a/src/ShellUI.Native.Templates/Templates/CheckboxTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/CheckboxTemplate.cs
@@ -130,6 +130,124 @@ private void UpdateVisualState()
_check.IsVisible = IsChecked;
}
}
+",
+ [NativePlatform.Avalonia] = @"using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Checkbox — h-4 w-4 rounded-sm border border-primary; checked: bg-primary + check icon.
+// The whole row (box + label) is the hit target.
+public class Checkbox : Border
+{
+ public static readonly StyledProperty IsCheckedProperty =
+ AvaloniaProperty.Register(nameof(IsChecked), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty LabelProperty =
+ AvaloniaProperty.Register(nameof(Label));
+
+ public static readonly StyledProperty HasErrorProperty =
+ AvaloniaProperty.Register(nameof(HasError));
+
+ private readonly Border _box;
+ private readonly Icon _check;
+ private readonly TextBlock _label;
+
+ static Checkbox()
+ {
+ IsCheckedProperty.Changed.AddClassHandler((c, e) =>
+ {
+ c.UpdateVisualState();
+ c.CheckedChanged?.Invoke(c, (bool)e.NewValue!);
+ });
+ LabelProperty.Changed.AddClassHandler((c, _) => c.UpdateLabel());
+ HasErrorProperty.Changed.AddClassHandler((c, _) => c.UpdateVisualState());
+ IsEnabledProperty.Changed.AddClassHandler((c, _) => c.Opacity = c.IsEnabled ? 1.0 : 0.5);
+ }
+
+ public bool IsChecked
+ {
+ get => GetValue(IsCheckedProperty);
+ set => SetValue(IsCheckedProperty, value);
+ }
+
+ public string? Label
+ {
+ get => GetValue(LabelProperty);
+ set => SetValue(LabelProperty, value);
+ }
+
+ public bool HasError
+ {
+ get => GetValue(HasErrorProperty);
+ set => SetValue(HasErrorProperty, value);
+ }
+
+ public event EventHandler? CheckedChanged;
+
+ public Checkbox()
+ {
+ _check = new Icon { Kind = IconName.Check, Size = 12, StrokeWidth = 3, Token = ShellToken.PrimaryForeground };
+ _box = new Border
+ {
+ Child = _check,
+ Width = 16,
+ Height = 16,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(ShellTheme.RadiusSm),
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ _label = new TextBlock { FontSize = 14, VerticalAlignment = VerticalAlignment.Center, IsVisible = false };
+ _label.Token(TextBlock.ForegroundProperty, ShellToken.Foreground);
+
+ Child = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, MinHeight = 24, Children = { _box, _label } };
+ Background = Brushes.Transparent;
+ HorizontalAlignment = HorizontalAlignment.Left;
+ Cursor = new Cursor(StandardCursorType.Hand);
+ ShellFocus.Ring(this, _box);
+ UpdateVisualState();
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (e.InitialPressMouseButton != MouseButton.Left || !IsEffectivelyEnabled) return;
+ IsChecked = !IsChecked;
+ e.Handled = true;
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+ if (e.Key != Key.Space) return;
+ IsChecked = !IsChecked;
+ e.Handled = true;
+ }
+
+ private void UpdateLabel()
+ {
+ _label.Text = Label ?? string.Empty;
+ _label.IsVisible = !string.IsNullOrEmpty(Label);
+ }
+
+ private void UpdateVisualState()
+ {
+ _box.Token(BorderBrushProperty, HasError ? ShellToken.Destructive : ShellToken.Primary);
+ if (IsChecked)
+ _box.Token(BackgroundProperty, ShellToken.Primary);
+ else
+ {
+ _box.ClearToken(BackgroundProperty);
+ _box.Background = Brushes.Transparent;
+ }
+ _check.IsVisible = IsChecked;
+ }
+}
"
};
diff --git a/src/ShellUI.Native.Templates/Templates/IconTemplate.cs b/src/ShellUI.Native.Templates/Templates/IconTemplate.cs
index 02cc764..a7d2c93 100644
--- a/src/ShellUI.Native.Templates/Templates/IconTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/IconTemplate.cs
@@ -8,7 +8,7 @@ public static class IconTemplate
{
Name = "icon",
DisplayName = "Icon",
- Description = "Lucide stroke icons drawn with MAUI shapes (no font or package)",
+ Description = "Lucide stroke icons drawn as native shapes (no font or package)",
Category = ComponentCategory.Utility,
FilePath = "Icon.cs",
Dependencies = new List { "shell" },
@@ -362,6 +362,344 @@ private void UpdateColor()
}
}
}
+",
+ [NativePlatform.Avalonia] = @"using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Stroke icons from ShellIcons (https://github.com/shellui-dev/shell-icons), derived from
+// Lucide 0.475.0 (ISC license) — the icon set ShellUI uses. Drawn as Avalonia geometry: no font or
+// package, crisp at any size, theme-aware. Generated by scripts/generate-icons.py; a curated set
+// of 110 icons — add names there and re-run to include more.
+// Usage: Tint with Token=""MutedForeground"" or Color=""Red"".
+// The icon is picked with Kind: every Avalonia control already has a Name.
+public enum IconName
+{
+ None,
+ Check,
+ X,
+ Plus,
+ Minus,
+ Search,
+ Filter,
+ Settings,
+ SlidersHorizontal,
+ Pencil,
+ Trash,
+ Trash2,
+ Copy,
+ Clipboard,
+ Save,
+ Download,
+ Upload,
+ Share,
+ Share2,
+ ExternalLink,
+ Link,
+ RefreshCw,
+ RotateCcw,
+ Undo2,
+ Redo2,
+ LogIn,
+ LogOut,
+ Send,
+ Paperclip,
+ ChevronDown,
+ ChevronUp,
+ ChevronLeft,
+ ChevronRight,
+ ChevronsUpDown,
+ ChevronsLeft,
+ ChevronsRight,
+ ArrowLeft,
+ ArrowRight,
+ ArrowUp,
+ ArrowDown,
+ ArrowUpRight,
+ Menu,
+ Ellipsis,
+ EllipsisVertical,
+ GripVertical,
+ House,
+ LayoutDashboard,
+ PanelLeft,
+ Info,
+ CircleAlert,
+ CircleCheck,
+ CircleX,
+ CircleHelp,
+ TriangleAlert,
+ Ban,
+ Loader,
+ LoaderCircle,
+ Bell,
+ Circle,
+ CirclePlus,
+ Dot,
+ User,
+ Users,
+ UserPlus,
+ Lock,
+ LockOpen,
+ Key,
+ Shield,
+ Eye,
+ EyeOff,
+ Mail,
+ Inbox,
+ Archive,
+ MessageCircle,
+ Phone,
+ Calendar,
+ Clock,
+ Globe,
+ MapPin,
+ Image,
+ File,
+ FileText,
+ Folder,
+ Tag,
+ Bookmark,
+ Heart,
+ Star,
+ ThumbsUp,
+ ShoppingCart,
+ CreditCard,
+ DollarSign,
+ Activity,
+ TrendingUp,
+ ChartColumn,
+ Zap,
+ Sparkles,
+ Command,
+ Terminal,
+ Code,
+ Moon,
+ Sun,
+ Monitor,
+ Smartphone,
+ Laptop,
+ Wifi,
+ Camera,
+ Mic,
+ Play,
+ Pause,
+ Volume2,
+ Lightbulb
+}
+
+public static class IconPaths
+{
+ // Absolute path data on Lucide's 24x24 grid, drawn with a 2px round stroke.
+ public static string Get(IconName name) => name switch
+ {
+ IconName.Check => ""M 20 6 L 9 17 L 4 12"",
+ IconName.X => ""M 18 6 L 6 18 M 6 6 L 18 18"",
+ IconName.Plus => ""M 5 12 L 19 12 M 12 5 L 12 19"",
+ IconName.Minus => ""M 5 12 L 19 12"",
+ IconName.Search => ""M 19 11 C 19 15.418 15.418 19 11 19 C 6.582 19 3 15.418 3 11 C 3 6.582 6.582 3 11 3 C 15.418 3 19 6.582 19 11 Z M 21 21 L 16.7 16.7"",
+ IconName.Filter => ""M 22 3 L 2 3 L 10 12.46 L 10 19 L 14 21 L 14 12.46 L 22 3 Z"",
+ IconName.Settings => ""M 12.22 2 L 11.78 2 C 10.675 2 9.78 2.895 9.78 4 L 9.78 4.18 C 9.779 4.894 9.398 5.553 8.78 5.91 L 8.35 6.16 C 7.731 6.517 6.969 6.517 6.35 6.16 L 6.2 6.08 C 5.244 5.529 4.023 5.855 3.47 6.81 L 3.25 7.19 C 2.699 8.146 3.025 9.367 3.98 9.92 L 4.13 10.02 C 4.745 10.375 5.126 11.03 5.13 11.74 L 5.13 12.25 C 5.133 12.967 4.751 13.631 4.13 13.99 L 3.98 14.08 C 3.025 14.633 2.699 15.854 3.25 16.81 L 3.47 17.19 C 4.023 18.145 5.244 18.471 6.2 17.92 L 6.35 17.84 C 6.969 17.483 7.731 17.483 8.35 17.84 L 8.78 18.09 C 9.398 18.447 9.779 19.106 9.78 19.82 L 9.78 20 C 9.78 21.105 10.675 22 11.78 22 L 12.22 22 C 13.325 22 14.22 21.105 14.22 20 L 14.22 19.82 C 14.221 19.106 14.602 18.447 15.22 18.09 L 15.65 17.84 C 16.269 17.483 17.031 17.483 17.65 17.84 L 17.8 17.92 C 18.756 18.471 19.977 18.145 20.53 17.19 L 20.75 16.8 C 21.301 15.844 20.975 14.623 20.02 14.07 L 19.87 13.99 C 19.249 13.631 18.867 12.967 18.87 12.25 L 18.87 11.75 C 18.867 11.033 19.249 10.369 19.87 10.01 L 20.02 9.92 C 20.975 9.367 21.301 8.146 20.75 7.19 L 20.53 6.81 C 19.977 5.855 18.756 5.529 17.8 6.08 L 17.65 6.16 C 17.031 6.517 16.269 6.517 15.65 6.16 L 15.22 5.91 C 14.602 5.553 14.221 4.894 14.22 4.18 L 14.22 4 C 14.22 2.895 13.325 2 12.22 2 Z M 15 12 C 15 13.657 13.657 15 12 15 C 10.343 15 9 13.657 9 12 C 9 10.343 10.343 9 12 9 C 13.657 9 15 10.343 15 12 Z"",
+ IconName.SlidersHorizontal => ""M 21 4 L 14 4 M 10 4 L 3 4 M 21 12 L 12 12 M 8 12 L 3 12 M 21 20 L 16 20 M 12 20 L 3 20 M 14 2 L 14 6 M 8 10 L 8 14 M 16 18 L 16 22"",
+ IconName.Pencil => ""M 21.174 6.812 C 22.275 5.711 22.275 3.926 21.174 2.826 C 20.074 1.725 18.289 1.724 17.188 2.825 L 3.842 16.174 C 3.61 16.406 3.438 16.691 3.342 17.004 L 2.021 21.356 C 1.968 21.532 2.017 21.723 2.147 21.853 C 2.277 21.983 2.468 22.031 2.644 21.978 L 6.997 20.658 C 7.31 20.563 7.595 20.392 7.827 20.161 Z M 15 5 L 19 9"",
+ IconName.Trash => ""M 3 6 L 21 6 M 19 6 L 19 20 C 19 21 18 22 17 22 L 7 22 C 6 22 5 21 5 20 L 5 6 M 8 6 L 8 4 C 8 3 9 2 10 2 L 14 2 C 15 2 16 3 16 4 L 16 6"",
+ IconName.Trash2 => ""M 3 6 L 21 6 M 19 6 L 19 20 C 19 21 18 22 17 22 L 7 22 C 6 22 5 21 5 20 L 5 6 M 8 6 L 8 4 C 8 3 9 2 10 2 L 14 2 C 15 2 16 3 16 4 L 16 6 M 10 11 L 10 17 M 14 11 L 14 17"",
+ IconName.Copy => ""M 10 8 L 20 8 C 21.105 8 22 8.895 22 10 L 22 20 C 22 21.105 21.105 22 20 22 L 10 22 C 8.895 22 8 21.105 8 20 L 8 10 C 8 8.895 8.895 8 10 8 Z M 4 16 C 2.9 16 2 15.1 2 14 L 2 4 C 2 2.9 2.9 2 4 2 L 14 2 C 15.1 2 16 2.9 16 4"",
+ IconName.Clipboard => ""M 9 2 L 15 2 C 15.552 2 16 2.448 16 3 L 16 5 C 16 5.552 15.552 6 15 6 L 9 6 C 8.448 6 8 5.552 8 5 L 8 3 C 8 2.448 8.448 2 9 2 Z M 16 4 L 18 4 C 19.105 4 20 4.895 20 6 L 20 20 C 20 21.105 19.105 22 18 22 L 6 22 C 4.895 22 4 21.105 4 20 L 4 6 C 4 4.895 4.895 4 6 4 L 8 4"",
+ IconName.Save => ""M 15.2 3 C 15.728 3.008 16.231 3.223 16.6 3.6 L 20.4 7.4 C 20.777 7.769 20.992 8.272 21 8.8 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 5 C 3 3.895 3.895 3 5 3 Z M 17 21 L 17 14 C 17 13.448 16.552 13 16 13 L 8 13 C 7.448 13 7 13.448 7 14 L 7 21 M 7 3 L 7 7 C 7 7.552 7.448 8 8 8 L 15 8"",
+ IconName.Download => ""M 21 15 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 15 M 7 10 L 12 15 L 17 10 M 12 15 L 12 3"",
+ IconName.Upload => ""M 21 15 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 15 M 17 8 L 12 3 L 7 8 M 12 3 L 12 15"",
+ IconName.Share => ""M 4 12 L 4 20 C 4 21.105 4.895 22 6 22 L 18 22 C 19.105 22 20 21.105 20 20 L 20 12 M 16 6 L 12 2 L 8 6 M 12 2 L 12 15"",
+ IconName.Share2 => ""M 21 5 C 21 6.657 19.657 8 18 8 C 16.343 8 15 6.657 15 5 C 15 3.343 16.343 2 18 2 C 19.657 2 21 3.343 21 5 Z M 9 12 C 9 13.657 7.657 15 6 15 C 4.343 15 3 13.657 3 12 C 3 10.343 4.343 9 6 9 C 7.657 9 9 10.343 9 12 Z M 21 19 C 21 20.657 19.657 22 18 22 C 16.343 22 15 20.657 15 19 C 15 17.343 16.343 16 18 16 C 19.657 16 21 17.343 21 19 Z M 8.59 13.51 L 15.42 17.49 M 15.41 6.51 L 8.59 10.49"",
+ IconName.ExternalLink => ""M 15 3 L 21 3 L 21 9 M 10 14 L 21 3 M 18 13 L 18 19 C 18 20.105 17.105 21 16 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 8 C 3 6.895 3.895 6 5 6 L 11 6"",
+ IconName.Link => ""M 10 13 C 10.869 14.162 12.2 14.889 13.647 14.992 C 15.094 15.096 16.514 14.566 17.54 13.54 L 20.54 10.54 C 22.435 8.578 22.408 5.46 20.479 3.531 C 18.55 1.602 15.432 1.575 13.47 3.47 L 11.75 5.18 M 14 11 C 13.131 9.838 11.8 9.111 10.353 9.008 C 8.906 8.904 7.486 9.434 6.46 10.46 L 3.46 13.46 C 1.565 15.422 1.592 18.54 3.521 20.469 C 5.45 22.398 8.568 22.425 10.53 20.53 L 12.24 18.82"",
+ IconName.RefreshCw => ""M 3 12 C 3 7.029 7.029 3 12 3 C 14.516 3.009 16.931 3.991 18.74 5.74 L 21 8 M 21 3 L 21 8 L 16 8 M 21 12 C 21 16.971 16.971 21 12 21 C 9.484 20.991 7.069 20.009 5.26 18.26 L 3 16 M 8 16 L 3 16 L 3 21"",
+ IconName.RotateCcw => ""M 3 12 C 3 16.971 7.029 21 12 21 C 16.971 21 21 16.971 21 12 C 21 7.029 16.971 3 12 3 C 9.484 3.009 7.069 3.991 5.26 5.74 L 3 8 M 3 3 L 3 8 L 8 8"",
+ IconName.Undo2 => ""M 9 14 L 4 9 L 9 4 M 4 9 L 14.5 9 C 17.538 9 20 11.462 20 14.5 C 20 17.538 17.538 20 14.5 20 L 11 20"",
+ IconName.Redo2 => ""M 15 14 L 20 9 L 15 4 M 20 9 L 9.5 9 C 6.462 9 4 11.462 4 14.5 C 4 17.538 6.462 20 9.5 20 L 13 20"",
+ IconName.LogIn => ""M 15 3 L 19 3 C 20.105 3 21 3.895 21 5 L 21 19 C 21 20.105 20.105 21 19 21 L 15 21 M 10 17 L 15 12 L 10 7 M 15 12 L 3 12"",
+ IconName.LogOut => ""M 9 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 5 C 3 3.895 3.895 3 5 3 L 9 3 M 16 17 L 21 12 L 16 7 M 21 12 L 9 12"",
+ IconName.Send => ""M 14.536 21.686 C 14.614 21.88 14.804 22.005 15.013 22 C 15.222 21.994 15.405 21.86 15.473 21.662 L 21.973 2.662 C 22.038 2.481 21.993 2.279 21.857 2.143 C 21.721 2.007 21.519 1.962 21.338 2.027 L 2.338 8.527 C 2.14 8.595 2.006 8.778 2 8.987 C 1.995 9.196 2.12 9.386 2.314 9.464 L 10.244 12.644 C 10.751 12.847 11.152 13.248 11.356 13.754 Z M 21.854 2.147 L 10.914 13.086"",
+ IconName.Paperclip => ""M 13.234 20.252 L 21 12.3 M 16 6 L 7.586 14.586 C 6.805 15.367 6.805 16.633 7.586 17.414 C 8.367 18.195 9.633 18.195 10.414 17.414 L 18.828 8.828 C 20.39 7.266 20.39 4.734 18.828 3.172 C 17.266 1.61 14.734 1.61 13.172 3.172 L 4.757 11.757 C 2.414 14.1 2.414 17.9 4.757 20.243 C 7.1 22.586 10.9 22.586 13.243 20.243"",
+ IconName.ChevronDown => ""M 6 9 L 12 15 L 18 9"",
+ IconName.ChevronUp => ""M 18 15 L 12 9 L 6 15"",
+ IconName.ChevronLeft => ""M 15 18 L 9 12 L 15 6"",
+ IconName.ChevronRight => ""M 9 18 L 15 12 L 9 6"",
+ IconName.ChevronsUpDown => ""M 7 15 L 12 20 L 17 15 M 7 9 L 12 4 L 17 9"",
+ IconName.ChevronsLeft => ""M 11 17 L 6 12 L 11 7 M 18 17 L 13 12 L 18 7"",
+ IconName.ChevronsRight => ""M 6 17 L 11 12 L 6 7 M 13 17 L 18 12 L 13 7"",
+ IconName.ArrowLeft => ""M 12 19 L 5 12 L 12 5 M 19 12 L 5 12"",
+ IconName.ArrowRight => ""M 5 12 L 19 12 M 12 5 L 19 12 L 12 19"",
+ IconName.ArrowUp => ""M 5 12 L 12 5 L 19 12 M 12 19 L 12 5"",
+ IconName.ArrowDown => ""M 12 5 L 12 19 M 19 12 L 12 19 L 5 12"",
+ IconName.ArrowUpRight => ""M 7 7 L 17 7 L 17 17 M 7 17 L 17 7"",
+ IconName.Menu => ""M 4 12 L 20 12 M 4 6 L 20 6 M 4 18 L 20 18"",
+ IconName.Ellipsis => ""M 13 12 C 13 12.552 12.552 13 12 13 C 11.448 13 11 12.552 11 12 C 11 11.448 11.448 11 12 11 C 12.552 11 13 11.448 13 12 Z M 20 12 C 20 12.552 19.552 13 19 13 C 18.448 13 18 12.552 18 12 C 18 11.448 18.448 11 19 11 C 19.552 11 20 11.448 20 12 Z M 6 12 C 6 12.552 5.552 13 5 13 C 4.448 13 4 12.552 4 12 C 4 11.448 4.448 11 5 11 C 5.552 11 6 11.448 6 12 Z"",
+ IconName.EllipsisVertical => ""M 13 12 C 13 12.552 12.552 13 12 13 C 11.448 13 11 12.552 11 12 C 11 11.448 11.448 11 12 11 C 12.552 11 13 11.448 13 12 Z M 13 5 C 13 5.552 12.552 6 12 6 C 11.448 6 11 5.552 11 5 C 11 4.448 11.448 4 12 4 C 12.552 4 13 4.448 13 5 Z M 13 19 C 13 19.552 12.552 20 12 20 C 11.448 20 11 19.552 11 19 C 11 18.448 11.448 18 12 18 C 12.552 18 13 18.448 13 19 Z"",
+ IconName.GripVertical => ""M 10 12 C 10 12.552 9.552 13 9 13 C 8.448 13 8 12.552 8 12 C 8 11.448 8.448 11 9 11 C 9.552 11 10 11.448 10 12 Z M 10 5 C 10 5.552 9.552 6 9 6 C 8.448 6 8 5.552 8 5 C 8 4.448 8.448 4 9 4 C 9.552 4 10 4.448 10 5 Z M 10 19 C 10 19.552 9.552 20 9 20 C 8.448 20 8 19.552 8 19 C 8 18.448 8.448 18 9 18 C 9.552 18 10 18.448 10 19 Z M 16 12 C 16 12.552 15.552 13 15 13 C 14.448 13 14 12.552 14 12 C 14 11.448 14.448 11 15 11 C 15.552 11 16 11.448 16 12 Z M 16 5 C 16 5.552 15.552 6 15 6 C 14.448 6 14 5.552 14 5 C 14 4.448 14.448 4 15 4 C 15.552 4 16 4.448 16 5 Z M 16 19 C 16 19.552 15.552 20 15 20 C 14.448 20 14 19.552 14 19 C 14 18.448 14.448 18 15 18 C 15.552 18 16 18.448 16 19 Z"",
+ IconName.House => ""M 15 21 L 15 13 C 15 12.448 14.552 12 14 12 L 10 12 C 9.448 12 9 12.448 9 13 L 9 21 M 3 10 C 3 9.411 3.259 8.852 3.709 8.472 L 10.709 2.473 C 11.454 1.843 12.546 1.843 13.291 2.473 L 20.291 8.472 C 20.741 8.852 21 9.411 21 10 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 Z"",
+ IconName.LayoutDashboard => ""M 4 3 L 9 3 C 9.552 3 10 3.448 10 4 L 10 11 C 10 11.552 9.552 12 9 12 L 4 12 C 3.448 12 3 11.552 3 11 L 3 4 C 3 3.448 3.448 3 4 3 Z M 15 3 L 20 3 C 20.552 3 21 3.448 21 4 L 21 7 C 21 7.552 20.552 8 20 8 L 15 8 C 14.448 8 14 7.552 14 7 L 14 4 C 14 3.448 14.448 3 15 3 Z M 15 12 L 20 12 C 20.552 12 21 12.448 21 13 L 21 20 C 21 20.552 20.552 21 20 21 L 15 21 C 14.448 21 14 20.552 14 20 L 14 13 C 14 12.448 14.448 12 15 12 Z M 4 16 L 9 16 C 9.552 16 10 16.448 10 17 L 10 20 C 10 20.552 9.552 21 9 21 L 4 21 C 3.448 21 3 20.552 3 20 L 3 17 C 3 16.448 3.448 16 4 16 Z"",
+ IconName.PanelLeft => ""M 5 3 L 19 3 C 20.105 3 21 3.895 21 5 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 5 C 3 3.895 3.895 3 5 3 Z M 9 3 L 9 21"",
+ IconName.Info => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 12 16 L 12 12 M 12 8 L 12.01 8"",
+ IconName.CircleAlert => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 12 8 L 12 12 M 12 16 L 12.01 16"",
+ IconName.CircleCheck => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 9 12 L 11 14 L 15 10"",
+ IconName.CircleX => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 15 9 L 9 15 M 9 9 L 15 15"",
+ IconName.CircleHelp => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 9.09 9 C 9.575 7.62 10.985 6.791 12.427 7.039 C 13.869 7.286 14.922 8.537 14.92 10 C 14.92 12 11.92 13 11.92 13 M 12 17 L 12.01 17"",
+ IconName.TriangleAlert => ""M 21.73 18 L 13.73 4 C 13.375 3.373 12.71 2.986 11.99 2.986 C 11.27 2.986 10.605 3.373 10.25 4 L 2.25 18 C 1.891 18.622 1.893 19.388 2.254 20.008 C 2.616 20.628 3.282 21.006 4 21 L 20 21 C 20.714 20.999 21.374 20.618 21.731 19.999 C 22.087 19.38 22.087 18.618 21.73 18 M 12 9 L 12 13 M 12 17 L 12.01 17"",
+ IconName.Ban => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 4.9 4.9 L 19.1 19.1"",
+ IconName.Loader => ""M 12 2 L 12 6 M 16.2 7.8 L 19.1 4.9 M 18 12 L 22 12 M 16.2 16.2 L 19.1 19.1 M 12 18 L 12 22 M 4.9 19.1 L 7.8 16.2 M 2 12 L 6 12 M 4.9 4.9 L 7.8 7.8"",
+ IconName.LoaderCircle => ""M 21 12 C 21 15.899 18.489 19.354 14.781 20.559 C 11.073 21.764 7.01 20.444 4.719 17.29 C 2.427 14.135 2.427 9.864 4.719 6.709 C 7.011 3.555 11.073 2.235 14.781 3.44"",
+ IconName.Bell => ""M 10.268 21 C 10.625 21.619 11.286 22 12 22 C 12.714 22 13.375 21.619 13.732 21 M 3.262 15.326 C 2.995 15.619 2.926 16.041 3.086 16.403 C 3.245 16.766 3.604 17 4 17 L 20 17 C 20.396 17 20.755 16.767 20.915 16.405 C 21.075 16.042 21.006 15.62 20.74 15.327 C 19.41 13.956 18 12.499 18 8 C 18 4.686 15.314 2 12 2 C 8.686 2 6 4.686 6 8 C 6 12.499 4.589 13.956 3.262 15.326"",
+ IconName.Circle => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z"",
+ IconName.CirclePlus => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 8 12 L 16 12 M 12 8 L 12 16"",
+ IconName.Dot => ""M 13.1 12.1 C 13.1 12.652 12.652 13.1 12.1 13.1 C 11.548 13.1 11.1 12.652 11.1 12.1 C 11.1 11.548 11.548 11.1 12.1 11.1 C 12.652 11.1 13.1 11.548 13.1 12.1 Z"",
+ IconName.User => ""M 19 21 L 19 19 C 19 16.791 17.209 15 15 15 L 9 15 C 6.791 15 5 16.791 5 19 L 5 21 M 16 7 C 16 9.209 14.209 11 12 11 C 9.791 11 8 9.209 8 7 C 8 4.791 9.791 3 12 3 C 14.209 3 16 4.791 16 7 Z"",
+ IconName.Users => ""M 16 21 L 16 19 C 16 16.791 14.209 15 12 15 L 6 15 C 3.791 15 2 16.791 2 19 L 2 21 M 13 7 C 13 9.209 11.209 11 9 11 C 6.791 11 5 9.209 5 7 C 5 4.791 6.791 3 9 3 C 11.209 3 13 4.791 13 7 Z M 22 21 L 22 19 C 21.999 17.177 20.765 15.586 19 15.13 M 16 3.13 C 17.77 3.583 19.008 5.178 19.008 7.005 C 19.008 8.832 17.77 10.427 16 10.88"",
+ IconName.UserPlus => ""M 16 21 L 16 19 C 16 16.791 14.209 15 12 15 L 6 15 C 3.791 15 2 16.791 2 19 L 2 21 M 13 7 C 13 9.209 11.209 11 9 11 C 6.791 11 5 9.209 5 7 C 5 4.791 6.791 3 9 3 C 11.209 3 13 4.791 13 7 Z M 19 8 L 19 14 M 22 11 L 16 11"",
+ IconName.Lock => ""M 5 11 L 19 11 C 20.105 11 21 11.895 21 13 L 21 20 C 21 21.105 20.105 22 19 22 L 5 22 C 3.895 22 3 21.105 3 20 L 3 13 C 3 11.895 3.895 11 5 11 Z M 7 11 L 7 7 C 7 4.239 9.239 2 12 2 C 14.761 2 17 4.239 17 7 L 17 11"",
+ IconName.LockOpen => ""M 5 11 L 19 11 C 20.105 11 21 11.895 21 13 L 21 20 C 21 21.105 20.105 22 19 22 L 5 22 C 3.895 22 3 21.105 3 20 L 3 13 C 3 11.895 3.895 11 5 11 Z M 7 11 L 7 7 C 6.997 4.431 8.942 2.278 11.498 2.02 C 14.053 1.762 16.389 3.483 16.9 6"",
+ IconName.Key => ""M 15.5 7.5 L 17.8 9.8 C 18.189 10.181 18.811 10.181 19.2 9.8 L 21.3 7.7 C 21.681 7.311 21.681 6.689 21.3 6.3 L 19 4 M 21 2 L 11.4 11.6 M 13 15.5 C 13 18.538 10.538 21 7.5 21 C 4.462 21 2 18.538 2 15.5 C 2 12.462 4.462 10 7.5 10 C 10.538 10 13 12.462 13 15.5 Z"",
+ IconName.Shield => ""M 20 13 C 20 18 16.5 20.5 12.34 21.95 C 12.122 22.024 11.886 22.02 11.67 21.94 C 7.5 20.5 4 18 4 13 L 4 6 C 4 5.448 4.448 5 5 5 C 7 5 9.5 3.8 11.24 2.28 C 11.678 1.906 12.322 1.906 12.76 2.28 C 14.51 3.81 17 5 19 5 C 19.552 5 20 5.448 20 6 Z"",
+ IconName.Eye => ""M 2.062 12.348 C 1.979 12.123 1.979 11.877 2.062 11.652 C 3.722 7.627 7.646 5.001 12 5.001 C 16.354 5.001 20.278 7.627 21.938 11.652 C 22.021 11.877 22.021 12.123 21.938 12.348 C 20.278 16.373 16.354 18.999 12 18.999 C 7.646 18.999 3.722 16.373 2.062 12.348 M 15 12 C 15 13.657 13.657 15 12 15 C 10.343 15 9 13.657 9 12 C 9 10.343 10.343 9 12 9 C 13.657 9 15 10.343 15 12 Z"",
+ IconName.EyeOff => ""M 10.733 5.076 C 15.52 4.506 20.101 7.194 21.938 11.651 C 22.021 11.876 22.021 12.122 21.938 12.347 C 21.57 13.238 21.085 14.076 20.494 14.837 M 14.084 14.158 C 12.907 15.295 11.036 15.279 9.879 14.121 C 8.721 12.964 8.705 11.093 9.842 9.916 M 17.479 17.499 C 14.795 19.089 11.553 19.434 8.594 18.446 C 5.635 17.457 3.251 15.232 2.062 12.348 C 1.979 12.123 1.979 11.877 2.062 11.652 C 2.949 9.502 4.509 7.697 6.508 6.509 M 2 2 L 22 22"",
+ IconName.Mail => ""M 4 4 L 20 4 C 21.105 4 22 4.895 22 6 L 22 18 C 22 19.105 21.105 20 20 20 L 4 20 C 2.895 20 2 19.105 2 18 L 2 6 C 2 4.895 2.895 4 4 4 Z M 22 7 L 13.03 12.7 C 12.4 13.095 11.6 13.095 10.97 12.7 L 2 7"",
+ IconName.Inbox => ""M 22 12 L 16 12 L 14 15 L 10 15 L 8 12 L 2 12 M 5.45 5.11 L 2 12 L 2 18 C 2 19.105 2.895 20 4 20 L 20 20 C 21.105 20 22 19.105 22 18 L 22 12 L 18.55 5.11 C 18.212 4.43 17.519 4 16.76 4 L 7.24 4 C 6.481 4 5.788 4.43 5.45 5.11 Z"",
+ IconName.Archive => ""M 3 3 L 21 3 C 21.552 3 22 3.448 22 4 L 22 7 C 22 7.552 21.552 8 21 8 L 3 8 C 2.448 8 2 7.552 2 7 L 2 4 C 2 3.448 2.448 3 3 3 Z M 4 8 L 4 19 C 4 20.105 4.895 21 6 21 L 18 21 C 19.105 21 20 20.105 20 19 L 20 8 M 10 12 L 14 12"",
+ IconName.MessageCircle => ""M 7.9 20 C 11.843 22.023 16.672 20.874 19.282 17.292 C 21.892 13.71 21.505 8.762 18.372 5.628 C 15.238 2.495 10.29 2.108 6.708 4.718 C 3.126 7.328 1.977 12.157 4 16.1 L 2 22 Z"",
+ IconName.Phone => ""M 22 16.92 L 22 19.92 C 22.002 20.483 21.767 21.021 21.352 21.402 C 20.937 21.783 20.381 21.971 19.82 21.92 C 16.743 21.586 13.787 20.534 11.19 18.85 C 8.774 17.315 6.725 15.266 5.19 12.85 C 3.5 10.241 2.448 7.271 2.12 4.18 C 2.069 3.621 2.256 3.066 2.635 2.652 C 3.013 2.237 3.549 2.001 4.11 2 L 7.11 2 C 8.114 1.99 8.97 2.726 9.11 3.72 C 9.237 4.68 9.471 5.623 9.81 6.53 C 10.085 7.261 9.909 8.085 9.36 8.64 L 8.09 9.91 C 9.514 12.414 11.586 14.486 14.09 15.91 L 15.36 14.64 C 15.915 14.091 16.739 13.915 17.47 14.19 C 18.377 14.529 19.32 14.763 20.28 14.89 C 21.286 15.032 22.025 15.905 22 16.92 Z"",
+ IconName.Calendar => ""M 8 2 L 8 6 M 16 2 L 16 6 M 5 4 L 19 4 C 20.105 4 21 4.895 21 6 L 21 20 C 21 21.105 20.105 22 19 22 L 5 22 C 3.895 22 3 21.105 3 20 L 3 6 C 3 4.895 3.895 4 5 4 Z M 3 10 L 21 10"",
+ IconName.Clock => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 12 6 L 12 12 L 16 14"",
+ IconName.Globe => ""M 22 12 C 22 17.523 17.523 22 12 22 C 6.477 22 2 17.523 2 12 C 2 6.477 6.477 2 12 2 C 17.523 2 22 6.477 22 12 Z M 12 2 C 6.667 7.6 6.667 16.4 12 22 C 17.333 16.4 17.333 7.6 12 2 M 2 12 L 22 12"",
+ IconName.MapPin => ""M 20 10 C 20 14.993 14.461 20.193 12.601 21.799 C 12.245 22.067 11.755 22.067 11.399 21.799 C 9.539 20.193 4 14.993 4 10 C 4 5.582 7.582 2 12 2 C 16.418 2 20 5.582 20 10 M 15 10 C 15 11.657 13.657 13 12 13 C 10.343 13 9 11.657 9 10 C 9 8.343 10.343 7 12 7 C 13.657 7 15 8.343 15 10 Z"",
+ IconName.Image => ""M 5 3 L 19 3 C 20.105 3 21 3.895 21 5 L 21 19 C 21 20.105 20.105 21 19 21 L 5 21 C 3.895 21 3 20.105 3 19 L 3 5 C 3 3.895 3.895 3 5 3 Z M 11 9 C 11 10.105 10.105 11 9 11 C 7.895 11 7 10.105 7 9 C 7 7.895 7.895 7 9 7 C 10.105 7 11 7.895 11 9 Z M 21 15 L 17.914 11.914 C 17.133 11.133 15.867 11.133 15.086 11.914 L 6 21"",
+ IconName.File => ""M 15 2 L 6 2 C 4.895 2 4 2.895 4 4 L 4 20 C 4 21.105 4.895 22 6 22 L 18 22 C 19.105 22 20 21.105 20 20 L 20 7 Z M 14 2 L 14 6 C 14 7.105 14.895 8 16 8 L 20 8"",
+ IconName.FileText => ""M 15 2 L 6 2 C 4.895 2 4 2.895 4 4 L 4 20 C 4 21.105 4.895 22 6 22 L 18 22 C 19.105 22 20 21.105 20 20 L 20 7 Z M 14 2 L 14 6 C 14 7.105 14.895 8 16 8 L 20 8 M 10 9 L 8 9 M 16 13 L 8 13 M 16 17 L 8 17"",
+ IconName.Folder => ""M 20 20 C 21.105 20 22 19.105 22 18 L 22 8 C 22 6.895 21.105 6 20 6 L 12.1 6 C 11.42 6.007 10.784 5.668 10.41 5.1 L 9.6 3.9 C 9.23 3.338 8.603 3 7.93 3 L 4 3 C 2.895 3 2 3.895 2 5 L 2 18 C 2 19.105 2.895 20 4 20 Z"",
+ IconName.Tag => ""M 12.586 2.586 C 12.211 2.211 11.702 2 11.172 2 L 4 2 C 2.895 2 2 2.895 2 4 L 2 11.172 C 2 11.702 2.211 12.211 2.586 12.586 L 11.29 21.29 C 12.236 22.23 13.764 22.23 14.71 21.29 L 21.29 14.71 C 22.23 13.764 22.23 12.236 21.29 11.29 Z M 8 7.5 C 8 7.776 7.776 8 7.5 8 C 7.224 8 7 7.776 7 7.5 C 7 7.224 7.224 7 7.5 7 C 7.776 7 8 7.224 8 7.5 Z"",
+ IconName.Bookmark => ""M 19 21 L 12 17 L 5 21 L 5 5 C 5 3.895 5.895 3 7 3 L 17 3 C 18.105 3 19 3.895 19 5 L 19 21 Z"",
+ IconName.Heart => ""M 19 14 C 20.49 12.54 22 10.79 22 8.5 C 22 5.462 19.538 3 16.5 3 C 14.74 3 13.5 3.5 12 5 C 10.5 3.5 9.26 3 7.5 3 C 4.462 3 2 5.462 2 8.5 C 2 10.8 3.5 12.55 5 14 L 12 21 Z"",
+ IconName.Star => ""M 11.525 2.295 C 11.614 2.114 11.798 2 12 2 C 12.202 2 12.386 2.114 12.475 2.295 L 14.785 6.974 C 15.094 7.599 15.69 8.033 16.38 8.134 L 21.546 8.89 C 21.746 8.919 21.912 9.059 21.974 9.251 C 22.036 9.442 21.984 9.653 21.84 9.794 L 18.104 13.432 C 17.604 13.919 17.375 14.622 17.493 15.31 L 18.375 20.45 C 18.41 20.65 18.329 20.852 18.165 20.971 C 18.001 21.09 17.783 21.105 17.604 21.01 L 12.986 18.582 C 12.368 18.258 11.631 18.258 11.013 18.582 L 6.396 21.01 C 6.217 21.105 6 21.089 5.836 20.97 C 5.673 20.851 5.591 20.649 5.626 20.45 L 6.507 15.311 C 6.625 14.622 6.397 13.92 5.896 13.432 L 2.16 9.795 C 2.014 9.654 1.962 9.443 2.024 9.25 C 2.087 9.058 2.253 8.917 2.454 8.889 L 7.619 8.134 C 8.31 8.034 8.907 7.6 9.216 6.974 Z"",
+ IconName.ThumbsUp => ""M 7 10 L 7 22 M 15 5.88 L 14 10 L 19.83 10 C 20.46 10 21.052 10.296 21.43 10.8 C 21.808 11.304 21.926 11.956 21.75 12.56 L 19.42 20.56 C 19.171 21.413 18.389 22 17.5 22 L 4 22 C 2.895 22 2 21.105 2 20 L 2 12 C 2 10.895 2.895 10 4 10 L 6.76 10 C 7.519 10 8.212 9.57 8.55 8.89 L 12 2 C 12.955 2.012 13.853 2.459 14.437 3.215 C 15.022 3.971 15.229 4.952 15 5.88 Z"",
+ IconName.ShoppingCart => ""M 9 21 C 9 21.552 8.552 22 8 22 C 7.448 22 7 21.552 7 21 C 7 20.448 7.448 20 8 20 C 8.552 20 9 20.448 9 21 Z M 20 21 C 20 21.552 19.552 22 19 22 C 18.448 22 18 21.552 18 21 C 18 20.448 18.448 20 19 20 C 19.552 20 20 20.448 20 21 Z M 2.05 2.05 L 4.05 2.05 L 6.71 14.47 C 6.911 15.408 7.751 16.071 8.71 16.05 L 18.49 16.05 C 19.428 16.048 20.238 15.396 20.44 14.48 L 22.09 7.05 L 5.12 7.05"",
+ IconName.CreditCard => ""M 4 5 L 20 5 C 21.105 5 22 5.895 22 7 L 22 17 C 22 18.105 21.105 19 20 19 L 4 19 C 2.895 19 2 18.105 2 17 L 2 7 C 2 5.895 2.895 5 4 5 Z M 2 10 L 22 10"",
+ IconName.DollarSign => ""M 12 2 L 12 22 M 17 5 L 9.5 5 C 7.567 5 6 6.567 6 8.5 C 6 10.433 7.567 12 9.5 12 L 14.5 12 C 16.433 12 18 13.567 18 15.5 C 18 17.433 16.433 19 14.5 19 L 6 19"",
+ IconName.Activity => ""M 22 12 L 19.52 12 C 18.622 11.998 17.833 12.595 17.59 13.46 L 15.24 21.82 C 15.209 21.927 15.111 22 15 22 C 14.889 22 14.791 21.927 14.76 21.82 L 9.24 2.18 C 9.209 2.073 9.111 2 9 2 C 8.889 2 8.791 2.073 8.76 2.18 L 6.41 10.54 C 6.168 11.401 5.384 11.997 4.49 12 L 2 12"",
+ IconName.TrendingUp => ""M 22 7 L 13.5 15.5 L 8.5 10.5 L 2 17 M 16 7 L 22 7 L 22 13"",
+ IconName.ChartColumn => ""M 3 3 L 3 19 C 3 20.105 3.895 21 5 21 L 21 21 M 18 17 L 18 9 M 13 17 L 13 5 M 8 17 L 8 14"",
+ IconName.Zap => ""M 4 14 C 3.614 14.001 3.261 13.78 3.095 13.432 C 2.928 13.083 2.977 12.67 3.22 12.37 L 13.12 2.17 C 13.273 1.993 13.528 1.946 13.734 2.057 C 13.94 2.167 14.042 2.405 13.98 2.63 L 12.06 8.65 C 11.945 8.958 11.988 9.302 12.176 9.571 C 12.364 9.841 12.672 10.001 13 10 L 20 10 C 20.386 9.999 20.739 10.22 20.905 10.568 C 21.072 10.917 21.023 11.33 20.78 11.63 L 10.88 21.83 C 10.727 22.007 10.472 22.054 10.266 21.943 C 10.06 21.833 9.958 21.595 10.02 21.37 L 11.94 15.35 C 12.055 15.042 12.012 14.698 11.824 14.429 C 11.636 14.159 11.328 13.999 11 14 Z"",
+ IconName.Sparkles => ""M 9.937 15.5 C 9.755 14.795 9.205 14.245 8.5 14.063 L 2.365 12.481 C 2.15 12.42 2.002 12.224 2.002 12 C 2.002 11.776 2.15 11.58 2.365 11.519 L 8.5 9.936 C 9.205 9.754 9.755 9.204 9.937 8.5 L 11.519 2.365 C 11.579 2.149 11.776 2 12 2 C 12.225 2 12.422 2.149 12.482 2.365 L 14.063 8.5 C 14.245 9.205 14.795 9.755 15.5 9.937 L 21.635 11.518 C 21.852 11.578 22.002 11.775 22.002 12 C 22.002 12.225 21.852 12.422 21.635 12.482 L 15.5 14.063 C 14.795 14.245 14.245 14.795 14.063 15.5 L 12.481 21.635 C 12.421 21.851 12.224 22 12 22 C 11.775 22 11.578 21.851 11.518 21.635 Z M 20 3 L 20 7 M 22 5 L 18 5 M 4 17 L 4 19 M 5 18 L 3 18"",
+ IconName.Command => ""M 15 6 L 15 18 C 15 19.657 16.343 21 18 21 C 19.657 21 21 19.657 21 18 C 21 16.343 19.657 15 18 15 L 6 15 C 4.343 15 3 16.343 3 18 C 3 19.657 4.343 21 6 21 C 7.657 21 9 19.657 9 18 L 9 6 C 9 4.343 7.657 3 6 3 C 4.343 3 3 4.343 3 6 C 3 7.657 4.343 9 6 9 L 18 9 C 19.657 9 21 7.657 21 6 C 21 4.343 19.657 3 18 3 C 16.343 3 15 4.343 15 6"",
+ IconName.Terminal => ""M 4 17 L 10 11 L 4 5 M 12 19 L 20 19"",
+ IconName.Code => ""M 16 18 L 22 12 L 16 6 M 8 6 L 2 12 L 8 18"",
+ IconName.Moon => ""M 12 3 C 9.515 5.485 9.515 9.515 12 12 C 14.485 14.485 18.515 14.485 21 12 C 21 16.971 16.971 21 12 21 C 7.029 21 3 16.971 3 12 C 3 7.029 7.029 3 12 3 Z"",
+ IconName.Sun => ""M 16 12 C 16 14.209 14.209 16 12 16 C 9.791 16 8 14.209 8 12 C 8 9.791 9.791 8 12 8 C 14.209 8 16 9.791 16 12 Z M 12 2 L 12 4 M 12 20 L 12 22 M 4.93 4.93 L 6.34 6.34 M 17.66 17.66 L 19.07 19.07 M 2 12 L 4 12 M 20 12 L 22 12 M 6.34 17.66 L 4.93 19.07 M 19.07 4.93 L 17.66 6.34"",
+ IconName.Monitor => ""M 4 3 L 20 3 C 21.105 3 22 3.895 22 5 L 22 15 C 22 16.105 21.105 17 20 17 L 4 17 C 2.895 17 2 16.105 2 15 L 2 5 C 2 3.895 2.895 3 4 3 Z M 8 21 L 16 21 M 12 17 L 12 21"",
+ IconName.Smartphone => ""M 7 2 L 17 2 C 18.105 2 19 2.895 19 4 L 19 20 C 19 21.105 18.105 22 17 22 L 7 22 C 5.895 22 5 21.105 5 20 L 5 4 C 5 2.895 5.895 2 7 2 Z M 12 18 L 12.01 18"",
+ IconName.Laptop => ""M 20 16 L 20 7 C 20 5.895 19.105 5 18 5 L 6 5 C 4.895 5 4 5.895 4 7 L 4 16 M 20 16 L 4 16 M 20 16 L 21.28 18.55 C 21.437 18.861 21.42 19.231 21.237 19.527 C 21.053 19.823 20.728 20.002 20.38 20 L 3.62 20 C 3.272 20.002 2.947 19.823 2.763 19.527 C 2.58 19.231 2.563 18.861 2.72 18.55 L 4 16"",
+ IconName.Wifi => ""M 12 20 L 12.01 20 M 2 8.82 C 7.694 3.727 16.306 3.727 22 8.82 M 5 12.859 C 8.888 9.048 15.112 9.048 19 12.859 M 8.5 16.429 C 10.444 14.523 13.556 14.523 15.5 16.429"",
+ IconName.Camera => ""M 14.5 4 L 9.5 4 L 7 7 L 4 7 C 2.895 7 2 7.895 2 9 L 2 18 C 2 19.105 2.895 20 4 20 L 20 20 C 21.105 20 22 19.105 22 18 L 22 9 C 22 7.895 21.105 7 20 7 L 17 7 L 14.5 4 Z M 15 13 C 15 14.657 13.657 16 12 16 C 10.343 16 9 14.657 9 13 C 9 11.343 10.343 10 12 10 C 13.657 10 15 11.343 15 13 Z"",
+ IconName.Mic => ""M 12 2 C 10.343 2 9 3.343 9 5 L 9 12 C 9 13.657 10.343 15 12 15 C 13.657 15 15 13.657 15 12 L 15 5 C 15 3.343 13.657 2 12 2 Z M 19 10 L 19 12 C 19 15.866 15.866 19 12 19 C 8.134 19 5 15.866 5 12 L 5 10 M 12 19 L 12 22"",
+ IconName.Play => ""M 6 3 L 20 12 L 6 21 L 6 3 Z"",
+ IconName.Pause => ""M 15 4 L 17 4 C 17.552 4 18 4.448 18 5 L 18 19 C 18 19.552 17.552 20 17 20 L 15 20 C 14.448 20 14 19.552 14 19 L 14 5 C 14 4.448 14.448 4 15 4 Z M 7 4 L 9 4 C 9.552 4 10 4.448 10 5 L 10 19 C 10 19.552 9.552 20 9 20 L 7 20 C 6.448 20 6 19.552 6 19 L 6 5 C 6 4.448 6.448 4 7 4 Z"",
+ IconName.Volume2 => ""M 11 4.702 C 11 4.417 10.828 4.161 10.565 4.052 C 10.301 3.943 9.999 4.003 9.797 4.204 L 6.413 7.587 C 6.149 7.852 5.79 8.001 5.416 8 L 3 8 C 2.448 8 2 8.448 2 9 L 2 15 C 2 15.552 2.448 16 3 16 L 5.416 16 C 5.79 15.999 6.149 16.148 6.413 16.413 L 9.796 19.797 C 9.998 19.999 10.301 20.06 10.565 19.95 C 10.829 19.841 11 19.583 11 19.298 Z M 16 9 C 17.333 10.778 17.333 13.222 16 15 M 19.364 18.364 C 21.052 16.676 22 14.387 22 12 C 22 9.613 21.052 7.324 19.364 5.636"",
+ IconName.Lightbulb => ""M 15 14 C 15.2 13 15.7 12.3 16.5 11.5 C 17.5 10.6 18 9.3 18 8 C 18 4.686 15.314 2 12 2 C 8.686 2 6 4.686 6 8 C 6 9 6.2 10.2 7.5 11.5 C 8.2 12.2 8.8 13 9 14 M 9 18 L 15 18 M 10 22 L 14 22"",
+ _ => string.Empty
+ };
+}
+
+public class Icon : Control
+{
+ public static readonly StyledProperty KindProperty =
+ AvaloniaProperty.Register(nameof(Kind));
+
+ public static readonly StyledProperty SizeProperty =
+ AvaloniaProperty.Register(nameof(Size), 16.0);
+
+ public static readonly StyledProperty StrokeWidthProperty =
+ AvaloniaProperty.Register(nameof(StrokeWidth), 2.0);
+
+ public static readonly StyledProperty TokenProperty =
+ AvaloniaProperty.Register(nameof(Token), ShellToken.Foreground);
+
+ // Explicit color; overrides Token when set.
+ public static readonly StyledProperty ColorProperty =
+ AvaloniaProperty.Register(nameof(Color));
+
+ // The token's brush, kept in sync with the theme by a resource binding.
+ private static readonly StyledProperty TintProperty =
+ AvaloniaProperty.Register(""Tint"");
+
+ private static readonly Dictionary Geometries = new();
+
+ static Icon()
+ {
+ AffectsMeasure(SizeProperty);
+ AffectsRender(KindProperty, SizeProperty, StrokeWidthProperty, ColorProperty, TintProperty);
+ TokenProperty.Changed.AddClassHandler((icon, _) => icon.Token(TintProperty, icon.Token));
+ IsHitTestVisibleProperty.OverrideDefaultValue(false);
+ HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Center);
+ VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Center);
+ }
+
+ public Icon() => this.Token(TintProperty, Token);
+
+ public IconName Kind
+ {
+ get => GetValue(KindProperty);
+ set => SetValue(KindProperty, value);
+ }
+
+ public double Size
+ {
+ get => GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ public double StrokeWidth
+ {
+ get => GetValue(StrokeWidthProperty);
+ set => SetValue(StrokeWidthProperty, value);
+ }
+
+ public ShellToken Token
+ {
+ get => GetValue(TokenProperty);
+ set => SetValue(TokenProperty, value);
+ }
+
+ public Color? Color
+ {
+ get => GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ protected override Size MeasureOverride(Size availableSize) => new(Size, Size);
+
+ public override void Render(DrawingContext context)
+ {
+ var brush = Color is { } color ? new SolidColorBrush(color) : GetValue(TintProperty);
+ if (GetGeometry(Kind) is not { } geometry || brush is null) return;
+ // Scale the 24x24 grid to Size; the stroke scales with it so icons keep Lucide's weight.
+ var scale = Size / 24.0;
+ using (context.PushTransform(Matrix.CreateScale(scale, scale)))
+ context.DrawGeometry(null, new Pen(brush, StrokeWidth, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), geometry);
+ }
+
+ private static Geometry? GetGeometry(IconName kind)
+ {
+ if (!Geometries.TryGetValue(kind, out var geometry))
+ {
+ var data = IconPaths.Get(kind);
+ geometry = string.IsNullOrEmpty(data) ? null : Geometry.Parse(data);
+ Geometries[kind] = geometry;
+ }
+ return geometry;
+ }
+}
"
};
}
diff --git a/src/ShellUI.Native.Templates/Templates/InputTemplate.cs b/src/ShellUI.Native.Templates/Templates/InputTemplate.cs
index 7c321fb..dfaeef6 100644
--- a/src/ShellUI.Native.Templates/Templates/InputTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/InputTemplate.cs
@@ -184,6 +184,183 @@ private void UpdateVisualState()
public new void Focus() => _entry.Focus();
public new void Unfocus() => _entry.Unfocus();
}
+",
+ [NativePlatform.Avalonia] = @"using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Text input — h-10 rounded-md border border-input px-3 text-sm.
+// Focus: border-ring + soft ring glow. Error: border-destructive.
+// The inner TextBox's Fluent chrome is cleared so this Border is the only frame.
+public class Input : Border
+{
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register (nameof(Text), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty PlaceholderProperty =
+ AvaloniaProperty.Register (nameof(Placeholder));
+
+ public static readonly StyledProperty IsPasswordProperty =
+ AvaloniaProperty.Register (nameof(IsPassword));
+
+ public static readonly StyledProperty HasErrorProperty =
+ AvaloniaProperty.Register (nameof(HasError));
+
+ public static readonly StyledProperty IsReadOnlyProperty =
+ AvaloniaProperty.Register (nameof(IsReadOnly));
+
+ public static readonly StyledProperty MaxLengthProperty =
+ AvaloniaProperty.Register (nameof(MaxLength));
+
+ // Fluent's TextBox template takes its background and border brushes from these resources in
+ // every state (hover, focus, disabled); overriding them on the TextBox clears that chrome.
+ private static readonly string[] ClearedBrushes =
+ {
+ ""TextControlBackground"", ""TextControlBackgroundPointerOver"", ""TextControlBackgroundFocused"", ""TextControlBackgroundDisabled"",
+ ""TextControlBorderBrush"", ""TextControlBorderBrushPointerOver"", ""TextControlBorderBrushFocused"", ""TextControlBorderBrushDisabled"",
+ };
+
+ private readonly TextBox _textBox;
+ private bool _isFocused;
+
+ static Input()
+ {
+ TextProperty.Changed.AddClassHandler ((i, e) =>
+ {
+ if (i._textBox.Text != (string?)e.NewValue) i._textBox.Text = (string?)e.NewValue;
+ });
+ PlaceholderProperty.Changed.AddClassHandler ((i, e) => i._textBox.PlaceholderText = (string?)e.NewValue);
+ IsPasswordProperty.Changed.AddClassHandler ((i, e) => i._textBox.PasswordChar = (bool)e.NewValue! ? '•' : default);
+ IsReadOnlyProperty.Changed.AddClassHandler ((i, e) => i._textBox.IsReadOnly = (bool)e.NewValue!);
+ MaxLengthProperty.Changed.AddClassHandler ((i, e) => i._textBox.MaxLength = (int)e.NewValue!);
+ HasErrorProperty.Changed.AddClassHandler ((i, _) => i.UpdateVisualState());
+ IsEnabledProperty.Changed.AddClassHandler ((i, _) => i.Opacity = i.IsEnabled ? 1.0 : 0.5);
+ }
+
+ public string? Text
+ {
+ get => GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public string? Placeholder
+ {
+ get => GetValue(PlaceholderProperty);
+ set => SetValue(PlaceholderProperty, value);
+ }
+
+ public bool IsPassword
+ {
+ get => GetValue(IsPasswordProperty);
+ set => SetValue(IsPasswordProperty, value);
+ }
+
+ public bool HasError
+ {
+ get => GetValue(HasErrorProperty);
+ set => SetValue(HasErrorProperty, value);
+ }
+
+ public bool IsReadOnly
+ {
+ get => GetValue(IsReadOnlyProperty);
+ set => SetValue(IsReadOnlyProperty, value);
+ }
+
+ // 0 means no limit.
+ public int MaxLength
+ {
+ get => GetValue(MaxLengthProperty);
+ set => SetValue(MaxLengthProperty, value);
+ }
+
+ public event EventHandler? TextChanged;
+
+ // Raised on Enter.
+ public event EventHandler? Completed;
+
+ public Input()
+ {
+ _textBox = new TextBox
+ {
+ FontSize = 14,
+ BorderThickness = new Thickness(0),
+ Padding = new Thickness(0),
+ MinHeight = 0,
+ VerticalAlignment = VerticalAlignment.Center,
+ VerticalContentAlignment = VerticalAlignment.Center
+ };
+ foreach (var key in ClearedBrushes)
+ _textBox.Resources[key] = Brushes.Transparent;
+ _textBox.Resources[""TextControlBorderThemeThickness""] = new Thickness(0);
+ _textBox.Resources[""TextControlBorderThemeThicknessFocused""] = new Thickness(0);
+ _textBox.Resources[""TextControlThemePadding""] = new Thickness(0);
+ _textBox.Token(TextBox.ForegroundProperty, ShellToken.Foreground)
+ .Token(TextBox.CaretBrushProperty, ShellToken.Foreground)
+ .Token(TextBox.PlaceholderForegroundProperty, ShellToken.MutedForeground);
+
+ _textBox.TextChanged += (_, e) =>
+ {
+ Text = _textBox.Text;
+ TextChanged?.Invoke(this, e);
+ };
+ _textBox.KeyDown += (_, e) =>
+ {
+ if (e.Key == Key.Enter) Completed?.Invoke(this, EventArgs.Empty);
+ };
+ _textBox.GotFocus += (_, _) => { _isFocused = true; UpdateVisualState(); };
+ _textBox.LostFocus += (_, _) => { _isFocused = false; UpdateVisualState(); };
+
+ Child = _textBox;
+ Height = 40;
+ Padding = new Thickness(12, 0);
+ BorderThickness = new Thickness(1);
+ CornerRadius = new CornerRadius(ShellTheme.RadiusMd);
+ Background = Brushes.Transparent;
+ Cursor = new Cursor(StandardCursorType.Ibeam);
+ UpdateVisualState();
+ }
+
+ // The glow color is a snapshot of a token, so it is repainted on theme changes.
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ ShellTheme.ThemeChanged += OnThemeChanged;
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+ ShellTheme.ThemeChanged -= OnThemeChanged;
+ }
+
+ private void OnThemeChanged(object? sender, EventArgs e) => UpdateVisualState();
+
+ // Clicking the padding focuses the text box too.
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ base.OnPointerPressed(e);
+ if (!_textBox.IsFocused) _textBox.Focus();
+ }
+
+ public new bool Focus(NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None)
+ => _textBox.Focus(method, keyModifiers);
+
+ private void UpdateVisualState()
+ {
+ var stroke = HasError ? ShellToken.Destructive : _isFocused ? ShellToken.Ring : ShellToken.Input;
+ this.Token(BorderBrushProperty, stroke);
+ BoxShadow = _isFocused || HasError
+ ? new BoxShadows(new BoxShadow { Blur = 6, Color = ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring, 0.35) })
+ : default;
+ }
+}
"
};
}
diff --git a/src/ShellUI.Native.Templates/Templates/LabelTemplate.cs b/src/ShellUI.Native.Templates/Templates/LabelTemplate.cs
index a23dbe4..7addbe6 100644
--- a/src/ShellUI.Native.Templates/Templates/LabelTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/LabelTemplate.cs
@@ -111,6 +111,89 @@ private void UpdateVisualState()
}
}
+public enum LabelSize { Xs, Sm, Default, Lg, Xl, Xxl, Xxxl }
+public enum LabelWeight { Light, Normal, Medium, Semibold, Bold }
+public enum LabelVariant { Default, Muted, Destructive, Success, Warning }
+",
+ [NativePlatform.Avalonia] = @"using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Typography label with size / weight / variant — theme-aware.
+// Named ShellLabel because Avalonia already has a Label control.
+public class ShellLabel : TextBlock
+{
+ public static readonly StyledProperty SizeProperty =
+ AvaloniaProperty.Register(nameof(Size), LabelSize.Default);
+
+ public static readonly StyledProperty WeightProperty =
+ AvaloniaProperty.Register(nameof(Weight), LabelWeight.Normal);
+
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ static ShellLabel()
+ {
+ SizeProperty.Changed.AddClassHandler((l, _) => l.UpdateVisualState());
+ WeightProperty.Changed.AddClassHandler((l, _) => l.UpdateVisualState());
+ VariantProperty.Changed.AddClassHandler((l, _) => l.UpdateVisualState());
+ }
+
+ public LabelSize Size
+ {
+ get => GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ public LabelWeight Weight
+ {
+ get => GetValue(WeightProperty);
+ set => SetValue(WeightProperty, value);
+ }
+
+ public LabelVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public ShellLabel() => UpdateVisualState();
+
+ private void UpdateVisualState()
+ {
+ FontSize = Size switch
+ {
+ LabelSize.Xs => 12,
+ LabelSize.Sm => 14,
+ LabelSize.Lg => 18,
+ LabelSize.Xl => 20,
+ LabelSize.Xxl => 24,
+ LabelSize.Xxxl => 30,
+ _ => 16
+ };
+
+ FontWeight = Weight switch
+ {
+ LabelWeight.Light => FontWeight.Light,
+ LabelWeight.Medium => FontWeight.Medium,
+ LabelWeight.Semibold => FontWeight.SemiBold,
+ LabelWeight.Bold => FontWeight.Bold,
+ _ => FontWeight.Normal
+ };
+
+ this.Token(ForegroundProperty, Variant switch
+ {
+ LabelVariant.Muted => ShellToken.MutedForeground,
+ LabelVariant.Destructive => ShellToken.Destructive,
+ LabelVariant.Success => ShellToken.Success,
+ LabelVariant.Warning => ShellToken.Warning,
+ _ => ShellToken.Foreground
+ });
+ }
+}
+
public enum LabelSize { Xs, Sm, Default, Lg, Xl, Xxl, Xxxl }
public enum LabelWeight { Light, Normal, Medium, Semibold, Bold }
public enum LabelVariant { Default, Muted, Destructive, Success, Warning }
diff --git a/src/ShellUI.Native.Templates/Templates/ProgressTemplate.cs b/src/ShellUI.Native.Templates/Templates/ProgressTemplate.cs
index e05a9a9..a74801b 100644
--- a/src/ShellUI.Native.Templates/Templates/ProgressTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/ProgressTemplate.cs
@@ -146,6 +146,126 @@ private void UpdateFill(bool animate)
}
}
+public enum ProgressVariant
+{
+ Default,
+ Success,
+ Warning,
+ Destructive
+}
+",
+ [NativePlatform.Avalonia] = @"using System;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Progress bar — h-2 rounded-full track, fill animates to the new value.
+public class Progress : Grid
+{
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register(nameof(Value));
+
+ public static readonly StyledProperty MaximumProperty =
+ AvaloniaProperty.Register(nameof(Maximum), 100.0);
+
+ public static readonly StyledProperty VariantProperty =
+ AvaloniaProperty.Register(nameof(Variant));
+
+ public static readonly StyledProperty ShowLabelProperty =
+ AvaloniaProperty.Register(nameof(ShowLabel));
+
+ private const double TrackHeight = 8;
+
+ private readonly Panel _track;
+ private readonly Border _trackFill;
+ private readonly Border _fill;
+ private readonly TextBlock _label;
+ private readonly Transitions _fillTransitions = new()
+ {
+ new DoubleTransition { Property = WidthProperty, Duration = TimeSpan.FromMilliseconds(300), Easing = new CubicEaseOut() }
+ };
+
+ static Progress()
+ {
+ ValueProperty.Changed.AddClassHandler((p, _) => p.UpdateFill(animate: true));
+ MaximumProperty.Changed.AddClassHandler((p, _) => p.UpdateFill(animate: true));
+ VariantProperty.Changed.AddClassHandler((p, _) => p.UpdateVisualState());
+ ShowLabelProperty.Changed.AddClassHandler((p, _) => p.UpdateVisualState());
+ }
+
+ public double Value
+ {
+ get => GetValue(ValueProperty);
+ set => SetValue(ValueProperty, value);
+ }
+
+ public double Maximum
+ {
+ get => GetValue(MaximumProperty);
+ set => SetValue(MaximumProperty, value);
+ }
+
+ public ProgressVariant Variant
+ {
+ get => GetValue(VariantProperty);
+ set => SetValue(VariantProperty, value);
+ }
+
+ public bool ShowLabel
+ {
+ get => GetValue(ShowLabelProperty);
+ set => SetValue(ShowLabelProperty, value);
+ }
+
+ public double Percentage => Maximum > 0 ? Math.Clamp(Value / Maximum, 0, 1) * 100 : 0;
+
+ public Progress()
+ {
+ // Track tint is the fill color at 20% (bg-primary/20), so it follows the variant.
+ _trackFill = new Border { Opacity = 0.2, CornerRadius = new CornerRadius(TrackHeight / 2) };
+ _fill = new Border { CornerRadius = new CornerRadius(TrackHeight / 2), HorizontalAlignment = HorizontalAlignment.Left, Width = 0 };
+ _track = new Panel { Height = TrackHeight, VerticalAlignment = VerticalAlignment.Center, Children = { _trackFill, _fill } };
+ _track.SizeChanged += (_, _) => UpdateFill(animate: false);
+
+ _label = new TextBlock { FontSize = 12, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(12, 0, 0, 0), IsVisible = false };
+ _label.Token(TextBlock.ForegroundProperty, ShellToken.MutedForeground);
+
+ ColumnDefinitions = new ColumnDefinitions(""*,Auto"");
+ SetColumn(_label, 1);
+ Children.Add(_track);
+ Children.Add(_label);
+ UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ var token = Variant switch
+ {
+ ProgressVariant.Success => ShellToken.Success,
+ ProgressVariant.Warning => ShellToken.Warning,
+ ProgressVariant.Destructive => ShellToken.Destructive,
+ _ => ShellToken.Primary
+ };
+ _fill.Token(Border.BackgroundProperty, token);
+ _trackFill.Token(Border.BackgroundProperty, token);
+ _label.IsVisible = ShowLabel;
+ UpdateFill(animate: false);
+ }
+
+ private void UpdateFill(bool animate)
+ {
+ _label.Text = $""{Percentage:F0}%"";
+ if (_track.Bounds.Width <= 0) return;
+ // Resizes jump to the new width; only value changes animate.
+ _fill.Transitions = animate ? _fillTransitions : null;
+ _fill.Width = _track.Bounds.Width * Percentage / 100;
+ }
+}
+
public enum ProgressVariant
{
Default,
diff --git a/src/ShellUI.Native.Templates/Templates/SeparatorTemplate.cs b/src/ShellUI.Native.Templates/Templates/SeparatorTemplate.cs
index 1f7cc08..fb5b550 100644
--- a/src/ShellUI.Native.Templates/Templates/SeparatorTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/SeparatorTemplate.cs
@@ -66,6 +66,51 @@ private void UpdateVisualState()
}
}
+public enum SeparatorOrientation
+{
+ Horizontal,
+ Vertical
+}
+",
+ [NativePlatform.Avalonia] = @"using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace YourProjectNamespace.Components.UI;
+
+// 1px divider in the Border token.
+public class Separator : Border
+{
+ public static readonly StyledProperty OrientationProperty =
+ AvaloniaProperty.Register(nameof(Orientation));
+
+ static Separator()
+ {
+ OrientationProperty.Changed.AddClassHandler((s, _) => s.UpdateVisualState());
+ }
+
+ public SeparatorOrientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ public Separator()
+ {
+ this.Token(BackgroundProperty, ShellToken.Border);
+ UpdateVisualState();
+ }
+
+ private void UpdateVisualState()
+ {
+ var horizontal = Orientation == SeparatorOrientation.Horizontal;
+ Height = horizontal ? 1 : double.NaN;
+ Width = horizontal ? double.NaN : 1;
+ HorizontalAlignment = horizontal ? HorizontalAlignment.Stretch : HorizontalAlignment.Center;
+ VerticalAlignment = horizontal ? VerticalAlignment.Center : VerticalAlignment.Stretch;
+ }
+}
+
public enum SeparatorOrientation
{
Horizontal,
diff --git a/src/ShellUI.Native.Templates/Templates/ShellTemplate.cs b/src/ShellUI.Native.Templates/Templates/ShellTemplate.cs
index d1ceec3..b678353 100644
--- a/src/ShellUI.Native.Templates/Templates/ShellTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/ShellTemplate.cs
@@ -1059,6 +1059,246 @@ public static void ActivateAncestor(Element from)
}
}
}
+",
+ [NativePlatform.Avalonia] = @"using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Styling;
+
+namespace YourProjectNamespace.Components.UI;
+
+// ShellUI Native core utilities. Installed by `shellui-native init`; every component depends on it.
+public static class Shell
+{
+ // Joins class names, skipping null or empty ones.
+ public static string Cn(params string?[] classes)
+ => string.Join("" "", classes.Where(c => !string.IsNullOrWhiteSpace(c)));
+}
+
+// Design tokens mirroring ShellUI's CSS variables (--background, --primary, ...).
+public enum ShellToken
+{
+ Background, Foreground,
+ Card, CardForeground,
+ Popover, PopoverForeground,
+ Primary, PrimaryForeground,
+ Secondary, SecondaryForeground,
+ Muted, MutedForeground,
+ Accent, AccentForeground,
+ Destructive, DestructiveForeground,
+ Success, SuccessForeground,
+ Warning, WarningForeground,
+ Info, InfoForeground,
+ Border, Input, Ring,
+ Overlay
+}
+
+/* Publishes both palettes as theme dictionaries of the application resources (""ShellUIPrimary"" is
+ the Color, ""ShellUIPrimaryBrush"" the Brush). Avalonia resolves the active variant's palette, so a
+ theme switch repaints without re-publishing. Your own XAML can use them too:
+ Foreground=""{DynamicResource ShellUIMutedForegroundBrush}"".
+ Customize before the first window opens:
+ ShellTheme.Light[ShellToken.Primary] = Color.Parse(""#2563EB"");
+ ShellTheme.Apply(); */
+public static class ShellTheme
+{
+ // Based on ShellUI's default neutral theme, softened from pure black/white for native text
+ // rendering. Success/Warning/Info extend it (ShellUI has no semantic status colors).
+ public static Dictionary Light { get; } = new()
+ {
+ [ShellToken.Background] = Color.Parse(""#FCFCFC""),
+ [ShellToken.Foreground] = Color.Parse(""#0A0A0A""),
+ [ShellToken.Card] = Color.Parse(""#FFFFFF""),
+ [ShellToken.CardForeground] = Color.Parse(""#0A0A0A""),
+ [ShellToken.Popover] = Color.Parse(""#FFFFFF""),
+ [ShellToken.PopoverForeground] = Color.Parse(""#0A0A0A""),
+ [ShellToken.Primary] = Color.Parse(""#171717""),
+ [ShellToken.PrimaryForeground] = Color.Parse(""#FAFAFA""),
+ [ShellToken.Secondary] = Color.Parse(""#F0F0F0""),
+ [ShellToken.SecondaryForeground] = Color.Parse(""#171717""),
+ [ShellToken.Muted] = Color.Parse(""#F5F5F5""),
+ [ShellToken.MutedForeground] = Color.Parse(""#737373""),
+ [ShellToken.Accent] = Color.Parse(""#F0F0F0""),
+ [ShellToken.AccentForeground] = Color.Parse(""#171717""),
+ [ShellToken.Destructive] = Color.Parse(""#E54B4F""),
+ [ShellToken.DestructiveForeground] = Color.Parse(""#FFFFFF""),
+ [ShellToken.Success] = Color.Parse(""#16A34A""),
+ [ShellToken.SuccessForeground] = Color.Parse(""#FFFFFF""),
+ [ShellToken.Warning] = Color.Parse(""#D97706""),
+ [ShellToken.WarningForeground] = Color.Parse(""#FFFFFF""),
+ [ShellToken.Info] = Color.Parse(""#2563EB""),
+ [ShellToken.InfoForeground] = Color.Parse(""#FFFFFF""),
+ [ShellToken.Border] = Color.Parse(""#E4E4E4""),
+ [ShellToken.Input] = Color.Parse(""#E4E4E4""),
+ [ShellToken.Ring] = Color.Parse(""#A3A3A3""),
+ [ShellToken.Overlay] = Color.Parse(""#80000000""),
+ };
+
+ public static Dictionary Dark { get; } = new()
+ {
+ [ShellToken.Background] = Color.Parse(""#0A0A0A""),
+ [ShellToken.Foreground] = Color.Parse(""#FAFAFA""),
+ [ShellToken.Card] = Color.Parse(""#141414""),
+ [ShellToken.CardForeground] = Color.Parse(""#FAFAFA""),
+ [ShellToken.Popover] = Color.Parse(""#171717""),
+ [ShellToken.PopoverForeground] = Color.Parse(""#FAFAFA""),
+ [ShellToken.Primary] = Color.Parse(""#FAFAFA""),
+ [ShellToken.PrimaryForeground] = Color.Parse(""#171717""),
+ [ShellToken.Secondary] = Color.Parse(""#262626""),
+ [ShellToken.SecondaryForeground] = Color.Parse(""#FAFAFA""),
+ [ShellToken.Muted] = Color.Parse(""#262626""),
+ [ShellToken.MutedForeground] = Color.Parse(""#A3A3A3""),
+ [ShellToken.Accent] = Color.Parse(""#262626""),
+ [ShellToken.AccentForeground] = Color.Parse(""#FAFAFA""),
+ [ShellToken.Destructive] = Color.Parse(""#FF5B5B""),
+ [ShellToken.DestructiveForeground] = Color.Parse(""#0A0A0A""),
+ [ShellToken.Success] = Color.Parse(""#22C55E""),
+ [ShellToken.SuccessForeground] = Color.Parse(""#0A0A0A""),
+ [ShellToken.Warning] = Color.Parse(""#F59E0B""),
+ [ShellToken.WarningForeground] = Color.Parse(""#0A0A0A""),
+ [ShellToken.Info] = Color.Parse(""#3B82F6""),
+ [ShellToken.InfoForeground] = Color.Parse(""#FFFFFF""),
+ [ShellToken.Border] = Color.Parse(""#2A2A2A""),
+ [ShellToken.Input] = Color.Parse(""#333333""),
+ [ShellToken.Ring] = Color.Parse(""#737373""),
+ [ShellToken.Overlay] = Color.Parse(""#B3000000""),
+ };
+
+ // --radius: 0.5rem. Tailwind's rounded-sm / rounded-md / rounded-lg derive from it.
+ public const double RadiusSm = 4;
+ public const double RadiusMd = 6;
+ public const double RadiusLg = 8;
+
+ private static bool _initialized;
+
+ // Raised when a palette is published or the theme variant changes, for anything resources
+ // can't reach (e.g. a BoxShadow).
+ public static event EventHandler? ThemeChanged;
+
+ public static bool IsDarkMode => Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
+
+ public static string ColorKey(ShellToken token) => $""ShellUI{token}"";
+ public static string BrushKey(ShellToken token) => $""ShellUI{token}Brush"";
+
+ // Current value of a token, for code that can't bind.
+ public static Color Get(ShellToken token) => (IsDarkMode ? Dark : Light)[token];
+
+ // A token at an opacity, for glows and focus rings (Tailwind's ring/35).
+ public static Color Get(ShellToken token, double opacity)
+ {
+ var color = Get(token);
+ return new Color((byte)Math.Round(color.A * opacity), color.R, color.G, color.B);
+ }
+
+ // Black at an opacity, for drop shadows (shadow-sm is black at 5%).
+ public static Color Shadow(double opacity) => new((byte)Math.Round(255 * opacity), 0, 0, 0);
+
+ public static void EnsureInitialized()
+ {
+ if (_initialized) return;
+ var app = Application.Current;
+ if (app is null) return;
+ _initialized = true;
+ app.ActualThemeVariantChanged += (_, _) => ThemeChanged?.Invoke(null, EventArgs.Empty);
+ Apply();
+ }
+
+ // Re-publishes both palettes. Call after editing Light/Dark at runtime.
+ public static void Apply()
+ {
+ var app = Application.Current;
+ if (app is null) return;
+ app.Resources.ThemeDictionaries[ThemeVariant.Light] = Build(Light);
+ app.Resources.ThemeDictionaries[ThemeVariant.Dark] = Build(Dark);
+ ThemeChanged?.Invoke(null, EventArgs.Empty);
+ }
+
+ private static ResourceDictionary Build(Dictionary palette)
+ {
+ var dictionary = new ResourceDictionary();
+ foreach (var (token, color) in palette)
+ {
+ dictionary[ColorKey(token)] = color;
+ dictionary[BrushKey(token)] = new SolidColorBrush(color);
+ }
+ return dictionary;
+ }
+
+ // ThemeVariant.Default follows the OS setting.
+ public static void SetTheme(ThemeVariant theme)
+ {
+ if (Application.Current is not { } app) return;
+ EnsureInitialized();
+ app.RequestedThemeVariant = theme;
+ }
+
+ public static void ToggleTheme() => SetTheme(IsDarkMode ? ThemeVariant.Light : ThemeVariant.Dark);
+}
+
+public static class ShellThemeExtensions
+{
+ // One binding per control and property. A resource binding outlives ClearValue and a later
+ // SetValue, and would repaint the property on the next theme change, so it is disposed instead.
+ private static readonly ConditionalWeakTable> Bindings = new();
+
+ // Binds a Color or Brush property to a theme token, replacing any earlier token binding.
+ // Returns the control for chaining.
+ public static T Token(this T control, AvaloniaProperty property, ShellToken token) where T : StyledElement
+ {
+ ShellTheme.EnsureInitialized();
+ control.ClearToken(property);
+ var key = typeof(IBrush).IsAssignableFrom(property.PropertyType) ? ShellTheme.BrushKey(token) : ShellTheme.ColorKey(token);
+ Bindings.GetOrCreateValue(control)[property] = control.Bind(property, control.GetResourceObservable(key));
+ return control;
+ }
+
+ // Removes the token binding, so a plain value (e.g. Brushes.Transparent) can be set.
+ public static T ClearToken(this T control, AvaloniaProperty property) where T : StyledElement
+ {
+ if (Bindings.TryGetValue(control, out var bindings) && bindings.Remove(property, out var binding))
+ binding.Dispose();
+ return control;
+ }
+}
+
+/* shadcn's focus-visible ring (ring-2 ring-ring ring-offset-2): a 2px Background gap, then a 2px
+ Ring band. Shown for keyboard focus only, so a click doesn't leave a ring behind. Replaces
+ Fluent's focus adorner. */
+public static class ShellFocus
+{
+ private static readonly HashSet Shown = new();
+
+ static ShellFocus() => ShellTheme.ThemeChanged += (_, _) =>
+ {
+ foreach (var border in Shown) Paint(border);
+ };
+
+ public static void Ring(Control control, Border target)
+ {
+ control.Focusable = true;
+ control.FocusAdorner = null;
+ control.GotFocus += (_, e) =>
+ {
+ if (e.NavigationMethod is not (NavigationMethod.Tab or NavigationMethod.Directional)) return;
+ Shown.Add(target);
+ Paint(target);
+ };
+ control.LostFocus += (_, _) =>
+ {
+ Shown.Remove(target);
+ target.BoxShadow = default;
+ };
+ }
+
+ private static void Paint(Border border) => border.BoxShadow = new BoxShadows(
+ new BoxShadow { Spread = 2, Color = ShellTheme.Get(ShellToken.Background) },
+ new[] { new BoxShadow { Spread = 4, Color = ShellTheme.Get(ShellToken.Ring) } });
+}
"
};
}
diff --git a/src/ShellUI.Native.Templates/Templates/SwitchTemplate.cs b/src/ShellUI.Native.Templates/Templates/SwitchTemplate.cs
index 07df148..a8f16ec 100644
--- a/src/ShellUI.Native.Templates/Templates/SwitchTemplate.cs
+++ b/src/ShellUI.Native.Templates/Templates/SwitchTemplate.cs
@@ -126,6 +126,132 @@ private void UpdateVisualState(bool animate)
else _thumb.TranslationX = x;
}
}
+",
+ [NativePlatform.Avalonia] = @"using System;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Media.Transformation;
+
+namespace YourProjectNamespace.Components.UI;
+
+// Switch — h-6 w-11 rounded-full track (primary when on, input when off),
+// h-5 w-5 thumb in bg-background that slides translate-x-5.
+public class Switch : Border
+{
+ public static readonly StyledProperty IsToggledProperty =
+ AvaloniaProperty.Register(nameof(IsToggled), defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly StyledProperty LabelProperty =
+ AvaloniaProperty.Register(nameof(Label));
+
+ private const double TrackWidth = 44;
+ private const double TrackHeight = 24;
+ private const double Inset = 2;
+ private const double ThumbSize = TrackHeight - Inset * 2; // 20
+ private const double Travel = TrackWidth - Inset * 2 - ThumbSize; // 20
+
+ private readonly Border _track;
+ private readonly Border _thumb;
+ private readonly TextBlock _label;
+
+ static Switch()
+ {
+ IsToggledProperty.Changed.AddClassHandler