feat: improve modal sidebar hierarchy, collapse add-to-series, in-context feedback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 08:51:03 -04:00
parent 9d7d76953e
commit 7cae463fe8
3 changed files with 177 additions and 68 deletions
+64 -37
View File
@@ -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;
+67
View File
@@ -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
------------------------------------------------------------------------------*/
+46 -31
View File
@@ -11,40 +11,55 @@
<img id="modalImage" src="" alt="Full View">
</div>
<div id="modalTagEditor" class="tag-editor" data-image-id="">
<!-- Shown when image IS in a series -->
<div id="modalSeriesInfo" class="modal-series-info" style="display: none;">
<div class="series-info-row">
<span class="series-info-label">📚 Page</span>
<input type="number" id="modalPageNumber" min="1" class="series-page-input">
<span class="series-info-label">of</span>
<a id="modalSeriesName" href="#" class="series-info-link"></a>
<button id="updatePageBtn" class="btn-small" title="Update page number">Save</button>
<span id="pageUpdateStatus" class="page-update-status"></span>
<div id="modalTagEditor" class="tag-editor modal-sidebar" data-image-id="">
<!-- SECTION: Series -->
<div class="modal-sidebar-section modal-series-section">
<!-- Shown when image IS in a series -->
<div id="modalSeriesInfo" class="modal-series-info" style="display: none;">
<div class="series-info-row">
<span class="series-info-label">📚</span>
<span class="series-info-label">Page</span>
<input type="number" id="modalPageNumber" min="1" class="series-page-input">
<span class="series-info-label">of</span>
<a id="modalSeriesName" href="#" class="series-info-link"></a>
<button id="updatePageBtn" class="btn-small" title="Update page number">Save</button>
<span id="pageUpdateStatus" class="tag-feedback"></span>
</div>
</div>
<!-- Shown when image is NOT in a series -->
<div id="modalAddToSeries" class="modal-add-to-series" style="display: none;">
<button id="addToSeriesToggle" class="modal-series-toggle">
<span>📚 Add to Series</span>
<span class="toggle-arrow"></span>
</button>
<div id="addToSeriesBody" class="add-series-body" style="display: none;">
<div class="add-series-controls">
<select id="seriesSelect" class="series-select">
<option value="">Select series...</option>
</select>
<input type="number" id="addSeriesPageNumber" min="1" value="1" class="series-page-input" placeholder="#">
<button id="addToSeriesBtn" class="btn-small" disabled>Add</button>
</div>
<span id="addToSeriesStatus" class="tag-feedback"></span>
</div>
</div>
</div>
<!-- Shown when image is NOT in a series -->
<div id="modalAddToSeries" class="modal-add-to-series" style="display: none;">
<div class="add-series-header">
<span class="series-info-label">📚 Add to Series</span>
</div>
<div class="add-series-controls">
<select id="seriesSelect" class="series-select">
<option value="">Select series...</option>
</select>
<input type="number" id="addSeriesPageNumber" min="1" value="1" class="series-page-input" placeholder="#">
<button id="addToSeriesBtn" class="btn-small" disabled>Add</button>
</div>
<span id="addToSeriesStatus" class="page-update-status"></span>
<!-- SECTION: Tags -->
<div class="modal-sidebar-section modal-tags-section">
<div id="modalTagList" class="tags"></div>
<form id="modalTagForm" class="tag-form" autocomplete="off">
<div class="tag-form-wrapper">
<input type="text" id="tagInput" name="name" placeholder="Add tag..." autocomplete="off">
<div id="tagAutocomplete" class="tag-autocomplete"></div>
</div>
<button type="submit">Add</button>
</form>
<span id="tagActionFeedback" class="tag-feedback tag-feedback-inline"></span>
</div>
<div id="modalTagList" class="tags"></div>
<form id="modalTagForm" class="tag-form" autocomplete="off">
<div class="tag-form-wrapper">
<input type="text" id="tagInput" name="name" placeholder="Add tag..." autocomplete="off">
<div id="tagAutocomplete" class="tag-autocomplete"></div>
</div>
<button type="submit">Add</button>
</form>
</div>
</div>
</div>