mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 17:02:17 +00:00
fix(vcs): fit the Git log toolbar and rows to narrow panes
- Filters that do not fit the toolbar move behind a chevron that lists them, so the action buttons on the right stay visible. - Commits on the current branch are tinted unless the log is filtered to that branch, instead of dimming the others only when other branches happen to be loaded. - The HEAD commit is drawn as a ring around a dot. - A wide graph no longer paints the subject and references over the author column; references shrink down to their icon. - Scrollbar thumbs keep a minimum length on long lists.
This commit is contained in:
1 parent
89475f4fed
commit
f2b8db548f
4 files changed
+152
-78
No files matched your search
@@ -75,6 +75,18 @@
|
||||
flex: 0 1 260px;
|
||||
min-width: 100px;
|
||||
}
|
||||
.filterGroup {
|
||||
position: relative;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.filterHidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
.filterButton {
|
||||
flex: none;
|
||||
height: 24px;
|
||||
@@ -153,20 +165,21 @@
|
||||
cursor: default;
|
||||
font-size: var(--fs-ui);
|
||||
}
|
||||
/* View Options › Highlight › Current Branch: commits outside HEAD are dimmed. */
|
||||
.offBranch .subject,
|
||||
.offBranch .muted {
|
||||
color: var(--fg-subtle);
|
||||
/* View Options › Highlight › Current Branch: commits reachable from HEAD are tinted. */
|
||||
.currentBranch {
|
||||
--log-row-bg: var(--bg-log-current-branch);
|
||||
background: var(--log-row-bg);
|
||||
}
|
||||
.row:hover {
|
||||
background: var(--bg-tree-hover);
|
||||
}
|
||||
.selected,
|
||||
.selected:hover {
|
||||
background: var(--bg-tree-selected-inactive);
|
||||
--log-row-bg: var(--bg-tree-selected-inactive);
|
||||
background: var(--log-row-bg);
|
||||
}
|
||||
.list:focus-within .selected {
|
||||
background: var(--bg-tree-selected);
|
||||
--log-row-bg: var(--bg-tree-selected);
|
||||
}
|
||||
.colSubject {
|
||||
flex: 1;
|
||||
@@ -174,8 +187,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
/* Let graph endpoints overlap adjacent rows; text is clipped by .subject. */
|
||||
overflow: visible;
|
||||
/* Let graph endpoints overlap adjacent rows, but never paint over the next column. */
|
||||
overflow-x: clip;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.colAuthor {
|
||||
flex: none;
|
||||
@@ -221,7 +235,9 @@
|
||||
overflow: visible;
|
||||
}
|
||||
.refs {
|
||||
flex: none;
|
||||
flex: 0 1 auto;
|
||||
/* Shrinks to the first label's icon when the graph leaves no room. */
|
||||
min-width: 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -242,6 +258,7 @@
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
max-width: 180px;
|
||||
}
|
||||
/* Outlined tag glyph in the ref's colour. */
|
||||
|
||||
+123
-69
@@ -508,25 +508,25 @@ const DATE_OPTIONS: { label: string; days: number }[] = [
|
||||
{ label: "Last Year", days: 365 },
|
||||
];
|
||||
|
||||
function menuBelow(e: ReactMouseEvent<HTMLElement>, items: MenuItem[]) {
|
||||
const r = e.currentTarget.getBoundingClientRect();
|
||||
function menuBelow(anchor: HTMLElement | ReactMouseEvent<HTMLElement>, items: MenuItem[]) {
|
||||
const r = ("currentTarget" in anchor ? anchor.currentTarget : anchor).getBoundingClientRect();
|
||||
showContextMenu({ x: r.left, y: r.bottom + 2 }, items);
|
||||
}
|
||||
|
||||
function FilterButton({
|
||||
label,
|
||||
value,
|
||||
onOpen,
|
||||
onClear,
|
||||
}: {
|
||||
interface LogFilter {
|
||||
label: string;
|
||||
value: string | null;
|
||||
onOpen: (e: ReactMouseEvent<HTMLElement>) => void;
|
||||
onOpen: (anchor: HTMLElement) => void;
|
||||
onClear: () => void;
|
||||
}) {
|
||||
}
|
||||
|
||||
function FilterButton({ label, value, onOpen, onClear, hidden }: LogFilter & { hidden?: boolean }) {
|
||||
return (
|
||||
<span className={clsx(styles.filterButton, value && styles.filterActive)}>
|
||||
<button type="button" className={styles.filterMain} onClick={onOpen} aria-haspopup="menu">
|
||||
<span
|
||||
className={clsx(styles.filterButton, value && styles.filterActive, hidden && styles.filterHidden)}
|
||||
inert={hidden}
|
||||
>
|
||||
<button type="button" className={styles.filterMain} onClick={(e) => onOpen(e.currentTarget)} aria-haspopup="menu">
|
||||
{value ? (
|
||||
<>
|
||||
{tx(label)}: <span className={styles.filterValue}>{value}</span>
|
||||
@@ -580,6 +580,23 @@ function FilterBar() {
|
||||
}, [repositories, filters.repositories, version]);
|
||||
const [text, setText] = useState(filters.text);
|
||||
const [branchAnchor, setBranchAnchor] = useState<HTMLElement | null>(null);
|
||||
// Filters that do not fit move behind a chevron, like a toolbar's overflow.
|
||||
const groupRef = useRef<HTMLSpanElement>(null);
|
||||
const [hiddenFrom, setHiddenFrom] = useState(Infinity);
|
||||
useLayoutEffect(() => {
|
||||
const group = groupRef.current;
|
||||
if (!group) return;
|
||||
const measure = () => {
|
||||
const items = [...group.children] as HTMLElement[];
|
||||
const i = items.findIndex((el) => el.offsetLeft + el.offsetWidth > group.clientWidth + 1);
|
||||
setHiddenFrom(i < 0 ? Infinity : i);
|
||||
};
|
||||
measure();
|
||||
const ro = new ResizeObserver(measure);
|
||||
ro.observe(group);
|
||||
for (const el of group.children) ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [filters, repositories.length]);
|
||||
|
||||
// Keep the field in sync when filters change elsewhere (Show History, reveal).
|
||||
const [seen, setSeen] = useState(filters);
|
||||
@@ -607,9 +624,9 @@ function FilterBar() {
|
||||
if (r) setLogBranches(r);
|
||||
};
|
||||
|
||||
const openUser = (e: ReactMouseEvent<HTMLElement>) => {
|
||||
const openUser = (anchor: HTMLElement) => {
|
||||
const authors = [...new Set(commits.map((c) => c.author))].sort((a, b) => a.localeCompare(b)).slice(0, 30);
|
||||
menuBelow(e, [
|
||||
menuBelow(anchor, [
|
||||
{ label: ME, checked: filters.authors.includes(ME), run: () => setLogFilters({ authors: [ME] }) },
|
||||
...(authors.length ? [{ kind: "separator" as const }] : []),
|
||||
...authors.map((a) => ({
|
||||
@@ -633,8 +650,8 @@ function FilterBar() {
|
||||
]);
|
||||
};
|
||||
|
||||
const openDate = (e: ReactMouseEvent<HTMLElement>) =>
|
||||
menuBelow(e, [
|
||||
const openDate = (anchor: HTMLElement) =>
|
||||
menuBelow(anchor, [
|
||||
...DATE_OPTIONS.map((o) => ({
|
||||
label: o.label,
|
||||
checked: filters.date?.label === o.label,
|
||||
@@ -650,8 +667,8 @@ function FilterBar() {
|
||||
},
|
||||
]);
|
||||
|
||||
const openPaths = (e: ReactMouseEvent<HTMLElement>) =>
|
||||
menuBelow(e, [
|
||||
const openPaths = (anchor: HTMLElement) =>
|
||||
menuBelow(anchor, [
|
||||
{
|
||||
label: "Select Folders…",
|
||||
run: async () => {
|
||||
@@ -664,6 +681,51 @@ function FilterBar() {
|
||||
},
|
||||
]);
|
||||
|
||||
const logFilters: LogFilter[] = [
|
||||
...(repositories.length > 1
|
||||
? [
|
||||
{
|
||||
label: "Repository",
|
||||
value: filters.repositories?.map(repoName).join(", ") || null,
|
||||
onOpen: (anchor: HTMLElement) =>
|
||||
menuBelow(
|
||||
anchor,
|
||||
repositories.map((repo) => ({
|
||||
label: repoName(repo),
|
||||
checked: filters.repositories?.includes(repo) ?? false,
|
||||
run: () => setLogFilters({ repositories: [repo] }),
|
||||
})),
|
||||
),
|
||||
onClear: () => setLogFilters({ repositories: [] }),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "Branch",
|
||||
value: filters.branches.join(", ") || null,
|
||||
onOpen: setBranchAnchor,
|
||||
onClear: () => setLogBranches([]),
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
value: filters.authors.join(", ") || null,
|
||||
onOpen: openUser,
|
||||
onClear: () => setLogFilters({ authors: [] }),
|
||||
},
|
||||
{
|
||||
label: "Date",
|
||||
value: filters.date?.label ?? null,
|
||||
onOpen: openDate,
|
||||
onClear: () => setLogFilters({ date: null }),
|
||||
},
|
||||
{
|
||||
label: "Paths",
|
||||
value: filters.path ? (root ? relTo(root, filters.path) : filters.path) || "." : null,
|
||||
onOpen: openPaths,
|
||||
onClear: () => setLogFilters({ path: null }),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.filters} role="toolbar" aria-label={tx("Log filters")}>
|
||||
{!showBranches && (
|
||||
@@ -692,29 +754,29 @@ function FilterBar() {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{repositories.length > 1 && (
|
||||
<FilterButton
|
||||
label="Repository"
|
||||
value={filters.repositories?.map(repoName).join(", ") || null}
|
||||
onOpen={(e) =>
|
||||
<span ref={groupRef} className={styles.filterGroup}>
|
||||
{logFilters.map((f, i) => (
|
||||
<FilterButton key={f.label} {...f} hidden={i >= hiddenFrom} />
|
||||
))}
|
||||
</span>
|
||||
{hiddenFrom < logFilters.length && (
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
title="More Filters"
|
||||
size="small"
|
||||
aria-haspopup="menu"
|
||||
onClick={(e) => {
|
||||
const anchor = e.currentTarget;
|
||||
menuBelow(
|
||||
e,
|
||||
repositories.map((repo) => ({
|
||||
label: repoName(repo),
|
||||
checked: filters.repositories?.includes(repo) ?? false,
|
||||
run: () => setLogFilters({ repositories: [repo] }),
|
||||
anchor,
|
||||
logFilters.slice(hiddenFrom).map((f) => ({
|
||||
label: f.value ? `${tx(f.label)}: ${f.value}` : f.label,
|
||||
run: () => f.onOpen(anchor),
|
||||
})),
|
||||
)
|
||||
}
|
||||
onClear={() => setLogFilters({ repositories: [] })}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FilterButton
|
||||
label="Branch"
|
||||
value={filters.branches.join(", ") || null}
|
||||
onOpen={(e) => setBranchAnchor(e.currentTarget)}
|
||||
onClear={() => setLogBranches([])}
|
||||
/>
|
||||
{branchAnchor && (
|
||||
<BranchFilterPopup
|
||||
anchor={branchAnchor}
|
||||
@@ -728,24 +790,6 @@ function FilterBar() {
|
||||
onSelectMany={() => void selectBranches()}
|
||||
/>
|
||||
)}
|
||||
<FilterButton
|
||||
label="User"
|
||||
value={filters.authors.join(", ") || null}
|
||||
onOpen={openUser}
|
||||
onClear={() => setLogFilters({ authors: [] })}
|
||||
/>
|
||||
<FilterButton
|
||||
label="Date"
|
||||
value={filters.date?.label ?? null}
|
||||
onOpen={openDate}
|
||||
onClear={() => setLogFilters({ date: null })}
|
||||
/>
|
||||
<FilterButton
|
||||
label="Paths"
|
||||
value={filters.path ? (root ? relTo(root, filters.path) : filters.path) || "." : null}
|
||||
onOpen={openPaths}
|
||||
onClear={() => setLogFilters({ path: null })}
|
||||
/>
|
||||
<span className={styles.grow} />
|
||||
<IconButton icon="refresh" title="Refresh" size="small" onClick={() => void reloadLog({ preserve: true })} />
|
||||
<IconButton
|
||||
@@ -909,13 +953,16 @@ function CommitList() {
|
||||
const laid = collapsed ? computeGraph(collapsed.graph).rows : allRows;
|
||||
return opts.longEdges ? laid : hideLongEdges(laid);
|
||||
}, [collapsed, allRows, opts.longEdges]);
|
||||
const filterBranches = useGitLog((s) => s.filters.branches);
|
||||
const currentBranch = useVcs((s) => s.branch?.head ?? null);
|
||||
// Nothing to tell apart when the log is filtered to the current branch.
|
||||
const onlyCurrent = filterBranches.length > 0 && filterBranches.every((b) => b === "HEAD" || b === currentBranch);
|
||||
const onBranch = useMemo(() => {
|
||||
if (!opts.highlightCurrentBranch) return null;
|
||||
if (!opts.highlightCurrentBranch || onlyCurrent) return null;
|
||||
const reach = new Set<string>();
|
||||
for (const head of headKeys) for (const id of reachableFrom(all.map(graphCommit), head)) reach.add(id);
|
||||
// Nothing to tell apart when only the current branch is shown.
|
||||
return all.every((c) => reach.has(commitKey(c))) ? null : reach;
|
||||
}, [all, headKeys, opts.highlightCurrentBranch]);
|
||||
return reach;
|
||||
}, [all, headKeys, opts.highlightCurrentBranch, onlyCurrent]);
|
||||
const loading = useGitLog((s) => s.loading);
|
||||
const done = useGitLog((s) => s.done);
|
||||
const error = useGitLog((s) => s.error);
|
||||
@@ -1025,7 +1072,7 @@ function CommitList() {
|
||||
selected={selectedSet.has(commitKey(c))}
|
||||
head={headKeys.has(commitKey(c))}
|
||||
mine={opts.highlightMine && isMine(c, user)}
|
||||
onBranch={!onBranch || onBranch.has(commitKey(c))}
|
||||
onBranch={!!onBranch?.has(commitKey(c))}
|
||||
opts={opts}
|
||||
remoteNames={new Set((c as RepoCommit).remoteNames ?? remoteNames)}
|
||||
onPick={pick}
|
||||
@@ -1079,7 +1126,7 @@ const CommitRow = memo(function CommitRow({
|
||||
id={id}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
className={clsx(styles.row, !onBranch && styles.offBranch, selected && styles.selected)}
|
||||
className={clsx(styles.row, onBranch && styles.currentBranch, selected && styles.selected)}
|
||||
style={{ top }}
|
||||
onMouseDown={(e) => {
|
||||
if (e.shiftKey) e.preventDefault(); // no text selection on range clicks
|
||||
@@ -1150,14 +1197,21 @@ export function GraphCell({ row, head }: { row: GraphRow; head?: boolean }) {
|
||||
strokeDasharray={s.dashed ? "2 2" : undefined}
|
||||
/>
|
||||
))}
|
||||
<circle
|
||||
cx={x(row.lane)}
|
||||
cy={cy}
|
||||
r={head ? 4.5 : 3.8}
|
||||
fill={head ? "var(--bg-app)" : laneColor(row.color)}
|
||||
stroke={laneColor(row.color)}
|
||||
strokeWidth={head ? 2 : 0}
|
||||
/>
|
||||
{head ? (
|
||||
<>
|
||||
<circle
|
||||
cx={x(row.lane)}
|
||||
cy={cy}
|
||||
r={5}
|
||||
fill="var(--log-row-bg, var(--bg-app))"
|
||||
stroke={laneColor(row.color)}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<circle cx={x(row.lane)} cy={cy} r={2.5} fill={laneColor(row.color)} />
|
||||
</>
|
||||
) : (
|
||||
<circle cx={x(row.lane)} cy={cy} r={3.8} fill={laneColor(row.color)} />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,6 +171,8 @@ a:hover {
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
border-radius: 6px;
|
||||
min-height: 32px;
|
||||
min-width: 32px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--scrollbar-thumb-hover);
|
||||
|
||||
@@ -264,6 +264,7 @@ html {
|
||||
--bg-scrim: rgba(0, 0, 0, 0.45);
|
||||
--fg-on-accent: #ffffff;
|
||||
--accent-soft: color-mix(in srgb, var(--accent) 16%, transparent);
|
||||
--bg-log-current-branch: color-mix(in srgb, var(--accent) 8%, var(--bg-app));
|
||||
--accent-hover: color-mix(in srgb, var(--accent) 86%, var(--fg-strong));
|
||||
--match: var(--accent);
|
||||
--shadow-tooltip: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
|
||||
Reference in new issue
Block a user