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:
@@ -1,12 +1,15 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@implements IDisposable
|
||||||
@inject IUserSessionService Session
|
@inject IUserSessionService Session
|
||||||
@inject IUserContextAccessor UserContext
|
@inject IUserContextAccessor UserContext
|
||||||
@inject ISessionCredentialStore CredStore
|
@inject ISessionCredentialStore CredStore
|
||||||
@inject ISessionManager SessionManager
|
@inject ISessionManager SessionManager
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
@inject IJSRuntime JS
|
||||||
@inject SharepointToolbox.Web.Services.OAuth.IOAuthFlowCache OAuthCache
|
@inject SharepointToolbox.Web.Services.OAuth.IOAuthFlowCache OAuthCache
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@using Microsoft.AspNetCore.WebUtilities
|
@using Microsoft.AspNetCore.WebUtilities
|
||||||
|
@using Microsoft.JSInterop
|
||||||
@using SharepointToolbox.Web.Core.Models
|
@using SharepointToolbox.Web.Core.Models
|
||||||
@using SharepointToolbox.Web.Services.Session
|
@using SharepointToolbox.Web.Services.Session
|
||||||
|
|
||||||
@@ -160,6 +163,9 @@
|
|||||||
{
|
{
|
||||||
if (!firstRender) return;
|
if (!firstRender) return;
|
||||||
|
|
||||||
|
// Apply persisted theme preference
|
||||||
|
await ApplyThemeAsync();
|
||||||
|
|
||||||
// Pick up token_key from OAuth callback redirect before checking credential state
|
// Pick up token_key from OAuth callback redirect before checking credential state
|
||||||
await HandleOAuthCallbackAsync();
|
await HandleOAuthCallbackAsync();
|
||||||
await RefreshCredentialState();
|
await RefreshCredentialState();
|
||||||
@@ -172,7 +178,8 @@
|
|||||||
Nav.NavigateTo(uri.GetLeftPart(UriPartial.Path), replace: true);
|
Nav.NavigateTo(uri.GetLeftPart(UriPartial.Path), replace: true);
|
||||||
if (_credModal is not null)
|
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 void ToggleSidebar() => _sidebarCollapsed = !_sidebarCollapsed;
|
||||||
|
|
||||||
private static string RoleChipClass(UserRole role) => role switch
|
private static string RoleChipClass(UserRole role) => role switch
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ else
|
|||||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-top:16px">
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-top:16px">
|
||||||
@foreach (var feature in _features)
|
@foreach (var feature in _features)
|
||||||
{
|
{
|
||||||
<a href="@feature.Href" style="text-decoration:none">
|
<a href="@feature.Href" style="text-decoration:none;color:inherit">
|
||||||
<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=''">
|
<div class="card feature-card">
|
||||||
<div style="font-size:28px;margin-bottom:8px">@feature.Icon</div>
|
<div style="font-size:28px;margin-bottom:8px">@feature.Icon</div>
|
||||||
<div style="font-weight:600;margin-bottom:4px">@feature.Title</div>
|
<div style="font-weight:600;margin-bottom:4px">@feature.Title</div>
|
||||||
<div class="text-muted">@feature.Description</div>
|
<div class="text-muted">@feature.Description</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
@page "/settings"
|
@page "/settings"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject IUserSessionService Session
|
@inject IUserSessionService Session
|
||||||
|
@inject IJSRuntime JS
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
|
||||||
<h1 class="page-title">Settings</h1>
|
<h1 class="page-title">Settings</h1>
|
||||||
|
|
||||||
@@ -19,7 +21,6 @@
|
|||||||
<select class="form-select" style="width:160px" @bind="_theme" @bind:after="Save">
|
<select class="form-select" style="width:160px" @bind="_theme" @bind:after="Save">
|
||||||
<option value="System">System</option>
|
<option value="System">System</option>
|
||||||
<option value="Light">Light</option>
|
<option value="Light">Light</option>
|
||||||
<option value="Dark">Dark</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,14 +45,15 @@
|
|||||||
{
|
{
|
||||||
var s = Session.Settings;
|
var s = Session.Settings;
|
||||||
_lang = s.Lang;
|
_lang = s.Lang;
|
||||||
_theme = s.Theme;
|
_theme = s.Theme is "System" or "Light" ? s.Theme : "System";
|
||||||
_autoTakeOwnership = s.AutoTakeOwnership;
|
_autoTakeOwnership = s.AutoTakeOwnership;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save()
|
private async Task Save()
|
||||||
{
|
{
|
||||||
Session.UpdateSettings(new AppSettings { Lang = _lang, Theme = _theme, AutoTakeOwnership = _autoTakeOwnership });
|
Session.UpdateSettings(new AppSettings { Lang = _lang, Theme = _theme, AutoTakeOwnership = _autoTakeOwnership });
|
||||||
SharepointToolbox.Web.Localization.TranslationSource.Instance.SetCulture(_lang);
|
SharepointToolbox.Web.Localization.TranslationSource.Instance.SetCulture(_lang);
|
||||||
|
await JS.InvokeVoidAsync("sptb.setTheme", _theme);
|
||||||
_saved = true;
|
_saved = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
_ = Task.Delay(2000).ContinueWith(_ => { _saved = false; InvokeAsync(StateHasChanged); });
|
_ = Task.Delay(2000).ContinueWith(_ => { _saved = false; InvokeAsync(StateHasChanged); });
|
||||||
|
|||||||
@@ -18,5 +18,14 @@
|
|||||||
</AuthorizeRouteView>
|
</AuthorizeRouteView>
|
||||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
</Found>
|
</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>
|
</Router>
|
||||||
</CascadingAuthenticationState>
|
</CascadingAuthenticationState>
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
@if (_visible)
|
@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-dialog">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>Connect to Microsoft</h3>
|
<h3 id="connect-modal-title">Connect to Microsoft</h3>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Authenticate to access <strong>@Session.CurrentProfile?.Name</strong>.
|
Authenticate to access <strong>@Session.CurrentProfile?.Name</strong>.
|
||||||
Your session token is stored in your browser only — never saved to disk.
|
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="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">
|
<button class="btn btn-primary" @onclick="ConnectAsync" disabled="@_connecting">
|
||||||
@(_connecting ? "Redirecting…" : "Connect via Microsoft")
|
@(_connecting ? "Redirecting…" : "Connect via Microsoft")
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" @onclick="Cancel" disabled="@_connecting">Cancel</button>
|
|
||||||
</div>
|
</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.
|
You will be redirected to Microsoft login. MFA is supported.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
private bool _connecting;
|
private bool _connecting;
|
||||||
private string _error = string.Empty;
|
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;
|
_connecting = false;
|
||||||
_visible = true;
|
_visible = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|||||||
@@ -175,6 +175,9 @@ if (!app.Environment.IsDevelopment())
|
|||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-execute unmatched (404) requests into the branded not-found page
|
||||||
|
app.UseStatusCodePagesWithReExecute("/not-found");
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
--warn: #797673;
|
--warn: #797673;
|
||||||
--text: #323130;
|
--text: #323130;
|
||||||
--text-muted: #605e5c;
|
--text-muted: #605e5c;
|
||||||
|
--surface-2: #2d2d4e;
|
||||||
--font: 'Segoe UI', system-ui, sans-serif;
|
--font: 'Segoe UI', system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,3 +161,44 @@ body {
|
|||||||
/* ── No-profile state ── */
|
/* ── No-profile state ── */
|
||||||
.no-profile { text-align: center; padding: 60px 20px; color: var(--text-muted); }
|
.no-profile { text-align: center; padding: 60px 20px; color: var(--text-muted); }
|
||||||
.no-profile h2 { color: var(--text); }
|
.no-profile h2 { color: var(--text); }
|
||||||
|
|
||||||
|
/* ── Modal ── */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed; inset: 0; z-index: 1000;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, .45);
|
||||||
|
padding: 20px;
|
||||||
|
animation: modal-fade .15s ease-out;
|
||||||
|
}
|
||||||
|
.modal-dialog {
|
||||||
|
background: var(--card-bg); border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
|
||||||
|
width: 100%; max-width: 440px; max-height: 90vh; overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
animation: modal-rise .15s ease-out;
|
||||||
|
}
|
||||||
|
.modal-header { margin-bottom: 16px; }
|
||||||
|
.modal-header h3 { margin: 0 0 6px 0; font-size: 18px; font-weight: 600; color: var(--text); }
|
||||||
|
.modal-header .text-muted { margin: 0; line-height: 1.4; }
|
||||||
|
.modal-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
|
||||||
|
@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
|
||||||
|
@keyframes modal-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||||
|
|
||||||
|
/* ── Progress panel ── */
|
||||||
|
.progress-panel { margin: 8px 0; }
|
||||||
|
.progress-fill.indeterminate {
|
||||||
|
width: 40%;
|
||||||
|
background: var(--accent);
|
||||||
|
animation: indeterminate 1.1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes indeterminate {
|
||||||
|
0% { margin-left: -40%; }
|
||||||
|
100% { margin-left: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Feature cards (Home) ── */
|
||||||
|
.feature-card { cursor: pointer; transition: box-shadow .15s, transform .15s; }
|
||||||
|
.feature-card:hover { box-shadow: 0 2px 8px rgba(0, 120, 212, .2); transform: translateY(-1px); }
|
||||||
|
|
||||||
|
/* ── Theme (light-only palette; System resolves via JS) ── */
|
||||||
|
[data-theme="light"] { color-scheme: light; }
|
||||||
|
|||||||
+6
-1
@@ -8,7 +8,12 @@ window.sptb = {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
},
|
},
|
||||||
setTheme: function(theme) {
|
setTheme: function(theme) {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
var t = (theme || 'System').toLowerCase();
|
||||||
|
if (t === 'system') {
|
||||||
|
t = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
},
|
},
|
||||||
scrollToBottom: function(el) {
|
scrollToBottom: function(el) {
|
||||||
if (el) el.scrollTop = el.scrollHeight;
|
if (el) el.scrollTop = el.scrollHeight;
|
||||||
|
|||||||
Reference in New Issue
Block a user