fix(ingest): implement beautiful upload loading state and fix button loading spinner visibility #66

Merged
mjasin merged 4 commits from bugfix/book-upload-indicator into infra/beta-deploy-test 2026-06-01 16:42:09 +00:00
2 changed files with 19 additions and 9 deletions
Showing only changes of commit f81b2acc40 - Show all commits
@@ -34,7 +34,14 @@
</div> </div>
</div> </div>
<div class="verification-state" style="@(IsVerifying && !IsParsing && !IsIndexing ? "display:flex;" : "display:none;")"> <div class="ingesting-state shimmer" style="@(IsIngesting && !IsIndexing ? "display:flex;" : "display:none;")">
<div class="shimmer-content">
<div class="spinner"></div>
<p>Saving book to library...</p>
</div>
</div>
<div class="verification-state" style="@(IsVerifying && !IsParsing && !IsIndexing && !IsIngesting ? "display:flex;" : "display:none;")">
@if (Metadata != null) @if (Metadata != null)
{ {
<div class="verification-layout"> <div class="verification-layout">
@@ -70,8 +77,8 @@
<div class="actions"> <div class="actions">
<NexusButton Class="btn-secondary" OnClick="Reset" Disabled="IsIngesting">Back</NexusButton> <NexusButton Class="btn-secondary" OnClick="Reset" Disabled="IsIngesting">Back</NexusButton>
<NexusButton Class="@($"btn-primary {(IsIngesting ? "btn-loading" : "")}")" <NexusButton Class="@($"btn-primary {(IsIngesting ? "btn-loading" : "")}")"
mjasin marked this conversation as resolved
Review

🟢 Button spinner runs inside a display:none parent — unnecessary GPU compositor work

The NexusButton with btn-loading class is inside .verification-state, which is hidden via display:none when IsIngesting is true. The CSS spinner animation (spin 0.8s linear infinite) and the new filter: drop-shadow(...) are applied to DOM nodes that are not rendered to the screen.

While browsers generally optimize display:none subtrees, filter properties can still hold GPU resources depending on the browser's paint/compositor implementation. Since ingesting-state now provides its own dedicated spinner, consider simplifying the button:

<NexusButton Class="btn-primary" OnClick="SaveToLibrary" Disabled="IsIngesting">
    Save to Library
</NexusButton>

The btn-loading state on the button is redundant now that ingesting-state gives full visual feedback.

🟢 **Button spinner runs inside a `display:none` parent — unnecessary GPU compositor work** The `NexusButton` with `btn-loading` class is inside `.verification-state`, which is hidden via `display:none` when `IsIngesting` is true. The CSS spinner animation (`spin 0.8s linear infinite`) and the new `filter: drop-shadow(...)` are applied to DOM nodes that are not rendered to the screen. While browsers generally optimize `display:none` subtrees, `filter` properties can still hold GPU resources depending on the browser's paint/compositor implementation. Since `ingesting-state` now provides its own dedicated spinner, consider simplifying the button: ```razor <NexusButton Class="btn-primary" OnClick="SaveToLibrary" Disabled="IsIngesting"> Save to Library </NexusButton> ``` The `btn-loading` state on the button is redundant now that `ingesting-state` gives full visual feedback.
OnClick="SaveToLibrary" OnClick="SaveToLibrary"
Disabled="IsIngesting"> Disabled="IsIngesting">
@(IsIngesting ? "" : "Save to Library") @(IsIngesting ? "" : "Save to Library")
</NexusButton> </NexusButton>
</div> </div>
@@ -79,7 +86,7 @@
</div> </div>
<div class="upload-state @(_isDragging ? "drag-over" : "")" <div class="upload-state @(_isDragging ? "drag-over" : "")"
style="@(!IsParsing && !IsVerifying && !IsIndexing ? "display:flex;" : "display:none;")" style="@(!IsParsing && !IsVerifying && !IsIndexing && !IsIngesting ? "display:flex;" : "display:none;")"
@ondragenter="OnDragEnter" @ondragenter="OnDragEnter"
@ondragleave="OnDragLeave"> @ondragleave="OnDragLeave">
<div class="drop-zone"> <div class="drop-zone">
@@ -118,8 +118,9 @@
z-index: 10; z-index: 10;
} }
/* Parsing State */ /* Parsing and Ingesting States */
.parsing-state { .parsing-state,
.ingesting-state {
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -158,7 +159,8 @@
filter: drop-shadow(0 0 8px rgba(0, 255, 153, 0.3)); filter: drop-shadow(0 0 8px rgba(0, 255, 153, 0.3));
} }
.parsing-state p { .parsing-state p,
.ingesting-state p {
color: var(--nexus-text); color: var(--nexus-text);
font-family: var(--nexus-font-mono, monospace); font-family: var(--nexus-font-mono, monospace);
font-size: 0.9rem; font-size: 0.9rem;
@@ -371,10 +373,11 @@
position: absolute; position: absolute;
width: 20px; width: 20px;
height: 20px; height: 20px;
border: 2px solid rgba(0, 0, 0, 0.1); border: 2px solid rgba(255, 255, 255, 0.2);
border-top-color: #000; border-top-color: var(--nexus-neon, #00ffaa);
border-radius: 50%; border-radius: 50%;
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa));
} }
/* Indexing State */ /* Indexing State */