Add frontend theme mode scaffolding

This commit is contained in:
2026-03-21 13:10:42 +01:00
parent d3b7819df3
commit 0b7cc846e7
5 changed files with 120 additions and 1 deletions

View File

@@ -40,6 +40,7 @@ It is intentionally implementation-focused:
| 2026-03-21 | Phase 0 | Completed | Created overhaul branch, audited the current frontend, and locked the route map, component boundaries, migration path, and shared-state ownership. | | 2026-03-21 | Phase 0 | Completed | Created overhaul branch, audited the current frontend, and locked the route map, component boundaries, migration path, and shared-state ownership. |
| 2026-03-21 | P1.1 | Completed | Replaced the legacy token root with semantic background, surface, text, border, focus, shadow, and semantic accent ramps while keeping compatibility aliases for incremental migration. | | 2026-03-21 | P1.1 | Completed | Replaced the legacy token root with semantic background, surface, text, border, focus, shadow, and semantic accent ramps while keeping compatibility aliases for incremental migration. |
| 2026-03-21 | P1.2 | Completed | Switched the app to Fraunces, IBM Plex Sans, and IBM Plex Mono with distinct display, body, UI, and code font roles instead of one shared heading font. | | 2026-03-21 | P1.2 | Completed | Switched the app to Fraunces, IBM Plex Sans, and IBM Plex Mono with distinct display, body, UI, and code font roles instead of one shared heading font. |
| 2026-03-21 | P1.3 | Completed | Added explicit light, dark, and system theme modes in the token layer and introduced a scoped `ThemeState` service for later shell controls. |
### Lessons Learned ### Lessons Learned
@@ -49,6 +50,7 @@ It is intentionally implementation-focused:
- Diagnostics already behaves like a separate workflow and should be moved under `Tools` early, before the `Tables` page is rewritten further. - Diagnostics already behaves like a separate workflow and should be moved under `Tools` early, before the `Tables` page is rewritten further.
- `localStorage` access is currently page-local and ad hoc. Theme, recents, pins, and table context need one shared storage boundary before more UI work starts. - `localStorage` access is currently page-local and ad hoc. Theme, recents, pins, and table context need one shared storage boundary before more UI work starts.
- The old typography setup coupled display and utility text under a single token. The new shell work needs separate display and UI font roles to avoid decorative type in controls. - The old typography setup coupled display and utility text under a single token. The new shell work needs separate display and UI font roles to avoid decorative type in controls.
- Theme mode selection can be prepared independently of persistence. Splitting those concerns keeps the theme CSS and the storage wiring reviewable.
## Target Outcomes ## Target Outcomes
@@ -252,7 +254,7 @@ Create the implementation foundation so the visual overhaul does not start with
| --- | --- | --- | | --- | --- | --- |
| `P1.1` | Completed | Semantic token layer landed in `wwwroot/app.css` with compatibility aliases to keep existing pages stable. | | `P1.1` | Completed | Semantic token layer landed in `wwwroot/app.css` with compatibility aliases to keep existing pages stable. |
| `P1.2` | Completed | Font loading now uses Fraunces, IBM Plex Sans, and IBM Plex Mono with explicit role-based tokens. | | `P1.2` | Completed | Font loading now uses Fraunces, IBM Plex Sans, and IBM Plex Mono with explicit role-based tokens. |
| `P1.3` | Pending | Theme modes will be introduced after typography is stable. | | `P1.3` | Completed | Explicit light, dark, and system modes now exist in CSS, backed by a scoped `ThemeState` service. |
| `P1.4` | Pending | Theme persistence depends on the theme state service. | | `P1.4` | Pending | Theme persistence depends on the theme state service. |
| `P1.5` | Pending | Shell replacement follows once tokens and theme plumbing exist. | | `P1.5` | Pending | Shell replacement follows once tokens and theme plumbing exist. |
| `P1.6` | Pending | Shell slots and nav utilities depend on the new shell. | | `P1.6` | Pending | Shell slots and nav utilities depend on the new shell. |

View File

@@ -0,0 +1,8 @@
namespace RolemasterDb.App.Frontend.AppState;
public enum ThemeMode
{
System,
Light,
Dark
}

View File

