feat(history): enhance commit history UI with richer styling

The commit history panel now renders a more structured commit header,
including an avatar, kind badge, and clearer metadata layout. The git
graph visuals were also refined with smoother paths and improved hover
effects to make the history easier to scan.

- Add commit kind/root/merge badges and author initials
- Improve graph rendering with curved paths and hover emphasis
- Update CSS for commit cards, ref chips, and graph styling
This commit is contained in:
Christoph Brandau
2026-07-03 21:32:20 +02:00
parent 2bacd473fc
commit 835bfae254
2 changed files with 281 additions and 32 deletions
+210 -16
View File
@@ -1427,19 +1427,161 @@
.commit-line strong { display: block; overflow: hidden; color: var(--color-ink); font-size: 13px; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }
.commit-line span { display: block; overflow: hidden; margin-top: 3px; color: var(--color-ink-dim); font-family: var(--font-mono); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.ref-list { display: flex; flex-wrap: wrap; gap: 4px; }
.ref-list span { max-width: 100%; overflow: hidden; padding: 2px 7px; border-radius: 999px; color: var(--color-accent); background: rgba(106,154,255,0.13); border: 1px solid rgba(106,154,255,0.22); font-size: 10.5px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
.commit-card-head {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 10px;
min-width: 0;
}
.commit-avatar {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border: 1px solid rgba(122,172,255,0.28);
border-radius: 10px;
color: #dce8ff;
background:
linear-gradient(180deg, rgba(122,172,255,0.2), rgba(65,209,255,0.08)),
rgba(18, 22, 38, 0.84);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 8px 20px rgba(0,0,0,0.16);
font-size: 11px;
font-weight: 900;
letter-spacing: 0.02em;
}
.commit-card-main { display: grid; gap: 4px; min-width: 0; }
.commit-title-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
min-width: 0;
}
.commit-summary {
display: block;
overflow: hidden;
color: #f6f8ff;
font-size: 13.5px;
font-weight: 800;
line-height: 1.28;
text-overflow: ellipsis;
white-space: nowrap;
}
.commit-kind {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
padding: 2px 7px;
border: 1px solid rgba(94,110,156,0.24);
border-radius: 999px;
color: var(--color-ink-dim);
background: rgba(255,255,255,0.035);
font-size: 10px;
font-weight: 900;
line-height: 1.3;
text-transform: uppercase;
}
.commit-kind.merge { color: #f09de2; border-color: rgba(208,96,192,0.32); background: rgba(208,96,192,0.12); }
.commit-kind.root { color: #e0b45c; border-color: rgba(224,180,92,0.3); background: rgba(224,180,92,0.11); }
.commit-meta-line {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
color: var(--color-ink-faint);
font-size: 11px;
}
.commit-meta-line > * { min-width: 0; }
.commit-hash {
flex: 0 0 auto;
padding: 2px 6px;
border: 1px solid rgba(65,209,255,0.16);
border-radius: 6px;
color: var(--color-accent);
background: rgba(65,209,255,0.075);
font-family: var(--font-mono);
font-size: 10.5px;
font-weight: 800;
}
.commit-author {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.commit-files { display: grid; gap: 5px; }
.commit-files-toggle { justify-content: flex-start; gap: 5px; min-height: 24px; padding: 0 7px; border-color: transparent; background: transparent; color: var(--color-ink-dim); font-size: 11.5px; font-weight: 700; }
.ref-list { display: flex; flex-wrap: wrap; gap: 4px; }
.ref-list .ref-chip {
max-width: 100%;
overflow: hidden;
padding: 2px 7px;
border-radius: 999px;
color: var(--color-accent);
background: rgba(106,154,255,0.13);
border: 1px solid rgba(106,154,255,0.22);
font-size: 10.5px;
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.ref-list .ref-chip.head {
color: #061021;
border-color: rgba(65,209,255,0.48);
background: linear-gradient(135deg, #41d1ff, #7c6cff);
box-shadow: 0 0 14px rgba(65,209,255,0.2);
}
.ref-list .ref-chip.branch { color: #55d886; background: rgba(78,202,118,0.12); border-color: rgba(78,202,118,0.26); }
.ref-list .ref-chip.remote { color: #9aa7ff; background: rgba(124,108,255,0.12); border-color: rgba(124,108,255,0.26); }
.ref-list .ref-chip.tag { color: #e0b45c; background: rgba(224,180,92,0.12); border-color: rgba(224,180,92,0.28); }
.commit-files {
display: grid;
gap: 5px;
padding: 6px;
border: 1px solid rgba(94,110,156,0.14);
border-radius: 8px;
background: rgba(7,8,16,0.16);
}
.commit-files-toggle {
justify-content: flex-start;
gap: 6px;
min-height: 26px;
padding: 0 8px;
border-color: transparent;
background: transparent;
color: var(--color-ink-dim);
font-size: 11.5px;
font-weight: 800;
}
.commit-files-toggle:hover:not(:disabled) { border-color: var(--color-border-subtle); background: var(--color-surface-hover); color: var(--color-ink); }
.commit-file-list { display: grid; gap: 4px; }
.commit-file-button { display: grid; grid-template-columns: auto minmax(0, 1fr); justify-content: stretch; width: 100%; min-height: 28px; padding: 4px 7px; text-align: left; border-color: var(--color-border-subtle); background: rgba(255,255,255,0.025); }
.commit-file-button {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
justify-content: stretch;
width: 100%;
min-height: 30px;
padding: 4px 7px;
text-align: left;
border-color: rgba(94,110,156,0.16);
background: rgba(255,255,255,0.03);
}
.commit-file-button:hover:not(:disabled) { border-color: rgba(65,209,255,0.22); background: rgba(65,209,255,0.055); }
.commit-file-button strong { overflow: hidden; color: var(--color-ink-muted); font-family: var(--font-mono); font-size: 11.5px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.commit-actions { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; }
.commit-actions time { min-width: 0; overflow: hidden; color: var(--color-ink-faint); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.commit-actions time,
.commit-time {
min-width: 0;
overflow: hidden;
color: var(--color-ink-faint);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.commit-action-buttons { display: flex; flex: 0 0 auto; align-items: center; gap: 5px; }
.commit-action-buttons button { flex: 0 0 auto; white-space: nowrap; }
.file-history-head { align-items: flex-start; }
@@ -1551,29 +1693,81 @@
/* --- Git graph --- */
.graph-list { padding: 0; }
.graph-list {
padding: 0;
background:
linear-gradient(90deg, rgba(65,209,255,0.04), transparent 22%),
var(--color-surface-dim);
}
.graph-row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0; margin: 0; padding: 0; border: none; border-radius: 0; background: none; }
.graph-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 0;
min-height: 86px;
margin: 0;
padding: 0;
border: none;
border-radius: 0;
background: none;
}
.graph-row + .graph-row { margin-top: 0; }
.graph-gutter { position: relative; align-self: stretch; background: var(--color-surface); }
.graph-gutter {
position: relative;
align-self: stretch;
border-right: 1px solid rgba(94,110,156,0.18);
background:
radial-gradient(circle at 50% 50%, rgba(255,255,255,0.055), transparent 34%),
linear-gradient(90deg, rgba(8,10,18,0.48), rgba(22,22,36,0.9));
box-shadow: inset -10px 0 16px rgba(0,0,0,0.12);
}
.graph-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.graph-svg path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.78;
transition: opacity 120ms ease, stroke-width 120ms ease;
}
.graph-dot {
position: absolute;
z-index: 2;
top: 50%;
width: 10px;
height: 10px;
width: 13px;
height: 13px;
border-radius: 999px;
background: var(--dot-color, #5a8cf8);
border: 2px solid var(--color-surface);
box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8);
border: 2px solid #111321;
box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8), 0 0 14px rgba(65,209,255,0.14);
transform: translate(-50%, -50%);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.graph-dot.merge { width: 12px; height: 12px; background: var(--color-surface); border-color: var(--dot-color, #5a8cf8); }
.graph-dot.merge { width: 16px; height: 16px; background: var(--color-surface-alt); border-color: var(--dot-color, #5a8cf8); box-shadow: 0 0 0 2px rgba(255,255,255,0.08), 0 0 16px rgba(208,96,192,0.24); }
.commit-body { display: grid; gap: 7px; min-width: 0; padding: 10px 12px; }
.commit-body {
display: grid;
gap: 8px;
min-width: 0;
padding: 12px;
background:
linear-gradient(90deg, rgba(255,255,255,0.035), transparent 34%),
rgba(28,29,48,0.55);
transition: background 120ms ease;
}
.graph-row + .graph-row .commit-body { border-top: 1px solid var(--color-border-subtle); }
.graph-row:hover .commit-body { background: var(--color-surface-hover); }
.graph-row:hover .commit-body {
background:
linear-gradient(90deg, rgba(65,209,255,0.075), transparent 36%),
var(--color-surface-hover);
}
.graph-row:hover .graph-svg path { opacity: 1; stroke-width: 2.6; }
.graph-row:hover .graph-dot { transform: translate(-50%, -50%) scale(1.08); }
.graph-row.merge-row .commit-body {
background:
linear-gradient(90deg, rgba(208,96,192,0.095), transparent 34%),
rgba(28,29,48,0.62);
}
/* --- Compare panel --- */
+71 -16
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { ChevronDown, ChevronRight, GitBranch, RotateCcw } from "@lucide/svelte";
import { ChevronDown, ChevronRight, GitBranch, GitMerge, RotateCcw } from "@lucide/svelte";
import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";
interface GraphSegment {
@@ -51,6 +51,14 @@
return col * GRAPH_LANE + GRAPH_LANE / 2;
}
function graphPath(seg: GraphSegment, fromY: number, toY: number): string {
const x1 = graphColX(seg.fromCol);
const x2 = graphColX(seg.toCol);
if (x1 === x2) return `M ${x1} ${fromY} L ${x2} ${toY}`;
const midY = (fromY + toY) / 2;
return `M ${x1} ${fromY} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${toY}`;
}
function computeGraph(items: GitCommit[]): { rows: GraphRow[]; columns: number } {
const rows: GraphRow[] = [];
let lanes: (string | null)[] = [];
@@ -123,6 +131,37 @@
: baseName(file.path);
}
function authorInitials(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "?";
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
}
function commitKind(commit: GitCommit): "merge" | "root" | "commit" {
if (commit.parents.length > 1) return "merge";
if (commit.parents.length === 0) return "root";
return "commit";
}
function commitKindLabel(commit: GitCommit): string {
const kind = commitKind(commit);
if (kind === "merge") return "merge";
if (kind === "root") return "root";
return "commit";
}
function refClass(ref: string): string {
if (ref.startsWith("HEAD")) return "head";
if (ref.startsWith("tag:")) return "tag";
if (ref.includes("/")) return "remote";
return "branch";
}
function refLabel(ref: string): string {
return ref.replace(/^HEAD ->\s*/, "").replace(/^tag:\s*/, "");
}
function formatCommitDate(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
@@ -151,22 +190,24 @@
<div class="history-list graph-list overflow-auto">
{#each commits as item, rowIndex (item.hash)}
{@const row = graphRows[rowIndex]}
<article class="commit-row graph-row">
<article class="commit-row graph-row" class:merge-row={item.parents.length > 1} class:root-row={item.parents.length === 0}>
<div class="graph-gutter" style={`width:${graphWidth}px`} aria-hidden="true">
{#if row}
<svg class="graph-svg" viewBox={`0 0 ${graphWidth} 100`} preserveAspectRatio="none">
{#each row.top as seg}
<line
x1={graphColX(seg.fromCol)} y1="0"
x2={graphColX(seg.toCol)} y2="50"
stroke={seg.color} stroke-width="2" vector-effect="non-scaling-stroke"
<path
d={graphPath(seg, 0, 50)}
stroke={seg.color}
stroke-width="2"
vector-effect="non-scaling-stroke"
/>
{/each}
{#each row.bottom as seg}
<line
x1={graphColX(seg.fromCol)} y1="50"
x2={graphColX(seg.toCol)} y2="100"
stroke={seg.color} stroke-width="2" vector-effect="non-scaling-stroke"
<path
d={graphPath(seg, 50, 100)}
stroke={seg.color}
stroke-width="2"
vector-effect="non-scaling-stroke"
/>
{/each}
</svg>
@@ -179,17 +220,31 @@
</div>
<div class="commit-body">
<div class="commit-line">
<div>
<strong title={item.summary}>{item.summary}</strong>
<span>{item.short_hash} - {item.author_name}</span>
<div class="commit-card-head">
<span class="commit-avatar">
{authorInitials(item.author_name)}
</span>
<div class="commit-card-main">
<div class="commit-title-row">
<strong class="commit-summary" title={item.summary}>{item.summary}</strong>
<span class={`commit-kind ${commitKind(item)}`}>
{#if item.parents.length > 1}
<GitMerge size={12} aria-hidden="true" />
{/if}
{commitKindLabel(item)}
</span>
</div>
<div class="commit-meta-line">
<span class="commit-hash">{item.short_hash}</span>
<span class="commit-author" title={item.author_email}>{item.author_name}</span>
</div>
</div>
</div>
{#if item.refs.length > 0}
<div class="ref-list" aria-label="Commit refs">
{#each item.refs as ref}
<span>{ref}</span>
<span class={`ref-chip ${refClass(ref)}`}>{refLabel(ref)}</span>
{/each}
</div>
{/if}
@@ -230,7 +285,7 @@
{/if}
<div class="commit-actions">
<time datetime={item.date}>{formatCommitDate(item.date)}</time>
<time class="commit-time" datetime={item.date}>{formatCommitDate(item.date)}</time>
<div class="commit-action-buttons">
<button class="btn-sm" type="button" onclick={() => onCreateBranchFromCommit(item)} disabled={isBusy} title="Create a new branch from this commit">
<GitBranch size={15} aria-hidden="true" />