Compare commits

..

No commits in common. "95c6feed167477e88cbd732bc9ab4e7f2487eec4" and "52a6f139bbf0689a46cbbb4f3ba853e9347831b5" have entirely different histories.

8 changed files with 103 additions and 125 deletions

View file

@ -436,33 +436,25 @@
sourceEl.textContent = 'server'; sourceEl.textContent = 'server';
} }
// Download-as-{docx,html,pdf} affordances. Server-mode + .md // Download-as-{docx,html,pdf} buttons. Server-mode + .md only:
// only: the server endpoint runs pandoc/chromium in a // the server endpoint runs pandoc/chromium in a container and
// container and returns the converted bytes. // returns the converted bytes. Click handlers wire up below
// // (after save() is defined) because they auto-save first when
// These are real <a> elements with href + download attributes, // the buffer is dirty.
// styled like buttons. That means right-click → "Copy link
// address" / "Open in new tab" / "Save link as" all work
// natively — users can share the conversion URL or download
// through their preferred path. Click is intercepted only
// when the buffer is dirty (auto-save first, then re-fire
// the click so the browser fetches the saved bytes).
var serverModeMd = window.app && window.app.state && var serverModeMd = window.app && window.app.state &&
window.app.state.source === 'server' && window.app.state.source === 'server' &&
node.url && /\.md$/i.test(node.name); node.url && /\.md$/i.test(node.name);
var convertBtns = []; var convertBtns = [];
if (serverModeMd) { if (serverModeMd && window.zddc && window.zddc.source &&
typeof window.zddc.source.downloadConverted === 'function') {
['docx', 'html', 'pdf'].forEach(function (fmt) { ['docx', 'html', 'pdf'].forEach(function (fmt) {
var a = document.createElement('a'); var btn = document.createElement('button');
a.className = 'btn btn-sm btn-secondary md-shell__download'; btn.className = 'btn btn-sm btn-secondary md-shell__download';
a.href = node.url + '?convert=' + encodeURIComponent(fmt); btn.type = 'button';
a.download = node.name.replace(/\.md$/i, '') + '.' + fmt; btn.textContent = fmt.toUpperCase();
a.textContent = fmt.toUpperCase(); btn.title = 'Download as ' + fmt.toUpperCase();
a.title = 'Download as ' + fmt.toUpperCase() btn.dataset.fmt = fmt;
+ ' (right-click to copy link or open in new tab)'; convertBtns.push(btn);
a.dataset.fmt = fmt;
a.rel = 'noopener';
convertBtns.push(a);
}); });
} }
@ -661,26 +653,14 @@
} }
}); });
// Download-as-* click handlers. The anchors are real <a href> // Download-as-* click handlers. Auto-save when the buffer is
// links so right-click / middle-click / Copy Link Address all // dirty so the converted file reflects what's on screen. If
// work natively. The JS handler only steps in when the buffer // the save fails the existing toast/status surfaces it; we
// is dirty — auto-save first, then re-fire the click so the // bail without firing the conversion.
// browser fetches the just-saved bytes. After the click is convertBtns.forEach(function (btn) {
// re-fired, currentInstance.dirty is false so the handler btn.addEventListener('click', async function () {
// is a no-op on the second pass and the native navigation var fmt = btn.dataset.fmt;
// proceeds. if (currentInstance.dirty) {
convertBtns.forEach(function (a) {
a.addEventListener('click', async function (e) {
var fmt = a.dataset.fmt;
if (!currentInstance.dirty) {
// Clean — let the browser handle the click. The
// server's response (DOCX/HTML/PDF bytes, 422,
// 503, etc.) lands in whatever target the user
// picked (current tab, new tab, save-as).
return;
}
// Dirty: intercept, save, retry.
e.preventDefault();
if (!writable) { if (!writable) {
if (window.zddc && window.zddc.toast) { if (window.zddc && window.zddc.toast) {
window.zddc.toast( window.zddc.toast(
@ -689,14 +669,23 @@
} }
return; return;
} }
statusEl.textContent = 'Saving before download…'; btn.disabled = true;
try { await save(); } catch (_) { /* save() surfaces its own error */ } try { await save(); } finally { btn.disabled = false; }
if (currentInstance.dirty) return; // save failed; toast already shown if (currentInstance.dirty) return; // save failed
statusEl.textContent = 'Downloading ' + fmt.toUpperCase() + '…'; }
// Re-trigger the click. dirty=false now so the handler btn.disabled = true;
// exits early on the second pass and the browser try {
// processes the native navigation. statusEl.textContent = 'Converting to ' + fmt.toUpperCase() + '…';
a.click(); await window.zddc.source.downloadConverted(node.url, node.name, fmt);
statusEl.textContent = 'Downloaded ' + fmt.toUpperCase();
} catch (e) {
statusEl.textContent = (e && e.message) || String(e);
if (window.zddc && window.zddc.toast) {
window.zddc.toast((e && e.message) || String(e), 'error');
}
} finally {
btn.disabled = false;
}
}); });
}); });
} }

