Compare commits

..

2 commits

Author SHA1 Message Date
8875d490f5 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 8s
2026-06-03 08:55:39 -05:00
3e8737b7c9 feat(browse): preview .docx/.xlsx + fix markdown-editor horizontal overflow
DOCX/XLSX preview: add renderDocx (docx-preview) and renderXlsx (SheetJS)
to shared/preview-lib.js — the natural home alongside renderTiff/
renderZipListing, reusable by every tool. browse dispatches Office files
to them in both the inline pane and the pop-out window via the existing
preview.isOffice() check, and browse/build.sh now bundles the
docx-preview + xlsx vendors. Renderers degrade to a friendly message if a
tool doesn't bundle the vendor.

Overflow fix: .md-shell used `grid-template-columns: 280px 1fr`. A bare
`1fr` is `minmax(auto, 1fr)`, whose `auto` floor is the editor's
min-content width (Toast UI's toolbar) — so the content track refused to
shrink and the whole shell overflowed #previewBody as the window narrowed
instead of the editor reflowing smaller. Switch both tracks to
minmax(0, …) in the CSS and in the three JS spots that rewrite the
columns on sidebar-drag, and give .md-shell__sidebar min-width: 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:55:14 -05:00
12 changed files with 698 additions and 21 deletions

View file

@ -42,6 +42,8 @@ concat_files \
# without an external HTTP dependency.
concat_files \
"../shared/vendor/jszip.min.js" \
"../shared/vendor/docx-preview.min.js" \
"../shared/vendor/xlsx.full.min.js" \
"../shared/vendor/utif.min.js" \
"../shared/vendor/js-yaml.min.js" \
"../shared/vendor/codemirror-yaml.min.js" \

View file

@ -589,7 +589,14 @@ body {
.md-shell {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 280px 1fr; /* JS overrides on resize */
/* minmax(0, ) on BOTH tracks is load-bearing: a bare `1fr` is
`minmax(auto, 1fr)`, whose `auto` floor is the editor's min-content
width (Toast UI's toolbar). That floor stops the content track from
shrinking, so the whole shell overflows #previewBody as the window
narrows instead of the editor getting narrower. minmax(0, 1fr) drops
the floor so the editor reflows down to nothing. JS overrides the
column widths on drag it preserves the minmax(0, ) form. */
grid-template-columns: minmax(0, 280px) minmax(0, 1fr);
grid-template-areas: "sidebar content";
height: 100%;
min-height: 0;
@ -606,6 +613,7 @@ body {
grid-area: sidebar;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
overflow: hidden;
border-right: 1px solid var(--border);

View file

@ -354,7 +354,7 @@
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) ──────────────
@ -595,7 +595,7 @@
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() {
@ -620,7 +620,7 @@
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)';
});
})();

View file

@ -176,6 +176,24 @@
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);
@ -273,6 +291,13 @@
} 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);

View file

@ -4,15 +4,23 @@
* 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<void>
* renderTiff(doc, container, arrayBuffer, opts) Promise<void>
* renderZipListing(doc, container, arrayBuffer, opts) Promise<void>
* renderDocx(doc, container, arrayBuffer, opts) Promise<void>
* renderXlsx(doc, container, arrayBuffer, opts) Promise<void>
* TIFF_EXTENSIONS, IMAGE_EXTENSIONS, TEXT_EXTENSIONS, OFFICE_EXTENSIONS
* isTiff(ext), isImage(ext), isText(ext), isZip(ext), isOffice(ext)
*
@ -525,6 +533,111 @@
});
}
// ── 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 = '<div class="preview-empty">DOCX preview unavailable '
+ '(renderer not bundled in this tool).</div>';
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 = '<div class="preview-empty" style="color:var(--danger,#c00)">'
+ 'Failed to render DOCX: ' + escapeHtml(err.message || err) + '</div>';
});
}
// ── 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 = '<div class="preview-empty">Spreadsheet preview unavailable '
+ '(renderer not bundled in this tool).</div>';
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 = '<div class="preview-empty" style="color:var(--danger,#c00)">'
+ 'Failed to render spreadsheet: ' + escapeHtml(err.message || err) + '</div>';
}
return Promise.resolve();
}
// ── Public API ───────────────────────────────────────────────────────────
if (!root.zddc) root.zddc = {};
@ -541,6 +654,8 @@
loadLibrary: loadLibrary,
renderTiff: renderTiff,
renderZipListing: renderZipListing,
renderDocx: renderDocx,
renderXlsx: renderXlsx,
formatSize: formatSize,
formatDate: formatDate
};

View file

