Move campaign selector into header

This commit is contained in:
2026-05-18 20:13:14 +02:00
parent ff28f70b51
commit ecc799ae7f
10 changed files with 98 additions and 43 deletions

View File

@@ -1,9 +1,8 @@
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@using RpgRoller.Components.Pages.HomeControls @using RpgRoller.Components.Pages.HomeControls
<CampaignManagementPanel <CampaignManagementPanel
Campaigns="Workspace.State.Campaigns" Campaigns="Workspace.State.Campaigns"
SelectedCampaignId="Workspace.State.SelectedCampaignId"
SelectedCampaign="Workspace.State.SelectedCampaign" SelectedCampaign="Workspace.State.SelectedCampaign"
Rulesets="Workspace.State.Rulesets" Rulesets="Workspace.State.Rulesets"
IsMutating="Workspace.State.IsMutating" IsMutating="Workspace.State.IsMutating"
@@ -11,7 +10,6 @@
CanEditCharacter="Workspace.Campaigns.CanEditCharacter" CanEditCharacter="Workspace.Campaigns.CanEditCharacter"
CanDeleteCharacter="Workspace.Campaigns.CanDeleteCharacter" CanDeleteCharacter="Workspace.Campaigns.CanDeleteCharacter"
CanDeleteCampaign="Workspace.State.CanDeleteSelectedCampaign" CanDeleteCampaign="Workspace.State.CanDeleteSelectedCampaign"
CampaignSelectionChanged="OnCampaignSelectionChangedAsync"
CampaignCreated="Workspace.Campaigns.OnCampaignCreatedAsync" CampaignCreated="Workspace.Campaigns.OnCampaignCreatedAsync"
DeleteCampaignRequested="Workspace.Campaigns.DeleteSelectedCampaignAsync" DeleteCampaignRequested="Workspace.Campaigns.DeleteSelectedCampaignAsync"
CreateCharacterRequested="Workspace.Campaigns.OpenCreateCharacterModal" CreateCharacterRequested="Workspace.Campaigns.OpenCreateCharacterModal"
@@ -22,10 +20,4 @@
@code { @code {
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!; [Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
private async Task OnCampaignSelectionChangedAsync(ChangeEventArgs args)
{
await Workspace.Campaigns.OnCampaignSelectionChangedAsync(args);
await Workspace.RequestRefreshAsync();
}
} }

View File

@@ -1,4 +1,4 @@
<header class="workspace-header"> <header class="workspace-header">
<div class="header-row"> <div class="header-row">
<h1>@Title</h1> <h1>@Title</h1>
@if (User is null) @if (User is null)
@@ -15,7 +15,23 @@
} }
@if (ShowCampaign) @if (ShowCampaign)
{ {
<p class="header-campaign">Campaign: <strong>@(CampaignName ?? "No campaign selected")</strong></p> <div class="header-campaign">
<label for="@CampaignSelectId">Campaign</label>
@if (Campaigns.Count == 0)
{
<span>No campaigns yet</span>
}
else
{
<select id="@CampaignSelectId"
@onchange="CampaignSelectionChanged">
@foreach (var campaign in Campaigns)
{
<option value="@campaign.Id" selected="@(campaign.Id == SelectedCampaignId)">@campaign.Name</option>
}
</select>
}
</div>
} }
<div class="header-connection-cell"> <div class="header-connection-cell">
@if (ShowConnectionState) @if (ShowConnectionState)

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using RpgRoller.Contracts; using RpgRoller.Contracts;
@@ -18,7 +18,13 @@ public partial class AppHeader
[Parameter] public bool ShowCampaign { get; set; } [Parameter] public bool ShowCampaign { get; set; }
[Parameter] public string? CampaignName { get; set; } [Parameter] public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
[Parameter] public Guid? SelectedCampaignId { get; set; }
[Parameter] public string CampaignSelectId { get; set; } = "header-campaign-select";
[Parameter] public EventCallback<ChangeEventArgs> CampaignSelectionChanged { get; set; }
[Parameter] public bool ShowConnectionState { get; set; } = true; [Parameter] public bool ShowConnectionState { get; set; } = true;

View File

@@ -1,4 +1,4 @@
<main class="management-screen"> <main class="management-screen">
<section class="card"> <section class="card">
<div class="section-head"> <div class="section-head">
<h2>Campaign</h2> <h2>Campaign</h2>
@@ -9,13 +9,14 @@
} }
else else
{ {
<label for="campaign-select">Current campaign</label> <div class="campaign-current">
<select id="campaign-select" @onchange="CampaignSelectionChanged"> <span>Current campaign</span>
@foreach (var campaign in Campaigns) <strong>@(SelectedCampaign is null ? "No campaign selected" : SelectedCampaign.Name)</strong>
@if (SelectedCampaign is not null)
{ {
<option value="@campaign.Id" selected="@(campaign.Id == SelectedCampaignId)">@campaign.Name (@campaign.RulesetId), GM: @campaign.Gm.DisplayName, @campaign.CharacterCount characters</option> <p>@SelectedCampaign.RulesetId, GM: @SelectedCampaign.Gm.DisplayName, @SelectedCampaign.Characters.Length characters</p>
} }
</select> </div>
} }
<button type="button" <button type="button"

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using RpgRoller.Contracts; using RpgRoller.Contracts;
@@ -74,9 +74,6 @@ public partial class CampaignManagementPanel
[Parameter] [Parameter]
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = []; public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
[Parameter]
public Guid? SelectedCampaignId { get; set; }
[Parameter] [Parameter]
public CampaignRoster? SelectedCampaign { get; set; } public CampaignRoster? SelectedCampaign { get; set; }
@@ -98,9 +95,6 @@ public partial class CampaignManagementPanel
[Parameter] [Parameter]
public bool CanDeleteCampaign { get; set; } public bool CanDeleteCampaign { get; set; }
[Parameter]
public EventCallback<ChangeEventArgs> CampaignSelectionChanged { get; set; }
[Parameter] [Parameter]
public EventCallback<Guid> CampaignCreated { get; set; } public EventCallback<Guid> CampaignCreated { get; set; }

