PlotDirector Phase 18B - Dashboard Card Content Refinement and Equal Height Layout
This commit is contained in:
parent
191ed8c540
commit
e761e42ff7
@ -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>
|
||||
|
||||
@ -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 ↔ @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">
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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%));
|
||||
|
||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user