diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs index e6b838d5b..202f7e3a2 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs @@ -90,6 +90,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "526", Text = "Spinner", Href = DemoRouteConstants.Demos_URL_Spinners, IconName = IconName.ArrowRepeat, ParentId = "5" }, new (){ Id = "527", Text = "Tabs", Href = DemoRouteConstants.Demos_URL_Tabs, IconName = IconName.WindowPlus, ParentId = "5" }, new (){ Id = "528", Text = "Theme Switcher", Href = DemoRouteConstants.Demos_URL_ThemeSwitcher, IconName = IconName.NintendoSwitch, ParentId = "5" }, + new (){ Id = "5285", Text = "Timeline", Href = DemoRouteConstants.Demos_URL_Timeline, IconName = IconName.ClockHistory, ParentId = "5" }, new (){ Id = "529", Text = "Toasts", Href = DemoRouteConstants.Demos_URL_Toasts, IconName = IconName.ExclamationTriangleFill, ParentId = "5" }, new (){ Id = "530", Text = "Tooltips", Href = DemoRouteConstants.Demos_URL_Tooltips, IconName = IconName.ChatSquareDotsFill, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs index 1402be6ea..d6aa2e9e7 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs @@ -89,6 +89,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "526", Text = "Spinner", Href = DemoRouteConstants.Demos_URL_Spinners, IconName = IconName.ArrowRepeat, ParentId = "5" }, new (){ Id = "527", Text = "Tabs", Href = DemoRouteConstants.Demos_URL_Tabs, IconName = IconName.WindowPlus, ParentId = "5" }, new (){ Id = "528", Text = "Theme Switcher", Href = DemoRouteConstants.Demos_URL_ThemeSwitcher, IconName = IconName.NintendoSwitch, ParentId = "5" }, + new (){ Id = "5285", Text = "Timeline", Href = DemoRouteConstants.Demos_URL_Timeline, IconName = IconName.ClockHistory, ParentId = "5" }, new (){ Id = "529", Text = "Toasts", Href = DemoRouteConstants.Demos_URL_Toasts, IconName = IconName.ExclamationTriangleFill, ParentId = "5" }, new (){ Id = "530", Text = "Tooltips", Href = DemoRouteConstants.Demos_URL_Tooltips, IconName = IconName.ChatSquareDotsFill, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/TimelineDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/TimelineDocumentation.razor new file mode 100644 index 000000000..8331cb5dd --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/TimelineDocumentation.razor @@ -0,0 +1,40 @@ +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + +
+
The default orientation renders events vertically with the timeline rail aligned to the left.
+ +
+ +
+
Use Orientation, Alignment, and AlternateItems to select a layout.
+ +
+ +
+
Items can include images, badges, custom fragments, custom icon classes, and multiple accessible actions.
+ +
+ +
+
The component rerenders as items are added to or removed from the bound collection.
+ +
+ +@code { + private const string pageUrl = DemoRouteConstants.Demos_URL_Timeline; + private const string pageTitle = "Blazor Timeline"; + private const string pageDescription = "The Blazor Bootstrap Timeline component displays chronological events in responsive vertical or horizontal layouts."; + private const string metaTitle = "Blazor Timeline Component"; + private const string metaDescription = "Display chronological events with responsive layouts, custom content, images, icons, badges, and actions."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Timeline; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_01_Vertical.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_01_Vertical.razor new file mode 100644 index 000000000..0fc45d09a --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_01_Vertical.razor @@ -0,0 +1,10 @@ + + +@code { + private readonly List items = + [ + new() { Title = "Project created", Description = "The project workspace and repository were created.", Timestamp = "March 3, 2026", IconCssClass = "bi bi-folder-plus" }, + new() { Title = "Design approved", Description = "The component design was reviewed and approved.", Timestamp = "March 8, 2026", IconCssClass = "bi bi-check2-circle", IconColor = IconColor.Success, Badge = "Approved", BadgeColor = BadgeColor.Success }, + new() { Title = "Development started", Description = "Implementation began on the reusable timeline component.", Timestamp = "March 10, 2026", IconCssClass = "bi bi-code-slash", IconColor = IconColor.Info } + ]; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_02_Layouts.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_02_Layouts.razor new file mode 100644 index 000000000..b89822b9a --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_02_Layouts.razor @@ -0,0 +1,17 @@ +

Right aligned

+ + +

Alternating

+ + +

Horizontal

+ + +@code { + private readonly List items = + [ + new() { Title = "Plan", Description = "Define the requirements.", Timestamp = "Step 1", IconCssClass = "bi bi-list-check" }, + new() { Title = "Build", Description = "Implement the component.", Timestamp = "Step 2", IconCssClass = "bi bi-tools", IconColor = IconColor.Warning }, + new() { Title = "Ship", Description = "Release to users.", Timestamp = "Step 3", IconCssClass = "bi bi-rocket-takeoff", IconColor = IconColor.Success } + ]; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_03_Content_Actions.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_03_Content_Actions.razor new file mode 100644 index 000000000..ddfbaa507 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_03_Content_Actions.razor @@ -0,0 +1,40 @@ +@if (!string.IsNullOrWhiteSpace(message)) +{ + @message +} + + + + +@code { + private readonly List items = []; + private string? message; + + protected override void OnInitialized() + { + var release = new TimelineItem + { + Title = "Version 4.0 released", + Description = "The release includes new components and accessibility improvements.", + Timestamp = "August 19, 2026 at 10:30 AM", + Badge = "Latest", + BadgeColor = BadgeColor.Success, + IconCssClass = "bi bi-github", + IconColor = IconColor.Dark, + ImageUrl = "https://picsum.photos/seed/blazor-timeline/900/300", + ImageAlt = "Abstract release banner", + ChildContent = builder => builder.AddMarkupContent(0, "
Package available from NuGet.
") + }; + + release.Actions = + [ + new() { Text = "View", IconCssClass = "bi bi-eye", Color = ButtonColor.Primary, OnClick = EventCallback.Factory.Create(this, item => ShowMessage($"Viewing {item.Title}")) }, + new() { Text = "Download", IconCssClass = "bi bi-download", Color = ButtonColor.Success, OnClick = EventCallback.Factory.Create(this, item => ShowMessage($"Downloading {item.Title}")) }, + new() { Text = "Delete", IconCssClass = "bi bi-trash", Color = ButtonColor.Danger, Disabled = true, AriaLabel = "Delete release (disabled)" } + ]; + + items.Add(release); + } + + private void ShowMessage(string value) => message = value; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_04_Dynamic_Data.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_04_Dynamic_Data.razor new file mode 100644 index 000000000..c50a75c08 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Timeline/Timeline_Demo_04_Dynamic_Data.razor @@ -0,0 +1,31 @@ +
+ + +
+ + + +@code { + private readonly List items = []; + private int eventNumber; + + protected override void OnInitialized() => AddItem(); + + private void AddItem() + { + eventNumber++; + items.Insert(0, new TimelineItem + { + Title = $"Event {eventNumber}", + Description = "This item was added to the bound collection.", + Timestamp = DateTime.Now.ToString("g"), + IconCssClass = "bi bi-plus-lg" + }); + } + + private void RemoveItem() + { + if (items.Count > 0) + items.RemoveAt(0); + } +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Timeline/Timeline_Doc_01_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Timeline/Timeline_Doc_01_Documentation.razor new file mode 100644 index 000000000..9c3dc5cf2 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Timeline/Timeline_Doc_01_Documentation.razor @@ -0,0 +1,33 @@ +@attribute [Route(pageUrl)] +@layout DocsMainLayout + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +@code { + private const string componentName = nameof(Timeline); + private const string pageUrl = DemoRouteConstants.Docs_URL_Timeline; + private const string pageTitle = componentName; + private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} component and its layout options."; + private const string metaTitle = $"Blazor {componentName} Component"; + private const string metaDescription = $"API reference for the Blazor Bootstrap {componentName} component."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Timeline; +} diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs index ece6c25ad..062c82609 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs @@ -97,6 +97,7 @@ public static class DemoRouteConstants public const string Demos_URL_Spinners = Demos_URL_Prefix + "/spinners"; public const string Demos_URL_Tabs = Demos_URL_Prefix + "/tabs"; public const string Demos_URL_ThemeSwitcher = Demos_URL_Prefix + "/theme-switcher"; + public const string Demos_URL_Timeline = Demos_URL_Prefix + "/timeline"; public const string Demos_URL_Toasts = Demos_URL_Prefix + "/toasts"; public const string Demos_URL_Tooltips = Demos_URL_Prefix + "/tooltips"; @@ -192,6 +193,7 @@ public static class DemoRouteConstants public const string Docs_URL_Spinners = Docs_URL_Prefix + "/spinners"; public const string Docs_URL_Tabs = Docs_URL_Prefix + "/tabs"; public const string Docs_URL_ThemeSwitcher = Docs_URL_Prefix + "/theme-switcher"; + public const string Docs_URL_Timeline = Docs_URL_Prefix + "/timeline"; public const string Docs_URL_Toasts = Docs_URL_Prefix + "/toasts"; public const string Docs_URL_Tooltips = Docs_URL_Prefix + "/tooltips"; diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs index 127097a7b..eccf13d21 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs @@ -86,6 +86,7 @@ public class DemoScreenshotSrcConstants public const string Demos_URL_SortableList = DemoScreenshotSrcPrefix + "sortable-list.png"; public const string Demos_URL_Spinners = DemoScreenshotSrcPrefix + "spinners.png"; public const string Demos_URL_Tabs = DemoScreenshotSrcPrefix + "tabs.png"; + public const string Demos_URL_Timeline = DemoScreenshotSrcPrefix + "home.png"; // TODO: replace with timeline screenshot public const string Demos_URL_ThemeSwitcher = DemoScreenshotSrcPrefix + "theme-switcher.png"; public const string Demos_URL_Toasts = DemoScreenshotSrcPrefix + "toasts.png"; public const string Demos_URL_Tooltips = DemoScreenshotSrcPrefix + "tooltips.png"; diff --git a/blazorbootstrap/Components/Timeline/Timeline.razor b/blazorbootstrap/Components/Timeline/Timeline.razor new file mode 100644 index 000000000..0784c420c --- /dev/null +++ b/blazorbootstrap/Components/Timeline/Timeline.razor @@ -0,0 +1,68 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + +
    + @if (Items is not null) + { + @foreach (var item in Items) + { +
  1. + +
    +
    + @if (!string.IsNullOrWhiteSpace(item.Timestamp)) + { + + } + @if (!string.IsNullOrWhiteSpace(item.Title) || !string.IsNullOrWhiteSpace(item.Badge)) + { +
    + @if (!string.IsNullOrWhiteSpace(item.Title)) + { +

    @item.Title

    + } + @if (!string.IsNullOrWhiteSpace(item.Badge)) + { + @item.Badge + } +
    + } + @if (!string.IsNullOrWhiteSpace(item.Description)) + { +

    @item.Description

    + } + @if (!string.IsNullOrWhiteSpace(item.ImageUrl)) + { + @item.ImageAlt + } + @item.ChildContent + @if (item.Actions?.Any() == true) + { +
    + @foreach (var action in item.Actions) + { + + } +
    + } +
    +
    +
  2. + } + } +
diff --git a/blazorbootstrap/Components/Timeline/Timeline.razor.cs b/blazorbootstrap/Components/Timeline/Timeline.razor.cs new file mode 100644 index 000000000..106a05179 --- /dev/null +++ b/blazorbootstrap/Components/Timeline/Timeline.razor.cs @@ -0,0 +1,49 @@ +namespace BlazorBootstrap; + +/// +/// Displays a responsive chronological sequence of events. +/// +public partial class Timeline : BlazorBootstrapComponentBase +{ + protected override string? ClassNames => + BuildClassNames(Class, + ("bb-timeline", true), + ("bb-timeline-vertical", Orientation == TimelineOrientation.Vertical), + ("bb-timeline-horizontal", Orientation == TimelineOrientation.Horizontal), + ("bb-timeline-right", Orientation == TimelineOrientation.Vertical && Alignment == TimelineAlignment.Right), + ("bb-timeline-alternate", Orientation == TimelineOrientation.Vertical && AlternateItems)); + + private static string GetActionClass(TimelineAction action) => + BuildClassNames(action.CssClass, ("btn btn-sm", true), (action.Color.ToButtonColorClass(), action.Color != ButtonColor.None)); + + private static string GetBadgeClass(TimelineItem item) => + BuildClassNames(("badge", true), (item.BadgeColor.ToBadgeColorClass(), item.BadgeColor != BadgeColor.None)); + + private static string GetIconClass(TimelineItem item) => + BuildClassNames((item.IconCssClass, true), (item.IconColor.ToIconColorClass(), item.IconColor != IconColor.None)); + + private static string GetItemClass(TimelineItem item) => BuildClassNames(item.CssClass, ("bb-timeline-item", true)); + + private static Task InvokeActionAsync(TimelineAction action, TimelineItem item) => + action.Disabled || !action.OnClick.HasDelegate ? Task.CompletedTask : action.OnClick.InvokeAsync(item); + + /// Gets or sets the accessible name of the timeline. + [Parameter] + public string AriaLabel { get; set; } = "Timeline"; + + /// Gets or sets the alignment of a vertical timeline. + [Parameter] + public TimelineAlignment Alignment { get; set; } = TimelineAlignment.Left; + + /// Gets or sets whether vertical items alternate around a centered rail on larger screens. + [Parameter] + public bool AlternateItems { get; set; } + + /// Gets or sets the events to display. + [Parameter] + public IEnumerable? Items { get; set; } + + /// Gets or sets the timeline orientation. + [Parameter] + public TimelineOrientation Orientation { get; set; } = TimelineOrientation.Vertical; +} diff --git a/blazorbootstrap/Components/Timeline/Timeline.razor.css b/blazorbootstrap/Components/Timeline/Timeline.razor.css new file mode 100644 index 000000000..6ccac2203 --- /dev/null +++ b/blazorbootstrap/Components/Timeline/Timeline.razor.css @@ -0,0 +1,86 @@ +.bb-timeline { + --bb-timeline-color: var(--bs-primary); + --bb-timeline-gap: 1.5rem; + list-style: none; + margin: 0; + padding: 0; +} + +.bb-timeline-item { position: relative; } +.bb-timeline-marker { + align-items: center; + background: var(--bs-body-bg); + border: .2rem solid var(--bb-timeline-color); + border-radius: 50%; + display: flex; + height: 2.25rem; + justify-content: center; + width: 2.25rem; + z-index: 1; +} +.bb-timeline-content { min-width: 0; } +.bb-timeline-time { display: block; margin-bottom: .5rem; } + +.bb-timeline-vertical { position: relative; } +.bb-timeline-vertical::before { + background: var(--bb-timeline-color); + bottom: 0; + content: ""; + left: 1.05rem; + position: absolute; + top: 0; + width: .15rem; +} +.bb-timeline-vertical > .bb-timeline-item { + padding: 0 0 var(--bb-timeline-gap) 3.25rem; +} +.bb-timeline-vertical > .bb-timeline-item:last-child { padding-bottom: 0; } +.bb-timeline-vertical > .bb-timeline-item > .bb-timeline-marker { + left: 0; + position: absolute; + top: .75rem; +} + +.bb-timeline-vertical.bb-timeline-right::before { left: auto; right: 1.05rem; } +.bb-timeline-vertical.bb-timeline-right > .bb-timeline-item { padding-left: 0; padding-right: 3.25rem; } +.bb-timeline-vertical.bb-timeline-right > .bb-timeline-item > .bb-timeline-marker { left: auto; right: 0; } + +.bb-timeline-horizontal { + display: flex; + gap: var(--bb-timeline-gap); + overflow-x: auto; + padding-top: 3.25rem; + position: relative; +} +.bb-timeline-horizontal::before { + background: var(--bb-timeline-color); + content: ""; + height: .15rem; + left: 0; + position: absolute; + right: 0; + top: 1.05rem; +} +.bb-timeline-horizontal > .bb-timeline-item { flex: 1 0 min(20rem, 85vw); } +.bb-timeline-horizontal > .bb-timeline-item > .bb-timeline-marker { + left: 0; + position: absolute; + top: -3.25rem; +} + +@media (min-width: 768px) { + .bb-timeline-vertical.bb-timeline-alternate::before { left: calc(50% - .075rem); right: auto; } + .bb-timeline-vertical.bb-timeline-alternate > .bb-timeline-item { padding-left: 0; padding-right: calc(50% + 1.75rem); } + .bb-timeline-vertical.bb-timeline-alternate > .bb-timeline-item:nth-child(even) { padding-left: calc(50% + 1.75rem); padding-right: 0; } + .bb-timeline-vertical.bb-timeline-alternate > .bb-timeline-item > .bb-timeline-marker, + .bb-timeline-vertical.bb-timeline-alternate > .bb-timeline-item:nth-child(even) > .bb-timeline-marker { + left: calc(50% - 1.125rem); + right: auto; + } +} + +@media (max-width: 767.98px) { + .bb-timeline-vertical.bb-timeline-alternate::before { left: 1.05rem; right: auto; } + .bb-timeline-vertical.bb-timeline-alternate > .bb-timeline-item { padding-left: 3.25rem; padding-right: 0; } + .bb-timeline-vertical.bb-timeline-alternate > .bb-timeline-item > .bb-timeline-marker { left: 0; right: auto; } +} diff --git a/blazorbootstrap/Enums/TimelineAlignment.cs b/blazorbootstrap/Enums/TimelineAlignment.cs new file mode 100644 index 000000000..35320c4eb --- /dev/null +++ b/blazorbootstrap/Enums/TimelineAlignment.cs @@ -0,0 +1,13 @@ +namespace BlazorBootstrap; + +/// +/// Defines the side on which a vertical timeline is displayed. +/// +public enum TimelineAlignment +{ + /// Displays the timeline rail on the left. + Left, + + /// Displays the timeline rail on the right. + Right +} diff --git a/blazorbootstrap/Enums/TimelineOrientation.cs b/blazorbootstrap/Enums/TimelineOrientation.cs new file mode 100644 index 000000000..392a6fa12 --- /dev/null +++ b/blazorbootstrap/Enums/TimelineOrientation.cs @@ -0,0 +1,13 @@ +namespace BlazorBootstrap; + +/// +/// Defines the direction in which timeline items are laid out. +/// +public enum TimelineOrientation +{ + /// Displays items from top to bottom. + Vertical, + + /// Displays items from left to right. + Horizontal +} diff --git a/blazorbootstrap/Models/TimelineAction.cs b/blazorbootstrap/Models/TimelineAction.cs new file mode 100644 index 000000000..ecb3d0e76 --- /dev/null +++ b/blazorbootstrap/Models/TimelineAction.cs @@ -0,0 +1,28 @@ +namespace BlazorBootstrap; + +/// +/// Represents an action displayed on a . +/// +public class TimelineAction +{ + /// Gets or sets the accessible label for the action. + public string? AriaLabel { get; set; } + + /// Gets or sets the Bootstrap button color. + public ButtonColor Color { get; set; } = ButtonColor.Secondary; + + /// Gets or sets additional CSS classes applied to the button. + public string? CssClass { get; set; } + + /// Gets or sets a value indicating whether the action is disabled. + public bool Disabled { get; set; } + + /// Gets or sets Bootstrap Icons, Font Awesome, or custom icon CSS classes. + public string? IconCssClass { get; set; } + + /// Gets or sets the callback invoked with the containing item. + public EventCallback OnClick { get; set; } + + /// Gets or sets the button text. + public string? Text { get; set; } +} diff --git a/blazorbootstrap/Models/TimelineItem.cs b/blazorbootstrap/Models/TimelineItem.cs new file mode 100644 index 000000000..093b6d331 --- /dev/null +++ b/blazorbootstrap/Models/TimelineItem.cs @@ -0,0 +1,46 @@ +namespace BlazorBootstrap; + +/// +/// Represents an event rendered by a . +/// +public class TimelineItem +{ + /// Gets or sets the actions displayed below the item content. + public IEnumerable? Actions { get; set; } + + /// Gets or sets an optional badge or label. + public string? Badge { get; set; } + + /// Gets or sets the Bootstrap color used by the badge. + public BadgeColor BadgeColor { get; set; } = BadgeColor.Secondary; + + /// Gets or sets additional content rendered in the item body. + public RenderFragment? ChildContent { get; set; } + + /// Gets or sets additional CSS classes applied to the item. + public string? CssClass { get; set; } + + /// Gets or sets the description. + public string? Description { get; set; } + + /// Gets or sets Bootstrap Icons, Font Awesome, or custom icon CSS classes. + public string? IconCssClass { get; set; } + + /// Gets or sets the icon color. + public IconColor IconColor { get; set; } = IconColor.Primary; + + /// Gets or sets an accessible description of the image. + public string? ImageAlt { get; set; } + + /// Gets or sets the optional image URL. + public string? ImageUrl { get; set; } + + /// Gets or sets a value indicating whether the timeline marker icon is hidden. + public bool ShowIcon { get; set; } = true; + + /// Gets or sets the optional timestamp. + public string? Timestamp { get; set; } + + /// Gets or sets the item title. + public string? Title { get; set; } +}