release: v0.0.13 lockstep
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 3s
Build + deploy releases / build-and-deploy (push) Successful in 8s
Build + deploy releases / notify-chart-prod (push) Successful in 1s

This commit is contained in:
ZDDC 2026-05-03 20:21:06 -05:00
parent 7caf3ecf3f
commit 127163dfa2
9 changed files with 151 additions and 55 deletions

View file

@ -1774,7 +1774,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp">v0.0.12</span>
<span class="build-timestamp">v0.0.13</span>
</div>
<button id="select-directory" class="btn btn-primary" title="Select a Directory">Select Directory</button>
</div>

View file

@ -2113,7 +2113,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp">v0.0.12</span>
<span class="build-timestamp">v0.0.13</span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data" style="font-size:1.1rem;"></button>

View file

@ -585,6 +585,13 @@ body {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.browse-table-wrap {
flex: 1;
overflow: auto;
min-height: 0;
}
.toolbar {
@ -633,7 +640,16 @@ body {
border-collapse: collapse;
font-size: 0.9rem;
background: var(--bg);
flex: 1;
/* No flex:1 — tables don't reliably distribute extra height across
rows the way flex columns do. With few rows we'd get tall rows
that shrink as more children are loaded. The wrap div handles
scrolling instead. */
}
.browse-table tbody tr {
/* Pin rows to a deterministic height so table layout never
redistributes vertical space across them. */
line-height: 1.4;
}
.browse-table thead th {
@ -775,7 +791,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp">v0.0.12</span>
<span class="build-timestamp">v0.0.13</span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Select Directory</button>
</div>
@ -796,8 +812,10 @@ body {
<li><b>Local</b> — click <i>Select Directory</i> to pick any folder
on your computer (Chromium-based browsers).</li>
</ul>
<p>Once loaded: click folders to expand, click headers to sort, type
in the filter to narrow by name. Click any file to open it.</p>
<p>Once loaded: click a folder to expand it, <b>shift-click</b>
to expand its entire subtree (or collapse it again),
click column headers to sort, type in the filter to narrow
by name. Click any file to open it.</p>
</div>
</div>
@ -808,6 +826,7 @@ body {
placeholder="Filter by name (substring)..." />
<span class="toolbar__count" id="entryCount"></span>
</div>
<div class="browse-table-wrap">
<table class="browse-table" id="browseTable">
<thead>
<tr>
@ -820,6 +839,7 @@ body {
<tbody id="browseTbody"></tbody>
</table>
</div>
</div>
</main>
<div id="statusBar" class="status-bar"></div>
@ -1684,34 +1704,93 @@ body {
}
}
// Load a folder's children (lazy; idempotent re-loads).
async function loadChildren(node) {
if (node.loaded) return;
try {
var raw;
if (state.source === 'server') {
raw = await loader.fetchServerChildren(pathFor(node) + '/');
} else if (state.source === 'fs') {
raw = await loader.fetchFsChildren(node.handle);
} else {
return;
}
setChildren(node.id, raw);
} catch (e) {
window.app.modules.events.statusError(
'Failed to load ' + node.name + ': ' + e.message);
}
}
// Toggle a folder's expanded state. Loads children on first expand.
async function toggleFolder(nodeId) {
var n = state.nodes.get(nodeId);
if (!n || !n.isDir) return;
if (!n.expanded && !n.loaded) {
try {
var raw;
if (state.source === 'server') {
var childPath = state.currentPath
+ n.name + '/'; // server URLs are relative paths
// Walk up the parent chain to build the full path.
childPath = pathFor(n) + '/';
raw = await loader.fetchServerChildren(childPath);
} else if (state.source === 'fs') {
raw = await loader.fetchFsChildren(n.handle);
} else {
return;
}
window.app.modules.tree.setChildren(nodeId, raw);
} catch (e) {
window.app.modules.events.statusError('Failed to load folder: ' + e.message);
return;
}
await loadChildren(n);
if (!n.loaded) return; // load failed
}
n.expanded = !n.expanded;
render();
}
// Recursive expand: load + expand all descendants of nodeId. Used
// for Shift-click on a folder. Walks breadth-first, fanning out
// through children, grand-children, etc. until every reachable
// folder is loaded and marked expanded. Status bar shows progress
// because deeply-nested trees can take a while.
//
// Parallelism: kept conservative (per-level fan-out) to avoid
// hammering zddc-server with hundreds of concurrent listing
// fetches. Browsers also throttle per-origin concurrency, but
// queuing politely is friendlier than fighting that.
async function expandSubtree(nodeId) {
var root = state.nodes.get(nodeId);
if (!root || !root.isDir) return;
var status = window.app.modules.events.statusInfo;
status('Expanding subtree…');
var processed = 0;
var queue = [root];
while (queue.length) {
var batch = queue;
queue = [];
// Load this level's children in parallel (Promise.all).
await Promise.all(batch.map(function (n) { return loadChildren(n); }));
for (var i = 0; i < batch.length; i++) {
var n = batch[i];
n.expanded = true;
processed++;
for (var j = 0; j < n.childIds.length; j++) {
var c = state.nodes.get(n.childIds[j]);
if (c && c.isDir) queue.push(c);
}
}
// Re-render after each level so the user sees progress
// rather than a long pause then a sudden full-tree dump.
render();
status('Expanding subtree… (' + processed + ' folders loaded)');
}
status('Expanded ' + processed + ' folder' + (processed === 1 ? '' : 's'));
}
// Recursive collapse: mark this node and every descendant as
// collapsed. Doesn't unload — if the user re-expands later, the
// children are still in memory and re-render is instant.
function collapseSubtree(nodeId) {
var root = state.nodes.get(nodeId);
if (!root || !root.isDir) return;
function walk(n) {
n.expanded = false;
for (var i = 0; i < n.childIds.length; i++) {
var c = state.nodes.get(n.childIds[i]);
if (c && c.isDir) walk(c);
}
}
walk(root);
render();
}
// Compute the URL/path for a node by walking parents.
function pathFor(node) {
var parts = [];
@ -1734,6 +1813,8 @@ body {
setChildren: setChildren,
render: render,
toggleFolder: toggleFolder,
expandSubtree: expandSubtree,
collapseSubtree: collapseSubtree,
setSort: function (key) {
if (state.sort.key === key) {
state.sort.dir = -state.sort.dir;
@ -1834,21 +1915,36 @@ body {
}
// Tree-row clicks (event delegation on tbody).
// Click semantics on a folder row:
// - plain click → toggle just this folder
// - shift-click → recursive expand/collapse of the whole
// subtree (matches common file-explorer
// convention; e.g. Finder, VSCode tree,
// Windows Explorer)
// - alt-click → ALSO recursive (alt is sometimes the
// expand-all key on Linux DEs; bind both
// so muscle memory works either way)
// File rows: let the <a> tag's natural target=_blank do its
// job — don't intercept.
var tbody = document.getElementById('browseTbody');
if (tbody) {
tbody.addEventListener('click', function (e) {
var row = e.target.closest('tr.tree-row');
if (!row) return;
var isDir = row.dataset.isdir === 'true';
if (!isDir) {
// Let the <a> tag's natural target=_blank handle file
// clicks. Don't intercept.
return;
}
// Folder: toggle on chevron OR anywhere on the row except
// the file link (no link in folder rows).
if (!isDir) return;
e.preventDefault();
tree.toggleFolder(parseInt(row.dataset.id, 10));
var id = parseInt(row.dataset.id, 10);
if (e.shiftKey || e.altKey) {
var node = state.nodes.get(id);
if (node && node.expanded) {
tree.collapseSubtree(id);
} else {
tree.expandSubtree(id);
}
} else {
tree.toggleFolder(id);
}
});
}
}

View file

@ -1376,7 +1376,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp">v0.0.12</span>
<span class="build-timestamp">v0.0.13</span>
</div>
<button id="selectDirectoryBtn" class="btn btn-primary">Select Directory</button>
<button id="refreshBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory">Refresh</button>

View file

@ -866,7 +866,7 @@ body {
</g>
</svg>
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp">v0.0.12</span>
<span class="build-timestamp">v0.0.13</span>
</div>
<div class="header-right">
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)"></button>

View file

@ -1774,7 +1774,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp">v0.0.12</span>
<span class="build-timestamp">v0.0.13</span>
</div>
<button id="select-directory" class="btn btn-primary" title="Select a Directory">Select Directory</button>
</div>

View file

@ -2210,7 +2210,7 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp">v0.0.12</span>
<span class="build-timestamp">v0.0.13</span>
</div>
<div class="app-header__spacer"></div>
<div class="app-header__icons">

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.12
transmittal=v0.0.12
classifier=v0.0.12
mdedit=v0.0.12
landing=v0.0.12
form=v0.0.12
browse=v0.0.12
archive=v0.0.13
transmittal=v0.0.13
classifier=v0.0.13
mdedit=v0.0.13
landing=v0.0.13
form=v0.0.13
browse=v0.0.13

View file

@ -722,7 +722,7 @@ body.help-open .app-header {
</g>
</svg>
<span class="app-header__title" id="form-title">ZDDC Form</span>
<span class="build-timestamp">v0.0.12</span>
<span class="build-timestamp">v0.0.13</span>
</div>
<div class="header-right">
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)"></button>