refactor: finish route-first shell

This commit is contained in:
2026-05-04 21:58:22 +02:00
parent 9c3f7c039e
commit 73dc4a9cd4
14 changed files with 127 additions and 128 deletions

Binary file not shown.

View File

@@ -2,6 +2,10 @@
@inherits AuthenticatedPageBase
<Workspace Route="WorkspaceRoute.Admin" LoggedOut="OnLoggedOutAsync">
<ChildContent Context="workspace">
<AdminWorkspaceContent Workspace="workspace"/>
<WorkspaceRouteView Workspace="workspace">
<ChildContent Context="readyWorkspace">
<AdminWorkspaceContent Workspace="readyWorkspace"/>
</ChildContent>
</WorkspaceRouteView>
</ChildContent>
</Workspace>

View File

@@ -2,6 +2,10 @@
@inherits AuthenticatedPageBase
<Workspace Route="WorkspaceRoute.Campaigns" LoggedOut="OnLoggedOutAsync">
<ChildContent Context="workspace">
<CampaignsWorkspaceContent Workspace="workspace"/>
<WorkspaceRouteView Workspace="workspace">
<ChildContent Context="readyWorkspace">
<CampaignsWorkspaceContent Workspace="readyWorkspace"/>
</ChildContent>
</WorkspaceRouteView>
</ChildContent>
</Workspace>

View File

@@ -82,43 +82,30 @@
</div>
<section class="custom-roll-panel" aria-label="Custom roll panel">
@if (EnableCustomRollComposer)
{
<form class="custom-roll-composer" @onsubmit="SubmitCustomRollAsync" @onsubmit:preventDefault>
<div class="custom-roll-composer-head">
<label for="custom-roll-expression" class="custom-roll-label">Custom roll</label>
<span class="muted">@CustomRollStatusText</span>
</div>
<div class="custom-roll-composer-row">
<input id="custom-roll-expression"
@key="CustomRollInputVersion"
@ref="CustomRollInputRef"
class="@CustomRollInputCssClass"
@bind="CustomRollExpression"
@bind:event="oninput"
placeholder="@CustomRollPlaceholder"
title="@CustomRollInputTitle"
aria-invalid="@HasCustomRollError"
aria-describedby="@CustomRollInputDescribedBy"
disabled="@IsCustomRollDisabled"/>
<button type="submit" disabled="@IsCustomRollDisabled">Roll</button>
</div>
<p class="field-help">@CustomRollHelpText</p>
@if (HasCustomRollError)
{
<p id="@CustomRollErrorElementId" class="field-error" role="alert">@CustomRollErrorMessage</p>
}
</form>
}
else
{
<div class="custom-roll-composer">
<div class="custom-roll-composer-head">
<span class="custom-roll-label">Custom roll</span>
<span class="muted">@CustomRollStatusText</span>
</div>
<p class="field-help">Loading roll composer...</p>
<form class="custom-roll-composer" @onsubmit="SubmitCustomRollAsync" @onsubmit:preventDefault>
<div class="custom-roll-composer-head">
<label for="custom-roll-expression" class="custom-roll-label">Custom roll</label>
<span class="muted">@CustomRollStatusText</span>
</div>
}
<div class="custom-roll-composer-row">
<input id="custom-roll-expression"
@key="CustomRollInputVersion"
@ref="CustomRollInputRef"
class="@CustomRollInputCssClass"
@bind="CustomRollExpression"
@bind:event="oninput"
placeholder="@CustomRollPlaceholder"
title="@CustomRollInputTitle"
aria-invalid="@HasCustomRollError"
aria-describedby="@CustomRollInputDescribedBy"
disabled="@IsCustomRollDisabled"/>
<button type="submit" disabled="@IsCustomRollDisabled">Roll</button>
</div>
<p class="field-help">@CustomRollHelpText</p>
@if (HasCustomRollError)
{
<p id="@CustomRollErrorElementId" class="field-error" role="alert">@CustomRollErrorMessage</p>
}
</form>
</section>
</aside>

View File

