diff --git a/browse/js/events.js b/browse/js/events.js
index 05beaed..0942fc6 100644
--- a/browse/js/events.js
+++ b/browse/js/events.js
@@ -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();
diff --git a/browse/js/loader.js b/browse/js/loader.js
index d5241d5..06f60bc 100644
--- a/browse/js/loader.js
+++ b/browse/js/loader.js
@@ -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
};
diff --git a/browse/js/preview.js b/browse/js/preview.js
index 8f767ea..cec602f 100644
--- a/browse/js/preview.js
+++ b/browse/js/preview.js
@@ -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
/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).
diff --git a/browse/js/tree.js b/browse/js/tree.js
index b8e9fdc..2c6d026 100644
--- a/browse/js/tree.js
+++ b/browse/js/tree.js
@@ -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">'
diff --git a/browse/js/util.js b/browse/js/util.js
index 38060ab..3d1fec3 100644
--- a/browse/js/util.js
+++ b/browse/js/util.js
@@ -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
diff --git a/zddc/internal/fs/tree.go b/zddc/internal/fs/tree.go
index 747e496..2d9f697 100644
--- a/zddc/internal/fs/tree.go
+++ b/zddc/internal/fs/tree.go
@@ -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
diff --git a/zddc/internal/listing/types.go b/zddc/internal/listing/types.go
index ee11e26..0429ee8 100644
--- a/zddc/internal/listing/types.go
+++ b/zddc/internal/listing/types.go
@@ -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 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"`
}