diff --git a/packages/app/e2e/browser/diff-row-alignment.spec.ts b/packages/app/e2e/browser/diff-row-alignment.spec.ts
index 840be71a9..ac9d4dfe6 100644
--- a/packages/app/e2e/browser/diff-row-alignment.spec.ts
+++ b/packages/app/e2e/browser/diff-row-alignment.spec.ts
@@ -494,25 +494,34 @@ test("workspace file panes keep their controls on shared alignment rails", async
const fileHoverColor = await fileRow.evaluate((row) => getComputedStyle(row).backgroundColor);
expect(fileHoverColor).toBe(folderHoverColor);
- const [folderStat, fileStat, optionsChevron, explorerCloseIcon, diffModeLabel] =
+ const diffFolderName = folderRow.getByText("src", { exact: true });
+ const diffFileName = fileRow.getByText("use-mounted-tab-set.ts", { exact: true });
+ const diffFolderIconLocator = folderRow.locator("svg").first();
+ const diffFileIconLocator = fileRow.locator("svg").first();
+ const [diffFolderIcon, diffFileIcon, diffFolderNameBounds, diffFileNameBounds] =
await Promise.all([
- readTextInkBounds(page.getByTestId("diff-folder-src-stat"), "last"),
- readTextInkBounds(page.getByTestId("diff-file-0-stat"), "last"),
- readSvgInkBounds(page.getByTestId("changes-options-menu").locator("svg")),
- readSvgInkBounds(page.getByTestId("explorer-close").locator("svg")),
- readTextInkBounds(page.getByTestId("changes-diff-status-trigger")),
+ readSvgInkBounds(diffFolderIconLocator),
+ readSvgInkBounds(diffFileIconLocator),
+ diffFolderName.boundingBox(),
+ diffFileName.boundingBox(),
]);
- const expandedFolderChevron = await readSvgInkBounds(folderRow.locator("svg"));
+ expect(diffFolderNameBounds).not.toBeNull();
+ expect(diffFileNameBounds).not.toBeNull();
+ const diffTreeIconNameGap = diffFolderNameBounds!.x - diffFolderIcon.right;
+ expectSameRail(diffTreeIconNameGap, diffFileNameBounds!.x - diffFileIcon.right);
+
+ const [folderStat, fileStat, optionsChevron, explorerCloseIcon] = await Promise.all([
+ readTextInkBounds(page.getByTestId("diff-folder-src-stat"), "last"),
+ readTextInkBounds(page.getByTestId("diff-file-0-stat"), "last"),
+ readSvgInkBounds(page.getByTestId("changes-options-menu").locator("svg")),
+ readSvgInkBounds(page.getByTestId("explorer-close").locator("svg")),
+ ]);
expectSameRail(folderStat.right, fileStat.right);
expectSameRail(flatFileStat.right, fileStat.right);
expectSameRail(fileStat.right, explorerCloseIcon.right);
expectSameRail(fileStat.right, optionsChevron.right);
expectSameRail(optionsChevron.right, explorerCloseIcon.right);
- expectSameRail(expandedFolderChevron.left, diffModeLabel.left);
-
await folderRow.click();
- const collapsedFolderChevron = await readSvgInkBounds(folderRow.locator("svg"));
- expectSameRail(collapsedFolderChevron.left, diffModeLabel.left);
await folderRow.click();
await dragOverlayScrollbarDown(page, diffScroll);
@@ -536,15 +545,49 @@ test("workspace file panes keep their controls on shared alignment rails", async
expect(fileMenuBounds!.y).toBeGreaterThan(fileExplorerRowBounds!.y + 10);
await page.keyboard.press("Escape");
- const [sortLabel, firstRowIcon, treeBounds, rowBounds] = await Promise.all([
+ const directoryRow = page.getByTestId(/^file-explorer-row-\d+$/).filter({ hasText: "src" });
+ const directoryName = directoryRow.getByText("src", { exact: true });
+ const [collapsedDirectoryIcon, collapsedDirectoryNameBounds] = await Promise.all([
+ readSvgInkBounds(directoryRow.locator("svg").first()),
+ directoryName.boundingBox(),
+ ]);
+ expect(collapsedDirectoryNameBounds).not.toBeNull();
+
+ await directoryRow.click();
+ const nestedFileRow = page
+ .getByTestId(/^file-explorer-row-\d+$/)
+ .filter({ hasText: "use-mounted-tab-set.ts" });
+ await expect(nestedFileRow).toBeVisible();
+ const [expandedDirectoryIcon, expandedDirectoryNameBounds, fileIcon, fileNameBounds] =
+ await Promise.all([
+ readSvgInkBounds(directoryRow.locator("svg").first()),
+ directoryName.boundingBox(),
+ readSvgInkBounds(nestedFileRow.locator("svg").first()),
+ nestedFileRow.getByText("use-mounted-tab-set.ts", { exact: true }).boundingBox(),
+ ]);
+ expect(expandedDirectoryNameBounds).not.toBeNull();
+ expect(fileNameBounds).not.toBeNull();
+ expect(expandedDirectoryNameBounds!.x).toBeCloseTo(collapsedDirectoryNameBounds!.x, 1);
+ expectSameRail(
+ collapsedDirectoryNameBounds!.x - collapsedDirectoryIcon.right,
+ fileNameBounds!.x - fileIcon.right,
+ );
+ expectSameRail(
+ expandedDirectoryNameBounds!.x - expandedDirectoryIcon.right,
+ fileNameBounds!.x - fileIcon.right,
+ );
+ expectSameRail(diffTreeIconNameGap, fileNameBounds!.x - fileIcon.right);
+
+ const readmeRow = page.getByTestId(/^file-explorer-row-\d+$/).filter({ hasText: "README.md" });
+ const [sortLabel, fileRowIcon, treeBounds, rowBounds] = await Promise.all([
readTextInkBounds(page.getByTestId("files-sort-label")),
- readSvgInkBounds(page.getByTestId("file-explorer-row-0").locator("svg").first()),
+ readSvgInkBounds(readmeRow.locator("svg").first()),
page.getByTestId("file-explorer-tree-scroll").boundingBox(),
page.getByTestId("file-explorer-row-0").boundingBox(),
]);
expect(treeBounds).not.toBeNull();
expect(rowBounds).not.toBeNull();
- expectSameRail(firstRowIcon.left, sortLabel.left);
+ expectSameRail(fileRowIcon.left, sortLabel.left);
expect(rowBounds!.x).toBeCloseTo(treeBounds!.x, 0);
expect(rowBounds!.x + rowBounds!.width).toBeCloseTo(treeBounds!.x + treeBounds!.width, 0);
});
diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx
index 798c28456..798ad36b9 100644
--- a/packages/app/src/components/file-explorer-pane.tsx
+++ b/packages/app/src/components/file-explorer-pane.tsx
@@ -22,6 +22,9 @@ import {
treeRowPaddingLeft,
WORKSPACE_FILE_ROW_TRAILING_PADDING,
WORKSPACE_FILE_ROW_VERTICAL_PADDING,
+ WORKSPACE_TREE_ICON_LABEL_GAP,
+ WORKSPACE_TREE_ICON_SIZE,
+ WORKSPACE_TREE_LOADING_ICON_SIZE,
} from "@/components/tree-primitives";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import {
@@ -185,10 +188,15 @@ function TreeRowItem({
{(() => {
if (!isDirectory) {
- return ;
+ return ;
}
if (loading) {
- return ;
+ return (
+
+ );
}
return ;
})()}
@@ -1074,10 +1082,14 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
flexDirection: "row",
alignItems: "center",
- gap: theme.spacing[2],
+ gap: WORKSPACE_TREE_ICON_LABEL_GAP,
minWidth: 0,
},
entryIcon: {
+ width: WORKSPACE_TREE_ICON_SIZE,
+ height: WORKSPACE_TREE_ICON_SIZE,
+ alignItems: "center",
+ justifyContent: "center",
flexShrink: 0,
},
entryName: {
diff --git a/packages/app/src/components/tree-primitives.tsx b/packages/app/src/components/tree-primitives.tsx
index a5743a024..01c26d5fa 100644
--- a/packages/app/src/components/tree-primitives.tsx
+++ b/packages/app/src/components/tree-primitives.tsx
@@ -12,6 +12,9 @@ import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
// drift apart.
export const TREE_INDENT_PER_LEVEL = 16;
export const WORKSPACE_FILE_ROW_VERTICAL_PADDING = SPACING[1.5];
+export const WORKSPACE_TREE_ICON_SIZE = 16;
+export const WORKSPACE_TREE_LOADING_ICON_SIZE = 14;
+export const WORKSPACE_TREE_ICON_LABEL_GAP = SPACING[2];
/**
* Trailing glyph rail shared with the explorer X and Changes options chevron.
* The extra 2px is optical: text ink ends inside its layout box, while the
@@ -25,7 +28,9 @@ export function treeRowPaddingLeft(depth: number): number {
return SPACING[3] + depth * TREE_INDENT_PER_LEVEL;
}
-const foregroundMutedIconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
+const foregroundExtraMutedIconColorMapping = (theme: Theme) => ({
+ color: theme.colors.foregroundExtraMuted,
+});
const ThemedChevronRight = withUnistyles(ChevronRight);
@@ -65,7 +70,10 @@ export function TreeChevron({ expanded }: { expanded: boolean }) {
: [styles.chevron, styles.chevronCollapsed]
}
>
-
+
);
}
@@ -79,19 +87,19 @@ const styles = StyleSheet.create((theme: Theme) => ({
backgroundColor: theme.colors.surface2,
},
chevron: {
- width: 16,
- height: 16,
+ width: WORKSPACE_TREE_ICON_SIZE,
+ height: WORKSPACE_TREE_ICON_SIZE,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
},
- // Lucide's right-pointing path occupies the middle of its square view box.
- // Move the painted edge—not the 16px layout slot—onto the tree's glyph rail.
+ // Lucide leaves space around the chevron path. Align its painted right edge inside the fixed
+ // slot; the rotated glyph is wider, so it needs half the optical offset.
chevronCollapsed: {
- left: -4,
+ left: 4,
},
chevronExpanded: {
- left: -2,
+ left: 2,
transform: [{ rotate: "90deg" }],
},
}));
diff --git a/packages/app/src/git/diff-folder-row.tsx b/packages/app/src/git/diff-folder-row.tsx
index 337fc9a9b..992165a07 100644
--- a/packages/app/src/git/diff-folder-row.tsx
+++ b/packages/app/src/git/diff-folder-row.tsx
@@ -14,6 +14,7 @@ import {
treeRowPaddingLeft,
WORKSPACE_FILE_ROW_TRAILING_PADDING,
WORKSPACE_FILE_ROW_VERTICAL_PADDING,
+ WORKSPACE_TREE_ICON_LABEL_GAP,
} from "@/components/tree-primitives";
import { type Theme } from "@/styles/theme";
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
@@ -80,7 +81,9 @@ export function DiffFolderRow({
testID={testID ? `${testID}-toggle` : undefined}
>
-
+
+
+
{displayName}
@@ -115,10 +118,14 @@ const styles = StyleSheet.create((theme: Theme) => ({
left: {
flexDirection: "row",
alignItems: "center",
- gap: theme.spacing[1],
+ gap: WORKSPACE_TREE_ICON_LABEL_GAP,
flex: 1,
minWidth: 0,
},
+ chevronOpticalOffset: {
+ // The Changes directory chevron reads high beside the folder label.
+ transform: [{ translateY: 2 }],
+ },
right: {
flexDirection: "row",
alignItems: "center",
diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx
index a3ffbd9b0..53bc9894b 100644
--- a/packages/app/src/git/diff-pane.tsx
+++ b/packages/app/src/git/diff-pane.tsx
@@ -50,6 +50,8 @@ import {
treeRowPaddingLeft,
WORKSPACE_FILE_ROW_TRAILING_PADDING,
WORKSPACE_FILE_ROW_VERTICAL_PADDING,
+ WORKSPACE_TREE_ICON_LABEL_GAP,
+ WORKSPACE_TREE_ICON_SIZE,
} from "@/components/tree-primitives";
import { MaterialFileIcon } from "@/components/material-file-icon";
import { FileChangeIcon } from "@/components/file-change-icon";
@@ -1021,7 +1023,10 @@ const DiffFileHeader = memo(function DiffFileHeader({
const fileName = file.path.split("/").pop() ?? file.path;
const headerContent = (
<>
-
+
{showDir ? null : (
@@ -3156,6 +3161,9 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
minWidth: 0,
},
+ fileHeaderLeftTree: {
+ gap: WORKSPACE_TREE_ICON_LABEL_GAP,
+ },
fileHeaderRight: {
flexDirection: "row",
alignItems: "center",
@@ -3163,6 +3171,8 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0,
},
fileIcon: {
+ width: WORKSPACE_TREE_ICON_SIZE,
+ height: WORKSPACE_TREE_ICON_SIZE,
flexShrink: 0,
alignItems: "center",
justifyContent: "center",