Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,12 @@ such detail into a token. New layout spacing must use the scale.
each tint with Pierre's existing background instead of replacing it.
- Navigation uses a quiet background, aligned text/icons, and a clear current
item. Reviewed files retain readable text and a check; do not fade the entire row.
- “Hide reviewed files” is an icon toggle beside the file filter, matching the
field's height and `--border-muted` boundary; pressed uses the accent toggle
state. `useFileNavigation` owns one visible file set for the tree, diff list
and Alt+J/K. Marking the file in view hides it at once and moves selection to
its next visible neighbor. Empty results say “All files reviewed” with a
“Show reviewed files” action; comment navigation to a hidden file shows them.
- Navigating from the file list briefly pulses the destination file header with
the accent for about one second. Reduced-motion mode uses static feedback that
remains visible long enough to identify the destination without animation.
Expand Down
54 changes: 37 additions & 17 deletions apps/web/src/DiffWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export function DiffWorkspace() {
selectVisibleFile,
filter,
setFilter,
matchingFiles,
setHideReviewed,
navigationTarget,
commentNavigationTarget,
navigateComment,
Expand Down Expand Up @@ -629,24 +631,32 @@ export function DiffWorkspace() {
);

const loadingChanges = !repository && !diff.notice;
// Matching files with none visible were all hidden as reviewed.
const allReviewedHidden = matchingFiles.length > 0;
const emptyTitle = !repository
? "Cannot load changes"
: filter
? "No matching files"
: piped
? "No file changes in this input"
: mode === "staged"
? "Nothing staged"
: "No changes in this snapshot";
: allReviewedHidden
? filter
? "All matching files reviewed"
: "All files reviewed"
: filter
? "No matching files"
: piped
? "No file changes in this input"
: mode === "staged"
? "Nothing staged"
: "No changes in this snapshot";
const emptyDescription = !repository
? "Check that the server is running, then refresh."
: filter
? "Try a different filename or clear the filter."
: piped
? "Run a command that emits a Git patch, then pipe it into diffx."
: mode === "staged"
? "No staged changes were collected in this snapshot."
: "No changes in this snapshot. Collect another snapshot with diffx or diffx sync.";
: allReviewedHidden
? "Reviewed files are hidden."
: filter
? "Try a different filename or clear the filter."
: piped
? "Run a command that emits a Git patch, then pipe it into diffx."
: mode === "staged"
? "No staged changes were collected in this snapshot."
: "No changes in this snapshot. Collect another snapshot with diffx or diffx sync.";
return (
<main>
<DiffToolbar />
Expand Down Expand Up @@ -809,14 +819,24 @@ export function DiffWorkspace() {
<>
<h2>{emptyTitle}</h2>
<p>{emptyDescription}</p>
{!!filter && (
{allReviewedHidden ? (
<Button
type="button"
variant="outline"
onClick={() => setFilter("")}
onClick={() => setHideReviewed(false)}
>
Clear filter
Show reviewed files
</Button>
) : (
!!filter && (
<Button
type="button"
variant="outline"
onClick={() => setFilter("")}
>
Clear filter
</Button>
)
)}
</>
)}
Expand Down
64 changes: 43 additions & 21 deletions apps/web/src/FileExplorerNav.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useMemo } from "react";
import { XIcon } from "lucide-react";
import { EyeOffIcon, XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Toggle } from "@/components/ui/toggle";
import {
togglePath,
useDiffSource,
Expand All @@ -26,6 +27,9 @@ export function FileExplorerNav() {
setTab,
filter,
setFilter,
hideReviewed,
setHideReviewed,
matchingFiles,
files,
activePath,
closed,
Expand Down Expand Up @@ -162,35 +166,53 @@ export function FileExplorerNav() {
</Button>
</div>
<TabsContent value="files" id="file-panel">
<div className="search-box">
<svg viewBox="0 0 16 16" aria-hidden="true">
<circle cx="7" cy="7" r="4.5" />
<path d="m10.5 10.5 3 3" />
</svg>
<Input
ref={search}
id="search"
type="search"
placeholder="Filter files…"
aria-label="Filter files"
value={filter}
onChange={(event) => {
setFilter(event.target.value);
setFilteredClosed(new Set());
}}
/>
<kbd>Alt+/</kbd>
<div className="file-filter-row">
<div className="search-box">
<svg viewBox="0 0 16 16" aria-hidden="true">
<circle cx="7" cy="7" r="4.5" />
<path d="m10.5 10.5 3 3" />
</svg>
<Input
ref={search}
id="search"
type="search"
placeholder="Filter files…"
aria-label="Filter files"
value={filter}
onChange={(event) => {
setFilter(event.target.value);
setFilteredClosed(new Set());
}}
/>
<kbd>Alt+/</kbd>
</div>
<Toggle
id="hide-reviewed"
className="hide-reviewed-toggle"
aria-label="Hide reviewed files"
title="Hide reviewed files"
pressed={hideReviewed}
onPressedChange={setHideReviewed}
>
<EyeOffIcon aria-hidden="true" />
</Toggle>
</div>
<p className="visually-hidden" role="status" aria-live="polite">
{filter
{filter || hideReviewed
? `${files.length} of ${allFiles.length} changed files shown`
: ""}
</p>
<FileNavigation
files={files}
selected={activePath}
closed={filter ? filteredClosed : closed}
filtering={!!filter}
emptyMessage={
!allFiles.length
? "No changed files"
: matchingFiles.length
? "All files reviewed"
: "No matching files"
}
isReviewed={isReviewed}
commentCounts={commentCounts}
stagingMetadata={stagingMetadataEnabled}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ function WorkspaceProvider({ children }: { children: ReactNode }) {
refresh,
changeMode,
setCollapsed,
isReviewed: reviewed.isReviewed,
setFeedback: review.setFeedback,
showSidebar,
closeMobile,
Expand Down
8 changes: 3 additions & 5 deletions apps/web/src/file-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ interface Props {
files: ChangedFile[];
selected: string;
closed: Set<string>;
filtering: boolean;
emptyMessage: string;
isReviewed: (file: ChangedFile) => boolean;
commentCounts: Map<string, CommentCount>;
stagingMetadata: boolean;
Expand All @@ -105,7 +105,7 @@ export function FileNavigation({
files,
selected,
closed,
filtering,
emptyMessage,
isReviewed,
commentCounts,
stagingMetadata,
Expand Down Expand Up @@ -238,9 +238,7 @@ export function FileNavigation({
{files.length ? (
renderNodes(buildFileTree(files), 0)
) : (
<p className="tree-folder">
{filtering ? "No matching files" : "No changed files"}
</p>
<p className="tree-folder">{emptyMessage}</p>
)}
</nav>
);
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ export function sessionPaths(key: string): Set<string> {
}
}

export function sessionFlag(key: string): boolean {
try {
return sessionStorage.getItem(key) === "true";
} catch {
return false;
}
}

export function saveSessionFlag(key: string, value: boolean) {
try {
if (value) sessionStorage.setItem(key, "true");
else sessionStorage.removeItem(key);
} catch {
// Storage is optional.
}
}

export function saveSessionPaths(key: string, paths: ReadonlySet<string>) {
try {
if (paths.size) sessionStorage.setItem(key, JSON.stringify([...paths]));
Expand Down
27 changes: 26 additions & 1 deletion apps/web/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -555,11 +555,17 @@ kbd {
padding: 1px var(--space-1);
color: var(--muted);
}
.file-filter-row {
display: flex;
gap: var(--space-1-5);
margin: 0 var(--space-3) var(--space-2);
}
.search-box {
display: flex;
flex: 1;
min-width: 0;
align-items: center;
gap: var(--space-1-5);
margin: 0 var(--space-3) var(--space-2);
height: var(--control-small);
min-height: var(--control-small);
padding: 0 var(--space-1-5);
Expand All @@ -583,6 +589,25 @@ kbd {
outline: var(--focus-ring);
outline-offset: var(--focus-offset);
}
/* Pairs with the filter field; pressed state uses the shared accent toggle. */
.hide-reviewed-toggle {
flex: none;
width: var(--control-small);
min-width: var(--control-small);
height: var(--control-small);
padding: 0;
border: 1px solid var(--border-muted);
background: var(--bg);
color: var(--muted);
}
.hide-reviewed-toggle:hover {
background: var(--hover);
color: var(--fg);
}
.hide-reviewed-toggle[aria-pressed="true"] {
background: var(--accent-bg);
color: var(--accent);
}
#file-tree {
padding: 0 var(--space-1) var(--space-4);
flex: 1;
Expand Down
Loading
Loading