Refactor critical tables layout to CSS grid

This commit is contained in:
2026-03-21 11:44:37 +01:00
parent 7322b93120
commit 89f393ca35
3 changed files with 148 additions and 213 deletions

View File

@@ -97,151 +97,57 @@
<p class="table-browser-edit-hint">Use the curation action or edit action on any filled result.</p> <p class="table-browser-edit-hint">Use the curation action or edit action on any filled result.</p>
</header> </header>
@{
var displayColumns = GetDisplayColumns(detail);
var gridTemplateStyle = BuildGridTemplateStyle(detail);
}
<div class="table-scroll"> <div class="table-scroll">
<table class="critical-table"> <div class="critical-table-grid" role="table" aria-label="@detail.DisplayName">
<thead> @if (detail.Groups.Count > 0)
@if (detail.Groups.Count > 0) {
{ <div class="critical-table-grid-row critical-table-grid-group-row" role="row" style="@gridTemplateStyle">
<tr> <div class="critical-table-grid-header-cell critical-table-grid-corner" aria-hidden="true"></div>
<th class="roll-band-header" rowspan="2"></th> @foreach (var group in detail.Groups)
@foreach (var group in detail.Groups) {
{ <div
<th colspan="@detail.Columns.Count">@group.Label</th> class="critical-table-grid-header-cell critical-table-grid-group-header"
} role="columnheader"
</tr> style="@BuildColumnSpanStyle(detail.Columns.Count)">
<tr> <span>@group.Label</span>
@foreach (var group in detail.Groups) </div>
{ }
foreach (var column in detail.Columns) </div>
{ }
<th>
<span>@column.Label</span>
</th>
}
}
</tr>
}
else
{
<tr>
<th class="roll-band-header"></th>
@foreach (var column in detail.Columns)
{
<th>
<span>@column.Label</span>
</th>
}
</tr>
}
</thead>
<tbody>
@foreach (var rollBand in detail.RollBands)
{
<tr>
<th class="roll-band-header">@rollBand.Label</th>
@if (detail.Groups.Count > 0)
{
foreach (var group in detail.Groups)
{
foreach (var column in detail.Columns)
{
@if (TryGetCell(rollBand.Label, group.Key, column.Key, out var groupedCell))
{
<td class="@GetCellCssClass(groupedCell)">
<div class="critical-table-cell-shell">
<div class="critical-table-cell-actions">
@if (groupedCell.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(groupedCell.ResultId)">
Needs Curation
</button>
}
<button <div class="critical-table-grid-row critical-table-grid-column-row" role="row" style="@gridTemplateStyle">
type="button" <div class="critical-table-grid-header-cell critical-table-grid-roll-band-header" aria-hidden="true"></div>
class="critical-cell-action-button is-edit" @foreach (var displayColumn in displayColumns)
title="Open the full editor for this cell." {
@onclick="() => OpenCellEditorAsync(groupedCell.ResultId)"> <div class="critical-table-grid-header-cell critical-table-grid-column-header" role="columnheader">
Edit <span>@displayColumn.ColumnLabel</span>
</button> </div>
</div> }
</div>
<CompactCriticalCell @foreach (var rollBand in detail.RollBands)
Description="@(groupedCell.Description ?? string.Empty)" {
Effects="@(groupedCell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())" <div class="critical-table-grid-row critical-table-grid-body-row" role="row" style="@gridTemplateStyle">
Branches="@(groupedCell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" /> <div class="critical-table-grid-header-cell critical-table-grid-roll-band" role="rowheader">@rollBand.Label</div>
</div> @foreach (var displayColumn in displayColumns)
</td> {
} @if (TryGetCell(rollBand.Label, displayColumn.GroupKey, displayColumn.ColumnKey, out var cell))
else {
{ @RenderCriticalTableCell(cell)
<td class="critical-table-cell">
<span class="empty-cell">—</span>
</td>
}
}
}
} }
else else
{ {
foreach (var column in detail.Columns) @RenderEmptyCriticalTableCell()
{
@if (TryGetCell(rollBand.Label, null, column.Key, out var cell))
{
<td 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>
</td>
}
else
{
<td class="critical-table-cell">
<span class="empty-cell">—</span>
</td>
}
}
} }
</tr> }
} </div>
</tbody> }
</table> </div>
</div> </div>
@{ @{
@@ -319,8 +225,6 @@
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 Dictionary<(string RollBand, string? GroupKey, string ColumnKey), CriticalTableCellDetail>? cellIndex;
private int tableLayoutVersion;
private int appliedLayoutVersion = -1;
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;
@@ -381,7 +285,6 @@
{ {
tableDetail = null; tableDetail = null;
cellIndex = null; cellIndex = null;
tableLayoutVersion++;
return; return;
} }
@@ -406,7 +309,6 @@
{ {
isDetailLoading = false; isDetailLoading = false;
BuildCellIndex(); BuildCellIndex();
tableLayoutVersion++;
} }
} }
@@ -440,14 +342,6 @@
// During prerender localStorage is unavailable. Retry after interactive render. // During prerender localStorage is unavailable. Retry after interactive render.
} }
} }
if (tableDetail is null || appliedLayoutVersion == tableLayoutVersion)
{
return;
}
await JSRuntime.InvokeVoidAsync("rolemasterTables.alignCriticalCells");
appliedLayoutVersion = tableLayoutVersion;
} }
private void BuildCellIndex() private void BuildCellIndex()
@@ -838,6 +732,28 @@
? "critical-table-cell is-curated" ? "critical-table-cell is-curated"
: "critical-table-cell needs-curation"; : "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: 96px repeat({dataColumnCount}, minmax(190px, 250px));";
}
private static string BuildColumnSpanStyle(int span) => $"grid-column: span {span};";
private string GetSelectedTableLabel() => private string GetSelectedTableLabel() =>
SelectedTableReference?.Label ?? "Select a table"; SelectedTableReference?.Label ?? "Select a table";
@@ -872,4 +788,42 @@
classes.Add(table.CurationPercentage >= 100 ? "is-curated" : "needs-curation"); classes.Add(table.CurationPercentage >= 100 ? "is-curated" : "needs-curation");
return string.Join(' ', classes); return string.Join(' ', classes);
} }
private RenderFragment RenderCriticalTableCell(CriticalTableCellDetail cell) => @<div class="@GetCellCssClass(cell)" role="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" role="cell">
<span class="empty-cell">—</span>
</div>;
} }

