diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md index b25a593..54ababd 100644 --- a/docs/tables_frontend_overhaul_implementation_plan.md +++ b/docs/tables_frontend_overhaul_implementation_plan.md @@ -62,6 +62,7 @@ It is intentionally implementation-focused: | 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. | +| 2026-03-21 | P3.2 | Completed | Replaced the floating table picker with a permanent left-rail layout, converted the old selector component into a real page header, and kept the current selection flow intact inside the new reference frame. | ### Lessons Learned @@ -93,6 +94,7 @@ It is intentionally implementation-focused: - 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. +- The `Tables` navigation model needs its own persistent geometry before advanced behaviors land. Converting the selector to a real rail first keeps later search and keyboard work from being tangled up with another structural rewrite. ## Target Outcomes @@ -454,7 +456,7 @@ Build the shared interaction infrastructure needed by multiple destinations befo | 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.2` | Completed | The old dropdown picker is gone; `/tables` now uses a permanent left rail and a real page header while keeping the current selection flow intact. | | `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. | diff --git a/src/RolemasterDb.App/Components/Pages/Tables.razor b/src/RolemasterDb.App/Components/Pages/Tables.razor index 99fbed4..9047abe 100644 --- a/src/RolemasterDb.App/Components/Pages/Tables.razor +++ b/src/RolemasterDb.App/Components/Pages/Tables.razor @@ -11,16 +11,7 @@ Critical Tables
- + @if (referenceData is null) { @@ -30,34 +21,49 @@ {

No critical tables are available yet.

} - else if (!hasResolvedStoredTableSelection) + else { -

Restoring table context...

- } - else if (isDetailLoading) - { -

Loading the selected table...

- } - else if (!string.IsNullOrWhiteSpace(detailError)) - { -

@detailError

- } - else if (tableDetail is null) - { -

The selected table could not be loaded.

- } - else if (tableDetail is { } detail) - { -
- +
+ - +
+ @if (!hasResolvedStoredTableSelection) + { +

Restoring table context...

+ } + else if (isDetailLoading) + { +

Loading the selected table...

+ } + else if (!string.IsNullOrWhiteSpace(detailError)) + { +

@detailError

+ } + else if (tableDetail is null) + { +

The selected table could not be loaded.

+ } + else if (tableDetail is { } detail) + { +
+ + + +
+ } +
}
@@ -127,7 +133,6 @@ private string? curationQuickParseError; private int? curatingResultId; private CriticalCellEditorModel? curationModel; - private bool isTableMenuOpen; private bool hasResolvedStoredTableSelection; private CriticalTableReference? SelectedTableReference => referenceData?.CriticalTables.FirstOrDefault(item => string.Equals(item.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase)); @@ -138,25 +143,9 @@ isReferenceDataLoading = false; } - private void ToggleTableMenu() - { - if (IsTableSelectionDisabled) - { - return; - } - - isTableMenuOpen = !isTableMenuOpen; - } - - private void CloseTableMenu() - { - isTableMenuOpen = false; - } - private async Task SelectTableAsync(string tableSlug) { selectedTableSlug = tableSlug; - isTableMenuOpen = false; await LoadTableDetailAsync(); await PersistAndSyncTableContextAsync(); } diff --git a/src/RolemasterDb.App/Components/Tables/TablesIndexRail.razor b/src/RolemasterDb.App/Components/Tables/TablesIndexRail.razor new file mode 100644 index 0000000..d39093b --- /dev/null +++ b/src/RolemasterDb.App/Components/Tables/TablesIndexRail.razor @@ -0,0 +1,59 @@ +
+
+

Table Index

+

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

