diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md index a8a94be..b25a593 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-03-21` -- Current focus: `Phase 2` +- Current focus: `Phase 3` - Document mode: living plan and progress log ### Progress Log @@ -61,6 +61,7 @@ It is intentionally implementation-focused: | 2026-03-21 | Post-P2 fix 1 | Completed | Fixed the shell omnibox drawer regression by adding explicit shell offset variables, constraining drawer/body scrolling, and giving the omnibox its own backdrop geometry so the flyout opens within the visible viewport instead of collapsing into invalid top/bottom positioning. | | 2026-03-21 | Post-P2 fix 2 | Completed | Rebuilt the shell omnibox as a dedicated command palette instead of a repurposed drawer, with shell-owned overlay markup, explicit viewport-safe geometry, autofocus, Escape and navigation close behavior, and a stable scrollable result body. | | 2026-03-21 | Post-P2 fix 3 | Completed | Moved omnibox overlay ownership from the header subtree into `AppShell` itself via a shared omnibox state service and a top-level palette host, which restored full-screen backdrop coverage and reliable outside-click close behavior. | +| 2026-03-21 | P3.1 | Completed | Split `Tables.razor` into focused table components for the selector header, context bar, canvas, and legend while leaving loading, deep-link synchronization, and dialog state in the page host. | ### Lessons Learned @@ -91,6 +92,7 @@ It is intentionally implementation-focused: - Shared overlay primitives should not depend on undeclared layout variables. If a drawer needs shell offsets, the shell must define them explicitly and overlay-specific backdrops should be adjustable instead of assuming full-screen dimming is always correct. - A command palette is not just a styled drawer. It needs shell-owned geometry, predictable focus behavior, and a bounded scroll region; treating it as a generic side panel led directly to the layout regressions found in Phase 2. - Backdrop and outside-click behavior depend on overlay ownership as much as CSS. If the trigger owns the overlay inside a sticky header subtree, fixed-position assumptions can break; shell-level overlays should be rendered by the shell, not by individual header controls. +- The `Tables` rewrite is safer when orchestration and rendering are separated early. Keeping loading, persistence, and dialog state in the page host while extracting render-only components makes later layout and interaction changes much lower risk. ## Target Outcomes @@ -372,7 +374,7 @@ Establish the shared shell, tokens, typography, and theme system that every dest ### Status -`In progress` +`Completed` ### Task Progress @@ -443,6 +445,27 @@ Build the shared interaction infrastructure needed by multiple destinations befo ## Phase 3: `Tables` Reference Experience +### Status + +`In progress` + +### Task Progress + +| Task | Status | Notes | +| --- | --- | --- | +| `P3.1` | Completed | `Tables.razor` now acts as the stateful host while selector/header/canvas/legend rendering lives in dedicated `Components/Tables` components. | +| `P3.2` | Pending | Replace the selector dropdown shell with the permanent left rail layout. | +| `P3.3` | Pending | Add search, keyboard navigation, pinned/recent sections, curated percentage chips, and family filtering to the rail. | +| `P3.4` | Pending | Introduce the sticky context bar with roll jump and mode/filter controls. | +| `P3.5` | Pending | Rework the canvas for sticky headers, sticky roll bands, stronger reading emphasis, and density control. | +| `P3.6` | Pending | Remove visible resting-state action stacks from non-selected cells. | +| `P3.7` | Pending | Add the desktop selection-driven inspector. | +| `P3.8` | Pending | Add the mobile bottom-sheet inspector variant. | +| `P3.9` | Pending | Move legend/help to an on-demand secondary surface. | +| `P3.10` | Pending | Hide maintenance and developer noise in default reference mode. | +| `P3.11` | Pending | Preserve editor and curation entry points through the inspector. | +| `P3.12` | Pending | Normalize click/tap/keyboard selection and close the phase with a hardening pass. | + ### Goal Turn `/tables` into the canonical reference surface for reading and inspecting critical tables quickly. diff --git a/src/RolemasterDb.App/Components/Pages/Tables.razor b/src/RolemasterDb.App/Components/Pages/Tables.razor index 0d94695..99fbed4 100644 --- a/src/RolemasterDb.App/Components/Pages/Tables.razor +++ b/src/RolemasterDb.App/Components/Pages/Tables.razor @@ -1,8 +1,6 @@ @page "/tables" @rendermode InteractiveServer @using System -@using System.Collections.Generic -@using System.Diagnostics.CodeAnalysis @using System.Linq @inject NavigationManager NavigationManager @inject LookupService LookupService @@ -13,65 +11,16 @@ Critical Tables
-
-
- -
- - - @if (isTableMenuOpen && referenceData is not null) - { - - -
- @foreach (var table in referenceData.CriticalTables) - { - - } -
- } -
-
- -

Choose a table, then read from the roll band on the left across to the result you need.

-
+ @if (referenceData is null) { @@ -99,95 +48,16 @@ } else if (tableDetail is { } detail) { - var readingHint = detail.Groups.Count > 0 - ? "Find the roll band on the left, then read across to the group and severity you need." - : "Find the roll band on the left, then read across to the severity you need."; -
-
-
-

@detail.DisplayName

-

@readingHint

-
-
- -

Use the curation action or edit action on any filled result.

-
-
+ - @{ - var displayColumns = GetDisplayColumns(detail); - var gridTemplateStyle = BuildGridTemplateStyle(detail); - } - -
-
- @if (detail.Groups.Count > 0) - { - - @foreach (var group in detail.Groups) - { -
- @group.Label -
- } - } - - - @foreach (var displayColumn in displayColumns) - { -
- @displayColumn.ColumnLabel -
- } - - @foreach (var rollBand in detail.RollBands) - { -
@rollBand.Label
- @foreach (var displayColumn in displayColumns) - { - @if (TryGetCell(rollBand.Label, displayColumn.GroupKey, displayColumn.ColumnKey, out var cell)) - { - @RenderCriticalTableCell(cell) - } - else - { - @RenderEmptyCriticalTableCell() - } - } - } -
-
- - @{ - var legendEntries = detail.Legend ?? Array.Empty(); - } - - @if (legendEntries.Count > 0) - { -
-
-

Reading help

-

These symbols show the effects attached to a result at a glance.

-
-
- @foreach (var entry in legendEntries) - { -
- @entry.Symbol -
- @entry.Label - @entry.Description -
-
- } -
-
- } +
}
@@ -237,7 +107,6 @@ private bool isDetailLoading; private bool isReferenceDataLoading = true; private string? detailError; - private Dictionary<(string RollBand, string? GroupKey, string ColumnKey), CriticalTableCellDetail>? cellIndex; private bool IsTableSelectionDisabled => isReferenceDataLoading || (referenceData?.CriticalTables.Count ?? 0) == 0; private bool isEditorOpen; private bool isEditorLoading; @@ -297,14 +166,12 @@ if (string.IsNullOrWhiteSpace(selectedTableSlug)) { tableDetail = null; - cellIndex = null; return; } isDetailLoading = true; detailError = null; tableDetail = null; - cellIndex = null; try { @@ -324,7 +191,6 @@ finally { isDetailLoading = false; - BuildCellIndex(); } } @@ -361,32 +227,6 @@ } } - private void BuildCellIndex() - { - if (tableDetail?.Cells is null) - { - cellIndex = null; - return; - } - - cellIndex = new Dictionary<(string, string?, string), CriticalTableCellDetail>(); - foreach (var cell in tableDetail.Cells) - { - cellIndex[(cell.RollBand, cell.GroupKey, cell.ColumnKey)] = cell; - } - } - - private bool TryGetCell(string rollBand, string? groupKey, string columnKey, [NotNullWhen(true)] out CriticalTableCellDetail? cell) - { - if (cellIndex is null) - { - cell = null; - return false; - } - - return cellIndex.TryGetValue((rollBand, groupKey, columnKey), out cell); - } - private async Task OpenCellEditorAsync(int resultId) { if (string.IsNullOrWhiteSpace(selectedTableSlug)) @@ -744,36 +584,6 @@ } } - private static string GetCellCssClass(CriticalTableCellDetail cell) => - cell.IsCurated - ? "critical-table-cell is-curated" - : "critical-table-cell needs-curation"; - - private static IReadOnlyList<(string? GroupKey, string ColumnKey, string ColumnLabel)> GetDisplayColumns(CriticalTableDetail detail) - { - if (detail.Groups.Count == 0) - { - return detail.Columns - .Select(column => ((string?)null, column.Key, column.Label)) - .ToList(); - } - - return detail.Groups - .SelectMany(group => detail.Columns.Select(column => ((string?)group.Key, column.Key, column.Label))) - .ToList(); - } - - private static string BuildGridTemplateStyle(CriticalTableDetail detail) - { - var dataColumnCount = detail.Columns.Count * Math.Max(detail.Groups.Count, 1); - return $"grid-template-columns: max-content repeat({dataColumnCount}, minmax(0, 1fr));"; - } - - private static string BuildColumnSpanStyle(int span) => $"grid-column: span {span};"; - - private string GetSelectedTableLabel() => - SelectedTableReference?.Label ?? "Select a table"; - private Task TogglePinnedTableAsync() { if (SelectedTableReference is not { } selectedTable) @@ -820,55 +630,4 @@ new( TableSlug: selectedTableSlug, Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference); - - private string GetTableOptionCssClass(CriticalTableReference table) - { - var classes = new List(); - - if (string.Equals(table.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase)) - { - classes.Add("is-selected"); - } - - classes.Add(table.CurationPercentage >= 100 ? "is-curated" : "needs-curation"); - return string.Join(' ', classes); - } - - private RenderFragment RenderCriticalTableCell(CriticalTableCellDetail cell) => @
-
-
- @if (cell.IsCurated) - { - Curated - } - else - { - - } - - -
- - -
-
; - - private static RenderFragment RenderEmptyCriticalTableCell() => @
- -
; } diff --git a/src/RolemasterDb.App/Components/Tables/TablesCanvas.razor b/src/RolemasterDb.App/Components/Tables/TablesCanvas.razor new file mode 100644 index 0000000..0587521 --- /dev/null +++ b/src/RolemasterDb.App/Components/Tables/TablesCanvas.razor @@ -0,0 +1,135 @@ +
+
+ @if (Detail.Groups.Count > 0) + { + + @foreach (var group in Detail.Groups) + { +
+ @group.Label +
+ } + } + + + @foreach (var displayColumn in displayColumns) + { +
+ @displayColumn.ColumnLabel +
+ } + + @foreach (var rollBand in Detail.RollBands) + { +
@rollBand.Label
+ @foreach (var displayColumn in displayColumns) + { + if (TryGetCell(rollBand.Label, displayColumn.GroupKey, displayColumn.ColumnKey, out var resolvedCell) && resolvedCell is not null) + { + var cell = resolvedCell; + +
+
+
+ @if (cell.IsCurated) + { + Curated + } + else + { + + } + + +
+ + +
+
+ } + else + { +
+ +
+ } + } + } +
+
+ + + +@code { + private readonly Dictionary<(string RollBand, string? GroupKey, string ColumnKey), CriticalTableCellDetail> cellIndex = new(); + private readonly List<(string? GroupKey, string ColumnKey, string ColumnLabel)> displayColumns = new(); + private string gridTemplateStyle = string.Empty; + + [Parameter, EditorRequired] + public CriticalTableDetail Detail { get; set; } = default!; + + [Parameter] + public EventCallback OnOpenCuration { get; set; } + + [Parameter] + public EventCallback OnOpenEditor { get; set; } + + protected override void OnParametersSet() + { + cellIndex.Clear(); + displayColumns.Clear(); + + foreach (var cell in Detail.Cells) + { + cellIndex[(cell.RollBand, cell.GroupKey, cell.ColumnKey)] = cell; + } + + if (Detail.Groups.Count == 0) + { + foreach (var column in Detail.Columns) + { + displayColumns.Add((null, column.Key, column.Label)); + } + } + else + { + foreach (var group in Detail.Groups) + { + foreach (var column in Detail.Columns) + { + displayColumns.Add((group.Key, column.Key, column.Label)); + } + } + } + + var dataColumnCount = Detail.Columns.Count * Math.Max(Detail.Groups.Count, 1); + gridTemplateStyle = $"grid-template-columns: max-content repeat({dataColumnCount}, minmax(0, 1fr));"; + } + + private bool TryGetCell(string rollBand, string? groupKey, string columnKey, out CriticalTableCellDetail? cell) => + cellIndex.TryGetValue((rollBand, groupKey, columnKey), out cell); + + private static string BuildColumnSpanStyle(int span) => $"grid-column: span {span};"; + + private static string GetCellCssClass(CriticalTableCellDetail cell) => + cell.IsCurated + ? "critical-table-cell is-curated" + : "critical-table-cell needs-curation"; +} diff --git a/src/RolemasterDb.App/Components/Tables/TablesContextBar.razor b/src/RolemasterDb.App/Components/Tables/TablesContextBar.razor new file mode 100644 index 0000000..e24d6e9 --- /dev/null +++ b/src/RolemasterDb.App/Components/Tables/TablesContextBar.razor @@ -0,0 +1,28 @@ +
+
+

@Detail.DisplayName

+

@GetReadingHint()

+
+
+ +

Use the curation action or edit action on any filled result.

+
+
+ +@code { + [Parameter, EditorRequired] + public CriticalTableDetail Detail { get; set; } = default!; + + [Parameter] + public bool IsPinned { get; set; } + + [Parameter] + public EventCallback OnTogglePin { get; set; } + + private string GetReadingHint() => + Detail.Groups.Count > 0 + ? "Find the roll band on the left, then read across to the group and severity you need." + : "Find the roll band on the left, then read across to the severity you need."; +} diff --git a/src/RolemasterDb.App/Components/Tables/TablesLegend.razor b/src/RolemasterDb.App/Components/Tables/TablesLegend.razor new file mode 100644 index 0000000..b707a74 --- /dev/null +++ b/src/RolemasterDb.App/Components/Tables/TablesLegend.razor @@ -0,0 +1,26 @@ +@if (LegendEntries.Count > 0) +{ +
+
+

Reading help

+

These symbols show the effects attached to a result at a glance.

+
+
+ @foreach (var entry in LegendEntries) + { +
+ @entry.Symbol +
+ @entry.Label + @entry.Description +
+
+ } +
+
+} + +@code { + [Parameter] + public IReadOnlyList LegendEntries { get; set; } = Array.Empty(); +} diff --git a/src/RolemasterDb.App/Components/Tables/TablesPageHeader.razor b/src/RolemasterDb.App/Components/Tables/TablesPageHeader.razor new file mode 100644 index 0000000..1e11ab5 --- /dev/null +++ b/src/RolemasterDb.App/Components/Tables/TablesPageHeader.razor @@ -0,0 +1,105 @@ +
+
+ +
+ + + @if (IsTableMenuOpen && ReferenceData is not null) + { + + +
+ @foreach (var table in ReferenceData.CriticalTables) + { + + } +
+ } +
+
+ +

Choose a table, then read from the roll band on the left across to the result you need.

+
+ +@code { + [Parameter] + public LookupReferenceData? ReferenceData { get; set; } + + [Parameter] + public CriticalTableReference? SelectedTableReference { get; set; } + + [Parameter] + public string SelectedTableSlug { get; set; } = string.Empty; + + [Parameter] + public bool IsTableMenuOpen { get; set; } + + [Parameter] + public bool IsTableSelectionDisabled { get; set; } + + [Parameter] + public Func? IsPinned { get; set; } + + [Parameter] + public EventCallback OnToggleTableMenu { get; set; } + + [Parameter] + public EventCallback OnCloseTableMenu { get; set; } + + [Parameter] + public EventCallback OnSelectTable { get; set; } + + private bool GetIsPinned(string tableSlug) => IsPinned?.Invoke(tableSlug) ?? false; + + private string GetSelectedTableLabel() => SelectedTableReference?.Label ?? "Select a table"; + + private string GetTableOptionCssClass(CriticalTableReference table) + { + var classes = new List(); + + if (string.Equals(table.Key, SelectedTableSlug, StringComparison.OrdinalIgnoreCase)) + { + classes.Add("is-selected"); + } + + classes.Add(table.CurationPercentage >= 100 ? "is-curated" : "needs-curation"); + return string.Join(' ', classes); + } +} diff --git a/src/RolemasterDb.App/Components/_Imports.razor b/src/RolemasterDb.App/Components/_Imports.razor index 1c0af0e..db80264 100644 --- a/src/RolemasterDb.App/Components/_Imports.razor +++ b/src/RolemasterDb.App/Components/_Imports.razor @@ -14,4 +14,5 @@ @using RolemasterDb.App.Components.Primitives @using RolemasterDb.App.Components.Shell @using RolemasterDb.App.Components.Shared +@using RolemasterDb.App.Components.Tables @using RolemasterDb.App.Components.Tools