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
97 changes: 51 additions & 46 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,10 @@ component internals; layout remains aligned to the 4px rhythm.
| Large conceptual separation | 32 or 48, rarely needed in review chrome |

Use `--page-gutter` for notices and footer: 24 desktop, 16 below
1012px, 12 below 768px. Header, toolbar, and file frames share `--diff-gutter`
(12px). Their trailing actions account for the viewport's native scrollbar inset.
1012px, 12 below 768px. Toolbar and file frames share `--diff-gutter`
(12px), accounting for the viewport's native scrollbar inset. Header leading
content uses the same gutter; its rightmost Theme action anchors directly to
the shell's right edge, independently of diff scrolling.
Use explicit margins instead of relying on browser-default paragraph spacing.

Custom values are acceptable only for documented optical or renderer geometry:
Expand All @@ -198,61 +200,63 @@ such detail into a token. New layout spacing must use the scale.
islands, `--radius-lg` outer corners, and a 1px neutral outline without
elevation. The activity rail and file sidebar form one continuous navigation
island; notices, toolbar, viewer, and status bar form the review island.
The optional Projects column remains independently placed and rounded.
Repos and Diff share the navigation panel beside the activity rail.
Keep these frames outside Pierre's measured file geometry. At compact widths,
remove the empty navigation island and retain the inset review surface;
modal drawers use the same inset and corners without clipping focus or menus.
- Header: `--topbar-height` (40 desktop, 44 narrow or coarse pointer). Keep
- Header: `--topbar-height` (36 desktop, 44 narrow or coarse pointer). Keep
desktop controls at 32px, header icons at 16px, the divider at 16px, and group
spacing at 8px so the header recedes into the editor chrome. Toolbar and sidebar tabs: `--toolbar-height`
(44) as the desktop baseline. The narrow toolbar matches the 44px page header;
spacing at 6px so the header recedes into the editor chrome. Toolbar and sidebar
tabs share the compact 36px desktop rhythm (`--control-small` plus two
`--space-1` insets). The narrow toolbar matches the 44px page header;
its icon buttons match the header's 44px touch targets and 16px icons. Scope
controls fill the row without adding border/padding height; abbreviate the
visible “All changes” label to “All”, retaining its full accessible name.
- Header and display-toolbar icon actions share borderless ghost buttons,
16px icons, and 1.7px strokes, including menu and dialog triggers. Hover and
expanded states use the same neutral surface feedback. On mobile, both rows
use 8px action gaps and the same trailing inset so icon columns align.
use 8px action gaps; the toolbar retains its diff gutter while Theme reaches
the header's right edge.
- Header uses `--shell-canvas`, a quiet brand, and the review identity as its primary
context. Keep the secondary scope heading and muted monospace path inline;
hide the path at 768–1011px and both below 768px. Group ghost Refresh and
delete actions at the trailing edge; share `--hover` feedback with the trigger.
sync actions at the trailing edge; share `--hover` feedback with the trigger.
Theme is the rightmost header action in every layout, with the same ghost
styling, 32px desktop hit area and 16px icon. Keep its outer edge flush with
the shell; never apply the diff scrollbar inset to header actions. Snapshot deletion is absent
from the header.
A local checkout process replaces Refresh with an icon-only “Sync all
checkouts” action (folder-sync icon) that never swaps the open review.
- Header lead: the “Show/Hide projects” toggle (panel-left open/close icon),
brand and divider always start the header, in that order. The toggle sits in
an `--activity-width` box so it centers over the activity bar's icons. The
drawer pushes the header right; the lead never moves into the drawer or
reorders. Below 768px the lead uses the normal header gutter.
- Header text uses cap-height trimming with token padding to center UI and
monospace labels without clipping ascenders or descenders.
- Header lead: brand and divider identify the app. Desktop navigation controls
live in the activity rail. Below 768px, Repos and file-sidebar controls
remain in the header because the rail is hidden.
- Activity bar: a fixed `--activity-width` column (48px; 52px for coarse
pointers) between the projects drawer and file sidebar, spanning the header's
bottom edge to the shell's bottom inset on the navigation island's `--panel`.
Keep the rail and file tree in one rounded island, separated by the file
sidebar's 1px `--border-muted` leading edge. Views
(Diff now; Files and Git later) are `--activity-item` square ghost icon
buttons with 20px icons, `--radius-md`, hover `--hover`, and the current view
in accent text on `--accent-bg` with `aria-current="page"`. Theme and account
actions sit at its bottom, as in an editor's activity bar. Below 768px the bar
hides and those actions return to the header's trailing edge.
- Projects drawer: the outermost full-height column, `--projects-width` (16rem)
on `--panel` with the shell's rounded outline. Its header matches `--topbar-height` and
holds the “Projects” panel title (UI label size, medium, secondary text), a
ghost “Browse all reviews” icon action that opens the review picker, and a
close action in the modal drawer only. Below it: a “Search projects” field
styled like the file filter that filters current streams in place (Escape
clears it; matching repositories expand), then repositories ordered by their
most active visible stream. Streams sort by estimated code activity, with
captured collection time as the fallback and stable identity ties. Repository
and stream ages describe this activity; snapshot history retains its chronology.
Repository rows match tree-row height with a chevron, repository icon, medium
name and muted monospace age. Stream rows indent under them: branch icon, name,
trailing monospace changed-file count, and a muted metadata line (worktree,
harness, host only when a repository has several, age). Show only current
heads with changes. The current stream uses `--accent-bg`, accent text and
`aria-current="page"`. Unopened arrivals show a small accent dot labeled “New
changes”; a collapsed repository carries its streams' dot. The file
sidebar toggle uses a list-tree icon so the two panel controls stay distinct.
Collapse state persists per browser.
pointers), spanning the workbench below the header to the shell's bottom inset.
Diff (file-diff icon) followed by Repos (repository icon) select one shared panel.
Clicking the active view toggles its panel; clicking another view opens it.
Keep the rail and selected panel in one rounded island, separated by a 1px
`--border-muted` edge. Views use `--activity-item` square ghost buttons,
20px icons and `--radius-md`; selection uses accent text on `--accent-bg`
with `aria-current="page"`, while `aria-expanded` describes panel visibility.
Account actions sit at the bottom. Below 768px account actions move
into the header before the rightmost theme control. Below 1012px navigation panels become modal drawers.
- Repos panel: shared navigation width on `--panel`, sharing the file tree
and diff's top and bottom edges below the header. Its header matches
the compact sidebar tabs and display toolbar and holds the “Repos” title (UI label size, medium,
secondary text), a ghost magnifier labeled “Search reviews” that opens the
existing picker, and a close action in the modal drawer only. Search is
available through that popup or Cmd/Ctrl+K. Selecting a stream loads its diff
and switches the shared panel to Diff; mobile closes the drawer.
Repositories sort by their most active visible stream. Streams sort by
estimated code activity, with captured collection time as fallback and stable
identity ties. Rows retain compact tree density, truncation, ages and metadata.
Show only current heads with changes. The current stream uses `--accent-bg`,
accent text and `aria-current="page"`. Unopened arrivals show a small accent
dot labeled “New changes”; collapsed repositories carry their streams' dots.
Repository collapse state persists per browser. Switching views preserves
the open diff and review state; both views share one user-controlled width.
- A newer head for the viewed stream shows a one-line `--accent-bg` notice below
the header with an outline-muted “View latest” action. Never swap the open review.
- A compact header action counts unopened changed heads across projects and
Expand All @@ -263,7 +267,8 @@ such detail into a token. New layout spacing must use the scale.
labels; preserve the header's review identity and compact-layout controls.
- Status bar: `--statusbar-height` (28), with shortcuts shed before server metrics.
- The diff display toolbar uses `--panel`, matching the header and status bar.
- Desktop sidebar: `--sidebar-width` defaults to 330px. `use-sidebar.ts` owns
- Desktop navigation panel: `--sidebar-width` defaults to 330px. `Navigation.tsx` owns
the shared frame and resize handle for Diff and Repos; `use-sidebar.ts` owns
the user's pixel width, bounded to 200–520px and available viewport space.
Do not add competing CSS defaults or override a saved width at tablet sizes.
- Sidebar interiors share a 16px inset. Tree rows use 8px base padding plus
Expand All @@ -278,8 +283,8 @@ such detail into a token. New layout spacing must use the scale.
- Keep overlays anchored to their owning region. Sidebar/resizer layering uses
`--layer-sidebar` / `--layer-resizer`; modal dialogs use the native top layer.
The resize handle overlaps the navigation island's trailing edge and adds no
layout width. Drag distance changes only the file sidebar width, regardless of
the Projects column or shell insets. The shell gutter separates navigation from
layout width. Drag distance changes the shared navigation panel width, regardless of
view switches or shell insets. The shell gutter separates navigation from
the review island; the measured 12px diff gutter remains inside that island.

