diff --git a/src/ASTRAIN.Client/Components/KebabMenu.razor b/src/ASTRAIN.Client/Components/KebabMenu.razor
new file mode 100644
index 0000000..e20c937
--- /dev/null
+++ b/src/ASTRAIN.Client/Components/KebabMenu.razor
@@ -0,0 +1,94 @@
+@implements IAsyncDisposable
+@inject IJSRuntime JS
+
+
+
+@code {
+ [Parameter] public string AriaLabel { get; set; } = "Menu";
+ [Parameter] public RenderFragment ChildContent { get; set; } = default!;
+
+ private bool _isOpen;
+ private ElementReference _menuRef;
+ private DotNetObjectReference? _dotNetRef;
+ private IJSObjectReference? _jsRef;
+ private readonly KebabMenuContext _context;
+
+ public KebabMenu()
+ {
+ _context = new KebabMenuContext(Close);
+ }
+
+ ///
+ /// Toggles the open state of the menu.
+ ///
+ private void Toggle()
+ {
+ _isOpen = !_isOpen;
+ }
+
+ ///
+ /// Closes the menu if it is open.
+ ///
+ [JSInvokable]
+ public void Close()
+ {
+ if (!_isOpen)
+ {
+ return;
+ }
+
+ _isOpen = false;
+ _ = InvokeAsync(StateHasChanged);
+ }
+
+ ///
+ /// Called after the component has been rendered. Registers the JavaScript event handler on first render.
+ ///
+ /// True if this is the first render; otherwise, false.
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (!firstRender)
+ {
+ return;
+ }
+
+ _dotNetRef = DotNetObjectReference.Create(this);
+ _jsRef = await JS.InvokeAsync("kebabMenu.register", _menuRef, _dotNetRef);
+ }
+
+ ///
+ /// Disposes the JavaScript object reference and event handler.
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (_jsRef is not null)
+ {
+ await _jsRef.InvokeVoidAsync("dispose");
+ await _jsRef.DisposeAsync();
+ }
+
+ _dotNetRef?.Dispose();
+ }
+
+ ///
+ /// Initializes a new instance of the KebabMenuContext class.
+ ///
+ /// The action to close the menu.
+ public sealed class KebabMenuContext
+ {
+ public KebabMenuContext(Action close)
+ {
+ Close = close;
+ }
+
+ public Action Close { get; }
+ }
+}
diff --git a/src/ASTRAIN.Client/_Imports.razor b/src/ASTRAIN.Client/_Imports.razor
index 16ace81..b2a7307 100644
--- a/src/ASTRAIN.Client/_Imports.razor
+++ b/src/ASTRAIN.Client/_Imports.razor
@@ -7,6 +7,7 @@
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using ASTRAIN.Client
+@using ASTRAIN.Client.Components
@using ASTRAIN.Client.Layout
@using ASTRAIN.Client.Services
@using ASTRAIN.Shared.Dtos
diff --git a/src/ASTRAIN.Client/wwwroot/css/app.css b/src/ASTRAIN.Client/wwwroot/css/app.css
index aba190b..a402999 100644
--- a/src/ASTRAIN.Client/wwwroot/css/app.css
+++ b/src/ASTRAIN.Client/wwwroot/css/app.css
@@ -154,6 +154,74 @@ p {
flex-wrap: wrap;
}
+.start-button {
+ width: 44px;
+ height: 44px;
+ padding: 0;
+ border-radius: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+}
+
+.kebab-menu {
+ position: relative;
+}
+
+.kebab-button {
+ list-style: none;
+ background: transparent;
+ color: #ffffff;
+ border: 1px solid #3a3a3a;
+ border-radius: 12px;
+ width: 44px;
+ height: 44px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 1.4rem;
+}
+
+.kebab-button::-webkit-details-marker {
+ display: none;
+}
+
+.kebab-menu__items {
+ position: absolute;
+ right: 0;
+ top: calc(100% + 0.4rem);
+ min-width: 140px;
+ background: #151515;
+ border: 1px solid #2a2a2a;
+ border-radius: 12px;
+ padding: 0.35rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ z-index: 10;
+}
+
+.kebab-menu__item {
+ background: transparent;
+ color: #ffffff;
+ border: none;
+ border-radius: 10px;
+ text-align: left;
+ padding: 0.5rem 0.75rem;
+ cursor: pointer;
+ font-size: 0.95rem;
+}
+
+.kebab-menu__item:hover {
+ background: #222222;
+}
+
+.kebab-menu__item.danger {
+ color: #ff6b6b;
+}
+
.checkbox-row {
display: flex;
gap: 0.5rem;
@@ -216,6 +284,8 @@ p {
.main-content {
flex: 1;
padding: 1.5rem 1.25rem 6rem;
+ width: 100%;
+ margin: 0 auto;
}
.brand-header {
@@ -305,7 +375,7 @@ p {
@media (min-width: 768px) {
.main-content {
padding: 2rem 2.5rem 7rem;
- max-width: 960px;
+ max-width: 720px;
margin: 0 auto;
}
diff --git a/src/ASTRAIN.Client/wwwroot/index.html b/src/ASTRAIN.Client/wwwroot/index.html
index 41ccd75..3568de7 100644
--- a/src/ASTRAIN.Client/wwwroot/index.html
+++ b/src/ASTRAIN.Client/wwwroot/index.html
@@ -30,6 +30,7 @@
Reload
🗙
+