+
+ +
+ @foreach (var table in Tables) + { + + } +
+
+ +@code { + [Parameter] + public IReadOnlyList Tables { get; set; } = Array.Empty(); + + [Parameter] + public string SelectedTableSlug { get; set; } = string.Empty; + + [Parameter] + public Func? IsPinned { get; set; } + + [Parameter] + public EventCallback OnSelectTable { get; set; } + + private bool GetIsPinned(string tableSlug) => IsPinned?.Invoke(tableSlug) ?? false; + + 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/Tables/TablesPageHeader.razor b/src/RolemasterDb.App/Components/Tables/TablesPageHeader.razor index 1e11ab5..8df0e8c 100644 --- a/src/RolemasterDb.App/Components/Tables/TablesPageHeader.razor +++ b/src/RolemasterDb.App/Components/Tables/TablesPageHeader.razor @@ -1,105 +1,7 @@ -
-
- -
- - - @if (IsTableMenuOpen && ReferenceData is not null) - { - - -
- @foreach (var table in ReferenceData.CriticalTables) - { - - } -
- } -
+
+
+

Reference

+

Critical Tables

+

Browse the index, open a table, and read the result directly from the grid without leaving the page.

- -

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/wwwroot/app.css b/src/RolemasterDb.App/wwwroot/app.css index c734b8c..f16c9bc 100644 --- a/src/RolemasterDb.App/wwwroot/app.css +++ b/src/RolemasterDb.App/wwwroot/app.css @@ -1115,150 +1115,148 @@ pre, gap: 1rem; } -.table-browser-toolbar { +.tables-page-header { display: grid; - gap: 0.85rem; + gap: 0.5rem; } -.table-selector { - display: flex; - flex-direction: column; +.tables-page-header-copy { + display: grid; + gap: 0.35rem; + max-width: 52rem; +} + +.tables-page-eyebrow { + margin: 0; + color: var(--accent-strong); + font-family: var(--font-ui); + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.tables-page-intro, +.tables-index-copy, +.table-browser-reading-hint, +.table-browser-edit-hint { + margin: 0; + color: var(--ink-soft); +} + +.tables-reference-layout { + display: grid; + grid-template-columns: minmax(17rem, 20rem) minmax(0, 1fr); + gap: 1rem; + align-items: start; +} + +.tables-reference-rail { + position: sticky; + top: calc(var(--shell-header-height) + 1rem); +} + +.tables-index-rail { + display: grid; + gap: 0.85rem; + padding: 1rem; + border-radius: 18px; + background: var(--surface-card-subtle); + border: 1px solid rgba(127, 96, 55, 0.16); + box-shadow: 0 16px 26px rgba(41, 22, 11, 0.06); +} + +.tables-index-rail-header { + display: grid; gap: 0.35rem; } -.table-select-shell { - position: relative; +.tables-index-title { + margin: 0; + color: var(--ink-strong); + font-family: var(--font-display); + font-size: clamp(1.1rem, 1rem + 0.4vw, 1.35rem); } -.table-select-trigger { +.tables-index-list { + display: grid; + gap: 0.35rem; +} + +.table-index-option { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; - gap: 0.5rem; + gap: 0.75rem; + width: 100%; + padding: 0.7rem 0.8rem; + border: 1px solid transparent; + border-radius: 12px; + background: rgba(255, 250, 242, 0.72); + color: inherit; text-align: left; - padding: 0.55rem 0.7rem; - border: 1px solid rgba(127, 96, 55, 0.28); - border-radius: 8px; - background: rgba(255, 252, 247, 0.96); - box-shadow: none; - appearance: none; - transition: border-color 0.16s ease, background-color 0.16s ease; + transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease; } -.table-select-trigger:hover, -.table-select-trigger:focus-visible { - border-color: rgba(184, 121, 59, 0.4); - background: rgba(255, 248, 239, 0.98); +.table-index-option:hover, +.table-index-option:focus-visible { + border-color: rgba(184, 121, 59, 0.28); + background: rgba(255, 247, 235, 0.94); outline: none; } -.table-select-trigger-copy { - display: block; +.table-index-option.is-selected { + border-color: rgba(41, 22, 11, 0.18); + background: rgba(248, 238, 221, 0.98); +} + +.table-index-option.is-curated { + background: rgba(102, 138, 83, 0.12); +} + +.table-index-option.is-curated:hover, +.table-index-option.is-curated:focus-visible { + background: rgba(102, 138, 83, 0.18); +} + +.table-index-option.needs-curation { + background: rgba(184, 121, 59, 0.12); +} + +.table-index-option.needs-curation:hover, +.table-index-option.needs-curation:focus-visible { + background: rgba(184, 121, 59, 0.18); +} + +.table-index-option-copy { + display: grid; + gap: 0.18rem; min-width: 0; } -.table-select-trigger-title { +.table-index-option-title { color: var(--ink); font-family: var(--font-ui); + font-size: 0.98rem; + font-weight: 600; } -.table-select-trigger-chips, -.table-select-option-chips { +.table-index-option-meta { + color: var(--ink-soft); + font-size: 0.8rem; +} + +.table-index-option-chips { display: flex; align-items: center; gap: 0.25rem; justify-content: flex-end; } -.table-selector-backdrop { - position: fixed; - inset: 0; - border: none; - background: transparent; - padding: 0; - z-index: 15; -} - -.table-select-menu { - position: absolute; - left: 0; - right: 0; - top: calc(100% + 0.1rem); - z-index: 20; - display: grid; - gap: 0; - padding: 0.1rem; - border-radius: 8px; - background: rgba(255, 250, 242, 0.98); - border: 1px solid rgba(127, 96, 55, 0.22); - box-shadow: 0 10px 18px rgba(41, 22, 11, 0.1); - max-height: min(28rem, 60vh); - overflow: auto; -} - -.table-select-option { - border: none; - border-radius: 0; - background: transparent; - padding: 0.35rem 0.45rem; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.45rem; - text-align: left; - transition: background-color 0.16s ease, outline-color 0.16s ease; -} - -.table-select-option:hover, -.table-select-option:focus-visible { - outline: 1px solid rgba(41, 22, 11, 0.16); - outline-offset: -1px; -} - -.table-select-option.is-selected { - outline: 1px solid rgba(41, 22, 11, 0.14); - outline-offset: -1px; -} - -.table-select-option.is-curated { - background: rgba(102, 138, 83, 0.12); -} - -.table-select-option.is-curated:hover, -.table-select-option.is-curated:focus-visible { - background: rgba(102, 138, 83, 0.18); -} - -.table-select-option.needs-curation { - background: rgba(184, 121, 59, 0.12); -} - -.table-select-option.needs-curation:hover, -.table-select-option.needs-curation:focus-visible { - background: rgba(184, 121, 59, 0.18); -} - -.table-select-option-main { - display: block; +.tables-reference-main { min-width: 0; } -.table-select-option-title { - color: var(--ink); - font-family: var(--font-ui); - font-weight: 400; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.table-browser-toolbar-copy, -.table-browser-reading-hint, -.table-browser-edit-hint { - margin: 0; - color: var(--ink-soft); -} - .table-shell { border-radius: 20px; padding: 1.2rem; @@ -1283,6 +1281,16 @@ pre, white-space: nowrap; } +@media (max-width: 1023px) { + .tables-reference-layout { + grid-template-columns: minmax(0, 1fr); + } + + .tables-reference-rail { + position: static; + } +} + .table-shell .table-scroll { overflow-x: auto; }