ZDDC/browse/js/app.js
ZDDC 319a3c0ce7 fix(browse): dblclick navigates; show virtual canonical folders at project root
#5 — Double-click on a folder no longer toggles collapse.

Root cause: the single-click handler called tree.render() immediately,
which replaced the clicked row element. The browser's double-click
detection requires the second click to land on the SAME target as the
first, so dblclick never fired for folders.

Fix: defer the single-click toggle by 220ms. A pending dblclick within
the window cancels the toggle and runs navigateIntoFolder instead.
Modifier-clicks (shift/alt for recursive) and ZIP expands skip the
deferral — they're never followed by a dblclick navigation.

#3 — Browse at /<project>/ now always shows the four canonical
folders (archive, working, staging, reviewing) even when they don't
yet exist on disk. Each missing folder is synthesized client-side as
a "virtual" row: muted icon + label + "(empty)" hint, double-clickable
to navigate. zddc-server already serves an empty listing for these
paths (commit 3fc3717), so navigation into a virtual folder works
without 404 and the user lands in a sensible empty workspace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:23:14 -05:00

77 lines
3 KiB
JavaScript

// app.js — bootstrap. Runs after every other module's IIFE has
// registered its functions on window.app.modules.
(function () {
'use strict';
var state = window.app.state;
var loader = window.app.modules.loader;
var tree = window.app.modules.tree;
var events = window.app.modules.events;
// Canonical folders that should appear at the root of a project
// view even if they don't yet exist on disk. Matches the four
// stage cards on the project landing page. zddc-server returns an
// empty listing for these paths (see commit 3fc3717), so
// navigating into a virtual folder works without 404.
var CANONICAL_PROJECT_FOLDERS = ['archive', 'working', 'staging', 'reviewing'];
// Decide whether `path` looks like a project root — i.e. exactly
// one path segment after the leading slash. /Project-1/ → yes;
// / → no; /Project-1/working/ → no.
function isProjectRoot(path) {
if (!path || path === '/') return false;
var trimmed = path.replace(/^\/+|\/+$/g, '');
if (!trimmed) return false;
return trimmed.indexOf('/') < 0;
}
// Merge virtual entries for any canonical folders absent from the
// server's listing. Each virtual entry is shaped like a normal
// directory entry so the tree renderer treats it the same way.
function withVirtualCanonicals(entries, path) {
if (!isProjectRoot(path)) return entries;
var present = Object.create(null);
entries.forEach(function (e) { if (e.isDir) present[e.name] = true; });
var augmented = entries.slice();
CANONICAL_PROJECT_FOLDERS.forEach(function (name) {
if (!present[name]) {
augmented.push({
name: name,
isDir: true,
size: 0,
modTime: null,
ext: '',
url: path.replace(/\/$/, '') + '/' + name + '/',
virtual: true
});
}
});
return augmented;
}
async function bootstrap() {
events.init();
// Try server auto-detect. If this page is served by zddc-server
// (or any server with a Caddy-shaped JSON listing), load the
// current directory automatically. Otherwise show the empty
// state with the "Select Directory" button.
var detected = await loader.autoDetectServerMode();
if (detected) {
var entries = withVirtualCanonicals(detected.entries, detected.path);
tree.setRoot(entries);
events.showBrowseRoot();
tree.render();
events.statusInfo('Loaded ' + detected.entries.length + ' item'
+ (detected.entries.length === 1 ? '' : 's')
+ ' from ' + detected.path);
}
// Else: empty state stays visible; user can click Select Directory.
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {
bootstrap();
}
})();