View file

@ -2470,7 +2470,7 @@ td[data-field="trackingNumber"] {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span> <span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:55:11 · 9245017</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:17:48 · 7aec631</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>

View file

@ -1657,7 +1657,7 @@ html, body {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span> <span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:55:11 · 9245017</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:17:49 · 7aec631</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>
@ -6743,33 +6743,25 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
sourceEl.textContent = 'server'; sourceEl.textContent = 'server';
} }
// Download-as-{docx,html,pdf} affordances. Server-mode + .md // Download-as-{docx,html,pdf} buttons. Server-mode + .md only:
// only: the server endpoint runs pandoc/chromium in a // the server endpoint runs pandoc/chromium in a container and
// container and returns the converted bytes. // returns the converted bytes. Click handlers wire up below
// // (after save() is defined) because they auto-save first when
// These are real <a> elements with href + download attributes, // the buffer is dirty.
// styled like buttons. That means right-click → "Copy link
// address" / "Open in new tab" / "Save link as" all work
// natively — users can share the conversion URL or download
// through their preferred path. Click is intercepted only
// when the buffer is dirty (auto-save first, then re-fire
// the click so the browser fetches the saved bytes).
var serverModeMd = window.app && window.app.state && var serverModeMd = window.app && window.app.state &&
window.app.state.source === 'server' && window.app.state.source === 'server' &&
node.url && /\.md$/i.test(node.name); node.url && /\.md$/i.test(node.name);
var convertBtns = []; var convertBtns = [];
if (serverModeMd) { if (serverModeMd && window.zddc && window.zddc.source &&
typeof window.zddc.source.downloadConverted === 'function') {
['docx', 'html', 'pdf'].forEach(function (fmt) { ['docx', 'html', 'pdf'].forEach(function (fmt) {
var a = document.createElement('a'); var btn = document.createElement('button');
a.className = 'btn btn-sm btn-secondary md-shell__download'; btn.className = 'btn btn-sm btn-secondary md-shell__download';
a.href = node.url + '?convert=' + encodeURIComponent(fmt); btn.type = 'button';
a.download = node.name.replace(/\.md$/i, '') + '.' + fmt; btn.textContent = fmt.toUpperCase();
a.textContent = fmt.toUpperCase(); btn.title = 'Download as ' + fmt.toUpperCase();
a.title = 'Download as ' + fmt.toUpperCase() btn.dataset.fmt = fmt;
+ ' (right-click to copy link or open in new tab)'; convertBtns.push(btn);
a.dataset.fmt = fmt;
a.rel = 'noopener';
convertBtns.push(a);
}); });
} }
@ -6968,26 +6960,14 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
} }
}); });
// Download-as-* click handlers. The anchors are real <a href> // Download-as-* click handlers. Auto-save when the buffer is
// links so right-click / middle-click / Copy Link Address all // dirty so the converted file reflects what's on screen. If
// work natively. The JS handler only steps in when the buffer // the save fails the existing toast/status surfaces it; we
// is dirty — auto-save first, then re-fire the click so the // bail without firing the conversion.
// browser fetches the just-saved bytes. After the click is convertBtns.forEach(function (btn) {
// re-fired, currentInstance.dirty is false so the handler btn.addEventListener('click', async function () {
// is a no-op on the second pass and the native navigation var fmt = btn.dataset.fmt;
// proceeds. if (currentInstance.dirty) {
convertBtns.forEach(function (a) {
a.addEventListener('click', async function (e) {
var fmt = a.dataset.fmt;
if (!currentInstance.dirty) {
// Clean — let the browser handle the click. The
// server's response (DOCX/HTML/PDF bytes, 422,
// 503, etc.) lands in whatever target the user
// picked (current tab, new tab, save-as).
return;
}
// Dirty: intercept, save, retry.
e.preventDefault();
if (!writable) { if (!writable) {
if (window.zddc && window.zddc.toast) { if (window.zddc && window.zddc.toast) {
window.zddc.toast( window.zddc.toast(
@ -6996,14 +6976,23 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
} }
return; return;
} }
statusEl.textContent = 'Saving before download…'; btn.disabled = true;
try { await save(); } catch (_) { /* save() surfaces its own error */ } try { await save(); } finally { btn.disabled = false; }
if (currentInstance.dirty) return; // save failed; toast already shown if (currentInstance.dirty) return; // save failed
statusEl.textContent = 'Downloading ' + fmt.toUpperCase() + '…'; }
// Re-trigger the click. dirty=false now so the handler btn.disabled = true;
// exits early on the second pass and the browser try {
// processes the native navigation. statusEl.textContent = 'Converting to ' + fmt.toUpperCase() + '…';
a.click(); await window.zddc.source.downloadConverted(node.url, node.name, fmt);
statusEl.textContent = 'Downloaded ' + fmt.toUpperCase();
} catch (e) {
statusEl.textContent = (e && e.message) || String(e);
if (window.zddc && window.zddc.toast) {
window.zddc.toast((e && e.message) || String(e), 'error');
}
} finally {
btn.disabled = false;
}
}); });
}); });
} }

