From 61eee6a0e8b27ce17be206ac069c2a3715241ca4 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 12:49:48 +0200 Subject: [PATCH 1/9] feat(avalonia): add the Avalonia demo app with shell, icon and theme-toggle examples/Avalonia.Demo (Avalonia 12.1.3, .NET 10) is where Avalonia components are developed, as MAUI.Demo is for MAUI. Its root namespace is AvaloniaDemo so it doesn't sit inside Avalonia's, and implicit usings are off as in 'dotnet new avalonia.app', so a component missing a using fails in the demo instead of in a user's app. shell keeps the MAUI token names, palettes and radii and publishes both palettes as theme dictionaries, so a theme switch repaints without re-publishing; Token() binds a property to a resource. icon draws the same 110 Lucide icons with a custom Render and picks the icon with Kind, since every Avalonia control already has a Name. generate-icons.py now writes both Icon.cs files from the same icon list. --- ShellUI.Native.slnx | 1 + examples/Avalonia.Demo/App.axaml | 10 + examples/Avalonia.Demo/App.axaml.cs | 26 ++ examples/Avalonia.Demo/AvaloniaDemo.csproj | 21 ++ examples/Avalonia.Demo/Components/UI/Icon.cs | 337 ++++++++++++++++++ examples/Avalonia.Demo/Components/UI/Shell.cs | 179 ++++++++++ .../Components/UI/ThemeToggle.cs | 119 +++++++ examples/Avalonia.Demo/MainWindow.axaml | 43 +++ examples/Avalonia.Demo/MainWindow.axaml.cs | 32 ++ examples/Avalonia.Demo/Program.cs | 24 ++ examples/Avalonia.Demo/app.manifest | 18 + scripts/generate-icons.py | 41 ++- 12 files changed, 839 insertions(+), 12 deletions(-) create mode 100644 examples/Avalonia.Demo/App.axaml create mode 100644 examples/Avalonia.Demo/App.axaml.cs create mode 100644 examples/Avalonia.Demo/AvaloniaDemo.csproj create mode 100644 examples/Avalonia.Demo/Components/UI/Icon.cs create mode 100644 examples/Avalonia.Demo/Components/UI/Shell.cs create mode 100644 examples/Avalonia.Demo/Components/UI/ThemeToggle.cs create mode 100644 examples/Avalonia.Demo/MainWindow.axaml create mode 100644 examples/Avalonia.Demo/MainWindow.axaml.cs create mode 100644 examples/Avalonia.Demo/Program.cs create mode 100644 examples/Avalonia.Demo/app.manifest 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/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/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/Shell.cs b/examples/Avalonia.Demo/Components/UI/Shell.cs new file mode 100644 index 0000000..c0e7f52 --- /dev/null +++ b/examples/Avalonia.Demo/Components/UI/Shell.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +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]; + + 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 +{ + // Binds a Color or Brush property to a theme token. Returns the control for chaining. + public static T Token(this T control, AvaloniaProperty property, ShellToken token) where T : StyledElement + { + ShellTheme.EnsureInitialized(); + var key = typeof(IBrush).IsAssignableFrom(property.PropertyType) ? ShellTheme.BrushKey(token) : ShellTheme.ColorKey(token); + control.Bind(property, control.GetResourceObservable(key)); + return control; + } +} 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/MainWindow.axaml b/examples/Avalonia.Demo/MainWindow.axaml new file mode 100644 index 0000000..dbe2971 --- /dev/null +++ b/examples/Avalonia.Demo/MainWindow.axaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/Avalonia.Demo/MainWindow.axaml.cs b/examples/Avalonia.Demo/MainWindow.axaml.cs new file mode 100644 index 0000000..2720206 --- /dev/null +++ b/examples/Avalonia.Demo/MainWindow.axaml.cs @@ -0,0 +1,32 @@ +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(); + + 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/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__": From 1f6b92341e197fc75b56873fc2728b2a45fbe3bd Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 12:49:49 +0200 Subject: [PATCH 2/9] feat(templates): ship Avalonia templates for shell, icon and theme-toggle sync-templates.py also copies examples/Avalonia.Demo into each template's [NativePlatform.Avalonia] entry. Dependencies still come from the MAUI file; the script fails if an Avalonia file uses one the template doesn't declare. Tests check every Avalonia template for the namespace placeholder, no MAUI types, no hard-coded colors, and Avalonia content in all of its dependencies, so an Avalonia install never pulls in MAUI-only code. --- scripts/sync-templates.py | 64 +++- .../Templates/IconTemplate.cs | 340 +++++++++++++++++- .../Templates/ShellTemplate.cs | 180 ++++++++++ .../Templates/ThemeToggleTemplate.cs | 120 +++++++ .../ComponentRegistryTests.cs | 8 +- .../TemplateContentTests.cs | 50 ++- 6 files changed, 745 insertions(+), 17 deletions(-) diff --git a/scripts/sync-templates.py b/scripts/sync-templates.py index 9218212..a666bdb 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 = [] + for file in sorted(os.listdir(AVALONIA_DEMO)): + if not file.endswith(".cs"): + continue + name = file[:-3] + source = open(os.path.join(AVALONIA_DEMO, 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.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/ShellTemplate.cs b/src/ShellUI.Native.Templates/Templates/ShellTemplate.cs index d1ceec3..7f6a5cd 100644 --- a/src/ShellUI.Native.Templates/Templates/ShellTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/ShellTemplate.cs @@ -1059,6 +1059,186 @@ public static void ActivateAncestor(Element from) } } } +", + [NativePlatform.Avalonia] = @"using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +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]; + + 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 +{ + // Binds a Color or Brush property to a theme token. Returns the control for chaining. + public static T Token(this T control, AvaloniaProperty property, ShellToken token) where T : StyledElement + { + ShellTheme.EnsureInitialized(); + var key = typeof(IBrush).IsAssignableFrom(property.PropertyType) ? ShellTheme.BrushKey(token) : ShellTheme.ColorKey(token); + control.Bind(property, control.GetResourceObservable(key)); + return control; + } +} " }; } diff --git a/src/ShellUI.Native.Templates/Templates/ThemeToggleTemplate.cs b/src/ShellUI.Native.Templates/Templates/ThemeToggleTemplate.cs index 88beee5..9ded185 100644 --- a/src/ShellUI.Native.Templates/Templates/ThemeToggleTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/ThemeToggleTemplate.cs @@ -104,6 +104,126 @@ private void ShowIcons(bool dark, bool animate) _ = hide.ScaleToAsync(0.5, 200, Easing.CubicIn); } } +", + [NativePlatform.Avalonia] = @"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 YourProjectNamespace.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/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs b/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs index 2da6951..81f094e 100644 --- a/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs +++ b/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs @@ -89,17 +89,15 @@ public void GetComponentContent_returns_null_for_known_component_but_unsupported } [Fact] - public void SupportsPlatform_reports_MAUI_for_all_components_and_no_others_yet() + public void SupportsPlatform_reports_MAUI_for_all_components_and_no_WinUI_or_WPF() { foreach (var name in ComponentRegistry.Components.Keys) { Assert.True(ComponentRegistry.SupportsPlatform(name, NativePlatform.MAUI), $"'{name}' must support MAUI (baseline platform)."); - // Until the Phase 2 branch (feat/avalonia-implementation) lands, no component - // reports Avalonia/WinUI/WPF support. This test will need updating when - // Avalonia content is added to templates — that's the intended trigger. - Assert.False(ComponentRegistry.SupportsPlatform(name, NativePlatform.Avalonia)); + // Avalonia is being added component by component (see TemplateContentTests); + // WinUI and WPF have no templates. Assert.False(ComponentRegistry.SupportsPlatform(name, NativePlatform.WinUI)); Assert.False(ComponentRegistry.SupportsPlatform(name, NativePlatform.WPF)); } diff --git a/tests/ShellUI.Native.Tests/TemplateContentTests.cs b/tests/ShellUI.Native.Tests/TemplateContentTests.cs index 2c98d86..48efb35 100644 --- a/tests/ShellUI.Native.Tests/TemplateContentTests.cs +++ b/tests/ShellUI.Native.Tests/TemplateContentTests.cs @@ -9,12 +9,60 @@ namespace ShellUI.Native.Tests; // // Post Template System v2 (2026-07-18): content is keyed by (name, NativePlatform). // The MAUI baseline invariants are unchanged — every component must have MAUI content -// today. Avalonia support is opt-in per-template and NOT asserted here. +// today. Avalonia is added per template; its invariants are checked for templates that have it. public class TemplateContentTests { public static IEnumerable AllRegisteredComponents() => ComponentRegistry.Components.Keys.Select(name => new object[] { name }); + public static IEnumerable AvaloniaComponents() + => ComponentRegistry.Components.Keys + .Where(name => ComponentRegistry.SupportsPlatform(name, NativePlatform.Avalonia)) + .Select(name => new object[] { name }); + + // Phase 2 foundation; grows as components are ported. + [Theory] + [InlineData("shell")] + [InlineData("icon")] + [InlineData("theme-toggle")] + public void Foundation_components_have_Avalonia_content(string name) + { + Assert.False(string.IsNullOrWhiteSpace(ComponentRegistry.GetComponentContent(name, NativePlatform.Avalonia))); + } + + [Theory] + [MemberData(nameof(AvaloniaComponents))] + public void Avalonia_template_is_Avalonia_code_with_the_namespace_placeholder(string name) + { + var content = ComponentRegistry.GetComponentContent(name, NativePlatform.Avalonia)!; + Assert.Contains("namespace YourProjectNamespace.Components.UI;", content); + Assert.Contains("using Avalonia", content); + Assert.DoesNotContain("Microsoft.Maui", content); + Assert.DoesNotContain("AvaloniaDemo", content); + } + + // An Avalonia install must never pull in a dependency that only has MAUI code. + [Theory] + [MemberData(nameof(AvaloniaComponents))] + public void Avalonia_template_dependencies_have_Avalonia_content(string name) + { + foreach (var dependency in ComponentRegistry.GetMetadata(name)!.Dependencies) + Assert.True(ComponentRegistry.SupportsPlatform(dependency, NativePlatform.Avalonia), + $"'{name}' has Avalonia content but its dependency '{dependency}' does not."); + } + + [Theory] + [MemberData(nameof(AvaloniaComponents))] + public void Avalonia_components_use_theme_tokens_not_hardcoded_colors(string name) + { + if (name == "shell") return; // defines the palettes + + var content = ComponentRegistry.GetComponentContent(name, NativePlatform.Avalonia)!; + Assert.DoesNotContain("Color.Parse", content); + Assert.DoesNotContain("Color.FromRgb", content); + Assert.DoesNotContain("Color.FromArgb", content); + } + [Theory] [MemberData(nameof(AllRegisteredComponents))] public void Every_registered_component_has_MAUI_content(string name) From 9c4899ab8f1573f50df95599581bffa0044a59de Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 12:49:49 +0200 Subject: [PATCH 3/9] feat(cli): Avalonia next steps after init, platform-aware list In an Avalonia project init prints where to call ShellTheme.EnsureInitialized and the XAML namespace, instead of the MAUI steps. list shows only components with a template for the project's platform, so Avalonia projects don't see MAUI-only components as available. --- src/ShellUI.Native.CLI/Services/ComponentManager.cs | 2 ++ src/ShellUI.Native.CLI/Services/InitService.cs | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) 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) From 93f674bcee9abf6c7b398978fb12df69ce889211 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 12:49:50 +0200 Subject: [PATCH 4/9] ci: build the Avalonia demo on Linux, Windows and macOS --- .github/workflows/ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 From 3d0dd79e21e8f7a46806cd91f99208923d8382c3 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 12:49:51 +0200 Subject: [PATCH 5/9] docs: Avalonia component pattern, Phase 2 progress and the demo ARCHITECTURE describes the pattern in use (one C# file per component built in code, no .axaml) instead of the planned TemplatedControl one. DEVELOPMENT_PLAN records the foundation, the remaining P0/P1 work and the move to the ShellIcons packages once they are published. --- docs/ARCHITECTURE.md | 40 ++++++++++++++++++++++++---------------- docs/DEVELOPMENT_PLAN.md | 25 +++++++++++++++++++------ examples/README.md | 15 ++++++++++++--- 3 files changed, 55 insertions(+), 25 deletions(-) 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