feat(history-ui): add resizable commit history aside

The commit history panel is now resizable with a draggable handle and
keyboard support. The chosen width is persisted in local storage so the
layout stays consistent across sessions. Git commit loading was also
adjusted to include all branch tips for a more complete graph.

- Add width persistence and pointer/keyboard resizing for history panel
- Update commit graph query to use topo-order across all refs
- Extend tests to ensure branch tips are included in commit results
This commit is contained in:
Christoph Brandau
2026-07-03 23:35:46 +02:00
parent 835bfae254
commit 9c371ec520
4 changed files with 821 additions and 100 deletions
+384 -79
View File
@@ -187,6 +187,199 @@
}
.section-head h2 { margin: 1px 0 0; color: var(--color-ink); font-size: 14px; line-height: 1.2; font-weight: 600; }
.section-head-actions {
display: flex;
align-items: center;
flex: 0 0 auto;
justify-content: flex-end;
gap: 8px;
min-width: 0;
}
.graph-branch-dialog-button {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 26px;
padding: 0 9px;
overflow: hidden;
border: 1px solid rgba(91,209,138,0.24);
border-radius: 999px;
color: #b6f1c4;
background: rgba(34,68,48,0.28);
font-size: 11px;
font-weight: 800;
white-space: nowrap;
transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
}
.graph-branch-dialog-button:hover {
border-color: rgba(91,209,138,0.42);
background: rgba(34,68,48,0.42);
}
.graph-branch-dialog-button span {
padding: 1px 5px;
border-radius: 999px;
color: #061021;
background: #6ce18f;
font-size: 10px;
line-height: 1.2;
}
.branch-filter-backdrop {
position: fixed;
inset: 0;
z-index: 80;
display: grid;
place-items: center;
padding: 24px;
background: rgba(4,8,18,0.58);
backdrop-filter: blur(8px);
}
.branch-filter-dialog {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
width: min(520px, 100%);
max-height: min(680px, calc(100vh - 48px));
overflow: hidden;
border: 1px solid rgba(94,110,156,0.24);
border-radius: 10px;
background:
linear-gradient(180deg, rgba(255,255,255,0.045), transparent 70%),
var(--color-surface);
box-shadow: 0 24px 80px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05);
}
.branch-filter-dialog-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 14px 12px;
border-bottom: 1px solid var(--color-border-subtle);
background: linear-gradient(90deg, rgba(100,108,255,0.12), rgba(65,209,255,0.04));
}
.branch-filter-dialog-head h3 {
margin: 1px 0 0;
color: var(--color-ink);
font-size: 16px;
font-weight: 750;
}
.dialog-icon-button {
display: inline-grid;
place-items: center;
width: 30px;
height: 30px;
padding: 0;
border: 1px solid rgba(94,110,156,0.18);
border-radius: 7px;
color: var(--color-ink-dim);
background: rgba(255,255,255,0.035);
}
.dialog-icon-button:hover {
color: var(--color-ink);
border-color: rgba(65,209,255,0.28);
background: rgba(65,209,255,0.08);
}
.branch-filter-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid var(--color-border-subtle);
color: var(--color-ink-dim);
font-size: 12px;
font-weight: 700;
}
.branch-filter-actions {
display: inline-flex;
gap: 6px;
}
.branch-filter-actions button {
min-height: 24px;
padding: 0 8px;
border-color: rgba(94,110,156,0.16);
border-radius: 999px;
color: var(--color-ink-dim);
background: rgba(255,255,255,0.035);
font-size: 10.5px;
font-weight: 800;
}
.branch-filter-actions button:hover:not(:disabled) {
border-color: rgba(65,209,255,0.28);
color: var(--color-ink);
background: rgba(65,209,255,0.08);
}
.branch-filter-actions button:disabled {
cursor: default;
opacity: 0.45;
}
.branch-filter-dialog-list {
display: grid;
align-content: start;
gap: 4px;
min-height: 0;
padding: 8px;
overflow: auto;
scrollbar-width: thin;
}
.branch-filter-option {
display: grid;
grid-template-columns: 16px 16px minmax(0, 1fr);
align-items: center;
gap: 8px;
min-height: 32px;
padding: 0 9px;
border: 1px solid transparent;
border-radius: 7px;
color: #a8eeba;
background: transparent;
cursor: pointer;
font-size: 12px;
font-weight: 750;
}
.branch-filter-option:hover {
border-color: rgba(91,209,138,0.18);
background: rgba(34,68,48,0.22);
}
.branch-filter-option.muted {
color: var(--color-ink-faint);
opacity: 0.72;
}
.branch-filter-option input {
width: 14px;
height: 14px;
accent-color: #6ce18f;
}
.branch-filter-option svg {
color: currentColor;
}
.branch-filter-option span {
min-width: 0;
overflow: hidden;
font-family: var(--font-mono);
text-overflow: ellipsis;
white-space: nowrap;
}
.eyebrow {
display: block;
color: var(--color-ink-faint);
@@ -862,7 +1055,7 @@
.workspace {
display: grid;
grid-template-columns: clamp(220px, 18vw, 280px) minmax(0, 1fr) clamp(400px, 40vw, 620px);
grid-template-columns: clamp(220px, 18vw, 280px) minmax(0, 1fr) minmax(560px, var(--history-aside-width, 620px));
flex: 1 1 0;
min-width: 0;
min-height: 0;
@@ -870,13 +1063,45 @@
}
.history-aside {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
grid-template-columns: minmax(0, 1fr) 300px;
min-width: 0;
min-height: 0;
gap: 8px;
}
.history-resize-handle {
position: absolute;
z-index: 20;
top: 0;
bottom: 0;
left: -7px;
width: 12px;
cursor: col-resize;
touch-action: none;
}
.history-resize-handle::before {
content: "";
position: absolute;
top: 12px;
bottom: 12px;
left: 5px;
width: 2px;
border-radius: 999px;
background: transparent;
transition: background 120ms ease, box-shadow 120ms ease;
}
.history-resize-handle:hover::before,
.history-resize-handle.resizing::before {
background: rgba(65,209,255,0.62);
box-shadow: 0 0 14px rgba(65,209,255,0.3);
}
.history-resize-handle:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.left-sidebar {
display: grid;
grid-template-rows: minmax(200px, 0.9fr) minmax(240px, 1.1fr);
@@ -1431,97 +1656,126 @@
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 10px;
gap: 7px;
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;
width: 24px;
height: 24px;
border: 1px solid rgba(122,172,255,0.18);
border-radius: 999px;
color: #b8c5df;
background: rgba(18, 22, 38, 0.72);
font-size: 9px;
font-weight: 800;
}
.commit-card-main { display: grid; gap: 4px; min-width: 0; }
.commit-card-main { display: grid; gap: 2px; min-width: 0; }
.commit-title-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
gap: 6px;
min-width: 0;
}
.commit-summary {
display: block;
overflow: hidden;
color: #f6f8ff;
font-size: 13.5px;
font-weight: 800;
line-height: 1.28;
color: #edf2ff;
font-size: 12.5px;
font-weight: 700;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.commit-kind {
display: inline-flex;
align-items: center;
gap: 4px;
gap: 3px;
min-width: 0;
padding: 2px 7px;
border: 1px solid rgba(94,110,156,0.24);
padding: 1px 5px;
border: 1px solid rgba(94,110,156,0.18);
border-radius: 999px;
color: var(--color-ink-dim);
background: rgba(255,255,255,0.035);
font-size: 10px;
font-weight: 900;
background: rgba(255,255,255,0.025);
font-size: 9px;
font-weight: 800;
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-kind.merge { color: #dca6da; border-color: rgba(208,96,192,0.24); background: rgba(208,96,192,0.07); }
.commit-kind.root { color: #d7b66b; border-color: rgba(224,180,92,0.22); background: rgba(224,180,92,0.07); }
.commit-meta-line {
display: flex;
align-items: center;
gap: 7px;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
color: var(--color-ink-faint);
font-size: 11px;
font-size: 10.5px;
}
.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);
padding: 0;
border: 0;
color: #8db8ff;
background: transparent;
font-family: var(--font-mono);
font-size: 10.5px;
font-weight: 800;
font-weight: 700;
}
.commit-local-branches {
display: inline-flex;
flex: 0 1 auto;
flex-wrap: wrap;
gap: 3px;
min-width: 0;
max-width: min(100%, 260px);
}
.commit-branch-chip {
display: inline-flex;
align-items: center;
gap: 3px;
min-width: 0;
max-width: 100%;
height: 17px;
padding: 0 6px 0 5px;
overflow: hidden;
border: 1px solid rgba(91,209,138,0.22);
border-radius: 999px;
color: #a8eeba;
background: rgba(34,68,48,0.42);
font-family: var(--font-mono);
font-size: 9.5px;
font-weight: 750;
line-height: 1;
text-overflow: ellipsis;
white-space: nowrap;
}
.commit-branch-chip svg {
flex: 0 0 auto;
color: #76d995;
}
.commit-author {
flex: 1 1 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ref-list { display: flex; flex-wrap: wrap; gap: 4px; }
.ref-list { display: flex; flex-wrap: wrap; gap: 3px; }
.ref-list .ref-chip {
max-width: 100%;
overflow: hidden;
padding: 2px 7px;
padding: 1px 6px;
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;
background: rgba(106,154,255,0.09);
border: 1px solid rgba(106,154,255,0.16);
font-size: 9.5px;
font-weight: 700;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
@@ -1532,9 +1786,9 @@
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); }
.ref-list .ref-chip.branch { color: #7ddf9c; background: rgba(78,202,118,0.08); border-color: rgba(78,202,118,0.18); }
.ref-list .ref-chip.remote { color: #aeb6ff; background: rgba(124,108,255,0.08); border-color: rgba(124,108,255,0.18); }
.ref-list .ref-chip.tag { color: #dbc078; background: rgba(224,180,92,0.08); border-color: rgba(224,180,92,0.2); }
.commit-files {
display: grid;
@@ -1695,16 +1949,14 @@
.graph-list {
padding: 0;
background:
linear-gradient(90deg, rgba(65,209,255,0.04), transparent 22%),
var(--color-surface-dim);
background: var(--color-surface-dim);
}
.graph-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 0;
min-height: 86px;
min-height: 58px;
margin: 0;
padding: 0;
border: none;
@@ -1716,57 +1968,109 @@
.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);
min-width: 42px;
border-right: 1px solid rgba(94,110,156,0.12);
background: rgba(7,8,16,0.2);
}
.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;
opacity: 0.76;
transition: opacity 120ms ease, stroke-width 120ms ease;
}
.graph-svg path.hidden-branch {
opacity: 0.08;
}
.graph-dot {
position: absolute;
z-index: 2;
top: 50%;
width: 13px;
height: 13px;
width: 12px;
height: 12px;
border-radius: 999px;
background: 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);
box-shadow: 0 0 0 1px rgba(255,255,255,0.07);
transform: translate(-50%, -50%);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.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); }
.graph-dot.hidden-branch {
opacity: 0.16;
box-shadow: none;
}
.graph-dot.tip { width: 14px; height: 14px; box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8); }
.graph-dot.merge {
width: 15px;
height: 15px;
background: var(--color-surface-alt);
border-color: var(--dot-color, #5a8cf8);
box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
}
.graph-hover-branches {
position: absolute;
z-index: 5;
top: 50%;
display: flex;
flex-wrap: wrap;
gap: 3px;
max-width: 190px;
opacity: 0;
pointer-events: none;
transform: translateY(-50%) translateX(-4px);
transition: opacity 120ms ease, transform 120ms ease;
}
.graph-gutter:hover .graph-hover-branches,
.graph-row:hover .graph-hover-branches {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
.graph-hover-branches span {
display: inline-flex;
align-items: center;
gap: 3px;
max-width: 180px;
height: 18px;
padding: 0 6px 0 5px;
overflow: hidden;
border: 1px solid rgba(91,209,138,0.28);
border-radius: 999px;
color: #b2f0c2;
background: rgba(20,35,29,0.94);
box-shadow: 0 8px 22px rgba(0,0,0,0.3);
font-family: var(--font-mono);
font-size: 9.5px;
font-weight: 750;
line-height: 1;
text-overflow: ellipsis;
white-space: nowrap;
}
.graph-hover-branches svg {
flex: 0 0 auto;
color: #76d995;
}
.commit-body {
display: grid;
gap: 8px;
gap: 5px;
min-width: 0;
padding: 12px;
background:
linear-gradient(90deg, rgba(255,255,255,0.035), transparent 34%),
rgba(28,29,48,0.55);
padding: 8px 10px;
background: rgba(28,29,48,0.34);
transition: background 120ms ease;
}
.graph-row + .graph-row .commit-body { border-top: 1px solid var(--color-border-subtle); }
.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 + .graph-row .commit-body { border-top: 1px solid rgba(94,110,156,0.1); }
.graph-row:hover .commit-body { background: rgba(37,40,62,0.54); }
.graph-row:hover .graph-svg path { opacity: 1; stroke-width: 2.65; }
.graph-row:hover .graph-svg path.hidden-branch { opacity: 0.12; stroke-width: 2.2; }
.graph-row:hover .graph-dot { transform: translate(-50%, -50%) scale(1.12); }
.graph-row:hover .graph-dot.hidden-branch { transform: translate(-50%, -50%) scale(1); }
.graph-row.merge-row .commit-body {
background: rgba(36,31,54,0.46);
}
.graph-row.tip-row .commit-body {
background:
linear-gradient(90deg, rgba(208,96,192,0.095), transparent 34%),
rgba(28,29,48,0.62);
linear-gradient(90deg, rgba(105,167,255,0.055), transparent 32%),
rgba(28,29,48,0.38);
}
/* --- Compare panel --- */
@@ -3241,16 +3545,16 @@
/* --- Responsive breakpoints --- */
@media (min-width: 1800px) {
.workspace { grid-template-columns: 320px minmax(0, 1fr) 680px; }
.workspace { grid-template-columns: 320px minmax(0, 1fr) minmax(560px, var(--history-aside-width, 680px)); }
}
@media (max-width: 1400px) {
.workspace { grid-template-columns: clamp(200px, 17vw, 265px) minmax(0, 1fr) clamp(400px, 40vw, 580px); }
.workspace { grid-template-columns: clamp(200px, 17vw, 265px) minmax(0, 1fr) minmax(540px, var(--history-aside-width, 580px)); }
}
/* Stack CommitPanel below StatusPanel; history panels stay side by side */
@media (max-width: 1100px) {
.workspace { grid-template-columns: clamp(185px, 16vw, 220px) minmax(0, 1fr) clamp(380px, 38vw, 500px); }
.workspace { grid-template-columns: clamp(185px, 16vw, 220px) minmax(0, 1fr) minmax(500px, var(--history-aside-width, 560px)); }
.top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 14px var(--commit-panel-height, 190px); }
}
@@ -3258,6 +3562,7 @@
@media (max-width: 960px) {
.workspace { grid-template-columns: 180px minmax(0, 1fr) 300px; gap: 6px; }
.history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1.3fr) minmax(0, 0.7fr); }
.history-resize-handle { display: none; }
.shell-body { gap: 6px; }
.left-sidebar { gap: 6px; grid-template-rows: minmax(180px, 0.8fr) minmax(200px, 1.2fr); }
.section-head { min-height: 40px; padding: 6px 10px; }