fix(browse): dblclick rescopes client-side instead of hard nav

Hard navigation to /<path>/ lets zddc-server's auto-serve kick in for
canonical folders — /archive/ swaps in the archive tool, /staging/ the
transmittal tool, etc. — so double-clicking a canonical folder from
inside browse silently swapped the user out of browse, contrary to
what they expected.

Fix: client-side rescope. navigateIntoFolder() now fetches the new
directory listing via the loader, calls tree.setRoot() with virtual
canonicals re-applied, and pushes the new URL via history.pushState.
The page never reloads. A subsequent reload still works (browse loads
itself at the new URL since trailing-slash → ServeDirectory → embedded
browse SPA).

Side effects:
  - augmentRoot (the canonical-folder injection helper) exposed via
    window.app.modules so events.js can re-apply it on rescope.
  - popstate handler: back/forward in the browser triggers the same
    rescope path with the historic URL.
  - Selection + preview reset on rescope; the previous file's preview
    isn't carried into the new scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-11 11:45:14 -05:00
parent cb2cf1ebe3
commit dbb7f6c9b5
2 changed files with 73 additions and 1 deletions

View file

@ -49,6 +49,9 @@
return augmented; return augmented;
} }
// Expose for events.js's client-side rescope on dblclick.
window.app.modules.augmentRoot = withVirtualCanonicals;
async function bootstrap() { async function bootstrap() {
events.init(); events.init();
@ -67,6 +70,27 @@
+ ' from ' + detected.path); + ' from ' + detected.path);
} }
// Else: empty state stays visible; user can click Select Directory. // Else: empty state stays visible; user can click Select Directory.
// Browser back / forward: client-side rescope when the URL
// changes via popstate. We can't tell server-vs-fs mode from
// popstate alone, so only honor it in server mode.
window.addEventListener('popstate', async function () {
if (window.app.state.source !== 'server') return;
var path = location.pathname;
if (!path.endsWith('/')) path += '/';
try {
var es = await loader.fetchServerChildren(path);
window.app.state.currentPath = path;
window.app.state.selectedId = null;
window.app.state.lastPreviewedNodeId = null;
tree.setRoot(withVirtualCanonicals(es, path));
tree.render();
var previewBody = document.getElementById('previewBody');
if (previewBody) previewBody.innerHTML = '';
var previewTitle = document.getElementById('previewTitle');
if (previewTitle) previewTitle.textContent = 'No file selected';
} catch (_e) { /* swallow — leave the tree as-is */ }
});
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {

View file

@ -311,9 +311,16 @@
async function navigateIntoFolder(node) { async function navigateIntoFolder(node) {
if (state.source === 'server') { if (state.source === 'server') {
// Rescope client-side rather than hard-navigating. A hard
// nav would let zddc-server's auto-serve kick in and swap
// us out of browse for canonical folders (e.g. /archive/
// → archive tool, /staging/ → transmittal). Staying in
// browse is what the user asked for; pushState keeps the
// URL bar accurate so a reload would re-load browse at the
// new scope.
var url = window.app.modules.tree.pathFor(node); var url = window.app.modules.tree.pathFor(node);
if (!url.endsWith('/')) url += '/'; if (!url.endsWith('/')) url += '/';
window.location.assign(url); await rescopeServer(url, node.name);
return; return;
} }
if (state.source === 'fs') { if (state.source === 'fs') {
@ -333,6 +340,47 @@
} }
} }
// Client-side rescope for server mode. Updates the URL via
// history.pushState, fetches the new directory listing, and
// re-renders the tree from scratch. Page DOES NOT reload.
async function rescopeServer(url, displayName) {
var entries;
try {
entries = await loader.fetchServerChildren(url);
} catch (e) {
statusError('Failed to enter ' + displayName + ': ' + (e.message || e));
return;
}
state.currentPath = url;
// Selection / preview belong to the old scope; clear them so
// the new root doesn't carry stale highlight state.
state.selectedId = null;
state.lastPreviewedNodeId = null;
// Inject virtual canonical folders at the new scope if it's a
// project root. (app.js owns this helper; expose via window.app.)
var augment = window.app.modules.augmentRoot;
var rootEntries = (typeof augment === 'function')
? augment(entries, url)
: entries;
tree.setRoot(rootEntries);
tree.render();
// Reset the preview pane so the user sees an "empty selection"
// state at the new scope instead of the previous file.
var previewBody = document.getElementById('previewBody');
if (previewBody) previewBody.innerHTML = '';
var previewTitle = document.getElementById('previewTitle');
if (previewTitle) previewTitle.textContent = 'No file selected';
var previewMeta = document.getElementById('previewMeta');
if (previewMeta) previewMeta.textContent = '';
// pushState so the URL bar reflects the new scope. A real
// reload would re-load browse at this URL (trailing slash →
// ServeDirectory → embedded browse SPA).
try {
history.pushState({ zddcBrowse: true, path: url }, '', url);
} catch (_e) { /* private browsing edge cases */ }
statusInfo('Entered ' + displayName);
}
// Public API // Public API
window.app.modules.events = { window.app.modules.events = {
init: init, init: init,