Detach custom roll panel from log feed
This commit is contained in:
@@ -67,7 +67,7 @@ Gameplay capabilities now include:
|
||||
- Rolemaster create/edit forms now keep the expression authoritative, show generic Rolemaster syntax help, and reveal `FumbleRange` only when the expression is an open-ended percentile roll
|
||||
- Rolemaster roll execution now supports generic standard Rolemaster rolls (`NdS+x`, with implicit count `1` for `dS`) plus open-ended percentile (`d100!+x`) with recursive high-end chaining and low-end subtraction based on `FumbleRange`; low-end trigger rolls are shown for auditability but do not count toward the total
|
||||
- Compact campaign-log summaries stay dense for Rolemaster rolls, while lazy-loaded roll detail includes ordered die metadata for each open-ended follow-up step
|
||||
- Play screen campaign logs now include a bottom-mounted custom roll composer that records arbitrary expressions against the selected character without creating a skill; invalid expressions stay inline on the field with tooltip/error styling instead of using error toasts
|
||||
- Play screen campaign logs now include a detached bottom custom-roll panel below the scrollable log feed; it records arbitrary expressions against the selected character without creating a skill, and invalid expressions stay inline on the field with tooltip/error styling instead of using error toasts
|
||||
- Startup migration coverage is validated against a copied temp-file instance of `RpgRoller/App_Data/rpgroller.development.db` before feature work is considered complete
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -1,80 +1,83 @@
|
||||
<aside @ref="LogPanelRef" class="card log-panel">
|
||||
<div class="section-head"><h2>Campaign Log</h2></div>
|
||||
@if (IsCampaignDataLoading)
|
||||
{
|
||||
<div class="skeleton-stack">
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line short"></div>
|
||||
</div>
|
||||
}
|
||||
else if (CampaignLog.Count == 0)
|
||||
{
|
||||
<p class="empty">No log entries yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="log-list">
|
||||
@foreach (var entry in CampaignLog)
|
||||
{
|
||||
var isExpanded = ExpandedRollId == entry.RollId;
|
||||
<li class="log-entry @LogEntryCssClass(entry, isExpanded, FreshRollId == entry.RollId)">
|
||||
<button type="button"
|
||||
class="log-entry-toggle"
|
||||
aria-expanded="@isExpanded"
|
||||
@onclick="() => ToggleRollDetailRequested.InvokeAsync(entry.RollId)">
|
||||
<span class="log-entry-main">
|
||||
<span class="log-entry-copy">
|
||||
<span class="log-entry-actor">@entry.RollerLabel</span>
|
||||
<span class="log-entry-action">rolled</span>
|
||||
<span class="log-entry-skill">@entry.SkillName</span>
|
||||
<span class="log-entry-action">with</span>
|
||||
<span class="log-entry-character">@entry.CharacterName</span>
|
||||
<div @ref="LogFeedRef" class="log-panel-feed">
|
||||
@if (IsCampaignDataLoading)
|
||||
{
|
||||
<div class="skeleton-stack">
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line short"></div>
|
||||
</div>
|
||||
}
|
||||
else if (CampaignLog.Count == 0)
|
||||
{
|
||||
<p class="empty">No log entries yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="log-list">
|
||||
@foreach (var entry in CampaignLog)
|
||||
{
|
||||
var isExpanded = ExpandedRollId == entry.RollId;
|
||||
<li class="log-entry @LogEntryCssClass(entry, isExpanded, FreshRollId == entry.RollId)">
|
||||
<button type="button"
|
||||
class="log-entry-toggle"
|
||||
aria-expanded="@isExpanded"
|
||||
@onclick="() => ToggleRollDetailRequested.InvokeAsync(entry.RollId)">
|
||||
<span class="log-entry-main">
|
||||
<span class="log-entry-copy">
|
||||
<span class="log-entry-actor">@entry.RollerLabel</span>
|
||||
<span class="log-entry-action">rolled</span>
|
||||
<span class="log-entry-skill">@entry.SkillName</span>
|
||||
<span class="log-entry-action">with</span>
|
||||
<span class="log-entry-character">@entry.CharacterName</span>
|
||||
</span>
|
||||
<span class="roll-total inline">@entry.Result</span>
|
||||
</span>
|
||||
<span class="roll-total inline">@entry.Result</span>
|
||||
</span>
|
||||
@if (HasSummary(entry))
|
||||
@if (HasSummary(entry))
|
||||
{
|
||||
<span class="log-summary-row">
|
||||
@foreach (var badge in GetEventBadges(entry))
|
||||
{
|
||||
<span class="log-event-badge @badge.Tone">@badge.Label</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(entry.SummaryText))
|
||||
{
|
||||
<span class="log-summary-text">@entry.SummaryText</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
<span class="log-meta"><span class="badge @entry.VisibilityStyle">@entry.VisibilityLabel</span>
|
||||
<time
|
||||
title="@entry.TimestampUtc.ToString("O")">@entry.TimestampUtc.ToLocalTime().ToString("g")</time>
|
||||
</span>
|
||||
</button>
|
||||
@if (isExpanded)
|
||||
{
|
||||
<span class="log-summary-row">
|
||||
@foreach (var badge in GetEventBadges(entry))
|
||||
<div class="log-detail">
|
||||
@if (IsRollDetailLoading(entry.RollId))
|
||||
{
|
||||
<span class="log-event-badge @badge.Tone">@badge.Label</span>
|
||||
<p class="muted">Loading roll detail...</p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(entry.SummaryText))
|
||||
else if (!string.IsNullOrWhiteSpace(GetRollDetailError(entry.RollId)))
|
||||
{
|
||||
<span class="log-summary-text">@entry.SummaryText</span>
|
||||
<p class="field-error">@GetRollDetailError(entry.RollId)</p>
|
||||
}
|
||||
</span>
|
||||
else if (ResolveRollDetail(entry.RollId) is { } detail)
|
||||
{
|
||||
<RollDiceStrip Dice="detail.Dice" AriaLabel="Log roll dice"/>
|
||||
<p>@detail.Breakdown</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<span class="log-meta"><span class="badge @entry.VisibilityStyle">@entry.VisibilityLabel</span>
|
||||
<time
|
||||
title="@entry.TimestampUtc.ToString("O")">@entry.TimestampUtc.ToLocalTime().ToString("g")</time>
|
||||
</span>
|
||||
</button>
|
||||
@if (isExpanded)
|
||||
{
|
||||
<div class="log-detail">
|
||||
@if (IsRollDetailLoading(entry.RollId))
|
||||
{
|
||||
<p class="muted">Loading roll detail...</p>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(GetRollDetailError(entry.RollId)))
|
||||
{
|
||||
<p class="field-error">@GetRollDetailError(entry.RollId)</p>
|
||||
}
|
||||
else if (ResolveRollDetail(entry.RollId) is { } detail)
|
||||
{
|
||||
<RollDiceStrip Dice="detail.Dice" AriaLabel="Log roll dice"/>
|
||||
<p>@detail.Breakdown</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
|
||||
<form class="custom-roll-composer" @onsubmit="SubmitCustomRollAsync" @onsubmit:preventDefault>
|
||||
<section class="custom-roll-panel" aria-label="Custom roll panel">
|
||||
<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>
|
||||
@@ -98,5 +101,6 @@
|
||||
{
|
||||
<p id="@CustomRollErrorElementId" class="field-error" role="alert">@CustomRollErrorMessage</p>
|
||||
}
|
||||
</form>
|
||||
</form>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
@@ -23,7 +23,7 @@ public partial class CampaignLogPanel
|
||||
{
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("rpgRollerApi.scrollElementToBottom", LogPanelRef);
|
||||
await JS.InvokeVoidAsync("rpgRollerApi.scrollElementToBottom", LogFeedRef);
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
@@ -44,6 +44,7 @@ public partial class CampaignLogPanel
|
||||
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||
|
||||
private ElementReference LogPanelRef { get; set; }
|
||||
private ElementReference LogFeedRef { get; set; }
|
||||
private ElementReference CustomRollInputRef { get; set; }
|
||||
private int LastRenderedLogCount { get; set; }
|
||||
private Guid? LastRenderedLogRollId { get; set; }
|
||||
|
||||
@@ -336,12 +336,21 @@ select:focus-visible {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
align-content: start;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
align-content: stretch;
|
||||
}
|
||||
|
||||
.app-play .log-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-panel-feed {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-right: 0.15rem;
|
||||
}
|
||||
|
||||
.skill-list {
|
||||
@@ -618,11 +627,17 @@ select:focus-visible {
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.custom-roll-panel {
|
||||
border-top: 1px solid color-mix(in srgb, var(--card-border) 72%, #ffffff 28%);
|
||||
background: color-mix(in srgb, var(--card) 88%, #ffffff 12%);
|
||||
border-radius: 0.95rem;
|
||||
padding: 0.85rem 0.9rem 0.9rem;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.custom-roll-composer {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
padding-top: 0.2rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--card-border) 72%, #ffffff 28%);
|
||||
}
|
||||
|
||||
.custom-roll-composer-head {
|
||||
|
||||
Reference in New Issue
Block a user