View File

@@ -737,36 +737,57 @@ textarea {
overflow-x: auto; overflow-x: auto;
} }
.critical-table { .critical-table-grid {
width: 100%; width: max-content;
border-collapse: collapse; min-width: 100%;
font-size: 1.5rem; font-size: 1.5rem;
border-top: 1px solid rgba(127, 96, 55, 0.2);
border-left: 1px solid rgba(127, 96, 55, 0.2);
} }
.critical-table th, .critical-table-grid-row {
.critical-table td { display: grid;
border: 1px solid rgba(127, 96, 55, 0.2); align-items: stretch;
}
.critical-table-grid-header-cell {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
padding: 0.35rem; padding: 0.35rem;
vertical-align: top; border-right: 1px solid rgba(127, 96, 55, 0.2);
} border-bottom: 1px solid rgba(127, 96, 55, 0.2);
.critical-table th {
background: rgba(238, 223, 193, 0.45); background: rgba(238, 223, 193, 0.45);
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
vertical-align: middle;
letter-spacing: 0.08em; letter-spacing: 0.08em;
box-sizing: border-box;
} }
.critical-table td { .critical-table-grid-column-row .critical-table-grid-header-cell,
background: rgba(255, 255, 255, 0.85); .critical-table-grid-group-row .critical-table-grid-header-cell {
min-width: 190px; font-size: 2rem;
max-width: 250px; }
padding: 0.55rem;
.critical-table-grid-corner,
.critical-table-grid-roll-band-header {
background: rgba(255, 247, 230, 0.52);
}
.critical-table-grid-roll-band {
background: rgba(255, 247, 230, 0.52);
font-size: 1.5rem;
} }
.critical-table-cell { .critical-table-cell {
position: relative; position: relative;
display: flex;
min-width: 0;
padding: 0.55rem;
border-right: 1px solid rgba(127, 96, 55, 0.2);
border-bottom: 1px solid rgba(127, 96, 55, 0.2);
box-sizing: border-box;
} }
.critical-table-cell.is-curated { .critical-table-cell.is-curated {
@@ -785,8 +806,8 @@ textarea {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.55rem; gap: 0.55rem;
height: 100%; flex: 1 1 auto;
min-height: 100%; min-height: 0;
} }
.critical-table-cell-shell > .critical-cell { .critical-table-cell-shell > .critical-cell {
@@ -826,23 +847,16 @@ textarea {
background: rgba(255, 248, 236, 0.98); background: rgba(255, 248, 236, 0.98);
} }
.critical-table td .critical-cell { .critical-table-grid .critical-cell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: 0.25rem;
box-sizing: border-box; box-sizing: border-box;
} }
.critical-table .roll-band-header { .critical-table-cell-empty {
width: 96px; align-items: center;
background: rgba(255, 247, 230, 0.52); justify-content: center;
font-size: 1.5rem;
text-align: center;
vertical-align: middle;
}
.critical-table thead th {
font-size: 2rem;
} }
.empty-cell { .empty-cell {

View File

@@ -1,36 +1,3 @@
window.rolemasterTables = window.rolemasterTables || { window.rolemasterTables = window.rolemasterTables || {
alignCriticalCells() { alignCriticalCells() {}
const tables = document.querySelectorAll(".critical-table");
for (const table of tables) {
for (const shell of table.querySelectorAll("td > .critical-table-cell-shell")) {
shell.style.minHeight = "";
const criticalCell = shell.querySelector(":scope > .critical-cell");
if (criticalCell) {
criticalCell.style.minHeight = "";
}
}
for (const row of table.tBodies) {
for (const tr of row.rows) {
for (const cell of tr.cells) {
const shell = cell.querySelector(":scope > .critical-table-cell-shell");
if (!shell) {
continue;
}
const computedStyle = window.getComputedStyle(cell);
const paddingTop = Number.parseFloat(computedStyle.paddingTop) || 0;
const paddingBottom = Number.parseFloat(computedStyle.paddingBottom) || 0;
const contentHeight = Math.max(0, cell.clientHeight - paddingTop - paddingBottom);
shell.style.minHeight = `${contentHeight}px`;
}
}
}
}
}
}; };
window.addEventListener("resize", () => window.rolemasterTables.alignCriticalCells());