From e28aeccfd158db67529d71da04742213a8ec4272 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:16:37 -0700 Subject: [PATCH] Redesign pool fix match modal: fixed height, no layout shift (#186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modal now uses a fixed 600px height from open — results scroll within a dedicated area instead of growing the modal and pushing inputs up. This eliminates the layout shift that caused accidental result clicks. Other fixes: - Input fields now have labels (Track, Artist) - Overlay dismiss uses mousedown with stopPropagation to prevent accidental close when clicking near inputs - Reduced results from 50 to 20 for faster response - Clean minimal design matching app style - Mobile: full-screen modal, stacked inputs with 44px touch targets --- webui/static/script.js | 87 ++++++----- webui/static/style.css | 331 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 372 insertions(+), 46 deletions(-) diff --git a/webui/static/script.js b/webui/static/script.js index e4131d7f..cf860332 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -57806,39 +57806,52 @@ function openPoolFixModal(trackId, trackName, artistName) { fixOverlay = document.createElement('div'); fixOverlay.className = 'pool-fix-overlay'; fixOverlay.id = 'pool-fix-overlay'; - fixOverlay.onclick = (e) => { if (e.target === fixOverlay) closePoolFixModal(); }; + + // Only close on click to the overlay itself — use a dedicated close zone + // to prevent accidental dismissal when clicking near inputs + fixOverlay.addEventListener('mousedown', (e) => { + if (e.target === fixOverlay) { + e.preventDefault(); // Prevent stealing focus from inputs + closePoolFixModal(); + } + }); fixOverlay.innerHTML = ` -
-
+
+

Fix Track Match

- +
-
-
-

Source Track

-
-
${_esc(trackName)}
-
${_esc(artistName)}
+
+
+
Original Track
+
+ ${_esc(trackName)} + + ${_esc(artistName)}
-
-

Search for Match

-
- - - + -
-

Results

-
-
Searching...
+
+
+
Searching...
- `; @@ -57853,7 +57866,11 @@ function openPoolFixModal(trackId, trackName, artistName) { trackInput.addEventListener('keypress', enterHandler); artistInput.addEventListener('keypress', enterHandler); - // Auto-search (delay allows modal layout to settle and prevents accidental clicks) + // Focus the track input + trackInput.focus(); + trackInput.select(); + + // Auto-search after a delay setTimeout(() => searchPoolFix(), 500); } @@ -57871,42 +57888,42 @@ async function searchPoolFix() { const trackVal = trackInput.value.trim(); const artistVal = artistInput.value.trim(); if (!trackVal && !artistVal) { - resultsContainer.innerHTML = '
Enter a search term
'; + resultsContainer.innerHTML = '
Enter a search term
'; return; } - resultsContainer.innerHTML = '
Searching...
'; + resultsContainer.innerHTML = '
Searching...
'; try { const params = new URLSearchParams(); if (trackVal) params.set('track', trackVal); if (artistVal) params.set('artist', artistVal); - params.set('limit', '50'); + params.set('limit', '20'); const res = await fetch(`/api/spotify/search_tracks?${params.toString()}`); const data = await res.json(); const tracks = data.tracks || []; if (tracks.length === 0) { - resultsContainer.innerHTML = '
No results found
'; + resultsContainer.innerHTML = '
No results found
'; return; } - resultsContainer.innerHTML = tracks.map((track, i) => { + resultsContainer.innerHTML = tracks.map((track) => { const artists = (track.artists || []).join(', '); const duration = track.duration_ms ? formatDuration(track.duration_ms) : ''; + const albumText = track.album ? ` · ${_esc(track.album)}` : ''; return ` -
-
-
${_esc(track.name || 'Unknown')}
-
${_esc(artists)}
-
${_esc(track.album || '')}
- ${duration ? `
${duration}
` : ''} +
+
+
${_esc(track.name || 'Unknown')}
+
${_esc(artists)}${albumText}
+ ${duration ? `
${duration}
` : ''}
`; }).join(''); } catch (err) { - resultsContainer.innerHTML = `
Search failed: ${_esc(err.message)}
`; + resultsContainer.innerHTML = `
Search failed: ${_esc(err.message)}
`; } } diff --git a/webui/static/style.css b/webui/static/style.css index c2b6501c..30b58b10 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -8673,12 +8673,12 @@ body { border-color: rgba(239, 68, 68, 0.3); } -/* ===== Fix Sub-Modal (kept) ===== */ +/* ===== Pool Fix Match Modal ===== */ .pool-fix-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(8px); z-index: 10001; display: flex; align-items: center; @@ -8687,16 +8687,325 @@ body { } .pool-fix-modal { - background: linear-gradient(135deg, rgba(26, 26, 26, 0.98) 0%, rgba(18, 18, 18, 0.99) 100%); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 16px; - width: 90%; - max-width: 700px; - max-height: 80vh; + background: rgba(18, 18, 18, 0.98); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 14px; + width: 560px; + max-width: 92vw; + /* FIXED HEIGHT — prevents layout shift when results load */ + height: 600px; + max-height: 85vh; display: flex; flex-direction: column; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); - animation: slideUp 0.25s ease; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6); + animation: slideUp 0.2s ease; + overflow: hidden; +} + +.pool-fix-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 18px 22px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + flex-shrink: 0; +} + +.pool-fix-header h2 { + font-size: 17px; + font-weight: 700; + color: #fff; + margin: 0; + letter-spacing: -0.2px; +} + +.pool-fix-close { + width: 32px; + height: 32px; + border-radius: 8px; + border: none; + background: rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.5); + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.pool-fix-close:hover { + background: rgba(255, 255, 255, 0.12); + color: #fff; +} + +.pool-fix-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + padding: 18px 22px 0; + overflow: hidden; +} + +/* Source track display — compact */ +.pool-fix-source { + padding: 12px 14px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 10px; + margin-bottom: 16px; + flex-shrink: 0; +} + +.pool-fix-source-label { + font-size: 10px; + font-weight: 700; + color: rgba(255, 255, 255, 0.35); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 6px; +} + +.pool-fix-source-row { + display: flex; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; +} + +.pool-fix-source-title { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); +} + +.pool-fix-source-sep { + color: rgba(255, 255, 255, 0.2); + font-size: 13px; +} + +.pool-fix-source-artist { + font-size: 13px; + color: rgba(255, 255, 255, 0.5); +} + +/* Search inputs */ +.pool-fix-search { + margin-bottom: 14px; + flex-shrink: 0; +} + +.pool-fix-input-row { + display: flex; + gap: 10px; + align-items: flex-end; +} + +.pool-fix-input-wrap { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.pool-fix-input-wrap label { + font-size: 11px; + font-weight: 600; + color: rgba(255, 255, 255, 0.4); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.pool-fix-input-wrap input { + width: 100%; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + padding: 9px 12px; + color: #fff; + font-size: 14px; + font-family: inherit; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + box-sizing: border-box; +} + +.pool-fix-input-wrap input:focus { + outline: none; + border-color: rgba(var(--accent-rgb), 0.5); + box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.1); +} + +.pool-fix-input-wrap input::placeholder { + color: rgba(255, 255, 255, 0.25); +} + +.pool-fix-search-btn { + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 9px 18px; + color: rgba(255, 255, 255, 0.8); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; + flex-shrink: 0; + height: 38px; +} + +.pool-fix-search-btn:hover { + background: rgba(255, 255, 255, 0.12); + color: #fff; +} + +/* Results area — FIXED, scrolls internally, no layout shift */ +.pool-fix-results-area { + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.pool-fix-results-list { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 4px; + padding-bottom: 8px; +} + +.pool-fix-results-list::-webkit-scrollbar { + width: 5px; +} +.pool-fix-results-list::-webkit-scrollbar-track { + background: transparent; +} +.pool-fix-results-list::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; +} +.pool-fix-results-list::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); +} + +/* Result rows */ +.pool-fix-result { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: 8px; + cursor: pointer; + transition: background 0.12s ease; + flex-shrink: 0; +} + +.pool-fix-result:hover { + background: rgba(255, 255, 255, 0.06); +} + +.pool-fix-result-main { + flex: 1; + min-width: 0; +} + +.pool-fix-result-title { + font-size: 14px; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pool-fix-result-meta { + font-size: 12px; + color: rgba(255, 255, 255, 0.4); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: 2px; +} + +.pool-fix-result-dur { + font-size: 12px; + color: rgba(255, 255, 255, 0.3); + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} + +/* Empty / loading states */ +.pool-fix-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + padding: 40px 20px; + color: rgba(255, 255, 255, 0.3); + font-size: 13px; + flex: 1; +} + +.pool-fix-spinner { + width: 24px; + height: 24px; + border: 2px solid rgba(255, 255, 255, 0.08); + border-top-color: rgba(255, 255, 255, 0.4); + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +/* Footer */ +.pool-fix-footer { + padding: 14px 22px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + display: flex; + justify-content: flex-end; + flex-shrink: 0; +} + +.pool-fix-cancel { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + padding: 8px 18px; + color: rgba(255, 255, 255, 0.6); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.pool-fix-cancel:hover { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.85); +} + +/* Mobile */ +@media (max-width: 600px) { + .pool-fix-modal { + width: 100%; + max-width: 100%; + height: 100vh; + max-height: 100vh; + border-radius: 0; + } + + .pool-fix-input-row { + flex-direction: column; + gap: 8px; + } + + .pool-fix-search-btn { + width: 100%; + height: 44px; + } } /* Mirrored playlist track modal */