Recipes
Complete, copy-paste building blocks for the screens most apps need. They assume the packages are installed and App.xaml merges the Joufflu resources with ThemeManager.Instance.Initialize() called at startup (see Getting started). For a whole app shell, follow the Tutorial.
Every snippet uses these namespaces:
xmlns:feedback="clr-namespace:Joufflu.Feedback.Controls;assembly=Joufflu.Feedback"
xmlns:inputs="clr-namespace:Joufflu.Inputs.Controls;assembly=Joufflu.Inputs"
xmlns:joufflu="clr-namespace:Joufflu;assembly=Joufflu"
xmlns:nav="clr-namespace:Joufflu.Navigation.Controls;assembly=Joufflu.Navigation"
xmlns:toolkit="clr-namespace:Joufflu.Toolkit;assembly=Joufflu"
A settings page
A Card per group of settings, headings sized with Sizing.Size, Spacing.Gap instead of per-element margins, and a toast to confirm the save.
<StackPanel Margin="{DynamicResource {x:Static joufflu:Dimensions.Spacing}}" toolkit:Spacing.Gap="16">
<TextBlock toolkit:Sizing.Size="xl" FontWeight="Bold" Text="Settings" />
<Border Style="{StaticResource Card}">
<StackPanel toolkit:Spacing.Gap="12">
<TextBlock toolkit:Sizing.Size="lg" FontWeight="SemiBold" Text="General" />
<TextBlock Opacity="{DynamicResource {x:Static joufflu:Colors.MutedOpacity}}" Text="Applies to every project." />
<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />
<inputs:NumericUpDown Value="{Binding MaxItems, Mode=TwoWay}" />
<ComboBox ItemsSource="{Binding Languages}" SelectedItem="{Binding Language}" />
<CheckBox Content="Check for updates at startup" IsChecked="{Binding CheckUpdates}" />
</StackPanel>
</Border>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" toolkit:Spacing.Gap="8">
<Button Command="{Binding ResetCommand}" Content="Reset" Style="{StaticResource SecondaryButton}" />
<Button Command="{Binding SaveCommand}" Content="Save" Style="{StaticResource PrimaryButton}" />
</StackPanel>
</StackPanel>
// SaveCommand, with the shared ToastService injected
private void Save()
{
_settings.Save();
_toasts.Success("Settings saved.", "Done");
}
Brand it: a custom theme
Customizing is overriding tokens, not restyling controls. Create a dictionary with the keys you want to change, merge it after the Joufflu resources, and every control, restyled native and Joufflu control picks the change up. Use the gallery’s Customize theme window (the palette button in its title bar) to pick colours and dimensions live and copy the generated dictionary rather than writing it by hand.
<!-- App.xaml -->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Joufflu;component/Resources.xaml" />
<ResourceDictionary Source="MyTheme.xaml" /> <!-- your overrides, after Joufflu -->
</ResourceDictionary.MergedDictionaries>
To make a palette selectable by name next to System / Light / Dark, register it before Initialize():
ThemeManager.Instance.Register(
"Ocean",
new Uri("pack://application:,,,/MyApp;component/Themes/Ocean.xaml"),
isDark: true);
ThemeManager.Instance.Initialize();
The keys to override are listed in Design tokens and the full flow in Customize theme and Theme.
A theme switcher
ThemeManager.Theme takes System, Light, Dark or the name of a registered custom theme and re-themes the whole UI live. Persisting the choice is up to the app; see Theme.
using Joufflu.Themes;
ThemeManager.Instance.Theme = ThemeManager.Dark;
A searchable, paged table
Search debounces typing before it raises SearchChanged, Paging tells the view model which slice to show. The view model filters, counts and slices.
<Grid Margin="{DynamicResource {x:Static joufflu:Dimensions.Spacing}}" toolkit:Spacing.Gap="0,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<inputs:Search x:Name="Search" />
<DataGrid Grid.Row="1" AutoGenerateColumns="False" ItemsSource="{Binding PageItems}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Id}" Header="Id" />
<DataGridTextColumn Binding="{Binding Name}" Header="Name" />
<DataGridTemplateColumn Header="Status">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<feedback:Badge Variant="Success">Active</feedback:Badge>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<nav:Paging
Grid.Row="2"
Total="{Binding Total}"
PageNumber="{Binding PageNumber, Mode=TwoWay}"
Capacity="{Binding Capacity, Mode=TwoWay}" />
</Grid>
// code-behind: route the debounced text to the view model
Search.SearchChanged += text => ((ItemsViewModel)DataContext).Filter = text;
See Paging for the view-model side.
Confirm before deleting
One awaited call shows a modal over the whole window, with the confirm button coloured for a destructive action. The Overlayer is the one the window’s OverlayContainer is bound to.
bool? confirmed = await _overlays.Confirm(
"Delete the selected item? This action cannot be undone.",
"Please confirm",
EnumConfirmationType.Danger);
if (confirmed == true)
{
Delete();
_toasts.Success("Item deleted.");
}
A status row with badges and a spinner
<StackPanel Orientation="Horizontal" toolkit:Spacing.Gap="8">
<feedback:Spinner />
<TextBlock VerticalAlignment="Center" Text="Syncing..." />
<feedback:Badge Variant="Info">12 pending</feedback:Badge>
<feedback:Badge Variant="Danger">3 errors</feedback:Badge>
</StackPanel>
A form with a different density
Sizing.Size (xs, sm, md, lg…) is inherited, so setting it once on a panel resizes every control inside it.
<StackPanel toolkit:Sizing.Size="sm" toolkit:Spacing.Gap="8">
<TextBox />
<ComboBox />
<Button Content="Apply" Style="{StaticResource PrimaryButton}" />
</StackPanel>
Rules these recipes follow
- Look changes are token overrides in a dictionary merged after Joufflu (or a registered theme), never edits to control styles.
- Colours come from the theme (
{DynamicResource {x:Static joufflu:Brushes.ForegroundBrush}}), never from hex values, so Light / Dark and custom themes keep working. - Gaps between children use
Spacing.Gap, not aMarginon each child. - Roles are named styles (
PrimaryButton,DangerButton,Card) and tokens (Sizing.Sizefor text sizes,Colors.MutedOpacityfor muted text), not literal values. - Modals and toasts go through the shared
OverlayerandToastService, neverMessageBox.