PlotDirector Phase 18B - Dashboard Card Content Refinement and Equal Height Layout

This commit is contained in:
Nick Beckley 2026-06-30 20:08:24 +01:00
parent 191ed8c540
commit e761e42ff7
6 changed files with 372 additions and 58 deletions

View File

@ -2,9 +2,15 @@
@{
var warningCount = Model.Warnings.Count + Model.AgeContinuityWarnings.Count;
var dependencyCount = Model.DependenciesThisSceneNeeds.Count + Model.DependenciesNeedingThisScene.Count;
var summary = warningCount == 0
? "No active continuity warnings for this scene."
: $"{warningCount} warning{(warningCount == 1 ? "" : "s")} need review.";
var warnings = Model.Warnings.Concat(Model.AgeContinuityWarnings)
.OrderByDescending(x => x.LastDetectedDate)
.ThenByDescending(x => x.CreatedDate)
.Take(3)
.ToList();
var lastValidated = Model.Warnings.Concat(Model.AgeContinuityWarnings)
.Select(x => (DateTime?)x.LastDetectedDate)
.OrderByDescending(x => x)
.FirstOrDefault();
}
<article class="scene-inspector-v2-card" data-scene-summary-part="continuity">
@ -16,19 +22,29 @@
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="warnings" data-scene-id="@Model.SceneID" data-scene-editor-url="@Url.Action("SceneContinuityEditor", "Warnings", new { sceneId = Model.SceneID })">Review warnings</button>
</div>
<div class="scene-inspector-v2-counts">
<div class="@(warningCount > 0 ? "attention" : "")">
<span>Warnings</span>
<strong>@warningCount</strong>
</div>
<div>
<span>Dependencies</span>
<strong>@dependencyCount</strong>
</div>
</div>
<div class="scene-inspector-v2-section">
<span>Continuity health summary</span>
<p>@summary</p>
@if (warningCount == 0)
{
<p class="scene-inspector-v2-good-state">No active continuity warnings for this scene.</p>
@if (lastValidated.HasValue)
{
<p class="muted">Last checked @lastValidated.Value.ToString("d MMM yyyy HH:mm").</p>
}
@if (dependencyCount > 0)
{
<p class="muted">@dependencyCount scene dependenc@(dependencyCount == 1 ? "y" : "ies") linked.</p>
}
}
else
{
<p class="scene-inspector-v2-warning-state">@warningCount warning@(warningCount == 1 ? "" : "s") need review.</p>
<ul class="scene-inspector-v2-compact-list">
@foreach (var warning in warnings)
{
<li><strong>@warning.WarningTypeName</strong> @warning.Message</li>
}
</ul>
}
</div>
</article>

View File

