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
21 changes: 15 additions & 6 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,17 @@ such detail into a token. New layout spacing must use the scale.

- Use a full-width application shell, not a centered marketing container.
The diff receives all width remaining after navigation.
- Header: `--topbar-height` (56 desktop, 48 narrow). Toolbar and sidebar tabs: `--toolbar-height`
(44) as the desktop baseline. The narrow toolbar matches the 48px page header;
its icon buttons match the header theme button's 32px control height.
- Header: `--topbar-height` (40 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;
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.
- Header uses `--panel`, a quiet brand, and the review trigger 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/theme
Expand Down Expand Up @@ -253,7 +261,7 @@ such detail into a token. New layout spacing must use the scale.
## Borders, radii, and shadows

- Use the shared `outline-muted` Button variant (`--border-muted`) for compact
diff actions and reset reviewed. Segmented layout controls and the grouped
file-level diff actions and reset reviewed. Segmented layout controls and the grouped
file-filter field share this token.
Text, icons, hover and focus identify these actions; avoid per-button colors.
- Use 1px `--border-muted` for separators and `--border-control` for interactive
Expand Down Expand Up @@ -345,7 +353,7 @@ such detail into a token. New layout spacing must use the scale.
branch, worktree, and directory identity.
Use a search icon and shortcut hint, never a dropdown chevron. Click and
Cmd/Ctrl+K open the same searchable “Switch review” dialog. Position it at
`--topbar-height` plus `--space-6` on desktop (80px default), `--space-4` on
`--topbar-height` plus `--space-6` on desktop (64px default), `--space-4` on
mobile (16px default). Bound picker width to 35rem and viewport gutters. The
picker owns focus and stays available during context errors.
- Search leads the picker; keep “Switch review” as its screen-reader title.
Expand Down Expand Up @@ -410,6 +418,7 @@ Do not add motion for decoration. New animation must honor
- Mobile retains the review trigger's repository/branch or Piped/time context; truncate long
labels and hide its shortcut hint. Hide the secondary heading/path and refresh
text, preserving accessible labels and theme/sidebar controls.
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.
Its tab strip matches `--topbar-height`, including the tablet header height.
Expand All @@ -422,7 +431,7 @@ Do not add motion for decoration. New animation must honor
- At mobile widths or coarse pointers, app-owned controls target 44px hit heights;
icon controls also reach 44px width. Text-input sizing uses `--text-input-touch`.
The compact View options and collapse/expand icons match the theme control's
32px size within the narrow 48px toolbar, per the workspace alignment brief.
44px size within the narrow 44px toolbar, matching the header touch targets.
Drawer navigation and summary controls retain desktop density, per the same brief.
- Pierre code rows and header slots retain measured geometry. Larger library
gutter/header targets need a coordinated renderer-metric change, not a CSS override.
Expand Down
25 changes: 17 additions & 8 deletions apps/web/src/DiffToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,21 @@ export function DiffToolbar() {
}}
>
{scopes.values.map((value) => (
<ToggleGroupItem key={value} value={value} data-mode={value}>
{value === "all"
? "All changes"
: value === "staged"
? "Staged"
: "Unstaged"}
<ToggleGroupItem
key={value}
value={value}
data-mode={value}
aria-label={value === "all" ? "All changes" : undefined}
>
{value === "all" ? (
<>
All<span className="scope-label-detail"> changes</span>
</>
) : value === "staged" ? (
"Staged"
) : (
"Unstaged"
)}
</ToggleGroupItem>
))}
</ToggleGroup>
Expand Down Expand Up @@ -94,7 +103,7 @@ export function DiffToolbar() {
<Button
type="button"
id="view-options"
variant="outline-muted"
variant="ghost"
size="icon-sm"
aria-label="View options"
title="View options"
Expand Down Expand Up @@ -153,7 +162,7 @@ export function DiffToolbar() {
<Button
type="button"
id="toggle-all-files"
variant="outline-muted"
variant="ghost"
size="icon-sm"
disabled={files.length === 0}
aria-label={allCollapsed ? "Expand all files" : "Collapse all files"}
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export function Header() {
onClick={sidebar.toggle}
>
{sidebar.expanded ? (
<PanelLeftCloseIcon className="size-(--icon-lg)" />
<PanelLeftCloseIcon className="size-(--icon-base)" />
) : (
<PanelLeftOpenIcon className="size-(--icon-lg)" />
<PanelLeftOpenIcon className="size-(--icon-base)" />
)}
</Button>
<a className="brand" href="/" aria-label="diffx home">
Expand Down Expand Up @@ -125,11 +125,11 @@ export function Header() {
}
>
{themePreference === "system" ? (
<MonitorIcon className="size-(--icon-lg)" aria-hidden="true" />
<MonitorIcon className="size-(--icon-base)" aria-hidden="true" />
) : themePreference === "dark" ? (
<MoonIcon className="size-(--icon-lg)" aria-hidden="true" />
<MoonIcon className="size-(--icon-base)" aria-hidden="true" />
) : (
<SunIcon className="size-(--icon-lg)" aria-hidden="true" />
<SunIcon className="size-(--icon-base)" aria-hidden="true" />
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" aria-label="Theme">
Expand Down
11 changes: 6 additions & 5 deletions apps/web/src/review.css
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@

/* Enlarge app chrome, never Pierre's measured header or code-row slots. */
@media (max-width: 767px), (pointer: coarse) {
.topbar [data-slot="button"],
.topbar button,
[data-slot="button"][data-size="icon"]:not(.tree-control),
.copy-comments-actions [data-slot="button"],
.webmcp-instruction-button,
Expand All @@ -790,11 +790,12 @@
}
#view-options,
#toggle-all-files {
width: var(--control-height);
height: var(--control-height);
min-width: var(--control-height);
min-height: var(--control-height);
width: var(--touch-target);
height: var(--touch-target);
min-width: var(--touch-target);
min-height: var(--touch-target);
}
.topbar button[data-size="icon"],
[data-slot="button"][data-size="icon"]:not(.tree-control),
#refresh {
min-width: var(--touch-target);
Expand Down
27 changes: 19 additions & 8 deletions apps/web/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ kbd {
.topbar {
height: var(--topbar-height);
display: flex;
gap: var(--space-3);
gap: var(--space-2);
align-items: center;
border-bottom: 1px solid var(--border);
background: var(--panel);
Expand All @@ -118,18 +118,17 @@ kbd {
}
.header-divider {
width: 1px;
height: var(--space-6);
height: var(--space-4);
align-self: center;
background: var(--border);
flex-shrink: 0;
}
.header-heading {
display: flex;
align-items: center;
gap: var(--space-3);
gap: var(--space-2);
flex: 1;
min-width: 0;
margin-left: var(--space-2);
}
.header-heading h1 {
flex-shrink: 0;
Expand Down Expand Up @@ -172,7 +171,8 @@ kbd {
justify-content: center;
padding: 0;
}
.topbar [data-slot="button"] svg {
.topbar button svg,
.toolbar button svg {
stroke-width: 1.7;
}
.topbar [data-variant="ghost"]:hover,
Expand Down Expand Up @@ -763,7 +763,7 @@ h2 {
.main-footer .footer-details {
margin-left: auto;
}
.topbar {
.header-actions {
gap: var(--space-2);
}
.brand {
Expand Down Expand Up @@ -792,8 +792,16 @@ h2 {
}
#view-options svg,
#toggle-all-files svg {
width: var(--icon-lg);
height: var(--icon-lg);
width: var(--icon-base);
height: var(--icon-base);
}
.toolbar .segmented {
border: 0;
padding: 0;
outline: 1px solid var(--border-muted);
}
.scope-label-detail {
display: none;
}
.toolbar-spacer {
min-width: 0;
Expand Down Expand Up @@ -1162,6 +1170,9 @@ h2 {
.context-switcher-name {
max-width: 40%;
}
.context-switcher-worktree {
flex-shrink: 0;
}
.project-picker {
top: var(--space-4);
max-height: calc(100dvh - var(--space-4) - var(--page-gutter));
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
--control-small: 1.75rem;
--control-large: 2.5rem;
--touch-target: 2.75rem;
--topbar-height: 3.5rem;
--topbar-height: 2.5rem;
--toolbar-height: 2.75rem;
--statusbar-height: calc(
var(--text-xs) * var(--leading-ui) + 2 * var(--space-2) + 1px
Expand Down Expand Up @@ -121,6 +121,10 @@
@media (max-width: 767px) {
:root {
--page-gutter: var(--space-3);
--topbar-height: 3rem;
}
}
@media (max-width: 767px), (pointer: coarse) {
:root {
--topbar-height: var(--touch-target);
}
}
22 changes: 19 additions & 3 deletions apps/web/test/browser/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,10 @@ test("header icons and sidebar tab indicator use design-system sizes", async ({
viewOptions: size("#view-options svg"),
refresh: size("#refresh svg"),
theme: size("#theme svg"),
chromeStrokes: Array.from(
document.querySelectorAll(".topbar button svg, .toolbar button svg"),
(element) => getComputedStyle(element).strokeWidth,
),
tabBorderWidth: getComputedStyle(tab).borderBottomWidth,
indicatorColor: indicator.backgroundColor,
indicatorOpacity: indicator.opacity,
Expand All @@ -1260,19 +1264,22 @@ test("header icons and sidebar tab indicator use design-system sizes", async ({
(dividerBox.top + dividerBox.height / 2),
),
dividerHeight: dividerBox.height,
headerHeight: topbarBox.height,
accent,
};
});

expect(styles).toMatchObject({
sidebar: "20px",
sidebar: "16px",
refresh: "16px",
theme: "20px",
theme: "16px",
tabBorderWidth: "1px",
indicatorOpacity: "1",
dividerHeight: 24,
dividerHeight: 16,
headerHeight: 40,
});
expect(styles.branch).toBe(styles.viewOptions);
expect(styles.chromeStrokes.every((stroke) => stroke === "1.7px")).toBe(true);
expect(styles.dividerCenterOffset).toBeLessThanOrEqual(0.5);
expect(styles.indicatorColor).toBe(styles.accent);
});
Expand Down Expand Up @@ -1614,9 +1621,18 @@ test("drawer aligns its header and retains desktop navigation density", async ({
const toolbar = await page.locator(".toolbar").boundingBox();
const theme = await page.locator("#theme").boundingBox();
const options = await page.locator("#view-options").boundingBox();
const remove = await page
.getByRole("button", { name: "Delete snapshot", exact: true })
.boundingBox();
const collapse = await page.locator("#toggle-all-files").boundingBox();
expect(header.height).toBe(44);
expect(toolbar?.height).toBe(header.height);
expect(theme?.width).toBe(44);
expect(theme?.height).toBe(44);
expect(options?.width).toBe(theme?.width);
expect(options?.height).toBe(theme?.height);
expect(options?.x).toBe(remove?.x);
expect(collapse?.x).toBe(theme?.x);
}
await page
.locator("#sidebar")
Expand Down
7 changes: 6 additions & 1 deletion apps/web/test/browser/design-refresh.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,11 @@ test("diff header actions use shared muted borders and collapse files directly",
});
await expect(options).toHaveText("");
await expect(options).toHaveAttribute("title", "View options");
await expect(options).toHaveAttribute("data-variant", "ghost");
await expect(page.locator("#toggle-all-files")).toHaveAttribute(
"data-variant",
"ghost",
);
const reviewed = page.locator(".review-button").first();
await expect(reviewed).toHaveText("Reviewed");
const comment = page.locator(".file-comment-button").first();
Expand All @@ -523,7 +528,7 @@ test("diff header actions use shared muted borders and collapse files directly",
const expected = getComputedStyle(probe).borderColor;
const actual = Array.from(
document.querySelectorAll(
".search-box, .layout-control, #view-options, #toggle-all-files, .review-button, .file-comment-button, #reset-reviewed",
".search-box, .layout-control, .review-button, .file-comment-button, #reset-reviewed",
),
(element) => getComputedStyle(element).borderTopColor,
);
Expand Down
14 changes: 14 additions & 0 deletions apps/web/test/browser/local-workspace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ test("comparison uses the captured merge base and follows the selected scope", a
await expect(popup).toContainText("Unstaged changes compare the index");
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "All changes", exact: true }).click();
for (const width of [320, 390]) {
await page.setViewportSize({ width, height: 844 });
await expect(page.locator(".topbar")).toHaveCSS("height", "44px");
await expect(page.locator(".toolbar")).toHaveCSS("height", "44px");
for (const name of ["All changes", "Staged", "Unstaged", "View options"]) {
const control = page.getByRole("button", { name, exact: true });
await expect(control).toBeVisible();
const bounds = await control.boundingBox();
if (!bounds) throw new Error(`Missing mobile control: ${name}`);
expect(bounds.height).toBeGreaterThanOrEqual(44);
expect(bounds.x).toBeGreaterThanOrEqual(0);
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width);
}
}
await page.setViewportSize({ width: 390, height: 844 });
await expect(trigger).toBeVisible();
await expect(trigger).toContainText("Comparison");
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading
Loading