diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html
index f7dbf8e..a541a97 100644
--- a/zddc/internal/apps/embedded/archive.html
+++ b/zddc/internal/apps/embedded/archive.html
@@ -2470,7 +2470,7 @@ td[data-field="trackingNumber"] {
diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html
index a6c2ab1..e7ce223 100644
--- a/zddc/internal/apps/embedded/browse.html
+++ b/zddc/internal/apps/embedded/browse.html
@@ -1657,7 +1657,7 @@ html, body {
@@ -6743,25 +6743,33 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
sourceEl.textContent = 'server';
}
- // Download-as-{docx,html,pdf} buttons. Server-mode + .md only:
- // the server endpoint runs pandoc/chromium in a container and
- // returns the converted bytes. Click handlers wire up below
- // (after save() is defined) because they auto-save first when
- // the buffer is dirty.
+ // Download-as-{docx,html,pdf} affordances. Server-mode + .md
+ // only: the server endpoint runs pandoc/chromium in a
+ // container and returns the converted bytes.
+ //
+ // These are real elements with href + download attributes,
+ // 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 &&
window.app.state.source === 'server' &&
node.url && /\.md$/i.test(node.name);
var convertBtns = [];
- if (serverModeMd && window.zddc && window.zddc.source &&
- typeof window.zddc.source.downloadConverted === 'function') {
+ if (serverModeMd) {
['docx', 'html', 'pdf'].forEach(function (fmt) {
- var btn = document.createElement('button');
- btn.className = 'btn btn-sm btn-secondary md-shell__download';
- btn.type = 'button';
- btn.textContent = fmt.toUpperCase();
- btn.title = 'Download as ' + fmt.toUpperCase();
- btn.dataset.fmt = fmt;
- convertBtns.push(btn);
+ var a = document.createElement('a');
+ a.className = 'btn btn-sm btn-secondary md-shell__download';
+ a.href = node.url + '?convert=' + encodeURIComponent(fmt);
+ a.download = node.name.replace(/\.md$/i, '') + '.' + fmt;
+ a.textContent = fmt.toUpperCase();
+ a.title = 'Download as ' + fmt.toUpperCase()
+ + ' (right-click to copy link or open in new tab)';
+ a.dataset.fmt = fmt;
+ a.rel = 'noopener';
+ convertBtns.push(a);
});
}
@@ -6960,39 +6968,42 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
}
});
- // Download-as-* click handlers. Auto-save when the buffer is
- // dirty so the converted file reflects what's on screen. If
- // the save fails the existing toast/status surfaces it; we
- // bail without firing the conversion.
- convertBtns.forEach(function (btn) {
- btn.addEventListener('click', async function () {
- var fmt = btn.dataset.fmt;
- if (currentInstance.dirty) {
- if (!writable) {
- if (window.zddc && window.zddc.toast) {
- window.zddc.toast(
- 'This source is read-only — save a copy elsewhere first.',
- 'error');
- }
- return;
- }
- btn.disabled = true;
- try { await save(); } finally { btn.disabled = false; }
- if (currentInstance.dirty) return; // save failed
+ // Download-as-* click handlers. The anchors are real
+ // links so right-click / middle-click / Copy Link Address all
+ // work natively. The JS handler only steps in when the buffer
+ // is dirty — auto-save first, then re-fire the click so the
+ // browser fetches the just-saved bytes. After the click is
+ // re-fired, currentInstance.dirty is false so the handler
+ // is a no-op on the second pass and the native navigation
+ // proceeds.
+ 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;
}
- btn.disabled = true;
- try {
- statusEl.textContent = 'Converting to ' + fmt.toUpperCase() + '…';
- await window.zddc.source.downloadConverted(node.url, node.name, fmt);
- statusEl.textContent = 'Downloaded ' + fmt.toUpperCase();
- } catch (e) {
- statusEl.textContent = (e && e.message) || String(e);
+ // Dirty: intercept, save, retry.
+ e.preventDefault();
+ if (!writable) {
if (window.zddc && window.zddc.toast) {
- window.zddc.toast((e && e.message) || String(e), 'error');
+ window.zddc.toast(
+ 'This source is read-only — save a copy elsewhere first.',
+ 'error');
}
- } finally {
- btn.disabled = false;
+ return;
}
+ statusEl.textContent = 'Saving before download…';
+ try { await save(); } catch (_) { /* save() surfaces its own error */ }
+ if (currentInstance.dirty) return; // save failed; toast already shown
+ statusEl.textContent = 'Downloading ' + fmt.toUpperCase() + '…';
+ // Re-trigger the click. dirty=false now so the handler
+ // exits early on the second pass and the browser
+ // processes the native navigation.
+ a.click();
});
});
}
diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html
index 1a4d7a0..8fe6b68 100644
--- a/zddc/internal/apps/embedded/classifier.html
+++ b/zddc/internal/apps/embedded/classifier.html
@@ -1681,7 +1681,7 @@ body.help-open .app-header {
diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html
index 146be7c..2e3bd80 100644
--- a/zddc/internal/apps/embedded/index.html
+++ b/zddc/internal/apps/embedded/index.html
@@ -1424,7 +1424,7 @@ body {