Polish file viewer UX review punchlist (PAP-1962)

Addresses UX review feedback on PAP-1963:

- Fix denial-body leak for too_large/size codes (describeDenial no longer
  echoes fallback for known codes; the call site passes empty fallback so
  unknown codes show a generic message).
- Sticky line-number gutter with solid code-bg fill; highlight border
  moves from absolute-positioned sibling onto the sticky gutter so the
  amber marker stays pinned when users scroll long lines horizontally.
- Swap FileSearch copy-link icon for Link2.
- WorkspaceFileLink now sets a real deep-link href so modifier-click
  opens the file viewer in a new tab instead of scrolling to the top.
- Collapse duplicate title/description in the sheet header when the
  basename equals the full path (e.g. .env.production).
- Drop the duplicate title/helper block from OpenFilePrompt (sheet
  header already carries the title + description).
- Replace ArrowLeft with Cloud for the remote-workspace state.
- Retire arbitrary text-[12.5px]/[11px]/[0.78em] sizes in favor of text-xs.
- Unify 404 copy on "active workspace" everywhere.
- Raise chip-row contrast by using bg-muted instead of bg-background.

Also commits previously untracked visual-regression infra:
- Storybook stories for 12 file viewer states
- screenshot-file-viewer.mjs / screenshot-one.mjs
- describeDenial unit tests

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-09 21:35:48 +00:00
1 parent 4bad0cf17f
commit 9b58daba6e
8 files changed
+710 -39

No files matched your search

+19 -3
View File
@@ -1,9 +1,13 @@
import type { MouseEvent, ReactNode } from "react";
import { FileCode2 } from "lucide-react";
import { useLocation } from "@/lib/router";
import { cn } from "@/lib/utils";
import type { ParsedWorkspaceFileRef } from "@/lib/workspace-file-parser";
import { formatWorkspaceFileRefDisplay } from "@/lib/workspace-file-parser";
import { useFileViewer } from "@/context/FileViewerContext";
import {
useFileViewer,
writeFileViewerStateToSearch,
} from "@/context/FileViewerContext";
export interface WorkspaceFileLinkProps {
workspaceFileRef: ParsedWorkspaceFileRef;
@@ -25,6 +29,7 @@ export function WorkspaceFileLink({
title,
}: WorkspaceFileLinkProps) {
const viewer = useFileViewer();
const location = useLocation();
const display = typeof label !== "undefined" ? label : formatWorkspaceFileRefDisplay(workspaceFileRef);
const canOpen = !!(onOpen || viewer);
const lineSuffix = workspaceFileRef.line
@@ -37,10 +42,21 @@ export function WorkspaceFileLink({
? `Open ${workspaceFileRef.path}${lineSuffix} in the file viewer`
: `Workspace file ${workspaceFileRef.path}${lineSuffix}`);
const deepLinkSearch = writeFileViewerStateToSearch(location.search, {
path: workspaceFileRef.path,
line: workspaceFileRef.line ?? null,
column: workspaceFileRef.column ?? null,
workspace: "auto",
});
const href = canOpen
? `${location.pathname}${deepLinkSearch}${location.hash}`
: "#";
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
return;
}
if (event.button !== 0) return;
if (!canOpen) return;
event.preventDefault();
if (onOpen) onOpen(workspaceFileRef);
@@ -49,14 +65,14 @@ export function WorkspaceFileLink({
return (
<a
href="#"
href={href}
role={canOpen ? "button" : undefined}
data-workspace-file-link="true"
data-workspace-file-path={workspaceFileRef.path}
aria-label={ariaLabel}
title={tooltip}
className={cn(
"paperclip-workspace-file-link inline-flex items-center gap-1 rounded-sm border border-border bg-muted/60 px-1.5 py-0.5 font-mono text-[0.78em] leading-tight text-foreground/90 align-baseline no-underline hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
"paperclip-workspace-file-link inline-flex items-center gap-1 rounded-sm border border-border bg-muted/60 px-1.5 py-0.5 font-mono text-xs leading-tight text-foreground/90 align-baseline no-underline hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
className,
)}
onClick={handleClick}