### Pierre boundary
Expand Down Expand Up @@ -415,7 +420,7 @@ such detail into a token. New layout spacing must use the scale.
branch, and repository icon. Give the repository name priority when long context labels truncate.
Piped reviews use a terminal icon, “Piped”, and collection time; omit repository,
branch, worktree, and directory identity.
Switching lives in the projects drawer; never add a dropdown chevron.
Switching lives in the repos drawer; never add a dropdown chevron.
Cmd/Ctrl+K and the drawer's browse action open the searchable “Switch review”
dialog. Position it at
`--topbar-height` plus `--space-6` on desktop (64px default), `--space-4` on
Expand Down Expand Up @@ -487,7 +492,7 @@ Do not add motion for decoration. New animation must honor
Keep the linked-worktree icon visible when its text label is hidden.
- Collapse navigation to the toggle-controlled modal drawer; its width is
`min(320px, 88vw)`. Keep file selection, comments, and export reachable there.
The projects drawer follows the same modal pattern from its own toggle; opening
The repos drawer follows the same modal pattern from its own toggle; opening
it closes the file drawer, and choosing a stream closes it.
Its tab strip matches `--topbar-height`, including the tablet header height.
File/folder rows, filter, summary toggle, and review progress retain desktop
Expand Down
2 changes: 1 addition & 1 deletion apps/web/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A local review workspace that combines a browser-based Git diff with comments th
- Reviews can cover all, staged, or unstaged changes. The file tree, reviewed-file marks, and comments help track progress.
- Comments can be copied as agent-ready XML. The same review comments are available to compatible coding agents through MCP and, in supported browsers, WebMCP.
- The CLI can also display a piped Git patch as a fixed review.
- A remote diffx server can collect from many agent sessions across many repositories. The Projects drawer keeps that work navigable: each repository lists only its current changed branches, newest first, and new arrivals are marked without replacing the review in progress.
- A remote diffx server can collect from many agent sessions across many repositories. The Repos drawer keeps that work navigable: each repository lists only its current changed branches, newest first, and new arrivals are marked without replacing the review in progress.
- Review data is stored by the local diffx server, as documented in the README.

