Split Tables page into focused components
This commit is contained in:
@@ -30,7 +30,7 @@ It is intentionally implementation-focused:
|
|||||||
|
|
||||||
- Branch: `frontend/tables-overhaul`
|
- Branch: `frontend/tables-overhaul`
|
||||||
- Last updated: `2026-03-21`
|
- Last updated: `2026-03-21`
|
||||||
- Current focus: `Phase 2`
|
- Current focus: `Phase 3`
|
||||||
- Document mode: living plan and progress log
|
- Document mode: living plan and progress log
|
||||||
|
|
||||||
### 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 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 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 | 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
|
### 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.
|
- 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.
|
- 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.
|
- 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
|
## Target Outcomes
|
||||||
|
|
||||||
@@ -372,7 +374,7 @@ Establish the shared shell, tokens, typography, and theme system that every dest
|
|||||||
|
|
||||||
### Status
|
### Status
|
||||||
|
|
||||||
`In progress`
|
`Completed`
|
||||||
|
|
||||||
### Task Progress
|
### Task Progress
|
||||||
|
|
||||||
@@ -443,6 +445,27 @@ Build the shared interaction infrastructure needed by multiple destinations befo
|
|||||||
|
|
||||||
## Phase 3: `Tables` Reference Experience
|
## 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
|
### Goal
|
||||||
|
|
||||||
Turn `/tables` into the canonical reference surface for reading and inspecting critical tables quickly.
|
Turn `/tables` into the canonical reference surface for reading and inspecting critical tables quickly.
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
@page "/tables"
|
@page "/tables"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@using System
|
@using System
|
||||||
@using System.Collections.Generic
|
|
||||||
@using System.Diagnostics.CodeAnalysis
|
|
||||||
@using System.Linq
|
@using System.Linq
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject LookupService LookupService
|
@inject LookupService LookupService
|
||||||
@@ -13,65 +11,16 @@
|
|||||||
<PageTitle>Critical Tables</PageTitle>
|
<PageTitle>Critical Tables</PageTitle>
|
||||||
|
|
||||||
<section class="panel tables-page">
|
<section class="panel tables-page">
|
||||||
<div class="table-browser-toolbar">
|
<TablesPageHeader
|
||||||
<div class="table-selector">
|
ReferenceData="referenceData"
|
||||||
<label id="critical-table-selector-label">Table</label>
|
SelectedTableReference="SelectedTableReference"
|
||||||
<div class="table-select-shell">
|
SelectedTableSlug="selectedTableSlug"
|
||||||
<button
|
IsTableMenuOpen="isTableMenuOpen"
|
||||||
type="button"
|
IsTableSelectionDisabled="IsTableSelectionDisabled"
|
||||||
class="input-shell table-select-trigger"
|
IsPinned="PinnedTablesState.IsPinned"
|
||||||
aria-haspopup="listbox"
|
OnToggleTableMenu="ToggleTableMenu"
|
||||||
aria-expanded="@isTableMenuOpen"
|
OnCloseTableMenu="CloseTableMenu"
|
||||||
aria-labelledby="critical-table-selector-label critical-table-selector-value"
|
OnSelectTable="SelectTableAsync" />
|
||||||
@onclick="ToggleTableMenu"
|
|
||||||
disabled="@IsTableSelectionDisabled">
|
|
||||||
<span class="table-select-trigger-copy">
|
|
||||||
<span id="critical-table-selector-value" class="table-select-trigger-title">@GetSelectedTableLabel()</span>
|
|
||||||
</span>
|
|
||||||
@if (SelectedTableReference is { } selected)
|
|
||||||
{
|
|
||||||
<span class="table-select-trigger-chips">
|
|
||||||
@if (PinnedTablesState.IsPinned(selected.Key))
|
|
||||||
{
|
|
||||||
<StatusChip Tone="accent">Pinned</StatusChip>
|
|
||||||
}
|
|
||||||
<span class="chip">@($"{selected.CurationPercentage}%")</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (isTableMenuOpen && referenceData is not null)
|
|
||||||
{
|
|
||||||
<button type="button" class="table-selector-backdrop" @onclick="CloseTableMenu" aria-label="Close table selector"></button>
|
|
||||||
|
|
||||||
<div class="table-select-menu" role="listbox" aria-labelledby="critical-table-selector-label">
|
|
||||||
@foreach (var table in referenceData.CriticalTables)
|
|
||||||
{
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="option"
|
|
||||||
aria-selected="@string.Equals(table.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase)"
|
|
||||||
class="table-select-option @GetTableOptionCssClass(table)"
|
|
||||||
@onclick="() => SelectTableAsync(table.Key)">
|
|
||||||
<span class="table-select-option-main">
|
|
||||||
<strong class="table-select-option-title">@table.Label</strong>
|
|
||||||
</span>
|
|
||||||
<span class="table-select-option-chips">
|
|
||||||
@if (PinnedTablesState.IsPinned(table.Key))
|
|
||||||
{
|
|
||||||
<StatusChip Tone="accent">Pinned</StatusChip>
|
|
||||||
}
|
|
||||||
<span class="chip">@($"{table.CurationPercentage}%")</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="table-browser-toolbar-copy">Choose a table, then read from the roll band on the left across to the result you need.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (referenceData is null)
|
@if (referenceData is null)
|
||||||
{
|
{
|
||||||
@@ -99,95 +48,16 @@
|
|||||||
}
|
}
|
||||||
else if (tableDetail is { } detail)
|
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.";
|
|
||||||
|
|
||||||
<div class="table-shell">
|
<div class="table-shell">
|
||||||
<header class="table-browser-header">
|
<TablesContextBar
|
||||||
<div>
|
Detail="detail"
|
||||||
<h2 class="panel-title">@detail.DisplayName</h2>
|
IsPinned="PinnedTablesState.IsPinned(detail.Slug)"
|
||||||
<p class="table-browser-reading-hint">@readingHint</p>
|
OnTogglePin="TogglePinnedTableAsync" />
|
||||||
</div>
|
|
||||||
<div class="action-row">
|
|
||||||
<button type="button" class="btn btn-link" @onclick="TogglePinnedTableAsync">
|
|
||||||
@(PinnedTablesState.IsPinned(detail.Slug) ? "Unpin table" : "Pin table")
|
|
||||||
</button>
|
|
||||||
<p class="table-browser-edit-hint">Use the curation action or edit action on any filled result.</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
@{
|
<TablesCanvas
|
||||||
var displayColumns = GetDisplayColumns(detail);
|
Detail="detail"
|
||||||
var gridTemplateStyle = BuildGridTemplateStyle(detail);
|
OnOpenCuration="OpenCellCurationAsync"
|
||||||
}
|
OnOpenEditor="OpenCellEditorAsync" />
|
||||||
|
|
||||||
<div class="table-scroll">
|
|
||||||
<div class="critical-table-grid" role="group" aria-label="@detail.DisplayName" style="@gridTemplateStyle">
|
|
||||||
@if (detail.Groups.Count > 0)
|
|
||||||
{
|
|
||||||
<div class="critical-table-grid-header-cell critical-table-grid-corner" aria-hidden="true"></div>
|
|
||||||
@foreach (var group in detail.Groups)
|
|
||||||
{
|
|
||||||
<div
|
|
||||||
class="critical-table-grid-header-cell critical-table-grid-group-header"
|
|
||||||
style="@BuildColumnSpanStyle(detail.Columns.Count)">
|
|
||||||
<span>@group.Label</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="critical-table-grid-header-cell critical-table-grid-roll-band-header" aria-hidden="true"></div>
|
|
||||||
@foreach (var displayColumn in displayColumns)
|
|
||||||
{
|
|
||||||
<div class="critical-table-grid-header-cell critical-table-grid-column-header">
|
|
||||||
<span>@displayColumn.ColumnLabel</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@foreach (var rollBand in detail.RollBands)
|
|
||||||
{
|
|
||||||
<div class="critical-table-grid-header-cell critical-table-grid-roll-band">@rollBand.Label</div>
|
|
||||||
@foreach (var displayColumn in displayColumns)
|
|
||||||
{
|
|
||||||
@if (TryGetCell(rollBand.Label, displayColumn.GroupKey, displayColumn.ColumnKey, out var cell))
|
|
||||||
{
|
|
||||||
@RenderCriticalTableCell(cell)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@RenderEmptyCriticalTableCell()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@{
|
|
||||||
var legendEntries = detail.Legend ?? Array.Empty<CriticalTableLegendEntry>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (legendEntries.Count > 0)
|
|
||||||
{
|
|
||||||
<div class="critical-legend">
|
|
||||||
<div class="critical-legend-header">
|
|
||||||
<h4>Reading help</h4>
|
|
||||||
<p class="muted">These symbols show the effects attached to a result at a glance.</p>
|
|
||||||
</div>
|
|
||||||
<div class="legend-grid">
|
|
||||||
@foreach (var entry in legendEntries)
|
|
||||||
{
|
|
||||||
<div class="legend-item" title="@entry.Tooltip">
|
|
||||||
<span class="legend-symbol">@entry.Symbol</span>
|
|
||||||
<div>
|
|
||||||
<strong>@entry.Label</strong>
|
|
||||||
<span class="muted">@entry.Description</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
@@ -237,7 +107,6 @@
|
|||||||
private bool isDetailLoading;
|
private bool isDetailLoading;
|
||||||
private bool isReferenceDataLoading = true;
|
private bool isReferenceDataLoading = true;
|
||||||
private string? detailError;
|
private string? detailError;
|
||||||
private Dictionary<(string RollBand, string? GroupKey, string ColumnKey), CriticalTableCellDetail>? cellIndex;
|
|
||||||
private bool IsTableSelectionDisabled => isReferenceDataLoading || (referenceData?.CriticalTables.Count ?? 0) == 0;
|
private bool IsTableSelectionDisabled => isReferenceDataLoading || (referenceData?.CriticalTables.Count ?? 0) == 0;
|
||||||
private bool isEditorOpen;
|
private bool isEditorOpen;
|
||||||
private bool isEditorLoading;
|
private bool isEditorLoading;
|
||||||
@@ -297,14 +166,12 @@
|
|||||||
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
||||||
{
|
{
|
||||||
tableDetail = null;
|
tableDetail = null;
|
||||||
cellIndex = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isDetailLoading = true;
|
isDetailLoading = true;
|
||||||
detailError = null;
|
detailError = null;
|
||||||
tableDetail = null;
|
tableDetail = null;
|
||||||
cellIndex = null;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -324,7 +191,6 @@
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isDetailLoading = false;
|
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)
|
private async Task OpenCellEditorAsync(int resultId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
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()
|
private Task TogglePinnedTableAsync()
|
||||||
{
|
{
|
||||||
if (SelectedTableReference is not { } selectedTable)
|
if (SelectedTableReference is not { } selectedTable)
|
||||||
@@ -820,55 +630,4 @@
|
|||||||
new(
|
new(
|
||||||
TableSlug: selectedTableSlug,
|
TableSlug: selectedTableSlug,
|
||||||
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
|
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
|
||||||
|
|
||||||
private string GetTableOptionCssClass(CriticalTableReference table)
|
|
||||||
{
|
|
||||||
var classes = new List<string>();
|
|
||||||
|
|
||||||
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) => @<div class="@GetCellCssClass(cell)">
|
|
||||||
<div class="critical-table-cell-shell">
|
|
||||||
<div class="critical-table-cell-actions">
|
|
||||||
@if (cell.IsCurated)
|
|
||||||
{
|
|
||||||
<span class="critical-cell-status-chip is-curated">Curated</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="critical-cell-action-button is-curation"
|
|
||||||
title="Open the curation preview for this cell."
|
|
||||||
@onclick="() => OpenCellCurationAsync(cell.ResultId)">
|
|
||||||
Needs Curation
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="critical-cell-action-button is-edit"
|
|
||||||
title="Open the full editor for this cell."
|
|
||||||
@onclick="() => OpenCellEditorAsync(cell.ResultId)">
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CompactCriticalCell
|
|
||||||
Description="@(cell.Description ?? string.Empty)"
|
|
||||||
Effects="@(cell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
|
|
||||||
Branches="@(cell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
private static RenderFragment RenderEmptyCriticalTableCell() => @<div class="critical-table-cell critical-table-cell-empty">
|
|
||||||
<span class="empty-cell">—</span>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
135
src/RolemasterDb.App/Components/Tables/TablesCanvas.razor
Normal file
135
src/RolemasterDb.App/Components/Tables/TablesCanvas.razor
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<div class="table-scroll">
|
||||||
|
<div class="critical-table-grid" role="group" aria-label="@Detail.DisplayName" style="@gridTemplateStyle">
|
||||||
|
@if (Detail.Groups.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="critical-table-grid-header-cell critical-table-grid-corner" aria-hidden="true"></div>
|
||||||
|
@foreach (var group in Detail.Groups)
|
||||||
|
{
|
||||||
|
<div
|
||||||
|
class="critical-table-grid-header-cell critical-table-grid-group-header"
|
||||||
|
style="@BuildColumnSpanStyle(Detail.Columns.Count)">
|
||||||
|
<span>@group.Label</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="critical-table-grid-header-cell critical-table-grid-roll-band-header" aria-hidden="true"></div>
|
||||||
|
@foreach (var displayColumn in displayColumns)
|
||||||
|
{
|
||||||
|
<div class="critical-table-grid-header-cell critical-table-grid-column-header">
|
||||||
|
<span>@displayColumn.ColumnLabel</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var rollBand in Detail.RollBands)
|
||||||
|
{
|
||||||
|
<div class="critical-table-grid-header-cell critical-table-grid-roll-band">@rollBand.Label</div>
|
||||||
|
@foreach (var displayColumn in displayColumns)
|
||||||
|
{
|
||||||
|
if (TryGetCell(rollBand.Label, displayColumn.GroupKey, displayColumn.ColumnKey, out var resolvedCell) && resolvedCell is not null)
|
||||||
|
{
|
||||||
|
var cell = resolvedCell;
|
||||||
|
|
||||||
|
<div class="@GetCellCssClass(cell)">
|
||||||
|
<div class="critical-table-cell-shell">
|
||||||
|
<div class="critical-table-cell-actions">
|
||||||
|
@if (cell.IsCurated)
|
||||||
|
{
|
||||||
|
<span class="critical-cell-status-chip is-curated">Curated</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="critical-cell-action-button is-curation"
|
||||||
|
title="Open the curation preview for this cell."
|
||||||
|
@onclick="() => OnOpenCuration.InvokeAsync(cell.ResultId)">
|
||||||
|
Needs Curation
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="critical-cell-action-button is-edit"
|
||||||
|
title="Open the full editor for this cell."
|
||||||
|
@onclick="() => OnOpenEditor.InvokeAsync(cell.ResultId)">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CompactCriticalCell
|
||||||
|
Description="@(cell.Description ?? string.Empty)"
|
||||||
|
Effects="@(cell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
|
||||||
|
Branches="@(cell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="critical-table-cell critical-table-cell-empty">
|
||||||
|
<span class="empty-cell">—</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TablesLegend LegendEntries="@(Detail.Legend ?? Array.Empty<CriticalTableLegendEntry>())" />
|
||||||
|
|
||||||
|
@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<int> OnOpenCuration { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<int> 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";
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<header class="table-browser-header">
|
||||||
|
<div>
|
||||||
|
<h2 class="panel-title">@Detail.DisplayName</h2>
|
||||||
|
<p class="table-browser-reading-hint">@GetReadingHint()</p>
|
||||||
|
</div>
|
||||||
|
<div class="action-row">
|
||||||
|
<button type="button" class="btn btn-link" @onclick="() => OnTogglePin.InvokeAsync()">
|
||||||
|
@(IsPinned ? "Unpin table" : "Pin table")
|
||||||
|
</button>
|
||||||
|
<p class="table-browser-edit-hint">Use the curation action or edit action on any filled result.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@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.";
|
||||||
|
}
|
||||||
26
src/RolemasterDb.App/Components/Tables/TablesLegend.razor
Normal file
26
src/RolemasterDb.App/Components/Tables/TablesLegend.razor
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@if (LegendEntries.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="critical-legend">
|
||||||
|
<div class="critical-legend-header">
|
||||||
|
<h4>Reading help</h4>
|
||||||
|
<p class="muted">These symbols show the effects attached to a result at a glance.</p>
|
||||||
|
</div>
|
||||||
|
<div class="legend-grid">
|
||||||
|
@foreach (var entry in LegendEntries)
|
||||||
|
{
|
||||||
|
<div class="legend-item" title="@entry.Tooltip">
|
||||||
|
<span class="legend-symbol">@entry.Symbol</span>
|
||||||
|
<div>
|
||||||
|
<strong>@entry.Label</strong>
|
||||||
|
<span class="muted">@entry.Description</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<CriticalTableLegendEntry> LegendEntries { get; set; } = Array.Empty<CriticalTableLegendEntry>();
|
||||||
|
}
|
||||||
105
src/RolemasterDb.App/Components/Tables/TablesPageHeader.razor
Normal file
105
src/RolemasterDb.App/Components/Tables/TablesPageHeader.razor
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<div class="table-browser-toolbar">
|
||||||
|
<div class="table-selector">
|
||||||
|
<label id="critical-table-selector-label">Table</label>
|
||||||
|
<div class="table-select-shell">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="input-shell table-select-trigger"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded="@IsTableMenuOpen"
|
||||||
|
aria-labelledby="critical-table-selector-label critical-table-selector-value"
|
||||||
|
@onclick="() => OnToggleTableMenu.InvokeAsync()"
|
||||||
|
disabled="@IsTableSelectionDisabled">
|
||||||
|
<span class="table-select-trigger-copy">
|
||||||
|
<span id="critical-table-selector-value" class="table-select-trigger-title">@GetSelectedTableLabel()</span>
|
||||||
|
</span>
|
||||||
|
@if (SelectedTableReference is { } selected)
|
||||||
|
{
|
||||||
|
<span class="table-select-trigger-chips">
|
||||||
|
@if (GetIsPinned(selected.Key))
|
||||||
|
{
|
||||||
|
<StatusChip Tone="accent">Pinned</StatusChip>
|
||||||
|
}
|
||||||
|
<span class="chip">@($"{selected.CurationPercentage}%")</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (IsTableMenuOpen && ReferenceData is not null)
|
||||||
|
{
|
||||||
|
<button type="button" class="table-selector-backdrop" @onclick="() => OnCloseTableMenu.InvokeAsync()" aria-label="Close table selector"></button>
|
||||||
|
|
||||||
|
<div class="table-select-menu" role="listbox" aria-labelledby="critical-table-selector-label">
|
||||||
|
@foreach (var table in ReferenceData.CriticalTables)
|
||||||
|
{
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected="@string.Equals(table.Key, SelectedTableSlug, StringComparison.OrdinalIgnoreCase)"
|
||||||
|
class="table-select-option @GetTableOptionCssClass(table)"
|
||||||
|
@onclick="() => OnSelectTable.InvokeAsync(table.Key)">
|
||||||
|
<span class="table-select-option-main">
|
||||||
|
<strong class="table-select-option-title">@table.Label</strong>
|
||||||
|
</span>
|
||||||
|
<span class="table-select-option-chips">
|
||||||
|
@if (GetIsPinned(table.Key))
|
||||||
|
{
|
||||||
|
<StatusChip Tone="accent">Pinned</StatusChip>
|
||||||
|
}
|
||||||
|
<span class="chip">@($"{table.CurationPercentage}%")</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="table-browser-toolbar-copy">Choose a table, then read from the roll band on the left across to the result you need.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@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<string, bool>? IsPinned { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback OnToggleTableMenu { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback OnCloseTableMenu { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<string> 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<string>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,4 +14,5 @@
|
|||||||
@using RolemasterDb.App.Components.Primitives
|
@using RolemasterDb.App.Components.Primitives
|
||||||
@using RolemasterDb.App.Components.Shell
|
@using RolemasterDb.App.Components.Shell
|
||||||
@using RolemasterDb.App.Components.Shared
|
@using RolemasterDb.App.Components.Shared
|
||||||
|
@using RolemasterDb.App.Components.Tables
|
||||||
@using RolemasterDb.App.Components.Tools
|
@using RolemasterDb.App.Components.Tools
|
||||||
|
|||||||
Reference in New Issue
Block a user