diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md index d3cdcb4..85a56aa 100644 --- a/docs/tables_frontend_overhaul_implementation_plan.md +++ b/docs/tables_frontend_overhaul_implementation_plan.md @@ -30,7 +30,7 @@ It is intentionally implementation-focused: - Branch: `frontend/tables-overhaul` - Last updated: `2026-04-12` -- Current focus: `Phase 5` +- Current focus: `Phase 6` - Document mode: living plan and progress log ### Progress Log @@ -77,6 +77,7 @@ It is intentionally implementation-focused: | 2026-04-11 | Post-P3 fix 2 | Completed | Simplified `/tables` by removing static prose and context controls, dropped the redundant selected-result inspector in favor of a floating action menu, and moved the canvas onto its own scroll region so sticky headers layer correctly beneath the context bar. | | 2026-04-12 | Phase 4 planning | Planned | Expanded the `Curation` phase from a route placeholder into a concrete migration plan that moves queue-first curation out of `Tables` and into a dedicated workflow surface. | | 2026-04-12 | Phase 4 | Completed | Replaced the placeholder `/curation` route with a real queue-first workspace, added queue scope and context persistence, moved browse-to-curation handoff out of `Tables`, and preserved diagnostics and full-editor escape hatches without keeping queue work on the reference page. | +| 2026-04-12 | Phase 5 | Completed | Consolidated the existing tooling routes into a coherent `Tools` workspace with a real hub, shared tooling page frame, preserved-context exits from diagnostics back into `Tables` and `Curation`, and a grouped API reference surface. | ### Lessons Learned @@ -688,6 +689,22 @@ Create a dedicated queue-first curation workflow so repair work is fast and does ## Phase 5: `Tools` Consolidation +### Status + +`Completed` + +### Task Progress + +| Task | Status | Notes | +| --- | --- | --- | +| `P5.1` | Completed | The thin `Tools` panel was replaced with a real hub page presenting diagnostics and API docs as destination cards. | +| `P5.2` | Completed | Diagnostics remained on the canonical `/tools/diagnostics` route established earlier. | +| `P5.3` | Completed | API docs remained on the canonical `/tools/api` route established earlier. | +| `P5.4` | Completed | Tooling pages now reuse a shared tooling frame and the same table-context URI patterns already used by `Tables`, `Curation`, and diagnostics. | +| `P5.5` | Completed | Diagnostics now exposes preserved-context links back into `Tables` and `Curation`, plus a stable return path to the `Tools` hub. | +| `P5.6` | Completed | Deep inspection remains isolated to tooling surfaces; browse and curation flows still link outward instead of embedding engineering detail inline. | +| `P5.7` | Completed | The tooling pages now share a stronger cool-slate documentation/workbench treatment instead of ad hoc panel stacks. | + ### Goal Separate diagnostic and developer tooling from player-facing flows without losing deep-link usefulness. diff --git a/src/RolemasterDb.App/Components/App.razor b/src/RolemasterDb.App/Components/App.razor index 5554142..8d6223c 100644 --- a/src/RolemasterDb.App/Components/App.razor +++ b/src/RolemasterDb.App/Components/App.razor @@ -2,28 +2,27 @@ - - - - - - - - - - + + + + + + + + + + - - - + + + - - - - + + + - + \ No newline at end of file diff --git a/src/RolemasterDb.App/Components/Layout/ReconnectModal.razor b/src/RolemasterDb.App/Components/Layout/ReconnectModal.razor index e740b0c..76f9067 100644 --- a/src/RolemasterDb.App/Components/Layout/ReconnectModal.razor +++ b/src/RolemasterDb.App/Components/Layout/ReconnectModal.razor @@ -1,4 +1,4 @@ - +
diff --git a/src/RolemasterDb.App/Components/Pages/Tools.razor b/src/RolemasterDb.App/Components/Pages/Tools.razor index 6cce8e6..9dc98b0 100644 --- a/src/RolemasterDb.App/Components/Pages/Tools.razor +++ b/src/RolemasterDb.App/Components/Pages/Tools.razor @@ -2,12 +2,39 @@ Tools -
-

Tools

-

Diagnostics and API documentation now live under the `Tools` destination so engineering workflows stay reachable without polluting player-facing navigation.

