Add frontend theme mode scaffolding
This commit is contained in:
@@ -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 | 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.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
|
||||
|
||||
@@ -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.
|
||||
- `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.
|
||||
- Theme mode selection can be prepared independently of persistence. Splitting those concerns keeps the theme CSS and the storage wiring reviewable.
|
||||
|
||||
## 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.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.5` | Pending | Shell replacement follows once tokens and theme plumbing exist. |
|
||||
| `P1.6` | Pending | Shell slots and nav utilities depend on the new shell. |
|
||||
|
||||
8
src/RolemasterDb.App/Frontend/AppState/ThemeMode.cs
Normal file
8
src/RolemasterDb.App/Frontend/AppState/ThemeMode.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace RolemasterDb.App.Frontend.AppState;
|
||||
|
||||
public enum ThemeMode
|
||||
{
|
||||
System,
|
||||
Light,
|
||||
Dark
|
||||
}
|
||||
19
src/RolemasterDb.App/Frontend/AppState/ThemeState.cs
Normal file
19
src/RolemasterDb.App/Frontend/AppState/ThemeState.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RolemasterDb.App.Components;
|
||||
using RolemasterDb.App.Data;
|
||||
using RolemasterDb.App.Frontend.AppState;
|
||||
using RolemasterDb.App.Features;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -11,6 +12,7 @@ builder.Services.AddRazorComponents()
|
||||
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
|
||||
builder.Services.AddSingleton<CriticalImportArtifactLocator>();
|
||||
builder.Services.AddScoped<LookupService>();
|
||||
builder.Services.AddScoped<ThemeState>();
|
||||
|
||||
var app = builder.Build();
|
||||
await RolemasterDbInitializer.InitializeAsync(app.Services);
|
||||
|
||||
@@ -52,6 +52,94 @@
|
||||
--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 {
|
||||
min-height: 100%;
|
||||
background:
|
||||
|
||||
Reference in New Issue
Block a user