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>
</header>
@{
var displayColumns = GetDisplayColumns(detail);
var gridTemplateStyle = BuildGridTemplateStyle(detail);
}
<div class="table-scroll">
<table class="critical-table">
<thead>
@if (detail.Groups.Count > 0)
{
<tr>
<th class="roll-band-header" rowspan="2"></th>
@foreach (var group in detail.Groups)
{
<th colspan="@detail.Columns.Count">@group.Label</th>
}
</tr>
<tr>
@foreach (var group in detail.Groups)
{
foreach (var column in detail.Columns)
{
<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>
}
<div class="critical-table-grid" role="table" aria-label="@detail.DisplayName">
@if (detail.Groups.Count > 0)
{
<div class="critical-table-grid-row critical-table-grid-group-row" role="row" style="@gridTemplateStyle">
<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"
role="columnheader"
style="@BuildColumnSpanStyle(detail.Columns.Count)">
<span>@group.Label</span>
</div>
}
</div>
}
<button
type="button"
class="critical-cell-action-button is-edit"
title="Open the full editor for this cell."
@onclick="() => OpenCellEditorAsync(groupedCell.ResultId)">
Edit
</button>
</div>
<div class="critical-table-grid-row critical-table-grid-column-row" role="row" style="@gridTemplateStyle">
<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" role="columnheader">
<span>@displayColumn.ColumnLabel</span>
</div>
}
</div>
<CompactCriticalCell
Description="@(groupedCell.Description ?? string.Empty)"
Effects="@(groupedCell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
Branches="@(groupedCell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
</div>
</td>
}
else
{
<td class="critical-table-cell">
<span class="empty-cell">—</span>
</td>
}
}
}
@foreach (var rollBand in detail.RollBands)
{
<div class="critical-table-grid-row critical-table-grid-body-row" role="row" style="@gridTemplateStyle">
<div class="critical-table-grid-header-cell critical-table-grid-roll-band" role="rowheader">@rollBand.Label</div>
@foreach (var displayColumn in displayColumns)
{
@if (TryGetCell(rollBand.Label, displayColumn.GroupKey, displayColumn.ColumnKey, out var cell))
{
@RenderCriticalTableCell(cell)
}
else
{
foreach (var column in detail.Columns)
{
@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>
}
}
@RenderEmptyCriticalTableCell()
}
</tr>
}
</tbody>
</table>
}
</div>
}
</div>
</div>
@{
@@ -319,8 +225,6 @@
private bool isReferenceDataLoading = true;
private string? detailError;
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 isEditorOpen;
private bool isEditorLoading;
@@ -381,7 +285,6 @@
{
tableDetail = null;
cellIndex = null;
tableLayoutVersion++;
return;
}
@@ -406,7 +309,6 @@
{
isDetailLoading = false;
BuildCellIndex();
tableLayoutVersion++;
}
}
@@ -440,14 +342,6 @@
// 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()
@@ -838,6 +732,28 @@
? "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: 96px repeat({dataColumnCount}, minmax(190px, 250px));";
}
private static string BuildColumnSpanStyle(int span) => $"grid-column: span {span};";
private string GetSelectedTableLabel() =>
SelectedTableReference?.Label ?? "Select a table";
@@ -872,4 +788,42 @@
classes.Add(table.CurationPercentage >= 100 ? "is-curated" : "needs-curation");
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;
}
.critical-table {
width: 100%;
border-collapse: collapse;
.critical-table-grid {
width: max-content;
min-width: 100%;
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 td {
border: 1px solid rgba(127, 96, 55, 0.2);
.critical-table-grid-row {
display: grid;
align-items: stretch;
}
.critical-table-grid-header-cell {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
padding: 0.35rem;
vertical-align: top;
}
.critical-table th {
border-right: 1px solid rgba(127, 96, 55, 0.2);
border-bottom: 1px solid rgba(127, 96, 55, 0.2);
background: rgba(238, 223, 193, 0.45);
text-transform: uppercase;
text-align: center;
vertical-align: middle;
letter-spacing: 0.08em;
box-sizing: border-box;
}
.critical-table td {
background: rgba(255, 255, 255, 0.85);
min-width: 190px;
max-width: 250px;
padding: 0.55rem;
.critical-table-grid-column-row .critical-table-grid-header-cell,
.critical-table-grid-group-row .critical-table-grid-header-cell {
font-size: 2rem;
}
.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 {
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 {
@@ -785,8 +806,8 @@ textarea {
display: flex;
flex-direction: column;
gap: 0.55rem;
height: 100%;
min-height: 100%;
flex: 1 1 auto;
min-height: 0;
}
.critical-table-cell-shell > .critical-cell {
@@ -826,23 +847,16 @@ textarea {
background: rgba(255, 248, 236, 0.98);
}
.critical-table td .critical-cell {
.critical-table-grid .critical-cell {
display: flex;
flex-direction: column;
gap: 0.25rem;
box-sizing: border-box;
}
.critical-table .roll-band-header {
width: 96px;
background: rgba(255, 247, 230, 0.52);
font-size: 1.5rem;
text-align: center;
vertical-align: middle;
}
.critical-table thead th {
font-size: 2rem;
.critical-table-cell-empty {
align-items: center;
justify-content: center;
}
.empty-cell {

View File

@@ -1,36 +1,3 @@
window.rolemasterTables = window.rolemasterTables || {
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`;
}
}
}
}
}
alignCriticalCells() {}
};
window.addEventListener("resize", () => window.rolemasterTables.alignCriticalCells());