Fix GUI polish issues across auth modal, theme, and 404

- Add missing modal CSS (.modal-overlay/.modal-dialog/.modal-header):
  the "Connect to Microsoft" auth modal was rendering unstyled inline
  at the bottom of the page. Now a centered dialog with backdrop.
- Surface OAuth connect errors in the modal instead of silently
  reopening it with no explanation.
- MainLayout: implement IDisposable so event handlers are actually
  unsubscribed (Dispose existed but was never invoked).
- Wire up the Settings theme selector (was a dead control): drop the
  unsupported Dark option, call sptb.setTheme on save and on load,
  resolve System via prefers-color-scheme.
- Add branded 404 page via UseStatusCodePagesWithReExecute + Routes
  <NotFound> (blank white page before).
- Add .progress-fill.indeterminate animation and .progress-panel.
- Home: replace inline JS hover handlers with a .feature-card CSS class.
- Define missing --surface-2 variable referenced by MainLayout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 11:16:01 +02:00
parent d19092c84e
commit 5a23783e07
9 changed files with 101 additions and 14 deletions
+17 -1
View File
@@ -1,12 +1,15 @@
@inherits LayoutComponentBase
@implements IDisposable
@inject IUserSessionService Session
@inject IUserContextAccessor UserContext
@inject ISessionCredentialStore CredStore
@inject ISessionManager SessionManager
@inject NavigationManager Nav
@inject IJSRuntime JS
@inject SharepointToolbox.Web.Services.OAuth.IOAuthFlowCache OAuthCache
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.WebUtilities
@using Microsoft.JSInterop
@using SharepointToolbox.Web.Core.Models
@using SharepointToolbox.Web.Services.Session
@@ -160,6 +163,9 @@
{
if (!firstRender) return;
// Apply persisted theme preference
await ApplyThemeAsync();
// Pick up token_key from OAuth callback redirect before checking credential state
await HandleOAuthCallbackAsync();
await RefreshCredentialState();
@@ -172,7 +178,8 @@
Nav.NavigateTo(uri.GetLeftPart(UriPartial.Path), replace: true);
if (_credModal is not null)
{
await _credModal.ShowAsync();
// Surface the failure reason instead of silently reopening the modal
await _credModal.ShowAsync(err!);
}
}
@@ -234,6 +241,15 @@
});
}
private async Task ApplyThemeAsync()
{
try
{
await JS.InvokeVoidAsync("sptb.setTheme", Session.Settings.Theme);
}
catch (JSException) { /* best-effort; JS not yet available */ }
}
private void ToggleSidebar() => _sidebarCollapsed = !_sidebarCollapsed;
private static string RoleChipClass(UserRole role) => role switch
+2 -2
View File
@@ -29,8 +29,8 @@ else
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-top:16px">
@foreach (var feature in _features)
{
<a href="@feature.Href" style="text-decoration:none">
<div class="card" style="cursor:pointer;transition:box-shadow .15s" onmouseover="this.style.boxShadow='0 2px 8px rgba(0,120,212,.2)'" onmouseout="this.style.boxShadow=''">
<a href="@feature.Href" style="text-decoration:none;color:inherit">
<div class="card feature-card">
<div style="font-size:28px;margin-bottom:8px">@feature.Icon</div>
<div style="font-weight:600;margin-bottom:4px">@feature.Title</div>
<div class="text-muted">@feature.Description</div>
+10
View File
@@ -0,0 +1,10 @@
@page "/not-found"
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
<PageTitle>Page not found — SharePoint Toolbox</PageTitle>
<div class="no-profile">
<h2>Page not found</h2>
<p>The page you requested doesn't exist or has moved.</p>
<a href="/" class="btn btn-primary">Back to Home</a>
</div>
+5 -3
View File
@@ -1,7 +1,9 @@
@page "/settings"
@attribute [Authorize]
@inject IUserSessionService Session
@inject IJSRuntime JS
@rendermode InteractiveServer
@using Microsoft.JSInterop
<h1 class="page-title">Settings</h1>
@@ -19,7 +21,6 @@
<select class="form-select" style="width:160px" @bind="_theme" @bind:after="Save">
<option value="System">System</option>
<option value="Light">Light</option>
<option value="Dark">Dark</option>
</select>
</div>
</div>
@@ -44,14 +45,15 @@
{
var s = Session.Settings;
_lang = s.Lang;
_theme = s.Theme;
_theme = s.Theme is "System" or "Light" ? s.Theme : "System";
_autoTakeOwnership = s.AutoTakeOwnership;
}
private void Save()
private async Task Save()
{
Session.UpdateSettings(new AppSettings { Lang = _lang, Theme = _theme, AutoTakeOwnership = _autoTakeOwnership });
SharepointToolbox.Web.Localization.TranslationSource.Instance.SetCulture(_lang);
await JS.InvokeVoidAsync("sptb.setTheme", _theme);
_saved = true;
StateHasChanged();
_ = Task.Delay(2000).ContinueWith(_ => { _saved = false; InvokeAsync(StateHasChanged); });
+9
View File
@@ -18,5 +18,14 @@
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(Layout.MainLayout)">
<div class="no-profile">
<h2>Page not found</h2>
<p>The page you requested doesn't exist.</p>
<a href="/" class="btn btn-primary">Back to Home</a>
</div>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
@@ -7,10 +7,10 @@
@if (_visible)
{
<div class="modal-overlay">
<div class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="connect-modal-title">
<div class="modal-dialog">
<div class="modal-header">
<h3>Connect to Microsoft</h3>
<h3 id="connect-modal-title">Connect to Microsoft</h3>
<p class="text-muted">
Authenticate to access <strong>@Session.CurrentProfile?.Name</strong>.
Your session token is stored in your browser only — never saved to disk.
@@ -22,14 +22,14 @@
<div class="alert alert-error">@_error</div>
}
<div class="flex-row mt-8">
<div class="modal-footer">
<button class="btn btn-secondary" @onclick="Cancel" disabled="@_connecting">Cancel</button>
<button class="btn btn-primary" @onclick="ConnectAsync" disabled="@_connecting">
@(_connecting ? "Redirecting…" : "Connect via Microsoft")
</button>
<button class="btn btn-secondary" @onclick="Cancel" disabled="@_connecting">Cancel</button>
</div>
<p class="text-muted" style="font-size:11px;margin-top:8px">
<p class="text-muted" style="font-size:11px;margin-top:8px;text-align:right">
You will be redirected to Microsoft login. MFA is supported.
</p>
</div>
@@ -43,9 +43,9 @@
private bool _connecting;
private string _error = string.Empty;
public async Task ShowAsync()
public async Task ShowAsync(string? error = null)
{
_error = string.Empty;
_error = error ?? string.Empty;
_connecting = false;
_visible = true;
await InvokeAsync(StateHasChanged);