feat(browse): render default_tool=tables dirs (mdl/rsk/ssr) as click-to-table leaves

A directory whose cascade default_tool is "tables" (mdl/rsk/ssr and any
operator-configured table dir) now shows in the browse tree as a leaf with
a table icon — no expand chevron — and clicking it opens the tables tool in
the preview pane (an iframe scoped to the dir, mirroring grid.js's
classifier embed) instead of expanding/navigating into the folder.

Detection rides the cascade, not hardcoded names: the directory listing now
carries a per-entry default_tool hint (listing.FileInfo.DefaultTool, set via
zddc.DefaultToolAt for both on-disk children and the virtual canonical peers
mdl/rsk/ssr). Browse's util.isTableLeaf(node) keys off it; tree.js renders
the leaf, events.js routes its click/Enter to the preview (excluding it from
expand/navigate), and preview.js renders the iframe at the dir's NO-SLASH
URL (the default_tool route — <dir>/tables.html 404s for a virtual dir).

Server mode only (the hint is absent on file://, so offline folders stay
ordinary expandable dirs). Validated end-to-end in a containerized browser:
mdl/rsk/ssr are leaves, normal folders keep their chevrons, and clicking mdl
loads the tables view inline without navigating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 17:18:47 -05:00
parent 70591dcfa6
commit 9b20e4451f
7 changed files with 90 additions and 5 deletions

View file

@ -353,7 +353,10 @@
var node = state.nodes.get(id);
if (!node) return;
var isExpandable = row.dataset.isdir === 'true' || row.dataset.iszip === 'true';
// Table-leaf dirs (mdl/rsk/ssr) are NOT expandable — they fall
// through to the preview path, which opens the tables tool.
var isExpandable = (row.dataset.isdir === 'true' || row.dataset.iszip === 'true')
&& row.dataset.tableleaf !== 'true';
if (isExpandable) {
e.preventDefault();
@ -419,6 +422,7 @@
var row = e.target.closest('.tree-row');
if (!row) return;
if (row.dataset.isdir !== 'true') return;
if (row.dataset.tableleaf === 'true') return; // leaf: single-click previews
var id = parseInt(row.dataset.id, 10);
var node = state.nodes.get(id);
if (!node) return;
@ -490,7 +494,10 @@
var curIdx = visible.indexOf(state.selectedId);
var node = state.selectedId != null
? state.nodes.get(state.selectedId) : null;
var expandable = !!(node && (node.isDir || node.isZip));
// Table-leaf dirs aren't expandable: Enter/Space previews them
// (opens the table) rather than toggling.
var expandable = !!(node && (node.isDir || node.isZip)
&& !window.app.modules.util.isTableLeaf(node));
var nextId = null;
var previewModule = previewMod();

View file

@ -68,6 +68,11 @@
// context-menu affordance (server mode only — offline has no
// authenticated identity to attribute saves to).
history: !!e.history,
// Server-computed: cascade-resolved default tool for a DIRECTORY
// entry (e.g. "tables", "classifier"). Browse renders a dir whose
// defaultTool=="tables" (mdl/rsk/ssr) as a click-to-table leaf —
// the table opens in the preview pane instead of the dir expanding.
defaultTool: (typeof e.default_tool === 'string') ? e.default_tool : '',
// FS-API specific (null in server mode):
handle: null
};

View file