@@ -171,8 +171,6 @@ public partial class CampaignLogPanel
[Parameter] public string RollVisibility { get; set; } = "public";
[Parameter] public bool EnableCustomRollComposer { get; set; }
[Parameter] public bool IsMutating { get; set; }
[Parameter] public EventCallback<RollResult> CustomRollCreated { get; set; }

View File

@@ -49,34 +49,20 @@
</span>
</h3>
<div class="skill-filter-wrap">
@if (EnableInteractiveControls)
{
<label class="sr-only" for="skill-filter-input">Filter skills</label>
<input id="skill-filter-input"
class="skill-filter-input"
type="search"
placeholder="Filter skills"
@bind="SkillFilterText"
@bind:event="oninput"/>
}
else
{
<p class="muted">Loading skill controls...</p>
}
<label class="sr-only" for="skill-filter-input">Filter skills</label>
<input id="skill-filter-input"
class="skill-filter-input"
type="search"
placeholder="Filter skills"
@bind="SkillFilterText"
@bind:event="oninput"/>
</div>
<div class="chip-toolbar">
@if (EnableInteractiveControls)
{
<label class="visibility-control" for="roll-visibility">Visibility</label>
<select id="roll-visibility" value="@(RollVisibility == "private" ? "private" : "public")" @onchange="OnRollVisibilityChangedAsync">
<option value="public">Public</option>
<option value="private">Private</option>
</select>
}
else
{
<p class="muted">Visibility: @(RollVisibility == "private" ? "Private" : "Public")</p>
}
<label class="visibility-control" for="roll-visibility">Visibility</label>
<select id="roll-visibility" value="@(RollVisibility == "private" ? "private" : "public")" @onchange="OnRollVisibilityChangedAsync">
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</div>
</div>
@{

View File

@@ -370,8 +370,6 @@ public partial class CharacterPanel
[Parameter] public string RollVisibility { get; set; } = "public";
[Parameter] public bool EnableInteractiveControls { get; set; }
[Parameter] public EventCallback<string> RollVisibilityChanged { get; set; }
[Parameter] public Func<Guid, string> OwnerLabel { get; set; } = _ => string.Empty;

View File

@@ -2,6 +2,10 @@
@inherits AuthenticatedPageBase
<Workspace Route="WorkspaceRoute.Play" LoggedOut="OnLoggedOutAsync">
<ChildContent Context="workspace">
<PlayWorkspaceContent Workspace="workspace"/>
<WorkspaceRouteView Workspace="workspace">
<ChildContent Context="readyWorkspace">
<PlayWorkspaceContent Workspace="readyWorkspace"/>
</ChildContent>
</WorkspaceRouteView>
</ChildContent>
</Workspace>

View File

@@ -12,7 +12,6 @@
SelectedCharacterSkillGroups="Workspace.State.PlaySelectedCharacterSkillGroups"
SelectedCampaignRulesetId="@(Workspace.State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
RollVisibility="Workspace.State.RollVisibility"
EnableInteractiveControls="Workspace.EnableCharacterControls"
RollVisibilityChanged="Workspace.Session.OnRollVisibilityChangedAsync"
OwnerLabel="Workspace.State.OwnerLabel"
SkillDefinitionLabel="Workspace.State.SkillDefinitionLabel"
@@ -38,7 +37,6 @@
SelectedCharacterName="@(Workspace.State.PlaySelectedCharacter?.Name)"
SelectedCampaignRulesetId="@(Workspace.State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
RollVisibility="Workspace.State.RollVisibility"
EnableCustomRollComposer="Workspace.EnableCustomRollComposer"
IsMutating="Workspace.State.IsMutating"
ToggleRollDetailRequested="Workspace.Play.ToggleRollDetailAsync"
ResolveRollDetail="Workspace.Play.ResolveRollDetail"

View File

@@ -14,34 +14,6 @@ public partial class Workspace : IAsyncDisposable
State.IsScreenMenuOpen = false;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
State.HasInteractiveRenderStarted = true;
if (firstRender)
{
await Session.InitializeAsync();
HasSessionInitialized = true;
await InvokeAsync(StateHasChanged);
return;
}
if (!HasSessionInitialized)
return;
if (!EnableCharacterControls)
{
EnableCharacterControls = true;
await InvokeAsync(StateHasChanged);
return;
}
if (EnableCustomRollComposer)
return;
EnableCustomRollComposer = true;
await InvokeAsync(StateHasChanged);
}
[JSInvokable]
public Task OnStateEventReceived(CampaignStateSnapshot state)
{
@@ -126,6 +98,22 @@ public partial class Workspace : IAsyncDisposable
return InvokeAsync(StateHasChanged);
}
private Task InitializeRouteAsync()
{
return InitializationTask ??= InitializeRouteCoreAsync();
}
private async Task InitializeRouteCoreAsync()
{
if (HasSessionInitialized)
return;
State.HasInteractiveRenderStarted = true;
await Session.InitializeAsync();
HasSessionInitialized = true;
await RequestRefreshAsync();
}
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
{
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
@@ -145,16 +133,14 @@ public partial class Workspace : IAsyncDisposable
private WorkspaceState State { get; } = new();
private bool HasSessionInitialized { get; set; }
private bool EnableCharacterControls { get; set; }
private bool EnableCustomRollComposer { get; set; }
private bool IsPlayRoute => Route == WorkspaceRoute.Play;
private bool IsCampaignsRoute => Route == WorkspaceRoute.Campaigns;
private bool IsAdminRoute => Route == WorkspaceRoute.Admin;
private string AppCssClass => IsPlayRoute ? "rr-app app-play" : "rr-app";
private WorkspacePageContext PageContext => new(State, Play, Campaigns, Admin, Scope, Session,
RequestRefreshAsync, EnableCharacterControls, EnableCustomRollComposer, AdminDatabaseDownloadUrl,
HeaderMenuItems, IsPlayRoute, IsCampaignsRoute, IsAdminRoute);
InitializeRouteAsync, HasSessionInitialized, RequestRefreshAsync, AdminDatabaseDownloadUrl, HeaderMenuItems,
IsPlayRoute, IsCampaignsRoute, IsAdminRoute);
private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(State, Feedback, JS, WorkspaceQuery,
() => IsPlayRoute, Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync,
@@ -231,4 +217,5 @@ public partial class Workspace : IAsyncDisposable
private WorkspaceCampaignScopeCoordinator? m_Scope;
private WorkspaceSessionCoordinator? m_Session;
private Task? InitializationTask { get; set; }
}

