-
+
diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs
index ad2eaff..7d1b324 100644
--- a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs
+++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Components;
using Launcher.Services;
using CommunityToolkit.Maui.Storage;
+using SharpEngine.Shared.Dto;
namespace Launcher.UI.Components.Dialogs
{
diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css
new file mode 100644
index 0000000..a76d394
--- /dev/null
+++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css
@@ -0,0 +1,9 @@
+.field-path-container {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.field-name-container {
+ margin-bottom: 8px;
+}
diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css
new file mode 100644
index 0000000..0ee1ee3
--- /dev/null
+++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css
@@ -0,0 +1 @@
+.field-path-container{display:flex;align-items:center;gap:8px;}.field-name-container{margin-bottom:8px;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss
new file mode 100644
index 0000000..6457dd0
--- /dev/null
+++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss
@@ -0,0 +1,9 @@
+.field-path-container {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.field-name-container {
+ margin-bottom: 8px;
+}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Dialogs/Notification.razor b/Launcher/UI/Components/Dialogs/Notification.razor
deleted file mode 100644
index 8a40e57..0000000
--- a/Launcher/UI/Components/Dialogs/Notification.razor
+++ /dev/null
@@ -1,29 +0,0 @@
-@using Launcher.Services
-@inherits DialogBase
-
-@inject INotificationService NotificationService
-
-
-
- @foreach (var notification in NotificationService.Notifications)
- {
-
- @notification.Key @(notification.Value > 1 ? $"({notification.Value})" : "")
-
-
- }
-
-
-
-@code {
- ///
Gets or sets the type of the notification.
- [Parameter]
- public NotificationType Type { get; set; } = NotificationType.Warning;
-
- ///
- protected override void OnInitialized()
- {
- NotificationService.SetInvokeAsync(InvokeAsync);
- NotificationService.SetOnNotificationsChanged(StateHasChanged);
- }
-}
diff --git a/Launcher/UI/Components/DiscardIcon.razor.css b/Launcher/UI/Components/DiscardIcon.razor.css
deleted file mode 100644
index 301f29c..0000000
--- a/Launcher/UI/Components/DiscardIcon.razor.css
+++ /dev/null
@@ -1,6 +0,0 @@
-span {
- cursor: pointer;
-}
-span:hover {
- filter: brightness(0.8);
-}
diff --git a/Launcher/UI/Components/DiscardIcon.razor.min.css b/Launcher/UI/Components/DiscardIcon.razor.min.css
deleted file mode 100644
index 6048a35..0000000
--- a/Launcher/UI/Components/DiscardIcon.razor.min.css
+++ /dev/null
@@ -1 +0,0 @@
-span{cursor:pointer;}span:hover{filter:brightness(.8);}
\ No newline at end of file
diff --git a/Launcher/UI/Components/DiscardIcon.razor.scss b/Launcher/UI/Components/DiscardIcon.razor.scss
deleted file mode 100644
index f5cb62a..0000000
--- a/Launcher/UI/Components/DiscardIcon.razor.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-span {
- cursor: pointer;
-
- &:hover {
- filter: brightness(0.8);
- }
-}
diff --git a/Launcher/UI/Components/Flyouts/Flyout.razor b/Launcher/UI/Components/Flyouts/Flyout.razor
new file mode 100644
index 0000000..5fca179
--- /dev/null
+++ b/Launcher/UI/Components/Flyouts/Flyout.razor
@@ -0,0 +1,56 @@
+@using Launcher.Enums
+
+
+@code {
+ ///
Gets or sets the header content.
+ [Parameter]
+ public RenderFragment Header { get; set; }
+
+ ///
Gets or sets the main content.
+ [Parameter]
+ public RenderFragment Content { get; set; }
+
+ ///
Gets or sets the footer content.
+ [Parameter]
+ public RenderFragment Footer { get; set; }
+
+ ///
Gets or sets the title.
+ [Parameter]
+ public string Title { get; set; } = default!;
+
+ ///
Gets or sets whether the flyout is visible.
+ [Parameter]
+ public bool IsVisible { get; set; }
+
+ ///
Gets or sets the event executed when the visibility changes.
+ [Parameter]
+ public EventCallback
IsVisibleChanged { get; set; }
+
+ private string _isVisibleStyle => IsVisible ? "grid" : "none";
+
+ private async Task Close()
+ {
+ IsVisible = false;
+ await IsVisibleChanged.InvokeAsync(IsVisible);
+ }
+}
diff --git a/Launcher/UI/Components/Flyouts/Flyout.razor.css b/Launcher/UI/Components/Flyouts/Flyout.razor.css
new file mode 100644
index 0000000..82882fa
--- /dev/null
+++ b/Launcher/UI/Components/Flyouts/Flyout.razor.css
@@ -0,0 +1,38 @@
+.flyout {
+ display: none;
+ position: absolute;
+ top: 0;
+ width: 50%;
+ height: 100vh;
+ z-index: 9999;
+ background: white;
+ border-right: 1px solid black;
+ left: 50px;
+ grid-template-rows: 70px auto 70px;
+}
+
+.flyout-header {
+ display: flex;
+ justify-content: space-between;
+}
+.flyout-header button {
+ border: none;
+ background: none;
+}
+
+.flyout-content {
+ overflow-y: scroll;
+ padding-left: 1em;
+}
+
+.flyout-footer {
+ border-top: 1px solid black;
+ padding: 5px;
+ display: flex;
+ justify-content: end;
+ gap: 8px;
+}
+
+.flyout-footer, .flyout-header, .flyout-content {
+ padding: 1em;
+}
diff --git a/Launcher/UI/Components/Flyouts/Flyout.razor.min.css b/Launcher/UI/Components/Flyouts/Flyout.razor.min.css
new file mode 100644
index 0000000..f1bb45f
--- /dev/null
+++ b/Launcher/UI/Components/Flyouts/Flyout.razor.min.css
@@ -0,0 +1 @@
+.flyout{display:none;position:absolute;top:0;width:50%;height:100vh;z-index:9999;background:#fff;border-right:1px solid #000;left:50px;grid-template-rows:70px auto 70px;}.flyout-header{display:flex;justify-content:space-between;}.flyout-header button{border:0;background:none;}.flyout-content{overflow-y:scroll;padding-left:1em;}.flyout-footer{border-top:1px solid #000;padding:5px;display:flex;justify-content:end;gap:8px;}.flyout-footer,.flyout-header,.flyout-content{padding:1em;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Flyouts/Flyout.razor.scss b/Launcher/UI/Components/Flyouts/Flyout.razor.scss
new file mode 100644
index 0000000..decc8e0
--- /dev/null
+++ b/Launcher/UI/Components/Flyouts/Flyout.razor.scss
@@ -0,0 +1,39 @@
+.flyout {
+ display: none;
+ position: absolute;
+ top: 0;
+ width: 50%;
+ height: 100vh;
+ z-index: 9999;
+ background: white;
+ border-right: 1px solid black;
+ left: 50px;
+ grid-template-rows: 70px auto 70px;
+}
+
+.flyout-header {
+ display: flex;
+ justify-content: space-between;
+
+ button {
+ border: none;
+ background: none;
+ }
+}
+
+.flyout-content {
+ overflow-y: scroll;
+ padding-left: 1em;
+}
+
+.flyout-footer {
+ border-top: 1px solid black;
+ padding: 5px;
+ display: flex;
+ justify-content: end;
+ gap: 8px;
+}
+
+.flyout-footer, .flyout-header, .flyout-content {
+ padding: 1em;
+}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Flyouts/FlyoutBase.cs b/Launcher/UI/Components/Flyouts/FlyoutBase.cs
new file mode 100644
index 0000000..0d0d302
--- /dev/null
+++ b/Launcher/UI/Components/Flyouts/FlyoutBase.cs
@@ -0,0 +1,46 @@
+using Microsoft.AspNetCore.Components;
+
+namespace Launcher.UI.Components.Flyouts;
+
+///
+/// Represents a base class for flyout components.
+///
+public class FlyoutBase : ComponentBase
+{
+ /// Gets or sets whether the flyout is visible.
+ [Parameter]
+ public bool IsVisible { get; set; }
+
+ /// Gets or sets the event executed when the visibility changes.
+ [Parameter]
+ public EventCallback IsVisibleChanged { get; set; }
+
+ /// Gets or sets the event executed when the flyout is closed.
+ [Parameter]
+ public EventCallback OnClose { get; set; }
+
+ ///
+ /// Handles changes in visibility asynchronously and updates the visibility state.
+ ///
+ /// Indicates whether the element is currently visible or not.
+ ///
+ /// A representing an asynchronous operation.
+ ///
+ public virtual async Task OnIsVisibleChangedAsync(bool visibility)
+ {
+ IsVisible = visibility;
+ await IsVisibleChanged.InvokeAsync(visibility);
+ }
+
+ ///
+ /// Closes the flyout.
+ ///
+ ///
+ /// A representing an asynchronous operation.
+ ///
+ public virtual async Task Close()
+ {
+ await OnClose.InvokeAsync();
+ await OnIsVisibleChangedAsync(false);
+ }
+}
diff --git a/Launcher/UI/Components/Flyouts/SettingsFlyout.razor b/Launcher/UI/Components/Flyouts/SettingsFlyout.razor
new file mode 100644
index 0000000..e8c7930
--- /dev/null
+++ b/Launcher/UI/Components/Flyouts/SettingsFlyout.razor
@@ -0,0 +1,19 @@
+@inherits FlyoutBase
+
+
+
+
+
+@code {
+ [Inject]
+ private INotificationService _notificationService { get; init; } = default!;
+
+ private async Task SaveSettings()
+ {
+ _notificationService.Show("Settings saved.");
+ await Close();
+ }
+}
diff --git a/Launcher/UI/Components/Grid.razor b/Launcher/UI/Components/Grid.razor
deleted file mode 100644
index a65a5ef..0000000
--- a/Launcher/UI/Components/Grid.razor
+++ /dev/null
@@ -1,35 +0,0 @@
-@typeparam TItem
-
-
-
-
- | Name |
- Last Modified |
- Actions |
-
-
-
- @foreach (var item in Items)
- {
-
-
- @Row(item)
-
-
- }
-
-
-
-@code {
- ///
- /// Gets or sets the items to be rendered onto the grid.
- ///
- [Parameter]
- public List Items { get; set; }
-
- ///
- /// Gets or sets a template by which each row should be rendered.
- ///
- [Parameter]
- public RenderFragment Row { get; set; }
-}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Grid/Grid.razor b/Launcher/UI/Components/Grid/Grid.razor
new file mode 100644
index 0000000..2b23f84
--- /dev/null
+++ b/Launcher/UI/Components/Grid/Grid.razor
@@ -0,0 +1,84 @@
+@using System.ComponentModel
+@using SharpEngine.Shared.Attributes
+
+@typeparam TItem
+
+
+
+
+
+
+
+ @foreach (var item in Items)
+ {
+
+
+ @GridRowTemplate(item)
+
+
+ }
+
+
+
+
+@code {
+ ///
+ /// Gets or sets the items to be rendered onto the grid.
+ ///
+ [Parameter]
+ public List Items { get; set; } = [];
+
+ ///
+ /// Gets or sets a template by which each row should be rendered.
+ ///
+ [Parameter]
+ public RenderFragment GridRowTemplate { get; set; }
+
+ [Parameter]
+ public EventCallback> FilterChanged { get; set; }
+
+ private List _headers { get; set; } = [];
+ private Dictionary _filterModes { get; set; } = new();
+
+ ///
+ protected override Task OnInitializedAsync()
+ {
+ GetHeaders();
+ return base.OnInitializedAsync();
+ }
+
+ private void GetHeaders()
+ {
+ var props = typeof(TItem).GetProperties();
+ var headers = props.Select(prop =>
+ {
+ var displayNameAttribute = (DisplayNameAttribute?)prop.GetCustomAttributes(typeof(DisplayNameAttribute), false).FirstOrDefault();
+ var filterAttribute = (FilterAttribute?)prop.GetCustomAttributes(typeof(FilterAttribute), false).FirstOrDefault();
+
+ return new GridHeaderInfo(displayNameAttribute?.DisplayName ?? prop.Name, filterAttribute?.IsFilterable ?? true);
+ });
+
+ _headers = headers.ToList();
+ }
+
+ private async Task OnFilterChanged(FilterMode filterMode, string header)
+ {
+ if (_filterModes.TryGetValue(header, out var mode))
+ _filterModes[header] = filterMode;
+ else
+ _filterModes.Add(header, filterMode);
+
+ await FilterChanged.InvokeAsync(_filterModes);
+ }
+}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Grid/Grid.razor.css b/Launcher/UI/Components/Grid/Grid.razor.css
new file mode 100644
index 0000000..96e0633
--- /dev/null
+++ b/Launcher/UI/Components/Grid/Grid.razor.css
@@ -0,0 +1,19 @@
+.auto-width-table {
+ table-layout: auto;
+ width: 100%;
+}
+
+.auto-width-table th, .auto-width-table td {
+ white-space: nowrap;
+ padding: 8px;
+ text-align: left;
+}
+
+.table-header {
+ border-bottom: 1px solid black;
+}
+
+.table-content {
+ overflow-y: scroll;
+ height: 250px;
+}
diff --git a/Launcher/UI/Components/Grid/Grid.razor.min.css b/Launcher/UI/Components/Grid/Grid.razor.min.css
new file mode 100644
index 0000000..74f740a
--- /dev/null
+++ b/Launcher/UI/Components/Grid/Grid.razor.min.css
@@ -0,0 +1 @@
+.auto-width-table{table-layout:auto;width:100%;}.auto-width-table th,.auto-width-table td{white-space:nowrap;padding:8px;text-align:left;}.table-header{border-bottom:1px solid #000;}.table-content{overflow-y:scroll;height:250px;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Grid/Grid.razor.scss b/Launcher/UI/Components/Grid/Grid.razor.scss
new file mode 100644
index 0000000..daf24c1
--- /dev/null
+++ b/Launcher/UI/Components/Grid/Grid.razor.scss
@@ -0,0 +1,19 @@
+.auto-width-table {
+ table-layout: auto;
+ width: 100%;
+}
+
+.auto-width-table th, .auto-width-table td {
+ white-space: nowrap;
+ padding: 8px;
+ text-align: left;
+}
+
+.table-header {
+ border-bottom: 1px solid black;
+}
+
+.table-content {
+ overflow-y: scroll;
+ height: 250px;
+}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Grid/GridHeader.cs b/Launcher/UI/Components/Grid/GridHeader.cs
new file mode 100644
index 0000000..91e07ec
--- /dev/null
+++ b/Launcher/UI/Components/Grid/GridHeader.cs
@@ -0,0 +1,24 @@
+namespace Launcher.UI.Components.Grid;
+
+///
+/// Contains information about a grid header.
+///
+public class GridHeaderInfo
+{
+ /// Gets or sets the title of the column.
+ public string Title { get; set; }
+
+ /// Gets or sets whether the column allows filtering.
+ public bool AllowFiltering { get; set; }
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// Specifies the header title for the grid.
+ /// Indicates whether filtering is permitted on the grid header.
+ public GridHeaderInfo(string title, bool allowFiltering)
+ {
+ Title = title;
+ AllowFiltering = allowFiltering;
+ }
+}
diff --git a/Launcher/UI/Components/Grid/GridHeader.razor b/Launcher/UI/Components/Grid/GridHeader.razor
new file mode 100644
index 0000000..e5a8481
--- /dev/null
+++ b/Launcher/UI/Components/Grid/GridHeader.razor
@@ -0,0 +1,47 @@
+
+
+ |
+
+@code {
+ /// Gets or sets the contents of the grid row header.
+ [Parameter]
+ public string Title { get; set; } = default!;
+
+ /// Gets or sets whether filtering is allowed for the header.
+ [Parameter]
+ public bool AllowFiltering { get; set; }
+
+ /// Gets or sets the event executed when the filter mode changes.
+ [Parameter]
+ public EventCallback FilterChanged { get; set; }
+
+ /// Gets or sets the filter mode of the header.
+ [Parameter]
+ public FilterMode FilterMode { get; set; } = FilterMode.None;
+
+ private string _filterClass => AllowFiltering ? "filter-btn" : "filter-disabled";
+
+ private SvgIcon _icon => this.FilterMode switch
+ {
+ FilterMode.Ascending => SvgIcon.ChevronUp,
+ FilterMode.Descending => SvgIcon.ChevronDown,
+ _ => SvgIcon.None
+ };
+
+ private async Task OnFilterClicked()
+ {
+ FilterMode = FilterMode switch
+ {
+ FilterMode.None => FilterMode.Ascending,
+ FilterMode.Ascending => FilterMode.Descending,
+ FilterMode.Descending => FilterMode.None,
+ _ => FilterMode.None
+ };
+
+ if (AllowFiltering)
+ await FilterChanged.InvokeAsync(FilterMode);
+ }
+}
diff --git a/Launcher/UI/Components/Grid/GridHeader.razor.css b/Launcher/UI/Components/Grid/GridHeader.razor.css
new file mode 100644
index 0000000..642257e
--- /dev/null
+++ b/Launcher/UI/Components/Grid/GridHeader.razor.css
@@ -0,0 +1,15 @@
+.btn {
+ background: transparent;
+ border: none;
+ padding: 0;
+}
+
+.filter-btn {
+ display: flex;
+ align-items: center;
+}
+
+.filter-disabled {
+ cursor: none;
+ pointer-events: none;
+}
diff --git a/Launcher/UI/Components/Grid/GridHeader.razor.min.css b/Launcher/UI/Components/Grid/GridHeader.razor.min.css
new file mode 100644
index 0000000..b5f165e
--- /dev/null
+++ b/Launcher/UI/Components/Grid/GridHeader.razor.min.css
@@ -0,0 +1 @@
+.btn{background:transparent;border:0;padding:0;}.filter-btn{display:flex;align-items:center;}.filter-disabled{cursor:none;pointer-events:none;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Grid/GridHeader.razor.scss b/Launcher/UI/Components/Grid/GridHeader.razor.scss
new file mode 100644
index 0000000..e0f25a0
--- /dev/null
+++ b/Launcher/UI/Components/Grid/GridHeader.razor.scss
@@ -0,0 +1,15 @@
+.btn {
+ background: transparent;
+ border: none;
+ padding: 0;
+}
+
+.filter-btn {
+ display: flex;
+ align-items: center;
+}
+
+.filter-disabled {
+ cursor: none;
+ pointer-events: none;
+}
\ No newline at end of file
diff --git a/Launcher/UI/Components/GridRow.razor b/Launcher/UI/Components/Grid/GridRow.razor
similarity index 100%
rename from Launcher/UI/Components/GridRow.razor
rename to Launcher/UI/Components/Grid/GridRow.razor
diff --git a/Launcher/UI/Components/DiscardIcon.razor b/Launcher/UI/Components/Icons/DiscardIcon.razor
similarity index 54%
rename from Launcher/UI/Components/DiscardIcon.razor
rename to Launcher/UI/Components/Icons/DiscardIcon.razor
index 0b36549..ee38ca5 100644
--- a/Launcher/UI/Components/DiscardIcon.razor
+++ b/Launcher/UI/Components/Icons/DiscardIcon.razor
@@ -1,11 +1,10 @@
-✖
+
+
+
@code {
/// Gets or sets the event executed when the discard icon is clicked.
[Parameter]
public EventCallback DiscardClicked { get; set; }
-
- private async Task OnDiscardClicked()
- => await DiscardClicked.InvokeAsync();
}
diff --git a/Launcher/UI/Components/Icons/Icon.razor b/Launcher/UI/Components/Icons/Icon.razor
new file mode 100644
index 0000000..4c2b415
--- /dev/null
+++ b/Launcher/UI/Components/Icons/Icon.razor
@@ -0,0 +1,73 @@
+@using Launcher.Enums
+
+@switch (IconType)
+{
+ case SvgIcon.Home:
+
+ break;
+ case SvgIcon.Cog:
+
+ break;
+ case SvgIcon.Lens:
+
+ break;
+ case SvgIcon.Discard:
+ ✖
+ break;
+ case SvgIcon.Close:
+
+ break;
+ case SvgIcon.ChevronDown:
+
+ break;
+ case SvgIcon.ChevronUp:
+
+ break;
+ default:
+ break;
+}
+
+@code {
+
+ /// Gets or sets the type of the icon.
+ [Parameter]
+ public SvgIcon IconType { get; set; }
+
+ /// Gets or sets the size of the icon in pixels.
+ [Parameter]
+ public int SizePx { get; set; } = 16;
+}
diff --git a/Launcher/UI/Components/Layout.razor b/Launcher/UI/Components/Layout.razor
index 21adf40..507dd2d 100644
--- a/Launcher/UI/Components/Layout.razor
+++ b/Launcher/UI/Components/Layout.razor
@@ -1,5 +1,23 @@
@inherits LayoutComponentBase
-@Body
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+ @Body
+
+
+
+
+
+@code {
+ private bool _settingsVisible;
+
+ private void OpenSettingsFlyout()
+ => _settingsVisible = true;
+}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Layout.razor.css b/Launcher/UI/Components/Layout.razor.css
new file mode 100644
index 0000000..cb2bbbb
--- /dev/null
+++ b/Launcher/UI/Components/Layout.razor.css
@@ -0,0 +1,6 @@
+.content {
+ margin: 0 1em;
+ padding: 0;
+ display: grid;
+ grid-template-rows: 100px auto;
+}
diff --git a/Launcher/UI/Components/Layout.razor.min.css b/Launcher/UI/Components/Layout.razor.min.css
new file mode 100644
index 0000000..aee90ef
--- /dev/null
+++ b/Launcher/UI/Components/Layout.razor.min.css
@@ -0,0 +1 @@
+.content{margin:0 1em;padding:0;display:grid;grid-template-rows:100px auto;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Layout.razor.scss b/Launcher/UI/Components/Layout.razor.scss
new file mode 100644
index 0000000..6a00208
--- /dev/null
+++ b/Launcher/UI/Components/Layout.razor.scss
@@ -0,0 +1,6 @@
+.content {
+ margin: 0 1em;
+ padding: 0;
+ display: grid;
+ grid-template-rows: 100px auto;
+}
diff --git a/Launcher/UI/Components/Navigation/SideBarItem.razor b/Launcher/UI/Components/Navigation/SideBarItem.razor
new file mode 100644
index 0000000..fe8bc2b
--- /dev/null
+++ b/Launcher/UI/Components/Navigation/SideBarItem.razor
@@ -0,0 +1,14 @@
+
+
+@code {
+ ///
Gets or set the icon for the sidebar item.
+ [Parameter]
+ public SvgIcon Icon { get; set; }
+
+ ///
Gets or sets the event executed when the sidebar item is clicked.
+ [Parameter]
+ public EventCallback OnClick { get; set; }
+
+ private async Task OnItemClicked()
+ => await OnClick.InvokeAsync();
+}
diff --git a/Launcher/UI/Components/Navigation/SideBarItem.razor.css b/Launcher/UI/Components/Navigation/SideBarItem.razor.css
new file mode 100644
index 0000000..63ddf80
--- /dev/null
+++ b/Launcher/UI/Components/Navigation/SideBarItem.razor.css
@@ -0,0 +1,5 @@
+.sidebar-item {
+ cursor: pointer;
+ list-style: none;
+ padding: 0.5rem 1em;
+}
diff --git a/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css b/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css
new file mode 100644
index 0000000..38c8234
--- /dev/null
+++ b/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css
@@ -0,0 +1 @@
+.sidebar-item{cursor:pointer;list-style:none;padding:.5rem 1em;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Navigation/SideBarItem.razor.scss b/Launcher/UI/Components/Navigation/SideBarItem.razor.scss
new file mode 100644
index 0000000..73c8089
--- /dev/null
+++ b/Launcher/UI/Components/Navigation/SideBarItem.razor.scss
@@ -0,0 +1,5 @@
+.sidebar-item {
+ cursor: pointer;
+ list-style: none;
+ padding: 0.5rem 1em;
+}
diff --git a/Launcher/UI/Components/Navigation/Sidebar.razor b/Launcher/UI/Components/Navigation/Sidebar.razor
new file mode 100644
index 0000000..bc3ca2c
--- /dev/null
+++ b/Launcher/UI/Components/Navigation/Sidebar.razor
@@ -0,0 +1,11 @@
+
+
+@code {
+ ///
Gets or sets the sidebar items to be rendered within the sidebar.
+ [Parameter, EditorRequired]
+ public RenderFragment ChildContent { get; set; }
+}
diff --git a/Launcher/UI/Components/Navigation/Sidebar.razor.css b/Launcher/UI/Components/Navigation/Sidebar.razor.css
new file mode 100644
index 0000000..8d28b22
--- /dev/null
+++ b/Launcher/UI/Components/Navigation/Sidebar.razor.css
@@ -0,0 +1,6 @@
+.sidebar {
+ border-right: 1px solid black;
+}
+.sidebar ul {
+ padding: 0;
+}
diff --git a/Launcher/UI/Components/Navigation/Sidebar.razor.min.css b/Launcher/UI/Components/Navigation/Sidebar.razor.min.css
new file mode 100644
index 0000000..d701475
--- /dev/null
+++ b/Launcher/UI/Components/Navigation/Sidebar.razor.min.css
@@ -0,0 +1 @@
+.sidebar{border-right:1px solid #000;}.sidebar ul{padding:0;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Navigation/Sidebar.razor.scss b/Launcher/UI/Components/Navigation/Sidebar.razor.scss
new file mode 100644
index 0000000..6a71104
--- /dev/null
+++ b/Launcher/UI/Components/Navigation/Sidebar.razor.scss
@@ -0,0 +1,7 @@
+.sidebar {
+ border-right: 1px solid black;
+
+ ul {
+ padding: 0;
+ }
+}
diff --git a/Launcher/UI/Components/Notification/Notification.razor b/Launcher/UI/Components/Notification/Notification.razor
new file mode 100644
index 0000000..a46aee5
--- /dev/null
+++ b/Launcher/UI/Components/Notification/Notification.razor
@@ -0,0 +1,30 @@
+@inherits DialogBase
+
+@inject INotificationService NotificationService
+
+
+
+
+ @foreach (var notification in NotificationService.Notifications)
+ {
+
+ @notification.Key @(notification.Value > 1 ? $"({notification.Value})" : "")
+
+
+ }
+
+
+
+
+@code {
+ ///
Gets or sets the type of the notification.
+ [Parameter]
+ public NotificationType Type { get; set; } = NotificationType.Warning;
+
+ ///
+ protected override void OnInitialized()
+ {
+ NotificationService.SetInvokeAsync(InvokeAsync);
+ NotificationService.SetOnNotificationsChanged(StateHasChanged);
+ }
+}
diff --git a/Launcher/UI/Components/Notification/Notification.razor.css b/Launcher/UI/Components/Notification/Notification.razor.css
new file mode 100644
index 0000000..e12e310
--- /dev/null
+++ b/Launcher/UI/Components/Notification/Notification.razor.css
@@ -0,0 +1,18 @@
+.notification-container {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+}
+
+.notification {
+ position: relative;
+ display: flex;
+ top: 50px;
+ flex-direction: column;
+ align-items: end;
+}
+
+.notification-content {
+ pointer-events: auto;
+}
diff --git a/Launcher/UI/Components/Notification/Notification.razor.min.css b/Launcher/UI/Components/Notification/Notification.razor.min.css
new file mode 100644
index 0000000..c478afd
--- /dev/null
+++ b/Launcher/UI/Components/Notification/Notification.razor.min.css
@@ -0,0 +1 @@
+.notification-container{width:100%;height:100%;position:absolute;pointer-events:none;}.notification{position:relative;display:flex;top:50px;flex-direction:column;align-items:end;}.notification-content{pointer-events:auto;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Notification/Notification.razor.scss b/Launcher/UI/Components/Notification/Notification.razor.scss
new file mode 100644
index 0000000..f44c2d1
--- /dev/null
+++ b/Launcher/UI/Components/Notification/Notification.razor.scss
@@ -0,0 +1,18 @@
+.notification-container {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+}
+
+.notification {
+ position: relative;
+ display: flex;
+ top: 50px;
+ flex-direction: column;
+ align-items: end;
+}
+
+.notification-content {
+ pointer-events: auto;
+}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Notification/NotificationType.cs b/Launcher/UI/Components/Notification/NotificationType.cs
new file mode 100644
index 0000000..a5fd255
--- /dev/null
+++ b/Launcher/UI/Components/Notification/NotificationType.cs
@@ -0,0 +1,32 @@
+namespace Launcher.UI.Components.Notification
+{
+ ///
+ /// Represents the type of notification.
+ ///
+ public enum NotificationType
+ {
+ ///
Primary notification type.
+ Primary,
+
+ ///
Secondary notification type.
+ Secondary,
+
+ ///
Success notification type.
+ Success,
+
+ ///
Danger notification type.
+ Danger,
+
+ ///
Warning notification type.
+ Warning,
+
+ ///
Info notification type.
+ Info,
+
+ ///
Light notification type.
+ Light,
+
+ ///
Dark notification type.
+ Dark,
+ }
+}
diff --git a/Launcher/UI/Components/Dialogs/NotificationType.cs b/Launcher/UI/Components/Notification/NotificationTypeExtensions.cs
similarity index 59%
rename from Launcher/UI/Components/Dialogs/NotificationType.cs
rename to Launcher/UI/Components/Notification/NotificationTypeExtensions.cs
index 9f34c0a..7762cf4 100644
--- a/Launcher/UI/Components/Dialogs/NotificationType.cs
+++ b/Launcher/UI/Components/Notification/NotificationTypeExtensions.cs
@@ -1,35 +1,5 @@
-namespace Launcher.UI.Components
+namespace Launcher.UI.Components.Notification
{
- ///
- /// Represents the type of notification.
- ///
- public enum NotificationType
- {
- ///
Primary notification type.
- Primary,
-
- ///
Secondary notification type.
- Secondary,
-
- ///
Success notification type.
- Success,
-
- ///
Danger notification type.
- Danger,
-
- ///
Warning notification type.
- Warning,
-
- ///
Info notification type.
- Info,
-
- ///
Light notification type.
- Light,
-
- ///
Dark notification type.
- Dark,
- }
-
///
/// Extension methods for .
///
diff --git a/Launcher/UI/Components/Searchbox.razor b/Launcher/UI/Components/Searchbox.razor
new file mode 100644
index 0000000..2417a3e
--- /dev/null
+++ b/Launcher/UI/Components/Searchbox.razor
@@ -0,0 +1,35 @@
+
+
+
+
+
+@code {
+ ///
Gets or sets the value of the search box.
+ [Parameter]
+ public string Value { get; set; } = default!;
+
+ ///
Gets or sets the event executed when the value of the search box changes.
+ [Parameter]
+ public EventCallback
ValueChanged { get; set; }
+
+ /// Gets or sets the delay in milliseconds before the value is updated after the user stops typing.
+ [Parameter]
+ public int DebounceDelayMilliseconds { get; set; } = 1000;
+
+ /// Gets or sets any extra CSS classes to apply to the search box.
+ [Parameter]
+ public string? CssClass { get; set; }
+
+ private CancellationTokenSource _tokenSource { get; set; } = new();
+
+ private async Task OnChangeAsync(ChangeEventArgs e)
+ {
+ _tokenSource = new CancellationTokenSource();
+ var token = _tokenSource.Token;
+
+ await Task.Delay(DebounceDelayMilliseconds, token);
+
+ Value = e.Value?.ToString() ?? string.Empty;
+ await ValueChanged.InvokeAsync(Value);
+ }
+}
diff --git a/Launcher/UI/Components/Searchbox.razor.css b/Launcher/UI/Components/Searchbox.razor.css
new file mode 100644
index 0000000..9f2bc34
--- /dev/null
+++ b/Launcher/UI/Components/Searchbox.razor.css
@@ -0,0 +1,14 @@
+.searchbox {
+ border: 1px solid black;
+ width: 40%;
+ justify-self: end;
+}
+
+.searchbox-btn {
+ border: none;
+ background: none;
+}
+
+.searchbox-input {
+ border: none;
+}
diff --git a/Launcher/UI/Components/Searchbox.razor.min.css b/Launcher/UI/Components/Searchbox.razor.min.css
new file mode 100644
index 0000000..2641fc8
--- /dev/null
+++ b/Launcher/UI/Components/Searchbox.razor.min.css
@@ -0,0 +1 @@
+.searchbox{border:1px solid #000;width:40%;justify-self:end;}.searchbox-btn{border:0;background:none;}.searchbox-input{border:0;}
\ No newline at end of file
diff --git a/Launcher/UI/Components/Searchbox.razor.scss b/Launcher/UI/Components/Searchbox.razor.scss
new file mode 100644
index 0000000..4dd8607
--- /dev/null
+++ b/Launcher/UI/Components/Searchbox.razor.scss
@@ -0,0 +1,14 @@
+.searchbox {
+ border: 1px solid black;
+ width: 40%;
+ justify-self: end;
+}
+
+.searchbox-btn {
+ border: none;
+ background: none;
+}
+
+.searchbox-input {
+ border: none;
+}
\ No newline at end of file
diff --git a/Launcher/UI/Home.razor b/Launcher/UI/Home.razor
index 35fb873..0eae1e4 100644
--- a/Launcher/UI/Home.razor
+++ b/Launcher/UI/Home.razor
@@ -1,10 +1,5 @@
@page "/"
-@using Launcher.UI.Components
-@using Launcher.UI.Components.Dialogs
-@using System.Diagnostics
-
-
@@ -17,15 +12,30 @@