From 7cae463fe82b339877cb8f01f16eaf0c6793cb0f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 20 Mar 2026 08:51:03 -0400 Subject: [PATCH] feat: improve modal sidebar hierarchy, collapse add-to-series, in-context feedback Co-Authored-By: Claude Sonnet 4.6 --- app/static/js/view-modal.js | 101 +++++++++++++++++++----------- app/static/style.css | 67 ++++++++++++++++++++ app/templates/_gallery_modal.html | 77 ++++++++++++++--------- 3 files changed, 177 insertions(+), 68 deletions(-) diff --git a/app/static/js/view-modal.js b/app/static/js/view-modal.js index 5dda426..7563d28 100644 --- a/app/static/js/view-modal.js +++ b/app/static/js/view-modal.js @@ -28,8 +28,22 @@ document.addEventListener('DOMContentLoaded', () => { const addSeriesPageNumber = document.getElementById('addSeriesPageNumber'); const addToSeriesBtn = document.getElementById('addToSeriesBtn'); const addToSeriesStatus = document.getElementById('addToSeriesStatus'); + const addToSeriesToggle = document.getElementById('addToSeriesToggle'); + const addToSeriesBody = document.getElementById('addToSeriesBody'); let seriesTagsLoaded = false; + // In-context feedback: show a brief message next to an element, auto-fades + function showFeedback(el, message, isError = false) { + if (!el) return; + el.textContent = message; + el.style.color = isError ? 'var(--flash-danger)' : 'var(--flash-success)'; + el.classList.add('visible'); + clearTimeout(el._feedbackTimeout); + el._feedbackTimeout = setTimeout(() => { + el.classList.remove('visible'); + }, 1500); + } + // Autocomplete state let autocompleteItems = []; let autocompleteSelectedIndex = -1; @@ -133,12 +147,15 @@ document.addEventListener('DOMContentLoaded', () => { function hideSeriesInfo() { if (seriesInfoEl) seriesInfoEl.style.display = 'none'; currentSeriesPageId = null; - if (pageUpdateStatus) pageUpdateStatus.textContent = ''; + if (pageUpdateStatus) { pageUpdateStatus.textContent = ''; pageUpdateStatus.classList.remove('visible'); } } function hideAddToSeries() { if (addToSeriesEl) addToSeriesEl.style.display = 'none'; - if (addToSeriesStatus) addToSeriesStatus.textContent = ''; + if (addToSeriesStatus) { addToSeriesStatus.textContent = ''; addToSeriesStatus.classList.remove('visible'); } + // Reset toggle state + if (addToSeriesToggle) addToSeriesToggle.classList.remove('open'); + if (addToSeriesBody) addToSeriesBody.style.display = 'none'; } async function loadSeriesTags() { @@ -169,25 +186,10 @@ document.addEventListener('DOMContentLoaded', () => { function showAddToSeries(seriesPage = null) { if (!addToSeriesEl) return; - loadSeriesTags().then(() => { - // Pre-fill if image is already in a series - if (seriesPage && seriesSelect) { - seriesSelect.value = seriesPage.series_tag_id; - if (addSeriesPageNumber) addSeriesPageNumber.value = seriesPage.page_number; - if (addToSeriesBtn) { - addToSeriesBtn.disabled = false; - addToSeriesBtn.textContent = 'Update'; - } - } else { - if (seriesSelect) seriesSelect.value = ''; - if (addSeriesPageNumber) addSeriesPageNumber.value = '1'; - if (addToSeriesBtn) { - addToSeriesBtn.disabled = true; - addToSeriesBtn.textContent = 'Add'; - } - } - }); - if (addToSeriesStatus) addToSeriesStatus.textContent = ''; + // Store pending pre-fill data for when the toggle is opened + addToSeriesEl._pendingSeriesPage = seriesPage || null; + if (addToSeriesStatus) { addToSeriesStatus.textContent = ''; addToSeriesStatus.classList.remove('visible'); } + // Show the outer container; inner body stays hidden until toggle click addToSeriesEl.style.display = 'block'; } @@ -200,7 +202,7 @@ document.addEventListener('DOMContentLoaded', () => { const isUpdate = addToSeriesBtn?.textContent === 'Update'; if (addToSeriesBtn) addToSeriesBtn.disabled = true; - if (addToSeriesStatus) addToSeriesStatus.textContent = isUpdate ? 'Updating...' : 'Adding...'; + showFeedback(addToSeriesStatus, isUpdate ? 'Updating…' : 'Adding…'); try { const r = await fetch(`/api/image/${imageId}/add-to-series`, { @@ -211,18 +213,15 @@ document.addEventListener('DOMContentLoaded', () => { const j = await r.json(); if (j.ok) { - if (addToSeriesStatus) { - addToSeriesStatus.textContent = '✓ Saved'; - setTimeout(() => { addToSeriesStatus.textContent = ''; }, 2000); - } + showFeedback(addToSeriesStatus, 'Saved'); // Fully reload series info to show updated management interface await loadSeriesInfo(imageId); } else { - if (addToSeriesStatus) addToSeriesStatus.textContent = j.error || 'Error'; + showFeedback(addToSeriesStatus, j.error || 'Error', true); if (addToSeriesBtn) addToSeriesBtn.disabled = false; } } catch (e) { - if (addToSeriesStatus) addToSeriesStatus.textContent = 'Error'; + showFeedback(addToSeriesStatus, 'Error', true); if (addToSeriesBtn) addToSeriesBtn.disabled = false; } } @@ -245,7 +244,7 @@ document.addEventListener('DOMContentLoaded', () => { pageNumberInput.value = seriesPage.page_number; } - if (pageUpdateStatus) pageUpdateStatus.textContent = ''; + if (pageUpdateStatus) { pageUpdateStatus.textContent = ''; pageUpdateStatus.classList.remove('visible'); } seriesInfoEl.style.display = 'block'; } @@ -279,12 +278,12 @@ document.addEventListener('DOMContentLoaded', () => { if (!currentSeriesPageId || !pageNumberInput) return; const newPageNumber = parseInt(pageNumberInput.value); if (isNaN(newPageNumber) || newPageNumber < 1) { - if (pageUpdateStatus) pageUpdateStatus.textContent = 'Invalid'; + showFeedback(pageUpdateStatus, 'Invalid', true); return; } if (updatePageBtn) updatePageBtn.disabled = true; - if (pageUpdateStatus) pageUpdateStatus.textContent = 'Saving...'; + showFeedback(pageUpdateStatus, 'Saving…'); try { const fd = new FormData(); @@ -295,15 +294,12 @@ document.addEventListener('DOMContentLoaded', () => { }); const j = await r.json(); if (j.ok) { - if (pageUpdateStatus) { - pageUpdateStatus.textContent = '✓'; - setTimeout(() => { pageUpdateStatus.textContent = ''; }, 2000); - } + showFeedback(pageUpdateStatus, 'Saved'); } else { - if (pageUpdateStatus) pageUpdateStatus.textContent = j.error || 'Error'; + showFeedback(pageUpdateStatus, j.error || 'Error', true); } } catch (e) { - if (pageUpdateStatus) pageUpdateStatus.textContent = 'Error'; + showFeedback(pageUpdateStatus, 'Error', true); } finally { if (updatePageBtn) updatePageBtn.disabled = false; } @@ -354,6 +350,35 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Toggle "Add to Series" collapse + if (addToSeriesToggle) { + addToSeriesToggle.addEventListener('click', () => { + const isOpen = addToSeriesToggle.classList.toggle('open'); + addToSeriesBody.style.display = isOpen ? 'block' : 'none'; + // Load series options on first open, then apply any pending pre-fill + if (isOpen) { + const seriesPage = addToSeriesEl._pendingSeriesPage; + loadSeriesTags().then(() => { + if (seriesPage && seriesSelect) { + seriesSelect.value = seriesPage.series_tag_id; + if (addSeriesPageNumber) addSeriesPageNumber.value = seriesPage.page_number; + if (addToSeriesBtn) { + addToSeriesBtn.disabled = false; + addToSeriesBtn.textContent = 'Update'; + } + } else { + if (seriesSelect) seriesSelect.value = ''; + if (addSeriesPageNumber) addSeriesPageNumber.value = '1'; + if (addToSeriesBtn) { + addToSeriesBtn.disabled = true; + addToSeriesBtn.textContent = 'Add'; + } + } + }); + } + }); + } + async function addTag(imageId, name) { const fd = new FormData(); fd.append('name', name); @@ -361,6 +386,7 @@ document.addEventListener('DOMContentLoaded', () => { const j = await r.json(); if (j.ok) { await loadTags(imageId); + showFeedback(document.getElementById('tagActionFeedback'), 'Tag added'); return true; } return false; @@ -372,6 +398,7 @@ document.addEventListener('DOMContentLoaded', () => { const j = await r.json(); if (j.ok) { await loadTags(imageId); + showFeedback(document.getElementById('tagActionFeedback'), 'Tag removed'); return true; } return false; diff --git a/app/static/style.css b/app/static/style.css index 2114c33..989a8ce 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -598,6 +598,73 @@ header { .modal-close-hint { display: none; } } +/* Modal sidebar sections */ +.modal-sidebar-section { + padding: 0.75rem 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.07); +} +.modal-sidebar-section:last-child { + border-bottom: none; +} + +/* Series toggle button (for "Add to Series" collapsed state) */ +.modal-series-toggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + background: none; + border: none; + color: var(--text-muted); + font-size: 0.85rem; + cursor: pointer; + padding: 0.25rem 0; + transition: color 0.15s ease; +} +.modal-series-toggle:hover { + color: var(--text-dim); +} +.modal-series-toggle .toggle-arrow { + transition: transform 0.15s ease; + font-size: 0.9rem; +} +.modal-series-toggle.open .toggle-arrow { + transform: rotate(90deg); +} + +.add-series-body { + margin-top: 0.5rem; +} + +/* In-context feedback label */ +.tag-feedback { + font-size: 0.75rem; + color: var(--flash-success); + opacity: 0; + transition: opacity 0.2s ease; + display: inline-block; + margin-left: 0.4rem; +} +.tag-feedback.visible { + opacity: 1; +} +.tag-feedback-inline { + display: block; + margin-left: 0; + margin-top: 0.3rem; + min-height: 1.1em; +} + +/* Tag chip remove button — hidden at rest, visible on hover */ +.tag-chip .x { + opacity: 0; + transition: opacity 0.15s ease; + margin-left: 4px; +} +.tag-chip:hover .x { + opacity: 1; +} + /*------------------------------------------------------------------------------ Tag overlays & editor ------------------------------------------------------------------------------*/ diff --git a/app/templates/_gallery_modal.html b/app/templates/_gallery_modal.html index b724c00..69b373e 100644 --- a/app/templates/_gallery_modal.html +++ b/app/templates/_gallery_modal.html @@ -11,40 +11,55 @@ Full View -
- -