@@ -65,5 +84,18 @@
BackgroundColor="#8fb347">
+
+
+
+
\ No newline at end of file
diff --git a/src/Blashing.Shared/_Imports.razor b/src/Blashing.Shared/_Imports.razor
index 584fc30..8b155df 100644
--- a/src/Blashing.Shared/_Imports.razor
+++ b/src/Blashing.Shared/_Imports.razor
@@ -19,3 +19,4 @@
@using Blashing.Widgets;
@using Blashing.Widgets.ServerStatusSquares;
@using Blashing.Widgets.CircleCIBuildStatus;
+@using Blashing.Widgets.Timeline;
diff --git a/src/Blashing.Stories/Stories/TimelineWidget.stories.razor b/src/Blashing.Stories/Stories/TimelineWidget.stories.razor
new file mode 100644
index 0000000..b8a0049
--- /dev/null
+++ b/src/Blashing.Stories/Stories/TimelineWidget.stories.razor
@@ -0,0 +1,27 @@
+@using Blashing.Widgets.Timeline
+
+@attribute [Stories("Example/Timeline")]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Blashing.Widgets.Tests/TimelineWidgetTest.cs b/src/Blashing.Widgets.Tests/TimelineWidgetTest.cs
new file mode 100644
index 0000000..2fdc973
--- /dev/null
+++ b/src/Blashing.Widgets.Tests/TimelineWidgetTest.cs
@@ -0,0 +1,64 @@
+using Bunit;
+using Xunit;
+
+using Blashing.Widgets.Timeline;
+
+namespace Blashing.Widgets.Tests;
+
+public class TimelineWidgetTest : BunitContext
+{
+ [Fact]
+ public void TimelineWidgetMarkupShouldContainPassedInValues()
+ {
+ var title = "Timeline Title";
+ var moreInfo = "More Info";
+ var updatedAtMessage = "Updated At Message";
+ var items = new List
()
+ {
+ new TimelineItem("10:00", "Event One"),
+ new TimelineItem("11:30", "Event Two")
+ };
+
+ var cut = Render(parameters => parameters
+ .Add(p => p.Title, title)
+ .Add(p => p.MoreInfo, moreInfo)
+ .Add(p => p.UpdatedAtMessage, updatedAtMessage)
+ .Add(p => p.Items, items)
+ );
+
+ var expectedTitleMarkup = $"{title}
";
+ cut.FindAll("h1")[0].MarkupMatches(expectedTitleMarkup);
+
+ var expectedMoreInfoMarkup = $"{moreInfo}
";
+ cut.FindAll("p")[0].MarkupMatches(expectedMoreInfoMarkup);
+
+ var expectedUpdatedAtMessageMarkup = $"{updatedAtMessage}
";
+ cut.FindAll("p")[1].MarkupMatches(expectedUpdatedAtMessageMarkup);
+ }
+
+ [Fact]
+ public void TimelineWidgetShouldContainPassedInValues()
+ {
+ var title = "Timeline Title";
+ var moreInfo = "More Info";
+ var updatedAtMessage = "Updated At Message";
+ var items = new List()
+ {
+ new TimelineItem("10:00", "Event One"),
+ new TimelineItem("11:30", "Event Two")
+ };
+
+ var cut = Render(parameters => parameters
+ .Add(p => p.Title, title)
+ .Add(p => p.MoreInfo, moreInfo)
+ .Add(p => p.UpdatedAtMessage, updatedAtMessage)
+ .Add(p => p.Items, items)
+ );
+
+ var timelineWidget = cut.Instance;
+ Assert.Equal(timelineWidget.Title, title);
+ Assert.Equal(timelineWidget.MoreInfo, moreInfo);
+ Assert.Equal(timelineWidget.UpdatedAtMessage, updatedAtMessage);
+ Assert.Equal(timelineWidget.Items, items);
+ }
+}
diff --git a/src/Blashing.Widgets/Timeline/TimelineItem.cs b/src/Blashing.Widgets/Timeline/TimelineItem.cs
new file mode 100644
index 0000000..e1207c7
--- /dev/null
+++ b/src/Blashing.Widgets/Timeline/TimelineItem.cs
@@ -0,0 +1,3 @@
+namespace Blashing.Widgets.Timeline;
+
+public record TimelineItem(string Date, string Title, string? Color = null);
diff --git a/src/Blashing.Widgets/Timeline/TimelineWidget.razor b/src/Blashing.Widgets/Timeline/TimelineWidget.razor
new file mode 100644
index 0000000..606f7bb
--- /dev/null
+++ b/src/Blashing.Widgets/Timeline/TimelineWidget.razor
@@ -0,0 +1,19 @@
+@inherits Blashing.Core.Components.BaseWidget;
+
+
diff --git a/src/Blashing.Widgets/Timeline/TimelineWidget.razor.cs b/src/Blashing.Widgets/Timeline/TimelineWidget.razor.cs
new file mode 100644
index 0000000..898e381
--- /dev/null
+++ b/src/Blashing.Widgets/Timeline/TimelineWidget.razor.cs
@@ -0,0 +1,24 @@
+using Blashing.Core.Components;
+using Microsoft.AspNetCore.Components;
+
+namespace Blashing.Widgets.Timeline;
+
+public partial class TimelineWidget : BaseWidget
+{
+ [Parameter]
+ public string? Title { get; set; }
+
+ [Parameter]
+ public string? MoreInfo { get; set; }
+
+ [Parameter]
+ public string? UpdatedAtMessage { get; set; }
+
+ [Parameter]
+ public List? Items { get; set; } = new();
+
+ protected override void OnParametersSet()
+ {
+ BackgroundColor ??= "#4b4b4b";
+ }
+}
diff --git a/src/Blashing.Widgets/Timeline/TimelineWidget.razor.css b/src/Blashing.Widgets/Timeline/TimelineWidget.razor.css
new file mode 100644
index 0000000..747b028
--- /dev/null
+++ b/src/Blashing.Widgets/Timeline/TimelineWidget.razor.css
@@ -0,0 +1,75 @@
+.widget-timeline {
+ background-color: #4b4b4b;
+ padding-bottom: 70px;
+ overflow-y: auto;
+}
+
+.widget-timeline .title {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.widget-timeline .more-info {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.widget-timeline .updated-at {
+ color: rgba(0, 0, 0, 0.3);
+}
+
+.widget-timeline .timeline-list {
+ list-style: none;
+ padding: 10px 20px;
+ margin: 0;
+ position: relative;
+}
+
+/* vertical line through the centre of the dots */
+.widget-timeline .timeline-list::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ transform: translateX(-50%);
+ background: #E8F770;
+}
+
+.widget-timeline .timeline-event {
+ display: flex;
+ align-items: center;
+ margin-bottom: 18px;
+ position: relative;
+}
+
+/* left column: date */
+.widget-timeline .timeline-event .event-description-date {
+ flex: 1;
+ text-align: right;
+ padding-right: 14px;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+/* centre: dot */
+.widget-timeline .timeline-event .timeline-dot {
+ flex-shrink: 0;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #E8F770;
+ z-index: 1;
+ box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.5);
+}
+
+/* right column: title */
+.widget-timeline .timeline-event .event-description {
+ flex: 1;
+ padding-left: 14px;
+ color: white;
+ font-size: 14px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/src/Blashing.Widgets/Timeline/TimelineWidget.razor.scss b/src/Blashing.Widgets/Timeline/TimelineWidget.razor.scss
new file mode 100644
index 0000000..6f84833
--- /dev/null
+++ b/src/Blashing.Widgets/Timeline/TimelineWidget.razor.scss
@@ -0,0 +1,90 @@
+// ----------------------------------------------------------------------------
+// Sass declarations
+// ----------------------------------------------------------------------------
+$background-color: #4b4b4b;
+$timeline-color: #E8F770;
+$dot-color: #E8F770;
+
+$title-color: rgba(255, 255, 255, 0.7);
+$moreinfo-color: rgba(255, 255, 255, 0.7);
+
+// ----------------------------------------------------------------------------
+// Widget-timeline styles
+// ----------------------------------------------------------------------------
+.widget-timeline {
+
+ background-color: $background-color;
+ padding-bottom: 70px;
+ overflow-y: auto;
+
+ .title {
+ color: $title-color;
+ }
+
+ .more-info {
+ color: $moreinfo-color;
+ }
+
+ .updated-at {
+ color: rgba(0, 0, 0, 0.3);
+ }
+
+ .timeline-list {
+ list-style: none;
+ padding: 10px 20px;
+ margin: 0;
+ position: relative;
+
+ // vertical line through the centre of the dots
+ &::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ transform: translateX(-50%);
+ background: $timeline-color;
+ }
+ }
+
+ .timeline-event {
+ display: flex;
+ align-items: center;
+ margin-bottom: 18px;
+ position: relative;
+
+ // left column: date
+ .event-description-date {
+ flex: 1;
+ text-align: right;
+ padding-right: 14px;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 12px;
+ white-space: nowrap;
+ }
+
+ // centre: dot
+ .timeline-dot {
+ flex-shrink: 0;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: $dot-color;
+ z-index: 1;
+ box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.5);
+ }
+
+ // right column: title
+ .event-description {
+ flex: 1;
+ padding-left: 14px;
+ color: white;
+ font-size: 14px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+}
diff --git a/src/Blashing.Widgets/Timeline/_/timeline.coffee b/src/Blashing.Widgets/Timeline/_/timeline.coffee
new file mode 100644
index 0000000..9b2af42
--- /dev/null
+++ b/src/Blashing.Widgets/Timeline/_/timeline.coffee
@@ -0,0 +1,26 @@
+class Dashing.Timeline extends Dashing.Widget
+
+ ready: ->
+ @renderTimeline(@get('events'))
+
+ onData: (data) ->
+ # Handle incoming data
+ # You can access the html node of this widget with `@node` E8F770 616161
+ # Example: $(@node).fadeOut().fadeIn() will make the node flash each time data comes in.
+ if data.events
+ @renderTimeline(data.events)
+
+ renderTimeline: (events) ->
+ # Margins: zero if not set or the same as the opposite margin
+ # (you likely want this to keep the chart centered within the widget)
+ left = @get('leftMargin') || 0
+ right = @get('rightMargin') || left
+ top = @get('topMargin') || 0
+ bottom = @get('bottomMargin') || top
+
+ container = $(@node).parent()
+ # Gross hacks. Let's fix this.
+ width = (Dashing.widget_base_dimensions[0] * container.data("sizex")) + Dashing.widget_margins[0] * 2 * (container.data("sizex") - 1) - left - right
+ height = (Dashing.widget_base_dimensions[1] * container.data("sizey")) - ($(@node).find("h1").outerHeight() + 12) - top - bottom
+ id = "." + @get('id')
+ TimeKnots.draw(id, events, {horizontalLayout: false, color: "#222222", height: height, width: width, showLabels: true, labelFormat:"%H:%M"});
diff --git a/src/Blashing.Widgets/Timeline/_/timeline.html b/src/Blashing.Widgets/Timeline/_/timeline.html
new file mode 100644
index 0000000..4852ca5
--- /dev/null
+++ b/src/Blashing.Widgets/Timeline/_/timeline.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/Blashing.Widgets/Timeline/_/timeline.scss b/src/Blashing.Widgets/Timeline/_/timeline.scss
new file mode 100644
index 0000000..669c4ba
--- /dev/null
+++ b/src/Blashing.Widgets/Timeline/_/timeline.scss
@@ -0,0 +1,34 @@
+// ----------------------------------------------------------------------------
+// Sass declarations
+// ----------------------------------------------------------------------------
+$background-color: #4b4b4b;
+
+$title-color: rgba(255, 255, 255, 0.7);
+$moreinfo-color: rgba(255, 255, 255, 0.7);
+
+// ----------------------------------------------------------------------------
+// Widget-text styles
+// ----------------------------------------------------------------------------
+.widget-timeline {
+
+ background-color: $background-color;
+ padding-bottom: 70px;
+
+ .title {
+ color: $title-color;
+ }
+
+ .more-info {
+ color: $moreinfo-color;
+ }
+
+ .event-description {
+ fill: white;
+ font-size: 14px;
+ }
+
+ .event-description-date {
+ fill: white;
+ font-size: 12px;
+ }
+}