@ -3,6 +3,7 @@
var povCharacterId = Model.POVCharacterID
?? Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterID;
var suggestionCount = Model.CharacterSuggestions.Count + Model.AssetSuggestions.Count + Model.LocationSuggestions.Count;
static string PreviewText(string? value, string fallback) => string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
}
<article class="scene-inspector-v2-card" data-scene-summary-part="people">
@ -21,12 +22,20 @@
}
else
{
<ul class="scene-inspector-v2-list">
<ul class="scene-inspector-v2-character-list">
@foreach (var character in Model.SceneCharacters.Take(6))
{
var isPov = character.CharacterID == povCharacterId;
<li>
<strong>@character.CharacterName</strong>
<span>@(character.CharacterID == povCharacterId ? "POV" : character.RoleInSceneTypeName ?? "In scene")</span>
<partial name="~/Views/Shared/_Avatar.cshtml" model="@(new AvatarViewModel { DisplayName = character.CharacterName, ImagePath = character.CharacterImagePath, ThumbnailPath = character.CharacterThumbnailPath, Size = "sm", CssClass = isPov ? "scene-inspector-v2-avatar-pov" : "" })" />
<div>
<strong>@character.CharacterName</strong>
<span>@(isPov ? "Point of view" : character.RoleInSceneTypeName ?? character.PresenceTypeName ?? "In scene")</span>
</div>
@if (isPov)
{
<span class="scene-inspector-v2-mini-badge">POV</span>
}
</li>
}
</ul>
@ -37,27 +46,50 @@
}
</div>
<div class="scene-inspector-v2-counts">
<div>
<span>Character count</span>
<strong>@Model.SceneCharacters.Count</strong>
</div>
<div>
<span>Knowledge changes</span>
<strong>@Model.CharacterKnowledge.Count</strong>
</div>
<div>
<span>Relationship changes</span>
<strong>@Model.RelationshipEvents.Count</strong>
</div>
<div>
<span>Attribute changes</span>
<strong>@Model.CharacterAttributeEvents.Count</strong>
</div>
<div class="@(suggestionCount > 0 ? "attention" : "")">
<span>Suggestions</span>
<strong>@suggestionCount</strong>
</div>
<div class="scene-inspector-v2-preview-stack">
@if (Model.CharacterKnowledge.Any())
{
<details class="scene-inspector-v2-preview-group" open>
<summary>Knowledge changes <span>@Model.CharacterKnowledge.Count</span></summary>
<ul class="scene-inspector-v2-compact-list">
@foreach (var item in Model.CharacterKnowledge.Take(3))
{
<li><strong>@item.CharacterName</strong> @PreviewText(item.Description, $"is marked as {item.KnowledgeStateName}.")</li>
}
</ul>
</details>
}
@if (Model.RelationshipEvents.Any())
{
<details class="scene-inspector-v2-preview-group">
<summary>Relationship changes <span>@Model.RelationshipEvents.Count</span></summary>
<ul class="scene-inspector-v2-compact-list">
@foreach (var item in Model.RelationshipEvents.Take(3))
{
<li><strong>@item.CharacterAName &harr; @item.CharacterBName</strong> @PreviewText(item.Description, $"{item.RelationshipStateName} / {item.RelationshipTypeName}.")</li>
}
</ul>
</details>
}
@if (Model.CharacterAttributeEvents.Any())
{
<details class="scene-inspector-v2-preview-group">
<summary>Attribute changes <span>@Model.CharacterAttributeEvents.Count</span></summary>
<ul class="scene-inspector-v2-compact-list">
@foreach (var item in Model.CharacterAttributeEvents.Take(3))
{
<li><strong>@item.CharacterName</strong> @item.AttributeName: @item.AttributeValue</li>
}
</ul>
</details>
}
@if (suggestionCount > 0)
{
<div class="scene-inspector-v2-suggestion-strip">
<strong>@suggestionCount</strong>
<span>Word Companion suggestion@(suggestionCount == 1 ? "" : "s") pending</span>
</div>
}
</div>
<div class="scene-inspector-v2-actions">

View File

@ -63,8 +63,12 @@
<div class="scene-inspector-v2-metrics">
@foreach (var metric in Model.Metrics)
{
<div>
var range = Math.Max(1, metric.MaxValue - metric.MinValue);
var clampedValue = Math.Min(metric.MaxValue, Math.Max(metric.MinValue, metric.Value));
var percent = (int)Math.Round(((double)(clampedValue - metric.MinValue) / range) * 100);
<div class="scene-inspector-v2-metric-bar" style="--metric-percent: @percent%;" aria-label="@metric.MetricName value @metric.Value of @metric.MaxValue">
<span>@metric.MetricName</span>
<div aria-hidden="true"><i></i></div>
<strong>@metric.Value</strong>
</div>
}

View File

