From 9b20e4451f796066b1d0fa62baca3a194e693951 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Fri, 5 Jun 2026 17:18:47 -0500 Subject: [PATCH] feat(browse): render default_tool=tables dirs (mdl/rsk/ssr) as click-to-table leaves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 — /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) --- browse/js/events.js | 11 ++++++++-- browse/js/loader.js | 5 +++++ browse/js/preview.js | 37 +++++++++++++++++++++++++++++++++- browse/js/tree.js | 15 ++++++++++++-- browse/js/util.js | 12 +++++++++++ zddc/internal/fs/tree.go | 7 +++++++ zddc/internal/listing/types.go | 8 ++++++++ 7 files changed, 90 insertions(+), 5 deletions(-) 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"` }