+ + +
+ +
+ +
+
-
- Open diagnostics - Open API docs -
-
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/src/RolemasterDb.App/Components/Shared/CriticalCellCurationDialog.razor b/src/RolemasterDb.App/Components/Shared/CriticalCellCurationDialog.razor index 39dc8ec..72e4dec 100644 --- a/src/RolemasterDb.App/Components/Shared/CriticalCellCurationDialog.razor +++ b/src/RolemasterDb.App/Components/Shared/CriticalCellCurationDialog.razor @@ -103,7 +103,7 @@ { if (firstRender) { - jsModule = await JSRuntime.InvokeAsync("import", "./Components/Shared/CriticalCellEditorDialog.razor.js"); + jsModule = await JSRuntime.InvokeAsync("import", "./components/shared/critical-cell-editor-dialog.js"); await jsModule.InvokeVoidAsync("lockBackgroundScroll"); } @@ -165,4 +165,4 @@ private static string BuildColumnDisplayText(CriticalCellEditorModel model) => string.IsNullOrWhiteSpace(model.GroupLabel) ? model.ColumnLabel : $"{model.GroupLabel} / {model.ColumnLabel}"; -} \ No newline at end of file +} diff --git a/src/RolemasterDb.App/Components/Shared/CriticalCellEditorDialog.razor b/src/RolemasterDb.App/Components/Shared/CriticalCellEditorDialog.razor index bcd132c..cd2dffa 100644 --- a/src/RolemasterDb.App/Components/Shared/CriticalCellEditorDialog.razor +++ b/src/RolemasterDb.App/Components/Shared/CriticalCellEditorDialog.razor @@ -382,7 +382,7 @@ jsModule = await JSRuntime.InvokeAsync( "import", - "./Components/Shared/CriticalCellEditorDialog.razor.js"); + "./components/shared/critical-cell-editor-dialog.js"); await jsModule.InvokeVoidAsync("lockBackgroundScroll"); } diff --git a/src/RolemasterDb.App/Components/Tools/ApiPageContent.razor b/src/RolemasterDb.App/Components/Tools/ApiPageContent.razor index c36a76c..71e3257 100644 --- a/src/RolemasterDb.App/Components/Tools/ApiPageContent.razor +++ b/src/RolemasterDb.App/Components/Tools/ApiPageContent.razor @@ -1,8 +1,28 @@ -
-
-

Reference data

-

GET /api/reference-data