View file

@ -1681,7 +1681,7 @@ body.help-open .app-header {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span> <span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:55:11 · 9245017</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:17:48 · 7aec631</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button>

View file

@ -1424,7 +1424,7 @@ body {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC</span> <span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:55:11 · 9245017</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:17:49 · 7aec631</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">

View file

@ -2523,7 +2523,7 @@ dialog.modal--narrow {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span> <span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:55:10 · 9245017</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:17:48 · 7aec631</span></span>
</div> </div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span> <span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action; <!-- Publish split-button (Transmittal-specific primary action;

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line. # Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.17-beta · 2026-05-13 17:55:11 · 9245017 archive=v0.0.17-beta · 2026-05-13 17:17:48 · 7aec631
transmittal=v0.0.17-beta · 2026-05-13 17:55:10 · 9245017 transmittal=v0.0.17-beta · 2026-05-13 17:17:48 · 7aec631
classifier=v0.0.17-beta · 2026-05-13 17:55:11 · 9245017 classifier=v0.0.17-beta · 2026-05-13 17:17:48 · 7aec631
landing=v0.0.17-beta · 2026-05-13 17:55:11 · 9245017 landing=v0.0.17-beta · 2026-05-13 17:17:49 · 7aec631
form=v0.0.17-beta · 2026-05-13 17:55:11 · 9245017 form=v0.0.17-beta · 2026-05-13 17:17:49 · 7aec631
tables=v0.0.17-beta · 2026-05-13 17:55:11 · 9245017 tables=v0.0.17-beta · 2026-05-13 17:17:49 · 7aec631
browse=v0.0.17-beta · 2026-05-13 17:55:11 · 9245017 browse=v0.0.17-beta · 2026-05-13 17:17:49 · 7aec631

View file

@ -1300,7 +1300,7 @@ body.help-open .app-header {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span> <span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:55:11 · 9245017</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-13 17:17:49 · 7aec631</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">