diff --git a/src/TeamsISO.App.WinUI/Services/ThemeManager.cs b/src/TeamsISO.App.WinUI/Services/ThemeManager.cs
new file mode 100644
index 0000000..7cc683a
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Services/ThemeManager.cs
@@ -0,0 +1,108 @@
+using System;
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Windows.UI;
+using Windows.UI.ViewManagement;
+
+namespace TeamsISO.App.WinUI.Services;
+
+///
+/// Owns the active theme for the WinUI 3 host. Three preferences:
+/// System follows the Windows app-mode setting (default for new
+/// users); Dark and Light pin one regardless of the OS choice.
+/// The persistence path will land alongside the existing UIPreferences in
+/// the next commit — for now state lives in-process.
+///
+/// All public mutations push to subscribers so the
+/// host (MainWindow) can update the AppWindow title-bar button colors
+/// (system buttons aren't part of the visual tree and need a separate
+/// poke when ElementTheme changes).
+///
+public sealed class ThemeManager
+{
+ public static ThemeManager Current { get; } = new();
+
+ private ThemeManager()
+ {
+ _uiSettings = new UISettings();
+ _uiSettings.ColorValuesChanged += OnSystemColorsChanged;
+ }
+
+ private readonly UISettings _uiSettings;
+ private string _preference = "System";
+
+ public string Preference => _preference;
+
+ public event EventHandler? Themed;
+
+ ///
+ /// Resolve the preference to an absolute
+ /// suitable for .
+ /// System resolves to the OS app-mode.
+ ///
+ public ElementTheme ResolveTheme() => _preference switch
+ {
+ "Dark" => ElementTheme.Dark,
+ "Light" => ElementTheme.Light,
+ _ => IsSystemDark() ? ElementTheme.Dark : ElementTheme.Light,
+ };
+
+ public bool PreferenceMatches(string value) => string.Equals(_preference, value, StringComparison.Ordinal);
+
+ ///
+ /// Cycle dark ↔ light from the title-bar toggle. If the current
+ /// preference is System, the cycle pins to the opposite of the
+ /// currently-resolved theme so the click has a visible effect.
+ ///
+ public ElementTheme Toggle()
+ {
+ var current = ResolveTheme();
+ Set(current == ElementTheme.Dark ? "Light" : "Dark");
+ return ResolveTheme();
+ }
+
+ /// Set the preference and broadcast the resolved theme.
+ public void Set(string preference)
+ {
+ if (preference != "System" && preference != "Dark" && preference != "Light")
+ {
+ throw new ArgumentException("Preference must be System, Dark, or Light.", nameof(preference));
+ }
+
+ _preference = preference;
+ Themed?.Invoke(this, ResolveTheme());
+ }
+
+ private bool IsSystemDark()
+ {
+ // UISettings.GetColorValue(UIColorType.Background) returns
+ // black-ish in dark mode, white-ish in light mode — the most
+ // reliable cross-version check for app mode on desktop WinUI 3.
+ var bg = _uiSettings.GetColorValue(UIColorType.Background);
+ return ((5 * bg.G) + (2 * bg.R) + bg.B) < 8 * 128;
+ }
+
+ private void OnSystemColorsChanged(UISettings sender, object args)
+ {
+ // Only re-broadcast if the operator hasn't pinned a preference —
+ // otherwise the explicit choice wins regardless of what the OS does.
+ if (_preference == "System")
+ {
+ Themed?.Invoke(this, ResolveTheme());
+ }
+ }
+
+ ///
+ /// Compute the AppWindow title-bar foreground for the given resolved
+ /// theme so the system min/max/close buttons stay readable.
+ ///
+ public static Color TitleBarForegroundFor(ElementTheme theme) =>
+ theme == ElementTheme.Dark
+ ? Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6)
+ : Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A);
+
+ public static Color TitleBarHoverBgFor(ElementTheme theme) =>
+ theme == ElementTheme.Dark
+ ? Color.FromArgb(0xFF, 0x33, 0x34, 0x3A)
+ : Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1);
+}
diff --git a/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml b/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml
new file mode 100644
index 0000000..143430d
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml.cs b/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml.cs
new file mode 100644
index 0000000..89dbb47
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml.cs
@@ -0,0 +1,11 @@
+using Microsoft.UI.Xaml.Controls;
+
+namespace TeamsISO.App.WinUI.Views;
+
+public sealed partial class AboutDialog : ContentDialog
+{
+ public AboutDialog()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/TeamsISO.App.WinUI/Views/HelpDialog.xaml b/src/TeamsISO.App.WinUI/Views/HelpDialog.xaml
new file mode 100644
index 0000000..f03d7a2
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Views/HelpDialog.xaml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ External control via REST + WebSocket on 127.0.0.1:9755 and OSC on UDP 127.0.0.1:9000.
+ Self-contained HTML panel at /ui. Use Bitfocus Companion or TouchOSC. See
+ docs/CONTROL-SURFACE.md for the command vocabulary.
+
+
+
+
diff --git a/src/TeamsISO.App.WinUI/Views/HelpDialog.xaml.cs b/src/TeamsISO.App.WinUI/Views/HelpDialog.xaml.cs
new file mode 100644
index 0000000..25fc188
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Views/HelpDialog.xaml.cs
@@ -0,0 +1,11 @@
+using Microsoft.UI.Xaml.Controls;
+
+namespace TeamsISO.App.WinUI.Views;
+
+public sealed partial class HelpDialog : ContentDialog
+{
+ public HelpDialog()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/TeamsISO.App.WinUI/Views/MainWindow.xaml.cs b/src/TeamsISO.App.WinUI/Views/MainWindow.xaml.cs
index 7df0918..5d0adcf 100644
--- a/src/TeamsISO.App.WinUI/Views/MainWindow.xaml.cs
+++ b/src/TeamsISO.App.WinUI/Views/MainWindow.xaml.cs
@@ -2,6 +2,7 @@ using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using TeamsISO.App.WinUI.Models;
+using TeamsISO.App.WinUI.Services;
using Windows.Graphics;
using Windows.UI;
@@ -28,9 +29,6 @@ public sealed partial class MainWindow : Window
AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
- AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0x33, 0x34, 0x3A);
- AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0x3F, 0x3F, 0x47);
- AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6);
AppWindow.TitleBar.ButtonHoverForegroundColor = Colors.White;
// ── Initial size & position ───────────────────────────────────────
@@ -44,46 +42,45 @@ public sealed partial class MainWindow : Window
// table is populated from a static sample list so the visual design
// can be validated end-to-end against representative data.
ParticipantsRepeater.ItemsSource = MockParticipant.Sample();
+
+ // ── Theme system ──────────────────────────────────────────────────
+ // Subscribe to ThemeManager so picker changes from anywhere
+ // (settings drawer, title-bar toggle, system color change) reach
+ // the title-bar buttons and the visual tree consistently. Apply
+ // once at construction so the initial state matches the preference
+ // before the first frame.
+ ThemeManager.Current.Themed += (_, theme) => ApplyResolvedTheme(theme);
+ ApplyResolvedTheme(ThemeManager.Current.ResolveTheme());
}
///
- /// Cycle the active theme between Dark and Light. The actual swap target
- /// is the window's root visual element — RequestedTheme on that element
- /// propagates the {ThemeResource} swap across the whole tree, with no
- /// flicker. The system title-bar buttons aren't part of the visual tree,
- /// so their colors are recomputed inline.
- ///
- /// Persistence to UIPreferences.Theme happens in the view-model wiring
- /// commit (the WinUI host doesn't yet share UIPreferences with the WPF
- /// host; both will once the WinUI host owns the .NET startup).
+ /// Cycle the active theme between Dark and Light from the title-bar
+ /// toggle. The actual swap lives in ; this
+ /// handler just calls Toggle() and lets the subscription propagate.
///
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
+ {
+ ThemeManager.Current.Toggle();
+ }
+
+ ///
+ /// Push a resolved theme to the visual tree and to the AppWindow
+ /// title-bar buttons. Called on every
+ /// event and once at construction.
+ ///
+ private void ApplyResolvedTheme(ElementTheme theme)
{
if (Content is FrameworkElement root)
{
- var next = root.ActualTheme == ElementTheme.Dark
- ? ElementTheme.Light
- : ElementTheme.Dark;
- root.RequestedTheme = next;
-
- // Title-bar system buttons read from AppWindow.TitleBar directly,
- // not from the XAML resource dictionary, so we set them inline
- // for both themes. Dark-mode chrome lands on near-white glyphs;
- // light-mode chrome lands on near-black glyphs.
- if (next == ElementTheme.Dark)
- {
- AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6);
- AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0x33, 0x34, 0x3A);
- AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0x3F, 0x3F, 0x47);
- ThemeToggleIcon.Glyph = ""; // sun
- }
- else
- {
- AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A);
- AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1);
- AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0xE0, 0xE3, 0xE7);
- ThemeToggleIcon.Glyph = ""; // moon
- }
+ root.RequestedTheme = theme;
}
+
+ AppWindow.TitleBar.ButtonForegroundColor = ThemeManager.TitleBarForegroundFor(theme);
+ AppWindow.TitleBar.ButtonHoverBackgroundColor = ThemeManager.TitleBarHoverBgFor(theme);
+ AppWindow.TitleBar.ButtonPressedBackgroundColor = ThemeManager.TitleBarHoverBgFor(theme);
+
+ // Glyph cue: sun () means current is Light, click moves to Dark;
+ // moon () means current is Dark, click moves to Light.
+ ThemeToggleIcon.Glyph = theme == ElementTheme.Light ? "" : "";
}
}
diff --git a/src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml b/src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml
new file mode 100644
index 0000000..a4eedd5
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+ TeamsISO sits between Microsoft Teams' NDI broadcast and your live-production switcher.
+ One-time setup gets you to the participants table.
+
+
+
+
+
+
+
+
+
+
+ From https://ndi.video/tools/. TeamsISO won't start without it — the engine relies on
+ NDI 5 for discovery and routing. If the runtime is missing, you'll see a launch error;
+ install it then relaunch.
+
+
+
+
+
+
+
+
+
+
+
+ Teams admin must enable NDI broadcast for your tenant. In Teams, Settings → Devices →
+ "Allow NDI usage." Per-participant streams will appear as TeamsISO discovers them.
+
+
+
+
+
+
+
+
+
+
+
+ Defaults to 1920×1080 at 30 fps with letterbox aspect. Adjust under Settings → Routing.
+ Recording outputs land in %USERPROFILE%\Videos\TeamsISO\<date>\.
+
+
+
+
+
+
diff --git a/src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml.cs b/src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml.cs
new file mode 100644
index 0000000..f75b860
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml.cs
@@ -0,0 +1,18 @@
+using Microsoft.UI.Xaml.Controls;
+
+namespace TeamsISO.App.WinUI.Views;
+
+public sealed partial class OnboardingDialog : ContentDialog
+{
+ public OnboardingDialog()
+ {
+ InitializeComponent();
+ }
+
+ ///
+ /// True when the user wants the dialog suppressed on subsequent launches.
+ /// Caller persists this to UIPreferences alongside the existing
+ /// "shown welcome" flag.
+ ///
+ public bool SuppressFutureLaunches => DontShowAgain.IsChecked == true;
+}
diff --git a/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml b/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml
new file mode 100644
index 0000000..6b41439
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml.cs b/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml.cs
new file mode 100644
index 0000000..8657e0b
--- /dev/null
+++ b/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml.cs
@@ -0,0 +1,229 @@
+using System;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using TeamsISO.App.WinUI.Services;
+
+namespace TeamsISO.App.WinUI.Views;
+
+public sealed partial class SettingsDrawer : UserControl
+{
+ ///
+ /// Raised when the operator clicks the close button or hits Esc. The host
+ /// (MainWindow) handles the actual collapse animation since drawer
+ /// hosting lives at that level — the drawer just signals intent.
+ ///
+ public event EventHandler? CloseRequested;
+
+ public SettingsDrawer()
+ {
+ InitializeComponent();
+ BuildAppearanceTab();
+ }
+
+ private void OnCloseClick(object sender, RoutedEventArgs e) => CloseRequested?.Invoke(this, EventArgs.Empty);
+
+ private void OnApplyClick(object sender, RoutedEventArgs e)
+ {
+ // Tabs already persist on change; Apply is here for affordance
+ // consistency with the WPF host. Could surface a toast in the future.
+ CloseRequested?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void OnResetClick(object sender, RoutedEventArgs e)
+ {
+ // Defaults restoration plumbing follows in the view-model wiring
+ // commit. For now, just clear the dirty hint.
+ DirtyHint.Text = "Reset queued — apply or close to commit.";
+ }
+
+ ///
+ /// Tab-switch handler builds the body for the selected tab. We assemble
+ /// content imperatively rather than via separate XAML pages because the
+ /// drawer's content is data-driven (most rows are simple label + control
+ /// + tooltip triples) and pulling that into five XAML files would
+ /// triplicate the row layout. The body recomposes on every selection
+ /// change, which is cheap given the small surface.
+ ///
+ private void OnTabSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
+ {
+ TabContent.Children.Clear();
+ if (args.SelectedItemContainer is not NavigationViewItem item) return;
+
+ switch (item.Tag as string)
+ {
+ case "Appearance":
+ BuildAppearanceTab();
+ break;
+ case "Routing":
+ TabContent.Children.Add(SettingHeader("Routing"));
+ TabContent.Children.Add(SettingRow("Framerate", "30 fps", "Target framerate the engine normalizes incoming NDI feeds to."));
+ TabContent.Children.Add(SettingRow("Resolution", "1920 x 1080", "Output resolution applied to every routed participant."));
+ TabContent.Children.Add(SettingRow("Aspect mode", "Letterbox", "How sources whose aspect ratio doesn't match the target are framed."));
+ TabContent.Children.Add(SettingRow("Audio routing", "Per-participant", "Embed each participant's audio in their own NDI source."));
+ TabContent.Children.Add(SettingNote("Settings wired to the engine in a follow-up commit."));
+ break;
+ case "Display":
+ TabContent.Children.Add(SettingHeader("Display & participants"));
+ TabContent.Children.Add(SettingRow("Hide local self", "Yes", "Filter the operator's own preview from the participants table."));
+ TabContent.Children.Add(SettingRow("Auto-disable on departure", "No", "Keep routing alive when a participant goes offline."));
+ TabContent.Children.Add(SettingRow("Participant sort", "Join order", "Default sort for the participants table."));
+ TabContent.Children.Add(SettingRow("Minimize to tray", "No", "Keep TeamsISO running in the system tray when the window is minimized."));
+ TabContent.Children.Add(SettingRow("Launch Teams on startup", "No", "Start Teams in the background when TeamsISO opens."));
+ TabContent.Children.Add(SettingRow("Auto-hide Teams windows", "No", "Hide every visible Teams window after launch."));
+ TabContent.Children.Add(SettingRow("Auto-record on call", "No", "Start recording every active ISO when Teams enters a call."));
+ break;
+ case "Control":
+ TabContent.Children.Add(SettingHeader("External control surface"));
+ TabContent.Children.Add(SettingRow("REST + WebSocket", "127.0.0.1:9755", "HTTP control surface for Companion / Stream Deck."));
+ TabContent.Children.Add(SettingRow("OSC bridge", "127.0.0.1:9000", "UDP OSC for TouchOSC / hardware surfaces."));
+ TabContent.Children.Add(SettingRow("LAN reachable", "No", "Bind the REST surface to all interfaces (warning: no auth)."));
+ TabContent.Children.Add(SettingNote("Control surface protocol unchanged from the WPF host. See docs/CONTROL-SURFACE.md."));
+ break;
+ case "Advanced":
+ TabContent.Children.Add(SettingHeader("Advanced"));
+ TabContent.Children.Add(SettingRow("Embed Teams window", "No", "Experimental SetParent reparent of Teams' main window."));
+ TabContent.Children.Add(SettingRow("Logs", "%LOCALAPPDATA%\\TeamsISO\\Logs", "Where rolling daily Serilog files write."));
+ TabContent.Children.Add(SettingRow("Recordings", "%USERPROFILE%\\Videos\\TeamsISO", "Default per-show recording directory."));
+ TabContent.Children.Add(SettingRow("Diagnostic bundle", "Export", "Zip logs + config + presets for a bug report."));
+ break;
+ }
+ }
+
+ ///
+ /// Appearance tab — theme picker, accent palette peek. The theme picker
+ /// is the only setting that affects the UI immediately (others apply on
+ /// the engine layer once wired). Selection writes to UIPreferences and
+ /// flips Window.Content.RequestedTheme synchronously.
+ ///
+ private void BuildAppearanceTab()
+ {
+ TabContent.Children.Add(SettingHeader("Appearance"));
+
+ var themeLabel = new TextBlock
+ {
+ Style = (Style)Application.Current.Resources["TextBody"],
+ Text = "Theme",
+ Margin = new Thickness(0, 0, 0, 6),
+ };
+ TabContent.Children.Add(themeLabel);
+
+ var themeRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
+ foreach (var (label, value) in new[]
+ {
+ ("System", "System"),
+ ("Dark", "Dark"),
+ ("Light", "Light"),
+ })
+ {
+ var btn = new RadioButton
+ {
+ Content = label,
+ Tag = value,
+ GroupName = "Theme",
+ IsChecked = ThemeManager.Current.PreferenceMatches(value),
+ };
+ btn.Checked += (s, _) =>
+ {
+ if (s is RadioButton rb && rb.Tag is string v)
+ {
+ ThemeManager.Current.Set(v);
+ }
+ };
+ themeRow.Children.Add(btn);
+ }
+ TabContent.Children.Add(themeRow);
+
+ TabContent.Children.Add(SettingNote(
+ "Dark is the default for the 1:50am operator scene; light is for daytime " +
+ "production. System follows the Windows app-mode preference."));
+
+ TabContent.Children.Add(SettingDivider());
+ TabContent.Children.Add(SettingHeader("Accent peek"));
+
+ var accentRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 12 };
+ accentRow.Children.Add(AccentSwatch("Cyan", "AccentCyanSurface"));
+ accentRow.Children.Add(AccentSwatch("Coral", "AccentCoral"));
+ accentRow.Children.Add(AccentSwatch("Live", "StatusLive"));
+ accentRow.Children.Add(AccentSwatch("Warn", "StatusWarn"));
+ TabContent.Children.Add(accentRow);
+
+ TabContent.Children.Add(SettingNote(
+ "These accents work in both themes. Cyan stays bright as a surface fill " +
+ "(text on top is near-black regardless of theme). For inline text use, the " +
+ "light palette substitutes a darker cyan automatically."));
+ }
+
+ // ──────────────────── helpers ──────────────────────────────────────────
+
+ private static TextBlock SettingHeader(string text) => new()
+ {
+ Style = (Style)Application.Current.Resources["TextHeading"],
+ Text = text,
+ Margin = new Thickness(0, 0, 0, 8),
+ };
+
+ private static Grid SettingRow(string label, string value, string? tooltip = null)
+ {
+ var g = new Grid { Margin = new Thickness(0, 0, 0, 6) };
+ g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(180) });
+ g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ var l = new TextBlock
+ {
+ Style = (Style)Application.Current.Resources["TextBody"],
+ Text = label,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ var v = new TextBlock
+ {
+ Style = (Style)Application.Current.Resources["TextSubtle"],
+ Text = value,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ if (tooltip is not null)
+ {
+ ToolTipService.SetToolTip(l, tooltip);
+ }
+ Grid.SetColumn(l, 0);
+ Grid.SetColumn(v, 1);
+ g.Children.Add(l);
+ g.Children.Add(v);
+ return g;
+ }
+
+ private static TextBlock SettingNote(string text) => new()
+ {
+ Style = (Style)Application.Current.Resources["TextCaption"],
+ Text = text,
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(0, 4, 0, 12),
+ };
+
+ private static Border SettingDivider() => new()
+ {
+ Height = 1,
+ Background = (SolidColorBrush)Application.Current.Resources["BorderSubtle"],
+ Margin = new Thickness(0, 12, 0, 12),
+ };
+
+ private static Border AccentSwatch(string label, string brushKey)
+ {
+ var inner = new StackPanel { Orientation = Orientation.Vertical, Spacing = 4 };
+ var swatch = new Border
+ {
+ Width = 80,
+ Height = 32,
+ CornerRadius = new Microsoft.UI.Xaml.CornerRadius(6),
+ Background = (SolidColorBrush)Application.Current.Resources[brushKey],
+ };
+ var caption = new TextBlock
+ {
+ Style = (Style)Application.Current.Resources["TextCaption"],
+ Text = label,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ };
+ inner.Children.Add(swatch);
+ inner.Children.Add(caption);
+ return new Border { Child = inner };
+ }
+}