diff --git a/mdedit/dist/mdedit.html b/mdedit/dist/mdedit.html index fc4b5c5..f7cfb8f 100644 --- a/mdedit/dist/mdedit.html +++ b/mdedit/dist/mdedit.html @@ -1774,7 +1774,7 @@ body.help-open .app-header {
ZDDC Markdown - v0.0.12 + v0.0.13
diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index 4b03800..61f89b2 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -2113,7 +2113,7 @@ td[data-field="trackingNumber"] {
ZDDC Archive - v0.0.12 + v0.0.13
diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index bbc0f5f..294397c 100644 --- a/zddc/internal/apps/embedded/browse.html +++ b/zddc/internal/apps/embedded/browse.html @@ -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 {
ZDDC Browse - v0.0.12 + v0.0.13
@@ -796,8 +812,10 @@ body {
  • Local — click Select Directory to pick any folder on your computer (Chromium-based browsers).
  • -

    Once loaded: click folders to expand, click headers to sort, type - in the filter to narrow by name. Click any file to open it.

    +

    Once loaded: click a folder to expand it, shift-click + 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.

    @@ -808,17 +826,19 @@ body { placeholder="Filter by name (substring)..." /> - - - - - - - - - - -
    Name Size Type Modified
    +
    + + + + + + + + + + +
    Name Size Type Modified
    +
    @@ -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 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 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); + } }); } } diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html index 8779000..0301de1 100644 --- a/zddc/internal/apps/embedded/classifier.html +++ b/zddc/internal/apps/embedded/classifier.html @@ -1376,7 +1376,7 @@ body.help-open .app-header {
    ZDDC Classifier - v0.0.12 + v0.0.13
    diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html index 5ffcf8a..0aedd6b 100644 --- a/zddc/internal/apps/embedded/index.html +++ b/zddc/internal/apps/embedded/index.html @@ -866,7 +866,7 @@ body { ZDDC Archive - v0.0.12 + v0.0.13
    diff --git a/zddc/internal/apps/embedded/mdedit.html b/zddc/internal/apps/embedded/mdedit.html index fc4b5c5..f7cfb8f 100644 --- a/zddc/internal/apps/embedded/mdedit.html +++ b/zddc/internal/apps/embedded/mdedit.html @@ -1774,7 +1774,7 @@ body.help-open .app-header {
    ZDDC Markdown - v0.0.12 + v0.0.13
    diff --git a/zddc/internal/apps/embedded/transmittal.html b/zddc/internal/apps/embedded/transmittal.html index e3f9362..7c9a898 100644 --- a/zddc/internal/apps/embedded/transmittal.html +++ b/zddc/internal/apps/embedded/transmittal.html @@ -2210,7 +2210,7 @@ dialog.modal--narrow {
    ZDDC Transmittal - v0.0.12 + v0.0.13
    diff --git a/zddc/internal/apps/embedded/versions.txt b/zddc/internal/apps/embedded/versions.txt index 6824268..7d2f5ed 100644 --- a/zddc/internal/apps/embedded/versions.txt +++ b/zddc/internal/apps/embedded/versions.txt @@ -1,8 +1,8 @@ # Generated by build.sh — do not edit. One = 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 diff --git a/zddc/internal/handler/form.html b/zddc/internal/handler/form.html index a7f66a1..7e1ddf5 100644 --- a/zddc/internal/handler/form.html +++ b/zddc/internal/handler/form.html @@ -722,7 +722,7 @@ body.help-open .app-header { ZDDC Form - v0.0.12 + v0.0.13