From 8d11c4b0a1935ccfd8bb2069590ea658f057da03 Mon Sep 17 00:00:00 2001 From: hanafish <1106510024@qq.com> Date: Thu, 6 Aug 2026 22:14:06 +0800 Subject: [PATCH 1/2] fix(source-control): unify diff views and focus state --- .../DiffViewConsistency.md | 74 ++++++++++ src/engines/GitWorkflow/GitHubDiff/types.ts | 3 +- src/features/CodeMirror/Diff/index.tsx | 3 +- src/features/CodeViewer/GitDiffViewer.tsx | 3 +- .../SourceControlHeaderContent.test.ts | 82 +++++++++++ .../components/SourceControlHeaderContent.tsx | 44 ++++-- .../content/GitCommitDetailContent/index.tsx | 4 +- .../content/GitDiffContent/index.tsx | 9 +- .../AllChangesView.tsx | 7 +- .../SourceControlMainContent/FocusView.tsx | 15 +- .../SourceControlMainContent/index.tsx | 1 - .../content/SourceControlMainPane.tsx | 7 +- .../__tests__/FocusView.emptyState.test.ts | 44 ++++++ .../content/__tests__/TEST_CASES.md | 33 +++-- .../__tests__/sourceControlMainProps.test.ts | 54 +++++++ .../content/sourceControlMainProps.ts | 74 +++++++++- .../hooks/useSourceControlPaneActions.ts | 30 +--- .../Panels/EditorMainPane/index.tsx | 10 +- .../CodeEditor/Panels/EditorMainPane/types.ts | 2 + .../detail/PrChangesTab.tsx | 4 +- .../CodeEditor/__tests__/TEST_CASES.md | 11 +- .../sourceControlStateTransitions.test.ts | 136 ++++++++++++++++++ src/modules/WorkStation/CodeEditor/index.tsx | 3 + .../sourceControlStateTransitions.ts | 63 ++++++++ .../CodeEditor/useSourceControlSetup.ts | 21 ++- .../diffSessionReplay.useDetailContent.tsx | 5 + .../WorkStation/Diff/SessionReplay/index.tsx | 32 ++++- .../shared/DiffFileSection/index.tsx | 5 +- .../shared/DiffSectionList/index.tsx | 4 + .../shared/components/FileHeader/index.tsx | 3 +- .../codeEditor/diffViewModeAtom.test.ts | 42 ++++++ .../codeEditor/diffViewModeAtom.ts | 28 ++++ src/store/workstation/codeEditor/index.ts | 3 + src/types/git/types.ts | 3 + 34 files changed, 771 insertions(+), 91 deletions(-) create mode 100644 docs/frontend-ui-audit-2026-08-05/DiffViewConsistency.md create mode 100644 src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.test.ts create mode 100644 src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/__tests__/FocusView.emptyState.test.ts create mode 100644 src/modules/WorkStation/CodeEditor/__tests__/sourceControlStateTransitions.test.ts create mode 100644 src/modules/WorkStation/CodeEditor/sourceControlStateTransitions.ts create mode 100644 src/store/workstation/codeEditor/diffViewModeAtom.test.ts create mode 100644 src/store/workstation/codeEditor/diffViewModeAtom.ts diff --git a/docs/frontend-ui-audit-2026-08-05/DiffViewConsistency.md b/docs/frontend-ui-audit-2026-08-05/DiffViewConsistency.md new file mode 100644 index 000000000..508eaa580 --- /dev/null +++ b/docs/frontend-ui-audit-2026-08-05/DiffViewConsistency.md @@ -0,0 +1,74 @@ +# Frontend UI Audit: Diff View Consistency + +**Files:** + +- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx` +- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/index.tsx` +- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainPane.tsx` +- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainContent/index.tsx` +- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainContent/FocusView.tsx` +- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainContent/AllChangesView.tsx` +- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/GitDiffContent/index.tsx` +- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/GitCommitDetailContent/index.tsx` +- `src/modules/WorkStation/CodeEditor/Panels/EditorPrimarySidebar/content/PullRequestContent/detail/PrChangesTab.tsx` +- `src/modules/WorkStation/Diff/SessionReplay/index.tsx` +- `src/modules/WorkStation/Diff/SessionReplay/diffSessionReplay.useDetailContent.tsx` +- `src/modules/WorkStation/shared/DiffFileSection/index.tsx` +- `src/modules/WorkStation/shared/DiffSectionList/index.tsx` +- `src/modules/shared/components/FileHeader/index.tsx` +- `src/features/CodeMirror/Diff/index.tsx` +- `src/features/CodeViewer/GitDiffViewer.tsx` + +**Date:** 2026-08-05 + +**Auditor:** Codex + +## D1 — Design-system component usage + +| Line | Element | Verdict | Reason | Suggested change | +| -------------------------------------- | ------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `SourceControlHeaderContent.tsx:217` | Unified / split selector | keep with reason | Uses the existing `TabPill` design-system control, matching the file diff header interaction. | None. | +| `SessionReplay/index.tsx:269` | Unified / split selector | keep with reason | Uses the same `TabPill` control and shared preference as the other diff surfaces. | None. | +| `GitCommitDetailContent/index.tsx:377` | Collapsed file-list rail button | keep with reason | The control occupies a full-height 24 px rail; the current design-system `Button` variants do not model that geometry. | Keep the raw semantic button until the rail pattern is abstracted. | +| `PrChangesTab.tsx:214` | Collapsed file-list rail button | keep with reason | Same full-height rail geometry as commit details. | Keep the raw semantic button until the rail pattern is abstracted. | +| `DiffFileSection/index.tsx:418` | Sticky expandable file header | keep with reason | It is a full-width, multiline disclosure row rather than a standard action button. | Continue using a semantic button; consider a dedicated diff-section header only if the pattern gains more consumers. | +| `FocusView.tsx:51` | No-file Focus empty state | keep with reason | Reuses the workstation `NoTabsPlaceholder` with a focused instruction instead of assembling a bespoke empty state. | None. | + +## D2 — Color and token usage + +| Line | Element | Verdict | Reason | Suggested change | +| ---- | ----------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------- | +| — | Audited visible changes | keep with reason | New controls use existing semantic border, fill, and text tokens; no raw colors or duplicated token expressions were introduced. | None. | + +## D3 — Spacing and typography + +| Line | Element | Verdict | Reason | Suggested change | +| ---------------------------------------- | --------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | +| `DiffSectionList/index.tsx:51` | Virtual-list footer buffer `h-[100px]` | keep with reason | This is an exact scroll affordance rather than visual component sizing, and the spacing scale has no exact 100 px token. | None. | +| `DiffFileSection/index.tsx:321` | Loading placeholder `h-[480px] min-h-[320px]` | fix | Both values have exact Tailwind scale equivalents and are unrelated to the current diff-mode behavior. | In a separate visual-cleanup sweep, use `h-120 min-h-80` and visually verify loading layout. | +| `DiffFileSection/index.tsx:424` | Chevron reservation `w-[14px]` | keep with reason | The width deliberately matches the 14 px icon and has no exact standard spacing class. | None. | +| `DiffFileSection/index.tsx:436-463` | Dense diff metadata typography | keep with reason | The 13 px filename and 11 px metadata match the established dense diff header hierarchy; the standard text scale has no exact equivalents. | Keep until diff typography is tokenized system-wide. | +| `SourceControlHeaderContent.tsx:119-123` | Dense source-control header typography | keep with reason | The 11 px prefix and 13 px title preserve the existing editor-header hierarchy and were not introduced by this change. | Keep until editor header typography is tokenized system-wide. | + +## D4 — Accessibility basics + +| Line | Element | Verdict | Reason | Suggested change | +| -------------------------------------- | ---------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `SourceControlHeaderContent.tsx:217` | Unified / split selector | keep with reason | Both options have visible translated labels and expose button semantics through `TabPill`. | None. | +| `SessionReplay/index.tsx:269` | Unified / split selector | keep with reason | Both options have visible translated labels and expose button semantics through `TabPill`. | None. | +| `GitCommitDetailContent/index.tsx:377` | Icon-only collapsed file-list rail | fix | It has a tooltip title but no explicit accessible label. | Add the translated “show file list” string as `aria-label` in a coordinated rail-control sweep. | +| `PrChangesTab.tsx:214` | Icon-only collapsed file-list rail | fix | It has a tooltip title but no explicit accessible label. | Add the translated “show file list” string as `aria-label` in the same sweep. | +| `DiffFileSection/index.tsx:418` | Expandable file header | keep with reason | The visible filename supplies an accessible name and `aria-expanded` communicates disclosure state. | None. | +| `FocusView.tsx:51` | No-file Focus empty state | keep with reason | The translated caption explicitly tells the user to select a changed file and exposes no unrelated actions. | None. | + +## D5 — Duplication and abstraction candidates + +- **Abstract candidate:** the collapsed 24 px file-list rail appears in commit details, pull-request changes, and `GitFileDiffSplit`. A small shared rail-control component could centralize geometry, tooltip, and accessible naming. This is a multi-file sweep candidate and is intentionally not mixed into the diff-view consistency fix. +- The new unified / split selectors intentionally reuse `TabPill` and one shared persisted atom; no new visual-pattern duplication was introduced. + +## Summary + +- **Fix candidates:** 3 (two rail-control accessible labels; one standardizable loading height) +- **Keep with reason:** 12 +- **Abstract candidates:** 1 +- **Current change verdict:** the new controls follow the design system and introduce no new arbitrary colors, spacing, or inaccessible icon-only actions. diff --git a/src/engines/GitWorkflow/GitHubDiff/types.ts b/src/engines/GitWorkflow/GitHubDiff/types.ts index 52017e746..5c699e16a 100644 --- a/src/engines/GitWorkflow/GitHubDiff/types.ts +++ b/src/engines/GitWorkflow/GitHubDiff/types.ts @@ -3,6 +3,7 @@ * * Type definitions for the diff viewer architecture. */ +import type { DiffViewMode } from "@src/types/git/types"; // ============================================ // Diff Line Types @@ -98,7 +99,7 @@ export interface FileDiff { // ============================================ /** The view mode for the diff */ -export type DiffViewMode = "unified" | "split"; +export type { DiffViewMode } from "@src/types/git/types"; /** Expansion type for hunks */ export type DiffHunkExpansionType = "up" | "down" | "short" | "full"; diff --git a/src/features/CodeMirror/Diff/index.tsx b/src/features/CodeMirror/Diff/index.tsx index 626d57cc8..9d02fbd6e 100644 --- a/src/features/CodeMirror/Diff/index.tsx +++ b/src/features/CodeMirror/Diff/index.tsx @@ -31,6 +31,7 @@ import type { GitFileStatus } from "@src/config/gitStatus"; import { createLogger } from "@src/hooks/logger"; import { useEditorAppearanceSettings } from "@src/hooks/settings"; import { EditorService } from "@src/services/workStation/EditorService"; +import type { DiffViewMode } from "@src/types/git/types"; import { useSelectionExtension } from "../Editor/hooks/useSelectionExtension"; import type { TextSelectionInfo } from "../Editor/types"; @@ -67,7 +68,7 @@ export interface CodeMirrorDiffProps { /** Container height */ height?: string; /** Diff view mode: unified (inline) or split (side-by-side) */ - viewMode?: "unified" | "split"; + viewMode?: DiffViewMode; /** Read-only mode */ readOnly?: boolean; /** Show merge controls (accept/reject buttons) */ diff --git a/src/features/CodeViewer/GitDiffViewer.tsx b/src/features/CodeViewer/GitDiffViewer.tsx index d2f77b0f8..f95b651f0 100644 --- a/src/features/CodeViewer/GitDiffViewer.tsx +++ b/src/features/CodeViewer/GitDiffViewer.tsx @@ -7,6 +7,7 @@ import React, { useState } from "react"; import { Placeholder } from "@src/modules/shared/layouts/blocks"; +import type { DiffViewMode } from "@src/types/git/types"; import { ModernSplitDiff } from "./ModernSplitDiff"; import { VirtualizedModernDiff } from "./VirtualizedModernDiff"; @@ -15,7 +16,7 @@ import { VirtualizedModernDiff } from "./VirtualizedModernDiff"; // Types // ============================================ -export type DiffViewMode = "unified" | "split"; +export type { DiffViewMode } from "@src/types/git/types"; export interface GitDiffViewerProps { /** Original/old content */ diff --git a/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.test.ts b/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.test.ts new file mode 100644 index 000000000..34dfb6b28 --- /dev/null +++ b/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.test.ts @@ -0,0 +1,82 @@ +import type { TFunction } from "i18next"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it, vi } from "vitest"; + +import type { WorkStationTab } from "@src/store/workstation/tabs"; + +import { SourceControlHeaderContent } from "./SourceControlHeaderContent"; + +vi.mock("@src/components/Button", () => ({ + default: ({ title }: { title?: string }) => + createElement("button", { "data-title": title }), +})); + +vi.mock("@src/components/TabPill", () => ({ + default: ({ + activeTab, + tabs, + }: { + activeTab: string; + tabs: Array<{ key: string }>; + }) => + createElement("div", { + "data-active-tab": activeTab, + "data-tabs": tabs.map((tab) => tab.key).join(","), + }), +})); + +const t = ((key: string) => key) as TFunction; + +function sourceControlTab(mode: "focus" | "all-changes"): WorkStationTab { + return { + id: "source-control:changes", + type: "source-control", + title: "Review", + data: { + mode, + staged: false, + fileCount: 1, + focusPath: null, + historySelection: null, + }, + } as WorkStationTab; +} + +function renderHeader(mode: "focus" | "all-changes"): string { + return renderToStaticMarkup( + createElement(SourceControlHeaderContent, { + activeTab: sourceControlTab(mode), + sourceControlFilterMode: "uncommitted", + showSourceControlModePill: true, + gitReviewNavigationTotal: 0, + selectedIssue: null, + sourceControlRefreshSpinClass: undefined, + diffViewMode: "split", + t, + onDiffViewModeChange: vi.fn(), + onModeChange: vi.fn(), + onOpenHistoryInNewTab: vi.fn(), + onReviewPrevFile: vi.fn(), + onReviewNextFile: vi.fn(), + onCollapseAll: vi.fn(), + onRefresh: vi.fn(), + }) + ); +} + +describe("SourceControlHeaderContent diff view controls", () => { + it("shows the shared unified/split control in All Changes", () => { + const markup = renderHeader("all-changes"); + + expect(markup).toContain('data-active-tab="split"'); + expect(markup).toContain('data-tabs="unified,split"'); + }); + + it("keeps the aggregate diff control out of Focus mode", () => { + const markup = renderHeader("focus"); + + expect(markup).not.toContain('data-tabs="unified,split"'); + expect(markup).toContain('data-tabs="focus,all-changes"'); + }); +}); diff --git a/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx b/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx index c1f2fb905..3fcce3b50 100644 --- a/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx +++ b/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx @@ -28,6 +28,7 @@ import type { SourceControlHistorySelection, WorkStationTab, } from "@src/store/workstation/tabs"; +import type { DiffViewMode } from "@src/types/git/types"; export interface SourceControlHeaderContentProps { /** The active `source-control` tab (host guarantees the type). */ @@ -40,7 +41,9 @@ export interface SourceControlHeaderContentProps { sourceControlHeaderLeadingSlot?: ReactNode; sourceControlHeaderTrailingSlot?: ReactNode; sourceControlRefreshSpinClass: string | undefined; + diffViewMode: DiffViewMode; t: TFunction; + onDiffViewModeChange: (mode: DiffViewMode) => void; onModeChange: (mode: "focus" | "all-changes") => void; onOpenHistoryInNewTab: (selection: SourceControlHistorySelection) => void; onReviewPrevFile: () => void; @@ -60,7 +63,9 @@ export const SourceControlHeaderContent: React.FC< sourceControlHeaderLeadingSlot, sourceControlHeaderTrailingSlot, sourceControlRefreshSpinClass, + diffViewMode, t, + onDiffViewModeChange, onModeChange, onOpenHistoryInNewTab, onReviewPrevFile, @@ -197,16 +202,35 @@ export const SourceControlHeaderContent: React.FC< )} {showCollapseAll && ( -