Add sticky tables context controls
This commit is contained in:
@@ -64,6 +64,7 @@ It is intentionally implementation-focused:
|
|||||||
| 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.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. |
|
| 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. |
|
||||||
| 2026-03-21 | P3.3 | Completed | Added rail search, family filters, pinned and recent sections, curated status chips, and keyboard up/down plus Enter handling on top of the new permanent table index rail. |
|
| 2026-03-21 | P3.3 | Completed | Added rail search, family filters, pinned and recent sections, curated status chips, and keyboard up/down plus Enter handling on top of the new permanent table index rail. |
|
||||||
|
| 2026-03-21 | P3.4 | Completed | Added a sticky context bar with reference-mode tabs, variant and severity selectors, roll-jump state, and active filter chips, then wired those controls into page state and canvas filtering. |
|
||||||
|
|
||||||
### Lessons Learned
|
### Lessons Learned
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ It is intentionally implementation-focused:
|
|||||||
- 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` 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.
|
- 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.
|
||||||
- Rail keyboard behavior is easiest to maintain when it works from one deduplicated option order, even if the UI shows multiple sections. Keeping one internal option list avoids separate arrow-key state per section.
|
- Rail keyboard behavior is easiest to maintain when it works from one deduplicated option order, even if the UI shows multiple sections. Keeping one internal option list avoids separate arrow-key state per section.
|
||||||
|
- The context bar controls should own one shared view-state model before the canvas gets more visual treatment. Wiring the filters into the host page now avoids a second refactor when row, column, and cell emphasis land.
|
||||||
|
|
||||||
## Target Outcomes
|
## Target Outcomes
|
||||||
|
|
||||||
@@ -460,7 +462,7 @@ Build the shared interaction infrastructure needed by multiple destinations befo
|
|||||||
| `P3.1` | Completed | `Tables.razor` now acts as the stateful host while selector/header/canvas/legend rendering lives in dedicated `Components/Tables` components. |
|
| `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` | 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.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` | Completed | The rail now supports search-as-you-type, family filters, pinned and recent sections, curated status chips, and a deduplicated arrow/Enter keyboard path. |
|
| `P3.3` | Completed | The rail now supports search-as-you-type, family filters, pinned and recent sections, curated status chips, and a deduplicated arrow/Enter keyboard path. |
|
||||||
| `P3.4` | Pending | Introduce the sticky context bar with roll jump and mode/filter controls. |
|
| `P3.4` | Completed | The table surface now has a sticky context bar with mode tabs, variant/severity focus, roll-jump state, and active filter chips wired into host-page view state. |
|
||||||
| `P3.5` | Pending | Rework the canvas for sticky headers, sticky roll bands, stronger reading emphasis, and density control. |
|
| `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.6` | Pending | Remove visible resting-state action stacks from non-selected cells. |
|
||||||
| `P3.7` | Pending | Add the desktop selection-driven inspector. |
|
| `P3.7` | Pending | Add the desktop selection-driven inspector. |
|
||||||
|
|||||||
@@ -57,10 +57,21 @@
|
|||||||
<TablesContextBar
|
<TablesContextBar
|
||||||
Detail="detail"
|
Detail="detail"
|
||||||
IsPinned="PinnedTablesState.IsPinned(detail.Slug)"
|
IsPinned="PinnedTablesState.IsPinned(detail.Slug)"
|
||||||
OnTogglePin="TogglePinnedTableAsync" />
|
CurrentMode="referenceMode"
|
||||||
|
SelectedGroupKey="selectedGroupKey"
|
||||||
|
SelectedColumnKey="selectedColumnKey"
|
||||||
|
RollJumpValue="rollJumpValue"
|
||||||
|
OnTogglePin="TogglePinnedTableAsync"
|
||||||
|
OnModeChanged="UpdateReferenceModeAsync"
|
||||||
|
OnGroupChanged="UpdateSelectedGroupAsync"
|
||||||
|
OnColumnChanged="UpdateSelectedColumnAsync"
|
||||||
|
OnRollJumpChanged="UpdateRollJumpAsync" />
|
||||||
|
|
||||||
<TablesCanvas
|
<TablesCanvas
|
||||||
Detail="detail"
|
Detail="detail"
|
||||||
|
CurrentMode="referenceMode"
|
||||||
|
SelectedGroupKey="selectedGroupKey"
|
||||||
|
SelectedColumnKey="selectedColumnKey"
|
||||||
OnOpenCuration="OpenCellCurationAsync"
|
OnOpenCuration="OpenCellCurationAsync"
|
||||||
OnOpenEditor="OpenCellEditorAsync" />
|
OnOpenEditor="OpenCellEditorAsync" />
|
||||||
</div>
|
</div>
|
||||||
@@ -135,6 +146,10 @@
|
|||||||
private string? curationQuickParseError;
|
private string? curationQuickParseError;
|
||||||
private int? curatingResultId;
|
private int? curatingResultId;
|
||||||
private CriticalCellEditorModel? curationModel;
|
private CriticalCellEditorModel? curationModel;
|
||||||
|
private string referenceMode = TablesReferenceMode.Reference;
|
||||||
|
private string selectedGroupKey = string.Empty;
|
||||||
|
private string selectedColumnKey = string.Empty;
|
||||||
|
private string rollJumpValue = string.Empty;
|
||||||
private bool hasResolvedStoredTableSelection;
|
private bool hasResolvedStoredTableSelection;
|
||||||
private CriticalTableReference? SelectedTableReference =>
|
private CriticalTableReference? SelectedTableReference =>
|
||||||
referenceData?.CriticalTables.FirstOrDefault(item => string.Equals(item.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase));
|
referenceData?.CriticalTables.FirstOrDefault(item => string.Equals(item.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -170,14 +185,17 @@
|
|||||||
if (tableDetail is null)
|
if (tableDetail is null)
|
||||||
{
|
{
|
||||||
detailError = "The selected table could not be loaded.";
|
detailError = "The selected table could not be loaded.";
|
||||||
|
NormalizeViewStateForCurrentDetail();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await RecordRecentTableVisitAsync();
|
await RecordRecentTableVisitAsync();
|
||||||
|
NormalizeViewStateForCurrentDetail();
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
detailError = exception.Message;
|
detailError = exception.Message;
|
||||||
|
NormalizeViewStateForCurrentDetail();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -621,4 +639,75 @@
|
|||||||
new(
|
new(
|
||||||
TableSlug: selectedTableSlug,
|
TableSlug: selectedTableSlug,
|
||||||
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
|
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
|
||||||
|
|
||||||
|
private Task UpdateReferenceModeAsync(string mode)
|
||||||
|
{
|
||||||
|
referenceMode = NormalizeMode(mode);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task UpdateSelectedGroupAsync(string groupKey)
|
||||||
|
{
|
||||||
|
selectedGroupKey = NormalizeOptionalFilter(groupKey);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task UpdateSelectedColumnAsync(string columnKey)
|
||||||
|
{
|
||||||
|
selectedColumnKey = NormalizeOptionalFilter(columnKey);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task UpdateRollJumpAsync(string rollValue)
|
||||||
|
{
|
||||||
|
rollJumpValue = NormalizeRollInput(rollValue);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NormalizeViewStateForCurrentDetail()
|
||||||
|
{
|
||||||
|
referenceMode = NormalizeMode(referenceMode);
|
||||||
|
|
||||||
|
if (tableDetail is null)
|
||||||
|
{
|
||||||
|
selectedGroupKey = string.Empty;
|
||||||
|
selectedColumnKey = string.Empty;
|
||||||
|
rollJumpValue = string.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableDetail.Groups.All(group => !string.Equals(group.Key, selectedGroupKey, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
selectedGroupKey = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableDetail.Columns.All(column => !string.Equals(column.Key, selectedColumnKey, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
selectedColumnKey = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
rollJumpValue = NormalizeRollInput(rollJumpValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeMode(string? mode) =>
|
||||||
|
mode switch
|
||||||
|
{
|
||||||
|
TablesReferenceMode.NeedsCuration => TablesReferenceMode.NeedsCuration,
|
||||||
|
TablesReferenceMode.Curated => TablesReferenceMode.Curated,
|
||||||
|
_ => TablesReferenceMode.Reference
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string NormalizeOptionalFilter(string? value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
|
||||||
|
|
||||||
|
private static string NormalizeRollInput(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var digitsOnly = new string(value.Where(char.IsDigit).ToArray());
|
||||||
|
return digitsOnly.Length == 0 ? string.Empty : digitsOnly;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
@if (Detail.Groups.Count > 0)
|
@if (Detail.Groups.Count > 0)
|
||||||
{
|
{
|
||||||
<div class="critical-table-grid-header-cell critical-table-grid-corner" aria-hidden="true"></div>
|
<div class="critical-table-grid-header-cell critical-table-grid-corner" aria-hidden="true"></div>
|
||||||
@foreach (var group in Detail.Groups)
|
@foreach (var group in visibleGroups)
|
||||||
{
|
{
|
||||||
<div
|
<div
|
||||||
class="critical-table-grid-header-cell critical-table-grid-group-header"
|
class="critical-table-grid-header-cell critical-table-grid-group-header"
|
||||||
style="@BuildColumnSpanStyle(Detail.Columns.Count)">
|
style="@BuildColumnSpanStyle(visibleColumns.Count)">
|
||||||
<span>@group.Label</span>
|
<span>@group.Label</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
{
|
{
|
||||||
var cell = resolvedCell;
|
var cell = resolvedCell;
|
||||||
|
|
||||||
|
@if (MatchesModeFilter(cell))
|
||||||
|
{
|
||||||
<div class="@GetCellCssClass(cell)">
|
<div class="@GetCellCssClass(cell)">
|
||||||
<div class="critical-table-cell-shell">
|
<div class="critical-table-cell-shell">
|
||||||
<div class="critical-table-cell-actions">
|
<div class="critical-table-cell-actions">
|
||||||
@@ -65,6 +67,13 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
<div class="critical-table-cell critical-table-cell-empty tables-filtered-cell">
|
||||||
|
<span class="empty-cell">Filtered</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
<div class="critical-table-cell critical-table-cell-empty">
|
<div class="critical-table-cell critical-table-cell-empty">
|
||||||
<span class="empty-cell">—</span>
|
<span class="empty-cell">—</span>
|
||||||
@@ -80,11 +89,22 @@
|
|||||||
@code {
|
@code {
|
||||||
private readonly Dictionary<(string RollBand, string? GroupKey, string ColumnKey), CriticalTableCellDetail> cellIndex = new();
|
private readonly Dictionary<(string RollBand, string? GroupKey, string ColumnKey), CriticalTableCellDetail> cellIndex = new();
|
||||||
private readonly List<(string? GroupKey, string ColumnKey, string ColumnLabel)> displayColumns = new();
|
private readonly List<(string? GroupKey, string ColumnKey, string ColumnLabel)> displayColumns = new();
|
||||||
|
private readonly List<CriticalGroupReference> visibleGroups = new();
|
||||||
|
private readonly List<CriticalColumnReference> visibleColumns = new();
|
||||||
private string gridTemplateStyle = string.Empty;
|
private string gridTemplateStyle = string.Empty;
|
||||||
|
|
||||||
[Parameter, EditorRequired]
|
[Parameter, EditorRequired]
|
||||||
public CriticalTableDetail Detail { get; set; } = default!;
|
public CriticalTableDetail Detail { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string CurrentMode { get; set; } = TablesReferenceMode.Reference;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string SelectedGroupKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string SelectedColumnKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<int> OnOpenCuration { get; set; }
|
public EventCallback<int> OnOpenCuration { get; set; }
|
||||||
|
|
||||||
@@ -95,6 +115,8 @@
|
|||||||
{
|
{
|
||||||
cellIndex.Clear();
|
cellIndex.Clear();
|
||||||
displayColumns.Clear();
|
displayColumns.Clear();
|
||||||
|
visibleGroups.Clear();
|
||||||
|
visibleColumns.Clear();
|
||||||
|
|
||||||
foreach (var cell in Detail.Cells)
|
foreach (var cell in Detail.Cells)
|
||||||
{
|
{
|
||||||
@@ -103,29 +125,56 @@
|
|||||||
|
|
||||||
if (Detail.Groups.Count == 0)
|
if (Detail.Groups.Count == 0)
|
||||||
{
|
{
|
||||||
foreach (var column in Detail.Columns)
|
foreach (var column in Detail.Columns.Where(MatchesColumnFilter))
|
||||||
{
|
{
|
||||||
|
visibleColumns.Add(column);
|
||||||
displayColumns.Add((null, column.Key, column.Label));
|
displayColumns.Add((null, column.Key, column.Label));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var group in Detail.Groups)
|
foreach (var group in Detail.Groups.Where(MatchesGroupFilter))
|
||||||
{
|
{
|
||||||
foreach (var column in Detail.Columns)
|
visibleGroups.Add(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var column in Detail.Columns.Where(MatchesColumnFilter))
|
||||||
|
{
|
||||||
|
visibleColumns.Add(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in visibleGroups)
|
||||||
|
{
|
||||||
|
foreach (var column in visibleColumns)
|
||||||
{
|
{
|
||||||
displayColumns.Add((group.Key, column.Key, column.Label));
|
displayColumns.Add((group.Key, column.Key, column.Label));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dataColumnCount = Detail.Columns.Count * Math.Max(Detail.Groups.Count, 1);
|
var dataColumnCount = displayColumns.Count;
|
||||||
gridTemplateStyle = $"grid-template-columns: max-content repeat({dataColumnCount}, minmax(0, 1fr));";
|
gridTemplateStyle = $"grid-template-columns: max-content repeat({dataColumnCount}, minmax(0, 1fr));";
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetCell(string rollBand, string? groupKey, string columnKey, out CriticalTableCellDetail? cell) =>
|
private bool TryGetCell(string rollBand, string? groupKey, string columnKey, out CriticalTableCellDetail? cell) =>
|
||||||
cellIndex.TryGetValue((rollBand, groupKey, columnKey), out cell);
|
cellIndex.TryGetValue((rollBand, groupKey, columnKey), out cell);
|
||||||
|
|
||||||
|
private bool MatchesGroupFilter(CriticalGroupReference group) =>
|
||||||
|
string.IsNullOrWhiteSpace(SelectedGroupKey)
|
||||||
|
|| string.Equals(group.Key, SelectedGroupKey, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private bool MatchesColumnFilter(CriticalColumnReference column) =>
|
||||||
|
string.IsNullOrWhiteSpace(SelectedColumnKey)
|
||||||
|
|| string.Equals(column.Key, SelectedColumnKey, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private bool MatchesModeFilter(CriticalTableCellDetail cell) =>
|
||||||
|
CurrentMode switch
|
||||||
|
{
|
||||||
|
TablesReferenceMode.NeedsCuration => !cell.IsCurated,
|
||||||
|
TablesReferenceMode.Curated => cell.IsCurated,
|
||||||
|
_ => true
|
||||||
|
};
|
||||||
|
|
||||||
private static string BuildColumnSpanStyle(int span) => $"grid-column: span {span};";
|
private static string BuildColumnSpanStyle(int span) => $"grid-column: span {span};";
|
||||||
|
|
||||||
private static string GetCellCssClass(CriticalTableCellDetail cell) =>
|
private static string GetCellCssClass(CriticalTableCellDetail cell) =>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<header class="table-browser-header">
|
<header class="table-browser-header tables-context-bar">
|
||||||
|
<div class="tables-context-primary">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="panel-title">@Detail.DisplayName</h2>
|
<h2 class="panel-title">@Detail.DisplayName</h2>
|
||||||
<p class="table-browser-reading-hint">@GetReadingHint()</p>
|
<p class="table-browser-reading-hint">@GetReadingHint()</p>
|
||||||
@@ -7,22 +8,166 @@
|
|||||||
<button type="button" class="btn btn-link" @onclick="() => OnTogglePin.InvokeAsync()">
|
<button type="button" class="btn btn-link" @onclick="() => OnTogglePin.InvokeAsync()">
|
||||||
@(IsPinned ? "Unpin table" : "Pin table")
|
@(IsPinned ? "Unpin table" : "Pin table")
|
||||||
</button>
|
</button>
|
||||||
<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 inspector to inspect, curate, or edit the selected result.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tables-context-controls">
|
||||||
|
<SegmentedTabs
|
||||||
|
Items="modeTabs"
|
||||||
|
SelectedValue="CurrentMode"
|
||||||
|
SelectedValueChanged="OnModeChanged"
|
||||||
|
AriaLabel="Reference mode"
|
||||||
|
CssClass="tables-context-mode-tabs" />
|
||||||
|
|
||||||
|
<div class="tables-context-fields">
|
||||||
|
@if (Detail.Groups.Count > 1)
|
||||||
|
{
|
||||||
|
<label class="tables-context-field">
|
||||||
|
<span>Variant</span>
|
||||||
|
<select class="input-shell" value="@SelectedGroupKey" @onchange="HandleGroupChanged">
|
||||||
|
<option value="">All variants</option>
|
||||||
|
@foreach (var group in Detail.Groups)
|
||||||
|
{
|
||||||
|
<option value="@group.Key">@group.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Detail.Columns.Count > 1)
|
||||||
|
{
|
||||||
|
<label class="tables-context-field">
|
||||||
|
<span>Severity focus</span>
|
||||||
|
<select class="input-shell" value="@SelectedColumnKey" @onchange="HandleColumnChanged">
|
||||||
|
<option value="">All severities</option>
|
||||||
|
@foreach (var column in Detail.Columns)
|
||||||
|
{
|
||||||
|
<option value="@column.Key">@column.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
|
||||||
|
<label class="tables-context-field">
|
||||||
|
<span>Roll jump</span>
|
||||||
|
<input
|
||||||
|
class="input-shell"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
placeholder="e.g. 66"
|
||||||
|
value="@RollJumpValue"
|
||||||
|
@oninput="HandleRollJumpChanged" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (HasActiveFilters())
|
||||||
|
{
|
||||||
|
<div class="tables-context-filter-chips" aria-label="Active table filters">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(SelectedGroupLabel))
|
||||||
|
{
|
||||||
|
<button type="button" class="tables-context-filter-chip" @onclick="() => OnGroupChanged.InvokeAsync(string.Empty)">
|
||||||
|
Variant: @SelectedGroupLabel
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(SelectedColumnLabel))
|
||||||
|
{
|
||||||
|
<button type="button" class="tables-context-filter-chip" @onclick="() => OnColumnChanged.InvokeAsync(string.Empty)">
|
||||||
|
Severity: @SelectedColumnLabel
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(RollJumpValue))
|
||||||
|
{
|
||||||
|
<button type="button" class="tables-context-filter-chip" @onclick="() => OnRollJumpChanged.InvokeAsync(string.Empty)">
|
||||||
|
Roll: @RollJumpValue
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.Equals(CurrentMode, TablesReferenceMode.Reference, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
<button type="button" class="tables-context-filter-chip" @onclick="() => OnModeChanged.InvokeAsync(TablesReferenceMode.Reference)">
|
||||||
|
Mode: @GetModeLabel(CurrentMode)
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private readonly IReadOnlyList<SegmentedTabItem> modeTabs =
|
||||||
|
[
|
||||||
|
new(TablesReferenceMode.Reference, "Reference"),
|
||||||
|
new(TablesReferenceMode.NeedsCuration, "Needs Curation"),
|
||||||
|
new(TablesReferenceMode.Curated, "Curated")
|
||||||
|
];
|
||||||
|
|
||||||
[Parameter, EditorRequired]
|
[Parameter, EditorRequired]
|
||||||
public CriticalTableDetail Detail { get; set; } = default!;
|
public CriticalTableDetail Detail { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsPinned { get; set; }
|
public bool IsPinned { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string CurrentMode { get; set; } = TablesReferenceMode.Reference;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string SelectedGroupKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string SelectedColumnKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string RollJumpValue { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback OnTogglePin { get; set; }
|
public EventCallback OnTogglePin { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<string> OnModeChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<string> OnGroupChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<string> OnColumnChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<string> OnRollJumpChanged { get; set; }
|
||||||
|
|
||||||
private string GetReadingHint() =>
|
private string GetReadingHint() =>
|
||||||
Detail.Groups.Count > 0
|
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 group and severity you need."
|
||||||
: "Find the roll band on the left, then read across to the severity you need.";
|
: "Find the roll band on the left, then read across to the severity you need.";
|
||||||
|
|
||||||
|
private string? SelectedGroupLabel =>
|
||||||
|
Detail.Groups.FirstOrDefault(group => string.Equals(group.Key, SelectedGroupKey, StringComparison.OrdinalIgnoreCase))?.Label;
|
||||||
|
|
||||||
|
private string? SelectedColumnLabel =>
|
||||||
|
Detail.Columns.FirstOrDefault(column => string.Equals(column.Key, SelectedColumnKey, StringComparison.OrdinalIgnoreCase))?.Label;
|
||||||
|
|
||||||
|
private bool HasActiveFilters() =>
|
||||||
|
!string.IsNullOrWhiteSpace(SelectedGroupKey)
|
||||||
|
|| !string.IsNullOrWhiteSpace(SelectedColumnKey)
|
||||||
|
|| !string.IsNullOrWhiteSpace(RollJumpValue)
|
||||||
|
|| !string.Equals(CurrentMode, TablesReferenceMode.Reference, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private Task HandleGroupChanged(ChangeEventArgs args) =>
|
||||||
|
OnGroupChanged.InvokeAsync(args.Value?.ToString() ?? string.Empty);
|
||||||
|
|
||||||
|
private Task HandleColumnChanged(ChangeEventArgs args) =>
|
||||||
|
OnColumnChanged.InvokeAsync(args.Value?.ToString() ?? string.Empty);
|
||||||
|
|
||||||
|
private Task HandleRollJumpChanged(ChangeEventArgs args) =>
|
||||||
|
OnRollJumpChanged.InvokeAsync(args.Value?.ToString() ?? string.Empty);
|
||||||
|
|
||||||
|
private static string GetModeLabel(string mode) =>
|
||||||
|
mode switch
|
||||||
|
{
|
||||||
|
TablesReferenceMode.NeedsCuration => "Needs Curation",
|
||||||
|
TablesReferenceMode.Curated => "Curated",
|
||||||
|
_ => "Reference"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace RolemasterDb.App.Components.Tables;
|
||||||
|
|
||||||
|
public static class TablesReferenceMode
|
||||||
|
{
|
||||||
|
public const string Reference = "reference";
|
||||||
|
public const string NeedsCuration = "needs-curation";
|
||||||
|
public const string Curated = "curated";
|
||||||
|
}
|
||||||
@@ -1357,6 +1357,72 @@ pre,
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tables-context-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--shell-header-height) + 1rem);
|
||||||
|
z-index: 4;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 251, 245, 0.96), rgba(255, 251, 245, 0.92));
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid rgba(127, 96, 55, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .tables-context-bar {
|
||||||
|
background: linear-gradient(180deg, rgba(28, 31, 33, 0.96), rgba(28, 31, 33, 0.92));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-primary,
|
||||||
|
.tables-context-controls {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-controls {
|
||||||
|
margin-top: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-mode-tabs {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
min-width: min(100%, 11rem);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-filter-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-filter-chip {
|
||||||
|
border: 1px solid rgba(127, 96, 55, 0.2);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 246, 233, 0.88);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
transition: border-color 0.16s ease, background-color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-filter-chip:hover,
|
||||||
|
.tables-context-filter-chip:focus-visible {
|
||||||
|
border-color: rgba(184, 121, 59, 0.34);
|
||||||
|
background: rgba(255, 239, 214, 0.96);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.table-browser-reading-hint {
|
.table-browser-reading-hint {
|
||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
}
|
}
|
||||||
@@ -1497,6 +1563,13 @@ pre,
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tables-filtered-cell {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-cell {
|
.empty-cell {
|
||||||
color: #a08464;
|
color: #a08464;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -2175,6 +2248,14 @@ pre,
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tables-context-fields {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-context-field {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.critical-editor-header,
|
.critical-editor-header,
|
||||||
.critical-editor-footer {
|
.critical-editor-footer {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user