Overhaul workspace UX for denser play workflow
This commit is contained in:
14
FAQ.md
14
FAQ.md
@@ -77,6 +77,20 @@ Authenticated application state and behavior were moved into `Components/Pages/W
|
|||||||
|
|
||||||
`Workspace` initializes authenticated session data after the first render (`OnAfterRenderAsync`). During that first render pass, the header now intentionally shows a null-safe fallback label instead of dereferencing user fields before `/api/me` has been loaded.
|
`Workspace` initializes authenticated session data after the first render (`OnAfterRenderAsync`). During that first render pass, the header now intentionally shows a null-safe fallback label instead of dereferencing user fields before `/api/me` has been loaded.
|
||||||
|
|
||||||
|
## Where did Play/Campaign Management switching move?
|
||||||
|
|
||||||
|
Screen switching is now inside the header hamburger menu. The menu exposes `Play` and `Campaign Management` options while keeping the top bar compact.
|
||||||
|
|
||||||
|
## How do I create, edit, and roll skills in the Play column now?
|
||||||
|
|
||||||
|
Skills now use inline row chip actions:
|
||||||
|
|
||||||
|
- `✎` chip on each skill row to edit that skill
|
||||||
|
- `⚄` chip on each skill row to roll immediately
|
||||||
|
- a final `+` dummy row styled like a skill row to create a new skill
|
||||||
|
|
||||||
|
Roll visibility remains controlled in the skills header row.
|
||||||
|
|
||||||
## Why is auth form state kept in `AuthSection` instead of `Home`?
|
## Why is auth form state kept in `AuthSection` instead of `Home`?
|
||||||
|
|
||||||
Auth inputs, validation, and submit workflows are transient UI concerns, so they now live in `AuthSection`. `Home` keeps shared session/workspace state and cross-control refresh/orchestration only.
|
Auth inputs, validation, and submit workflows are transient UI concerns, so they now live in `AuthSection`. `Home` keeps shared session/workspace state and cross-control refresh/orchestration only.
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ Tracking against `UX.md` tasks and decisions.
|
|||||||
- Home was simplified to a minimal gateway (`Loading` / `Anonymous` / `Workspace`) in a single `Home.razor.cs` class.
|
- Home was simplified to a minimal gateway (`Loading` / `Anonymous` / `Workspace`) in a single `Home.razor.cs` class.
|
||||||
- The authenticated workspace shell/state/behavior was moved to `Components/Pages/Workspace.razor`.
|
- The authenticated workspace shell/state/behavior was moved to `Components/Pages/Workspace.razor`.
|
||||||
- Workspace header user identity rendering is now null-safe during first render (`Loading user...` fallback until `/api/me` loads).
|
- Workspace header user identity rendering is now null-safe during first render (`Loading user...` fallback until `/api/me` loads).
|
||||||
|
- Workspace header was compacted into a single horizontal row with hamburger menu screen switching and link-style logout.
|
||||||
- Concern controls now own their local form state and mutation workflows; the workspace host handles shared cross-control state refresh.
|
- Concern controls now own their local form state and mutation workflows; the workspace host handles shared cross-control state refresh.
|
||||||
- Skill create/edit flow is now owned by `CharacterPanel` (where characters and their skills are presented together).
|
- Skill create/edit flow is now owned by `CharacterPanel` (where characters and their skills are presented together).
|
||||||
|
- Skill interactions are now row-local chip actions (edit/roll) with an inline dummy `+` row for create-skill.
|
||||||
|
- Campaign log now auto-scrolls to the newest entry when new entries arrive.
|
||||||
|
|
||||||
## UX Checklist
|
## UX Checklist
|
||||||
|
|
||||||
@@ -19,8 +22,8 @@ Tracking against `UX.md` tasks and decisions.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 9.1 App load + session restore | Implemented | Health check on load, rulesets/session load, unauthorized session reset, API unhealthy retry banner. |
|
| 9.1 App load + session restore | Implemented | Health check on load, rulesets/session load, unauthorized session reset, API unhealthy retry banner. |
|
||||||
| 9.2 Authentication view | Implemented | Register/login cards, required validation, register password length check, server-error display. |
|
| 9.2 Authentication view | Implemented | Register/login cards, required validation, register password length check, server-error display. |
|
||||||
| 9.3 Shared authenticated header | Implemented | User chip, campaign/active context, connection state, screen switch, refresh, logout. |
|
| 9.3 Shared authenticated header | Implemented | Compact single-row header with user/campaign/connection context, hamburger menu screen switch, and link-style logout. |
|
||||||
| 9.4 Play screen character column | Implemented | Character icon tabs, sheet, modal edit/create flows, selected-tab-as-active behavior, skill list, d6 skill options (wild/fumble), roll controls, and die-state visualized last roll card. |
|
| 9.4 Play screen character column | Implemented | Character icon tabs, merged character+skills header row, modal edit/create flows, inline per-skill edit/roll chips, d6 skill options (wild/fumble), and die-state visualized last roll card. |
|
||||||
| 9.5 Play screen log column | Implemented | Chronological feed, private/public badges, private perspective styles (roller vs GM), per-entry dice visualization with die-state flags, local time + ISO tooltip. |
|
| 9.5 Play screen log column | Implemented | Chronological feed, private/public badges, private perspective styles (roller vs GM), per-entry dice visualization with die-state flags, local time + ISO tooltip. |
|
||||||
| 9.6 Campaign management screen | Implemented | Campaign selector/summary, create form, details card, character management actions with modal edit pattern. |
|
| 9.6 Campaign management screen | Implemented | Campaign selector/summary, create form, details card, character management actions with modal edit pattern. |
|
||||||
| 9.7 Tablet/mobile bottom bar | Implemented | `Character` / `Log` panel switch in play screen and per-tab session persistence. |
|
| 9.7 Tablet/mobile bottom bar | Implemented | `Character` / `Log` panel switch in play screen and per-tab session persistence. |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<aside class="card log-panel">
|
<aside @ref="LogPanelRef" class="card log-panel">
|
||||||
<div class="section-head"><h2>Campaign Log</h2></div>
|
<div class="section-head"><h2>Campaign Log</h2></div>
|
||||||
@if (IsCampaignDataLoading)
|
@if (IsCampaignDataLoading)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages.HomeControls;
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
@@ -7,6 +8,37 @@ namespace RpgRoller.Components.Pages.HomeControls;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public partial class CampaignLogPanel
|
public partial class CampaignLogPanel
|
||||||
{
|
{
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (IsCampaignDataLoading || CampaignLog.Count == 0)
|
||||||
|
{
|
||||||
|
LastRenderedLogCount = CampaignLog.Count;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstRender || CampaignLog.Count > LastRenderedLogCount)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("rpgRollerApi.scrollElementToBottom", LogPanelRef);
|
||||||
|
}
|
||||||
|
catch (JSDisconnectedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex) when (ex.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LastRenderedLogCount = CampaignLog.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IJSRuntime JS { get; set; } = null!;
|
||||||
|
|
||||||
|
private ElementReference LogPanelRef { get; set; }
|
||||||
|
private int LastRenderedLogCount { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsCampaignDataLoading { get; set; }
|
public bool IsCampaignDataLoading { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<section class="card character-panel">
|
<section class="card character-panel">
|
||||||
<div class="section-head"><h2>Character Context</h2></div>
|
|
||||||
@if (IsCampaignDataLoading)
|
@if (IsCampaignDataLoading)
|
||||||
{
|
{
|
||||||
<div class="skeleton-stack">
|
<div class="skeleton-stack">
|
||||||
@@ -31,27 +30,25 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (SelectedCharacter is not null)
|
@if (SelectedCharacter is not null)
|
||||||
{
|
{
|
||||||
<article class="character-sheet">
|
|
||||||
<h3>@SelectedCharacter.Name</h3>
|
|
||||||
<p>Owner: @OwnerLabel(SelectedCharacter.OwnerUserId)</p>
|
|
||||||
<p>Campaign: @SelectedCampaign.Name</p>
|
|
||||||
<span class="badge active">Active</span>
|
|
||||||
<div class="inline-actions">
|
|
||||||
<button type="button" disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
|
||||||
@onclick="() => EditCharacterRequested.InvokeAsync(SelectedCharacter)">Edit Character
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="skills-section">
|
<article class="skills-section">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h3>Skills</h3>
|
<h3 class="skills-heading">@SelectedCharacter.Name <span
|
||||||
<div class="inline-actions">
|
class="muted">| Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name</span>
|
||||||
<button type="button" disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
</h3>
|
||||||
@onclick="OpenCreateSkillModal">Create Skill
|
<div class="chip-toolbar">
|
||||||
</button>
|
<label class="visibility-control" for="roll-visibility">Visibility</label>
|
||||||
<button type="button"
|
<select id="roll-visibility" value="@RollVisibility" @onchange="OnRollVisibilityChangedAsync">
|
||||||
disabled="@(IsMutating || SelectedSkill is null || !CanEditSkill(SelectedSkill))"
|
<option value="public">Public</option>
|
||||||
@onclick="OpenEditSkillModal">Edit Skill
|
<option value="private">Private</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chip-button"
|
||||||
|
title="Edit character"
|
||||||
|
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||||
|
@onclick="() => EditCharacterRequested.InvokeAsync(SelectedCharacter)">
|
||||||
|
<span aria-hidden="true">✎</span>
|
||||||
|
<span class="sr-only">Edit character</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,29 +56,49 @@
|
|||||||
{
|
{
|
||||||
<p class="empty">No skills for this character yet.</p>
|
<p class="empty">No skills for this character yet.</p>
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="skill-list">
|
<div class="skill-list">
|
||||||
@foreach (var skill in SelectedCharacterSkills)
|
@foreach (var skill in SelectedCharacterSkills)
|
||||||
{
|
{
|
||||||
var isSelectedSkill = SelectedSkillId == skill.Id;
|
<div class="skill-item">
|
||||||
<button type="button" class="skill-item @(isSelectedSkill ? "active" : string.Empty)"
|
<div class="skill-details">
|
||||||
@onclick="() => SkillSelected.InvokeAsync(skill.Id)">
|
<strong>@skill.Name</strong>
|
||||||
<strong>@skill.Name</strong><span>@SkillDefinitionLabel(skill)</span>
|
<span>@SkillDefinitionLabel(skill)</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-chip-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chip-button"
|
||||||
|
title="Edit skill"
|
||||||
|
disabled="@(IsMutating || !CanEditSkill(skill))"
|
||||||
|
@onclick="() => OpenEditSkillModal(skill)">
|
||||||
|
<span aria-hidden="true">✎</span>
|
||||||
|
<span class="sr-only">Edit @skill.Name</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chip-button"
|
||||||
|
title="Roll skill"
|
||||||
|
disabled="@(IsMutating || !CanRollSkill(skill))"
|
||||||
|
@onclick="() => RollSkillAsync(skill.Id)">
|
||||||
|
<span aria-hidden="true">⚄</span>
|
||||||
|
<span class="sr-only">Roll @skill.Name</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<form class="roll-panel" @onsubmit="OnRollSubmitAsync" @onsubmit:preventDefault>
|
<button
|
||||||
<label for="roll-visibility">Visibility</label>
|
type="button"
|
||||||
<select id="roll-visibility" value="@RollVisibility" @onchange="OnRollVisibilityChangedAsync">
|
class="skill-item create-skill-item"
|
||||||
<option value="public">Public</option>
|
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||||
<option value="private">Private</option>
|
@onclick="OpenCreateSkillModal">
|
||||||
</select>
|
<span class="skill-create-icon" aria-hidden="true">+</span>
|
||||||
<button type="submit"
|
<span>Add skill</span>
|
||||||
disabled="@(IsMutating || SelectedSkill is null || !CanRollSkill(SelectedSkill))">Roll Skill
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
|
@if (SelectedCharacterSkills.Count > 0)
|
||||||
|
{
|
||||||
|
<p class="muted">Use skill chips to edit or roll directly.</p>
|
||||||
|
}
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,18 +21,15 @@ public partial class CharacterPanel
|
|||||||
ShowCreateSkillModal = true;
|
ShowCreateSkillModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenEditSkillModal()
|
private void OpenEditSkillModal(SkillSummary skill)
|
||||||
{
|
{
|
||||||
if (SelectedSkill is null)
|
EditingSkillId = skill.Id;
|
||||||
return;
|
|
||||||
|
|
||||||
EditingSkillId = SelectedSkill.Id;
|
|
||||||
EditSkillInitialModel = new()
|
EditSkillInitialModel = new()
|
||||||
{
|
{
|
||||||
Name = SelectedSkill.Name,
|
Name = skill.Name,
|
||||||
DiceRollDefinition = SelectedSkill.DiceRollDefinition,
|
DiceRollDefinition = skill.DiceRollDefinition,
|
||||||
WildDice = SelectedSkill.WildDice,
|
WildDice = skill.WildDice,
|
||||||
AllowFumble = SelectedSkill.AllowFumble
|
AllowFumble = skill.AllowFumble
|
||||||
};
|
};
|
||||||
|
|
||||||
EditSkillFormVersion++;
|
EditSkillFormVersion++;
|
||||||
@@ -64,9 +61,9 @@ public partial class CharacterPanel
|
|||||||
await RollVisibilityChanged.InvokeAsync(selectedVisibility);
|
await RollVisibilityChanged.InvokeAsync(selectedVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnRollSubmitAsync()
|
private async Task RollSkillAsync(Guid skillId)
|
||||||
{
|
{
|
||||||
await RollRequested.InvokeAsync();
|
await RollRequested.InvokeAsync(skillId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string InitialsFor(string value)
|
private static string InitialsFor(string value)
|
||||||
@@ -107,12 +104,6 @@ public partial class CharacterPanel
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<SkillSummary> SelectedCharacterSkills { get; set; } = [];
|
public IReadOnlyList<SkillSummary> SelectedCharacterSkills { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public Guid? SelectedSkillId { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public SkillSummary? SelectedSkill { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsD6 { get; set; }
|
public bool IsD6 { get; set; }
|
||||||
|
|
||||||
@@ -143,9 +134,6 @@ public partial class CharacterPanel
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<Guid> CharacterSelected { get; set; }
|
public EventCallback<Guid> CharacterSelected { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public EventCallback<Guid> SkillSelected { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<CharacterSummary> EditCharacterRequested { get; set; }
|
public EventCallback<CharacterSummary> EditCharacterRequested { get; set; }
|
||||||
|
|
||||||
@@ -156,5 +144,5 @@ public partial class CharacterPanel
|
|||||||
public EventCallback<Guid> SkillUpdated { get; set; }
|
public EventCallback<Guid> SkillUpdated { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback RollRequested { get; set; }
|
public EventCallback<Guid> RollRequested { get; set; }
|
||||||
}
|
}
|
||||||
@@ -15,36 +15,47 @@
|
|||||||
|
|
||||||
<div class="workspace-shell">
|
<div class="workspace-shell">
|
||||||
<header class="workspace-header">
|
<header class="workspace-header">
|
||||||
<div class="header-group brand">
|
<div class="header-row">
|
||||||
<h1>RpgRoller</h1>
|
<h1>RpgRoller</h1>
|
||||||
<p>Tabletop utility cockpit</p>
|
|
||||||
</div>
|
|
||||||
<div class="header-group context">
|
|
||||||
@if (User is null)
|
@if (User is null)
|
||||||
{
|
{
|
||||||
<p><strong>Loading user...</strong></p>
|
<p class="header-identity"><strong>Loading user...</strong></p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p><strong>@User.DisplayName</strong> <span class="muted">(@User.Username)</span></p>
|
<p class="header-identity"><strong>@User.DisplayName</strong> <span class="muted">(@User.Username)</span></p>
|
||||||
}
|
}
|
||||||
<p>Campaign: <strong>@(SelectedCampaignName ?? "No campaign selected")</strong></p>
|
|
||||||
<p>Active: <strong>@(ActiveCharacterName ?? "None selected")</strong></p>
|
|
||||||
</div>
|
|
||||||
<div class="header-group controls">
|
|
||||||
<p class="connection @ConnectionStateCssClass">@ConnectionStateLabel</p>
|
<p class="connection @ConnectionStateCssClass">@ConnectionStateLabel</p>
|
||||||
<div class="switch-group" role="tablist" aria-label="Screen selector">
|
<p class="header-campaign">Campaign: <strong>@(SelectedCampaignName ?? "No campaign selected")</strong></p>
|
||||||
<button type="button" class="switch @(CurrentScreen == "play" ? "active" : string.Empty)"
|
<div class="header-menu-wrap">
|
||||||
|
<button
|
||||||
|
id="screen-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="menu-toggle"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="@IsScreenMenuOpen"
|
||||||
|
aria-controls="screen-menu"
|
||||||
|
@onclick="ToggleScreenMenu">
|
||||||
|
<span aria-hidden="true">☰</span>
|
||||||
|
<span class="menu-toggle-label">@CurrentScreenLabel</span>
|
||||||
|
</button>
|
||||||
|
@if (IsScreenMenuOpen)
|
||||||
|
{
|
||||||
|
<div id="screen-menu" class="screen-menu" role="menu" aria-labelledby="screen-menu-button">
|
||||||
|
<button type="button"
|
||||||
|
class="menu-item @(IsPlayScreen ? "active" : string.Empty)"
|
||||||
|
role="menuitem"
|
||||||
@onclick="SwitchToPlayAsync">Play
|
@onclick="SwitchToPlayAsync">Play
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="switch @(CurrentScreen == "management" ? "active" : string.Empty)"
|
<button type="button"
|
||||||
|
class="menu-item @(IsManagementScreen ? "active" : string.Empty)"
|
||||||
|
role="menuitem"
|
||||||
@onclick="SwitchToManagementAsync">Campaign Management
|
@onclick="SwitchToManagementAsync">Campaign Management
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
}
|
||||||
<button type="button" @onclick="ManualRefreshAsync" disabled="@IsMutating">Refresh</button>
|
|
||||||
<button type="button" class="ghost" @onclick="LogoutAsync" disabled="@IsMutating">Logout</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<a href="" class="logout-link" @onclick:preventDefault="true" @onclick="LogoutAsync">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -63,8 +74,6 @@
|
|||||||
SelectedCharacter="SelectedCharacter"
|
SelectedCharacter="SelectedCharacter"
|
||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
SelectedCharacterSkills="SelectedCharacterSkills"
|
SelectedCharacterSkills="SelectedCharacterSkills"
|
||||||
SelectedSkillId="SelectedSkillId"
|
|
||||||
SelectedSkill="SelectedSkill"
|
|
||||||
IsD6="IsSelectedCampaignD6"
|
IsD6="IsSelectedCampaignD6"
|
||||||
RollVisibility="RollVisibility"
|
RollVisibility="RollVisibility"
|
||||||
RollVisibilityChanged="OnRollVisibilityChanged"
|
RollVisibilityChanged="OnRollVisibilityChanged"
|
||||||
@@ -75,11 +84,10 @@
|
|||||||
CanEditSkill="CanEditSkill"
|
CanEditSkill="CanEditSkill"
|
||||||
CanRollSkill="CanRollSkill"
|
CanRollSkill="CanRollSkill"
|
||||||
CharacterSelected="SelectCharacterAsync"
|
CharacterSelected="SelectCharacterAsync"
|
||||||
SkillSelected="SelectSkill"
|
|
||||||
EditCharacterRequested="OpenEditCharacterModal"
|
EditCharacterRequested="OpenEditCharacterModal"
|
||||||
SkillCreated="OnSkillCreatedAsync"
|
SkillCreated="OnSkillCreatedAsync"
|
||||||
SkillUpdated="OnSkillUpdatedAsync"
|
SkillUpdated="OnSkillUpdatedAsync"
|
||||||
RollRequested="RollSelectedSkillAsync"/>
|
RollRequested="RollSkillAsync"/>
|
||||||
|
|
||||||
<CampaignLogPanel
|
<CampaignLogPanel
|
||||||
IsCampaignDataLoading="IsCampaignDataLoading"
|
IsCampaignDataLoading="IsCampaignDataLoading"
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
SelectedCampaign = null;
|
SelectedCampaign = null;
|
||||||
CampaignLog = [];
|
CampaignLog = [];
|
||||||
SelectedCharacterId = null;
|
SelectedCharacterId = null;
|
||||||
SelectedSkillId = null;
|
|
||||||
ConnectionState = "offline";
|
ConnectionState = "offline";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -157,7 +156,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
SelectedCampaign = await ApiClient.RequestAsync<CampaignDetails>("GET", $"/api/campaigns/{campaignId}");
|
SelectedCampaign = await ApiClient.RequestAsync<CampaignDetails>("GET", $"/api/campaigns/{campaignId}");
|
||||||
CampaignLog = (await ApiClient.RequestAsync<IReadOnlyList<CampaignLogEntry>>("GET", $"/api/campaigns/{campaignId}/log")).ToList();
|
CampaignLog = (await ApiClient.RequestAsync<IReadOnlyList<CampaignLogEntry>>("GET", $"/api/campaigns/{campaignId}/log")).ToList();
|
||||||
SyncSelectedCharacter();
|
SyncSelectedCharacter();
|
||||||
SyncSelectedSkill();
|
|
||||||
await EnsureSelectedCharacterActiveAsync();
|
await EnsureSelectedCharacterActiveAsync();
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||||
@@ -226,6 +224,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private async Task SwitchScreenAsync(string screen)
|
private async Task SwitchScreenAsync(string screen)
|
||||||
{
|
{
|
||||||
CurrentScreen = string.Equals(screen, "management", StringComparison.OrdinalIgnoreCase) ? "management" : "play";
|
CurrentScreen = string.Equals(screen, "management", StringComparison.OrdinalIgnoreCase) ? "management" : "play";
|
||||||
|
IsScreenMenuOpen = false;
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, CurrentScreen);
|
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, CurrentScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +263,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, campaignId.ToString());
|
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, campaignId.ToString());
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
await SyncStateEventsAsync();
|
await SyncStateEventsAsync();
|
||||||
|
IsScreenMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnCampaignCreatedAsync(Guid campaignId)
|
private async Task OnCampaignCreatedAsync(Guid campaignId)
|
||||||
@@ -327,7 +327,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private async Task SelectCharacterAsync(Guid characterId)
|
private async Task SelectCharacterAsync(Guid characterId)
|
||||||
{
|
{
|
||||||
SelectedCharacterId = characterId;
|
SelectedCharacterId = characterId;
|
||||||
SyncSelectedSkill();
|
|
||||||
await EnsureSelectedCharacterActiveAsync();
|
await EnsureSelectedCharacterActiveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,25 +366,37 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
SetStatus("Skill created.", false);
|
SetStatus("Skill created.", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnSkillUpdatedAsync(Guid skillId)
|
private async Task OnSkillUpdatedAsync(Guid _)
|
||||||
{
|
{
|
||||||
SelectedSkillId = skillId;
|
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
SetStatus("Skill updated.", false);
|
SetStatus("Skill updated.", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RollSelectedSkillAsync()
|
private async Task RollSkillAsync(Guid skillId)
|
||||||
{
|
{
|
||||||
if (SelectedSkill is null)
|
if (SelectedCampaign is null)
|
||||||
{
|
{
|
||||||
SetStatus("Select a skill to roll.", true);
|
SetStatus("No campaign selected.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedSkill = SelectedCampaign.Skills.FirstOrDefault(skill => skill.Id == skillId);
|
||||||
|
if (selectedSkill is null)
|
||||||
|
{
|
||||||
|
SetStatus("Skill is no longer available. Refresh campaign data.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanRollSkill(selectedSkill))
|
||||||
|
{
|
||||||
|
SetStatus("You are not allowed to roll this skill.", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{SelectedSkill.Id}/roll", new RollSkillRequest(RollVisibility));
|
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{selectedSkill.Id}/roll", new RollSkillRequest(RollVisibility));
|
||||||
|
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
SetStatus("Roll recorded.", false);
|
SetStatus("Roll recorded.", false);
|
||||||
@@ -407,11 +418,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectSkill(Guid skillId)
|
|
||||||
{
|
|
||||||
SelectedSkillId = skillId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanEditSkill(SkillSummary skill)
|
private bool CanEditSkill(SkillSummary skill)
|
||||||
{
|
{
|
||||||
if (SelectedCampaign is null)
|
if (SelectedCampaign is null)
|
||||||
@@ -526,21 +532,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
SelectedCharacterId = SelectedCampaign.Characters[0].Id;
|
SelectedCharacterId = SelectedCampaign.Characters[0].Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncSelectedSkill()
|
|
||||||
{
|
|
||||||
var skills = SelectedCharacterSkills;
|
|
||||||
if (skills.Count == 0)
|
|
||||||
{
|
|
||||||
SelectedSkillId = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SelectedSkillId.HasValue && skills.Any(skill => skill.Id == SelectedSkillId.Value))
|
|
||||||
return;
|
|
||||||
|
|
||||||
SelectedSkillId = skills[0].Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string OwnerLabel(Guid ownerUserId)
|
private string OwnerLabel(Guid ownerUserId)
|
||||||
{
|
{
|
||||||
if (User is not null && ownerUserId == User.Id)
|
if (User is not null && ownerUserId == User.Id)
|
||||||
@@ -624,7 +615,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
Campaigns = [];
|
Campaigns = [];
|
||||||
CampaignLog = [];
|
CampaignLog = [];
|
||||||
SelectedCharacterId = null;
|
SelectedCharacterId = null;
|
||||||
SelectedSkillId = null;
|
|
||||||
LastRoll = null;
|
LastRoll = null;
|
||||||
ShowCreateCharacterModal = false;
|
ShowCreateCharacterModal = false;
|
||||||
ShowEditCharacterModal = false;
|
ShowEditCharacterModal = false;
|
||||||
@@ -646,6 +636,11 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
LiveAnnouncement = message;
|
LiveAnnouncement = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleScreenMenu()
|
||||||
|
{
|
||||||
|
IsScreenMenuOpen = !IsScreenMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private IJSRuntime JS { get; set; } = null!;
|
private IJSRuntime JS { get; set; } = null!;
|
||||||
|
|
||||||
@@ -660,7 +655,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
||||||
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
||||||
private Guid? SelectedCharacterId { get; set; }
|
private Guid? SelectedCharacterId { get; set; }
|
||||||
private Guid? SelectedSkillId { get; set; }
|
|
||||||
private RollResult? LastRoll { get; set; }
|
private RollResult? LastRoll { get; set; }
|
||||||
private string RollVisibility { get; set; } = "public";
|
private string RollVisibility { get; set; } = "public";
|
||||||
|
|
||||||
@@ -674,6 +668,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private string MobilePanel { get; set; } = "character";
|
private string MobilePanel { get; set; } = "character";
|
||||||
private string ConnectionState { get; set; } = "offline";
|
private string ConnectionState { get; set; } = "offline";
|
||||||
private string LiveAnnouncement { get; set; } = string.Empty;
|
private string LiveAnnouncement { get; set; } = string.Empty;
|
||||||
|
private bool IsScreenMenuOpen { get; set; }
|
||||||
|
|
||||||
private bool ShowCreateCharacterModal { get; set; }
|
private bool ShowCreateCharacterModal { get; set; }
|
||||||
private bool ShowEditCharacterModal { get; set; }
|
private bool ShowEditCharacterModal { get; set; }
|
||||||
@@ -694,12 +689,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private CharacterSummary? SelectedCharacter =>
|
private CharacterSummary? SelectedCharacter =>
|
||||||
SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId);
|
SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId);
|
||||||
|
|
||||||
private SkillSummary? SelectedSkill =>
|
|
||||||
SelectedCampaign?.Skills.FirstOrDefault(s => s.Id == SelectedSkillId);
|
|
||||||
|
|
||||||
private string? ActiveCharacterName =>
|
|
||||||
SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId)?.Name;
|
|
||||||
|
|
||||||
private bool IsCurrentUserGm =>
|
private bool IsCurrentUserGm =>
|
||||||
SelectedCampaign is not null && User is not null && SelectedCampaign.Gm.Id == User.Id;
|
SelectedCampaign is not null && User is not null && SelectedCampaign.Gm.Id == User.Id;
|
||||||
|
|
||||||
@@ -711,6 +700,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase);
|
private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase);
|
||||||
private bool IsManagementScreen => !IsPlayScreen;
|
private bool IsManagementScreen => !IsPlayScreen;
|
||||||
|
private string CurrentScreenLabel => IsPlayScreen ? "Play" : "Campaign Management";
|
||||||
|
|
||||||
private string ConnectionStateLabel => ConnectionState switch
|
private string ConnectionStateLabel => ConnectionState switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -179,11 +179,20 @@ window.rpgRollerApi = (() => {
|
|||||||
connectStateStream();
|
connectStateStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollElementToBottom(element) {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request,
|
request,
|
||||||
getSessionValue,
|
getSessionValue,
|
||||||
setSessionValue,
|
setSessionValue,
|
||||||
startStateEvents,
|
startStateEvents,
|
||||||
stopStateEvents
|
stopStateEvents,
|
||||||
|
scrollElementToBottom
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -81,32 +81,35 @@ h3 {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 0.75rem;
|
|
||||||
grid-template-columns: 1.1fr 1.2fr 1fr;
|
|
||||||
background: linear-gradient(120deg, #f1e4c9, #efe0bf);
|
background: linear-gradient(120deg, #f1e4c9, #efe0bf);
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
padding: 0.85rem;
|
padding: 0.5rem 0.7rem;
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-group {
|
.header-row {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 0.2rem;
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand h1 {
|
.header-row h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 1.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand p,
|
.header-identity,
|
||||||
.context p {
|
.header-campaign {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.header-campaign {
|
||||||
justify-items: end;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-group,
|
.switch-group,
|
||||||
@@ -121,7 +124,7 @@ h3 {
|
|||||||
background: color-mix(in srgb, var(--card) 94%, #ffffff 6%);
|
background: color-mix(in srgb, var(--card) 94%, #ffffff 6%);
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
padding: 0.85rem;
|
padding: 0.7rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -216,7 +219,8 @@ select:focus-visible {
|
|||||||
.section-head {
|
.section-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +235,10 @@ select:focus-visible {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-play .play-screen > * {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.management-screen {
|
.management-screen {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -273,18 +281,18 @@ select:focus-visible {
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-sheet,
|
|
||||||
.skills-section,
|
.skills-section,
|
||||||
.last-roll {
|
.last-roll {
|
||||||
border: 1px dashed #a89066;
|
border: 1px dashed #a89066;
|
||||||
border-radius: 0.65rem;
|
border-radius: 0.65rem;
|
||||||
padding: 0.65rem;
|
padding: 0.55rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.4rem;
|
gap: 0.45rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-play .character-panel {
|
.app-play .character-panel {
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-panel {
|
.log-panel {
|
||||||
@@ -302,25 +310,146 @@ select:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.skill-item {
|
.skill-item {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
background: #f8f1df;
|
background: #f6ebd3;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border: 1px solid #b39f79;
|
border: 1px solid #b39f79;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.45rem 0.55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-item.active {
|
.skill-details {
|
||||||
border-color: #7b5a1f;
|
|
||||||
background: #ecdfc2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-panel {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
gap: 0.1rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-details span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-chip-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-button {
|
||||||
|
min-width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border-color: #8e7b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-skill-item {
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: start;
|
||||||
|
background: #f9f2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-create-icon {
|
||||||
|
width: 1.45rem;
|
||||||
|
height: 1.45rem;
|
||||||
|
border: 1px solid #8e7b57;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-heading {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-toolbar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visibility-control {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-toolbar select {
|
||||||
|
width: auto;
|
||||||
|
padding: 0.2rem 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-menu-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border-color: #8e7b57;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 0.3rem);
|
||||||
|
z-index: 40;
|
||||||
|
min-width: 14.5rem;
|
||||||
|
padding: 0.35rem;
|
||||||
|
background: #fff8ea;
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
box-shadow: 0 8px 16px rgba(34, 24, 9, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border-color: #8e7b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background: #ecd8ae;
|
||||||
|
border-color: #9a7f43;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-link {
|
||||||
|
color: var(--accent-2);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-link:hover {
|
||||||
|
color: #6b2419;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-link:focus-visible {
|
||||||
|
outline: 3px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.roll-total {
|
.roll-total {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -572,7 +701,6 @@ select:focus-visible {
|
|||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.workspace-header {
|
.workspace-header {
|
||||||
position: static;
|
position: static;
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-screen {
|
.play-screen {
|
||||||
@@ -595,6 +723,12 @@ select:focus-visible {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-row h1,
|
||||||
|
.header-identity,
|
||||||
|
.header-campaign {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-bottom-nav {
|
.mobile-bottom-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user