Improve file viewer browsing layout

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-09 21:35:48 +00:00
1 parent ce22ad7632
commit f2b869ecdc
7 files changed
+74 -27

No files matched your search

+1 -1
View File
@@ -50,7 +50,7 @@ export function ArtifactFileChip({
const content = (
<>
{showIcon ? <FileCode2 aria-hidden="true" className="h-3 w-3 shrink-0 opacity-70" /> : null}
<span className="truncate max-w-[38ch]">{display}</span>
<span className="max-w-full whitespace-normal break-all text-left">{display}</span>
</>
);
@@ -75,6 +75,21 @@ describe("FileContentViewer", () => {
expect(markup).toContain("break-words");
});
it("reserves code gutter space for at least four digit line numbers", () => {
const markup = renderToStaticMarkup(
<FileContentViewer
content={content({
content: {
encoding: "utf8",
data: "one\ntwo\nthree",
},
})}
highlightedLine={null}
/>,
);
expect(markup).toContain("calc(4ch + 2rem)");
});
it("renders video previews with native controls", () => {
const markup = renderToStaticMarkup(
<FileContentViewer
+46 -24
View File
@@ -94,6 +94,11 @@ function basename(path: string): string {
return idx < 0 ? path : path.slice(idx + 1);
}
function dirname(path: string): string | null {
const idx = path.lastIndexOf("/");
return idx < 0 ? null : path.slice(0, idx);
}
function middleTruncatePath(path: string, maxLen = 80): string {
if (path.length <= maxLen) return path;
const head = path.slice(0, Math.floor(maxLen / 2) - 1);
@@ -267,7 +272,7 @@ export function FileContentViewer({ content, highlightedLine, onLoaded }: FileCo
);
}
const gutterWidth = `${Math.max(2, String(lines.length).length)}ch`;
const gutterWidth = `calc(${Math.max(4, String(lines.length).length)}ch + 2rem)`;
return (
<div
@@ -299,7 +304,7 @@ export function FileContentViewer({ content, highlightedLine, onLoaded }: FileCo
isHighlighted &&
"opacity-100 bg-[var(--paperclip-code-highlight-bg,rgba(250,204,21,0.12))] border-l-2 border-[var(--paperclip-code-highlight-border,rgb(234,179,8))]",
)}
style={{ width: gutterWidth }}
style={{ width: gutterWidth, minWidth: gutterWidth }}
>
{lineNumber}
</span>
@@ -626,28 +631,45 @@ export function FileViewerSheet({
{announcement}
</div>
{state ? (
<FileViewerBody
resolveQuery={resolveQuery}
contentQuery={contentQuery}
elapsedMs={elapsedMs}
canPreview={canPreview}
highlightedLine={state.line ?? null}
onRetry={handleRetry}
onSetAnnouncement={setAnnouncement}
onFallbackToProject={
state.workspace !== "project" && !state.projectId && !state.workspaceId
? () =>
viewer.open({
path: state.path,
line: state.line,
column: state.column,
workspace: "project",
projectId: null,
workspaceId: null,
})
: null
}
/>
<div className="flex min-h-0 flex-1">
<aside className="hidden min-h-0 w-80 shrink-0 border-r border-border bg-muted/20 sm:flex">
<WorkspaceFileBrowser
key={`${state.projectId ?? ""}:${state.workspaceId ?? ""}:${dirname(state.path) ?? ""}`}
issueId={issueId}
companyId={companyId}
onOpen={handleBrowseOpen}
initialFolderPath={dirname(state.path)}
initialProjectId={state.projectId}
initialWorkspaceId={state.workspaceId}
autoFocusSearch={false}
className="min-h-0 flex-1 p-3"
/>
</aside>
<div className="flex min-w-0 flex-1 flex-col">
<FileViewerBody
resolveQuery={resolveQuery}
contentQuery={contentQuery}
elapsedMs={elapsedMs}
canPreview={canPreview}
highlightedLine={state.line ?? null}
onRetry={handleRetry}
onSetAnnouncement={setAnnouncement}
onFallbackToProject={
state.workspace !== "project" && !state.projectId && !state.workspaceId
? () =>
viewer.open({
path: state.path,
line: state.line,
column: state.column,
workspace: "project",
projectId: null,
workspaceId: null,
})
: null
}
/>
</div>
</div>
) : browseMode ? (
<WorkspaceFileBrowser
issueId={issueId}
+2
View File
@@ -282,6 +282,8 @@ describe("MarkdownBody", () => {
expect(html).toContain('data-workspace-file-link="true"');
expect(html).toContain('data-workspace-file-path="videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4"');
expect(html).toContain("videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4");
expect(html).not.toContain("max-w-[38ch]");
expect(html).not.toContain("paperclip-markdown-issue-ref");
expect(html).not.toContain('href="/issues/PAP-10306"');
});
@@ -198,6 +198,12 @@ describe("WorkspaceFileBrowser", () => {
expect(onOpen).toHaveBeenCalledWith({ path: "ui/src/pages/IssueDetail.tsx", workspace: "auto" });
});
it("can render without autofocus when embedded beside a preview", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem()])));
renderBrowser(vi.fn(), { autoFocusSearch: false });
expect(container.querySelector("input")?.hasAttribute("autofocus")).toBe(false);
});
it("does not render a Recent/All toggle", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem()])));
renderBrowser();
+3 -1
View File
@@ -219,6 +219,7 @@ export interface WorkspaceFileBrowserProps {
initialFolderPath?: string | null;
initialProjectId?: string | null;
initialWorkspaceId?: string | null;
autoFocusSearch?: boolean;
className?: string;
}
@@ -230,6 +231,7 @@ export function WorkspaceFileBrowser({
initialFolderPath,
initialProjectId,
initialWorkspaceId,
autoFocusSearch = true,
className,
}: WorkspaceFileBrowserProps) {
const [source, setSource] = useState<BrowserSource>(
@@ -512,7 +514,7 @@ export function WorkspaceFileBrowser({
aria-expanded={items.length > 0}
aria-controls={items.length > 0 ? listboxId : undefined}
aria-activedescendant={activeOptionId}
autoFocus
autoFocus={autoFocusSearch}
autoComplete="off"
spellCheck={false}
className="h-8 pl-8 font-mono text-xs"
+1 -1
View File
@@ -93,7 +93,7 @@ export function WorkspaceFileLink({
? <FolderOpen aria-hidden="true" className="h-3 w-3 shrink-0 opacity-70" />
: <FileCode2 aria-hidden="true" className="h-3 w-3 shrink-0 opacity-70" />
) : null}
<span className="truncate max-w-[38ch]">{display}</span>
<span className="max-w-full whitespace-normal break-all text-left">{display}</span>
</a>
);
}