View File

@@ -9,9 +9,9 @@ public sealed class WorkspacePageContext(
WorkspaceAdminCoordinator admin,
WorkspaceCampaignScopeCoordinator scope,
WorkspaceSessionCoordinator session,
Func<Task> initializeRouteAsync,
bool hasSessionInitialized,
Func<Task> requestRefreshAsync,
bool enableCharacterControls,
bool enableCustomRollComposer,
string adminDatabaseDownloadUrl,
IReadOnlyList<AppHeaderMenuItem> headerMenuItems,
bool isPlayRoute,
@@ -24,9 +24,9 @@ public sealed class WorkspacePageContext(
public WorkspaceAdminCoordinator Admin { get; } = admin;
public WorkspaceCampaignScopeCoordinator Scope { get; } = scope;
public WorkspaceSessionCoordinator Session { get; } = session;
public Func<Task> InitializeRouteAsync { get; } = initializeRouteAsync;
public bool HasSessionInitialized { get; } = hasSessionInitialized;
public Func<Task> RequestRefreshAsync { get; } = requestRefreshAsync;
public bool EnableCharacterControls { get; } = enableCharacterControls;
public bool EnableCustomRollComposer { get; } = enableCustomRollComposer;
public string AdminDatabaseDownloadUrl { get; } = adminDatabaseDownloadUrl;
public IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems { get; } = headerMenuItems;
public bool IsPlayRoute { get; } = isPlayRoute;

View File

@@ -0,0 +1,21 @@
@using Microsoft.AspNetCore.Components
@if (Workspace.HasSessionInitialized)
{
@ChildContent(Workspace)
}
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
await Workspace.InitializeRouteAsync();
await InvokeAsync(StateHasChanged);
}
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
[Parameter, EditorRequired] public RenderFragment<WorkspacePageContext> ChildContent { get; set; } = null!;
}