View File

@@ -1,4 +1,4 @@
<section class="card character-panel"> <section class="card character-panel">
@if (IsCampaignDataLoading) @if (IsCampaignDataLoading)
{ {
<div class="skeleton-stack"> <div class="skeleton-stack">
@@ -9,7 +9,7 @@
} }
else if (SelectedCampaign is null) else if (SelectedCampaign is null)
{ {
<p class="empty">No campaign selected. Choose one in Campaign Management.</p> <p class="empty">No campaign selected. Choose one in the header.</p>
} }
else if (SelectedCampaign.Characters.Length == 0) else if (SelectedCampaign.Characters.Length == 0)
{ {

View File

@@ -1,4 +1,4 @@
@using RpgRoller.Components.Pages.HomeControls @using RpgRoller.Components.Pages.HomeControls
<div class="@AppCssClass"> <div class="@AppCssClass">
<p class="sr-only" aria-live="polite">@State.LiveAnnouncement</p> <p class="sr-only" aria-live="polite">@State.LiveAnnouncement</p>
@@ -17,7 +17,9 @@
<AppHeader <AppHeader
User="State.User" User="State.User"
ShowCampaign="@ShowCampaignInHeader" ShowCampaign="@ShowCampaignInHeader"
CampaignName="@State.SelectedCampaignName" Campaigns="State.Campaigns"
SelectedCampaignId="State.SelectedCampaignId"
CampaignSelectionChanged="OnHeaderCampaignSelectionChangedAsync"
ShowConnectionState="@ShowConnectionStateInHeader" ShowConnectionState="@ShowConnectionStateInHeader"
ConnectionStateLabel="@State.ConnectionStateLabel" ConnectionStateLabel="@State.ConnectionStateLabel"
ConnectionStateCssClass="@State.ConnectionStateCssClass" ConnectionStateCssClass="@State.ConnectionStateCssClass"

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using RpgRoller.Components.Pages.HomeControls; using RpgRoller.Components.Pages.HomeControls;
@@ -84,6 +84,12 @@ public partial class Workspace : IAsyncDisposable
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task OnHeaderCampaignSelectionChangedAsync(ChangeEventArgs args)
{
await Campaigns.OnCampaignSelectionChangedAsync(args);
await RequestRefreshAsync();
}
private Task RedirectToPlayAsync() private Task RedirectToPlayAsync()
{ {
if (IsPlayRoute) if (IsPlayRoute)

View File

@@ -1,4 +1,4 @@
using RpgRoller.Components.Pages.HomeControls; using RpgRoller.Components.Pages.HomeControls;
using RpgRoller.Contracts; using RpgRoller.Contracts;
using RpgRoller.Domain; using RpgRoller.Domain;
@@ -91,10 +91,6 @@ public sealed class WorkspaceState
public HashSet<Guid> CampaignLogDetailsLoading { get; } = []; public HashSet<Guid> CampaignLogDetailsLoading { get; } = [];
public Dictionary<Guid, string> CampaignLogDetailErrors { get; } = []; public Dictionary<Guid, string> CampaignLogDetailErrors { get; } = [];
public string? SelectedCampaignName => SelectedCampaign?.Name ??
Campaigns.FirstOrDefault(campaign => campaign.Id == SelectedCampaignId)
?.Name;
public CharacterSummary? SelectedCharacter => public CharacterSummary? SelectedCharacter =>
SelectedCampaign?.Characters.FirstOrDefault(character => character.Id == SelectedCharacterId); SelectedCampaign?.Characters.FirstOrDefault(character => character.Id == SelectedCharacterId);

View File

@@ -1,4 +1,4 @@
:root { :root {
--bg-top: #f7f0d8; --bg-top: #f7f0d8;
--bg-bottom: #ecdfc4; --bg-bottom: #ecdfc4;
--button-hover: #dccfb4; --button-hover: #dccfb4;
@@ -120,14 +120,33 @@ h3 {
font-size: 1.15rem; font-size: 1.15rem;
} }
.header-identity, .header-identity {
.header-campaign {
margin: 0; margin: 0;
white-space: nowrap; white-space: nowrap;
} }
.header-campaign { .header-campaign {
display: flex;
align-items: center;
gap: 0.35rem;
color: var(--muted); color: var(--muted);
min-width: 12rem;
white-space: nowrap;
}
.header-campaign label {
font-weight: 700;
}
.header-campaign select {
max-width: 16rem;
min-width: 9rem;
padding: 0.25rem 0.45rem;
}
.header-campaign span {
font-weight: 700;
color: var(--text);
} }
.header-connection-cell { .header-connection-cell {
@@ -156,6 +175,20 @@ h3 {
gap: 0.75rem; gap: 0.75rem;
} }
.campaign-current {
display: grid;
gap: 0.15rem;
}
.campaign-current span,
.campaign-current p {
color: var(--muted);
}
.campaign-current p {
margin: 0;
}
.auth-grid { .auth-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1180,6 +1213,15 @@ select:focus-visible {
white-space: normal; white-space: normal;
} }
.header-campaign {
flex-wrap: wrap;
min-width: 0;
}
.header-campaign select {
max-width: 100%;
}
.mobile-bottom-nav { .mobile-bottom-nav {
display: flex; display: flex;
} }