chore(embedded): cut v0.0.27-beta

This commit is contained in:
ZDDC 2026-06-16 08:52:30 -05:00
parent 1bb5d1ad97
commit ca1452dde1
7 changed files with 117 additions and 20 deletions

View file

@ -2717,7 +2717,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-16 13:31:21 · be5b396</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a</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>

View file

@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:31:22 · be5b396</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:52:24 · 1bb5d1a</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>

View file

@ -1331,10 +1331,13 @@ body.is-elevated::after {
font-size: 0.8rem;
}
/* Resize Handle */
/* Resize Handle — sits just to the RIGHT of the split (past the 1px
border-right), overhanging the right pane's edge, so its grab area + hover
highlight never cover the folder tree's vertical scrollbar (which lives on the
left pane's right edge). */
.resize-handle {
position: absolute;
right: 0;
right: -6px;
top: 0;
bottom: 0;
width: 5px;
@ -1385,6 +1388,7 @@ body.is-elevated::after {
}
.tree-toolbar__label { color: var(--text-muted); font-size: 0.8rem; font-weight: 600; }
.classify-filters .filter-count { color: var(--text-muted); font-size: 0.85em; }
.export-list-btn { margin-left: auto; } /* push the export action to the toolbar's right edge */
/* Live filter box above a file tree. */
.tree-filter {
@ -2474,7 +2478,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
</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-16 13:31:21 · be5b396</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a</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>
@ -2533,6 +2537,8 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
<input type="checkbox" id="showEmptyCheckbox" checked>
Empty
</label>
<button class="btn btn-sm export-list-btn" id="exportListBtn"
title="Copy the filtered file list (path + file columns, no folders) as TSV — paste into Excel, edit, then paste back via “Paste rows”. Paste a full path into the Current name column to bind that exact file.">⬆ Export list</button>
</div>
<input type="search" id="treeFilterInput" class="tree-filter" spellcheck="false"
placeholder="Filter files… (e.g. master deliverables list)" aria-label="Filter files">
@ -6049,6 +6055,7 @@ X.B(E,Y);return E}return J}())
showAssignedCheckbox: document.getElementById('showAssignedCheckbox'),
showExcludedCheckbox: document.getElementById('showExcludedCheckbox'),
showEmptyCheckbox: document.getElementById('showEmptyCheckbox'),
exportListBtn: document.getElementById('exportListBtn'),
exportDatasetBtn: document.getElementById('exportDatasetBtn'),
importDatasetBtn: document.getElementById('importDatasetBtn'),
importDatasetInput: document.getElementById('importDatasetInput'),
@ -6266,6 +6273,11 @@ X.B(E,Y);return E}return J}())
[app.dom.showUnassignedCheckbox, app.dom.showPartialCheckbox, app.dom.showAssignedCheckbox, app.dom.showExcludedCheckbox, app.dom.showEmptyCheckbox]
.forEach(function (cb) { if (cb) cb.addEventListener('change', pushClassifyFilters); });
// Export the filtered file list (path + file TSV) for the Excel round-trip.
if (app.dom.exportListBtn) app.dom.exportListBtn.addEventListener('click', function () {
if (app.modules.tree && app.modules.tree.exportFilteredList) app.modules.tree.exportFilteredList();
});
// Collapse tree button
app.dom.collapseTreeBtn.addEventListener('click', handleCollapseTree);
@ -8061,8 +8073,17 @@ X.B(E,Y);return E}return J}())
return addTrackingPath(null, parseFolderLevels(tn + '_' + rev));
}
function assignFromRow(keys, row) {
if (!keys || !keys.length) return;
var leaf = leafForRow(row);
if (!leaf || !keys || !keys.length) return;
if (!leaf) {
// No tracking number on the row yet — still CLAIM these files for it
// (e.g. a pasted full path on a row whose tracking is still blank). The
// binding is recorded in row.bound; when a tracking/rev later lands,
// restampRow places the claimed files onto the new leaf.
keys.forEach(function (k) { row.placed[k] = true; (row.bound || (row.bound = Object.create(null)))[k] = true; });
notify();
return;
}
place(keys, leaf, 'tracking');
keys.forEach(function (k) {
row.placed[k] = true;
@ -8096,12 +8117,17 @@ X.B(E,Y);return E}return J}())
if (!keys.length) return;
var leaf = leafForRow(row);
if (!leaf) return;
var old = Object.create(null);
var old = Object.create(null), toPlace = [];
keys.forEach(function (k) {
var a = state.assignments[k];
if (a && a.trackingNodeId) { if (a.trackingNodeId !== leaf) old[a.trackingNodeId] = true; a.trackingNodeId = leaf; }
else if (row.bound && row.bound[k]) toPlace.push(k); // claimed by path, not yet placed → place now
else delete row.placed[k]; // user un-placed it elsewhere — don't resurrect
});
if (toPlace.length) {
place(toPlace, leaf, 'tracking');
if (row.title && row.title.trim()) toPlace.forEach(function (k) { var aa = state.assignments[k]; if (aa && !aa.titleOverride) setTitleOverride(k, row.title); });
}
clearHashConflicts();
Object.keys(old).forEach(pruneEmptyTrackingChain);
notify();
@ -8166,6 +8192,7 @@ X.B(E,Y);return E}return J}())
});
return { rows: rows, skipped: skipped };
}
function baseName(s) { return String(s == null ? '' : s).split(/[\/\\]/).pop(); }
function normTok(s) { return String(s == null ? '' : s).toUpperCase().replace(/[^A-Z0-9]/g, ''); }
function dropExt(s) { return String(s == null ? '' : s).replace(/\.[^.\/\\]+$/, ''); }
function nameKey(s) { return dropExt(s).toLowerCase().replace(/[^a-z0-9]+/g, ''); }
@ -8200,9 +8227,15 @@ X.B(E,Y);return E}return J}())
var out = [];
(files || []).forEach(function (f) {
var full = zddc.joinExtension(f.originalFilename, f.extension);
var key = srcKeyForFile(f);
var best = null;
named.forEach(function (r) {
var s = nameScore(r.currentName, full);
// A pasted FULL PATH equal to this file's key → an exact, direct
// bind (the strongest signal — wins over any name score).
if (r.currentName === key) { best = { row: r, confidence: 1, via: 'path' }; return; }
// Otherwise score on the name; a path that didn't match exactly is
// reduced to its basename so the fuzzy name match still applies.
var s = nameScore(baseName(r.currentName), full);
if (s > 0 && (!best || s > best.confidence)) best = { row: r, confidence: s, via: 'name' };
});
if (!best) { // fallback: tracking number in the filename
@ -10335,6 +10368,68 @@ X.B(E,Y);return E}return J}())
});
}
// ── Export the filtered file list to TSV (path + file) ──────────────────
// Every file passing the CURRENT tree filters (name search + the Show
// toggles), across the WHOLE tree — expand/collapse is display-only, so a
// collapsed folder's files are included just the same. `path` is the file's
// root-relative key (paste it into "Current name" to bind that exact file);
// `file` is the bare filename (paste it for a name to match/drop later).
function filteredFileObjects() {
var c = window.app.modules.classify;
var vis = anyFilter() ? computeVisible() : null;
var out = [];
(function walk(nodes) {
(nodes || []).forEach(function (n) {
(n.files || []).forEach(function (f) {
var show = vis ? !!vis.files[c.srcKeyForFile(f)] : classifyAllows(f);
if (show) out.push(f);
});
walk(n.children);
});
})(window.app.folderTree || []);
return out;
}
function buildExportTsv() {
var c = window.app.modules.classify;
var files = filteredFileObjects().slice().sort(function (a, b) {
return cmpName(c.srcKeyForFile(a), c.srcKeyForFile(b));
});
var lines = ['path\tfile'];
files.forEach(function (f) {
lines.push(c.srcKeyForFile(f) + '\t' + window.zddc.joinExtension(f.originalFilename, f.extension));
});
return { tsv: lines.join('\n'), count: files.length };
}
function exportFilteredList() {
var built = buildExportTsv();
if (!built.count) { window.zddc.toast('No files to export — nothing passes the current filters.', 'info'); return; }
copyOrDownload(built.tsv, built.count);
}
function copyOrDownload(text, count) {
function ok() { window.zddc.toast('Copied ' + count + ' file' + (count === 1 ? '' : 's') + ' (path + file) — paste into Excel.', 'success'); }
function download() {
try {
var blob = new Blob([text], { type: 'text/tab-separated-values' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a'); a.href = url; a.download = 'classifier-files.tsv';
document.body.appendChild(a); a.click(); a.remove();
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
window.zddc.toast('Clipboard unavailable — downloaded classifier-files.tsv instead.', 'info');
} catch (e) { window.zddc.toast('Could not copy or download the list — ' + (e.message || e), 'error'); }
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(ok, download);
return;
}
try {
var ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.focus(); ta.select();
var done = document.execCommand('copy'); ta.remove();
done ? ok() : download();
} catch (e) { download(); }
}
/**
* Render the folder tree
*/
@ -11110,7 +11205,9 @@ X.B(E,Y);return E}return J}())
selectAll,
revealFile,
setShowFilters,
setNameFilter
setNameFilter,
exportFilteredList,
_buildExportTsv: buildExportTsv
};
})();
@ -12019,7 +12116,7 @@ X.B(E,Y);return E}return J}())
}
function openPasteDialog(prefill) {
var c = C();
var m = scratchModal('Paste rows from Excel', 'Fixed columns, tab-separated as Excel copies: Tracking number · Rev (Status) · Title · Current name. A header row is skipped. The current name is matched against your files — exact matches are assigned automatically.');
var m = scratchModal('Paste rows from Excel', 'Fixed columns, tab-separated as Excel copies: Tracking number · Rev (Status) · Title · Current name. A header row is skipped. Current name accepts a bare filename (matched against your files — exact name matches are assigned automatically) OR a full path from “⬆ Export list” (binds that exact file directly on paste).');
var ta = document.createElement('textarea');
ta.className = 'scratch-paste__ta'; ta.rows = 6; ta.spellcheck = false;
ta.placeholder = 'ACME-AR-DWG-0001\tA (IFR)\tFloor plan\tIMG_4471.pdf';

View file

@ -1793,7 +1793,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-16 13:31:21 · be5b396</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a</span></span>
</div>
</div>
<div class="header-right">

View file

@ -2770,7 +2770,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-16 13:31:21 · be5b396</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a</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;

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-16 13:31:21 · be5b396
transmittal=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
classifier=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
landing=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
form=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
tables=v0.0.27-beta · 2026-06-16 13:31:22 · be5b396
browse=v0.0.27-beta · 2026-06-16 13:31:22 · be5b396
archive=v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a
transmittal=v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a
classifier=v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a
landing=v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a
form=v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a
tables=v0.0.27-beta · 2026-06-16 13:52:24 · 1bb5d1a
browse=v0.0.27-beta · 2026-06-16 13:52:24 · 1bb5d1a

View file

@ -1780,7 +1780,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-16 13:31:22 · be5b396</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:52:24 · 1bb5d1a</span></span>
</div>
</div>
<div class="header-right">