@ -3,6 +3,23 @@
var completed = Model.ChecklistItems.Count(x => x.IsCompleted);
var total = Model.ChecklistItems.Count;
var checklistLabel = total == 0 ? "No checklist yet" : $"{completed} of {total} complete";
var incompleteChecklist = Model.ChecklistItems.Where(x => !x.IsCompleted).Take(3).ToList();
static string? FileHref(string? filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
return null;
}
return filePath.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? filePath
: filePath.StartsWith("/")
? filePath
: !System.IO.Path.IsPathRooted(filePath)
? "/" + filePath.TrimStart('/', '\\')
: null;
}
}
<article class="scene-inspector-v2-card" data-scene-summary-part="writer">
@ -13,21 +30,6 @@
</div>
</div>
<div class="scene-inspector-v2-counts">
<div>
<span>Notes</span>
<strong>@Model.Notes.Count</strong>
</div>
<div>
<span>Checklist</span>
<strong>@checklistLabel</strong>
</div>
<div>
<span>Attachments</span>
<strong>@Model.Attachments.Count</strong>
</div>
</div>
@if (Model.Workflow.IsBlocked)
{
<div class="scene-inspector-v2-callout">
@ -36,6 +38,78 @@
</div>
}
<div class="scene-inspector-v2-preview-stack">
<div class="scene-inspector-v2-section">
<span>Notes</span>
@if (!Model.Notes.Any())
{
<p class="muted">No drafting notes yet.</p>
}
else
{
<ul class="scene-inspector-v2-compact-list">
@foreach (var note in Model.Notes.OrderByDescending(x => x.IsPinned).ThenByDescending(x => x.UpdatedDate).Take(3))
{
<li>@(string.IsNullOrWhiteSpace(note.NoteTitle) ? note.NoteText : note.NoteTitle)</li>
}
</ul>
}
</div>
<div class="scene-inspector-v2-section">
<span>Checklist</span>
@if (!Model.ChecklistItems.Any())
{
<p class="muted">No checklist items yet.</p>
}
else if (!incompleteChecklist.Any())
{
<p class="scene-inspector-v2-good-state">All @total checklist item@(total == 1 ? "" : "s") complete.</p>
}
else
{
<p class="muted">@checklistLabel</p>
<ul class="scene-inspector-v2-compact-list">
@foreach (var item in incompleteChecklist)
{
<li>@item.ItemText</li>
}
</ul>
}
</div>
<div class="scene-inspector-v2-section">
<span>Attachments</span>
@if (!Model.Attachments.Any())
{
<p class="muted">No scene references attached yet.</p>
}
else
{
<ul class="scene-inspector-v2-compact-list">
@foreach (var attachment in Model.Attachments.Take(3))
{
var href = FileHref(attachment.FilePath);
<li>
@if (!string.IsNullOrWhiteSpace(href))
{
<a href="@href" target="_blank" rel="noopener">@attachment.Title</a>
}
else if (!string.IsNullOrWhiteSpace(attachment.ExternalUrl))
{
<a href="@attachment.ExternalUrl" target="_blank" rel="noopener">@attachment.Title</a>
}
else
{
<span>@attachment.Title</span>
}
</li>
}
</ul>
}
</div>
</div>
<div class="scene-inspector-v2-actions">
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="notes" data-scene-id="@Model.SceneID" data-scene-editor-url="@Url.Action("SceneNotesEditor", "Scenes", new { sceneId = Model.SceneID })">Manage notes</button>
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="checklist" data-scene-id="@Model.SceneID" data-scene-editor-url="@Url.Action("SceneChecklistEditor", "Scenes", new { sceneId = Model.SceneID })">Manage checklist</button>

View File