@ -469,12 +469,47 @@
// ── Public entry ────────────────────────────────────────────────────────
async function showFilePreview(node, opts) {
if (node.isDir) return;
opts = opts || {};
// Table-leaf dirs (mdl/rsk/ssr, default_tool=tables) open the tables
// tool inline in the preview pane instead of expanding/navigating.
if (window.app.modules.util.isTableLeaf(node)) return renderTableLeaf(node);
if (node.isDir) return;
if (opts.popup) return renderInPopup(node);
return renderInline(node, opts);
}
// renderTableLeaf embeds the tables tool for a default_tool=tables
// directory as an iframe scoped to that dir — the same in-pane tool
// embed pattern grid.js uses for classifier. Server mode only (the
// default_tool listing hint that flags a table-leaf is absent offline,
// so this never fires on file:// — the dir stays an ordinary folder).
function renderTableLeaf(node) {
disposeEditors();
var container = document.getElementById('previewBody');
var titleEl = document.getElementById('previewTitle');
var metaEl = document.getElementById('previewMeta');
var popoutBtn = document.getElementById('previewPopout');
if (!container) return;
if (titleEl) titleEl.textContent = node.displayName || node.name;
if (metaEl) metaEl.textContent = 'table';
if (popoutBtn) popoutBtn.classList.add('hidden');
if (window.app.state.source !== 'server' || !node.url) {
renderEmpty(container, 'Table view is available in server mode.');
return;
}
// The tables tool is served at the dir's NO-SLASH URL (the cascade's
// default_tool routing). The trailing-slash form would serve the
// browse listing instead, and <dir>/tables.html 404s for a virtual
// dir (mdl/rsk/ssr have no on-disk folder). So strip the slash.
var src = node.url.replace(/\/+$/, '');
container.innerHTML = '';
var frame = document.createElement('iframe');
frame.className = 'preview-iframe';
frame.src = src;
frame.setAttribute('title', 'Table: ' + (node.displayName || node.name));
container.appendChild(frame);
}
window.app.modules.preview = {
showFilePreview: showFilePreview,
// Tear down any live editor + blank the pane (rescope / popstate).

View file

@ -58,7 +58,12 @@
// undefined — Caddy / FS-API listings (no verbs field).
// Per-entry gates skip the cascade check
// and fall back to canMutate / writable.
verbs: typeof raw.verbs === 'string' ? raw.verbs : undefined
verbs: typeof raw.verbs === 'string' ? raw.verbs : undefined,
// Cascade default tool for a directory entry. When "tables"
// (mdl/rsk/ssr), the node is a TABLE LEAF: rendered without a
// chevron and, on click, opens the tables tool in the preview
// pane instead of expanding/navigating. See isTableLeaf().
defaultTool: raw.defaultTool || ''
};
state.nodes.set(id, node);
return node;
@ -275,6 +280,8 @@
};
function symbolForNode(node) {
// Table-leaf dirs (mdl/rsk/ssr) read as a table, not a folder.
if (window.app.modules.util.isTableLeaf(node)) return 'icon-file-spreadsheet';
if (node.isDir) return 'icon-folder';
if (node.isZip) return 'icon-folder-archive';
// `.zddc` (no extension) is the cascade config — same family
@ -351,7 +358,10 @@
// via the events.js click handler (it sees the modifier key).
function rowHtml(node) {
var indent = 0.4 + node.depth * 1.0;
var expandable = node.isDir || node.isZip;
// Table-leaf dirs render like a file: no chevron, click opens the
// table in the preview pane (handled by events.js / preview.js).
var tableLeaf = window.app.modules.util.isTableLeaf(node);
var expandable = (node.isDir || node.isZip) && !tableLeaf;
var iconChar = iconForNode(node);
var chevronClass = 'tree-name__chevron'
+ (expandable ? '' : ' tree-name__chevron--leaf');
@ -385,6 +395,7 @@
+ '" data-id="' + node.id
+ '" data-isdir="' + node.isDir
+ '" data-iszip="' + node.isZip + '"'
+ (tableLeaf ? ' data-tableleaf="true"' : '')
+ (node.virtual ? ' data-virtual="true"' : '')
+ ' style="padding-left:' + indent + 'rem"'
+ ' role="treeitem" tabindex="-1">'

View file

@ -197,6 +197,17 @@
return name;
}
// isTableLeaf reports whether a directory node should behave as a
// click-to-table LEAF rather than an expandable folder — i.e. the
// cascade resolved its default tool to "tables" (mdl/rsk/ssr and any
// operator-configured table dir). The tree renders it without a
// chevron and the preview pane opens the tables tool for it. Server
// mode only: defaultTool is a server-computed listing hint, absent
// offline (file:// folders stay ordinary expandable dirs).
function isTableLeaf(node) {
return !!(node && node.isDir && node.defaultTool === 'tables');
}
window.app.modules.util = {
escapeHtml: escapeHtml,
hashContent: hashContent,
@ -207,6 +218,7 @@
fmtSize: fmtSize,
isZipMemberNode: isZipMemberNode,
isEditableZipMember: isEditableZipMember,
isTableLeaf: isTableLeaf,
saveFile: saveFile,
saveCopy: saveCopy,
ConflictError: ConflictError

View file

@ -170,6 +170,10 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
Declared: declared,
Title: title,
Verbs: subVerbs.String(),
// Cascade-resolved default tool for this child dir, so the
// browse client can render a tool-typed dir (e.g. tables) as
// a click-to-open leaf without re-walking the cascade.
DefaultTool: zddc.DefaultToolAt(fsRoot, subAbs),
}
result = append(result, fi)
continue
@ -384,6 +388,9 @@ func virtualCanonicalFolders(ctx context.Context, decider policy.Decider, fsRoot
DisplayName: lookupDisplay(displayMap, name),
Declared: true, // synthesized entries are by definition cascade-declared
Verbs: verbs.String(),
// Cascade default tool for this virtual peer — mdl/rsk/ssr resolve
// to "tables", which browse renders as a click-to-table leaf.
DefaultTool: zddc.DefaultToolAt(fsRoot, childAbs),
})
}
return synth

View file

@ -94,4 +94,12 @@ type FileInfo struct {
// where they apply. False/absent for directories, virtual entries,
// and files outside a history-enabled subtree.
History bool `json:"history,omitempty"`
// DefaultTool is the cascade-resolved default tool for a DIRECTORY
// entry (the tool served at <dir> with no trailing slash) — e.g.
// "tables", "classifier", "browse". Empty for files and for dirs with
// no declared default. Browse uses it to render a tool-typed dir as a
// leaf that opens the tool in the preview pane instead of expanding —
// e.g. mdl/rsk/ssr (default_tool=tables) become click-to-table entries.
DefaultTool string `json:"default_tool,omitempty"`
}