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",