diff --git a/static/css/components/card.css b/static/css/components/card.css index f3c83105..e9268abc 100644 --- a/static/css/components/card.css +++ b/static/css/components/card.css @@ -734,6 +734,21 @@ body.hide-card-version .hl-badge { } } +/* Grid-scoped loading overlay (replaces full-page overlay for VirtualScroller refreshes) */ +.grid-loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--lora-bg-transparent, oklch(0% 0 0 / 0.3)); + display: flex; + justify-content: center; + align-items: center; + z-index: 100; + pointer-events: none; +} + /* Add after the existing .model-card:hover styles */ @keyframes update-pulse { diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 6b54a352..c1a8d586 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -115,7 +115,10 @@ export class BaseModelApiClient { const pageState = this.getPageState(); try { - state.loadingManager.showSimpleLoading(`Loading more ${this.apiConfig.config.displayName}s...`); + // Use grid-scoped loading instead of full-page overlay + if (state.virtualScroller?.showGridLoading) { + state.virtualScroller.showGridLoading(); + } pageState.isLoading = true; if (resetPage) { @@ -154,7 +157,14 @@ export class BaseModelApiClient { throw error; } finally { pageState.isLoading = false; - state.loadingManager.hide(); + // Wait for the next rAF so refreshWithData's scheduleRender has + // completed rendering new cards before hiding the grid loading overlay. + // This eliminates the ~6.7ms blank-frame gap that caused the flicker. + if (state.virtualScroller?.hideGridLoading) { + requestAnimationFrame(() => { + state.virtualScroller.hideGridLoading(); + }); + } } } diff --git a/static/js/utils/VirtualScroller.js b/static/js/utils/VirtualScroller.js index ea3e21fe..bf216697 100644 --- a/static/js/utils/VirtualScroller.js +++ b/static/js/utils/VirtualScroller.js @@ -657,6 +657,9 @@ export class VirtualScroller { this.resizeObserver.disconnect(); } + // Remove any active grid loading overlay + this.hideGridLoading(); + // Remove rendered elements this.clearRenderedItems(); @@ -1130,4 +1133,30 @@ export class VirtualScroller { index: targetIndex }; } + + /** + * Show a grid-scoped loading indicator (replaces full-page overlay) + * Only covers the card grid area, leaving header/sidebar unaffected. + */ + showGridLoading() { + // Remove any stale overlay from a prior deferred hide (e.g. from final rAF) + this.hideGridLoading(); + const overlay = document.createElement('div'); + overlay.className = 'grid-loading-overlay'; + const spinner = document.createElement('div'); + spinner.className = 'loading-spinner'; + overlay.appendChild(spinner); + this.gridElement.appendChild(overlay); + this.gridLoadingOverlay = overlay; + } + + /** + * Hide the grid-scoped loading indicator. + */ + hideGridLoading() { + if (this.gridLoadingOverlay) { + this.gridLoadingOverlay.remove(); + this.gridLoadingOverlay = null; + } + } }