@ -2582,7 +2582,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>
@ -5125,15 +5125,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<void>
* renderTiff(doc, container, arrayBuffer, opts) → Promise<void>
* renderZipListing(doc, container, arrayBuffer, opts) → Promise<void>
* renderDocx(doc, container, arrayBuffer, opts) → Promise<void>
* renderXlsx(doc, container, arrayBuffer, opts) → Promise<void>
* TIFF_EXTENSIONS, IMAGE_EXTENSIONS, TEXT_EXTENSIONS, OFFICE_EXTENSIONS
* isTiff(ext), isImage(ext), isText(ext), isZip(ext), isOffice(ext)
*
@ -5646,6 +5654,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 = '<div class="preview-empty">DOCX preview unavailable '
+ '(renderer not bundled in this tool).</div>';
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 = '<div class="preview-empty" style="color:var(--danger,#c00)">'
+ 'Failed to render DOCX: ' + escapeHtml(err.message || err) + '</div>';
});
}
// ── 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 = '<div class="preview-empty">Spreadsheet preview unavailable '
+ '(renderer not bundled in this tool).</div>';
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 = '<div class="preview-empty" style="color:var(--danger,#c00)">'
+ 'Failed to render spreadsheet: ' + escapeHtml(err.message || err) + '</div>';
}
return Promise.resolve();
}
// ── Public API ───────────────────────────────────────────────────────────
if (!root.zddc) root.zddc = {};
@ -5662,6 +5775,8 @@ X.B(E,Y);return E}return J}())
loadLibrary: loadLibrary,
renderTiff: renderTiff,
renderZipListing: renderZipListing,
renderDocx: renderDocx,
renderXlsx: renderXlsx,
formatSize: formatSize,
formatDate: formatDate
};

File diff suppressed because one or more lines are too long

View file

@ -1793,7 +1793,7 @@ body.is-elevated::after {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use 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>
@ -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<void>
* renderTiff(doc, container, arrayBuffer, opts) → Promise<void>
* renderZipListing(doc, container, arrayBuffer, opts) → Promise<void>
* renderDocx(doc, container, arrayBuffer, opts) → Promise<void>
* renderXlsx(doc, container, arrayBuffer, opts) → Promise<void>
* 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 = '<div class="preview-empty">DOCX preview unavailable '
+ '(renderer not bundled in this tool).</div>';
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 = '<div class="preview-empty" style="color:var(--danger,#c00)">'
+ 'Failed to render DOCX: ' + escapeHtml(err.message || err) + '</div>';
});
}
// ── 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 = '<div class="preview-empty">Spreadsheet preview unavailable '
+ '(renderer not bundled in this tool).</div>';
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 = '<div class="preview-empty" style="color:var(--danger,#c00)">'
+ 'Failed to render spreadsheet: ' + escapeHtml(err.message || err) + '</div>';
}
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
};

View file

@ -1536,7 +1536,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b</span></span>
</div>
</div>
<div class="header-right">

View file

@ -2635,7 +2635,7 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b</span></span>
</div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action;
@ -5452,15 +5452,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<void>
* renderTiff(doc, container, arrayBuffer, opts) → Promise<void>
* renderZipListing(doc, container, arrayBuffer, opts) → Promise<void>
* renderDocx(doc, container, arrayBuffer, opts) → Promise<void>
* renderXlsx(doc, container, arrayBuffer, opts) → Promise<void>
* TIFF_EXTENSIONS, IMAGE_EXTENSIONS, TEXT_EXTENSIONS, OFFICE_EXTENSIONS
* isTiff(ext), isImage(ext), isText(ext), isZip(ext), isOffice(ext)
*
@ -5973,6 +5981,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 = '<div class="preview-empty">DOCX preview unavailable '
+ '(renderer not bundled in this tool).</div>';
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 = '<div class="preview-empty" style="color:var(--danger,#c00)">'
+ 'Failed to render DOCX: ' + escapeHtml(err.message || err) + '</div>';
});
}
// ── 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 = '<div class="preview-empty">Spreadsheet preview unavailable '
+ '(renderer not bundled in this tool).</div>';
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 = '<div class="preview-empty" style="color:var(--danger,#c00)">'
+ 'Failed to render spreadsheet: ' + escapeHtml(err.message || err) + '</div>';
}
return Promise.resolve();
}
// ── Public API ───────────────────────────────────────────────────────────
if (!root.zddc) root.zddc = {};
@ -5989,6 +6102,8 @@ X.B(E,Y);return E}return J}())
loadLibrary: loadLibrary,
renderTiff: renderTiff,
renderZipListing: renderZipListing,
renderDocx: renderDocx,
renderXlsx: renderXlsx,
formatSize: formatSize,
formatDate: formatDate
};

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4
transmittal=v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4
classifier=v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4
landing=v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4
form=v0.0.27-beta · 2026-06-02 19:01:22 · af07fa4
tables=v0.0.27-beta · 2026-06-02 19:01:23 · af07fa4
browse=v0.0.27-beta · 2026-06-02 19:01:23 · af07fa4
archive=v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b
transmittal=v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b
classifier=v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b
landing=v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b
form=v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b
tables=v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b
browse=v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b

View file

@ -1534,7 +1534,7 @@ body.is-elevated::after {
</svg>
<div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-02 19:01:23 · af07fa4</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b</span></span>
</div>
</div>
<div class="header-right">