style: complete Light Sepia theme overrides for user dashboard (#78)
Resolves #79 This Pull Request completes the visual implementation of the premium **"Warm Paper / Soft Sepia"** light theme across all user dashboard sub-modules and components. ### 🎨 Styling Refactoring & Light Sepia Layout We refactored isolated component styles (`.razor.css`) to ensure proper contrast, remove hardcoded dark tokens, and fix text visibility under the `.theme-light` environment: 1. **Dashboard & Sidebar Layouts:** * **MainHubLayout.razor.css** & **Dashboard.razor.css**: Replaced hardcoded dark backgrounds with `var(--bg-surface)` and `var(--bg-base)`. Overrode user name brackets, progress bar elements, active quiz cards, graph nodes, and buttons. 2. **Catalog & Library Pages:** * **Catalog.razor.css** & **MyBooks.razor.css**: Adjusted cover hover actions, overlay transparency, and progress tracks. Fixed course tile background gradients to use a warm, elegant `#e4e1d9` layer and `var(--text-main)` code text. 3. **Profile & Settings Views:** * **Profile.razor.css** & **Settings.razor.css**: Overrode token usage progress tracks, page title gradient transparency, section descriptions, and diagnostic button styling. 4. **Concepts Dashboard & Interactive Widgets:** * **ConceptsDashboard.razor.css** & **ConceptsMap.razor.css**: Transitioned node headers, unlocked/locked badges, warning blocks, and term pills to semantic colors. Removed neon glow animations for locked nodes. 5. **Intelligence Workspace & AI Responses:** * **Intelligence.razor.css** & **AiResponseRenderer.razor.css**: Refactored empty state welcome messages, placeholder styles, input fields, and robot avatar borders. Refined the linear-gradient mask fade effect to blend correctly into the light sepia surface environment rather than dropping into dark transparent channels. 6. **Dashboard Sidebar Widgets:** * **CurrentReadingWidget.razor.css** & **ContextualRecommendationsWidget.razor.css**: Replaced hardcoded `#1a1a1e` card containers and light text colors with semantic variables (`var(--bg-surface)`, `var(--border)`, `var(--text-main)`, and `var(--text-muted)`). ### 🛠️ Blazor CSS Isolation Compiler Compliance * Avoided the use of `:global(.theme-light)` selector overrides within isolated CSS rules because they are unsupported by Blazor's CSS isolation compiler and cause the compiled stylesheet to ignore them. * Replaced them with the correct standard selector format `.theme-light .some-class` which properly compiles to `.theme-light .some-class[b-xxxx]`, applying correct theme styles recursively to child scoped markup. ### 🧪 Verification * Checked that the solution builds cleanly with 0 compiler errors: `dotnet build NexusReader.slnx --no-restore` * Ran all unit tests successfully: `dotnet test NexusReader.slnx --no-restore` --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #78 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #78.
This commit is contained in:
@@ -11,13 +11,28 @@
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isLight = savedTheme === 'light' || (!savedTheme && !systemPrefersDark);
|
||||
if (isLight) {
|
||||
document.documentElement.classList.add('theme-light');
|
||||
} else {
|
||||
document.documentElement.classList.remove('theme-light');
|
||||
try {
|
||||
var themeMode = localStorage.getItem('theme-mode');
|
||||
var savedTheme = localStorage.getItem('theme');
|
||||
var isLight = false;
|
||||
|
||||
if (themeMode === '2' || savedTheme === 'light') {
|
||||
isLight = true;
|
||||
} else if (themeMode === '1' || savedTheme === 'dark') {
|
||||
isLight = false;
|
||||
} else {
|
||||
isLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
}
|
||||
|
||||
if (isLight) {
|
||||
document.documentElement.classList.add('theme-light');
|
||||
document.documentElement.classList.remove('theme-dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('theme-dark');
|
||||
document.documentElement.classList.remove('theme-light');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail silently
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -52,7 +52,9 @@ builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddScoped<IPlatformService, WebPlatformService>();
|
||||
builder.Services.AddScoped<INativeStorageService, NexusReader.Web.Services.NativeStorageService>();
|
||||
builder.Services.AddScoped<IThemeService, ThemeService>();
|
||||
builder.Services.AddScoped<IUserPreferenceStore, NexusReader.Web.Services.ServerUserPreferenceStore>();
|
||||
builder.Services.AddScoped<IThemeService, NexusReader.Web.Services.ServerThemeService>();
|
||||
builder.Services.AddScoped<IRecommendationService, NexusReader.Web.Services.ServerRecommendationService>();
|
||||
// Feature settings (avoiding direct raw IConfiguration injection in client pages)
|
||||
var featureSettings = builder.Configuration.GetSection("Features").Get<FeatureSettings>() ?? new FeatureSettings();
|
||||
builder.Services.AddSingleton(featureSettings);
|
||||
@@ -753,6 +755,20 @@ app.MapGet("/identity/profile", async (ClaimsPrincipal user, IMediator mediator)
|
||||
return Results.Ok(result.Value);
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapPost("/identity/theme", async (
|
||||
[Microsoft.AspNetCore.Mvc.FromBody] NexusReader.Application.DTOs.User.UpdateThemeRequest request,
|
||||
ClaimsPrincipal user,
|
||||
IMediator mediator) =>
|
||||
{
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrEmpty(userId)) return Results.Unauthorized();
|
||||
|
||||
var result = await mediator.Send(new NexusReader.Application.Commands.User.UpdateThemeCommand(userId, request.Mode));
|
||||
if (result.IsFailed) return Results.BadRequest(result.Errors.FirstOrDefault()?.Message);
|
||||
|
||||
return Results.Ok();
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentResults;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NexusReader.Application.Queries.Recommendations;
|
||||
using NexusReader.UI.Shared.Services;
|
||||
|
||||
namespace NexusReader.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Server-side implementation of <see cref="IRecommendationService"/> that executes
|
||||
/// the MediatR query directly inside the Web Server's request context.
|
||||
/// </summary>
|
||||
public sealed class ServerRecommendationService : IRecommendationService
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public ServerRecommendationService(IMediator mediator, IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public async Task<List<RecommendationDto>?> GetRecommendationsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext?.User == null)
|
||||
{
|
||||
return new List<RecommendationDto>();
|
||||
}
|
||||
|
||||
var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return new List<RecommendationDto>();
|
||||
}
|
||||
|
||||
var result = await _mediator.Send(new GetContextualRecommendationsQuery(userId), cancellationToken);
|
||||
if (result.IsSuccess && result.Value != null)
|
||||
{
|
||||
return result.Value.Recommendations;
|
||||
}
|
||||
|
||||
return new List<RecommendationDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using NexusReader.Domain.Enums;
|
||||
using NexusReader.UI.Shared.Services;
|
||||
|
||||
namespace NexusReader.Web.Services;
|
||||
|
||||
public sealed class ServerThemeService : IThemeService
|
||||
{
|
||||
public ThemeMode Mode => ThemeMode.System;
|
||||
public bool IsLightMode => false;
|
||||
|
||||
// Explicit event implementation to avoid CS0067 warning about unused events on the server
|
||||
public event Action<ThemeMode>? OnThemeChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
public Task SetThemeAsync(ThemeMode mode) => Task.CompletedTask;
|
||||
public Task ToggleTheme() => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Security.Claims;
|
||||
using FluentResults;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NexusReader.Data.Persistence;
|
||||
using NexusReader.Domain.Enums;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
|
||||
namespace NexusReader.Web.Services;
|
||||
|
||||
public class ServerUserPreferenceStore : IUserPreferenceStore
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public ServerUserPreferenceStore(
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public async Task<Result> SaveThemePreferenceAsync(ThemeMode mode)
|
||||
{
|
||||
var userId = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return Result.Fail("User is not authenticated on the server.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
var user = await dbContext.Users
|
||||
.AsTracking()
|
||||
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Fail("User not found in database.");
|
||||
}
|
||||
|
||||
user.ThemePreference = mode;
|
||||
await dbContext.SaveChangesAsync();
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error("Database failure updating theme preference.").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<ThemeMode>> GetThemePreferenceAsync()
|
||||
{
|
||||
var userId = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return Result.Fail("User is not authenticated on the server.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Fail("User not found in database.");
|
||||
}
|
||||
|
||||
return Result.Ok(user.ThemePreference);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error("Database failure reading theme preference.").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user