Implemented the Timeline C3 refinement.
This commit is contained in:
parent
36c214fab8
commit
e5ed45897a
@ -462,8 +462,8 @@ public sealed class ProjectTimelineSettings
|
||||
public bool ShowSceneCards { get; set; } = true;
|
||||
public bool ShowMetricShape { get; set; } = true;
|
||||
public bool ShowPlotLines { get; set; } = true;
|
||||
public bool ShowStoryAssets { get; set; } = true;
|
||||
public bool ShowCharacterAppearances { get; set; } = true;
|
||||
public bool ShowStoryAssets { get; set; }
|
||||
public bool ShowCharacterAppearances { get; set; }
|
||||
public bool ShowWarnings { get; set; } = true;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public DateTime UpdatedDate { get; set; }
|
||||
|
||||
@ -1896,8 +1896,8 @@ public sealed class TimelineService(
|
||||
.ToList();
|
||||
var needsPlotTimelineData = settings.ShowPlotLines || filter.PlotLineID.HasValue || filter.PlotThreadID.HasValue || filter.FocusType is "plotline" or "plotthread";
|
||||
var needsMetricTimelineData = settings.ShowMetricShape && selectedMetricIds.Any();
|
||||
var needsAssetTimelineData = settings.ShowStoryAssets || filter.StoryAssetID.HasValue || filter.FocusType == "asset";
|
||||
var needsCharacterTimelineData = settings.ShowCharacterAppearances || filter.CharacterID.HasValue || filter.FocusType == "character";
|
||||
var needsAssetTimelineData = settings.ShowSceneCards || settings.ShowStoryAssets || filter.StoryAssetID.HasValue || filter.FocusType == "asset";
|
||||
var needsCharacterTimelineData = settings.ShowSceneCards || settings.ShowCharacterAppearances || filter.CharacterID.HasValue || filter.FocusType == "character";
|
||||
var needsWarnings = settings.ShowWarnings || filter.WarningSeverityID.HasValue || filter.WarningTypeID.HasValue || filter.ShowWarningsOnly;
|
||||
|
||||
var timeline = await TimeTimelineLoadAsync(
|
||||
@ -2042,6 +2042,7 @@ public sealed class TimelineService(
|
||||
PlotLanes = settings.ShowPlotLines ? BuildPlotLanes(timeline, orderedScenes) : [],
|
||||
AssetLanes = settings.ShowStoryAssets ? BuildAssetLanes(assetTimeline.Assets, assetTimeline.Events, orderedScenes) : [],
|
||||
CharacterLanes = settings.ShowCharacterAppearances ? BuildCharacterLanes(characterTimeline.Characters, characterTimeline.Appearances, orderedScenes) : [],
|
||||
SceneOverviews = settings.ShowSceneCards ? BuildSceneOverviews(orderedScenes, characterTimeline.Appearances, assetTimeline.Assets, assetTimeline.Events) : new Dictionary<int, TimelineSceneOverviewViewModel>(),
|
||||
WarningsByScene = warningsByScene,
|
||||
MatchingSceneIDs = matchingSceneIds,
|
||||
FocusSceneIDs = focusSceneIds,
|
||||
@ -2519,6 +2520,96 @@ public sealed class TimelineService(
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<int, TimelineSceneOverviewViewModel> BuildSceneOverviews(
|
||||
IReadOnlyList<Scene> orderedScenes,
|
||||
IReadOnlyList<SceneCharacter> appearances,
|
||||
IReadOnlyList<StoryAsset> assets,
|
||||
IReadOnlyList<AssetEvent> assetEvents)
|
||||
{
|
||||
var assetImportanceById = assets.ToDictionary(x => x.StoryAssetID, x => x.Importance);
|
||||
var charactersByScene = appearances
|
||||
.GroupBy(x => x.SceneID)
|
||||
.ToDictionary(
|
||||
sceneGroup => sceneGroup.Key,
|
||||
sceneGroup => sceneGroup
|
||||
.GroupBy(x => x.CharacterID)
|
||||
.Select(characterGroup =>
|
||||
{
|
||||
var first = characterGroup
|
||||
.OrderByDescending(x => string.Equals(x.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase))
|
||||
.ThenBy(x => x.SceneCharacterID)
|
||||
.First();
|
||||
return new TimelineSceneCharacterOverviewViewModel
|
||||
{
|
||||
CharacterID = first.CharacterID,
|
||||
CharacterName = first.CharacterName,
|
||||
IsPov = string.Equals(first.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
})
|
||||
.OrderByDescending(x => x.IsPov)
|
||||
.ThenBy(x => x.CharacterName)
|
||||
.ToList());
|
||||
var assetsByScene = assetEvents
|
||||
.GroupBy(x => x.SceneID)
|
||||
.ToDictionary(
|
||||
sceneGroup => sceneGroup.Key,
|
||||
sceneGroup => sceneGroup
|
||||
.GroupBy(x => x.StoryAssetID)
|
||||
.Select(assetGroup =>
|
||||
{
|
||||
var first = assetGroup.OrderByDescending(x => x.UpdatedDate).First();
|
||||
return new TimelineSceneAssetOverviewViewModel
|
||||
{
|
||||
StoryAssetID = first.StoryAssetID,
|
||||
AssetName = first.AssetName,
|
||||
Importance = assetImportanceById.GetValueOrDefault(first.StoryAssetID),
|
||||
EventTypeName = first.AssetEventTypeName
|
||||
};
|
||||
})
|
||||
.OrderByDescending(x => x.Importance)
|
||||
.ThenBy(x => x.AssetName)
|
||||
.ToList());
|
||||
var characterLocationsByScene = appearances
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.LocationPath) || !string.IsNullOrWhiteSpace(x.LocationName))
|
||||
.GroupBy(x => x.SceneID)
|
||||
.ToDictionary(
|
||||
sceneGroup => sceneGroup.Key,
|
||||
sceneGroup => sceneGroup
|
||||
.Select(x => x.LocationPath ?? x.LocationName ?? string.Empty)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x)
|
||||
.ToList());
|
||||
|
||||
return orderedScenes.ToDictionary(scene => scene.SceneID, scene =>
|
||||
{
|
||||
var locations = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(scene.PrimaryLocationPath))
|
||||
{
|
||||
locations.Add(scene.PrimaryLocationPath);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(scene.PrimaryLocationName))
|
||||
{
|
||||
locations.Add(scene.PrimaryLocationName);
|
||||
}
|
||||
if (characterLocationsByScene.TryGetValue(scene.SceneID, out var characterLocations))
|
||||
{
|
||||
locations.AddRange(characterLocations);
|
||||
}
|
||||
|
||||
return new TimelineSceneOverviewViewModel
|
||||
{
|
||||
Characters = charactersByScene.GetValueOrDefault(scene.SceneID, []),
|
||||
Assets = assetsByScene.GetValueOrDefault(scene.SceneID, []),
|
||||
Locations = locations
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(6)
|
||||
.ToList()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static int CharacterLanePriority(Character character, IEnumerable<SceneCharacter> appearances)
|
||||
{
|
||||
var description = character.DefaultDescription ?? string.Empty;
|
||||
|
||||
61
PlotLine/Sql/070_TimelineDefaultSceneFocusedView.sql
Normal file
61
PlotLine/Sql/070_TimelineDefaultSceneFocusedView.sql
Normal file
@ -0,0 +1,61 @@
|
||||
DECLARE @constraintName sysname;
|
||||
|
||||
SELECT @constraintName = dc.name
|
||||
FROM sys.default_constraints dc
|
||||
INNER JOIN sys.columns c ON c.default_object_id = dc.object_id
|
||||
INNER JOIN sys.tables t ON t.object_id = c.object_id
|
||||
INNER JOIN sys.schemas s ON s.schema_id = t.schema_id
|
||||
WHERE s.name = N'dbo'
|
||||
AND t.name = N'ProjectTimelineSettings'
|
||||
AND c.name = N'ShowStoryAssets';
|
||||
|
||||
IF @constraintName IS NOT NULL
|
||||
BEGIN
|
||||
EXEC(N'ALTER TABLE dbo.ProjectTimelineSettings DROP CONSTRAINT ' + QUOTENAME(@constraintName));
|
||||
END;
|
||||
|
||||
IF COL_LENGTH(N'dbo.ProjectTimelineSettings', N'ShowStoryAssets') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProjectTimelineSettings
|
||||
ADD CONSTRAINT DF_ProjectTimelineSettings_ShowStoryAssets DEFAULT 0 FOR ShowStoryAssets;
|
||||
END;
|
||||
GO
|
||||
|
||||
DECLARE @constraintName sysname;
|
||||
|
||||
SELECT @constraintName = dc.name
|
||||
FROM sys.default_constraints dc
|
||||
INNER JOIN sys.columns c ON c.default_object_id = dc.object_id
|
||||
INNER JOIN sys.tables t ON t.object_id = c.object_id
|
||||
INNER JOIN sys.schemas s ON s.schema_id = t.schema_id
|
||||
WHERE s.name = N'dbo'
|
||||
AND t.name = N'ProjectTimelineSettings'
|
||||
AND c.name = N'ShowCharacterAppearances';
|
||||
|
||||
IF @constraintName IS NOT NULL
|
||||
BEGIN
|
||||
EXEC(N'ALTER TABLE dbo.ProjectTimelineSettings DROP CONSTRAINT ' + QUOTENAME(@constraintName));
|
||||
END;
|
||||
|
||||
IF COL_LENGTH(N'dbo.ProjectTimelineSettings', N'ShowCharacterAppearances') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProjectTimelineSettings
|
||||
ADD CONSTRAINT DF_ProjectTimelineSettings_ShowCharacterAppearances DEFAULT 0 FOR ShowCharacterAppearances;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.ProjectTimelineSettings_EnsureDefaults
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID)
|
||||
BEGIN
|
||||
INSERT dbo.ProjectTimelineSettings
|
||||
(ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, ShowStoryAssets, ShowCharacterAppearances, ShowWarnings)
|
||||
VALUES
|
||||
(@ProjectID, 1, 1, 1, 0, 0, 1);
|
||||
END
|
||||
END;
|
||||
GO
|
||||
@ -963,6 +963,7 @@ public sealed class TimelineViewModel
|
||||
public IReadOnlyList<PlotLineLaneViewModel> PlotLanes { get; set; } = [];
|
||||
public IReadOnlyList<AssetLaneViewModel> AssetLanes { get; set; } = [];
|
||||
public IReadOnlyList<CharacterLaneViewModel> CharacterLanes { get; set; } = [];
|
||||
public IReadOnlyDictionary<int, TimelineSceneOverviewViewModel> SceneOverviews { get; set; } = new Dictionary<int, TimelineSceneOverviewViewModel>();
|
||||
public IReadOnlyDictionary<int, IReadOnlyList<ContinuityWarning>> WarningsByScene { get; set; } = new Dictionary<int, IReadOnlyList<ContinuityWarning>>();
|
||||
public IReadOnlySet<int> MatchingSceneIDs { get; set; } = new HashSet<int>();
|
||||
public IReadOnlySet<int> FocusSceneIDs { get; set; } = new HashSet<int>();
|
||||
@ -978,8 +979,8 @@ public sealed class TimelineSettingsViewModel
|
||||
public bool ShowSceneCards { get; set; } = true;
|
||||
public bool ShowMetricShape { get; set; } = true;
|
||||
public bool ShowPlotLines { get; set; } = true;
|
||||
public bool ShowStoryAssets { get; set; } = true;
|
||||
public bool ShowCharacterAppearances { get; set; } = true;
|
||||
public bool ShowStoryAssets { get; set; }
|
||||
public bool ShowCharacterAppearances { get; set; }
|
||||
public bool ShowWarnings { get; set; } = true;
|
||||
public List<int> SelectedMetricTypeIDs { get; set; } = [];
|
||||
public IReadOnlyList<TimelineMetricSettingOptionViewModel> MetricOptions { get; set; } = [];
|
||||
@ -994,6 +995,28 @@ public sealed class TimelineMetricSettingOptionViewModel
|
||||
public bool IsSelected { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TimelineSceneOverviewViewModel
|
||||
{
|
||||
public IReadOnlyList<TimelineSceneCharacterOverviewViewModel> Characters { get; set; } = [];
|
||||
public IReadOnlyList<TimelineSceneAssetOverviewViewModel> Assets { get; set; } = [];
|
||||
public IReadOnlyList<string> Locations { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class TimelineSceneCharacterOverviewViewModel
|
||||
{
|
||||
public int CharacterID { get; set; }
|
||||
public string CharacterName { get; set; } = string.Empty;
|
||||
public bool IsPov { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TimelineSceneAssetOverviewViewModel
|
||||
{
|
||||
public int StoryAssetID { get; set; }
|
||||
public string AssetName { get; set; } = string.Empty;
|
||||
public int Importance { get; set; }
|
||||
public string? EventTypeName { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ProjectSceneMetricsViewModel
|
||||
{
|
||||
public Project Project { get; set; } = new();
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
int BookSceneColumns(BookTimelineViewModel book) => Math.Max(book.Chapters.Any() ? book.Chapters.Sum(ChapterSceneColumns) : 1, 1);
|
||||
var hasSelectedScene = Model.SelectedScene is not null;
|
||||
int CharacterAppearanceCount(CharacterLaneViewModel lane) => lane.SceneSlots.Sum(slot => slot.Appearances.Count);
|
||||
int HiddenCount<T>(IReadOnlyList<T> items, int visibleCount) => Math.Max(0, items.Count - visibleCount);
|
||||
string PlotMarkerKind(string? plotEventTypeName) => (plotEventTypeName ?? "Progress").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"start" => "start",
|
||||
@ -646,6 +647,7 @@
|
||||
<option value="book">Book</option>
|
||||
<option value="chapter">Chapter</option>
|
||||
<option value="scene-detail">Scene Detail</option>
|
||||
<option value="scene-overview">Scene Overview</option>
|
||||
</select>
|
||||
</label>
|
||||
@if (Model.Settings.ShowSceneCards) { <label><input type="checkbox" data-timeline-toggle="scene-cards" checked /> Scene cards</label> }
|
||||
@ -1041,6 +1043,7 @@ else
|
||||
var selectedClass = scene.SceneID == Model.SelectedSceneID ? "selected" : "";
|
||||
var matchClass = Model.MatchingSceneIDs.Contains(scene.SceneID) ? "filter-match" : "filter-dim";
|
||||
var focusClass = Model.FocusSceneIDs.Contains(scene.SceneID) ? "focus-match" : (Model.FocusSceneIDs.Any() ? "focus-dim" : "");
|
||||
var overview = Model.SceneOverviews.GetValueOrDefault(scene.SceneID) ?? new TimelineSceneOverviewViewModel();
|
||||
var povLabel = Model.ScenePovLabels.GetValueOrDefault(scene.SceneID, scene.POVCharacterID.HasValue ? "POV set" : "No POV");
|
||||
var metricSummary = string.Join(" / ", Model.MetricGraphs.Take(2).Select(graph => $"{graph.MetricName}: {graph.Points.FirstOrDefault(x => x.SceneID == scene.SceneID)?.Value ?? 0}"));
|
||||
var plotSearch = string.Join(' ', Model.PlotLanes
|
||||
@ -1111,6 +1114,87 @@ else
|
||||
</div>
|
||||
}
|
||||
<span class="scene-summary">@scene.Summary</span>
|
||||
<div class="scene-overview-panel">
|
||||
<div class="scene-overview-section scene-overview-characters">
|
||||
<span class="scene-overview-label">Characters</span>
|
||||
<div class="scene-overview-chip-row">
|
||||
@if (overview.Characters.Any())
|
||||
{
|
||||
foreach (var character in overview.Characters.Take(7))
|
||||
{
|
||||
<span class="scene-overview-chip @(character.IsPov ? "is-pov" : "")">@character.CharacterName@(character.IsPov ? " [POV]" : "")</span>
|
||||
}
|
||||
if (HiddenCount(overview.Characters, 7) > 0)
|
||||
{
|
||||
<span class="scene-overview-chip is-more">+@HiddenCount(overview.Characters, 7) more</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="scene-overview-empty">No characters linked</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="scene-overview-section scene-overview-assets">
|
||||
<span class="scene-overview-label">Assets</span>
|
||||
<div class="scene-overview-chip-row">
|
||||
@if (overview.Assets.Any())
|
||||
{
|
||||
foreach (var asset in overview.Assets.Take(6))
|
||||
{
|
||||
<span class="scene-overview-chip @(asset.Importance >= 8 ? "is-important" : "")">@asset.AssetName</span>
|
||||
}
|
||||
if (HiddenCount(overview.Assets, 6) > 0)
|
||||
{
|
||||
<span class="scene-overview-chip is-more">+@HiddenCount(overview.Assets, 6) more</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="scene-overview-empty">No assets linked</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="scene-overview-section scene-overview-locations">
|
||||
<span class="scene-overview-label">Locations</span>
|
||||
<div class="scene-overview-chip-row">
|
||||
@if (overview.Locations.Any())
|
||||
{
|
||||
foreach (var location in overview.Locations.Take(4))
|
||||
{
|
||||
<span class="scene-overview-chip">@location</span>
|
||||
}
|
||||
if (HiddenCount(overview.Locations, 4) > 0)
|
||||
{
|
||||
<span class="scene-overview-chip is-more">+@HiddenCount(overview.Locations, 4) more</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="scene-overview-empty">No locations linked</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (scene.PurposeLabels.Any() || !string.IsNullOrWhiteSpace(scene.Summary))
|
||||
{
|
||||
<div class="scene-overview-section scene-overview-purpose">
|
||||
<span class="scene-overview-label">Purpose</span>
|
||||
@if (scene.PurposeLabels.Any())
|
||||
{
|
||||
<div class="scene-overview-chip-row">
|
||||
@foreach (var purpose in scene.PurposeLabels.Take(4))
|
||||
{
|
||||
<span class="scene-overview-chip">@purpose.PurposeName</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(scene.Summary))
|
||||
{
|
||||
<span class="scene-overview-brief">@scene.Summary</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
@ -3078,6 +3078,73 @@ body.timeline-inspector-resizing {
|
||||
background: rgba(47, 111, 99, 0.08);
|
||||
}
|
||||
|
||||
.scene-overview-panel {
|
||||
display: none;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.scene-overview-section {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.scene-overview-label {
|
||||
color: var(--plotline-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.scene-overview-chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.scene-overview-chip,
|
||||
.scene-overview-empty,
|
||||
.scene-overview-brief {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
border-radius: 999px;
|
||||
padding: 3px 8px;
|
||||
background: rgba(47, 111, 99, 0.08);
|
||||
color: var(--plotline-ink);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 750;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.scene-overview-chip.is-pov {
|
||||
border: 1px solid rgba(197, 139, 43, 0.38);
|
||||
background: #fff3d3;
|
||||
}
|
||||
|
||||
.scene-overview-chip.is-important {
|
||||
border: 1px solid rgba(103, 80, 164, 0.3);
|
||||
background: #f0edf8;
|
||||
}
|
||||
|
||||
.scene-overview-chip.is-more,
|
||||
.scene-overview-empty {
|
||||
color: var(--plotline-muted);
|
||||
background: rgba(102, 113, 123, 0.08);
|
||||
}
|
||||
|
||||
.scene-overview-brief {
|
||||
display: block;
|
||||
width: auto;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
color: var(--plotline-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.timeline-scene-card.search-hit,
|
||||
.timeline-scene-card.keyboard-focus {
|
||||
outline: 3px solid rgba(197, 139, 43, 0.34);
|
||||
@ -3141,6 +3208,27 @@ body.timeline-inspector-resizing {
|
||||
-webkit-line-clamp: 7;
|
||||
}
|
||||
|
||||
.timeline-filtered-root.zoom-scene-overview {
|
||||
--timeline-scene-width: 380px;
|
||||
}
|
||||
|
||||
.timeline-filtered-root.zoom-scene-overview .timeline-scenes {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.timeline-filtered-root.zoom-scene-overview .timeline-scene-card {
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.timeline-filtered-root.zoom-scene-overview .scene-summary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-filtered-root.zoom-scene-overview .scene-overview-panel {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.timeline-filtered-root.density-expanded .timeline-scenes {
|
||||
min-height: 230px;
|
||||
}
|
||||
|
||||
@ -1217,7 +1217,7 @@
|
||||
"[data-timeline-minimap]"
|
||||
].join(",");
|
||||
const densityValues = ["compact", "comfortable", "expanded"];
|
||||
const zoomValues = ["book", "chapter", "scene-detail"];
|
||||
const zoomValues = ["book", "chapter", "scene-detail", "scene-overview"];
|
||||
const inspectorDrawer = root.querySelector("[data-inspector-drawer]");
|
||||
const inspectorResizer = root.querySelector("[data-inspector-resizer]");
|
||||
const inspectorClose = root.querySelector("[data-inspector-close]");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user