diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md
index 773db81..504f31b 100644
--- a/docs/tables_frontend_overhaul_implementation_plan.md
+++ b/docs/tables_frontend_overhaul_implementation_plan.md
@@ -53,6 +53,7 @@ It is intentionally implementation-focused:
| 2026-03-21 | P2.1 | Completed | Added canonical `/tools/diagnostics` and `/tools/api` routes with dedicated tooling page components, extracted the diagnostics and API content into shared tool components, and updated the `Tools` landing page to link to the new route structure. |
| 2026-03-21 | P2.2 | Completed | Replaced the old `/diagnostics` and `/api` pages with lightweight compatibility routes that reuse a shared redirect component, preserve query strings and fragments, and replace browser history while forwarding into the canonical `Tools` routes. |
| 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. |
### Lessons Learned
@@ -75,6 +76,7 @@ It is intentionally implementation-focused:
- For route migration in Blazor, extracting the destination UI into shared components keeps canonical routes and temporary compatibility routes from drifting while the redirect phase is still pending.
- Compatibility routes should stay declarative and shared. A single redirect component is enough for route forwarding and avoids copy-pasted `NavigationManager` lifecycle logic in page files.
- 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.
## Target Outcomes
@@ -365,7 +367,7 @@ Establish the shared shell, tokens, typography, and theme system that every dest
| `P2.1` | Completed | Canonical `Tools` child routes now exist, backed by dedicated route pages and shared tooling content components. |
| `P2.2` | Completed | Old `/diagnostics` and `/api` routes now forward into the canonical `Tools` routes with shared redirect behavior. |
| `P2.3` | Completed | Recent critical-table visits now persist through a shared app-state service and are recorded from the `Tables` page. |
-| `P2.4` | Pending | Pinned tables state has not been introduced yet. |
+| `P2.4` | Completed | Pinned tables now persist through a shared app-state service and can already be toggled from the `Tables` page. |
| `P2.5` | Pending | Table-context URL parsing and serialization still needs a shared model. |
| `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. |
diff --git a/src/RolemasterDb.App/Components/Pages/Tables.razor b/src/RolemasterDb.App/Components/Pages/Tables.razor
index 0bff37b..543c525 100644
--- a/src/RolemasterDb.App/Components/Pages/Tables.razor
+++ b/src/RolemasterDb.App/Components/Pages/Tables.razor
@@ -6,6 +6,7 @@
@using System.Linq
@inject IJSRuntime JSRuntime
@inject LookupService LookupService
+@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
Critical Tables
@@ -29,6 +30,10 @@
@if (SelectedTableReference is { } selected)
{
+ @if (PinnedTablesState.IsPinned(selected.Key))
+ {
+ Pinned
+ }
@($"{selected.CurationPercentage}%")
}
@@ -51,6 +56,10 @@
@table.Label
+ @if (PinnedTablesState.IsPinned(table.Key))
+ {
+ Pinned
+ }
@($"{table.CurationPercentage}%")
@@ -95,7 +104,12 @@
@detail.DisplayName
@readingHint
-
Use the curation action or edit action on any filled result.
+
+
+
Use the curation action or edit action on any filled result.