Phase 17Z - Legacy Retirement and Cleanup
This commit is contained in:
parent
0020ad37d7
commit
d334661178
@ -1668,32 +1668,6 @@ public sealed class ScenesController(
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(returnUrl)
|
||||
|
||||
@ -122,8 +122,6 @@ public interface ISceneService
|
||||
Task<SceneEditViewModel?> GetEditAsync(int sceneId);
|
||||
Task<int> SaveAsync(SceneEditViewModel model);
|
||||
Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId);
|
||||
Task<SceneOccupancyEditorViewModel?> GetOccupancyEditorAsync(int sceneId, int? floorPlanId = null);
|
||||
Task SaveOccupancyAsync(SceneOccupancySaveViewModel model);
|
||||
Task ArchiveAsync(int sceneId);
|
||||
Task MoveAsync(int sceneId, string direction);
|
||||
Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position);
|
||||
@ -1464,8 +1462,6 @@ public sealed class SceneService(
|
||||
IAssetRepository assets,
|
||||
ICharacterRepository characters,
|
||||
ILocationRepository locations,
|
||||
IFloorPlanRepository floorPlans,
|
||||
ISceneFloorPlanOccupancyRepository sceneOccupancy,
|
||||
IWarningRepository warnings,
|
||||
ISceneDependencyRepository sceneDependencies,
|
||||
IWriterWorkspaceRepository writerWorkspace,
|
||||
@ -1842,172 +1838,10 @@ public sealed class SceneService(
|
||||
ReturnProjectID = model.ReturnProjectID,
|
||||
ReturnBookID = model.ReturnBookID
|
||||
};
|
||||
model.Occupancy = model.SceneID == 0
|
||||
? new SceneOccupancyEditorViewModel()
|
||||
: await BuildOccupancyEditorAsync(model, projectAssets);
|
||||
model.LocationConsistencyMessages = BuildLocationConsistencyMessages(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)
|
||||
{
|
||||
if (model.SceneID == 0 || model.Project is null || model.Book is null)
|
||||
|
||||
@ -398,8 +398,6 @@ public sealed class SceneEditViewModel
|
||||
public IReadOnlyList<SelectListItem> SceneAttachmentTypeOptions { get; set; } = [];
|
||||
public StorageUsage? StorageUsage { get; set; }
|
||||
public StoryStateViewModel StoryState { get; set; } = new();
|
||||
public SceneOccupancyEditorViewModel Occupancy { get; set; } = new();
|
||||
|
||||
public int? ReturnProjectID { get; set; }
|
||||
public int? ReturnBookID { get; set; }
|
||||
public bool ReturnToTimeline { get; set; }
|
||||
@ -559,60 +557,6 @@ public sealed class SceneCharacterParticipationInputViewModel
|
||||
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 List<CharacterStoryStateViewModel> Characters { get; set; } = [];
|
||||
|
||||
@ -54,16 +54,6 @@
|
||||
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>
|
||||
|
||||
<div class="page-heading compact">
|
||||
@ -329,38 +319,6 @@
|
||||
</div>
|
||||
</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 class="theme-audit-preview" data-theme="dark" data-bs-theme="dark" aria-label="Dark theme preview">
|
||||
@ -613,38 +571,6 @@
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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 > @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 > @location.LocationPath</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-sm mt-3" type="submit">Save occupancy</button>
|
||||
}
|
||||
</form>
|
||||
@ -849,37 +849,10 @@ a:hover {
|
||||
box-shadow: 0 4px 12px rgba(62, 45, 28, 0.12);
|
||||
}
|
||||
|
||||
.scene-inspector-panel,
|
||||
.scene-inspector-root {
|
||||
.scene-inspector-panel {
|
||||
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-select {
|
||||
border-color: rgba(84, 68, 48, 0.2);
|
||||
@ -1503,45 +1476,6 @@ a:focus-visible {
|
||||
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 {
|
||||
border-color: rgba(84, 68, 48, 0.13);
|
||||
background: rgba(255, 253, 248, 0.78);
|
||||
@ -1892,10 +1826,6 @@ a:focus-visible {
|
||||
[data-theme="dark"] .writer-mini-card,
|
||||
[data-theme="dark"] .timeline-setting-card,
|
||||
[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"] .asset-event-item,
|
||||
[data-theme="dark"] .story-asset-event-item {
|
||||
@ -1989,25 +1919,6 @@ a:focus-visible {
|
||||
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"] .asset-event-item,
|
||||
[data-theme="dark"] .story-asset-event-item {
|
||||
@ -5108,7 +5019,4 @@ a:focus-visible {
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.inspector-nav-links {
|
||||
max-height: 8.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
2
PlotLine/wwwroot/css/plotline-theme.min.css
vendored
2
PlotLine/wwwroot/css/plotline-theme.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1842,27 +1842,6 @@ body.timeline-inspector-resizing {
|
||||
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 {
|
||||
border: 1px solid rgba(138, 111, 61, 0.22);
|
||||
border-radius: 8px;
|
||||
@ -2123,36 +2102,9 @@ body.timeline-inspector-resizing {
|
||||
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,
|
||||
.context-nav,
|
||||
.breadcrumb-trail,
|
||||
.inspector-nav-links {
|
||||
.breadcrumb-trail {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
@ -2189,97 +2141,6 @@ body.timeline-inspector-resizing {
|
||||
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 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@ -3628,18 +3489,6 @@ body.dragging-location [data-drag-type="location"] {
|
||||
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 {
|
||||
align-items: end;
|
||||
}
|
||||
@ -4409,8 +4258,7 @@ body.dragging-location [data-drag-type="location"] {
|
||||
.breadcrumb-trail,
|
||||
.context-header,
|
||||
.button-row,
|
||||
.story-bible-nav,
|
||||
.inspector-quick-nav {
|
||||
.story-bible-nav {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@ -6272,50 +6120,6 @@ body.scene-inspector-editor-resizing {
|
||||
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 {
|
||||
cursor: default;
|
||||
touch-action: auto;
|
||||
@ -6595,12 +6399,6 @@ body.scene-inspector-editor-resizing {
|
||||
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,
|
||||
.dark .floor-plan-occupancy-token {
|
||||
border-color: #1f2630;
|
||||
|
||||
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
@ -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 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]");
|
||||
if (!chartRoots.length) {
|
||||
|
||||
2
PlotLine/wwwroot/js/site.min.js
vendored
2
PlotLine/wwwroot/js/site.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user