fix(FN-XXX): improve git manager diff layout
This commit is contained in:
5
.changeset/improve-git-manager-diff-pane.md
Normal file
5
.changeset/improve-git-manager-diff-pane.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve the Git Manager diff layout and file path truncation in dashboard modals.
|
||||||
@@ -220,7 +220,7 @@ export function CommitDiffTab({ commitSha, mergeDetails }: CommitDiffTabProps) {
|
|||||||
{getStatusLabel(file.status)}
|
{getStatusLabel(file.status)}
|
||||||
</span>
|
</span>
|
||||||
<span className="changes-file-path" title={file.path}>
|
<span className="changes-file-path" title={file.path}>
|
||||||
{file.path}
|
<bdo dir="ltr">{file.path}</bdo>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="changes-file-stat"
|
className="changes-file-stat"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { Task } from "@fusion/core";
|
|||||||
import { getErrorMessage } from "@fusion/core";
|
import { getErrorMessage } from "@fusion/core";
|
||||||
import type { ToastType } from "../hooks/useToast";
|
import type { ToastType } from "../hooks/useToast";
|
||||||
import { useConfirm } from "../hooks/useConfirm";
|
import { useConfirm } from "../hooks/useConfirm";
|
||||||
import { truncateMiddle } from "../utils/truncatePath";
|
|
||||||
import { getPathBasename } from "../utils/pathDisplay";
|
import { getPathBasename } from "../utils/pathDisplay";
|
||||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||||
@@ -1054,6 +1053,8 @@ function ChangesPanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="gm-changes-split">
|
||||||
|
<div className="gm-changes-lists">
|
||||||
{/* Unstaged Changes */}
|
{/* Unstaged Changes */}
|
||||||
<div className="gm-file-section">
|
<div className="gm-file-section">
|
||||||
<div className="gm-file-section-header">
|
<div className="gm-file-section-header">
|
||||||
@@ -1116,7 +1117,7 @@ function ChangesPanel({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<FileStatusIcon status={f.status} />
|
<FileStatusIcon status={f.status} />
|
||||||
<span className="gm-file-name" title={f.file}>{truncateMiddle(f.file, 40)}</span>
|
<span className="gm-file-name" title={f.file}><bdo dir="ltr">{f.file}</bdo></span>
|
||||||
<FileStatusBadge status={f.status} />
|
<FileStatusBadge status={f.status} />
|
||||||
<button
|
<button
|
||||||
className="gm-icon-btn"
|
className="gm-icon-btn"
|
||||||
@@ -1188,7 +1189,7 @@ function ChangesPanel({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<FileStatusIcon status={f.status} />
|
<FileStatusIcon status={f.status} />
|
||||||
<span className="gm-file-name" title={f.file}>{truncateMiddle(f.file, 40)}</span>
|
<span className="gm-file-name" title={f.file}><bdo dir="ltr">{f.file}</bdo></span>
|
||||||
<FileStatusBadge status={f.status} />
|
<FileStatusBadge status={f.status} />
|
||||||
<button
|
<button
|
||||||
className="gm-icon-btn"
|
className="gm-icon-btn"
|
||||||
@@ -1207,33 +1208,43 @@ function ChangesPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Diff Viewer */}
|
</div>
|
||||||
{(selectedDiffTarget || loadingChangeDiff || changeDiff || changeDiffError) && (
|
{/* Diff Viewer (right pane on desktop, stacked below on mobile) */}
|
||||||
<div className="gm-diff-section">
|
<div className="gm-changes-diff">
|
||||||
{selectedDiffTarget && (
|
{(selectedDiffTarget || loadingChangeDiff || changeDiff || changeDiffError) ? (
|
||||||
<div className="gm-diff-target">
|
<div className="gm-diff-section">
|
||||||
<FileDiff size={14} />
|
{selectedDiffTarget && (
|
||||||
<span>{selectedDiffTarget.staged ? "Staged" : "Unstaged"} diff: </span>
|
<div className="gm-diff-target">
|
||||||
<code>{selectedDiffTarget.file}</code>
|
<FileDiff size={14} />
|
||||||
</div>
|
<span>{selectedDiffTarget.staged ? "Staged" : "Unstaged"} diff: </span>
|
||||||
)}
|
<code>{selectedDiffTarget.file}</code>
|
||||||
{loadingChangeDiff && (
|
</div>
|
||||||
<div className="gm-diff-loading">
|
)}
|
||||||
<Loader2 size={16} className="spin" />
|
{loadingChangeDiff && (
|
||||||
Loading diff...
|
<div className="gm-diff-loading">
|
||||||
</div>
|
<Loader2 size={16} className="spin" />
|
||||||
)}
|
Loading diff...
|
||||||
{changeDiffError && !loadingChangeDiff && (
|
</div>
|
||||||
<div className="gm-diff-error">{changeDiffError}</div>
|
)}
|
||||||
)}
|
{changeDiffError && !loadingChangeDiff && (
|
||||||
{changeDiff && !loadingChangeDiff && (
|
<div className="gm-diff-error">{changeDiffError}</div>
|
||||||
<div className="gm-diff-viewer">
|
)}
|
||||||
{changeDiff.stat && <pre className="gm-diff-stat">{changeDiff.stat}</pre>}
|
{changeDiff && !loadingChangeDiff && (
|
||||||
<pre className="gm-diff-patch">{changeDiff.patch}</pre>
|
<div className="gm-diff-viewer">
|
||||||
</div>
|
{changeDiff.stat && <pre className="gm-diff-stat">{changeDiff.stat}</pre>}
|
||||||
)}
|
<pre className="gm-diff-patch">{changeDiff.patch}</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="gm-diff-empty">
|
||||||
|
<FileDiff size={20} />
|
||||||
|
<span>Select a file to view its diff</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* /gm-changes-split */}
|
||||||
|
|
||||||
{/* Commit Form */}
|
{/* Commit Form */}
|
||||||
<form className="gm-commit-form" onSubmit={onCommit}>
|
<form className="gm-commit-form" onSubmit={onCommit}>
|
||||||
|
|||||||
@@ -2035,6 +2035,86 @@
|
|||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Split-pane: file lists on left, diff viewer on right ─────────────────
|
||||||
|
* Lazygit-style layout so the file lists stay visible while reading a diff.
|
||||||
|
* `.gm-content` (the panel scroll container) keeps overflow-y: auto, and the
|
||||||
|
* left column uses `position: sticky` so it pins to the top as the right
|
||||||
|
* column scrolls. Mobile (≤720px) collapses to a single column with the
|
||||||
|
* lists rendered first and the diff below — sticky disabled because there's
|
||||||
|
* no horizontal room for a second pane and a sticky list would eat the
|
||||||
|
* already-tight viewport.
|
||||||
|
*/
|
||||||
|
.gm-changes-split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(280px, 360px) 1fr;
|
||||||
|
gap: var(--space-md);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-changes-lists {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
align-self: start;
|
||||||
|
/* Cap the sticky column so its internal lists never push past the viewport
|
||||||
|
* — without this, two long lists at max-height 200px each + headers can
|
||||||
|
* exceed the available scroll-port height and the column visually clips. */
|
||||||
|
max-height: calc(100dvh - var(--overlay-padding-top, 10vh) - 280px);
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Don't let the sticky scrollbar overlap the diff column. */
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-changes-diff {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
/* Allow children (long file paths, code lines) to shrink below intrinsic
|
||||||
|
* width — required for `overflow: hidden` / `word-break` to work in the
|
||||||
|
* diff viewer. Without this the right column would push the layout wider
|
||||||
|
* than the modal. */
|
||||||
|
min-width: 0;
|
||||||
|
/* Match the lists' top so empty/loading/diff states align with the list
|
||||||
|
* column visually. */
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty placeholder shown when no file is selected. Gives the right pane
|
||||||
|
* structure on first open instead of a blank gap. */
|
||||||
|
.gm-diff-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-2xl) var(--space-lg);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 13px;
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.gm-changes-split {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.gm-changes-lists {
|
||||||
|
position: static;
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.gm-diff-empty {
|
||||||
|
/* Don't take a full screen of empty state on mobile — collapse to a
|
||||||
|
* compact hint when no file is selected. */
|
||||||
|
min-height: 0;
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── File Section ── */
|
/* ── File Section ── */
|
||||||
|
|
||||||
.gm-file-section {
|
.gm-file-section {
|
||||||
@@ -2118,11 +2198,18 @@
|
|||||||
accent-color: var(--todo);
|
accent-color: var(--todo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Show as much of the path as fits, truncating the LEFT side (parent dirs)
|
||||||
|
* with an ellipsis so the filename — the most identifying part — stays
|
||||||
|
* visible. `direction: rtl` flips the truncation side; the JSX wraps the
|
||||||
|
* value in `<bdo dir="ltr">` so the displayed text still reads naturally
|
||||||
|
* left-to-right. */
|
||||||
.gm-file-name {
|
.gm-file-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
direction: rtl;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -3555,6 +3642,8 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
direction: rtl;
|
||||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1087,11 +1087,16 @@
|
|||||||
margin-left: calc(16px + var(--space-sm)); /* align with text after icon + gap */
|
margin-left: calc(16px + var(--space-sm)); /* align with text after icon + gap */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Truncate parent dirs from the LEFT so the filename stays visible. JSX
|
||||||
|
* wraps the path in `<bdo dir="ltr">` to preserve readable left-to-right
|
||||||
|
* display while `direction: rtl` flips the ellipsis to the start. */
|
||||||
.changes-file-path {
|
.changes-file-path {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
direction: rtl;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user