Show a loading spinner on the Export/Preview buttons while exporting

The export dialog's Export and Preview buttons now disable and show a
spinner with status text while the corresponding async operation is
in flight, so the user knows the (sometimes slow) export is running
and the app isn't stuck.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-06-15 17:07:35 -05:00
co-authored by Claude Sonnet 4.6
parent 3de8ec978b
commit 5b54305b9f
3 changed files with 74 additions and 19 deletions
+34 -19
View File
@@ -413,28 +413,43 @@ function showExportDialog({
outDir: outDirInput.value.trim() || null,
});
const cancelBtn = el('button', { onClick: () => { close(); resolve(false); } }, 'Cancel');
const previewBtn = el('button', {
onClick: async () => {
if (typeof onPreview !== 'function') return;
setButtonLoading(previewBtn, true, 'Preview…');
try {
await onPreview(payload()); // keep dialog open so settings can be tweaked
} finally {
setButtonLoading(previewBtn, false);
}
},
}, 'Preview');
const exportBtn = el('button.primary', {
onClick: async () => {
if (typeof onExport !== 'function') return;
cancelBtn.disabled = true;
previewBtn.disabled = true;
setButtonLoading(exportBtn, true, 'Exporting…');
try {
const ok = await onExport(payload());
if (ok !== false) {
close();
resolve(true);
return;
}
} finally {
cancelBtn.disabled = false;
previewBtn.disabled = false;
setButtonLoading(exportBtn, false);
}
},
}, 'Export');
const { close } = openModal({
title: 'Export',
body,
footer: [
el('button', { onClick: () => { close(); resolve(false); } }, 'Cancel'),
el('button', {
onClick: async () => {
if (typeof onPreview !== 'function') return;
await onPreview(payload()); // keep dialog open so settings can be tweaked
},
}, 'Preview'),
el('button.primary', {
onClick: async () => {
if (typeof onExport !== 'function') return;
const ok = await onExport(payload());
if (ok !== false) {
close();
resolve(true);
}
},
}, 'Export'),
],
footer: [cancelBtn, previewBtn, exportBtn],
wide: true,
onClose: () => resolve(false),
});
+19
View File
@@ -90,6 +90,25 @@ button.tool.active {
border-color: var(--accent);
color: var(--accent-fg);
}
button:disabled { opacity: 0.6; cursor: default; }
button.loading {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
}
.spinner {
display: inline-block;
width: 13px;
height: 13px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
opacity: 0.85;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
input, select, textarea {
font: inherit;
+21
View File
@@ -34,6 +34,27 @@ function clearNode(node) {
while (node.firstChild) node.removeChild(node.firstChild);
}
/**
* Toggle a button into/out of a busy state: disables it and swaps its label
* for a spinner + `label` (e.g. "Exporting…"), restoring the original label
* afterwards. Used for actions that block on a slow main-process call.
*/
function setButtonLoading(btn, loading, label) {
if (loading) {
if (btn.dataset.origLabel === undefined) btn.dataset.origLabel = btn.textContent;
btn.disabled = true;
btn.classList.add('loading');
clearNode(btn);
btn.append(el('span.spinner'), label || btn.dataset.origLabel);
} else {
btn.disabled = false;
btn.classList.remove('loading');
clearNode(btn);
btn.append(btn.dataset.origLabel ?? '');
delete btn.dataset.origLabel;
}
}
function debounce(fn, ms) {
let t = null;
const wrapped = (...args) => {