@ -4535,12 +4535,12 @@ body.dragging-location [data-drag-type="location"] {
.scene-inspector-v2-grid {
display: grid;
align-items: stretch;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.scene-inspector-v2-edit .scene-inspector-v2-grid {
align-items: start;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
@ -4600,6 +4600,7 @@ body.dragging-location [data-drag-type="location"] {
display: grid;
align-content: start;
gap: 14px;
height: 100%;
min-width: 0;
padding: 16px;
}
@ -4689,7 +4690,7 @@ body.dragging-location [data-drag-type="location"] {
.scene-inspector-v2-metrics,
.scene-inspector-v2-counts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
gap: 8px;
}
@ -4759,6 +4760,181 @@ body.dragging-location [data-drag-type="location"] {
text-align: right;
}
.scene-inspector-v2-character-list,
.scene-inspector-v2-compact-list {
display: grid;
gap: 7px;
margin: 0;
padding: 0;
list-style: none;
}
.scene-inspector-v2-character-list li {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 9px;
min-width: 0;
}
.scene-inspector-v2-character-list strong,
.scene-inspector-v2-character-list span {
display: block;
}
.scene-inspector-v2-character-list div {
min-width: 0;
}
.scene-inspector-v2-character-list div span {
color: var(--plotline-muted);
font-size: 0.8rem;
}
.scene-inspector-v2-character-list .visual-avatar {
width: 34px;
}
.scene-inspector-v2-avatar-pov {
box-shadow: 0 0 0 2px rgba(47, 111, 99, 0.24);
}
.scene-inspector-v2-mini-badge {
border: 1px solid rgba(47, 111, 99, 0.2);
border-radius: 999px;
background: var(--plotline-soft);
color: var(--plotline-accent-dark);
padding: 2px 7px;
font-size: 0.7rem;
font-weight: 800;
}
.scene-inspector-v2-preview-stack {
display: grid;
gap: 8px;
}
.scene-inspector-v2-preview-group {
border: 1px solid rgba(47, 111, 99, 0.12);
border-radius: 8px;
background: rgba(47, 111, 99, 0.04);
padding: 8px 10px;
}
.scene-inspector-v2-preview-group summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
color: var(--plotline-accent-dark);
cursor: pointer;
font-size: 0.82rem;
font-weight: 800;
}
.scene-inspector-v2-preview-group summary span {
color: var(--plotline-muted);
font-size: 0.76rem;
}
.scene-inspector-v2-preview-group[open] summary {
margin-bottom: 7px;
}
.scene-inspector-v2-compact-list li {
min-width: 0;
color: var(--plotline-ink);
font-size: 0.88rem;
overflow-wrap: anywhere;
}
.scene-inspector-v2-compact-list li::before {
content: "";
display: inline-block;
width: 5px;
height: 5px;
margin-right: 7px;
border-radius: 999px;
background: rgba(47, 111, 99, 0.42);
vertical-align: middle;
}
.scene-inspector-v2-compact-list a {
color: var(--plotline-accent-dark);
font-weight: 700;
}
.scene-inspector-v2-suggestion-strip,
.scene-inspector-v2-good-state,
.scene-inspector-v2-warning-state {
border-radius: 8px;
padding: 8px 10px;
}
.scene-inspector-v2-suggestion-strip {
display: flex;
align-items: center;
gap: 8px;
border: 1px solid rgba(197, 139, 43, 0.24);
background: rgba(197, 139, 43, 0.1);
}
.scene-inspector-v2-suggestion-strip strong {
color: #805815;
font-size: 1rem;
}
.scene-inspector-v2-suggestion-strip span {
color: var(--plotline-muted);
font-size: 0.86rem;
font-weight: 700;
}
.scene-inspector-v2-good-state {
border: 1px solid rgba(47, 111, 99, 0.16);
background: rgba(47, 111, 99, 0.07);
color: var(--plotline-accent-dark);
font-weight: 800;
}
.scene-inspector-v2-good-state::before {
content: "\2713";
margin-right: 7px;
}
.scene-inspector-v2-warning-state {
border: 1px solid rgba(179, 95, 59, 0.22);
background: rgba(179, 95, 59, 0.1);
color: var(--plotline-danger, #b35f3b);
font-weight: 800;
}
.scene-inspector-v2-metric-bar {
display: grid;
grid-template-rows: auto 42px auto;
align-items: end;
gap: 6px;
text-align: center;
}
.scene-inspector-v2-metric-bar div {
display: flex;
align-items: end;
justify-content: center;
min-height: 42px;
border: 0;
background: transparent;
padding: 0;
}
.scene-inspector-v2-metric-bar i {
display: block;
width: 22px;
height: max(6px, var(--metric-percent));
border-radius: 6px 6px 2px 2px;
background: linear-gradient(180deg, #6fa99b, var(--plotline-accent-dark));
}
.scene-inspector-v2-callout {
border-left: 4px solid var(--plotline-warning, #c58b2b);
border-radius: 8px;
@ -5398,6 +5574,18 @@ body.scene-inspector-editor-resizing {
grid-template-columns: 1fr;
}
.scene-inspector-v2-timeline .scene-inspector-v2-metrics {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.scene-inspector-v2-timeline .scene-inspector-v2-metric-bar {
grid-template-rows: auto 34px auto;
}
.scene-inspector-v2-timeline .scene-inspector-v2-metric-bar div {
min-height: 34px;
}
@media (max-width: 1100px) {
.scene-inspector-v2-edit.editor-overlay-open {
grid-template-columns: minmax(0, 1fr) 10px minmax(360px, min(var(--scene-editor-width), 52%));

File diff suppressed because one or more lines are too long