diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md index 98a889b..72cb7e8 100644 --- a/docs/tables_frontend_overhaul_implementation_plan.md +++ b/docs/tables_frontend_overhaul_implementation_plan.md @@ -42,6 +42,7 @@ It is intentionally implementation-focused: | 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. | | 2026-03-21 | P1.4 | Completed | Added shared browser-storage wrappers, persisted theme mode in `localStorage`, and initialize/apply theme state from the layout on interactive render. | +| 2026-03-21 | P1.5 | Completed | Replaced the permanent sidebar layout with a sticky top app shell and mobile bottom navigation backed by dedicated shell components. | ### Lessons Learned @@ -53,6 +54,7 @@ It is intentionally implementation-focused: - 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. - Shared UI state events in Blazor should stay synchronous unless the event contract is async-aware. Layout refresh can be triggered safely with `InvokeAsync(StateHasChanged)` from a synchronous handler. +- Extracting shell markup into dedicated components is lower-risk than continuing to evolve `MainLayout.razor` directly. It isolates responsive frame work from page content and keeps later nav changes localized. ## Target Outcomes @@ -258,7 +260,7 @@ Create the implementation foundation so the visual overhaul does not start with | `P1.2` | Completed | Font loading now uses Fraunces, IBM Plex Sans, and IBM Plex Mono with explicit role-based tokens. | | `P1.3` | Completed | Explicit light, dark, and system modes now exist in CSS, backed by a scoped `ThemeState` service. | | `P1.4` | Completed | Theme mode now persists through a shared storage service and is applied from the layout during interactive startup. | -| `P1.5` | Pending | Shell replacement follows once tokens and theme plumbing exist. | +| `P1.5` | Completed | The sidebar is gone; pages now render inside a sticky top-shell with a mobile bottom nav. | | `P1.6` | Pending | Shell slots and nav utilities depend on the new shell. | | `P1.7` | Pending | Skip link and landmark work will land with the shell markup. | | `P1.8` | Pending | Tools-specific shell emphasis is final Phase 1 polish. | diff --git a/src/RolemasterDb.App/Components/Layout/MainLayout.razor b/src/RolemasterDb.App/Components/Layout/MainLayout.razor index c0a9d0c..7c88ccb 100644 --- a/src/RolemasterDb.App/Components/Layout/MainLayout.razor +++ b/src/RolemasterDb.App/Components/Layout/MainLayout.razor @@ -2,17 +2,9 @@ @implements IDisposable @inject RolemasterDb.App.Frontend.AppState.ThemeState ThemeState -
- - -
-
- @Body -
-
-
+ + @Body +
An unhandled error has occurred. diff --git a/src/RolemasterDb.App/Components/Layout/MainLayout.razor.css b/src/RolemasterDb.App/Components/Layout/MainLayout.razor.css index 142cf3b..eb5d72e 100644 --- a/src/RolemasterDb.App/Components/Layout/MainLayout.razor.css +++ b/src/RolemasterDb.App/Components/Layout/MainLayout.razor.css @@ -1,38 +1,3 @@ -.page { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -main { - flex: 1; - min-width: 0; -} - -.sidebar { - background: - radial-gradient(circle at top, rgba(196, 167, 107, 0.28), transparent 35%), - linear-gradient(180deg, #24130d 0%, #3c2415 46%, #130d0b 100%); - border-right: 1px solid rgba(196, 167, 107, 0.2); -} - -.content-shell { - padding: 1.5rem; -} - -@media (min-width: 641px) { - .page { - flex-direction: row; - } - - .sidebar { - width: 290px; - height: 100vh; - position: sticky; - top: 0; - } -} - #blazor-error-ui { color-scheme: light only; background: #682e24; diff --git a/src/RolemasterDb.App/Components/Shell/AppShell.razor b/src/RolemasterDb.App/Components/Shell/AppShell.razor new file mode 100644 index 0000000..73f399d --- /dev/null +++ b/src/RolemasterDb.App/Components/Shell/AppShell.razor @@ -0,0 +1,41 @@ +
+
+ +
+ +
+
+ @ChildContent +
+
+ + +
+ +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public RenderFragment? HeaderActions { get; set; } +} diff --git a/src/RolemasterDb.App/Components/Shell/AppShell.razor.css b/src/RolemasterDb.App/Components/Shell/AppShell.razor.css new file mode 100644 index 0000000..edeea16 --- /dev/null +++ b/src/RolemasterDb.App/Components/Shell/AppShell.razor.css @@ -0,0 +1,133 @@ +.app-shell { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.app-shell-header { + position: sticky; + top: 0; + z-index: 40; + padding: 0.9rem 1rem 0; +} + +.app-shell-bar { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 1rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border-default); + border-radius: 24px; + background: color-mix(in srgb, var(--bg-elevated) 78%, transparent); + box-shadow: var(--shadow-1); + backdrop-filter: blur(18px); +} + +.app-shell-brand { + display: inline-flex; + align-items: center; + gap: 0.85rem; + color: inherit; + text-decoration: none; +} + +.app-shell-brand:hover { + color: inherit; +} + +.app-shell-brand-mark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + border-radius: 18px; + background: linear-gradient(145deg, var(--accent-3), var(--accent-5)); + color: var(--text-on-accent); + font-family: var(--font-ui); + font-size: 0.85rem; + letter-spacing: 0.12em; + text-transform: uppercase; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); +} + +.app-shell-brand-copy { + display: grid; + gap: 0.1rem; +} + +.app-shell-brand-copy strong { + font-family: var(--font-display); + font-size: 1rem; + font-weight: 600; + line-height: 1.1; +} + +.app-shell-brand-copy span { + color: var(--text-secondary); + font-size: 0.82rem; +} + +.app-shell-header-nav { + min-width: 0; +} + +.app-shell-header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.75rem; + min-height: 2.75rem; +} + +.app-shell-main { + flex: 1 1 auto; + min-width: 0; + padding: 1rem 0 5.75rem; +} + +.content-shell { + width: min(1600px, 100%); + margin: 0 auto; + padding: 0 1rem; + box-sizing: border-box; +} + +.app-shell-mobile-nav { + position: sticky; + bottom: 0; + z-index: 35; + padding: 0 0.75rem 0.75rem; + margin-top: auto; +} + +@media (max-width: 767.98px) { + .app-shell-header { + padding: 0.65rem 0.75rem 0; + } + + .app-shell-bar { + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + padding: 0.7rem 0.85rem; + } + + .app-shell-brand-copy span { + display: none; + } + + .app-shell-header-nav { + display: none; + } +} + +@media (min-width: 768px) { + .app-shell-main { + padding-bottom: 1.25rem; + } + + .app-shell-mobile-nav { + display: none; + } +} diff --git a/src/RolemasterDb.App/Components/Shell/ShellPrimaryNav.razor b/src/RolemasterDb.App/Components/Shell/ShellPrimaryNav.razor new file mode 100644 index 0000000..eeeb87b --- /dev/null +++ b/src/RolemasterDb.App/Components/Shell/ShellPrimaryNav.razor @@ -0,0 +1,22 @@ +
+ + Lookup Desk + + + + Critical Tables + + + + API Surface + + + + Diagnostics + +
+ +@code { + [Parameter] + public bool IsBottomNav { get; set; } +} diff --git a/src/RolemasterDb.App/Components/Shell/ShellPrimaryNav.razor.css b/src/RolemasterDb.App/Components/Shell/ShellPrimaryNav.razor.css new file mode 100644 index 0000000..11a8230 --- /dev/null +++ b/src/RolemasterDb.App/Components/Shell/ShellPrimaryNav.razor.css @@ -0,0 +1,65 @@ +.shell-primary-nav { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.shell-primary-nav-link { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.75rem; + padding: 0.65rem 0.95rem; + border-radius: 999px; + border: 1px solid transparent; + color: var(--text-secondary); + text-decoration: none; + transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease; +} + +.shell-primary-nav-link:hover { + color: var(--text-primary); + background: color-mix(in srgb, var(--surface-2) 84%, transparent); + border-color: var(--border-subtle); + transform: translateY(-1px); +} + +.shell-primary-nav-link.active { + color: var(--text-primary); + background: color-mix(in srgb, var(--accent-1) 84%, var(--surface-2)); + border-color: color-mix(in srgb, var(--accent-3) 45%, var(--border-default)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16); +} + +.shell-primary-nav-label { + font-size: 0.92rem; + line-height: 1; + white-space: nowrap; +} + +.shell-primary-nav.is-bottom-nav { + justify-content: space-between; + gap: 0.35rem; + padding: 0.35rem; + border: 1px solid var(--border-default); + border-radius: 24px; + background: color-mix(in srgb, var(--bg-elevated) 88%, transparent); + box-shadow: var(--shadow-1); + backdrop-filter: blur(18px); +} + +.shell-primary-nav.is-bottom-nav .shell-primary-nav-link { + flex: 1 1 0; + min-width: 0; + padding-inline: 0.45rem; +} + +.shell-primary-nav.is-bottom-nav .shell-primary-nav-label { + font-size: 0.78rem; +} + +@media (max-width: 767.98px) { + .shell-primary-nav.is-top-nav { + display: none; + } +} diff --git a/src/RolemasterDb.App/Components/_Imports.razor b/src/RolemasterDb.App/Components/_Imports.razor index e516b63..883310b 100644 --- a/src/RolemasterDb.App/Components/_Imports.razor +++ b/src/RolemasterDb.App/Components/_Imports.razor @@ -11,4 +11,5 @@ @using RolemasterDb.App @using RolemasterDb.App.Components @using RolemasterDb.App.Components.Layout +@using RolemasterDb.App.Components.Shell @using RolemasterDb.App.Components.Shared