Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ internal override IEnumerable<NavItem> 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" },

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ internal override IEnumerable<NavItem> 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" },

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@attribute [Route(pageUrl)]
@layout DemosMainLayout

<DemosPageHeadSection PageUrl="@pageUrl"
PageTitle="@pageTitle"
PageDescription="@pageDescription"
MetaTitle="@metaTitle"
MetaDescription="@metaDescription"
ImageUrl="@imageUrl" />

<DocsLink Href="@DemoRouteConstants.Docs_URL_Timeline" />

<Section Size="HeadingSize.H2" Name="Vertical timeline" PageUrl="@pageUrl" Link="vertical-timeline">
<div class="mb-3">The default orientation renders events vertically with the timeline rail aligned to the left.</div>
<Demo Type="typeof(Timeline_Demo_01_Vertical)" Tabs="true" />
</Section>

<Section Size="HeadingSize.H2" Name="Layouts" PageUrl="@pageUrl" Link="layouts">
<div class="mb-3">Use <code>Orientation</code>, <code>Alignment</code>, and <code>AlternateItems</code> to select a layout.</div>
<Demo Type="typeof(Timeline_Demo_02_Layouts)" Tabs="true" />
</Section>

<Section Size="HeadingSize.H2" Name="Rich content and actions" PageUrl="@pageUrl" Link="content-actions">
<div class="mb-3">Items can include images, badges, custom fragments, custom icon classes, and multiple accessible actions.</div>
<Demo Type="typeof(Timeline_Demo_03_Content_Actions)" Tabs="true" />
</Section>

<Section Size="HeadingSize.H2" Name="Dynamic data" PageUrl="@pageUrl" Link="dynamic-data">
<div class="mb-3">The component rerenders as items are added to or removed from the bound collection.</div>
<Demo Type="typeof(Timeline_Demo_04_Dynamic_Data)" Tabs="true" />
</Section>

@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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Timeline Items="items" />

@code {
private readonly List<TimelineItem> 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 }
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<h3 class="h5">Right aligned</h3>
<Timeline Items="items" Alignment="TimelineAlignment.Right" Class="mb-5" />

<h3 class="h5">Alternating</h3>
<Timeline Items="items" AlternateItems="true" Class="mb-5" />

<h3 class="h5">Horizontal</h3>
<Timeline Items="items" Orientation="TimelineOrientation.Horizontal" />

@code {
private readonly List<TimelineItem> 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 }
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@if (!string.IsNullOrWhiteSpace(message))
{
<Alert Color="AlertColor.Info">@message</Alert>
}

<Timeline Items="items" AriaLabel="Release activity" Style="--bb-timeline-color: var(--bs-success);">
</Timeline>

@code {
private readonly List<TimelineItem> 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, "<div class=\"small text-body-secondary\"><i class=\"bi bi-box-seam me-1\"></i>Package available from NuGet.</div>")
};

release.Actions =
[
new() { Text = "View", IconCssClass = "bi bi-eye", Color = ButtonColor.Primary, OnClick = EventCallback.Factory.Create<TimelineItem>(this, item => ShowMessage($"Viewing {item.Title}")) },
new() { Text = "Download", IconCssClass = "bi bi-download", Color = ButtonColor.Success, OnClick = EventCallback.Factory.Create<TimelineItem>(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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="d-flex gap-2 mb-3">
<Button Color="ButtonColor.Primary" @onclick="AddItem">Add event</Button>
<Button Color="ButtonColor.Danger" Disabled="@(items.Count == 0)" @onclick="RemoveItem">Remove latest</Button>
</div>

<Timeline Items="items" />

@code {
private readonly List<TimelineItem> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@attribute [Route(pageUrl)]
@layout DocsMainLayout

<DocsPageHeadSection PageUrl="@pageUrl"
PageTitle="@pageTitle"
PageDescription="@pageDescription"
MetaTitle="@metaTitle"
MetaDescription="@metaDescription"
ImageUrl="@imageUrl" />

<DemoLink Href="@DemoRouteConstants.Demos_URL_Timeline" />

<Section Class="p-0" Size="HeadingSize.H3" Name="Timeline Parameters" PageUrl="@pageUrl" Link="timeline-parameters">
<DocxTable TItem="Timeline" DocType="DocType.Parameters" />
</Section>

<Section Class="p-0" Size="HeadingSize.H3" Name="TimelineOrientation Enum" PageUrl="@pageUrl" Link="timeline-orientation-enum">
<DocxTable TItem="TimelineOrientation" DocType="DocType.Enum" />
</Section>

<Section Class="p-0" Size="HeadingSize.H3" Name="TimelineAlignment Enum" PageUrl="@pageUrl" Link="timeline-alignment-enum">
<DocxTable TItem="TimelineAlignment" DocType="DocType.Enum" />
</Section>

@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 <code>{componentName}</code> 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;
}
2 changes: 2 additions & 0 deletions BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
68 changes: 68 additions & 0 deletions blazorbootstrap/Components/Timeline/Timeline.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@namespace BlazorBootstrap
@inherits BlazorBootstrapComponentBase

<ol @ref="Element" id="@Id" class="@ClassNames" style="@StyleNames" aria-label="@AriaLabel" @attributes="AdditionalAttributes">
@if (Items is not null)
{
@foreach (var item in Items)
{
<li class="@GetItemClass(item)">
<div class="bb-timeline-marker" aria-hidden="true">
@if (item.ShowIcon && !string.IsNullOrWhiteSpace(item.IconCssClass))
{
<i class="@GetIconClass(item)"></i>
}
</div>
<article class="bb-timeline-content card shadow-sm">
<div class="card-body">
@if (!string.IsNullOrWhiteSpace(item.Timestamp))
{
<time class="bb-timeline-time text-body-secondary small">@item.Timestamp</time>
}
@if (!string.IsNullOrWhiteSpace(item.Title) || !string.IsNullOrWhiteSpace(item.Badge))
{
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
@if (!string.IsNullOrWhiteSpace(item.Title))
{
<h3 class="h5 mb-0">@item.Title</h3>
}
@if (!string.IsNullOrWhiteSpace(item.Badge))
{
<span class="@GetBadgeClass(item)">@item.Badge</span>
}
</div>
}
@if (!string.IsNullOrWhiteSpace(item.Description))
{
<p class="mb-3">@item.Description</p>
}
@if (!string.IsNullOrWhiteSpace(item.ImageUrl))
{
<img src="@item.ImageUrl" alt="@item.ImageAlt" class="img-fluid rounded mb-3" loading="lazy" />
}
@item.ChildContent
@if (item.Actions?.Any() == true)
{
<div class="d-flex flex-wrap gap-2 mt-3" role="group" aria-label="Actions for @item.Title">
@foreach (var action in item.Actions)
{
<button type="button" class="@GetActionClass(action)" disabled="@action.Disabled"
aria-label="@(action.AriaLabel ?? action.Text)" @onclick="() => InvokeActionAsync(action, item)">
@if (!string.IsNullOrWhiteSpace(action.IconCssClass))
{
<i class="@action.IconCssClass" aria-hidden="true"></i>
}
@if (!string.IsNullOrWhiteSpace(action.Text))
{
<span class="@(string.IsNullOrWhiteSpace(action.IconCssClass) ? null : "ms-1")">@action.Text</span>
}
</button>
}
</div>
}
</div>
</article>
</li>
}
}
</ol>
49 changes: 49 additions & 0 deletions blazorbootstrap/Components/Timeline/Timeline.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace BlazorBootstrap;

/// <summary>
/// Displays a responsive chronological sequence of events.
/// </summary>
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);

/// <summary>Gets or sets the accessible name of the timeline.</summary>
[Parameter]
public string AriaLabel { get; set; } = "Timeline";

/// <summary>Gets or sets the alignment of a vertical timeline.</summary>
[Parameter]
public TimelineAlignment Alignment { get; set; } = TimelineAlignment.Left;

/// <summary>Gets or sets whether vertical items alternate around a centered rail on larger screens.</summary>
[Parameter]
public bool AlternateItems { get; set; }

/// <summary>Gets or sets the events to display.</summary>
[Parameter]
public IEnumerable<TimelineItem>? Items { get; set; }

/// <summary>Gets or sets the timeline orientation.</summary>
[Parameter]
public TimelineOrientation Orientation { get; set; } = TimelineOrientation.Vertical;
}
Loading