From 8db9382c1c029b201934d23523db5bdb0b66a97f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:19:37 +0000 Subject: [PATCH] feat(ux): replace loading overlay with contextual button spinner Replaces the disruptive full-screen loading overlay with a contextual loading spinner inside the "Generate Poster" button. This improves the user experience by providing immediate, non-blocking feedback directly on the interactive element. The implementation includes: - A new loading state for the primary button with a spinner. - Updated JavaScript to manage the button's disabled, loading, and accessible states. - Removal of the old, redundant loading overlay element and styles. Accessibility is addressed by dynamically updating the button's text and `aria-label` to "Generating..." and "Generating poster, please wait." respectively, ensuring screen reader users are informed of the ongoing action. --- web_app/static/index.html | 9 ++++----- web_app/static/script.js | 23 ++++++++++++++++++----- web_app/static/style.css | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/web_app/static/index.html b/web_app/static/index.html index 0d56371..b3084d0 100644 --- a/web_app/static/index.html +++ b/web_app/static/index.html @@ -116,7 +116,10 @@

Lyrics

- + @@ -143,10 +146,6 @@

Poster

- diff --git a/web_app/static/script.js b/web_app/static/script.js index 441a4ed..ff0be7a 100644 --- a/web_app/static/script.js +++ b/web_app/static/script.js @@ -20,7 +20,6 @@ const startLineInput = document.getElementById('startLine'); const endLineInput = document.getElementById('endLine'); const themeInput = document.getElementById('themeInput'); const fontInput = document.getElementById('fontInput'); -const loadingOverlay = document.getElementById('loadingOverlay'); let currentMetadata = null; let searchDebounceTimer = null; @@ -613,7 +612,15 @@ function handleLyricLineClick(lineNumber) { async function generatePoster() { if (!currentMetadata) return; - loadingOverlay.style.display = 'flex'; + const btnText = generateBtn.querySelector('.btn-text'); + if (!btnText) return; + + // --- New Loading State Logic --- + const originalText = btnText.textContent; + generateBtn.classList.add('loading'); + generateBtn.disabled = true; + btnText.textContent = 'Generating...'; + generateBtn.setAttribute('aria-label', 'Generating poster, please wait.'); const indexingToggle = document.getElementById('indexingToggle'); const accentToggle = document.getElementById('accentToggle'); @@ -635,8 +642,10 @@ async function generatePoster() { const end = endVal; if (isNaN(start) || isNaN(end) || start >= end) { - loadingOverlay.style.display = 'none'; showToast("Please enter a valid range (Start must be less than End).", "error"); + // --- Reset Loading State --- + generateBtn.classList.remove('loading'); + generateBtn.disabled = false; return; } @@ -669,13 +678,17 @@ async function generatePoster() { posterContainer.innerHTML = ''; posterContainer.appendChild(img); showDownloadButton(imageUrl, data.filename); - loadingOverlay.style.display = 'none'; }; } catch (error) { console.error("Generation failed", error); showToast(`Error: ${error.message}`, "error"); - loadingOverlay.style.display = 'none'; + } finally { + // --- Reset Loading State --- + generateBtn.classList.remove('loading'); + generateBtn.disabled = false; + btnText.textContent = originalText; + generateBtn.removeAttribute('aria-label'); } } diff --git a/web_app/static/style.css b/web_app/static/style.css index 468eb76..940d28d 100644 --- a/web_app/static/style.css +++ b/web_app/static/style.css @@ -159,6 +159,39 @@ header p { background: var(--accent-color); color: #1a1a1f; width: 100%; + position: relative; /* For spinner placement */ +} + +.primary-btn .btn-text { + transition: opacity 0.2s ease-out; +} + +/* Spinner is inside the button, inheriting from the main .spinner keyframes */ +.primary-btn .spinner { + position: absolute; + width: 20px; + height: 20px; + border-width: 2px; + border-color: rgba(0, 0, 0, 0.2); + border-top-color: #1a1a1f; /* Match button text color */ + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: none; /* Hidden by default */ +} + +.primary-btn.loading { + cursor: not-allowed; + pointer-events: none; +} + +.primary-btn.loading .btn-text { + opacity: 0; + visibility: hidden; /* Hide from screen readers */ +} + +.primary-btn.loading .spinner { + display: block; } .primary-btn:hover {