diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md index d69e550..2d1f74d 100644 --- a/docs/tables_frontend_overhaul_implementation_plan.md +++ b/docs/tables_frontend_overhaul_implementation_plan.md @@ -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. | diff --git a/src/RolemasterDb.App/Frontend/AppState/ThemeMode.cs b/src/RolemasterDb.App/Frontend/AppState/ThemeMode.cs new file mode 100644 index 0000000..3ccc5af --- /dev/null +++ b/src/RolemasterDb.App/Frontend/AppState/ThemeMode.cs @@ -0,0 +1,8 @@ +namespace RolemasterDb.App.Frontend.AppState; + +public enum ThemeMode +{ + System, + Light, + Dark +} diff --git a/src/RolemasterDb.App/Frontend/AppState/ThemeState.cs b/src/RolemasterDb.App/Frontend/AppState/ThemeState.cs new file mode 100644 index 0000000..424e456 --- /dev/null +++ b/src/RolemasterDb.App/Frontend/AppState/ThemeState.cs @@ -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(); + } +} diff --git a/src/RolemasterDb.App/Program.cs b/src/RolemasterDb.App/Program.cs index cb7264c..bb51e62 100644 --- a/src/RolemasterDb.App/Program.cs +++ b/src/RolemasterDb.App/Program.cs @@ -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(options => options.UseSqlite(connectionString)); builder.Services.AddSingleton(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); await RolemasterDbInitializer.InitializeAsync(app.Services); diff --git a/src/RolemasterDb.App/wwwroot/app.css b/src/RolemasterDb.App/wwwroot/app.css index 2030e5d..8321489 100644 --- a/src/RolemasterDb.App/wwwroot/app.css +++ b/src/RolemasterDb.App/wwwroot/app.css @@ -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: