diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md index 10a8af5..938028c 100644 --- a/docs/tables_frontend_overhaul_implementation_plan.md +++ b/docs/tables_frontend_overhaul_implementation_plan.md @@ -30,7 +30,7 @@ It is intentionally implementation-focused: - Branch: `frontend/tables-overhaul` - Last updated: `2026-04-12` -- Current focus: `Phase 7` +- Current focus: `Post-Phase 7 review` - Document mode: living plan and progress log ### Progress Log @@ -79,6 +79,7 @@ It is intentionally implementation-focused: | 2026-04-12 | Phase 4 | Completed | Replaced the placeholder `/curation` route with a real queue-first workspace, added queue scope and context persistence, moved browse-to-curation handoff out of `Tables`, and preserved diagnostics and full-editor escape hatches without keeping queue work on the reference page. | | 2026-04-12 | Phase 5 | Completed | Consolidated the existing tooling routes into a coherent `Tools` workspace with a real hub, shared tooling page frame, preserved-context exits from diagnostics back into `Tables` and `Curation`, and a grouped API reference surface. | | 2026-04-12 | Phase 6 | Completed | Reframed `/` as `Play`, replaced the old symmetric dashboard treatment with a resolver-first layout, aligned the page to the shared shell and token system, and added result-to-`Tables` deep links from both direct and attack-driven critical outcomes. | +| 2026-04-12 | Phase 7 | Completed | Hardened keyboard/focus behavior across shell and table workflows, raised compact controls to the 44px accessibility target, tightened sticky-shell scroll offsets, added explicit deep-link serializer coverage, removed obsolete old-shell and old-`/tables` component paths, and cleaned up the final route/shell documentation. | ### Lessons Learned @@ -212,12 +213,12 @@ Create the implementation foundation so the visual overhaul does not start with | App host | `src/RolemasterDb.App/Components/App.razor` | document shell, fonts, global CSS/script includes | keep as the document root; only update fonts and global assets during Phase 1 | | Router | `src/RolemasterDb.App/Components/Routes.razor` | route resolution and layout selection | keep single router; add compatibility route components instead of special middleware redirects | | App shell | `src/RolemasterDb.App/Components/Layout/MainLayout.razor` | current sidebar layout and page body host | replace with the new top app bar shell and mobile bottom nav | -| Primary nav | `src/RolemasterDb.App/Components/Layout/NavMenu.razor` | implementation-bucket navigation | retire after the new shell lands; replace with destination navigation primitives | +| Primary nav | `src/RolemasterDb.App/Components/Shell/ShellPrimaryNav.razor` | destination-based shell navigation | keep as the canonical top and mobile nav surface | | Home page | `src/RolemasterDb.App/Components/Pages/Home.razor` | live lookup flow | keep behavior, later restyle and reframe as `Play` | | Tables page | `src/RolemasterDb.App/Components/Pages/Tables.razor` | table selection, table rendering, persisted selection, editor launch, curation queue work | split into page shell, selection state, index rail, context bar, table canvas, inspector, and action services | | Diagnostics page | `src/RolemasterDb.App/Components/Pages/Diagnostics.razor` | engineering inspection of a selected cell | move under `Tools` and reuse shared table-selection state | | API page | `src/RolemasterDb.App/Components/Pages/Api.razor` | static API docs page | move under `Tools` with updated framing | -| Shared editor and curation UI | `src/RolemasterDb.App/Components/Shared/CriticalCellEditorDialog.razor`, `src/RolemasterDb.App/Components/Shared/CriticalCellCurationDialog.razor` | full editing and quick curation interactions | preserve behavior; change launch points and page ownership | +| Shared editor and curation UI | `src/RolemasterDb.App/Components/Shared/CriticalCellEditorDialog.razor`, `src/RolemasterDb.App/Components/Curation/CurationWorkspace.razor` | full editing and queue-first quick curation interactions | preserve behavior while keeping repeated curation work on the dedicated `/curation` surface | | Lookup and table contracts | `src/RolemasterDb.App/Features/LookupContracts.cs` | frontend-facing DTOs for tables, lookups, editing, and diagnostics | preserve contracts; build frontend state and deep links around them | ### Behaviors To Preserve @@ -226,7 +227,7 @@ Create the implementation foundation so the visual overhaul does not start with | --- | --- | --- | --- | | Selected table persistence | `Tables.razor` local storage key `rolemaster.tables.selectedTable` | shared per-destination context persistence service | move out of page code during Phase 2 | | Full cell editor flow | `Tables.razor` + `CriticalCellEditorDialog.razor` | stable editor entry from inspector, curation, and tools | editor remains modal for now | -| Quick curation flow and save-next behavior | `Tables.razor` + `CriticalCellCurationDialog.razor` | dedicated `Curation` workflow with reused quick-parse components | queue logic moves off the reference page | +| Quick curation flow and save-next behavior | `Curation.razor` + `CurationWorkspace.razor` | dedicated `Curation` workflow with reused quick-parse components | queue logic is no longer carried by the reference page | | Diagnostics selection model | `Diagnostics.razor` | shared table-position selector model used by `Tools` | existing selection order is a good starting point | | Attack and direct critical lookup flows | `Home.razor` | `Play` page behaviors | preserve contracts and calculation behavior | | Result preview components | `CriticalLookupResultCard.razor`, `CompactCriticalCell.razor`, related shared components | reusable reference and inspector content | keep and adapt instead of rewriting presentation logic from scratch | @@ -589,7 +590,7 @@ Turn `/tables` into the canonical reference surface for reading and inspecting c - The route already exists at `Components/Pages/Curation.razor`, but it is still a placeholder panel with no queue, no selection state, and no working-lane layout. - The active curation implementation currently lives inside `Components/Pages/Tables.razor` through page-local state such as `OpenCellCurationAsync`, `MarkCellCuratedAsync`, `LoadCurationCellAsync`, `ReparseCurationCellAsync`, and `FindNextUncuratedResultId`. -- The current curation UI is rendered by `Components/Shared/CriticalCellCurationDialog.razor`, which already contains the two most valuable pieces of the eventual Phase 4 surface: side-by-side parsed preview and source image, plus an inline quick-parse mode. +- The current queue-first curation UI now lives in `Components/Curation/CurationWorkspace.razor`, which keeps the two most valuable pieces of the old dialog flow: side-by-side parsed preview and source image, plus an inline quick-parse mode. - The current `next uncurated` behavior only walks the already loaded cells of the selected table. It does not yet operate across queue scopes such as all tables or the pinned set. - The full editor path already exists and should remain available, but the present implementation still depends on opening curation from the `Tables` browsing flow rather than from a dedicated repeated-action workflow. @@ -677,7 +678,7 @@ Create a dedicated queue-first curation workflow so repair work is fast and does ### Phase 4 Implementation Notes -- The lowest-risk migration path is to keep `CriticalCellCurationDialog` as the behavioral baseline, extract any queue and save helpers that are currently trapped in `Tables.razor`, and then rehost that workflow inside new `Components/Curation` surfaces. +- The lowest-risk migration path was to keep the old dialog behavior as the baseline, extract any queue and save helpers that were trapped in `Tables.razor`, and then rehost that workflow inside `Components/Curation` surfaces. - The current page-local `FindNextUncuratedResultId` logic is the clearest seam for early extraction. Once that becomes a shared queue helper, the same save-and-advance contract can back both the dedicated `Curation` page and any temporary compatibility entry points from `Tables`. - `Curation` should reuse the existing deep-link grammar rather than inventing a second identifier model. The distinction should be workflow mode and queue scope, not a parallel object-addressing scheme. - The implementation should keep normal curation modal-free on desktop and mobile. Full edit can still use a focused drawer or sheet, but the queue lane itself should be a stable page surface. @@ -770,6 +771,26 @@ Make the default landing experience feel like part of the same product and conne ## Phase 7: Hardening, Accessibility, Performance, And Rollout Cleanup +### Status + +`Completed` + +### Task Progress + +| Task | Status | Notes | +| --- | --- | --- | +| `P7.1` | Completed | Shared tabs, shell overlays, and the table index rail now expose stronger keyboard reachability and visible active state. | +| `P7.2` | Completed | Focus treatment was normalized across shell navigation, compact controls, table index actions, and interactive result surfaces. | +| `P7.3` | Completed | Accent, warning, success, and tooling emphasis continue to use distinct semantic color ramps in both themes after the final focus and hit-target pass. | +| `P7.4` | Completed | Sticky shell/context-bar and contained-scroll behavior were verified and tightened for the final desktop/mobile layout. | +| `P7.5` | Completed | Final shell scroll offsets now reserve space for sticky shell chrome so content is not obscured during navigation or restored context jumps. | +| `P7.6` | Completed | Shared table-context URL round-trip coverage now explicitly exercises the full serializer field set, including queue scope. | +| `P7.7` | Completed | The per-destination restore model established earlier remained the final persisted-context path and was reverified during the hardening pass. | +| `P7.8` | Completed | `TablesCanvas` now caches active row/column/group state and roll-jump context instead of recomputing those hot-path values throughout the render loop. | +| `P7.9` | Completed | Virtualization was not introduced because profiling did not justify the extra sticky-grid complexity. | +| `P7.10` | Completed | Obsolete old-shell and old-`/tables` components were removed from the active codebase, including the retired inspector and pre-queue curation dialog path. | +| `P7.11` | Completed | The implementation plan now reflects the final route map, shell model, and surviving component boundaries. | + ### Goal Stabilize the overhaul and ensure the final UX matches the bible under real usage conditions. diff --git a/src/RolemasterDb.App/Components/Layout/NavMenu.razor b/src/RolemasterDb.App/Components/Layout/NavMenu.razor deleted file mode 100644 index b5e1b0d..0000000 --- a/src/RolemasterDb.App/Components/Layout/NavMenu.razor +++ /dev/null @@ -1,31 +0,0 @@ - diff --git a/src/RolemasterDb.App/Components/Layout/NavMenu.razor.css b/src/RolemasterDb.App/Components/Layout/NavMenu.razor.css deleted file mode 100644 index 5a9395c..0000000 --- a/src/RolemasterDb.App/Components/Layout/NavMenu.razor.css +++ /dev/null @@ -1,98 +0,0 @@ -.nav-menu-shell { - position: relative; - min-height: 3.5rem; -} - -.navbar-toggler { - appearance: none; - cursor: pointer; - width: 3.5rem; - height: 2.5rem; - color: white; - position: absolute; - top: 0.5rem; - right: 1rem; - border: 1px solid rgba(226, 195, 128, 0.22); - background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 244, 218, 0.82%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.06); -} - -.navbar-toggler:checked { - background-color: rgba(226, 195, 128, 0.3); -} - -.bi { - display: inline-block; - position: relative; - width: 1.25rem; - height: 1.25rem; - margin-right: 0.75rem; - top: -1px; - background-size: cover; -} - -.bi-house-door-fill-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23fce7bc' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); -} - -.bi-list-nested-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23fce7bc' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); -} - -.nav-item { - font-size: 0.98rem; - padding-bottom: 0.35rem; -} - -.nav-item:last-of-type { - padding-bottom: 1rem; -} - -.nav-item ::deep .nav-link { - font-family: "Source Sans 3", "Segoe UI", sans-serif; - color: #f3ddbc; - background: none; - border: none; - border-radius: 12px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - width: 100%; - padding: 0 1rem; -} - -.nav-item ::deep a.active { - background: linear-gradient(135deg, rgba(230, 195, 126, 0.22), rgba(255, 245, 225, 0.1)); - color: #fff7e2; -} - -.nav-item ::deep .nav-link:hover { - background-color: rgba(255, 248, 234, 0.08); - color: #fff7e2; -} - -.nav-scrollable { - display: none; - padding: 3.5rem 0 0.5rem; -} - -.navbar-toggler:checked ~ .nav-scrollable { - display: block; -} - -@media (min-width: 641px) { - .navbar-toggler { - display: none; - } - - .nav-menu-shell { - min-height: 0; - } - - .nav-scrollable { - display: block; - height: 100vh; - overflow-y: auto; - padding: 1rem 0; - } -} diff --git a/src/RolemasterDb.App/Components/Primitives/SegmentedTabs.razor b/src/RolemasterDb.App/Components/Primitives/SegmentedTabs.razor index e7e18be..049367b 100644 --- a/src/RolemasterDb.App/Components/Primitives/SegmentedTabs.razor +++ b/src/RolemasterDb.App/Components/Primitives/SegmentedTabs.razor @@ -2,14 +2,17 @@ @foreach (var item in Items) { var isSelected = string.Equals(item.Value, SelectedValue, StringComparison.Ordinal); + var tabIndex = isSelected || (!HasSelectedValue && string.Equals(item.Value, FirstEnabledValue, StringComparison.Ordinal)) ? 0 : -1; - - -} - -@code { - [Parameter] - public bool IsOpen { get; set; } - - [Parameter] - public string Placement { get; set; } = "end"; - - [Parameter] - public string AriaLabel { get; set; } = "Drawer"; - - [Parameter] - public string CloseLabel { get; set; } = "Close panel"; - - [Parameter] - public string? Title { get; set; } - - [Parameter] - public RenderFragment? HeaderContent { get; set; } - - [Parameter] - public RenderFragment? ChildContent { get; set; } - - [Parameter] - public EventCallback OnClose { get; set; } - - [Parameter] - public string? CssClass { get; set; } - - private string BuildCssClass() - { - var classes = new List { "surface-drawer", $"is-{Placement.Trim().ToLowerInvariant()}" }; - if (!string.IsNullOrWhiteSpace(CssClass)) - { - classes.Add(CssClass); - } - - return string.Join(' ', classes); - } - - private Task HandleCloseAsync() => - OnClose.InvokeAsync(); -} diff --git a/src/RolemasterDb.App/Components/Shared/CriticalCellCurationDialog.razor b/src/RolemasterDb.App/Components/Shared/CriticalCellCurationDialog.razor deleted file mode 100644 index 72e4dec..0000000 --- a/src/RolemasterDb.App/Components/Shared/CriticalCellCurationDialog.razor +++ /dev/null @@ -1,168 +0,0 @@ -@using Microsoft.JSInterop -@using RolemasterDb.App.Components.Curation -@using RolemasterDb.App.Features -@implements IAsyncDisposable -@inject IJSRuntime JSRuntime - -
-
-
-
-

Curate Result Card

- @if (Model is not null) - { -

- @Model.TableName - · Column @BuildColumnDisplayText(Model) - · Roll band @Model.RollBand -

- } -
- -
- -
- -
-
-
- -@code { - - [Parameter, EditorRequired] - public CriticalCellEditorModel? Model { get; set; } - - [Parameter] - public bool IsLoading { get; set; } - - [Parameter] - public bool IsSaving { get; set; } - - [Parameter] - public bool IsReparsing { get; set; } - - [Parameter] - public bool IsQuickParseMode { get; set; } - - [Parameter] - public string? ErrorMessage { get; set; } - - [Parameter] - public string? QuickParseErrorMessage { get; set; } - - [Parameter] - public IReadOnlyList? LegendEntries { get; set; } - - [Parameter, EditorRequired] - public EventCallback OnClose { get; set; } - - [Parameter, EditorRequired] - public EventCallback OnMarkCurated { get; set; } - - [Parameter, EditorRequired] - public EventCallback OnEdit { get; set; } - - [Parameter, EditorRequired] - public EventCallback OnEnterQuickParse { get; set; } - - [Parameter, EditorRequired] - public EventCallback OnCancelQuickParse { get; set; } - - [Parameter, EditorRequired] - public EventCallback OnReparse { get; set; } - - private IJSObjectReference? jsModule; - private bool isBackdropPointerDown; - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - jsModule = await JSRuntime.InvokeAsync("import", "./components/shared/critical-cell-editor-dialog.js"); - - await jsModule.InvokeVoidAsync("lockBackgroundScroll"); - } - } - - public async ValueTask DisposeAsync() - { - if (jsModule is null) - { - return; - } - - try - { - await jsModule.InvokeVoidAsync("unlockBackgroundScroll"); - await jsModule.DisposeAsync(); - } - catch (JSDisconnectedException) - { - } - } - - private void HandleBackdropPointerDown() - { - isBackdropPointerDown = true; - } - - private async Task HandleBackdropPointerUp() - { - if (!isBackdropPointerDown) - { - return; - } - - isBackdropPointerDown = false; - await OnClose.InvokeAsync(); - } - - private void HandleBackdropPointerCancel() - { - isBackdropPointerDown = false; - } - - private void HandleDialogPointerDown() - { - isBackdropPointerDown = false; - } - - private void HandleDialogPointerUp() - { - isBackdropPointerDown = false; - } - - private void HandleDialogPointerCancel() - { - isBackdropPointerDown = false; - } - - private static string BuildColumnDisplayText(CriticalCellEditorModel model) => - string.IsNullOrWhiteSpace(model.GroupLabel) ? model.ColumnLabel : $"{model.GroupLabel} / {model.ColumnLabel}"; - -} diff --git a/src/RolemasterDb.App/Components/Shell/AppShell.razor b/src/RolemasterDb.App/Components/Shell/AppShell.razor index b1869f6..5da5c93 100644 --- a/src/RolemasterDb.App/Components/Shell/AppShell.razor +++ b/src/RolemasterDb.App/Components/Shell/AppShell.razor @@ -26,7 +26,7 @@