@@ -0,0 +1,19 @@
namespace RolemasterDb.App.Frontend.AppState;
public sealed class ThemeState
{
public ThemeMode CurrentMode { get; private set; } = ThemeMode.System;
public event Action? Changed;
public void SetMode(ThemeMode mode)
{
if (CurrentMode == mode)
{
return;
}
CurrentMode = mode;
Changed?.Invoke();
}
}

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RolemasterDb.App.Components; using RolemasterDb.App.Components;
using RolemasterDb.App.Data; using RolemasterDb.App.Data;
using RolemasterDb.App.Frontend.AppState;
using RolemasterDb.App.Features; using RolemasterDb.App.Features;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -11,6 +12,7 @@ builder.Services.AddRazorComponents()
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString)); builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
builder.Services.AddSingleton<CriticalImportArtifactLocator>(); builder.Services.AddSingleton<CriticalImportArtifactLocator>();
builder.Services.AddScoped<LookupService>(); builder.Services.AddScoped<LookupService>();
builder.Services.AddScoped<ThemeState>();
var app = builder.Build(); var app = builder.Build();
await RolemasterDbInitializer.InitializeAsync(app.Services); await RolemasterDbInitializer.InitializeAsync(app.Services);

View File

@@ -52,6 +52,94 @@
--shadow: var(--shadow-1); --shadow: var(--shadow-1);
} }
:root,
:root[data-theme="light"] {
color-scheme: light;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg-canvas: #161412;
--bg-canvas-strong: #0f0d0c;
--bg-elevated: #201c19;
--bg-overlay: rgba(5, 5, 6, 0.68);
--surface-1: rgba(33, 28, 25, 0.88);
--surface-2: rgba(40, 34, 30, 0.9);
--surface-3: rgba(49, 41, 36, 0.92);
--surface-tooling: rgba(27, 31, 37, 0.92);
--text-primary: #f3eadf;
--text-secondary: #cab8a7;
--text-tertiary: #a28d7d;
--text-on-accent: #fff6ec;
--border-subtle: rgba(209, 188, 163, 0.12);
--border-default: rgba(209, 188, 163, 0.2);
--border-strong: rgba(209, 188, 163, 0.32);
--focus-ring: rgba(237, 181, 109, 0.38);
--focus-border: rgba(237, 181, 109, 0.52);
--shadow-1: 0 18px 40px rgba(0, 0, 0, 0.3);
--shadow-2: 0 28px 60px rgba(0, 0, 0, 0.42);
--accent-1: #36271d;
--accent-2: #6e4b2f;
--accent-3: #a06c39;
--accent-4: #c98c4b;
--accent-5: #edb56d;
--success-1: #1d2a22;
--success-2: #476653;
--success-3: #88b58b;
--warning-1: #36281b;
--warning-2: #8a6436;
--warning-3: #e3b063;
--danger-1: #351d1a;
--danger-2: #92524d;
--danger-3: #e29b90;
--info-1: #18252f;
--info-2: #45677d;
--info-3: #9ec0d5;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]),
:root[data-theme="system"] {
color-scheme: dark;
--bg-canvas: #161412;
--bg-canvas-strong: #0f0d0c;
--bg-elevated: #201c19;
--bg-overlay: rgba(5, 5, 6, 0.68);
--surface-1: rgba(33, 28, 25, 0.88);
--surface-2: rgba(40, 34, 30, 0.9);
--surface-3: rgba(49, 41, 36, 0.92);
--surface-tooling: rgba(27, 31, 37, 0.92);
--text-primary: #f3eadf;
--text-secondary: #cab8a7;
--text-tertiary: #a28d7d;
--text-on-accent: #fff6ec;
--border-subtle: rgba(209, 188, 163, 0.12);
--border-default: rgba(209, 188, 163, 0.2);
--border-strong: rgba(209, 188, 163, 0.32);
--focus-ring: rgba(237, 181, 109, 0.38);
--focus-border: rgba(237, 181, 109, 0.52);
--shadow-1: 0 18px 40px rgba(0, 0, 0, 0.3);
--shadow-2: 0 28px 60px rgba(0, 0, 0, 0.42);
--accent-1: #36271d;
--accent-2: #6e4b2f;
--accent-3: #a06c39;
--accent-4: #c98c4b;
--accent-5: #edb56d;
--success-1: #1d2a22;
--success-2: #476653;
--success-3: #88b58b;
--warning-1: #36281b;
--warning-2: #8a6436;
--warning-3: #e3b063;
--danger-1: #351d1a;
--danger-2: #92524d;
--danger-3: #e29b90;
--info-1: #18252f;
--info-2: #45677d;
--info-3: #9ec0d5;
}
}
html, body { html, body {
min-height: 100%; min-height: 100%;
background: background: