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:
+64
-37
@@ -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;
|
||||
|
||||
@@ -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
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user