diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md
index 37245c7..b4df18d 100644
--- a/docs/tables_frontend_overhaul_implementation_plan.md
+++ b/docs/tables_frontend_overhaul_implementation_plan.md
@@ -55,6 +55,7 @@ It is intentionally implementation-focused:
| 2026-03-21 | P2.3 | Completed | Added a shared `RecentTablesState` service backed by browser storage, centralized the recents storage key, and started recording successful table visits from the `Tables` page so later omnibox and rail work has real shared data. |
| 2026-03-21 | P2.4 | Completed | Added a shared `PinnedTablesState` service with browser persistence, centralized the pin storage key, initialized pin state in the `Tables` page, and added a first live pin/unpin action plus pinned status chips so later omnibox and navigation work have real saved pins to consume. |
| 2026-03-21 | P2.5 | Completed | Added shared table-context URL types and a serializer, registered the serializer centrally, and started round-tripping table context through `/tables` and `/tools/diagnostics` so direct links restore the selected table and diagnostic cell context instead of relying only on local storage defaults. |
+| 2026-03-21 | P2.8 | Completed | Added a shared `TableContextState` service on top of browser storage and the URL serializer, moved the `Tables` page off page-local table selection persistence, and switched diagnostics to the same restore/persist/build-URI flow so table context logic now lives in shared frontend state instead of being reinvented per page. |
### Lessons Learned
@@ -79,6 +80,7 @@ It is intentionally implementation-focused:
- Shared browser-backed state becomes more useful once one real page writes to it immediately. Recording recents from `Tables` now keeps later omnibox work from being blocked on synthetic placeholder data.
- Shared pinned-state services also need one live writer early. A minimal pin/unpin affordance in the current `Tables` page is enough to validate persistence before the larger navigation surfaces consume pins.
- URL serializers only pay off when wired into real pages. Using the shared serializer in both `Tables` and diagnostics now gives the project one verified round-trip path before the larger table-context service lands.
+- The serializer alone is not the reusable boundary. The maintainable seam is a shared context-state helper that owns restore, persist, normalization, and URI-building conventions while pages keep only workflow-specific selection logic.
## Target Outcomes
@@ -373,7 +375,7 @@ Establish the shared shell, tokens, typography, and theme system that every dest
| `P2.5` | Completed | Shared table-context URLs now parse and serialize through one helper and are consumed by `/tables` and `/tools/diagnostics`. |
| `P2.6` | Pending | The shell omnibox is still a placeholder trigger. |
| `P2.7` | Pending | Shared primitives for chips, tabs, drawers, and inspector sections are not extracted yet. |
-| `P2.8` | Pending | Table-selection logic still lives inside individual pages. |
+| `P2.8` | Completed | Table-context restore, persistence, normalization, and URI building now flow through one shared state service used by `Tables` and diagnostics. |
### Goal
diff --git a/src/RolemasterDb.App/Components/Pages/Tables.razor b/src/RolemasterDb.App/Components/Pages/Tables.razor
index ae09846..7a0b763 100644
--- a/src/RolemasterDb.App/Components/Pages/Tables.razor
+++ b/src/RolemasterDb.App/Components/Pages/Tables.razor
@@ -5,11 +5,10 @@
@using System.Diagnostics.CodeAnalysis
@using System.Linq
@inject NavigationManager NavigationManager
-@inject IJSRuntime JSRuntime
@inject LookupService LookupService
@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
-@inject RolemasterDb.App.Frontend.AppState.TableContextUrlSerializer TableContextUrlSerializer
+@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
No critical tables are available yet.
} + else if (!hasResolvedStoredTableSelection) + { +Restoring table context...
+ } else if (isDetailLoading) {Loading the selected table...
@@ -227,7 +230,7 @@ } @code { - private const string SelectedTableStorageKey = "rolemaster.tables.selectedTable"; + private const string ContextDestination = "tables"; private LookupReferenceData? referenceData; private CriticalTableDetail? tableDetail; private string selectedTableSlug = string.Empty; @@ -285,9 +288,8 @@ { selectedTableSlug = tableSlug; isTableMenuOpen = false; - await PersistSelectedTableAsync(tableSlug); await LoadTableDetailAsync(); - SyncTableContextUrl(); + await PersistAndSyncTableContextAsync(); } private async Task LoadTableDetailAsync() @@ -336,35 +338,26 @@ if (!hasResolvedStoredTableSelection && referenceData?.CriticalTables.Count > 0) { - try + var initialContext = await TableContextState.RestoreAsync( + NavigationManager.Uri, + ContextDestination, + referenceData.CriticalTables, + RolemasterDb.App.Frontend.AppState.TableContextMode.Reference); + + hasResolvedStoredTableSelection = true; + + var resolvedTableSlug = initialContext.TableSlug ?? string.Empty; + if (string.IsNullOrWhiteSpace(selectedTableSlug) || + !string.Equals(resolvedTableSlug, selectedTableSlug, StringComparison.OrdinalIgnoreCase)) { - var routeContext = TableContextUrlSerializer.Parse(NavigationManager.Uri); - var storedTableSlug = await JSRuntime.InvokeAsyncNo critical tables are available yet.
} + else if (!hasInitializedContext) + { +Restoring diagnostic context...
+ } else {