-
{
+
+    
+        All tools
+        Open diagnostics
+    
+
+    
+        
+
+
+ Read models +

Reference and lookup payloads

+
+ +
+
+
+

Reference data

+ GET /api/reference-data +
+ +
{
   "attackTables": [
     {
       "key": "broadsword",
@@ -22,35 +42,52 @@
     }
   ]
 }
-
+
-
-

Attack lookup

-

POST /api/lookup/attack

-
{
+                    
+
+

Attack lookup

+ POST /api/lookup/attack +
+ +
{
   "attackTable": "broadsword",
   "armorType": "AT10",
   "roll": 111,
   "criticalRoll": 72
 }
-
+
-
-

Critical lookup

-

POST /api/lookup/critical

-
{
+                    
+
+

Critical lookup

+ POST /api/lookup/critical +
+ +
{
   "criticalType": "mana",
   "column": "E",
   "roll": 100,
   "group": null
 }
-

Response now includes table metadata, roll-band bounds, raw imported cell text, parse status, and parsed JSON alongside the gameplay description.

-
+
+
+
-
-

Cell editor load

-

GET /api/tables/critical/{slug}/cells/{resultId}

-
{
+            
+
+ Curation contracts +

Critical-cell editing workflow

+
+ +
+
+
+

Cell editor load

+ GET /api/tables/critical/{slug}/cells/{resultId} +
+ +
{
   "resultId": 412,
   "tableSlug": "slash",
   "tableName": "Slash Critical Strike Table",
@@ -73,19 +110,23 @@
   "effects": [],
   "branches": []
 }
-

Use this to retrieve the full editable result graph for one critical-table cell, including nested branches, normalized effects, and review notes for unresolved quick-parse tokens.

-
+
-
-

Cell source image

-

GET /api/tables/critical/{slug}/cells/{resultId}/source-image

-

Streams the importer-generated PNG crop for the current critical cell. Returns 404 when the row has no stored crop or the artifact is missing.

-
+
+
+

Cell source image

+ GET /api/tables/critical/{slug}/cells/{resultId}/source-image +
+ +
-
-

Cell re-parse

-

POST /api/tables/critical/{slug}/cells/{resultId}/reparse

-
{
+                    
+
+

Cell re-parse

+ POST /api/tables/critical/{slug}/cells/{resultId}/reparse +
+ +
{
   "currentState": {
     "rawCellText": "Strike to thigh. +8H\nWith greaves: blow glances aside.",
     "descriptionText": "Curated prose",
@@ -101,15 +142,17 @@
     "branches": []
   }
 }
-

Re-runs the shared single-cell parser, merges the generated result with the current override state, and returns the refreshed editor payload without saving changes. Unknown or partially parsed tokens are surfaced explicitly in the returned review data.

-
+
-
-

Cell editor save

-

PUT /api/tables/critical/{slug}/cells/{resultId}

-
{
+                    
+
+

Cell editor save

+ PUT /api/tables/critical/{slug}/cells/{resultId} +
+ +
{
   "rawCellText": "Corrected imported text",
-    "descriptionText": "Rewritten prose after manual review",
+  "descriptionText": "Rewritten prose after manual review",
   "rawAffixText": "+10H - must parry 2 rnds",
   "parseStatus": "manually_curated",
   "parsedJson": "{\"reviewed\":true}",
@@ -138,6 +181,9 @@
   ],
   "branches": []
 }
-

The save endpoint replaces the stored base result, branch rows, and effect rows for that cell with the submitted curated payload.

-
-
+ + + + + + diff --git a/src/RolemasterDb.App/Components/Tools/DiagnosticsPageContent.razor b/src/RolemasterDb.App/Components/Tools/DiagnosticsPageContent.razor index 4694ce3..5d2a232 100644 --- a/src/RolemasterDb.App/Components/Tools/DiagnosticsPageContent.razor +++ b/src/RolemasterDb.App/Components/Tools/DiagnosticsPageContent.razor @@ -5,151 +5,163 @@ @inject LookupService LookupService @inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState -
-
-
-

Critical Cell Diagnostics

-

Engineering-only parser metadata, provenance, and payload inspection live here instead of inside the normal curation modal.

-
-
+ + + All tools + @if (BuildTablesUri() is { } tablesUri) + { + Open tables + } + @if (BuildCurationUri() is { } curationUri) + { + Open curation + } + - @if (referenceData is null) - { -

Loading table list...

- } - else if (!referenceData.CriticalTables.Any()) - { -

No critical tables are available yet.

- } - else if (!hasInitializedContext) - { -

Restoring diagnostic context...

- } - else - { -
-
- - -
- -
- - -
- - @if (tableDetail is { Groups.Count: > 0 }) + +
+ @if (referenceData is null) { -
- - -
+

Loading table list...

} + else if (!referenceData.CriticalTables.Any()) + { +

No critical tables are available yet.

+ } + else if (!hasInitializedContext) + { +

Restoring diagnostic context...

+ } + else + { +
+
+ + +
-
- - + @if (tableDetail is not null) + { + @foreach (var rollBand in tableDetail.RollBands) + { + + } + } + +
+ + @if (tableDetail is { Groups.Count: > 0 }) { - @foreach (var column in tableDetail.Columns) - { - - } +
+ + +
} - -
-
- @if (!string.IsNullOrWhiteSpace(detailError)) - { -

@detailError

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

The selected table could not be loaded.

- } - else if (!tableDetail.Cells.Any()) - { -

The selected table has no filled cells to inspect.

- } - else if (selectedCell is null) - { -
-
-
- No Filled Cell At This Position -

Pick another roll band, variant, or severity to inspect a stored result.

+
+ +
-
- } - else - { -
- Inspecting - @tableDetail.DisplayName - · Roll band @selectedCell.RollBand - · Severity @selectedCell.ColumnLabel - @if (!string.IsNullOrWhiteSpace(selectedCell.GroupLabel)) - { - · Variant @selectedCell.GroupLabel - } - · Result ID @selectedCell.ResultId -
- @if (isDiagnosticsLoading) - { -

Loading diagnostics...

+ @if (!string.IsNullOrWhiteSpace(detailError)) + { +

@detailError

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

The selected table could not be loaded.

+ } + else if (!tableDetail.Cells.Any()) + { +

The selected table has no filled cells to inspect.

+ } + else if (selectedCell is null) + { +
+
+
+ No Filled Cell At This Position +
Pick another roll band, variant, or severity to inspect a stored result.
+
+
+
+ } + else + { +
+ Inspecting + @tableDetail.DisplayName + · Roll band @selectedCell.RollBand + · Severity @selectedCell.ColumnLabel + @if (!string.IsNullOrWhiteSpace(selectedCell.GroupLabel)) + { + · Variant @selectedCell.GroupLabel + } + · Result ID @selectedCell.ResultId +
+ + @if (isDiagnosticsLoading) + { +

Loading diagnostics...

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

@diagnosticsError

+ } + else if (diagnosticsModel is not null) + { + + } + } } - else if (!string.IsNullOrWhiteSpace(diagnosticsError)) - { -

@diagnosticsError

- } - else if (diagnosticsModel is not null) - { - - } - } - } -
+ + + @code { private LookupReferenceData? referenceData; @@ -438,6 +450,43 @@ ?? detail.RollBands.FirstOrDefault()?.Label ?? string.Empty; + private string? BuildTablesUri() + { + if (string.IsNullOrWhiteSpace(selectedTableSlug)) + { + return null; + } + + var snapshot = new RolemasterDb.App.Frontend.AppState.TableContextSnapshot( + TableSlug: selectedTableSlug, + GroupKey: selectedGroupKey, + ColumnKey: selectedColumnKey, + RollBand: selectedRollBand, + ResultId: selectedCell?.ResultId, + Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference); + + return TableContextState.BuildUri("/tables", snapshot); + } + + private string? BuildCurationUri() + { + if (selectedCell is null || string.IsNullOrWhiteSpace(selectedTableSlug)) + { + return null; + } + + var snapshot = new RolemasterDb.App.Frontend.AppState.TableContextSnapshot( + TableSlug: selectedTableSlug, + GroupKey: selectedGroupKey, + ColumnKey: selectedColumnKey, + RollBand: selectedRollBand, + ResultId: selectedCell.ResultId, + Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Curation, + QueueScope: RolemasterDb.App.Frontend.Curation.CurationQueueScopes.SelectedTable); + + return TableContextState.BuildUri("/curation", snapshot); + } + private static string ResolveColumnKey(CriticalTableDetail detail, string? columnKey) => detail.Columns.FirstOrDefault(item => string.Equals(item.Key, columnKey, StringComparison.Ordinal))?.Key ?? detail.Columns.FirstOrDefault()?.Key diff --git a/src/RolemasterDb.App/Components/Tools/ToolLinkCard.razor b/src/RolemasterDb.App/Components/Tools/ToolLinkCard.razor new file mode 100644 index 0000000..7ae9cc9 --- /dev/null +++ b/src/RolemasterDb.App/Components/Tools/ToolLinkCard.razor @@ -0,0 +1,49 @@ + + +@code { + [Parameter, EditorRequired] + public string Title { get; set; } = string.Empty; + + [Parameter] + public string? Summary { get; set; } + + [Parameter] + public string? Badge { get; set; } + + [Parameter, EditorRequired] + public string Href { get; set; } = string.Empty; + + [Parameter] + public string ActionLabel { get; set; } = "Open"; + + [Parameter] + public RenderFragment? Details { get; set; } +} diff --git a/src/RolemasterDb.App/Components/Tools/ToolPageFrame.razor b/src/RolemasterDb.App/Components/Tools/ToolPageFrame.razor new file mode 100644 index 0000000..860be6e --- /dev/null +++ b/src/RolemasterDb.App/Components/Tools/ToolPageFrame.razor @@ -0,0 +1,45 @@ +
+
+
+ @if (!string.IsNullOrWhiteSpace(Eyebrow)) + { + @Eyebrow + } + +

@Title

+ + @if (!string.IsNullOrWhiteSpace(Summary)) + { +
@Summary
+ } +
+ + @if (Actions is not null) + { +
+ @Actions +
+ } +
+ +
+ @ChildContent +
+
+ +@code { + [Parameter] + public string? Eyebrow { get; set; } + + [Parameter, EditorRequired] + public string Title { get; set; } = string.Empty; + + [Parameter] + public string? Summary { get; set; } + + [Parameter] + public RenderFragment? Actions { get; set; } + + [Parameter, EditorRequired] + public RenderFragment ChildContent { get; set; } = default!; +} diff --git a/src/RolemasterDb.App/Program.cs b/src/RolemasterDb.App/Program.cs index b1f4296..bc4ab11 100644 --- a/src/RolemasterDb.App/Program.cs +++ b/src/RolemasterDb.App/Program.cs @@ -5,10 +5,10 @@ using RolemasterDb.App.Frontend.AppState; using RolemasterDb.App.Features; var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseStaticWebAssets(); var connectionString = builder.Configuration.GetConnectionString("RolemasterDb") ?? "Data Source=rolemaster.db"; -builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); +builder.Services.AddRazorComponents().AddInteractiveServerComponents(); builder.Services.AddDbContextFactory(options => options.UseSqlite(connectionString)); builder.Services.AddSingleton(); builder.Services.AddScoped(); @@ -27,13 +27,13 @@ if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); } + app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); app.UseAntiforgery(); app.MapStaticAssets(); var api = app.MapGroup("/api"); -api.MapGet("/reference-data", async (LookupService lookupService, CancellationToken cancellationToken) => - Results.Ok(await lookupService.GetReferenceDataAsync(cancellationToken))); +api.MapGet("/reference-data", async (LookupService lookupService, CancellationToken cancellationToken) => Results.Ok(await lookupService.GetReferenceDataAsync(cancellationToken))); api.MapPost("/lookup/attack", async (AttackLookupRequest request, LookupService lookupService, CancellationToken cancellationToken) => { var result = await lookupService.LookupAttackAsync(request, cancellationToken); @@ -64,7 +64,6 @@ api.MapPut("/tables/critical/{slug}/cells/{resultId:int}", async (string slug, i var result = await lookupService.UpdateCriticalCellAsync(slug, resultId, request, cancellationToken); return result is null ? Results.NotFound() : Results.Ok(result); }); -app.MapRazorComponents() - .AddInteractiveServerRenderMode(); +app.MapRazorComponents().AddInteractiveServerRenderMode(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/src/RolemasterDb.App/wwwroot/app.css b/src/RolemasterDb.App/wwwroot/app.css index 81ce902..4ad7621 100644 --- a/src/RolemasterDb.App/wwwroot/app.css +++ b/src/RolemasterDb.App/wwwroot/app.css @@ -297,6 +297,105 @@ pre, color: color-mix(in srgb, var(--info-3) 46%, var(--text-primary)); } +.tool-page-frame, +.tool-page-body, +.tool-page-heading, +.tool-page-actions, +.tools-hub-grid, +.tool-link-card, +.tool-link-card-body, +.tool-link-card-header, +.tool-link-card-title-row, +.tool-link-card-meta, +.api-reference-groups, +.api-reference-group, +.api-reference-group-header, +.api-endpoint-card, +.api-endpoint-card-header { + display: grid; + gap: 1rem; +} + +.tool-page-header { + display: flex; + gap: 1rem; + align-items: start; + justify-content: space-between; + flex-wrap: wrap; +} + +.tool-page-eyebrow { + color: color-mix(in srgb, var(--info-3) 55%, var(--text-secondary)); + font-family: var(--font-ui); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.tool-page-summary, +.tool-link-card-summary { + color: var(--text-secondary); + max-width: 62ch; +} + +.tool-page-actions { + grid-auto-flow: column; + grid-auto-columns: max-content; + justify-content: end; + align-items: center; +} + +.tools-hub-grid { + grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); +} + +.tool-link-card { + padding: 1.1rem 1.15rem; + border-radius: 20px; + border: 1px solid color-mix(in srgb, var(--info-2) 24%, var(--border-default)); + background: linear-gradient(180deg, color-mix(in srgb, var(--surface-2) 86%, var(--surface-tooling)), color-mix(in srgb, var(--surface-1) 92%, var(--surface-tooling))); + box-shadow: var(--shadow-1); +} + +.tool-link-card-title { + margin: 0; + font-size: 1.15rem; +} + +.tool-link-card-meta { + gap: 0.4rem; + color: var(--text-secondary); +} + +.tool-link-card-actions { + display: flex; + justify-content: flex-start; + align-items: center; +} + +.api-reference-group-header h2 { + margin: 0; + font-size: 1.2rem; +} + +.api-endpoint-card { + align-content: start; +} + +.api-endpoint-card-header { + gap: 0.45rem; +} + +.api-endpoint-card-header .panel-title { + margin-bottom: 0; +} + +.api-endpoint-card-header code { + color: color-mix(in srgb, var(--info-3) 55%, var(--text-primary)); + font-size: 0.92rem; +} + .panel-title { margin: 0 0 0.35rem; font-size: 1.4rem; @@ -1125,6 +1224,7 @@ select.input-shell { .api-grid { display: grid; gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); } .blazor-error-boundary { @@ -2366,13 +2466,6 @@ select.input-shell { justify-items: start; } -.diagnostics-page-header { - display: flex; - align-items: start; - justify-content: space-between; - gap: 1rem; -} - .diagnostics-selector-grid { display: grid; gap: 0.85rem; @@ -2602,6 +2695,11 @@ select.input-shell { text-align: left; } + .tool-page-actions { + grid-auto-flow: row; + justify-content: start; + } + .diagnostics-page-header, .diagnostics-selection-summary, .curation-queue-bar-header, diff --git a/src/RolemasterDb.App/wwwroot/components/layout/reconnect-modal.js b/src/RolemasterDb.App/wwwroot/components/layout/reconnect-modal.js new file mode 100644 index 0000000..a44de78 --- /dev/null +++ b/src/RolemasterDb.App/wwwroot/components/layout/reconnect-modal.js @@ -0,0 +1,63 @@ +// Set up event handlers +const reconnectModal = document.getElementById("components-reconnect-modal"); +reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); + +const retryButton = document.getElementById("components-reconnect-button"); +retryButton.addEventListener("click", retry); + +const resumeButton = document.getElementById("components-resume-button"); +resumeButton.addEventListener("click", resume); + +function handleReconnectStateChanged(event) { + if (event.detail.state === "show") { + reconnectModal.showModal(); + } else if (event.detail.state === "hide") { + reconnectModal.close(); + } else if (event.detail.state === "failed") { + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } else if (event.detail.state === "rejected") { + location.reload(); + } +} + +async function retry() { + document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + const resumeSuccessful = await Blazor.resumeCircuit(); + if (!resumeSuccessful) { + location.reload(); + } else { + reconnectModal.close(); + } + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } +} + +async function resume() { + try { + const successful = await Blazor.resumeCircuit(); + if (!successful) { + location.reload(); + } + } catch { + reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed"); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === "visible") { + await retry(); + } +} diff --git a/src/RolemasterDb.App/wwwroot/components/shared/critical-cell-editor-dialog.js b/src/RolemasterDb.App/wwwroot/components/shared/critical-cell-editor-dialog.js new file mode 100644 index 0000000..2f4ddb2 --- /dev/null +++ b/src/RolemasterDb.App/wwwroot/components/shared/critical-cell-editor-dialog.js @@ -0,0 +1,26 @@ +const scrollLockClassName = "critical-editor-scroll-locked"; +let scrollLockCount = 0; + +export function lockBackgroundScroll() { + scrollLockCount++; + if (scrollLockCount !== 1) { + return; + } + + document.documentElement.classList.add(scrollLockClassName); + document.body.classList.add(scrollLockClassName); +} + +export function unlockBackgroundScroll() { + if (scrollLockCount === 0) { + return; + } + + scrollLockCount--; + if (scrollLockCount !== 0) { + return; + } + + document.documentElement.classList.remove(scrollLockClassName); + document.body.classList.remove(scrollLockClassName); +}