Kanban

Plain XAML, no code-behind: each kind of card has its own DataTemplate, converters turn priorities into colors and dates into words, multi-bindings count stories and work in progress, bound classes mark blocked and late cards, the buttons reach the page's commands through $parent bindings, run on the server, and cards drag between lanes with the a:Drag behavior (or the keyboard: Space, the arrows, Space).
Backlog
4
Epic
AIO-1
Native install from the browser
2 of 7 stories done
Due in 21 days
RI
Task
AIO-2
Pricing page copy
Marketing
No date
Unassigned
Bug
AIO-3
Date picker shows UTC on Safari
4 reports
· Safari 19
Due in 6 days
AN
Task
AIO-4
Audit color contrast
Accessibility
Due in 10 days
LE
In progress
3 / 3
Bug
AIO-5
Live charts stutter at 512 players
12 reports
· Chromium 153
2 days late
RI
Epic
AIO-6
Server-driven drag and drop
4 of 6 stories done
Due in 9 days
AN
Task
AIO-7
Blocked
Search index migration
Platform
Due tomorrow
DE
In review
1 / 2
Task
AIO-8
Two-factor sign-in
Security
Due today
KI
Done
2
Epic
AIO-9
Plans without Avalonia
9 of 9 stories done
Completed
RI
Bug
AIO-10
Hover style lost after morph
1 report
· Firefox 140
Completed
LE
View the XAML of this page
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:a="https://aioros.dev" xmlns:m="using:Aioros.Site.Demos.Kanban" xmlns:b="using:Aioros.Site.Demos.Kanban" x:DataType="m:KanbanModel"> <UserControl.Resources> <b:PriorityBrush x:Key="PriorityBrush" /> <b:DueText x:Key="DueText" /> <b:IsLate x:Key="IsLate" /> <b:Initials x:Key="Initials" /> <b:Counted x:Key="Counted" /> <ControlTheme x:Key="CardAction" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}"> <Setter Property="Background" Value="Transparent" /> <Setter Property="Padding" Value="6,4" /> <Setter Property="CornerRadius" Value="6" /> <Setter Property="MinHeight" Value="0" /> <Setter Property="Foreground" Value="{DynamicResource CosmoMutedForegroundBrush}" /> </ControlTheme> </UserControl.Resources> <UserControl.Styles> <Style Selector="Border.lane"> <Setter Property="Background" Value="{DynamicResource CosmoMutedBrush}" /> <Setter Property="CornerRadius" Value="14" /> <Setter Property="Padding" Value="12" /> <Setter Property="BorderThickness" Value="2" /> <Setter Property="BorderBrush" Value="Transparent" /> <Style Selector="^.over-limit"> <Setter Property="BorderBrush" Value="#E5484D" /> </Style> </Style> <Style Selector="Border.lane.over-limit TextBlock.count"> <Setter Property="Foreground" Value="#E5484D" /> <Setter Property="FontWeight" Value="Bold" /> </Style> <Style Selector="Border.card"> <Setter Property="Background" Value="{DynamicResource CosmoCardBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource CosmoBorderBrush}" /> <Setter Property="BorderThickness" Value="1" /> <Setter Property="CornerRadius" Value="12" /> <Setter Property="Padding" Value="12" /> <Setter Property="RenderTransform" Value="translate(0px, 0px)" /> <Setter Property="Transitions"> <Transitions> <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.18" Easing="CubicEaseOut" /> <BrushTransition Property="BorderBrush" Duration="0:0:0.18" /> </Transitions> </Setter> <Style Selector="^:pointerover"> <Setter Property="BorderBrush" Value="{DynamicResource CosmoPrimaryBrush}" /> <Setter Property="RenderTransform" Value="translate(0px, -2px)" /> </Style> <Style Selector="^.blocked"> <Setter Property="Background" Value="{DynamicResource CosmoAccentBrush}" /> <Setter Property="BorderBrush" Value="#E5484D" /> </Style> </Style> <Style Selector="TextBlock.wrap"> <Setter Property="TextWrapping" Value="Wrap" /> </Style> <Style Selector="Border.card.blocked TextBlock.title"> <Setter Property="Foreground" Value="{DynamicResource CosmoMutedForegroundBrush}" /> </Style> <Style Selector="Border.card.late TextBlock.due"> <Setter Property="Foreground" Value="#E5484D" /> <Setter Property="FontWeight" Value="SemiBold" /> </Style> <Style Selector="Border.card:not(.late) TextBlock.due"> <Setter Property="Foreground" Value="{DynamicResource CosmoMutedForegroundBrush}" /> </Style> <Style Selector="TextBlock.done-note"> <Setter Property="IsVisible" Value="False" /> <Setter Property="Foreground" Value="#30A46C" /> <Setter Property="FontWeight" Value="SemiBold" /> </Style> <Style Selector="Border.lane.done TextBlock.done-note"> <Setter Property="IsVisible" Value="True" /> </Style> <Style Selector="Border.lane.done TextBlock.due"> <Setter Property="IsVisible" Value="False" /> </Style> <Style Selector="Border.badge-blocked"> <Setter Property="Background" Value="#E5484D" /> <Setter Property="CornerRadius" Value="999" /> <Setter Property="Padding" Value="8,2" /> <Style.Animations> <Animation Duration="0:0:1.2" IterationCount="Infinite" PlaybackDirection="Alternate"> <KeyFrame Cue="0%"><Setter Property="Opacity" Value="1" /></KeyFrame> <KeyFrame Cue="100%"><Setter Property="Opacity" Value="0.55" /></KeyFrame> </Animation> </Style.Animations> </Style> <Style Selector="Border.kind"> <Setter Property="CornerRadius" Value="6" /> <Setter Property="Padding" Value="6,1" /> </Style> <Style Selector="Border.kind.task"><Setter Property="Background" Value="#E6F4FE" /></Style> <Style Selector="Border.kind.task TextBlock"><Setter Property="Foreground" Value="#0D74CE" /></Style> <Style Selector="Border.kind.bug"><Setter Property="Background" Value="#FFEBEC" /></Style> <Style Selector="Border.kind.bug TextBlock"><Setter Property="Foreground" Value="#CE2C31" /></Style> <Style Selector="Border.kind.epic"><Setter Property="Background" Value="#F2EEFF" /></Style> <Style Selector="Border.kind.epic TextBlock"><Setter Property="Foreground" Value="#6550B9" /></Style> <Style Selector="Border.avatar"> <Setter Property="Width" Value="26" /> <Setter Property="Height" Value="26" /> <Setter Property="CornerRadius" Value="13" /> <Setter Property="Background" Value="{DynamicResource KanbanAccentSoft}" /> </Style> <Style Selector="Border.avatar TextBlock"> <Setter Property="FontSize" Value="11" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="Foreground" Value="{DynamicResource CosmoPrimaryBrush}" /> </Style> </UserControl.Styles> <StackPanel Spacing="20"> <StackPanel Spacing="4"> <TextBlock Classes="h1" Text="Kanban" /> <TextBlock Classes="lead" Text="Plain XAML, no code-behind: each kind of card has its own DataTemplate, converters turn priorities into colors and dates into words, multi-bindings count stories and work in progress, bound classes mark blocked and late cards, the buttons reach the page's commands through $parent bindings, run on the server, and cards drag between lanes with the a:Drag behavior (or the keyboard: Space, the arrows, Space)." /> </StackPanel> <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"> <ItemsControl ItemsSource="{Binding Lanes}" MinWidth="1040"> <ItemsControl.ItemsPanel><ItemsPanelTemplate><UniformGrid Columns="4" ColumnSpacing="14" /></ItemsPanelTemplate></ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate x:DataType="m:LaneView"> <Border Classes="lane" Classes.over-limit="{Binding IsOverLimit}" Classes.done="{Binding IsDone}"> <StackPanel Spacing="10"> <Grid ColumnDefinitions="*,Auto" Margin="4,2,4,4"> <TextBlock Text="{Binding Title}" FontWeight="SemiBold" VerticalAlignment="Center" /> <Border Grid.Column="1" Classes="chip"> <Panel> <TextBlock Classes="small count" IsVisible="{Binding HasLimit}"> <TextBlock.Text><MultiBinding StringFormat="{}{0} / {1}"><Binding Path="Count" /><Binding Path="Limit" /></MultiBinding></TextBlock.Text> </TextBlock> <TextBlock Classes="small count" Text="{Binding Count}" IsVisible="{Binding !HasLimit}" /> </Panel> </Border> </Grid> <TextBlock Classes="small" Text="Over the work-in-progress limit" Foreground="#E5484D" IsVisible="{Binding IsOverLimit}" Margin="4,0" /> <ItemsControl ItemsSource="{Binding Cards}" a:Drag.Target="{Binding Key}" a:Drag.Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).PlaceCommand}" AutomationProperties.Name="{Binding Title}"> <ItemsControl.ItemsPanel><ItemsPanelTemplate><StackPanel Spacing="10" MinHeight="48" /></ItemsPanelTemplate></ItemsControl.ItemsPanel> <ItemsControl.DataTemplates> <DataTemplate x:DataType="m:TaskCard"> <Border Classes="card" Classes.blocked="{Binding Blocked}" Classes.late="{Binding Due, Converter={StaticResource IsLate}}" a:Drag.Item="{Binding Id}" AutomationProperties.Name="{Binding Title}"> <StackPanel Spacing="8"> <Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6"> <Border Width="8" Height="8" CornerRadius="4" VerticalAlignment="Center" Background="{Binding Priority, Converter={StaticResource PriorityBrush}}" /> <Border Grid.Column="1" Classes="kind task"><TextBlock Classes="small" Text="Task" FontWeight="SemiBold" /></Border> <TextBlock Grid.Column="2" Classes="muted small" Text="{Binding Id}" VerticalAlignment="Center" /> <Border Grid.Column="3" Classes="badge-blocked" IsVisible="{Binding Blocked}"><TextBlock Classes="small" Text="Blocked" Foreground="#FFFFFF" FontWeight="SemiBold" /></Border> </Grid> <TextBlock Classes="title wrap" Text="{Binding Title}" FontWeight="SemiBold" /> <Border Classes="chip" HorizontalAlignment="Left"><TextBlock Classes="small" Text="{Binding Area}" /></Border> <Grid ColumnDefinitions="*,Auto"> <TextBlock Classes="small due" Text="{Binding Due, Converter={StaticResource DueText}}" VerticalAlignment="Center" /> <TextBlock Classes="small done-note" Text="Completed" VerticalAlignment="Center" /> <Border Grid.Column="1" Classes="avatar" IsVisible="{Binding Owner, Converter={x:Static ObjectConverters.IsNotNull}}"> <TextBlock Text="{Binding Owner, Converter={StaticResource Initials}}" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Border> <TextBlock Grid.Column="1" Classes="muted small" Text="Unassigned" IsVisible="{Binding Owner, Converter={x:Static ObjectConverters.IsNull}}" VerticalAlignment="Center" /> </Grid> <StackPanel Orientation="Horizontal" Spacing="2"> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).MoveLeftCommand}" CommandParameter="{Binding Id}" IsVisible="{Binding CanMoveLeft}" AutomationProperties.Name="Move left"><a:Icon Kind="chevron-left" Size="16" /></Button> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).MoveRightCommand}" CommandParameter="{Binding Id}" IsVisible="{Binding CanMoveRight}" AutomationProperties.Name="Move right"><a:Icon Kind="chevron-right" Size="16" /></Button> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).BlockCommand}" CommandParameter="{Binding Id}" AutomationProperties.Name="Block or unblock"><a:Icon Kind="lock" Size="15" /></Button> </StackPanel> </StackPanel> </Border> </DataTemplate> <DataTemplate x:DataType="m:BugCard"> <Border Classes="card" Classes.blocked="{Binding Blocked}" Classes.late="{Binding Due, Converter={StaticResource IsLate}}" a:Drag.Item="{Binding Id}" AutomationProperties.Name="{Binding Title}"> <StackPanel Spacing="8"> <Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6"> <Border Width="8" Height="8" CornerRadius="4" VerticalAlignment="Center" Background="{Binding Priority, Converter={StaticResource PriorityBrush}}" /> <Border Grid.Column="1" Classes="kind bug"><TextBlock Classes="small" Text="Bug" FontWeight="SemiBold" /></Border> <TextBlock Grid.Column="2" Classes="muted small" Text="{Binding Id}" VerticalAlignment="Center" /> <Border Grid.Column="3" Classes="badge-blocked" IsVisible="{Binding Blocked}"><TextBlock Classes="small" Text="Blocked" Foreground="#FFFFFF" FontWeight="SemiBold" /></Border> </Grid> <TextBlock Classes="title wrap" Text="{Binding Title}" FontWeight="SemiBold" /> <StackPanel Orientation="Horizontal" Spacing="4"> <TextBlock Classes="muted small" Text="{Binding Reports, Converter={StaticResource Counted}, ConverterParameter=report}" /> <TextBlock Classes="muted small" Text="{Binding Environment, StringFormat='· {0}'}" /> </StackPanel> <Grid ColumnDefinitions="*,Auto"> <TextBlock Classes="small due" Text="{Binding Due, Converter={StaticResource DueText}}" VerticalAlignment="Center" /> <TextBlock Classes="small done-note" Text="Completed" VerticalAlignment="Center" /> <Border Grid.Column="1" Classes="avatar" IsVisible="{Binding Owner, Converter={x:Static ObjectConverters.IsNotNull}}"> <TextBlock Text="{Binding Owner, Converter={StaticResource Initials}}" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Border> <TextBlock Grid.Column="1" Classes="muted small" Text="Unassigned" IsVisible="{Binding Owner, Converter={x:Static ObjectConverters.IsNull}}" VerticalAlignment="Center" /> </Grid> <StackPanel Orientation="Horizontal" Spacing="2"> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).MoveLeftCommand}" CommandParameter="{Binding Id}" IsVisible="{Binding CanMoveLeft}" AutomationProperties.Name="Move left"><a:Icon Kind="chevron-left" Size="16" /></Button> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).MoveRightCommand}" CommandParameter="{Binding Id}" IsVisible="{Binding CanMoveRight}" AutomationProperties.Name="Move right"><a:Icon Kind="chevron-right" Size="16" /></Button> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).BlockCommand}" CommandParameter="{Binding Id}" AutomationProperties.Name="Block or unblock"><a:Icon Kind="lock" Size="15" /></Button> </StackPanel> </StackPanel> </Border> </DataTemplate> <DataTemplate x:DataType="m:EpicCard"> <Border Classes="card" Classes.blocked="{Binding Blocked}" Classes.late="{Binding Due, Converter={StaticResource IsLate}}" a:Drag.Item="{Binding Id}" AutomationProperties.Name="{Binding Title}"> <StackPanel Spacing="8"> <Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6"> <Border Width="8" Height="8" CornerRadius="4" VerticalAlignment="Center" Background="{Binding Priority, Converter={StaticResource PriorityBrush}}" /> <Border Grid.Column="1" Classes="kind epic"><TextBlock Classes="small" Text="Epic" FontWeight="SemiBold" /></Border> <TextBlock Grid.Column="2" Classes="muted small" Text="{Binding Id}" VerticalAlignment="Center" /> <Border Grid.Column="3" Classes="badge-blocked" IsVisible="{Binding Blocked}"><TextBlock Classes="small" Text="Blocked" Foreground="#FFFFFF" FontWeight="SemiBold" /></Border> </Grid> <TextBlock Classes="title wrap" Text="{Binding Title}" FontWeight="SemiBold" /> <ProgressBar Value="{Binding Percent}" Maximum="100" Height="6" MinHeight="6" /> <TextBlock Classes="muted small"> <TextBlock.Text><MultiBinding StringFormat="{}{0} of {1} stories done"><Binding Path="Done" /><Binding Path="Total" /></MultiBinding></TextBlock.Text> </TextBlock> <Grid ColumnDefinitions="*,Auto"> <TextBlock Classes="small due" Text="{Binding Due, Converter={StaticResource DueText}}" VerticalAlignment="Center" /> <TextBlock Classes="small done-note" Text="Completed" VerticalAlignment="Center" /> <Border Grid.Column="1" Classes="avatar" IsVisible="{Binding Owner, Converter={x:Static ObjectConverters.IsNotNull}}"> <TextBlock Text="{Binding Owner, Converter={StaticResource Initials}}" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Border> <TextBlock Grid.Column="1" Classes="muted small" Text="Unassigned" IsVisible="{Binding Owner, Converter={x:Static ObjectConverters.IsNull}}" VerticalAlignment="Center" /> </Grid> <StackPanel Orientation="Horizontal" Spacing="2"> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).MoveLeftCommand}" CommandParameter="{Binding Id}" IsVisible="{Binding CanMoveLeft}" AutomationProperties.Name="Move left"><a:Icon Kind="chevron-left" Size="16" /></Button> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).MoveRightCommand}" CommandParameter="{Binding Id}" IsVisible="{Binding CanMoveRight}" AutomationProperties.Name="Move right"><a:Icon Kind="chevron-right" Size="16" /></Button> <Button Theme="{StaticResource CardAction}" Command="{Binding $parent[UserControl].((m:KanbanModel)DataContext).BlockCommand}" CommandParameter="{Binding Id}" AutomationProperties.Name="Block or unblock"><a:Icon Kind="lock" Size="15" /></Button> </StackPanel> </StackPanel> </Border> </DataTemplate> </ItemsControl.DataTemplates> </ItemsControl> <TextBlock Classes="muted small" Text="No cards" IsVisible="{Binding IsEmpty}" HorizontalAlignment="Center" Margin="0,8" /> </StackPanel> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </ScrollViewer> <Expander Classes="source" Header="View the XAML of this page"> <Border Classes="code"><SelectableTextBlock Classes="source" Text="{Binding Xaml}" /></Border> </Expander> </StackPanel> </UserControl>