Move page back control into the topbar next to the menu.
build-and-deploy / deploy (push) Successful in 4m6s

Keep the chevron back button on the same sticky header row (left) as the menu icon (right).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
masoodafar-web
2026-10-10 04:10:22 +03:30
parent 8edb8048a7
commit 073e451891
5 changed files with 72 additions and 16 deletions
+11 -2
View File
@@ -1,14 +1,20 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@implements IDisposable @implements IDisposable
@inject AuthStateService Auth @inject AuthStateService Auth
@inject TopbarNavService TopbarNav
<div class="app-shell @(sidebarOpen ? "sidebar-open" : null) @(hasDock ? "has-player-dock" : null)"> <div class="app-shell @(sidebarOpen ? "sidebar-open" : null) @(hasDock ? "has-player-dock" : null)">
<header class="topbar"> <header class="topbar">
<div class="topbar-start">
@if (TopbarNav.ShowBack)
{
<button type="button" class="back-btn" @onclick="TopbarNav.GoBackAsync" aria-label="بازگشت">‹</button>
}
</div>
<a class="brand" href="/">تفسیر قرآن</a>
<button class="icon-btn" type="button" @onclick="ToggleSidebar" aria-label="منو"> <button class="icon-btn" type="button" @onclick="ToggleSidebar" aria-label="منو">
<span class="burger"></span> <span class="burger"></span>
</button> </button>
<a class="brand" href="/">تفسیر قرآن</a>
<div class="topbar-spacer"></div>
</header> </header>
<div class="backdrop" @onclick="CloseSidebar"></div> <div class="backdrop" @onclick="CloseSidebar"></div>
@@ -33,6 +39,7 @@
{ {
Auth.Changed += OnAuthChanged; Auth.Changed += OnAuthChanged;
Session.Changed += OnSessionChanged; Session.Changed += OnSessionChanged;
TopbarNav.Changed += OnTopbarChanged;
hasDock = Session.HasTrack; hasDock = Session.HasTrack;
} }
@@ -46,6 +53,7 @@
} }
private void OnAuthChanged() => InvokeAsync(StateHasChanged); private void OnAuthChanged() => InvokeAsync(StateHasChanged);
private void OnTopbarChanged() => InvokeAsync(StateHasChanged);
private void OnSessionChanged() private void OnSessionChanged()
{ {
hasDock = Session.HasTrack; hasDock = Session.HasTrack;
@@ -58,5 +66,6 @@
{ {
Auth.Changed -= OnAuthChanged; Auth.Changed -= OnAuthChanged;
Session.Changed -= OnSessionChanged; Session.Changed -= OnSessionChanged;
TopbarNav.Changed -= OnTopbarChanged;
} }
} }
+7 -2
View File
@@ -1,20 +1,21 @@
@page "/tafsir/{Slug}" @page "/tafsir/{Slug}"
@page "/tafsir/{Slug}/surah/{SurahNumber:int}" @page "/tafsir/{Slug}/surah/{SurahNumber:int}"
@page "/tafsir/{Slug}/surah/{SurahNumber:int}/ayah/{AyahNumber:int}" @page "/tafsir/{Slug}/surah/{SurahNumber:int}/ayah/{AyahNumber:int}"
@implements IDisposable
@inject TafsirApiClient Api @inject TafsirApiClient Api
@inject IDeviceIdStore DeviceStore @inject IDeviceIdStore DeviceStore
@inject NavigationManager Nav @inject NavigationManager Nav
@inject PlayerSessionService Player @inject PlayerSessionService Player
@inject TopbarNavService TopbarNav
<section class="tafsir-page"> <section class="tafsir-page">
<header class="page-header page-header-row"> <header class="page-header">
@if (loadingMeta) @if (loadingMeta)
{ {
<SkeletonBlock Variant="lines" Lines="1" /> <SkeletonBlock Variant="lines" Lines="1" />
} }
else else
{ {
<button type="button" class="back-btn" @onclick="GoBack" aria-label="بازگشت">‹</button>
<h1 class="page-title-inline"> <h1 class="page-title-inline">
<span>@title</span> <span>@title</span>
@if (!string.IsNullOrWhiteSpace(author)) @if (!string.IsNullOrWhiteSpace(author))
@@ -155,6 +156,8 @@
private string deviceId = ""; private string deviceId = "";
private bool ready; private bool ready;
protected override void OnInitialized() => TopbarNav.SetBack(GoBack);
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (!firstRender) return; if (!firstRender) return;
@@ -163,6 +166,8 @@
await LoadRouteAsync(); await LoadRouteAsync();
} }
public void Dispose() => TopbarNav.ClearBack();
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (!ready) return; if (!ready) return;
@@ -0,0 +1,36 @@
namespace TafsirQuran.UI.Services;
/// <summary>Lets pages put a back control in the shared topbar (aligned with the menu icon).</summary>
public sealed class TopbarNavService
{
private Func<Task>? _back;
public event Action? Changed;
public bool ShowBack => _back is not null;
public void SetBack(Action back) => SetBack(() =>
{
back();
return Task.CompletedTask;
});
public void SetBack(Func<Task> back)
{
_back = back;
Changed?.Invoke();
}
public void ClearBack()
{
if (_back is null) return;
_back = null;
Changed?.Invoke();
}
public async Task GoBackAsync()
{
if (_back is null) return;
await _back();
}
}
@@ -13,6 +13,7 @@ public static class UiServiceCollectionExtensions
services.AddScoped<IAuthTokenStore, BrowserAuthTokenStore>(); services.AddScoped<IAuthTokenStore, BrowserAuthTokenStore>();
services.AddScoped<AuthStateService>(); services.AddScoped<AuthStateService>();
services.AddScoped<PlayerSessionService>(); services.AddScoped<PlayerSessionService>();
services.AddScoped<TopbarNavService>();
services.AddScoped(sp => services.AddScoped(sp =>
{ {
var http = new HttpClient { BaseAddress = new Uri(normalized) }; var http = new HttpClient { BaseAddress = new Uri(normalized) };
+17 -12
View File
@@ -43,26 +43,39 @@ button, input { font: inherit; }
.topbar { .topbar {
position: sticky; top: 0; z-index: 20; position: sticky; top: 0; z-index: 20;
display: flex; align-items: center; gap: 0.75rem; display: grid;
grid-template-columns: 44px 1fr 44px;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 1rem; padding: 0.85rem 1rem;
backdrop-filter: blur(14px); backdrop-filter: blur(14px);
background: rgba(243, 239, 230, 0.82); background: rgba(243, 239, 230, 0.82);
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
direction: ltr; /* back left, menu right — same row */
}
.topbar-start {
width: 44px; height: 44px;
display: grid; place-items: center;
} }
.brand { .brand {
font-weight: 700; font-weight: 700;
font-size: 1.15rem; font-size: 1.15rem;
letter-spacing: -0.02em; letter-spacing: -0.02em;
text-align: center;
direction: rtl;
} }
.icon-btn { .icon-btn {
width: 42px; height: 42px; width: 44px; height: 44px;
border: 1px solid var(--line); border: 1px solid rgba(28, 43, 36, 0.14);
border-radius: 12px; border-radius: 12px;
background: var(--surface); background: #fff;
box-shadow: 0 2px 8px rgba(28, 43, 36, 0.08);
display: grid; place-items: center; display: grid; place-items: center;
cursor: pointer; cursor: pointer;
padding: 0;
} }
.burger, .burger::before, .burger::after { .burger, .burger::before, .burger::after {
@@ -281,14 +294,6 @@ button, input { font: inherit; }
} }
.chip:disabled { opacity: 0.45; cursor: default; } .chip:disabled { opacity: 0.45; cursor: default; }
.page-header-row {
display: grid;
grid-template-columns: 44px 1fr;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
direction: ltr; /* back button physical left, like TOC */
}
.page-title-inline { .page-title-inline {
margin: 0; font-size: clamp(1.15rem, 3.5vw, 1.45rem); font-weight: 700; margin: 0; font-size: clamp(1.15rem, 3.5vw, 1.45rem); font-weight: 700;
display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem;