Phase 17Z - Legacy Retirement and Cleanup

This commit is contained in:
Nick Beckley 2026-06-30 19:32:13 +01:00
parent 0020ad37d7
commit d334661178
12 changed files with 6 additions and 2881 deletions

View File

@ -1668,32 +1668,6 @@ public sealed class ScenesController(
return RedirectToAction(nameof(Edit), new { id = sceneId }); return RedirectToAction(nameof(Edit), new { id = sceneId });
} }
[HttpGet]
public async Task<IActionResult> OccupancyEditor(int sceneId, int? floorPlanId)
{
var model = await scenes.GetOccupancyEditorAsync(sceneId, floorPlanId);
return model is null ? NotFound() : PartialView("_SceneOccupancy", model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveOccupancy(SceneOccupancySaveViewModel model)
{
try
{
await scenes.SaveOccupancyAsync(model);
var refreshed = await scenes.GetOccupancyEditorAsync(model.SceneID, model.FloorPlanID);
return refreshed is null
? NotFound()
: PartialView("_SceneOccupancy", refreshed);
}
catch (InvalidOperationException ex)
{
Response.StatusCode = StatusCodes.Status400BadRequest;
return Json(new { ok = false, message = ex.Message });
}
}
private string? GetLocalReturnUrl(string? returnUrl = null) private string? GetLocalReturnUrl(string? returnUrl = null)
{ {
if (string.IsNullOrWhiteSpace(returnUrl) if (string.IsNullOrWhiteSpace(returnUrl)

View File

@ -122,8 +122,6 @@ public interface ISceneService
Task<SceneEditViewModel?> GetEditAsync(int sceneId); Task<SceneEditViewModel?> GetEditAsync(int sceneId);
Task<int> SaveAsync(SceneEditViewModel model); Task<int> SaveAsync(SceneEditViewModel model);
Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId); Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId);
Task<SceneOccupancyEditorViewModel?> GetOccupancyEditorAsync(int sceneId, int? floorPlanId = null);
Task SaveOccupancyAsync(SceneOccupancySaveViewModel model);
Task ArchiveAsync(int sceneId); Task ArchiveAsync(int sceneId);
Task MoveAsync(int sceneId, string direction); Task MoveAsync(int sceneId, string direction);
Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position); Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position);
@ -1464,8 +1462,6 @@ public sealed class SceneService(
IAssetRepository assets, IAssetRepository assets,
ICharacterRepository characters, ICharacterRepository characters,
ILocationRepository locations, ILocationRepository locations,
IFloorPlanRepository floorPlans,
ISceneFloorPlanOccupancyRepository sceneOccupancy,
IWarningRepository warnings, IWarningRepository warnings,
ISceneDependencyRepository sceneDependencies, ISceneDependencyRepository sceneDependencies,
IWriterWorkspaceRepository writerWorkspace, IWriterWorkspaceRepository writerWorkspace,
@ -1842,172 +1838,10 @@ public sealed class SceneService(
ReturnProjectID = model.ReturnProjectID, ReturnProjectID = model.ReturnProjectID,
ReturnBookID = model.ReturnBookID ReturnBookID = model.ReturnBookID
}; };
model.Occupancy = model.SceneID == 0
? new SceneOccupancyEditorViewModel()
: await BuildOccupancyEditorAsync(model, projectAssets);
model.LocationConsistencyMessages = BuildLocationConsistencyMessages(model); model.LocationConsistencyMessages = BuildLocationConsistencyMessages(model);
return model; return model;
} }
public async Task<SceneOccupancyEditorViewModel?> GetOccupancyEditorAsync(int sceneId, int? floorPlanId = null)
{
var sceneModel = await GetEditAsync(sceneId);
if (sceneModel is null)
{
return null;
}
if (floorPlanId.HasValue)
{
sceneModel.FloorPlanID = floorPlanId;
}
var projectAssets = sceneModel.Project is null ? [] : await assets.ListAssetsAsync(sceneModel.Project.ProjectID);
return await BuildOccupancyEditorAsync(sceneModel, projectAssets);
}
public async Task SaveOccupancyAsync(SceneOccupancySaveViewModel model)
{
var scene = await scenes.GetAsync(model.SceneID) ?? throw new InvalidOperationException("Scene not found.");
var context = await GetContextAsync(scene.ChapterID) ?? throw new InvalidOperationException("Scene context not found.");
if (model.FloorPlanID.HasValue)
{
var floorPlan = await floorPlans.GetAsync(model.FloorPlanID.Value);
if (floorPlan is null || floorPlan.ProjectID != context.Project.ProjectID)
{
throw new InvalidOperationException("Floor plan not found.");
}
if (model.InitialFloorPlanFloorID.HasValue)
{
var floors = await floorPlans.ListFloorsByPlanAsync(model.FloorPlanID.Value);
if (!floors.Any(x => x.FloorPlanFloorID == model.InitialFloorPlanFloorID.Value))
{
throw new InvalidOperationException("Initial floor does not belong to the selected floor plan.");
}
}
}
else
{
model.InitialFloorPlanFloorID = null;
}
var allowedLocations = model.FloorPlanID.HasValue
? (await floorPlans.ListBlocksByPlanAsync(model.FloorPlanID.Value)).Select(x => x.LocationID).ToHashSet()
: [];
var sceneCharacterIds = (await characters.ListSceneCharactersAsync(model.SceneID)).Select(x => x.CharacterID).ToHashSet();
var sceneAssetIds = (await assets.ListEventsBySceneAsync(model.SceneID)).Select(x => x.StoryAssetID).ToHashSet();
var rows = new List<SceneFloorPlanOccupancy>();
foreach (var character in model.Characters.Where(x => sceneCharacterIds.Contains(x.CharacterID)))
{
if (character.LocationID.HasValue && !allowedLocations.Contains(character.LocationID.Value))
{
throw new InvalidOperationException("Character location must be on the selected floor plan.");
}
rows.Add(new SceneFloorPlanOccupancy
{
CharacterID = character.CharacterID,
LocationID = character.LocationID ?? 0
});
}
foreach (var asset in model.Assets.Where(x => sceneAssetIds.Contains(x.StoryAssetID)))
{
if (asset.LocationID.HasValue && !allowedLocations.Contains(asset.LocationID.Value))
{
throw new InvalidOperationException("Asset location must be on the selected floor plan.");
}
rows.Add(new SceneFloorPlanOccupancy
{
StoryAssetID = asset.StoryAssetID,
LocationID = asset.LocationID ?? 0
});
}
await sceneOccupancy.SaveAsync(model.SceneID, model.FloorPlanID, model.InitialFloorPlanFloorID, rows);
await activity.RecordAsync(context.Project.ProjectID, "Updated", "Scene Occupancy", model.SceneID, scene.SceneTitle);
}
private async Task<SceneOccupancyEditorViewModel> BuildOccupancyEditorAsync(SceneEditViewModel model, IReadOnlyList<StoryAsset> projectAssets)
{
var floorPlanList = model.Project is null ? [] : await floorPlans.ListByProjectAsync(model.Project.ProjectID);
var selectedFloorPlanId = model.FloorPlanID ?? floorPlanList.FirstOrDefault()?.FloorPlanID;
var existingRows = model.SceneID == 0 ? [] : await sceneOccupancy.ListBySceneAsync(model.SceneID);
var floors = new List<SceneOccupancyFloorOption>();
var locationsOnPlans = new List<SceneOccupancyLocationOption>();
foreach (var plan in floorPlanList)
{
var planFloors = await floorPlans.ListFloorsByPlanAsync(plan.FloorPlanID);
var blocks = await floorPlans.ListBlocksByPlanAsync(plan.FloorPlanID);
var floorsById = planFloors.ToDictionary(x => x.FloorPlanFloorID);
floors.AddRange(planFloors.Select(floor => new SceneOccupancyFloorOption
{
FloorPlanID = floor.FloorPlanID,
FloorPlanFloorID = floor.FloorPlanFloorID,
Name = floor.Name,
SortOrder = floor.SortOrder
}));
locationsOnPlans.AddRange(blocks
.GroupBy(x => new { x.LocationID, x.FloorPlanFloorID })
.Select(group =>
{
floorsById.TryGetValue(group.Key.FloorPlanFloorID, out var floor);
var first = group.First();
return new SceneOccupancyLocationOption
{
FloorPlanID = plan.FloorPlanID,
FloorPlanFloorID = group.Key.FloorPlanFloorID,
FloorName = floor?.Name ?? "Floor",
FloorSortOrder = floor?.SortOrder ?? 0,
LocationID = group.Key.LocationID,
LocationName = first.LocationName,
LocationPath = first.LocationPath
};
}));
}
var existingByCharacter = existingRows.Where(x => x.CharacterID.HasValue).ToDictionary(x => x.CharacterID!.Value, x => x.LocationID);
var existingByAsset = existingRows.Where(x => x.StoryAssetID.HasValue).ToDictionary(x => x.StoryAssetID!.Value, x => x.LocationID);
var assetNames = projectAssets.ToDictionary(x => x.StoryAssetID, x => x.AssetName);
return new SceneOccupancyEditorViewModel
{
SceneID = model.SceneID,
FloorPlanID = selectedFloorPlanId,
InitialFloorPlanFloorID = model.InitialFloorPlanFloorID,
FloorPlanOptions = floorPlanList.OrderBy(x => x.Name).Select(x => new SelectListItem(x.Name, x.FloorPlanID.ToString())).ToList(),
Floors = floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name).ToList(),
Locations = locationsOnPlans.OrderBy(x => x.FloorSortOrder).ThenBy(x => x.FloorName).ThenBy(x => x.LocationPath).ToList(),
Characters = model.SceneCharacters
.GroupBy(x => x.CharacterID)
.Select(group => new SceneCharacterOccupancyRowViewModel
{
CharacterID = group.Key,
CharacterName = group.First().CharacterName,
LocationID = existingByCharacter.GetValueOrDefault(group.Key)
})
.OrderBy(x => x.CharacterName)
.ToList(),
Assets = model.AssetEvents
.GroupBy(x => x.StoryAssetID)
.Select(group => new SceneAssetOccupancyRowViewModel
{
StoryAssetID = group.Key,
AssetName = assetNames.GetValueOrDefault(group.Key, group.First().AssetName),
LocationID = existingByAsset.GetValueOrDefault(group.Key)
})
.OrderBy(x => x.AssetName)
.ToList()
};
}
private async Task<IReadOnlyList<ContinuityWarning>> BuildAgeContinuityWarningsAsync(SceneEditViewModel model) private async Task<IReadOnlyList<ContinuityWarning>> BuildAgeContinuityWarningsAsync(SceneEditViewModel model)
{ {
if (model.SceneID == 0 || model.Project is null || model.Book is null) if (model.SceneID == 0 || model.Project is null || model.Book is null)

View File

@ -398,8 +398,6 @@ public sealed class SceneEditViewModel
public IReadOnlyList<SelectListItem> SceneAttachmentTypeOptions { get; set; } = []; public IReadOnlyList<SelectListItem> SceneAttachmentTypeOptions { get; set; } = [];
public StorageUsage? StorageUsage { get; set; } public StorageUsage? StorageUsage { get; set; }
public StoryStateViewModel StoryState { get; set; } = new(); public StoryStateViewModel StoryState { get; set; } = new();
public SceneOccupancyEditorViewModel Occupancy { get; set; } = new();
public int? ReturnProjectID { get; set; } public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; } public int? ReturnBookID { get; set; }
public bool ReturnToTimeline { get; set; } public bool ReturnToTimeline { get; set; }
@ -559,60 +557,6 @@ public sealed class SceneCharacterParticipationInputViewModel
public string? EmotionalState { get; set; } public string? EmotionalState { get; set; }
} }
public sealed class SceneOccupancyEditorViewModel
{
public int SceneID { get; set; }
public int? FloorPlanID { get; set; }
public int? InitialFloorPlanFloorID { get; set; }
public IReadOnlyList<SelectListItem> FloorPlanOptions { get; set; } = [];
public IReadOnlyList<SceneOccupancyFloorOption> Floors { get; set; } = [];
public IReadOnlyList<SceneOccupancyLocationOption> Locations { get; set; } = [];
public IReadOnlyList<SceneCharacterOccupancyRowViewModel> Characters { get; set; } = [];
public IReadOnlyList<SceneAssetOccupancyRowViewModel> Assets { get; set; } = [];
}
public sealed class SceneOccupancyFloorOption
{
public int FloorPlanFloorID { get; set; }
public int FloorPlanID { get; set; }
public string Name { get; set; } = string.Empty;
public int SortOrder { get; set; }
}
public sealed class SceneOccupancyLocationOption
{
public int LocationID { get; set; }
public int FloorPlanID { get; set; }
public int FloorPlanFloorID { get; set; }
public string FloorName { get; set; } = string.Empty;
public int FloorSortOrder { get; set; }
public string LocationName { get; set; } = string.Empty;
public string LocationPath { get; set; } = string.Empty;
}
public sealed class SceneCharacterOccupancyRowViewModel
{
public int CharacterID { get; set; }
public string CharacterName { get; set; } = string.Empty;
public int? LocationID { get; set; }
}
public sealed class SceneAssetOccupancyRowViewModel
{
public int StoryAssetID { get; set; }
public string AssetName { get; set; } = string.Empty;
public int? LocationID { get; set; }
}
public sealed class SceneOccupancySaveViewModel
{
public int SceneID { get; set; }
public int? FloorPlanID { get; set; }
public int? InitialFloorPlanFloorID { get; set; }
public List<SceneCharacterOccupancyRowViewModel> Characters { get; set; } = [];
public List<SceneAssetOccupancyRowViewModel> Assets { get; set; } = [];
}
public sealed class StoryStateViewModel public sealed class StoryStateViewModel
{ {
public List<CharacterStoryStateViewModel> Characters { get; set; } = []; public List<CharacterStoryStateViewModel> Characters { get; set; } = [];

View File

@ -54,16 +54,6 @@
z-index: auto; z-index: auto;
} }
.theme-audit-inspector-sample {
display: grid;
gap: .75rem;
}
.theme-audit-inspector-sample .inspector-section,
.theme-audit-inspector-sample .warning-mini-card,
.theme-audit-inspector-sample .thread-event-item {
margin: 0;
}
</style> </style>
<div class="page-heading compact"> <div class="page-heading compact">
@ -329,38 +319,6 @@
</div> </div>
</section> </section>
<section class="theme-audit-section">
<h2>PlotDirector-Specific Components</h2>
<div class="scene-inspector-root theme-audit-inspector-sample">
<div class="inspector-quick-nav">
<input class="form-control form-control-sm inspector-search" type="search" placeholder="Find in scene..." />
<div class="inspector-nav-links">
<a class="active" href="#">Overview</a>
<a href="#">Characters</a>
<a href="#">Warnings</a>
</div>
</div>
<section class="inspector-section is-open">
<button class="inspector-accordion-toggle" type="button" aria-expanded="true"><span>Scene Inspector Card</span><span class="accordion-count">3</span></button>
<div class="inspector-accordion-body">
<p class="inspector-subheading">Compact scene metadata</p>
<div class="overview-chip-grid">
<span class="overview-chip">Draft</span>
<span class="overview-chip attention">1 warning</span>
<span class="purpose-chip">Reveal</span>
</div>
</div>
</section>
<article class="warning-mini-card">
<span class="severity-pill warning">Warning</span>
<p>Sample compact continuity warning card.</p>
</article>
<article class="thread-event-item">
<span class="thread-event-marker"></span>
<div><strong>Thread event</strong><p class="mb-0">A sample plot thread note.</p></div>
</article>
</div>
</section>
</section> </section>
<section class="theme-audit-preview" data-theme="dark" data-bs-theme="dark" aria-label="Dark theme preview"> <section class="theme-audit-preview" data-theme="dark" data-bs-theme="dark" aria-label="Dark theme preview">
@ -613,38 +571,6 @@
</div> </div>
</section> </section>
<section class="theme-audit-section">
<h2>PlotDirector-Specific Components</h2>
<div class="scene-inspector-root theme-audit-inspector-sample">
<div class="inspector-quick-nav">
<input class="form-control form-control-sm inspector-search" type="search" placeholder="Find in scene..." />
<div class="inspector-nav-links">
<a class="active" href="#">Overview</a>
<a href="#">Characters</a>
<a href="#">Warnings</a>
</div>
</div>
<section class="inspector-section is-open">
<button class="inspector-accordion-toggle" type="button" aria-expanded="true"><span>Scene Inspector Card</span><span class="accordion-count">3</span></button>
<div class="inspector-accordion-body">
<p class="inspector-subheading">Compact scene metadata</p>
<div class="overview-chip-grid">
<span class="overview-chip">Draft</span>
<span class="overview-chip attention">1 warning</span>
<span class="purpose-chip">Reveal</span>
</div>
</div>
</section>
<article class="warning-mini-card">
<span class="severity-pill warning">Warning</span>
<p>Sample compact continuity warning card.</p>
</article>
<article class="thread-event-item">
<span class="thread-event-marker"></span>
<div><strong>Thread event</strong><p class="mb-0">A sample plot thread note.</p></div>
</article>
</div>
</section>
</section> </section>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -1,98 +0,0 @@
@model SceneOccupancyEditorViewModel
@{
var selectedPlanId = Model.FloorPlanID;
var locationOptions = Model.Locations
.Where(x => !selectedPlanId.HasValue || x.FloorPlanID == selectedPlanId.Value)
.OrderBy(x => x.FloorSortOrder)
.ThenBy(x => x.FloorName)
.ThenBy(x => x.LocationPath)
.ToList();
}
<form asp-controller="Scenes" asp-action="SaveOccupancy" method="post" class="scene-occupancy-form" data-scene-occupancy-form data-editor-url="@Url.Action("OccupancyEditor", "Scenes")">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<div class="scene-occupancy-status" data-scene-occupancy-status>Saved</div>
<div class="row g-2">
<div class="col-sm-6">
<label class="form-label" for="OccupancyFloorPlanID">Floor Plan</label>
<select class="form-select form-select-sm" id="OccupancyFloorPlanID" name="FloorPlanID" data-occupancy-plan-select>
<option value="">No floor plan</option>
@foreach (var option in Model.FloorPlanOptions)
{
<option value="@option.Value" selected="@(option.Value == Model.FloorPlanID?.ToString())">@option.Text</option>
}
</select>
</div>
<div class="col-sm-6">
<label class="form-label" for="OccupancyInitialFloorID">Initial Floor</label>
<select class="form-select form-select-sm" id="OccupancyInitialFloorID" name="InitialFloorPlanFloorID" data-occupancy-floor-select>
<option value="">Use first floor</option>
@foreach (var floor in Model.Floors.Where(x => !selectedPlanId.HasValue || x.FloorPlanID == selectedPlanId.Value))
{
<option value="@floor.FloorPlanFloorID" selected="@(floor.FloorPlanFloorID == Model.InitialFloorPlanFloorID)">@floor.Name</option>
}
</select>
</div>
</div>
@if (!Model.FloorPlanOptions.Any())
{
<p class="muted mt-2 mb-0">No floor plans exist for this project yet.</p>
}
else
{
<div class="scene-occupancy-grid mt-3">
<section>
<h4>Characters</h4>
@if (!Model.Characters.Any())
{
<p class="muted">No characters appear in this scene yet.</p>
}
else
{
@for (var i = 0; i < Model.Characters.Count; i++)
{
<div class="scene-occupancy-row">
<input type="hidden" name="Characters[@i].CharacterID" value="@Model.Characters.ElementAt(i).CharacterID" />
<span>@Model.Characters.ElementAt(i).CharacterName</span>
<select class="form-select form-select-sm" name="Characters[@i].LocationID">
<option value="">Not placed</option>
@foreach (var location in locationOptions)
{
<option value="@location.LocationID" selected="@(location.LocationID == Model.Characters.ElementAt(i).LocationID)">@location.FloorName &gt; @location.LocationPath</option>
}
</select>
</div>
}
}
</section>
<section>
<h4>Assets</h4>
@if (!Model.Assets.Any())
{
<p class="muted">No story assets appear in this scene yet.</p>
}
else
{
@for (var i = 0; i < Model.Assets.Count; i++)
{
<div class="scene-occupancy-row">
<input type="hidden" name="Assets[@i].StoryAssetID" value="@Model.Assets.ElementAt(i).StoryAssetID" />
<span>@Model.Assets.ElementAt(i).AssetName</span>
<select class="form-select form-select-sm" name="Assets[@i].LocationID">
<option value="">Not placed</option>
@foreach (var location in locationOptions)
{
<option value="@location.LocationID" selected="@(location.LocationID == Model.Assets.ElementAt(i).LocationID)">@location.FloorName &gt; @location.LocationPath</option>
}
</select>
</div>
}
}
</section>
</div>
<button class="btn btn-primary btn-sm mt-3" type="submit">Save occupancy</button>
}
</form>

View File

@ -849,37 +849,10 @@ a:hover {
box-shadow: 0 4px 12px rgba(62, 45, 28, 0.12); box-shadow: 0 4px 12px rgba(62, 45, 28, 0.12);
} }
.scene-inspector-panel, .scene-inspector-panel {
.scene-inspector-root {
background: rgba(255, 251, 243, 0.72); background: rgba(255, 251, 243, 0.72);
} }
.inspector-section {
background: var(--plotline-surface-elevated);
border-color: var(--plotline-border-soft);
border-radius: var(--plotline-radius);
box-shadow: var(--plotline-shadow-subtle);
}
.scene-overview-card {
background: linear-gradient(135deg, #fffaf1, #f2e2cf);
}
.inspector-quick-nav,
.inspector-accordion-toggle {
background: rgba(255, 251, 243, 0.88);
border-color: rgba(84, 68, 48, 0.14);
}
.inspector-accordion-toggle {
color: var(--plotline-accent-dark);
border-radius: var(--plotline-radius-sm);
}
.inspector-accordion-toggle:hover {
background: rgba(122, 77, 44, 0.08);
}
.form-control, .form-control,
.form-select { .form-select {
border-color: rgba(84, 68, 48, 0.2); border-color: rgba(84, 68, 48, 0.2);
@ -1503,45 +1476,6 @@ a:focus-visible {
color: #211915; color: #211915;
} }
.scene-inspector-root {
display: grid;
gap: 0.85rem;
}
.inspector-section {
padding: 0.95rem;
}
.inspector-section h3 {
color: #211915;
font-size: 1.02rem;
}
.scene-overview-card {
border: 1px solid rgba(122, 77, 44, 0.18);
background:
linear-gradient(135deg, rgba(255, 250, 241, 0.98), rgba(242, 226, 207, 0.82));
}
.inspector-quick-nav {
top: 0.35rem;
box-shadow: 0 10px 26px rgba(62, 45, 28, 0.08);
}
.inspector-nav-links {
max-height: 6.2rem;
overflow: auto;
}
.inspector-nav-links a {
background: rgba(255, 253, 248, 0.84);
}
.inspector-accordion-toggle {
min-height: 2.25rem;
padding-inline: 0.2rem;
}
.writer-mini-card { .writer-mini-card {
border-color: rgba(84, 68, 48, 0.13); border-color: rgba(84, 68, 48, 0.13);
background: rgba(255, 253, 248, 0.78); background: rgba(255, 253, 248, 0.78);
@ -1892,10 +1826,6 @@ a:focus-visible {
[data-theme="dark"] .writer-mini-card, [data-theme="dark"] .writer-mini-card,
[data-theme="dark"] .timeline-setting-card, [data-theme="dark"] .timeline-setting-card,
[data-theme="dark"] .timeline-setting-card:has(input:checked), [data-theme="dark"] .timeline-setting-card:has(input:checked),
[data-theme="dark"] .scene-overview-card,
[data-theme="dark"] .inspector-section,
[data-theme="dark"] .inspector-quick-nav,
[data-theme="dark"] .inspector-accordion-toggle,
[data-theme="dark"] .thread-event-item, [data-theme="dark"] .thread-event-item,
[data-theme="dark"] .asset-event-item, [data-theme="dark"] .asset-event-item,
[data-theme="dark"] .story-asset-event-item { [data-theme="dark"] .story-asset-event-item {
@ -1989,25 +1919,6 @@ a:focus-visible {
background: var(--plotline-accent-dark); background: var(--plotline-accent-dark);
} }
[data-theme="dark"] .scene-inspector-root {
color: var(--app-text);
background: transparent;
}
[data-theme="dark"] .inspector-nav-links a {
color: #ead8c4;
background: rgba(231, 214, 190, 0.08);
border-color: rgba(231, 214, 190, 0.28);
}
[data-theme="dark"] .inspector-nav-links a:hover,
[data-theme="dark"] .inspector-nav-links a:focus,
[data-theme="dark"] .inspector-nav-links a.active {
color: #fff4e8;
background: rgba(241, 195, 143, 0.16);
border-color: rgba(241, 195, 143, 0.52);
}
[data-theme="dark"] .thread-event-item, [data-theme="dark"] .thread-event-item,
[data-theme="dark"] .asset-event-item, [data-theme="dark"] .asset-event-item,
[data-theme="dark"] .story-asset-event-item { [data-theme="dark"] .story-asset-event-item {
@ -5108,7 +5019,4 @@ a:focus-visible {
max-width: 260px; max-width: 260px;
} }
.inspector-nav-links {
max-height: 8.5rem;
}
} }

File diff suppressed because one or more lines are too long

View File

@ -1842,27 +1842,6 @@ body.timeline-inspector-resizing {
font-size: 1.25rem; font-size: 1.25rem;
} }
.scene-inspector-form {
display: grid;
gap: 14px;
}
.inspector-section {
border-top: 1px solid var(--plotline-line);
padding-top: 12px;
}
.inspector-section:first-of-type {
border-top: 0;
padding-top: 0;
}
.inspector-section h3 {
margin: 0 0 10px;
font-size: 0.95rem;
color: var(--plotline-accent-dark);
}
.location-helper { .location-helper {
border: 1px solid rgba(138, 111, 61, 0.22); border: 1px solid rgba(138, 111, 61, 0.22);
border-radius: 8px; border-radius: 8px;
@ -2123,36 +2102,9 @@ body.timeline-inspector-resizing {
min-width: 0; min-width: 0;
} }
.scene-inspector-root {
display: grid;
gap: 12px;
}
.scene-overview-card {
display: grid;
gap: 10px;
border: 1px solid rgba(47, 111, 99, 0.18);
border-radius: 10px;
padding: 12px;
background: linear-gradient(180deg, rgba(47, 111, 99, 0.08), rgba(255, 255, 255, 0.88));
}
.scene-overview-card h3 {
margin: 0;
font-size: 1.05rem;
}
.scene-overview-card .muted {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin: 0;
}
.overview-chip-grid, .overview-chip-grid,
.context-nav, .context-nav,
.breadcrumb-trail, .breadcrumb-trail {
.inspector-nav-links {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 7px; gap: 7px;
@ -2189,97 +2141,6 @@ body.timeline-inspector-resizing {
color: var(--plotline-muted); color: var(--plotline-muted);
} }
.inspector-quick-nav {
position: sticky;
top: 0;
z-index: 5;
display: grid;
gap: 8px;
border: 1px solid var(--plotline-line);
border-radius: 10px;
padding: 10px;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 10px 24px rgba(55, 48, 39, 0.08);
backdrop-filter: blur(8px);
}
.inspector-nav-links a {
border: 1px solid var(--plotline-line);
border-radius: 999px;
padding: 4px 8px;
color: var(--plotline-muted);
font-size: 0.76rem;
font-weight: 800;
text-decoration: none;
}
.inspector-nav-links a:hover,
.inspector-nav-links a.active {
color: var(--plotline-accent-dark);
border-color: rgba(47, 111, 99, 0.35);
background: rgba(47, 111, 99, 0.08);
}
.inspector-accordion-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
border: 0;
padding: 0;
color: var(--plotline-ink);
font-size: 0.95rem;
font-weight: 900;
text-align: left;
background: transparent;
}
.inspector-accordion-toggle::before {
content: "+";
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 999px;
margin-right: 7px;
color: var(--plotline-accent-dark);
background: rgba(47, 111, 99, 0.09);
}
.inspector-section.is-open > .inspector-accordion-toggle::before {
content: "-";
}
.inspector-accordion-toggle span:first-child {
margin-right: auto;
}
.accordion-count {
color: var(--plotline-muted);
font-size: 0.75rem;
}
.inspector-subheading {
margin: 14px 0 8px;
color: var(--plotline-muted);
font-size: 0.88rem;
font-weight: 900;
}
.inspector-accordion-body {
display: none;
margin-top: 12px;
}
.inspector-section.is-open > .inspector-accordion-body {
display: block;
}
.inspector-section.is-filtered-out {
display: none;
}
.breadcrumb-trail { .breadcrumb-trail {
margin-bottom: 12px; margin-bottom: 12px;
} }
@ -3628,18 +3489,6 @@ body.dragging-location [data-drag-type="location"] {
background: var(--plotline-accent); background: var(--plotline-accent);
} }
.inspector-actions {
position: sticky;
bottom: -16px;
display: flex;
gap: 8px;
border-top: 1px solid var(--plotline-line);
margin: 2px -16px -16px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
}
.story-bible-heading { .story-bible-heading {
align-items: end; align-items: end;
} }
@ -4409,8 +4258,7 @@ body.dragging-location [data-drag-type="location"] {
.breadcrumb-trail, .breadcrumb-trail,
.context-header, .context-header,
.button-row, .button-row,
.story-bible-nav, .story-bible-nav {
.inspector-quick-nav {
display: none !important; display: none !important;
} }
@ -6272,50 +6120,6 @@ body.scene-inspector-editor-resizing {
color: var(--muted-text, #5d6f65); color: var(--muted-text, #5d6f65);
} }
.scene-occupancy-form {
display: grid;
gap: 0.5rem;
}
.scene-occupancy-status {
justify-self: start;
border-radius: 999px;
padding: 0.2rem 0.55rem;
color: var(--plotline-muted);
background: var(--plotline-soft);
font-size: 0.78rem;
font-weight: 700;
}
.scene-occupancy-status[data-save-state="saving"] {
color: #7a4d08;
background: #fff4d6;
}
.scene-occupancy-status[data-save-state="failed"] {
color: #842029;
background: #f8d7da;
}
.scene-occupancy-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.scene-occupancy-grid h4 {
margin: 0 0 0.5rem;
font-size: 0.95rem;
}
.scene-occupancy-row {
display: grid;
grid-template-columns: minmax(8rem, 0.45fr) minmax(0, 0.55fr);
align-items: center;
gap: 0.5rem;
margin-bottom: 0.45rem;
}
.floor-plan-editor--occupancy .floor-plan-block { .floor-plan-editor--occupancy .floor-plan-block {
cursor: default; cursor: default;
touch-action: auto; touch-action: auto;
@ -6595,12 +6399,6 @@ body.scene-inspector-editor-resizing {
border-color: rgba(212, 154, 98, .22); border-color: rgba(212, 154, 98, .22);
} }
[data-bs-theme="dark"] .scene-occupancy-status,
.dark .scene-occupancy-status {
color: #d9c8b8;
background: rgba(212, 154, 98, .12);
}
[data-bs-theme="dark"] .floor-plan-occupancy-token, [data-bs-theme="dark"] .floor-plan-occupancy-token,
.dark .floor-plan-occupancy-token { .dark .floor-plan-occupancy-token {
border-color: #1f2630; border-color: #1f2630;

File diff suppressed because one or more lines are too long

View File

@ -186,137 +186,6 @@
}); });
})(); })();
(() => {
const inspectorRoots = document.querySelectorAll(".scene-inspector-root");
if (!inspectorRoots.length) {
return;
}
const currentReturnUrl = () => `${window.location.pathname}${window.location.search}${window.location.hash}`;
inspectorRoots.forEach((root) => {
root.addEventListener("submit", (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) {
return;
}
let returnUrlInput = form.querySelector("input[name='ReturnUrl']");
if (!returnUrlInput) {
returnUrlInput = document.createElement("input");
returnUrlInput.type = "hidden";
returnUrlInput.name = "ReturnUrl";
form.appendChild(returnUrlInput);
}
returnUrlInput.value = currentReturnUrl();
}, true);
});
})();
(() => {
const roots = document.querySelectorAll(".scene-inspector-root");
if (!roots.length) {
return;
}
const setStatus = (form, text, state) => {
const status = form.querySelector("[data-scene-occupancy-status]");
if (!status) {
return;
}
status.textContent = text;
status.dataset.saveState = state || "";
};
const replaceOccupancyForm = (form, html) => {
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
const nextForm = wrapper.querySelector("[data-scene-occupancy-form]");
if (nextForm) {
form.replaceWith(nextForm);
const sceneForm = nextForm.closest(".scene-inspector-root")?.querySelector(".scene-inspector-form");
const floorPlanInput = sceneForm?.querySelector("[name='FloorPlanID']");
const initialFloorInput = sceneForm?.querySelector("[name='InitialFloorPlanFloorID']");
const nextFloorPlan = nextForm.querySelector("[name='FloorPlanID']");
const nextInitialFloor = nextForm.querySelector("[name='InitialFloorPlanFloorID']");
if (floorPlanInput && nextFloorPlan) {
floorPlanInput.value = nextFloorPlan.value;
}
if (initialFloorInput && nextInitialFloor) {
initialFloorInput.value = nextInitialFloor.value;
}
}
};
roots.forEach((root) => {
root.addEventListener("change", async (event) => {
const planSelect = event.target.closest?.("[data-occupancy-plan-select]");
if (!planSelect) {
return;
}
const form = planSelect.closest("[data-scene-occupancy-form]");
const sceneId = form?.querySelector("[name='SceneID']")?.value;
const editorUrl = form?.dataset.editorUrl;
if (!form || !sceneId || !editorUrl) {
return;
}
setStatus(form, "Loading...", "saving");
const url = new URL(editorUrl, window.location.origin);
url.searchParams.set("sceneId", sceneId);
if (planSelect.value) {
url.searchParams.set("floorPlanId", planSelect.value);
}
try {
const response = await fetch(url, { headers: { "X-Requested-With": "XMLHttpRequest" } });
if (!response.ok) {
throw new Error("The occupancy editor could not be refreshed.");
}
replaceOccupancyForm(form, await response.text());
} catch (error) {
setStatus(form, error.message || "Load failed", "failed");
}
});
root.addEventListener("submit", async (event) => {
const form = event.target.closest?.("[data-scene-occupancy-form]");
if (!form) {
return;
}
event.preventDefault();
setStatus(form, "Saving...", "saving");
try {
const response = await fetch(form.action, {
method: "POST",
body: new FormData(form),
headers: { "X-Requested-With": "XMLHttpRequest" }
});
if (!response.ok) {
let message = "Save failed";
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
const payload = await response.json();
message = payload.message || message;
}
throw new Error(message);
}
replaceOccupancyForm(form, await response.text());
} catch (error) {
setStatus(form, error.message || "Save failed", "failed");
}
});
});
})();
(() => { (() => {
const modalElement = document.getElementById("plotlineConfirmModal"); const modalElement = document.getElementById("plotlineConfirmModal");
const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null; const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null;
@ -737,184 +606,6 @@
} }
})(); })();
(() => {
const roots = document.querySelectorAll(".scene-inspector-root");
if (!roots.length) {
return;
}
const slugify = (value) => value
.toLowerCase()
.replace(/&/g, "and")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
roots.forEach((root) => {
const sceneId = root.dataset.sceneId
|| root.querySelector(".scene-inspector-form [name='SceneID']")?.value
|| "new";
const storagePrefix = `plotline.inspector.${sceneId}.`;
const defaultOpen = new Set(["overview", "timing", "characters"]);
const sections = [...root.querySelectorAll(".inspector-section")];
const form = root.querySelector(".scene-inspector-form");
const timeEditor = root.querySelector("[data-scene-time-editor]");
if (form && timeEditor && timeEditor.dataset.enhanced !== "true") {
timeEditor.dataset.enhanced = "true";
const modeSelect = form.querySelector("[name='TimeModeID']");
const exactDateTimeModeId = timeEditor.dataset.exactDatetimeModeId || "";
const exactDateModeId = timeEditor.dataset.exactDateModeId || "";
const timeFields = [...timeEditor.querySelectorAll("[data-time-part-field]")];
const dateInputs = {
StartDateTime: timeEditor.querySelector("[data-date-part-for='StartDateTime']"),
EndDateTime: timeEditor.querySelector("[data-date-part-for='EndDateTime']")
};
const timeInputs = {
StartDateTime: timeEditor.querySelector("[data-time-part-for='StartDateTime']"),
EndDateTime: timeEditor.querySelector("[data-time-part-for='EndDateTime']")
};
const hiddenInputs = {
StartDateTime: form.querySelector("input[type='hidden'][name='StartDateTime']"),
EndDateTime: form.querySelector("input[type='hidden'][name='EndDateTime']")
};
const isExactDateTime = () => modeSelect?.value === exactDateTimeModeId;
const isExactDate = () => modeSelect?.value === exactDateModeId;
const setTimeVisibility = () => {
const showTime = isExactDateTime();
timeFields.forEach((field) => {
field.hidden = !showTime;
field.classList.toggle("d-none", !showTime);
});
};
const writeCombinedDateTime = (fieldName) => {
const hidden = hiddenInputs[fieldName];
const date = dateInputs[fieldName]?.value || "";
const time = timeInputs[fieldName]?.value || "";
if (!hidden) {
return;
}
if (!date) {
hidden.value = "";
return;
}
if (isExactDateTime()) {
hidden.value = `${date}T${time || "00:00"}`;
return;
}
if (isExactDate()) {
hidden.value = date;
return;
}
const existingTime = hidden.value.match(/^\\d{4}-\\d{2}-\\d{2}T(\\d{2}:\\d{2})/)?.[1];
hidden.value = existingTime ? `${date}T${existingTime}` : date;
};
const syncPostedDateTimes = () => {
writeCombinedDateTime("StartDateTime");
writeCombinedDateTime("EndDateTime");
};
modeSelect?.addEventListener("change", setTimeVisibility);
form.addEventListener("submit", syncPostedDateTimes);
setTimeVisibility();
}
sections.forEach((section) => {
if (section.dataset.enhanced === "true") {
return;
}
const heading = section.querySelector("h3, h2");
const title = section.dataset.sectionTitle || heading?.textContent?.trim() || "Overview";
const slug = slugify(title);
const sectionId = section.dataset.sectionId || `inspector-${slug}`;
const storageKey = sectionId.replace(/^inspector-/, "");
section.id = sectionId;
section.dataset.searchText = `${title} ${section.textContent}`.toLowerCase();
section.dataset.enhanced = "true";
const body = document.createElement("div");
body.className = "inspector-accordion-body";
while (section.firstChild) {
body.appendChild(section.firstChild);
}
const button = document.createElement("button");
button.type = "button";
button.className = "inspector-accordion-toggle";
button.setAttribute("aria-expanded", "false");
section.append(button, body);
const count = body.querySelectorAll(".asset-event-item, .thread-event-item, .warning-mini-card, .dependency-card, .metric-slider").length;
button.innerHTML = `<span>${title}</span><span class="accordion-count">${count || ""}</span>`;
const stored = localStorage.getItem(`${storagePrefix}${storageKey}`);
const isOpen = stored === null ? defaultOpen.has(storageKey) : stored === "open";
section.classList.toggle("is-open", isOpen);
button.setAttribute("aria-expanded", String(isOpen));
button.addEventListener("click", () => {
const nextOpen = !section.classList.contains("is-open");
section.classList.toggle("is-open", nextOpen);
button.setAttribute("aria-expanded", String(nextOpen));
localStorage.setItem(`${storagePrefix}${storageKey}`, nextOpen ? "open" : "closed");
});
});
root.querySelectorAll(".inspector-nav-links a").forEach((link) => {
link.addEventListener("click", (event) => {
const target = root.querySelector(link.getAttribute("href"));
if (!target) {
return;
}
event.preventDefault();
target.classList.add("is-open");
target.querySelector(".inspector-accordion-toggle")?.setAttribute("aria-expanded", "true");
target.scrollIntoView({ behavior: "smooth", block: "start" });
});
});
const search = root.querySelector("[data-inspector-search]");
search?.addEventListener("input", () => {
const query = search.value.trim().toLowerCase();
sections.forEach((section) => {
const matches = !query || section.dataset.searchText.includes(query);
section.classList.toggle("is-filtered-out", !matches);
if (matches && query) {
section.classList.add("is-open");
section.querySelector(".inspector-accordion-toggle")?.setAttribute("aria-expanded", "true");
}
});
});
if ("IntersectionObserver" in window) {
const navLinks = [...root.querySelectorAll(".inspector-nav-links a")];
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
navLinks.forEach((link) => {
link.classList.toggle("active", link.getAttribute("href") === `#${entry.target.id}`);
});
});
}, { root: root.closest(".scene-inspector-panel") || null, rootMargin: "-10% 0px -70% 0px" });
sections.forEach((section) => observer.observe(section));
}
});
})();
(() => { (() => {
const chartRoots = document.querySelectorAll("[data-metric-shape-chart]"); const chartRoots = document.querySelectorAll("[data-metric-shape-chart]");
if (!chartRoots.length) { if (!chartRoots.length) {

File diff suppressed because one or more lines are too long