feat(dashboard): Files view two-pane when expanded, single-panel stack in dock

DockFilesView uses a container query: narrow (dock) keeps the single-panel tree<->viewer navigation stack; wide (pop-out expand modal) shows the file tree and viewer side by side with an empty 'Select a file' state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-22 02:26:25 -07:00
parent 8ab8cd1526
commit 8637ff96fa
2 changed files with 123 additions and 24 deletions

View File

@@ -2,18 +2,52 @@
FNXC:RightDockFiles 2026-06-22-00:00:
The inline Files viewer fills the right-dock body and scrolls internally so the read-only FileEditor never overflows the dock.
The header is a compact bar: BACK on the left, a truncating file name in the middle, POP-OUT on the right.
FNXC:Files 2026-06-22-00:00:
Responsive single-panel vs two-pane layout driven entirely by a CSS container query.
The root is a query container (container-type: inline-size, container-name: dock-files). Both the tree pane (.dock-files-view__tree) and the viewer pane (.dock-files-view__viewer) are always rendered in the DOM; CSS decides visibility per container width.
- NARROW (default, in the dock): single-panel stack. The tree fills the root. When a file is selected (root [data-selected="true"]) the viewer pane covers the stack and the tree is hidden; BACK returns to the tree.
- WIDE (>=720px, the RightDockExpandModal pop-out): two-pane side-by-side. Tree pinned left (clamped width, scrollable), viewer flex:1 on the right (scrollable). Both always visible regardless of data-selected; BACK is hidden because the tree never disappears.
*/
.dock-files-view {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
/* FNXC:Files — establish the query container so child panes can respond to the dock vs expand-modal width. */
container-type: inline-size;
container-name: dock-files;
}
.dock-files-view--viewer {
/* FNXC:Files — NARROW default: tree fills the root as the single panel. */
.dock-files-view__tree {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1 1 auto;
overflow: hidden;
}
/*
FNXC:Files — NARROW default: viewer is the stacked second panel.
Hidden until a file is selected; when selected it overlays the tree as the single visible panel (the tree is hidden below).
*/
.dock-files-view__viewer {
display: none;
flex-direction: column;
min-height: 0;
flex: 1 1 auto;
overflow: hidden;
}
.dock-files-view[data-selected="true"] .dock-files-view__tree {
display: none;
}
.dock-files-view[data-selected="true"] .dock-files-view__viewer {
display: flex;
}
.dock-files-viewer__header {
display: flex;
align-items: center;
@@ -61,3 +95,50 @@ The header is a compact bar: BACK on the left, a truncating file name in the mid
.dock-files-viewer__status--error {
color: var(--danger, var(--text));
}
/* FNXC:Files — empty-state placeholder shown in the wide right pane until a file is selected. */
.dock-files-viewer__empty {
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 auto;
text-align: center;
}
/*
FNXC:Files 2026-06-22-00:00:
WIDE container (>=720px): two-pane side-by-side. Activated when DockFilesView is rendered in the wide RightDockExpandModal.
Both panes are always visible; data-selected no longer toggles visibility here.
*/
@container dock-files (min-width: 720px) {
.dock-files-view {
flex-direction: row;
}
/* Tree pinned left: clamped, scrollable, with a divider against the viewer. */
.dock-files-view__tree {
display: flex;
flex: 0 0 clamp(220px, 32%, 360px);
min-width: 0;
overflow: auto;
border-right: 1px solid var(--border);
}
/* Viewer fills the remaining width; always visible (empty-state until a file is selected). */
.dock-files-view__viewer,
.dock-files-view[data-selected="true"] .dock-files-view__viewer {
display: flex;
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
}
.dock-files-view[data-selected="true"] .dock-files-view__tree {
display: flex;
}
/* BACK is meaningless when the tree is always visible. */
.dock-files-view__viewer .dock-files-viewer__back {
display: none;
}
}

View File

@@ -19,6 +19,11 @@ FNXC:RightDockFiles 2026-06-22-00:00:
The right-dock Files tool opens a clicked file INLINE inside the dock as a read-only viewer instead of immediately launching the resizable/movable FileBrowserModal.
Clicking a file in the tree sets local `selectedFile` (it does NOT call `openFile`); the inline viewer reuses the read-only `FileEditor` so markdown previews and syntax highlighting match the rest of the app.
The viewer header carries a BACK button (clears `selectedFile`, returning to the tree) and a POP-OUT button that calls `openFile(path, { workspace: "project" })` to escalate to the existing resizable/movable modal. This preserves the modal path; it is now opt-in via pop-out rather than the default click behavior.
FNXC:Files 2026-06-22-00:00:
Responsive layout via CSS container query. The root is a query container (container-type: inline-size, container-name: dock-files). BOTH the tree pane and the viewer pane are always rendered in the DOM; CSS decides what is visible per container width.
- NARROW (dock, default): single-panel stack. Tree shows alone; selecting a file reveals the viewer pane which overlays the stack, and the BACK button returns to the tree. This preserves the prior navigation-stack UX.
- WIDE (>=720px, the RightDockExpandModal pop-out): two-pane side-by-side. Left pane = tree (always visible, clamped width, scrollable). Right pane = viewer (flex:1, scrollable) showing an empty-state until a file is selected. Selecting a file updates the right pane without hiding the tree, so the BACK button is hidden when wide.
*/
export function DockFilesView({ projectId, openFile }: DockFilesViewProps) {
const { t } = useTranslation("app");
@@ -66,11 +71,36 @@ export function DockFilesView({ projectId, openFile }: DockFilesViewProps) {
if (selectedFile) openFile?.(selectedFile, { workspace: "project" });
}, [openFile, selectedFile]);
if (selectedFile) {
const fileName = selectedFile.split("/").pop() || selectedFile;
return (
<div className="dock-files-view dock-files-view--viewer" data-testid="right-dock-files-view">
const fileName = selectedFile ? selectedFile.split("/").pop() || selectedFile : "";
// FNXC:Files 2026-06-22-00:00:
// `data-selected` on the root lets the container query distinguish "no file selected" (narrow: viewer pane hidden so only the tree shows) from "file selected" (narrow: viewer pane covers the stack). When wide both panes are always visible regardless of this flag.
return (
<div
className="dock-files-view"
data-testid="right-dock-files-view"
data-selected={selectedFile ? "true" : "false"}
>
{/* FNXC:Files — left pane: tree. Always in the DOM; CSS hides it only in the narrow single-panel stack when a file is selected. */}
<div className="dock-files-view__tree" data-testid="right-dock-files-tree">
<FileBrowser
entries={entries}
currentPath={currentPath}
onSelectFile={(path) => setSelectedFile(path)}
onNavigate={setPath}
loading={loading}
error={error}
onRetry={refresh}
workspace="project"
onRefresh={refresh}
projectId={projectId}
/>
</div>
{/* FNXC:Files — right pane: viewer. Always in the DOM; CSS shows it side-by-side when wide, or as the single-panel stack when narrow + a file is selected. */}
<div className="dock-files-view__viewer" data-testid="right-dock-files-viewer">
<div className="dock-files-viewer__header">
{/* FNXC:Files — BACK only matters in the narrow stack (returns to the tree); CSS hides it when wide since the tree is always visible. */}
<button
type="button"
className="btn btn-sm btn-icon dock-files-viewer__back"
@@ -81,11 +111,12 @@ export function DockFilesView({ projectId, openFile }: DockFilesViewProps) {
>
<ArrowLeft size={14} />
</button>
<span className="dock-files-viewer__title" title={selectedFile}>{fileName}</span>
<span className="dock-files-viewer__title" title={selectedFile ?? undefined}>{fileName}</span>
<button
type="button"
className="btn btn-sm btn-icon dock-files-viewer__popout"
onClick={handlePopOut}
disabled={!selectedFile}
aria-label={t("fileViewer.popOut", "Open in resizable window")}
title={t("fileViewer.popOut", "Open in resizable window")}
data-testid="right-dock-files-popout"
@@ -94,7 +125,11 @@ export function DockFilesView({ projectId, openFile }: DockFilesViewProps) {
</button>
</div>
<div className="dock-files-viewer__body">
{contentLoading ? (
{!selectedFile ? (
<div className="dock-files-viewer__status dock-files-viewer__empty" data-testid="right-dock-files-empty">
{t("fileViewer.selectAFile", "Select a file")}
</div>
) : contentLoading ? (
<div className="dock-files-viewer__status">{t("common.loading", "Loading...")}</div>
) : contentError ? (
<div className="dock-files-viewer__status dock-files-viewer__status--error">{contentError}</div>
@@ -103,23 +138,6 @@ export function DockFilesView({ projectId, openFile }: DockFilesViewProps) {
)}
</div>
</div>
);
}
return (
<div className="dock-files-view" data-testid="right-dock-files-view">
<FileBrowser
entries={entries}
currentPath={currentPath}
onSelectFile={(path) => setSelectedFile(path)}
onNavigate={setPath}
loading={loading}
error={error}
onRetry={refresh}
workspace="project"
onRefresh={refresh}
projectId={projectId}
/>
</div>
);
}