## Capabilities and Constraints
Expand Down
110 changes: 33 additions & 77 deletions apps/web/src/ActivityBar.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { FileDiffIcon, MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import { FileDiffIcon, FolderGit2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AccountButton } from "./account.tsx";
import { useAppearance } from "./appearance-context.tsx";
import { readThemePreference, type ThemePreference } from "./theme.ts";
import { useSidebarState } from "./sidebar-context.tsx";
import { useCompactChrome } from "./use-compact-chrome.ts";

// Workspace views; file and Git views join the diff view here.
const views = [{ id: "diff", label: "Diff", Icon: FileDiffIcon }];
const views = [
{ id: "diff", label: "Diff", Icon: FileDiffIcon },
{ id: "projects", label: "Repos", Icon: FolderGit2Icon },
] satisfies {
id: "projects" | "diff";
label: string;
Icon: typeof FileDiffIcon;
}[];

/** Fixed view switcher left of the file sidebar, hidden in compact chrome. */
export function ActivityBar() {
export function ActivityBar({ diffReady = true }: { diffReady?: boolean }) {
const compact = useCompactChrome();
const sidebar = useSidebarState();
if (compact) return null;
return (
<nav className="activity-bar" aria-label="Views">
Expand All @@ -28,7 +27,26 @@ export function ActivityBar() {
type="button"
variant="ghost"
size="icon"
aria-current="page"
id={`${id}-view`}
disabled={id === "diff" && !diffReady}
aria-current={
sidebar.view === id && (id !== "diff" || diffReady)
? "page"
: undefined
}
aria-controls={
id === "projects"
? "projects-nav"
: diffReady
? "sidebar"
: undefined
}
aria-expanded={
(id !== "diff" || diffReady) &&
sidebar.view === id &&
sidebar.expanded
}
onClick={() => sidebar.selectView(id)}
aria-label={label}
title={label}
>
Expand All @@ -38,70 +56,8 @@ export function ActivityBar() {
))}
</ul>
<div className="activity-actions">
<ShellActions side="right" />
<AccountButton />
</div>
</nav>
);
}

/** Theme and account actions: activity bar bottom, or header when compact. */
export function ShellActions({ side }: { side: "right" | "bottom" }) {
return (
<>
<ThemeMenu side={side} />
<AccountButton />
</>
);
}

function themeLabel(theme: ThemePreference) {
if (theme === "light") return "Light";
if (theme === "dark") return "Dark";
return "System";
}

function ThemeMenu({ side }: { side: "right" | "bottom" }) {
const { themePreference, setThemePreference } = useAppearance();
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
id="theme"
variant="ghost"
size="icon"
aria-label={`Theme: ${themeLabel(themePreference)}`}
title={`Theme: ${themeLabel(themePreference)}`}
/>
}
>
{themePreference === "system" ? (
<MonitorIcon aria-hidden="true" />
) : themePreference === "dark" ? (
<MoonIcon aria-hidden="true" />
) : (
<SunIcon aria-hidden="true" />
)}
</DropdownMenuTrigger>
<DropdownMenuContent side={side} align="end" aria-label="Theme">
<DropdownMenuRadioGroup
value={themePreference}
onValueChange={(value: unknown) =>
setThemePreference(readThemePreference(value))
}
>
<DropdownMenuRadioItem value="light" closeOnClick>
<SunIcon aria-hidden="true" /> Light
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark" closeOnClick>
<MoonIcon aria-hidden="true" /> Dark
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system" closeOnClick>
<MonitorIcon aria-hidden="true" /> System
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
12 changes: 5 additions & 7 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ActivityBar } from "./ActivityBar.tsx";
import { useReviewState } from "./app-state.tsx";
import { OutdatedCommentsNotice } from "./CommentsPanel.tsx";
import { DiffWorkspace } from "./DiffWorkspace.tsx";
import { FileExplorerNav, SidebarResizer } from "./FileExplorerNav.tsx";
import { FileExplorerNav } from "./FileExplorerNav.tsx";
import { Header } from "./Header.tsx";
import { Navigation } from "./Navigation.tsx";
import { NewerSnapshotNotice } from "./ProjectsNav.tsx";
import { CopyDialog } from "./review.tsx";
import { capabilityEnabled, useSession } from "./session-context.tsx";
Expand All @@ -17,11 +17,9 @@ export function App() {
<Header />
<SidebarBackdrop />
<div className="workbench">
<div className="navigation-island">
<ActivityBar />
<Navigation>
<FileExplorerNav />
</div>
<SidebarResizer />
</Navigation>
<div className="workspace">
<NewerSnapshotNotice />
<OutdatedCommentsNotice />
Expand All @@ -35,7 +33,7 @@ export function App() {

function SidebarBackdrop() {
const sidebar = useSidebarState();
return sidebar.mobile && sidebar.open ? (
return sidebar.mobile && sidebar.open && sidebar.view === "diff" ? (
<button
type="button"
className="sidebar-backdrop"
Expand Down
Loading
Loading