Consolidate tools and fix static assets
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-04-12`
|
- Last updated: `2026-04-12`
|
||||||
- Current focus: `Phase 5`
|
- Current focus: `Phase 6`
|
||||||
- Document mode: living plan and progress log
|
- Document mode: living plan and progress log
|
||||||
|
|
||||||
### 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-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 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 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
|
### Lessons Learned
|
||||||
|
|
||||||
@@ -688,6 +689,22 @@ Create a dedicated queue-first curation workflow so repair work is fast and does
|
|||||||
|
|
||||||
## Phase 5: `Tools` Consolidation
|
## 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
|
### Goal
|
||||||
|
|
||||||
Separate diagnostic and developer tooling from player-facing flows without losing deep-link usefulness.
|
Separate diagnostic and developer tooling from player-facing flows without losing deep-link usefulness.
|
||||||
|
|||||||
@@ -2,28 +2,27 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<base href="/" />
|
<base href="/"/>
|
||||||
<ResourcePreloader />
|
<ResourcePreloader/>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap"/>
|
||||||
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
|
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]"/>
|
||||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
<link rel="stylesheet" href="@Assets["app.css"]"/>
|
||||||
<link rel="stylesheet" href="@Assets["RolemasterDb.App.styles.css"]" />
|
<link rel="stylesheet" href="@Assets["RolemasterDb.App.styles.css"]"/>
|
||||||
<script src="@Assets["theme.js"]"></script>
|
<script src="@Assets["theme.js"]"></script>
|
||||||
<script>window.rolemasterTheme?.init("rolemaster.theme.mode");</script>
|
<script>window.rolemasterTheme?.init("rolemaster.theme.mode");</script>
|
||||||
<ImportMap />
|
<ImportMap/>
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
<HeadOutlet />
|
<HeadOutlet/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<Routes @rendermode="InteractiveServer" />
|
<Routes @rendermode="InteractiveServer"/>
|
||||||
<ReconnectModal />
|
<ReconnectModal/>
|
||||||
<script src="@Assets["tables.js"]"></script>
|
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
<script type="module" src="@Assets["components/layout/reconnect-modal.js"]"></script>
|
||||||
|
|
||||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||||
<div class="components-reconnect-container">
|
<div class="components-reconnect-container">
|
||||||
|
|||||||
@@ -2,12 +2,39 @@
|
|||||||
|
|
||||||
<PageTitle>Tools</PageTitle>
|
<PageTitle>Tools</PageTitle>
|
||||||
|
|
||||||
<section class="panel tooling-surface">
|
<ToolPageFrame
|
||||||
<h1 class="panel-title">Tools</h1>
|
Eyebrow="Developer workflows"
|
||||||
<p class="panel-copy">Diagnostics and API documentation now live under the `Tools` destination so engineering workflows stay reachable without polluting player-facing navigation.</p>
|
Title="Tools"
|
||||||
|
Summary="Inspection, parser review, and API surface reference stay isolated here from play and table-reading flows.">
|
||||||
|
<ChildContent>
|
||||||
|
<div class="tools-hub-grid">
|
||||||
|
<ToolLinkCard
|
||||||
|
Title="Diagnostics"
|
||||||
|
Badge="Context-aware"
|
||||||
|
Summary="Inspect one stored critical-table cell with parser provenance, payload state, and engineering diagnostics."
|
||||||
|
Href="/tools/diagnostics"
|
||||||
|
ActionLabel="Open diagnostics">
|
||||||
|
<Details>
|
||||||
|
<div class="tool-link-card-meta">
|
||||||
|
<span>Table, roll band, variant, severity</span>
|
||||||
|
<span>Jump back to Tables or Curation</span>
|
||||||
|
</div>
|
||||||
|
</Details>
|
||||||
|
</ToolLinkCard>
|
||||||
|
|
||||||
<div class="action-row">
|
<ToolLinkCard
|
||||||
<NavLink class="btn-link" href="/tools/diagnostics">Open diagnostics</NavLink>
|
Title="API Surface"
|
||||||
<NavLink class="btn-link" href="/tools/api">Open API docs</NavLink>
|
Badge="Reference"
|
||||||
</div>
|
Summary="Browse the core lookup and critical-cell editing endpoints without leaving the app shell."
|
||||||
</section>
|
Href="/tools/api"
|
||||||
|
ActionLabel="Open API docs">
|
||||||
|
<Details>
|
||||||
|
<div class="tool-link-card-meta">
|
||||||
|
<span>Reference data and lookup contracts</span>
|
||||||
|
<span>Load, re-parse, source-image, and save endpoints</span>
|
||||||
|
</div>
|
||||||
|
</Details>
|
||||||
|
</ToolLinkCard>
|
||||||
|
</div>
|
||||||
|
</ChildContent>
|
||||||
|
</ToolPageFrame>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./Components/Shared/CriticalCellEditorDialog.razor.js");
|
jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./components/shared/critical-cell-editor-dialog.js");
|
||||||
|
|
||||||
await jsModule.InvokeVoidAsync("lockBackgroundScroll");
|
await jsModule.InvokeVoidAsync("lockBackgroundScroll");
|
||||||
}
|
}
|
||||||
@@ -165,4 +165,4 @@
|
|||||||
private static string BuildColumnDisplayText(CriticalCellEditorModel model) =>
|
private static string BuildColumnDisplayText(CriticalCellEditorModel model) =>
|
||||||
string.IsNullOrWhiteSpace(model.GroupLabel) ? model.ColumnLabel : $"{model.GroupLabel} / {model.ColumnLabel}";
|
string.IsNullOrWhiteSpace(model.GroupLabel) ? model.ColumnLabel : $"{model.GroupLabel} / {model.ColumnLabel}";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,7 +382,7 @@
|
|||||||
|
|
||||||
jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
|
jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
|
||||||
"import",
|
"import",
|
||||||
"./Components/Shared/CriticalCellEditorDialog.razor.js");
|
"./components/shared/critical-cell-editor-dialog.js");
|
||||||
|
|
||||||
await jsModule.InvokeVoidAsync("lockBackgroundScroll");
|
await jsModule.InvokeVoidAsync("lockBackgroundScroll");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
<div class="api-grid">
|
<ToolPageFrame
|
||||||
<section class="panel tooling-surface">
|
Eyebrow="API reference"
|
||||||
<h2 class="panel-title">Reference data</h2>
|
Title="API Surface"
|
||||||
<p class="panel-copy"><code>GET /api/reference-data</code></p>
|
Summary="The stable lookup and critical-cell editing contracts are grouped here by workflow so diagnostics and curation work can move quickly without leaving the app shell.">
|
||||||
<pre class="code-block">{
|
<Actions>
|
||||||
|
<a class="btn btn-link" href="/tools">All tools</a>
|
||||||
|
<a class="btn btn-secondary" href="/tools/diagnostics">Open diagnostics</a>
|
||||||
|
</Actions>
|
||||||
|
|
||||||
|
<ChildContent>
|
||||||
|
<div class="api-reference-groups">
|
||||||
|
<section class="api-reference-group">
|
||||||
|
<div class="api-reference-group-header">
|
||||||
|
<span class="tool-page-eyebrow">Read models</span>
|
||||||
|
<h2>Reference and lookup payloads</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-grid">
|
||||||
|
<section class="panel tooling-surface api-endpoint-card">
|
||||||
|
<div class="api-endpoint-card-header">
|
||||||
|
<h3 class="panel-title">Reference data</h3>
|
||||||
|
<code>GET /api/reference-data</code>
|
||||||
|
</div>
|
||||||
|
<div class="tool-link-card-summary">Bootstraps attack tables, critical tables, labels, and high-level metadata for the main client flows.</div>
|
||||||
|
<pre class="code-block">{
|
||||||
"attackTables": [
|
"attackTables": [
|
||||||
{
|
{
|
||||||
"key": "broadsword",
|
"key": "broadsword",
|
||||||
@@ -22,35 +42,52 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}</pre>
|
}</pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel tooling-surface">
|
<section class="panel tooling-surface api-endpoint-card">
|
||||||
<h2 class="panel-title">Attack lookup</h2>
|
<div class="api-endpoint-card-header">
|
||||||
<p class="panel-copy"><code>POST /api/lookup/attack</code></p>
|
<h3 class="panel-title">Attack lookup</h3>
|
||||||
<pre class="code-block">{
|
<code>POST /api/lookup/attack</code>
|
||||||
|
</div>
|
||||||
|
<div class="tool-link-card-summary">Resolves attack-table outcomes for the player-facing `Play` surface.</div>
|
||||||
|
<pre class="code-block">{
|
||||||
"attackTable": "broadsword",
|
"attackTable": "broadsword",
|
||||||
"armorType": "AT10",
|
"armorType": "AT10",
|
||||||
"roll": 111,
|
"roll": 111,
|
||||||
"criticalRoll": 72
|
"criticalRoll": 72
|
||||||
}</pre>
|
}</pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel tooling-surface">
|
<section class="panel tooling-surface api-endpoint-card">
|
||||||
<h2 class="panel-title">Critical lookup</h2>
|
<div class="api-endpoint-card-header">
|
||||||
<p class="panel-copy"><code>POST /api/lookup/critical</code></p>
|
<h3 class="panel-title">Critical lookup</h3>
|
||||||
<pre class="code-block">{
|
<code>POST /api/lookup/critical</code>
|
||||||
|
</div>
|
||||||
|
<div class="tool-link-card-summary">Returns resolved critical outcomes plus the imported table metadata needed to inspect the underlying source row.</div>
|
||||||
|
<pre class="code-block">{
|
||||||
"criticalType": "mana",
|
"criticalType": "mana",
|
||||||
"column": "E",
|
"column": "E",
|
||||||
"roll": 100,
|
"roll": 100,
|
||||||
"group": null
|
"group": null
|
||||||
}</pre>
|
}</pre>
|
||||||
<p class="panel-copy">Response now includes table metadata, roll-band bounds, raw imported cell text, parse status, and parsed JSON alongside the gameplay description.</p>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel tooling-surface">
|
<section class="api-reference-group">
|
||||||
<h2 class="panel-title">Cell editor load</h2>
|
<div class="api-reference-group-header">
|
||||||
<p class="panel-copy"><code>GET /api/tables/critical/{slug}/cells/{resultId}</code></p>
|
<span class="tool-page-eyebrow">Curation contracts</span>
|
||||||
<pre class="code-block">{
|
<h2>Critical-cell editing workflow</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-grid">
|
||||||
|
<section class="panel tooling-surface api-endpoint-card">
|
||||||
|
<div class="api-endpoint-card-header">
|
||||||
|
<h3 class="panel-title">Cell editor load</h3>
|
||||||
|
<code>GET /api/tables/critical/{slug}/cells/{resultId}</code>
|
||||||
|
</div>
|
||||||
|
<div class="tool-link-card-summary">Loads the full editable graph for one stored critical-table cell, including review notes and nested branches.</div>
|
||||||
|
<pre class="code-block">{
|
||||||
"resultId": 412,
|
"resultId": 412,
|
||||||
"tableSlug": "slash",
|
"tableSlug": "slash",
|
||||||
"tableName": "Slash Critical Strike Table",
|
"tableName": "Slash Critical Strike Table",
|
||||||
@@ -73,19 +110,23 @@
|
|||||||
"effects": [],
|
"effects": [],
|
||||||
"branches": []
|
"branches": []
|
||||||
}</pre>
|
}</pre>
|
||||||
<p class="panel-copy">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.</p>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel tooling-surface">
|
<section class="panel tooling-surface api-endpoint-card">
|
||||||
<h2 class="panel-title">Cell source image</h2>
|
<div class="api-endpoint-card-header">
|
||||||
<p class="panel-copy"><code>GET /api/tables/critical/{slug}/cells/{resultId}/source-image</code></p>
|
<h3 class="panel-title">Cell source image</h3>
|
||||||
<p class="panel-copy">Streams the importer-generated PNG crop for the current critical cell. Returns <code>404</code> when the row has no stored crop or the artifact is missing.</p>
|
<code>GET /api/tables/critical/{slug}/cells/{resultId}/source-image</code>
|
||||||
</section>
|
</div>
|
||||||
|
<div class="tool-link-card-summary">Streams the importer-generated PNG crop for the current critical cell and returns `404` when no stored artifact is available.</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel tooling-surface">
|
<section class="panel tooling-surface api-endpoint-card">
|
||||||
<h2 class="panel-title">Cell re-parse</h2>
|
<div class="api-endpoint-card-header">
|
||||||
<p class="panel-copy"><code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code></p>
|
<h3 class="panel-title">Cell re-parse</h3>
|
||||||
<pre class="code-block">{
|
<code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code>
|
||||||
|
</div>
|
||||||
|
<div class="tool-link-card-summary">Re-runs the single-cell parser and merges generated output with the current override state without persisting changes.</div>
|
||||||
|
<pre class="code-block">{
|
||||||
"currentState": {
|
"currentState": {
|
||||||
"rawCellText": "Strike to thigh. +8H\nWith greaves: blow glances aside.",
|
"rawCellText": "Strike to thigh. +8H\nWith greaves: blow glances aside.",
|
||||||
"descriptionText": "Curated prose",
|
"descriptionText": "Curated prose",
|
||||||
@@ -101,15 +142,17 @@
|
|||||||
"branches": []
|
"branches": []
|
||||||
}
|
}
|
||||||
}</pre>
|
}</pre>
|
||||||
<p class="panel-copy">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.</p>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel tooling-surface">
|
<section class="panel tooling-surface api-endpoint-card">
|
||||||
<h2 class="panel-title">Cell editor save</h2>
|
<div class="api-endpoint-card-header">
|
||||||
<p class="panel-copy"><code>PUT /api/tables/critical/{slug}/cells/{resultId}</code></p>
|
<h3 class="panel-title">Cell editor save</h3>
|
||||||
<pre class="code-block">{
|
<code>PUT /api/tables/critical/{slug}/cells/{resultId}</code>
|
||||||
|
</div>
|
||||||
|
<div class="tool-link-card-summary">Replaces the stored base result, branch rows, and effect rows for the targeted cell with the submitted curated payload.</div>
|
||||||
|
<pre class="code-block">{
|
||||||
"rawCellText": "Corrected imported text",
|
"rawCellText": "Corrected imported text",
|
||||||
"descriptionText": "Rewritten prose after manual review",
|
"descriptionText": "Rewritten prose after manual review",
|
||||||
"rawAffixText": "+10H - must parry 2 rnds",
|
"rawAffixText": "+10H - must parry 2 rnds",
|
||||||
"parseStatus": "manually_curated",
|
"parseStatus": "manually_curated",
|
||||||
"parsedJson": "{\"reviewed\":true}",
|
"parsedJson": "{\"reviewed\":true}",
|
||||||
@@ -138,6 +181,9 @@
|
|||||||
],
|
],
|
||||||
"branches": []
|
"branches": []
|
||||||
}</pre>
|
}</pre>
|
||||||
<p class="panel-copy">The save endpoint replaces the stored base result, branch rows, and effect rows for that cell with the submitted curated payload.</p>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
</div>
|
||||||
|
</ChildContent>
|
||||||
|
</ToolPageFrame>
|
||||||
|
|||||||
@@ -5,151 +5,163 @@
|
|||||||
@inject LookupService LookupService
|
@inject LookupService LookupService
|
||||||
@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
|
@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
|
||||||
|
|
||||||
<section class="panel diagnostics-page tooling-surface">
|
<ToolPageFrame
|
||||||
<header class="diagnostics-page-header">
|
Eyebrow="Inspection workspace"
|
||||||
<div>
|
Title="Critical Cell Diagnostics"
|
||||||
<h2 class="panel-title">Critical Cell Diagnostics</h2>
|
Summary="Parser provenance, payload state, and engineering-only review stay here instead of leaking into browse or curation workflows.">
|
||||||
<p class="panel-copy">Engineering-only parser metadata, provenance, and payload inspection live here instead of inside the normal curation modal.</p>
|
<Actions>
|
||||||
</div>
|
<a class="btn btn-link" href="/tools">All tools</a>
|
||||||
</header>
|
@if (BuildTablesUri() is { } tablesUri)
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@tablesUri">Open tables</a>
|
||||||
|
}
|
||||||
|
@if (BuildCurationUri() is { } curationUri)
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@curationUri">Open curation</a>
|
||||||
|
}
|
||||||
|
</Actions>
|
||||||
|
|
||||||
@if (referenceData is null)
|
<ChildContent>
|
||||||
{
|
<div class="diagnostics-page">
|
||||||
<p class="muted">Loading table list...</p>
|
@if (referenceData is null)
|
||||||
}
|
|
||||||
else if (!referenceData.CriticalTables.Any())
|
|
||||||
{
|
|
||||||
<p class="muted">No critical tables are available yet.</p>
|
|
||||||
}
|
|
||||||
else if (!hasInitializedContext)
|
|
||||||
{
|
|
||||||
<p class="muted">Restoring diagnostic context...</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="diagnostics-selector-grid">
|
|
||||||
<div class="field-shell">
|
|
||||||
<label for="diagnostics-table-select">Table</label>
|
|
||||||
<select
|
|
||||||
id="diagnostics-table-select"
|
|
||||||
class="input-shell"
|
|
||||||
value="@selectedTableSlug"
|
|
||||||
@onchange="HandleTableChanged"
|
|
||||||
disabled="@isBusy">
|
|
||||||
@foreach (var table in referenceData.CriticalTables)
|
|
||||||
{
|
|
||||||
<option value="@table.Key">@table.Label</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-shell">
|
|
||||||
<label for="diagnostics-roll-band-select">Roll Band</label>
|
|
||||||
<select
|
|
||||||
id="diagnostics-roll-band-select"
|
|
||||||
class="input-shell"
|
|
||||||
value="@selectedRollBand"
|
|
||||||
@onchange="HandleRollBandChanged"
|
|
||||||
disabled="@(isBusy || tableDetail is null || !tableDetail.RollBands.Any())">
|
|
||||||
@if (tableDetail is not null)
|
|
||||||
{
|
|
||||||
@foreach (var rollBand in tableDetail.RollBands)
|
|
||||||
{
|
|
||||||
<option value="@rollBand.Label">@rollBand.Label</option>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (tableDetail is { Groups.Count: > 0 })
|
|
||||||
{
|
{
|
||||||
<div class="field-shell">
|
<p class="muted">Loading table list...</p>
|
||||||
<label for="diagnostics-group-select">Variant</label>
|
|
||||||
<select
|
|
||||||
id="diagnostics-group-select"
|
|
||||||
class="input-shell"
|
|
||||||
value="@selectedGroupKey"
|
|
||||||
@onchange="HandleGroupChanged"
|
|
||||||
disabled="@isBusy">
|
|
||||||
@foreach (var group in tableDetail.Groups)
|
|
||||||
{
|
|
||||||
<option value="@group.Key">@group.Label</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
else if (!referenceData.CriticalTables.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">No critical tables are available yet.</p>
|
||||||
|
}
|
||||||
|
else if (!hasInitializedContext)
|
||||||
|
{
|
||||||
|
<p class="muted">Restoring diagnostic context...</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="diagnostics-selector-grid">
|
||||||
|
<div class="field-shell">
|
||||||
|
<label for="diagnostics-table-select">Table</label>
|
||||||
|
<select
|
||||||
|
id="diagnostics-table-select"
|
||||||
|
class="input-shell"
|
||||||
|
value="@selectedTableSlug"
|
||||||
|
@onchange="HandleTableChanged"
|
||||||
|
disabled="@isBusy">
|
||||||
|
@foreach (var table in referenceData.CriticalTables)
|
||||||
|
{
|
||||||
|
<option value="@table.Key">@table.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field-shell">
|
<div class="field-shell">
|
||||||
<label for="diagnostics-column-select">Severity</label>
|
<label for="diagnostics-roll-band-select">Roll Band</label>
|
||||||
<select
|
<select
|
||||||
id="diagnostics-column-select"
|
id="diagnostics-roll-band-select"
|
||||||
class="input-shell"
|
class="input-shell"
|
||||||
value="@selectedColumnKey"
|
value="@selectedRollBand"
|
||||||
@onchange="HandleColumnChanged"
|
@onchange="HandleRollBandChanged"
|
||||||
disabled="@(isBusy || tableDetail is null || !tableDetail.Columns.Any())">
|
disabled="@(isBusy || tableDetail is null || !tableDetail.RollBands.Any())">
|
||||||
@if (tableDetail is not null)
|
@if (tableDetail is not null)
|
||||||
|
{
|
||||||
|
@foreach (var rollBand in tableDetail.RollBands)
|
||||||
|
{
|
||||||
|
<option value="@rollBand.Label">@rollBand.Label</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (tableDetail is { Groups.Count: > 0 })
|
||||||
{
|
{
|
||||||
@foreach (var column in tableDetail.Columns)
|
<div class="field-shell">
|
||||||
{
|
<label for="diagnostics-group-select">Variant</label>
|
||||||
<option value="@column.Key">@column.Label</option>
|
<select
|
||||||
}
|
id="diagnostics-group-select"
|
||||||
|
class="input-shell"
|
||||||
|
value="@selectedGroupKey"
|
||||||
|
@onchange="HandleGroupChanged"
|
||||||
|
disabled="@isBusy">
|
||||||
|
@foreach (var group in tableDetail.Groups)
|
||||||
|
{
|
||||||
|
<option value="@group.Key">@group.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(detailError))
|
<div class="field-shell">
|
||||||
{
|
<label for="diagnostics-column-select">Severity</label>
|
||||||
<p class="error-text">@detailError</p>
|
<select
|
||||||
}
|
id="diagnostics-column-select"
|
||||||
else if (tableDetail is null)
|
class="input-shell"
|
||||||
{
|
value="@selectedColumnKey"
|
||||||
<p class="muted">The selected table could not be loaded.</p>
|
@onchange="HandleColumnChanged"
|
||||||
}
|
disabled="@(isBusy || tableDetail is null || !tableDetail.Columns.Any())">
|
||||||
else if (!tableDetail.Cells.Any())
|
@if (tableDetail is not null)
|
||||||
{
|
{
|
||||||
<p class="muted">The selected table has no filled cells to inspect.</p>
|
@foreach (var column in tableDetail.Columns)
|
||||||
}
|
{
|
||||||
else if (selectedCell is null)
|
<option value="@column.Key">@column.Label</option>
|
||||||
{
|
}
|
||||||
<div class="critical-editor-card nested">
|
}
|
||||||
<div class="critical-editor-card-header">
|
</select>
|
||||||
<div>
|
|
||||||
<strong>No Filled Cell At This Position</strong>
|
|
||||||
<p class="muted critical-editor-inline-copy">Pick another roll band, variant, or severity to inspect a stored result.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="diagnostics-selection-summary">
|
|
||||||
<strong>Inspecting</strong>
|
|
||||||
<span>@tableDetail.DisplayName</span>
|
|
||||||
<span>· Roll band <strong>@selectedCell.RollBand</strong></span>
|
|
||||||
<span>· Severity <strong>@selectedCell.ColumnLabel</strong></span>
|
|
||||||
@if (!string.IsNullOrWhiteSpace(selectedCell.GroupLabel))
|
|
||||||
{
|
|
||||||
<span>· Variant <strong>@selectedCell.GroupLabel</strong></span>
|
|
||||||
}
|
|
||||||
<span>· Result ID <strong>@selectedCell.ResultId</strong></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (isDiagnosticsLoading)
|
@if (!string.IsNullOrWhiteSpace(detailError))
|
||||||
{
|
{
|
||||||
<p class="muted">Loading diagnostics...</p>
|
<p class="error-text">@detailError</p>
|
||||||
|
}
|
||||||
|
else if (tableDetail is null)
|
||||||
|
{
|
||||||
|
<p class="muted">The selected table could not be loaded.</p>
|
||||||
|
}
|
||||||
|
else if (!tableDetail.Cells.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">The selected table has no filled cells to inspect.</p>
|
||||||
|
}
|
||||||
|
else if (selectedCell is null)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-card nested">
|
||||||
|
<div class="critical-editor-card-header">
|
||||||
|
<div>
|
||||||
|
<strong>No Filled Cell At This Position</strong>
|
||||||
|
<div class="muted critical-editor-inline-copy">Pick another roll band, variant, or severity to inspect a stored result.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="diagnostics-selection-summary">
|
||||||
|
<strong>Inspecting</strong>
|
||||||
|
<span>@tableDetail.DisplayName</span>
|
||||||
|
<span>· Roll band <strong>@selectedCell.RollBand</strong></span>
|
||||||
|
<span>· Severity <strong>@selectedCell.ColumnLabel</strong></span>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(selectedCell.GroupLabel))
|
||||||
|
{
|
||||||
|
<span>· Variant <strong>@selectedCell.GroupLabel</strong></span>
|
||||||
|
}
|
||||||
|
<span>· Result ID <strong>@selectedCell.ResultId</strong></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isDiagnosticsLoading)
|
||||||
|
{
|
||||||
|
<p class="muted">Loading diagnostics...</p>
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(diagnosticsError))
|
||||||
|
{
|
||||||
|
<p class="error-text">@diagnosticsError</p>
|
||||||
|
}
|
||||||
|
else if (diagnosticsModel is not null)
|
||||||
|
{
|
||||||
|
<CriticalCellEngineeringDiagnostics Model="diagnosticsModel" />
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(diagnosticsError))
|
</div>
|
||||||
{
|
</ChildContent>
|
||||||
<p class="error-text">@diagnosticsError</p>
|
</ToolPageFrame>
|
||||||
}
|
|
||||||
else if (diagnosticsModel is not null)
|
|
||||||
{
|
|
||||||
<CriticalCellEngineeringDiagnostics Model="diagnosticsModel" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private LookupReferenceData? referenceData;
|
private LookupReferenceData? referenceData;
|
||||||
@@ -438,6 +450,43 @@
|
|||||||
?? detail.RollBands.FirstOrDefault()?.Label
|
?? detail.RollBands.FirstOrDefault()?.Label
|
||||||
?? string.Empty;
|
?? 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) =>
|
private static string ResolveColumnKey(CriticalTableDetail detail, string? columnKey) =>
|
||||||
detail.Columns.FirstOrDefault(item => string.Equals(item.Key, columnKey, StringComparison.Ordinal))?.Key
|
detail.Columns.FirstOrDefault(item => string.Equals(item.Key, columnKey, StringComparison.Ordinal))?.Key
|
||||||
?? detail.Columns.FirstOrDefault()?.Key
|
?? detail.Columns.FirstOrDefault()?.Key
|
||||||
|
|||||||
49
src/RolemasterDb.App/Components/Tools/ToolLinkCard.razor
Normal file
49
src/RolemasterDb.App/Components/Tools/ToolLinkCard.razor
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<article class="tool-link-card">
|
||||||
|
<div class="tool-link-card-body">
|
||||||
|
<div class="tool-link-card-header">
|
||||||
|
<div class="tool-link-card-title-row">
|
||||||
|
<h2 class="tool-link-card-title">@Title</h2>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Badge))
|
||||||
|
{
|
||||||
|
<span class="chip">@Badge</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Summary))
|
||||||
|
{
|
||||||
|
<div class="tool-link-card-summary">@Summary</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Details is not null)
|
||||||
|
{
|
||||||
|
<div class="tool-link-card-details">
|
||||||
|
@Details
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-link-card-actions">
|
||||||
|
<NavLink class="btn btn-secondary" href="@Href">@ActionLabel</NavLink>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
@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; }
|
||||||
|
}
|
||||||
45
src/RolemasterDb.App/Components/Tools/ToolPageFrame.razor
Normal file
45
src/RolemasterDb.App/Components/Tools/ToolPageFrame.razor
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<section class="panel tooling-surface tool-page-frame">
|
||||||
|
<header class="tool-page-header">
|
||||||
|
<div class="tool-page-heading">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Eyebrow))
|
||||||
|
{
|
||||||
|
<span class="tool-page-eyebrow">@Eyebrow</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1 class="panel-title">@Title</h1>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Summary))
|
||||||
|
{
|
||||||
|
<div class="tool-page-summary">@Summary</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Actions is not null)
|
||||||
|
{
|
||||||
|
<div class="tool-page-actions">
|
||||||
|
@Actions
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="tool-page-body">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@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!;
|
||||||
|
}
|
||||||
@@ -5,10 +5,10 @@ using RolemasterDb.App.Frontend.AppState;
|
|||||||
using RolemasterDb.App.Features;
|
using RolemasterDb.App.Features;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.WebHost.UseStaticWebAssets();
|
||||||
var connectionString = builder.Configuration.GetConnectionString("RolemasterDb") ?? "Data Source=rolemaster.db";
|
var connectionString = builder.Configuration.GetConnectionString("RolemasterDb") ?? "Data Source=rolemaster.db";
|
||||||
|
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
||||||
.AddInteractiveServerComponents();
|
|
||||||
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
|
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
|
||||||
builder.Services.AddSingleton<CriticalImportArtifactLocator>();
|
builder.Services.AddSingleton<CriticalImportArtifactLocator>();
|
||||||
builder.Services.AddScoped<LookupService>();
|
builder.Services.AddScoped<LookupService>();
|
||||||
@@ -27,13 +27,13 @@ if (!app.Environment.IsDevelopment())
|
|||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||||
app.UseAntiforgery();
|
app.UseAntiforgery();
|
||||||
|
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
var api = app.MapGroup("/api");
|
var api = app.MapGroup("/api");
|
||||||
api.MapGet("/reference-data", async (LookupService lookupService, CancellationToken cancellationToken) =>
|
api.MapGet("/reference-data", async (LookupService lookupService, CancellationToken cancellationToken) => Results.Ok(await lookupService.GetReferenceDataAsync(cancellationToken)));
|
||||||
Results.Ok(await lookupService.GetReferenceDataAsync(cancellationToken)));
|
|
||||||
api.MapPost("/lookup/attack", async (AttackLookupRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
|
api.MapPost("/lookup/attack", async (AttackLookupRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
|
||||||
{
|
{
|
||||||
var result = await lookupService.LookupAttackAsync(request, 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);
|
var result = await lookupService.UpdateCriticalCellAsync(slug, resultId, request, cancellationToken);
|
||||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||||
});
|
});
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
|
||||||
.AddInteractiveServerRenderMode();
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -297,6 +297,105 @@ pre,
|
|||||||
color: color-mix(in srgb, var(--info-3) 46%, var(--text-primary));
|
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 {
|
.panel-title {
|
||||||
margin: 0 0 0.35rem;
|
margin: 0 0 0.35rem;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
@@ -1125,6 +1224,7 @@ select.input-shell {
|
|||||||
.api-grid {
|
.api-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.blazor-error-boundary {
|
.blazor-error-boundary {
|
||||||
@@ -2366,13 +2466,6 @@ select.input-shell {
|
|||||||
justify-items: start;
|
justify-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diagnostics-page-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics-selector-grid {
|
.diagnostics-selector-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
@@ -2602,6 +2695,11 @@ select.input-shell {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-page-actions {
|
||||||
|
grid-auto-flow: row;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
.diagnostics-page-header,
|
.diagnostics-page-header,
|
||||||
.diagnostics-selection-summary,
|
.diagnostics-selection-summary,
|
||||||
.curation-queue-bar-header,
|
.curation-queue-bar-header,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user