* TIFF_EXTENSIONS, IMAGE_EXTENSIONS, TEXT_EXTENSIONS, OFFICE_EXTENSIONS
* isTiff(ext), isImage(ext), isText(ext), isZip(ext), isOffice(ext)
*
@@ -5557,6 +5607,111 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
});
}
+ // ── DOCX (docx-preview) ────────────────────────────────────────────────
+ //
+ // docx-preview renders Word-styled pages with an intrinsic page width, so
+ // we wrap it in a scroll container: a wide document scrolls WITHIN the
+ // preview pane rather than pushing the page wider. docx-preview is bundled
+ // by the tools that opt in (each build.sh concatenates
+ // shared/vendor/docx-preview.min.js → window.docx).
+
+ var DOCX_CSS =
+ '.zddc-docx{height:100%;min-width:0;overflow:auto;'
+ + 'background:var(--bg-secondary,#eee);padding:1rem;box-sizing:border-box;}'
+ + '.zddc-docx .docx-wrapper{background:transparent;padding:0;}';
+
+ function renderDocx(doc, container, arrayBuffer, opts) {
+ opts = opts || {};
+ injectStyles(doc, 'zddc-docx-styles', DOCX_CSS);
+ if (!window.docx || typeof window.docx.renderAsync !== 'function') {
+ container.innerHTML = 'DOCX preview unavailable '
+ + '(renderer not bundled in this tool).
';
+ return Promise.resolve();
+ }
+ container.innerHTML = '';
+ var scroll = doc.createElement('div');
+ scroll.className = 'zddc-docx';
+ container.appendChild(scroll);
+ return Promise.resolve(
+ window.docx.renderAsync(arrayBuffer, scroll, null, { inWrapper: true })
+ ).catch(function (err) {
+ container.innerHTML = ''
+ + 'Failed to render DOCX: ' + escapeHtml(err.message || err) + '
';
+ });
+ }
+
+ // ── XLSX / XLS (SheetJS) ───────────────────────────────────────────────
+ //
+ // Reads the workbook and renders the active sheet to an HTML table; a tab
+ // bar switches sheets when there's more than one. SheetJS is bundled by
+ // the tools that opt in (shared/vendor/xlsx.full.min.js → window.XLSX).
+
+ var XLSX_CSS =
+ '.zddc-xlsx{display:flex;flex-direction:column;height:100%;min-width:0;'
+ + 'min-height:0;overflow:hidden;}'
+ + '.zddc-xlsx__tabs{display:flex;flex-wrap:wrap;gap:0.25rem;padding:0.4rem;'
+ + 'border-bottom:1px solid var(--border,#ccc);flex:0 0 auto;}'
+ + '.zddc-xlsx__tab{padding:0.2rem 0.6rem;border:1px solid var(--border,#ccc);'
+ + 'border-radius:4px;background:var(--bg,#fff);color:var(--text,#222);'
+ + 'cursor:pointer;font-size:0.85rem;}'
+ + '.zddc-xlsx__tab.is-active{background:var(--primary,#2563eb);color:#fff;'
+ + 'border-color:var(--primary,#2563eb);}'
+ + '.zddc-xlsx__body{flex:1 1 auto;min-width:0;min-height:0;overflow:auto;}'
+ + '.zddc-xlsx__body table{border-collapse:collapse;font-size:0.85rem;'
+ + 'color:var(--text,#222);}'
+ + '.zddc-xlsx__body td,.zddc-xlsx__body th{border:1px solid var(--border,#ddd);'
+ + 'padding:0.2rem 0.45rem;white-space:nowrap;}';
+
+ function renderXlsx(doc, container, arrayBuffer, opts) {
+ opts = opts || {};
+ injectStyles(doc, 'zddc-xlsx-styles', XLSX_CSS);
+ if (!window.XLSX || typeof window.XLSX.read !== 'function') {
+ container.innerHTML = 'Spreadsheet preview unavailable '
+ + '(renderer not bundled in this tool).
';
+ return Promise.resolve();
+ }
+ try {
+ var wb = window.XLSX.read(arrayBuffer, { type: 'array' });
+ container.innerHTML = '';
+ var rootEl = doc.createElement('div');
+ rootEl.className = 'zddc-xlsx';
+ var body = doc.createElement('div');
+ body.className = 'zddc-xlsx__body';
+
+ function showSheet(name) {
+ var sheet = wb.Sheets[name];
+ body.innerHTML = sheet
+ ? window.XLSX.utils.sheet_to_html(sheet, { editable: false })
+ : '';
+ }
+
+ if (wb.SheetNames.length > 1) {
+ var tabs = doc.createElement('div');
+ tabs.className = 'zddc-xlsx__tabs';
+ wb.SheetNames.forEach(function (name, i) {
+ var btn = doc.createElement('button');
+ btn.className = 'zddc-xlsx__tab' + (i === 0 ? ' is-active' : '');
+ btn.textContent = name;
+ btn.addEventListener('click', function () {
+ var all = tabs.querySelectorAll('.zddc-xlsx__tab');
+ for (var j = 0; j < all.length; j++) all[j].classList.remove('is-active');
+ btn.classList.add('is-active');
+ showSheet(name);
+ });
+ tabs.appendChild(btn);
+ });
+ rootEl.appendChild(tabs);
+ }
+ rootEl.appendChild(body);
+ container.appendChild(rootEl);
+ showSheet(wb.SheetNames[0]);
+ } catch (err) {
+ container.innerHTML = ''
+ + 'Failed to render spreadsheet: ' + escapeHtml(err.message || err) + '
';
+ }
+ return Promise.resolve();
+ }
+
// ── Public API ───────────────────────────────────────────────────────────
if (!root.zddc) root.zddc = {};
@@ -5573,6 +5728,8 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
loadLibrary: loadLibrary,
renderTiff: renderTiff,
renderZipListing: renderZipListing,
+ renderDocx: renderDocx,
+ renderXlsx: renderXlsx,
formatSize: formatSize,
formatDate: formatDate
};
@@ -8125,6 +8282,24 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
return;
}
+ // Office docs (.docx via docx-preview, .xlsx/.xls via SheetJS) →
+ // shared/preview-lib renderers. .doc/.ppt etc. fall through to the
+ // download fallback below.
+ if (preview && preview.isOffice(ext)) {
+ try {
+ var officeBuf = await getArrayBuffer(node);
+ container.innerHTML = '';
+ if (ext === 'docx') {
+ await preview.renderDocx(document, container, officeBuf, { fileName: node.name });
+ } else {
+ await preview.renderXlsx(document, container, officeBuf, { fileName: node.name });
+ }
+ } catch (e) {
+ renderError(container, 'Failed to render ' + ext.toUpperCase() + ': ' + (e.message || e));
+ }
+ return;
+ }
+
if (preview && preview.isText(ext)) {
try {
var txtBuf = await getArrayBuffer(node);
@@ -8222,6 +8397,13 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
} else if (preview && preview.isZip(ext)) {
var zb = await getArrayBuffer(node);
await preview.renderZipListing(win.document, c, zb, { fileName: node.name });
+ } else if (preview && preview.isOffice(ext)) {
+ var ob = await getArrayBuffer(node);
+ if (ext === 'docx') {
+ await preview.renderDocx(win.document, c, ob, { fileName: node.name });
+ } else {
+ await preview.renderXlsx(win.document, c, ob, { fileName: node.name });
+ }
} else if (preview && preview.isText(ext)) {
var txb = await getArrayBuffer(node);
var text = new TextDecoder('utf-8', { fatal: false }).decode(txb);
@@ -8648,7 +8830,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
container.innerHTML = '';
var shell = document.createElement('div');
shell.className = 'md-shell';
- shell.style.gridTemplateColumns = lastSidebarWidth + 'px 1fr';
+ shell.style.gridTemplateColumns = 'minmax(0, ' + lastSidebarWidth + 'px) minmax(0, 1fr)';
container.appendChild(shell);
// ── Sidebar (col 1): front matter (top) + TOC (bottom) ──────────────
@@ -8889,7 +9071,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
var w = startW + dx;
w = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, w));
lastSidebarWidth = w;
- shell.style.gridTemplateColumns = w + 'px 1fr';
+ shell.style.gridTemplateColumns = 'minmax(0, ' + w + 'px) minmax(0, 1fr)';
e.preventDefault();
}
function onUp() {
@@ -8914,7 +9096,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
var w = Math.max(SIDEBAR_MIN_WIDTH,
Math.min(SIDEBAR_MAX_WIDTH, lastSidebarWidth + step));
lastSidebarWidth = w;
- shell.style.gridTemplateColumns = w + 'px 1fr';
+ shell.style.gridTemplateColumns = 'minmax(0, ' + w + 'px) minmax(0, 1fr)';
});
})();
diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html
index d45f403..071ecc4 100644
--- a/zddc/internal/apps/embedded/classifier.html
+++ b/zddc/internal/apps/embedded/classifier.html
@@ -1793,7 +1793,7 @@ body.is-elevated::after {
@@ -4396,15 +4396,23 @@ X.B(E,Y);return E}return J}())
* Cross-tool helpers for previewing file types that need a decoder:
* - TIFF (UTIF.js) — multi-page, browser-PDF-viewer-style toolbar
* - ZIP listing (JSZip) — sortable file-list view
+ * - DOCX (docx-preview) — Word-styled pages in a scroll container
+ * - XLSX/XLS (SheetJS) — sheet-to-HTML table with a sheet tab bar
*
* Renderers operate on any document (parent window or popup window), so the
* same code works for tools whose preview opens in a popup (classifier,
* archive, transmittal) and tools that render inline (browse).
*
+ * The DOCX/XLSX renderers expect their vendor lib bundled by the calling
+ * tool's build.sh (docx-preview.min.js → window.docx, xlsx.full.min.js →
+ * window.XLSX); they degrade to a friendly message if it isn't present.
+ *
* Public API on window.zddc.preview:
* loadLibrary(url) → Promise
* renderTiff(doc, container, arrayBuffer, opts) → Promise
* renderZipListing(doc, container, arrayBuffer, opts) → Promise
+ * renderDocx(doc, container, arrayBuffer, opts) → Promise
+ * renderXlsx(doc, container, arrayBuffer, opts) → Promise
* TIFF_EXTENSIONS, IMAGE_EXTENSIONS, TEXT_EXTENSIONS, OFFICE_EXTENSIONS
* isTiff(ext), isImage(ext), isText(ext), isZip(ext), isOffice(ext)
*
@@ -4917,6 +4925,111 @@ X.B(E,Y);return E}return J}())
});
}
+ // ── DOCX (docx-preview) ────────────────────────────────────────────────
+ //
+ // docx-preview renders Word-styled pages with an intrinsic page width, so
+ // we wrap it in a scroll container: a wide document scrolls WITHIN the
+ // preview pane rather than pushing the page wider. docx-preview is bundled
+ // by the tools that opt in (each build.sh concatenates
+ // shared/vendor/docx-preview.min.js → window.docx).
+
+ var DOCX_CSS =
+ '.zddc-docx{height:100%;min-width:0;overflow:auto;'
+ + 'background:var(--bg-secondary,#eee);padding:1rem;box-sizing:border-box;}'
+ + '.zddc-docx .docx-wrapper{background:transparent;padding:0;}';
+
+ function renderDocx(doc, container, arrayBuffer, opts) {
+ opts = opts || {};
+ injectStyles(doc, 'zddc-docx-styles', DOCX_CSS);
+ if (!window.docx || typeof window.docx.renderAsync !== 'function') {
+ container.innerHTML = 'DOCX preview unavailable '
+ + '(renderer not bundled in this tool).
';
+ return Promise.resolve();
+ }
+ container.innerHTML = '';
+ var scroll = doc.createElement('div');
+ scroll.className = 'zddc-docx';
+ container.appendChild(scroll);
+ return Promise.resolve(
+ window.docx.renderAsync(arrayBuffer, scroll, null, { inWrapper: true })
+ ).catch(function (err) {
+ container.innerHTML = ''
+ + 'Failed to render DOCX: ' + escapeHtml(err.message || err) + '
';
+ });
+ }
+
+ // ── XLSX / XLS (SheetJS) ───────────────────────────────────────────────
+ //
+ // Reads the workbook and renders the active sheet to an HTML table; a tab
+ // bar switches sheets when there's more than one. SheetJS is bundled by
+ // the tools that opt in (shared/vendor/xlsx.full.min.js → window.XLSX).
+
+ var XLSX_CSS =
+ '.zddc-xlsx{display:flex;flex-direction:column;height:100%;min-width:0;'
+ + 'min-height:0;overflow:hidden;}'
+ + '.zddc-xlsx__tabs{display:flex;flex-wrap:wrap;gap:0.25rem;padding:0.4rem;'
+ + 'border-bottom:1px solid var(--border,#ccc);flex:0 0 auto;}'
+ + '.zddc-xlsx__tab{padding:0.2rem 0.6rem;border:1px solid var(--border,#ccc);'
+ + 'border-radius:4px;background:var(--bg,#fff);color:var(--text,#222);'
+ + 'cursor:pointer;font-size:0.85rem;}'
+ + '.zddc-xlsx__tab.is-active{background:var(--primary,#2563eb);color:#fff;'
+ + 'border-color:var(--primary,#2563eb);}'
+ + '.zddc-xlsx__body{flex:1 1 auto;min-width:0;min-height:0;overflow:auto;}'
+ + '.zddc-xlsx__body table{border-collapse:collapse;font-size:0.85rem;'
+ + 'color:var(--text,#222);}'
+ + '.zddc-xlsx__body td,.zddc-xlsx__body th{border:1px solid var(--border,#ddd);'
+ + 'padding:0.2rem 0.45rem;white-space:nowrap;}';
+
+ function renderXlsx(doc, container, arrayBuffer, opts) {
+ opts = opts || {};
+ injectStyles(doc, 'zddc-xlsx-styles', XLSX_CSS);
+ if (!window.XLSX || typeof window.XLSX.read !== 'function') {
+ container.innerHTML = 'Spreadsheet preview unavailable '
+ + '(renderer not bundled in this tool).
';
+ return Promise.resolve();
+ }
+ try {
+ var wb = window.XLSX.read(arrayBuffer, { type: 'array' });
+ container.innerHTML = '';
+ var rootEl = doc.createElement('div');
+ rootEl.className = 'zddc-xlsx';
+ var body = doc.createElement('div');
+ body.className = 'zddc-xlsx__body';
+
+ function showSheet(name) {
+ var sheet = wb.Sheets[name];
+ body.innerHTML = sheet
+ ? window.XLSX.utils.sheet_to_html(sheet, { editable: false })
+ : '';
+ }
+
+ if (wb.SheetNames.length > 1) {
+ var tabs = doc.createElement('div');
+ tabs.className = 'zddc-xlsx__tabs';
+ wb.SheetNames.forEach(function (name, i) {
+ var btn = doc.createElement('button');
+ btn.className = 'zddc-xlsx__tab' + (i === 0 ? ' is-active' : '');
+ btn.textContent = name;
+ btn.addEventListener('click', function () {
+ var all = tabs.querySelectorAll('.zddc-xlsx__tab');
+ for (var j = 0; j < all.length; j++) all[j].classList.remove('is-active');
+ btn.classList.add('is-active');
+ showSheet(name);
+ });
+ tabs.appendChild(btn);
+ });
+ rootEl.appendChild(tabs);
+ }
+ rootEl.appendChild(body);
+ container.appendChild(rootEl);
+ showSheet(wb.SheetNames[0]);
+ } catch (err) {
+ container.innerHTML = ''
+ + 'Failed to render spreadsheet: ' + escapeHtml(err.message || err) + '
';
+ }
+ return Promise.resolve();
+ }
+
// ── Public API ───────────────────────────────────────────────────────────
if (!root.zddc) root.zddc = {};
@@ -4933,6 +5046,8 @@ X.B(E,Y);return E}return J}())
loadLibrary: loadLibrary,
renderTiff: renderTiff,
renderZipListing: renderZipListing,
+ renderDocx: renderDocx,
+ renderXlsx: renderXlsx,
formatSize: formatSize,
formatDate: formatDate
};
diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html
index 121f8df..f4783cd 100644
--- a/zddc/internal/apps/embedded/index.html
+++ b/zddc/internal/apps/embedded/index.html
@@ -1536,7 +1536,7 @@ body {