Compare commits

..

No commits in common. "127163dfa29450295c9bb6216afed81fbcb7b1a7" and "d6448159fa8f62eeb2819602ed3ff5398e489664" have entirely different histories.

14 changed files with 96 additions and 299 deletions

View file

@ -5,13 +5,6 @@
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.browse-table-wrap {
flex: 1;
overflow: auto;
min-height: 0;
}
.toolbar {
@ -60,16 +53,7 @@
border-collapse: collapse;
font-size: 0.9rem;
background: var(--bg);
/* 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;
flex: 1;
}
.browse-table thead th {

View file

@ -80,36 +80,21 @@
}
// 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) return;
e.preventDefault();
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);
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).
e.preventDefault();
tree.toggleFolder(parseInt(row.dataset.id, 10));
});
}
}

View file

@ -219,93 +219,34 @@
}
}
// 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) {
await loadChildren(n);
if (!n.loaded) return; // load failed
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;
}
}
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 = [];
@ -328,8 +269,6 @@
setChildren: setChildren,
render: render,
toggleFolder: toggleFolder,
expandSubtree: expandSubtree,
collapseSubtree: collapseSubtree,
setSort: function (key) {
if (state.sort.key === key) {
state.sort.dir = -state.sort.dir;

View file

@ -43,10 +43,8 @@
<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 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>
<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>
</div>
</div>
@ -57,19 +55,17 @@
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>
<th data-sort="name" class="col-name sortable">Name <span class="sort-arrow"></span></th>
<th data-sort="size" class="col-size sortable">Size <span class="sort-arrow"></span></th>
<th data-sort="ext" class="col-ext sortable">Type <span class="sort-arrow"></span></th>
<th data-sort="date" class="col-date sortable">Modified <span class="sort-arrow"></span></th>
</tr>
</thead>
<tbody id="browseTbody"></tbody>
</table>
</div>
<table class="browse-table" id="browseTable">
<thead>
<tr>
<th data-sort="name" class="col-name sortable">Name <span class="sort-arrow"></span></th>
<th data-sort="size" class="col-size sortable">Size <span class="sort-arrow"></span></th>
<th data-sort="ext" class="col-ext sortable">Type <span class="sort-arrow"></span></th>
<th data-sort="date" class="col-date sortable">Modified <span class="sort-arrow"></span></th>
</tr>
</thead>
<tbody id="browseTbody"></tbody>
</table>
</div>
</main>

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.13</span>
<span class="build-timestamp">v0.0.12</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.13</span>
<span class="build-timestamp">v0.0.12</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,13 +585,6 @@ body {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.browse-table-wrap {
flex: 1;
overflow: auto;
min-height: 0;
}
.toolbar {
@ -640,16 +633,7 @@ body {
border-collapse: collapse;
font-size: 0.9rem;
background: var(--bg);
/* 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;
flex: 1;
}
.browse-table thead th {
@ -791,7 +775,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp">v0.0.13</span>
<span class="build-timestamp">v0.0.12</span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Select Directory</button>
</div>
@ -812,10 +796,8 @@ 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 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>
<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>
</div>
</div>
@ -826,19 +808,17 @@ 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>
<th data-sort="name" class="col-name sortable">Name <span class="sort-arrow"></span></th>
<th data-sort="size" class="col-size sortable">Size <span class="sort-arrow"></span></th>
<th data-sort="ext" class="col-ext sortable">Type <span class="sort-arrow"></span></th>
<th data-sort="date" class="col-date sortable">Modified <span class="sort-arrow"></span></th>
</tr>
</thead>
<tbody id="browseTbody"></tbody>
</table>
</div>
<table class="browse-table" id="browseTable">
<thead>
<tr>
<th data-sort="name" class="col-name sortable">Name <span class="sort-arrow"></span></th>
<th data-sort="size" class="col-size sortable">Size <span class="sort-arrow"></span></th>
<th data-sort="ext" class="col-ext sortable">Type <span class="sort-arrow"></span></th>
<th data-sort="date" class="col-date sortable">Modified <span class="sort-arrow"></span></th>
</tr>
</thead>
<tbody id="browseTbody"></tbody>
</table>
</div>
</main>
@ -1704,93 +1684,34 @@ 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) {
await loadChildren(n);
if (!n.loaded) return; // load failed
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;
}
}
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 = [];
@ -1813,8 +1734,6 @@ 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;
@ -1915,36 +1834,21 @@ 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) return;
e.preventDefault();
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);
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).
e.preventDefault();
tree.toggleFolder(parseInt(row.dataset.id, 10));
});
}
}

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.13</span>
<span class="build-timestamp">v0.0.12</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.13</span>
<span class="build-timestamp">v0.0.12</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.13</span>
<span class="build-timestamp">v0.0.12</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.13</span>
<span class="build-timestamp">v0.0.12</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.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
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

View file

@ -80,13 +80,6 @@ func ServeDirectory(cfg config.Config, w http.ResponseWriter, r *http.Request) {
return
}
// Vary: Accept is critical — the same URL serves either the JSON
// listing or the embedded browse.html depending on the Accept
// header. Without Vary, browsers/CDNs cache one response and
// serve it for the other Accept value, breaking browse.html's
// auto-detect (which fetches the same URL with Accept: JSON).
w.Header().Set("Vary", "Accept")
if strings.Contains(accept, "application/json") {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache")
@ -116,10 +109,6 @@ func ServeDirectory(cfg config.Config, w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("X-ZDDC-Source", "embedded:browse")
// no-cache here too — browse.html has session-tied content (the
// directory listing it loads via fetch), and we want browser to
// always re-validate so deployed-binary updates appear immediately
// rather than after a 5-minute cache window.
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Cache-Control", "public, max-age=300, must-revalidate")
_, _ = w.Write(body)
}

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.13</span>
<span class="build-timestamp">v0.0.12</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>