diff --git a/demo/Ursa.Demo/Pages/TimelineDemo.axaml b/demo/Ursa.Demo/Pages/TimelineDemo.axaml
new file mode 100644
index 0000000..b217fad
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/TimelineDemo.axaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/Ursa.Demo/Pages/TimelineDemo.axaml.cs b/demo/Ursa.Demo/Pages/TimelineDemo.axaml.cs
new file mode 100644
index 0000000..fe07b76
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/TimelineDemo.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Ursa.Demo.ViewModels;
+
+namespace Ursa.Demo.Pages;
+
+public partial class TimelineDemo : UserControl
+{
+ public TimelineDemo()
+ {
+ InitializeComponent();
+ this.DataContext = new TimelineDemoViewModel();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Ursa.Demo.csproj b/demo/Ursa.Demo/Ursa.Demo.csproj
index 7cc1b6c..f66e639 100644
--- a/demo/Ursa.Demo/Ursa.Demo.csproj
+++ b/demo/Ursa.Demo/Ursa.Demo.csproj
@@ -5,6 +5,7 @@
enable
true
app.manifest
+ false
diff --git a/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs
new file mode 100644
index 0000000..2420ee3
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs
@@ -0,0 +1,115 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Ursa.Controls;
+
+namespace Ursa.Demo.ViewModels;
+
+public class TimelineDemoViewModel: ObservableObject
+{
+ public TimelineItemViewModel[] Items { get; } =
+ {
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "yyyy-MM-dd HH:mm:ss",
+ Description = "Item 1",
+ Content = "First",
+ ItemType = TimelineItemType.Success,
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 2",
+ Content = "Content 2",
+ ItemType = TimelineItemType.Success,
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 3",
+ Content = "Content 3",
+ ItemType = TimelineItemType.Ongoing,
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 4",
+ Content = "Content 4"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 5",
+ Content = "Content 5"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 6",
+ Content = "Content 6"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 7",
+ Content = "Content 71231"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 8",
+ Content = "Content 8123123"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 9",
+ Content = "Content 9123123"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 10",
+ Content = "Content 1231231231231231231230"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 11",
+ Content = "Content 11231231"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 12",
+ Content = "Content 12123123123123"
+ },
+ new()
+ {
+ Time = DateTime.Now,
+ TimeFormat = "HH:mm:ss",
+ Description = "Item 13",
+ Content = "Last"
+ }
+ };
+}
+
+public class TimelineItemViewModel: ObservableObject
+{
+ public DateTime Time { get; set; }
+ public string? TimeFormat { get; set; }
+ public string? Description { get; set; }
+ public string? Content { get; set; }
+ public TimelineItemType ItemType { get; set; }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Views/MainWindow.axaml b/demo/Ursa.Demo/Views/MainWindow.axaml
index de1f2dd..9ba93b8 100644
--- a/demo/Ursa.Demo/Views/MainWindow.axaml
+++ b/demo/Ursa.Demo/Views/MainWindow.axaml
@@ -35,6 +35,9 @@
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/Timeline.axaml b/src/Ursa.Themes.Semi/Controls/Timeline.axaml
new file mode 100644
index 0000000..63e311c
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/Timeline.axaml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index 4d15016..7c08f47 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -5,5 +5,6 @@
+
diff --git a/src/Ursa.Themes.Semi/Converters/TimelineItemTypeToIconForegroundConverter.cs b/src/Ursa.Themes.Semi/Converters/TimelineItemTypeToIconForegroundConverter.cs
new file mode 100644
index 0000000..2b2d1ab
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Converters/TimelineItemTypeToIconForegroundConverter.cs
@@ -0,0 +1,82 @@
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using Ursa.Controls;
+
+namespace Ursa.Themes.Semi.Converters;
+
+public class TimelineItemTypeToIconForegroundConverter: AvaloniaObject, IMultiValueConverter
+{
+ public static readonly StyledProperty DefaultBrushProperty = AvaloniaProperty.Register(
+ nameof(DefaultBrush));
+
+ public IBrush DefaultBrush
+ {
+ get => GetValue(DefaultBrushProperty);
+ set => SetValue(DefaultBrushProperty, value);
+ }
+
+ public static readonly StyledProperty OngoingBrushProperty = AvaloniaProperty.Register(
+ nameof(OngoingBrush));
+
+ public IBrush OngoingBrush
+ {
+ get => GetValue(OngoingBrushProperty);
+ set => SetValue(OngoingBrushProperty, value);
+ }
+
+ public static readonly StyledProperty SuccessBrushProperty = AvaloniaProperty.Register(
+ nameof(SuccessBrush));
+
+ public IBrush SuccessBrush
+ {
+ get => GetValue(SuccessBrushProperty);
+ set => SetValue(SuccessBrushProperty, value);
+ }
+
+ public static readonly StyledProperty WarningBrushProperty = AvaloniaProperty.Register(
+ nameof(WarningBrush));
+
+ public IBrush WarningBrush
+ {
+ get => GetValue(WarningBrushProperty);
+ set => SetValue(WarningBrushProperty, value);
+ }
+
+ public static readonly StyledProperty ErrorBrushProperty = AvaloniaProperty.Register(
+ nameof(ErrorBrush));
+
+ public IBrush ErrorBrush
+ {
+ get => GetValue(ErrorBrushProperty);
+ set => SetValue(ErrorBrushProperty, value);
+ }
+
+
+ public object? Convert(IList