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:
@@ -996,6 +996,8 @@ fn commits_for_repo(repo: &Path, limit: Option<u32>) -> Result<Vec<GitCommit>, S
|
|||||||
repo,
|
repo,
|
||||||
[
|
[
|
||||||
"log",
|
"log",
|
||||||
|
"--all",
|
||||||
|
"--topo-order",
|
||||||
"--decorate=short",
|
"--decorate=short",
|
||||||
"--name-status",
|
"--name-status",
|
||||||
"-M",
|
"-M",
|
||||||
@@ -3869,6 +3871,33 @@ mod tests {
|
|||||||
assert!(comparison.patch.contains("+original"));
|
assert!(comparison.patch.contains("+original"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commits_for_repo_includes_all_branch_tips_for_graph() {
|
||||||
|
let repo = init_temp_repo("commits_all_branches");
|
||||||
|
commit_initial_file(&repo.path);
|
||||||
|
let base_branch = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
|
|
||||||
|
run_git_test(&repo.path, ["checkout", "-q", "-b", "feature/graph"]);
|
||||||
|
fs::write(repo.path.join("feature.txt"), "feature\n")
|
||||||
|
.expect("feature file should be written");
|
||||||
|
run_git_test(&repo.path, ["add", "feature.txt"]);
|
||||||
|
run_git_test(&repo.path, ["commit", "-q", "-m", "feature graph"]);
|
||||||
|
let feature_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
|
||||||
|
|
||||||
|
run_git_test(&repo.path, ["checkout", "-q", base_branch.as_str()]);
|
||||||
|
fs::write(repo.path.join("main.txt"), "main\n").expect("main file should be written");
|
||||||
|
run_git_test(&repo.path, ["add", "main.txt"]);
|
||||||
|
run_git_test(&repo.path, ["commit", "-q", "-m", "main graph"]);
|
||||||
|
let main_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
|
||||||
|
|
||||||
|
let commits = commits_for_repo(&repo.path, Some(10)).expect("commits should load");
|
||||||
|
|
||||||
|
assert!(commits.iter().any(|commit| commit.hash == main_commit));
|
||||||
|
assert!(commits.iter().any(|commit| {
|
||||||
|
commit.hash == feature_commit && commit.refs.iter().any(|r| r.contains("feature/graph"))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
windows,
|
windows,
|
||||||
|
|||||||
+81
-1
@@ -117,9 +117,13 @@
|
|||||||
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
||||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||||
|
const HISTORY_ASIDE_WIDTH_KEY = "gitlite.historyAsideWidth.v1";
|
||||||
const COMMIT_PANEL_DEFAULT_HEIGHT = 220;
|
const COMMIT_PANEL_DEFAULT_HEIGHT = 220;
|
||||||
const COMMIT_PANEL_MIN_HEIGHT = COMMIT_PANEL_DEFAULT_HEIGHT;
|
const COMMIT_PANEL_MIN_HEIGHT = COMMIT_PANEL_DEFAULT_HEIGHT;
|
||||||
const COMMIT_PANEL_MAX_HEIGHT = 640;
|
const COMMIT_PANEL_MAX_HEIGHT = 640;
|
||||||
|
const HISTORY_ASIDE_DEFAULT_WIDTH = 620;
|
||||||
|
const HISTORY_ASIDE_MIN_WIDTH = 560;
|
||||||
|
const HISTORY_ASIDE_MAX_WIDTH = 920;
|
||||||
|
|
||||||
// ── State ──────────────────────────────────────────────────────────────────
|
// ── State ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -207,6 +211,10 @@
|
|||||||
let resizingCommitPanel = false;
|
let resizingCommitPanel = false;
|
||||||
let resizeStartY = 0;
|
let resizeStartY = 0;
|
||||||
let resizeStartHeight = 0;
|
let resizeStartHeight = 0;
|
||||||
|
let historyAsideWidth = loadHistoryAsideWidth();
|
||||||
|
let resizingHistoryAside = false;
|
||||||
|
let historyResizeStartX = 0;
|
||||||
|
let historyResizeStartWidth = 0;
|
||||||
|
|
||||||
// ── Derived ────────────────────────────────────────────────────────────────
|
// ── Derived ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -226,6 +234,7 @@
|
|||||||
: "";
|
: "";
|
||||||
$: canCompare = hasRepository && compareFrom.length > 0 && compareTo.length > 0 && compareFrom !== compareTo && !isBusy;
|
$: canCompare = hasRepository && compareFrom.length > 0 && compareTo.length > 0 && compareFrom !== compareTo && !isBusy;
|
||||||
$: localBranches = branches.filter((b) => !b.remote);
|
$: localBranches = branches.filter((b) => !b.remote);
|
||||||
|
$: localBranchNames = localBranches.map((b) => b.name);
|
||||||
$: remoteBranches = branches.filter((b) => b.remote);
|
$: remoteBranches = branches.filter((b) => b.remote);
|
||||||
$: repoSearchTerm = repoSearch.trim().toLowerCase();
|
$: repoSearchTerm = repoSearch.trim().toLowerCase();
|
||||||
$: openRepoRows = repoTabs.filter(repoMatchesSearch);
|
$: openRepoRows = repoTabs.filter(repoMatchesSearch);
|
||||||
@@ -625,6 +634,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampHistoryAsideWidth(value: number): number {
|
||||||
|
return Math.min(HISTORY_ASIDE_MAX_WIDTH, Math.max(HISTORY_ASIDE_MIN_WIDTH, Math.round(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHistoryAsideWidth(): number {
|
||||||
|
try {
|
||||||
|
const stored = Number(localStorage.getItem(HISTORY_ASIDE_WIDTH_KEY));
|
||||||
|
if (Number.isFinite(stored) && stored > 0) return clampHistoryAsideWidth(stored);
|
||||||
|
} catch {
|
||||||
|
// Fall through to the default below.
|
||||||
|
}
|
||||||
|
return HISTORY_ASIDE_DEFAULT_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistHistoryAsideWidth(value: number) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(HISTORY_ASIDE_WIDTH_KEY, String(value));
|
||||||
|
} catch {
|
||||||
|
// Local storage is best-effort only; resizing must keep working without it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startCommitPanelResize(event: PointerEvent) {
|
function startCommitPanelResize(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
resizingCommitPanel = true;
|
resizingCommitPanel = true;
|
||||||
@@ -653,6 +684,34 @@
|
|||||||
persistCommitPanelHeight(commitPanelHeight);
|
persistCommitPanelHeight(commitPanelHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startHistoryAsideResize(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
resizingHistoryAside = true;
|
||||||
|
historyResizeStartX = event.clientX;
|
||||||
|
historyResizeStartWidth = historyAsideWidth;
|
||||||
|
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHistoryAsideResizeMove(event: PointerEvent) {
|
||||||
|
if (!resizingHistoryAside) return;
|
||||||
|
historyAsideWidth = clampHistoryAsideWidth(historyResizeStartWidth + (historyResizeStartX - event.clientX));
|
||||||
|
}
|
||||||
|
|
||||||
|
function endHistoryAsideResize(event: PointerEvent) {
|
||||||
|
if (!resizingHistoryAside) return;
|
||||||
|
resizingHistoryAside = false;
|
||||||
|
persistHistoryAsideWidth(historyAsideWidth);
|
||||||
|
const target = event.currentTarget as HTMLElement;
|
||||||
|
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHistoryAsideResizeKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
||||||
|
event.preventDefault();
|
||||||
|
historyAsideWidth = clampHistoryAsideWidth(historyAsideWidth + (event.key === "ArrowLeft" ? 24 : -24));
|
||||||
|
persistHistoryAsideWidth(historyAsideWidth);
|
||||||
|
}
|
||||||
|
|
||||||
function rememberRecentRepo(path: string) {
|
function rememberRecentRepo(path: string) {
|
||||||
recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40);
|
recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40);
|
||||||
persistRepoLists();
|
persistRepoLists();
|
||||||
@@ -1945,7 +2004,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Workspace -->
|
<!-- Workspace -->
|
||||||
<section class="workspace" aria-label="Git workspace">
|
<section class="workspace" aria-label="Git workspace" style="--history-aside-width: {historyAsideWidth}px;">
|
||||||
|
|
||||||
<!-- Left sidebar: branches + explorer -->
|
<!-- Left sidebar: branches + explorer -->
|
||||||
<aside class="left-sidebar" aria-label="Repository navigation">
|
<aside class="left-sidebar" aria-label="Repository navigation">
|
||||||
@@ -2049,8 +2108,29 @@
|
|||||||
|
|
||||||
<!-- Right sidebar: commit graph + file history -->
|
<!-- Right sidebar: commit graph + file history -->
|
||||||
<aside class="history-aside" aria-label="Commit history">
|
<aside class="history-aside" aria-label="Commit history">
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="history-resize-handle"
|
||||||
|
class:resizing={resizingHistoryAside}
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-label="Resize history panel width"
|
||||||
|
aria-valuenow={historyAsideWidth}
|
||||||
|
aria-valuemin={HISTORY_ASIDE_MIN_WIDTH}
|
||||||
|
aria-valuemax={HISTORY_ASIDE_MAX_WIDTH}
|
||||||
|
tabindex="0"
|
||||||
|
onpointerdown={startHistoryAsideResize}
|
||||||
|
onpointermove={onHistoryAsideResizeMove}
|
||||||
|
onpointerup={endHistoryAsideResize}
|
||||||
|
onpointercancel={endHistoryAsideResize}
|
||||||
|
onkeydown={onHistoryAsideResizeKeydown}
|
||||||
|
></div>
|
||||||
<HistoryPanel
|
<HistoryPanel
|
||||||
{commits}
|
{commits}
|
||||||
|
{localBranchNames}
|
||||||
|
activeBranch={status?.current_branch ?? ""}
|
||||||
|
repositoryKey={activeRepoPath}
|
||||||
{hasRepository}
|
{hasRepository}
|
||||||
{isBusy}
|
{isBusy}
|
||||||
{expandedCommitHashes}
|
{expandedCommitHashes}
|
||||||
|
|||||||
+384
-79
@@ -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 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 {
|
.eyebrow {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--color-ink-faint);
|
color: var(--color-ink-faint);
|
||||||
@@ -862,7 +1055,7 @@
|
|||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
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;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -870,13 +1063,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.history-aside {
|
.history-aside {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
|
grid-template-columns: minmax(0, 1fr) 300px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
gap: 8px;
|
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 {
|
.left-sidebar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: minmax(200px, 0.9fr) minmax(240px, 1.1fr);
|
grid-template-rows: minmax(200px, 0.9fr) minmax(240px, 1.1fr);
|
||||||
@@ -1431,97 +1656,126 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 7px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.commit-avatar {
|
.commit-avatar {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 34px;
|
width: 24px;
|
||||||
height: 34px;
|
height: 24px;
|
||||||
border: 1px solid rgba(122,172,255,0.28);
|
border: 1px solid rgba(122,172,255,0.18);
|
||||||
border-radius: 10px;
|
border-radius: 999px;
|
||||||
color: #dce8ff;
|
color: #b8c5df;
|
||||||
background:
|
background: rgba(18, 22, 38, 0.72);
|
||||||
linear-gradient(180deg, rgba(122,172,255,0.2), rgba(65,209,255,0.08)),
|
font-size: 9px;
|
||||||
rgba(18, 22, 38, 0.84);
|
font-weight: 800;
|
||||||
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-card-main { display: grid; gap: 2px; min-width: 0; }
|
||||||
.commit-title-row {
|
.commit-title-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.commit-summary {
|
.commit-summary {
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #f6f8ff;
|
color: #edf2ff;
|
||||||
font-size: 13.5px;
|
font-size: 12.5px;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
line-height: 1.28;
|
line-height: 1.2;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.commit-kind {
|
.commit-kind {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 3px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 2px 7px;
|
padding: 1px 5px;
|
||||||
border: 1px solid rgba(94,110,156,0.24);
|
border: 1px solid rgba(94,110,156,0.18);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
color: var(--color-ink-dim);
|
color: var(--color-ink-dim);
|
||||||
background: rgba(255,255,255,0.035);
|
background: rgba(255,255,255,0.025);
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
font-weight: 900;
|
font-weight: 800;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
text-transform: uppercase;
|
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.merge { color: #dca6da; border-color: rgba(208,96,192,0.24); background: rgba(208,96,192,0.07); }
|
||||||
.commit-kind.root { color: #e0b45c; border-color: rgba(224,180,92,0.3); background: rgba(224,180,92,0.11); }
|
.commit-kind.root { color: #d7b66b; border-color: rgba(224,180,92,0.22); background: rgba(224,180,92,0.07); }
|
||||||
.commit-meta-line {
|
.commit-meta-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 7px;
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: var(--color-ink-faint);
|
color: var(--color-ink-faint);
|
||||||
font-size: 11px;
|
font-size: 10.5px;
|
||||||
}
|
}
|
||||||
.commit-meta-line > * { min-width: 0; }
|
.commit-meta-line > * { min-width: 0; }
|
||||||
.commit-hash {
|
.commit-hash {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 2px 6px;
|
padding: 0;
|
||||||
border: 1px solid rgba(65,209,255,0.16);
|
border: 0;
|
||||||
border-radius: 6px;
|
color: #8db8ff;
|
||||||
color: var(--color-accent);
|
background: transparent;
|
||||||
background: rgba(65,209,255,0.075);
|
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10.5px;
|
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 {
|
.commit-author {
|
||||||
|
flex: 1 1 80px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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 {
|
.ref-list .ref-chip {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 2px 7px;
|
padding: 1px 6px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
background: rgba(106,154,255,0.13);
|
background: rgba(106,154,255,0.09);
|
||||||
border: 1px solid rgba(106,154,255,0.22);
|
border: 1px solid rgba(106,154,255,0.16);
|
||||||
font-size: 10.5px;
|
font-size: 9.5px;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -1532,9 +1786,9 @@
|
|||||||
background: linear-gradient(135deg, #41d1ff, #7c6cff);
|
background: linear-gradient(135deg, #41d1ff, #7c6cff);
|
||||||
box-shadow: 0 0 14px rgba(65,209,255,0.2);
|
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.branch { color: #7ddf9c; background: rgba(78,202,118,0.08); border-color: rgba(78,202,118,0.18); }
|
||||||
.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.remote { color: #aeb6ff; background: rgba(124,108,255,0.08); border-color: rgba(124,108,255,0.18); }
|
||||||
.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.tag { color: #dbc078; background: rgba(224,180,92,0.08); border-color: rgba(224,180,92,0.2); }
|
||||||
|
|
||||||
.commit-files {
|
.commit-files {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1695,16 +1949,14 @@
|
|||||||
|
|
||||||
.graph-list {
|
.graph-list {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background:
|
background: var(--color-surface-dim);
|
||||||
linear-gradient(90deg, rgba(65,209,255,0.04), transparent 22%),
|
|
||||||
var(--color-surface-dim);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-row {
|
.graph-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
min-height: 86px;
|
min-height: 58px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -1716,57 +1968,109 @@
|
|||||||
.graph-gutter {
|
.graph-gutter {
|
||||||
position: relative;
|
position: relative;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
border-right: 1px solid rgba(94,110,156,0.18);
|
min-width: 42px;
|
||||||
background:
|
border-right: 1px solid rgba(94,110,156,0.12);
|
||||||
radial-gradient(circle at 50% 50%, rgba(255,255,255,0.055), transparent 34%),
|
background: rgba(7,8,16,0.2);
|
||||||
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 { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
||||||
.graph-svg path {
|
.graph-svg path {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
opacity: 0.78;
|
opacity: 0.76;
|
||||||
transition: opacity 120ms ease, stroke-width 120ms ease;
|
transition: opacity 120ms ease, stroke-width 120ms ease;
|
||||||
}
|
}
|
||||||
|
.graph-svg path.hidden-branch {
|
||||||
|
opacity: 0.08;
|
||||||
|
}
|
||||||
.graph-dot {
|
.graph-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 13px;
|
width: 12px;
|
||||||
height: 13px;
|
height: 12px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--dot-color, #5a8cf8);
|
background: var(--dot-color, #5a8cf8);
|
||||||
border: 2px solid #111321;
|
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%);
|
transform: translate(-50%, -50%);
|
||||||
transition: transform 120ms ease, box-shadow 120ms ease;
|
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 {
|
.commit-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 5px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 12px;
|
padding: 8px 10px;
|
||||||
background:
|
background: rgba(28,29,48,0.34);
|
||||||
linear-gradient(90deg, rgba(255,255,255,0.035), transparent 34%),
|
|
||||||
rgba(28,29,48,0.55);
|
|
||||||
transition: background 120ms ease;
|
transition: background 120ms ease;
|
||||||
}
|
}
|
||||||
.graph-row + .graph-row .commit-body { border-top: 1px solid var(--color-border-subtle); }
|
.graph-row + .graph-row .commit-body { border-top: 1px solid rgba(94,110,156,0.1); }
|
||||||
.graph-row:hover .commit-body {
|
.graph-row:hover .commit-body { background: rgba(37,40,62,0.54); }
|
||||||
background:
|
.graph-row:hover .graph-svg path { opacity: 1; stroke-width: 2.65; }
|
||||||
linear-gradient(90deg, rgba(65,209,255,0.075), transparent 36%),
|
.graph-row:hover .graph-svg path.hidden-branch { opacity: 0.12; stroke-width: 2.2; }
|
||||||
var(--color-surface-hover);
|
.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: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 {
|
.graph-row.merge-row .commit-body {
|
||||||
|
background: rgba(36,31,54,0.46);
|
||||||
|
}
|
||||||
|
.graph-row.tip-row .commit-body {
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, rgba(208,96,192,0.095), transparent 34%),
|
linear-gradient(90deg, rgba(105,167,255,0.055), transparent 32%),
|
||||||
rgba(28,29,48,0.62);
|
rgba(28,29,48,0.38);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Compare panel --- */
|
/* --- Compare panel --- */
|
||||||
@@ -3241,16 +3545,16 @@
|
|||||||
/* --- Responsive breakpoints --- */
|
/* --- Responsive breakpoints --- */
|
||||||
|
|
||||||
@media (min-width: 1800px) {
|
@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) {
|
@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 */
|
/* Stack CommitPanel below StatusPanel; history panels stay side by side */
|
||||||
@media (max-width: 1100px) {
|
@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); }
|
.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) {
|
@media (max-width: 960px) {
|
||||||
.workspace { grid-template-columns: 180px minmax(0, 1fr) 300px; gap: 6px; }
|
.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-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; }
|
.shell-body { gap: 6px; }
|
||||||
.left-sidebar { gap: 6px; grid-template-rows: minmax(180px, 0.8fr) minmax(200px, 1.2fr); }
|
.left-sidebar { gap: 6px; grid-template-rows: minmax(180px, 0.8fr) minmax(200px, 1.2fr); }
|
||||||
.section-head { min-height: 40px; padding: 6px 10px; }
|
.section-head { min-height: 40px; padding: 6px 10px; }
|
||||||
|
|||||||
@@ -1,28 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ChevronDown, ChevronRight, GitBranch, GitMerge, RotateCcw } from "@lucide/svelte";
|
import { ChevronDown, ChevronRight, GitBranch, GitMerge, RotateCcw, X } from "@lucide/svelte";
|
||||||
import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";
|
import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";
|
||||||
|
|
||||||
interface GraphSegment {
|
interface GraphSegment {
|
||||||
fromCol: number;
|
fromCol: number;
|
||||||
toCol: number;
|
toCol: number;
|
||||||
color: string;
|
color: string;
|
||||||
|
branches: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphRow {
|
interface GraphRow {
|
||||||
dotCol: number;
|
dotCol: number;
|
||||||
dotColor: string;
|
dotColor: string;
|
||||||
|
branchLabels: string[];
|
||||||
top: GraphSegment[];
|
top: GraphSegment[];
|
||||||
bottom: GraphSegment[];
|
bottom: GraphSegment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VisibleCommitEntry {
|
||||||
|
commit: GitCommit;
|
||||||
|
graphCommit: GitCommit;
|
||||||
|
}
|
||||||
|
|
||||||
const GRAPH_COLORS = [
|
const GRAPH_COLORS = [
|
||||||
"#2f6fb0", "#4aa777", "#c9851f", "#a05bd0",
|
"#69a7ff", "#5bd18a", "#d8a74a", "#ba82ff",
|
||||||
"#cc4b6e", "#1f9ab0", "#7a8a1f", "#b0631f",
|
"#ff7c9f", "#48c7d8", "#c5cf54", "#e18c55",
|
||||||
];
|
];
|
||||||
const GRAPH_LANE = 16;
|
const GRAPH_LANE = 18;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
commits: GitCommit[];
|
commits: GitCommit[];
|
||||||
|
localBranchNames: string[];
|
||||||
|
activeBranch: string;
|
||||||
|
repositoryKey: string;
|
||||||
hasRepository: boolean;
|
hasRepository: boolean;
|
||||||
isBusy: boolean;
|
isBusy: boolean;
|
||||||
expandedCommitHashes: Set<string>;
|
expandedCommitHashes: Set<string>;
|
||||||
@@ -34,6 +44,9 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
commits = [],
|
commits = [],
|
||||||
|
localBranchNames = [],
|
||||||
|
activeBranch = "",
|
||||||
|
repositoryKey = "",
|
||||||
hasRepository = false,
|
hasRepository = false,
|
||||||
isBusy = false,
|
isBusy = false,
|
||||||
expandedCommitHashes = new Set(),
|
expandedCommitHashes = new Set(),
|
||||||
@@ -43,6 +56,11 @@
|
|||||||
onCreateBranchFromCommit = () => {},
|
onCreateBranchFromCommit = () => {},
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
let hiddenGraphBranches = $state<Set<string>>(new Set());
|
||||||
|
let branchDialogOpen = $state(false);
|
||||||
|
let userAdjustedBranchFilter = $state(false);
|
||||||
|
let lastDefaultFilterKey = $state("");
|
||||||
|
|
||||||
function laneColor(col: number): string {
|
function laneColor(col: number): string {
|
||||||
return GRAPH_COLORS[((col % GRAPH_COLORS.length) + GRAPH_COLORS.length) % GRAPH_COLORS.length];
|
return GRAPH_COLORS[((col % GRAPH_COLORS.length) + GRAPH_COLORS.length) % GRAPH_COLORS.length];
|
||||||
}
|
}
|
||||||
@@ -59,13 +77,15 @@
|
|||||||
return `M ${x1} ${fromY} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${toY}`;
|
return `M ${x1} ${fromY} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${toY}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeGraph(items: GitCommit[]): { rows: GraphRow[]; columns: number } {
|
function computeGraph(items: GitCommit[], branchMembership = new Map<string, string[]>()): { rows: GraphRow[]; columns: number } {
|
||||||
const rows: GraphRow[] = [];
|
const rows: GraphRow[] = [];
|
||||||
let lanes: (string | null)[] = [];
|
let lanes: (string | null)[] = [];
|
||||||
|
let laneBranches: string[][] = [];
|
||||||
let maxColumns = 1;
|
let maxColumns = 1;
|
||||||
|
|
||||||
for (const commit of items) {
|
for (const commit of items) {
|
||||||
const before = lanes.slice();
|
const before = lanes.slice();
|
||||||
|
const beforeBranches = laneBranches.map((branches) => branches.slice());
|
||||||
|
|
||||||
let col = before.indexOf(commit.hash);
|
let col = before.indexOf(commit.hash);
|
||||||
if (col === -1) {
|
if (col === -1) {
|
||||||
@@ -75,18 +95,27 @@
|
|||||||
|
|
||||||
const after = before.slice();
|
const after = before.slice();
|
||||||
while (after.length <= col) after.push(null);
|
while (after.length <= col) after.push(null);
|
||||||
|
const afterBranches = beforeBranches.map((branches) => branches.slice());
|
||||||
|
while (afterBranches.length <= col) afterBranches.push([]);
|
||||||
|
|
||||||
for (let k = 0; k < after.length; k++) {
|
for (let k = 0; k < after.length; k++) {
|
||||||
if (after[k] === commit.hash) after[k] = null;
|
if (after[k] === commit.hash) {
|
||||||
|
after[k] = null;
|
||||||
|
afterBranches[k] = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentBranches = branchMembership.get(commit.hash) ?? localBranchRefs(commit);
|
||||||
|
const commitBranches = uniqueStrings([...(beforeBranches[col] ?? []), ...currentBranches]);
|
||||||
after[col] = commit.parents.length > 0 ? commit.parents[0] : null;
|
after[col] = commit.parents.length > 0 ? commit.parents[0] : null;
|
||||||
|
afterBranches[col] = after[col] ? commitBranches.slice() : [];
|
||||||
|
|
||||||
const fromCommit = new Set<number>([col]);
|
const fromCommit = new Set<number>([col]);
|
||||||
for (let p = 1; p < commit.parents.length; p++) {
|
for (let p = 1; p < commit.parents.length; p++) {
|
||||||
let slot = after.indexOf(null);
|
let slot = after.indexOf(null);
|
||||||
if (slot === -1) { slot = after.length; after.push(null); }
|
if (slot === -1) { slot = after.length; after.push(null); afterBranches.push([]); }
|
||||||
after[slot] = commit.parents[p];
|
after[slot] = commit.parents[p];
|
||||||
|
afterBranches[slot] = [];
|
||||||
fromCommit.add(slot);
|
fromCommit.add(slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,19 +123,28 @@
|
|||||||
for (let k = 0; k < before.length; k++) {
|
for (let k = 0; k < before.length; k++) {
|
||||||
const target = before[k];
|
const target = before[k];
|
||||||
if (target == null) continue;
|
if (target == null) continue;
|
||||||
top.push({ fromCol: k, toCol: target === commit.hash ? col : k, color: laneColor(k) });
|
top.push({ fromCol: k, toCol: target === commit.hash ? col : k, color: laneColor(k), branches: beforeBranches[k] ?? [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const bottom: GraphSegment[] = [];
|
const bottom: GraphSegment[] = [];
|
||||||
for (let k = 0; k < after.length; k++) {
|
for (let k = 0; k < after.length; k++) {
|
||||||
if (after[k] == null) continue;
|
if (after[k] == null) continue;
|
||||||
bottom.push({ fromCol: fromCommit.has(k) ? col : k, toCol: k, color: laneColor(k) });
|
bottom.push({
|
||||||
|
fromCol: fromCommit.has(k) ? col : k,
|
||||||
|
toCol: k,
|
||||||
|
color: laneColor(k),
|
||||||
|
branches: fromCommit.has(k) ? commitBranches : afterBranches[k] ?? [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.push({ dotCol: col, dotColor: laneColor(col), top, bottom });
|
rows.push({ dotCol: col, dotColor: laneColor(col), branchLabels: currentBranches, top, bottom });
|
||||||
|
|
||||||
lanes = after.slice();
|
lanes = after.slice();
|
||||||
while (lanes.length > 0 && lanes[lanes.length - 1] == null) lanes.pop();
|
laneBranches = afterBranches.map((branches) => branches.slice());
|
||||||
|
while (lanes.length > 0 && lanes[lanes.length - 1] == null) {
|
||||||
|
lanes.pop();
|
||||||
|
laneBranches.pop();
|
||||||
|
}
|
||||||
maxColumns = Math.max(maxColumns, before.length, after.length, col + 1);
|
maxColumns = Math.max(maxColumns, before.length, after.length, col + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,9 +189,159 @@
|
|||||||
return "commit";
|
return "commit";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let localBranchNameSet = $derived(new Set(localBranchNames));
|
||||||
|
|
||||||
|
function uniqueStrings(values: string[]): string[] {
|
||||||
|
return [...new Set(values.filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function branchIsVisible(branch: string): boolean {
|
||||||
|
return !hiddenGraphBranches.has(branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleBranchLabels(labels: string[]): string[] {
|
||||||
|
return labels.filter(branchIsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
function segmentIsVisible(segment: GraphSegment): boolean {
|
||||||
|
return segment.branches.length === 0 || segment.branches.some(branchIsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
function branchesAreVisible(branches: string[]): boolean {
|
||||||
|
if (localBranchNames.length === 0) return true;
|
||||||
|
return branches.some(branchIsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowGraphIsVisible(row: GraphRow | undefined): boolean {
|
||||||
|
return branchesAreVisible(row?.branchLabels ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestVisibleGraphParents(
|
||||||
|
hash: string,
|
||||||
|
visibleHashes: Set<string>,
|
||||||
|
commitByHash: Map<string, GitCommit>,
|
||||||
|
seen: Set<string>,
|
||||||
|
): string[] {
|
||||||
|
if (visibleHashes.has(hash)) return [hash];
|
||||||
|
if (seen.has(hash)) return [];
|
||||||
|
seen.add(hash);
|
||||||
|
|
||||||
|
const commit = commitByHash.get(hash);
|
||||||
|
if (!commit) return [];
|
||||||
|
return uniqueStrings(
|
||||||
|
commit.parents.flatMap((parentHash) => (
|
||||||
|
nearestVisibleGraphParents(parentHash, visibleHashes, commitByHash, new Set(seen))
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function branchMembershipByHash(items: GitCommit[]): Map<string, string[]> {
|
||||||
|
const commitByHash = new Map(items.map((commit) => [commit.hash, commit]));
|
||||||
|
const membership = new Map<string, Set<string>>();
|
||||||
|
|
||||||
|
for (const commit of items) {
|
||||||
|
for (const branch of localBranchRefs(commit)) {
|
||||||
|
const stack = [commit.hash];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const hash = stack.pop();
|
||||||
|
if (!hash || seen.has(hash)) continue;
|
||||||
|
seen.add(hash);
|
||||||
|
|
||||||
|
let branches = membership.get(hash);
|
||||||
|
if (!branches) {
|
||||||
|
branches = new Set<string>();
|
||||||
|
membership.set(hash, branches);
|
||||||
|
}
|
||||||
|
branches.add(branch);
|
||||||
|
|
||||||
|
const parentCommit = commitByHash.get(hash);
|
||||||
|
if (parentCommit) stack.push(...parentCommit.parents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
items.map((commit) => [
|
||||||
|
commit.hash,
|
||||||
|
localBranchNames.filter((branch) => membership.get(commit.hash)?.has(branch)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleCommitEntriesForGraph(items: GitCommit[], branchMembership: Map<string, string[]>): VisibleCommitEntry[] {
|
||||||
|
const visibleItems = items.filter((commit) => branchesAreVisible(branchMembership.get(commit.hash) ?? []));
|
||||||
|
const visibleHashes = new Set(visibleItems.map((commit) => commit.hash));
|
||||||
|
const commitByHash = new Map(items.map((commit) => [commit.hash, commit]));
|
||||||
|
|
||||||
|
return visibleItems.map((commit) => {
|
||||||
|
const parents = uniqueStrings(
|
||||||
|
commit.parents.flatMap((parentHash) => (
|
||||||
|
nearestVisibleGraphParents(parentHash, visibleHashes, commitByHash, new Set())
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
return { commit, graphCommit: { ...commit, parents } };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGraphBranch(branch: string) {
|
||||||
|
const next = new Set(hiddenGraphBranches);
|
||||||
|
if (next.has(branch)) next.delete(branch); else next.add(branch);
|
||||||
|
hiddenGraphBranches = next;
|
||||||
|
userAdjustedBranchFilter = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAllGraphBranches() {
|
||||||
|
hiddenGraphBranches = new Set();
|
||||||
|
userAdjustedBranchFilter = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAllGraphBranches() {
|
||||||
|
hiddenGraphBranches = new Set(localBranchNames);
|
||||||
|
userAdjustedBranchFilter = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBranchDialog() {
|
||||||
|
branchDialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBranchDialog() {
|
||||||
|
branchDialogOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBranchDialogKeydown(event: KeyboardEvent) {
|
||||||
|
if (branchDialogOpen && event.key === "Escape") {
|
||||||
|
closeBranchDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBranchDialogBackdropClick(event: MouseEvent) {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
closeBranchDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function localBranchRefs(commit: GitCommit): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const labels: string[] = [];
|
||||||
|
for (const ref of commit.refs) {
|
||||||
|
const label = refLabel(ref);
|
||||||
|
if (!localBranchNameSet.has(label) || seen.has(label)) continue;
|
||||||
|
seen.add(label);
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleRefs(commit: GitCommit): string[] {
|
||||||
|
return commit.refs.filter((ref) => !localBranchNameSet.has(refLabel(ref)));
|
||||||
|
}
|
||||||
|
|
||||||
function refClass(ref: string): string {
|
function refClass(ref: string): string {
|
||||||
if (ref.startsWith("HEAD")) return "head";
|
if (ref.startsWith("HEAD")) return "head";
|
||||||
if (ref.startsWith("tag:")) return "tag";
|
if (ref.startsWith("tag:")) return "tag";
|
||||||
|
if (localBranchNameSet.has(refLabel(ref))) return "branch";
|
||||||
if (ref.includes("/")) return "remote";
|
if (ref.includes("/")) return "remote";
|
||||||
return "branch";
|
return "branch";
|
||||||
}
|
}
|
||||||
@@ -168,45 +356,108 @@
|
|||||||
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
|
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
let graph = $derived(computeGraph(commits));
|
$effect(() => {
|
||||||
|
const available = new Set(localBranchNames);
|
||||||
|
const nextHidden = new Set([...hiddenGraphBranches].filter((branch) => available.has(branch)));
|
||||||
|
if (nextHidden.size !== hiddenGraphBranches.size) {
|
||||||
|
hiddenGraphBranches = nextHidden;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const defaultBranch = activeBranch && localBranchNames.includes(activeBranch)
|
||||||
|
? activeBranch
|
||||||
|
: (localBranchNames[0] ?? "");
|
||||||
|
const defaultFilterKey = `${repositoryKey}::${defaultBranch}`;
|
||||||
|
|
||||||
|
if (!defaultBranch) {
|
||||||
|
if (lastDefaultFilterKey !== defaultFilterKey) {
|
||||||
|
hiddenGraphBranches = new Set();
|
||||||
|
userAdjustedBranchFilter = false;
|
||||||
|
lastDefaultFilterKey = defaultFilterKey;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastDefaultFilterKey !== defaultFilterKey) {
|
||||||
|
userAdjustedBranchFilter = false;
|
||||||
|
lastDefaultFilterKey = defaultFilterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userAdjustedBranchFilter) {
|
||||||
|
hiddenGraphBranches = new Set(localBranchNames.filter((branch) => branch !== defaultBranch));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let branchMembership = $derived(branchMembershipByHash(commits));
|
||||||
|
let visibleCommitEntries = $derived(visibleCommitEntriesForGraph(commits, branchMembership));
|
||||||
|
let visibleCommits = $derived(visibleCommitEntries.map((entry) => entry.commit));
|
||||||
|
let graphCommits = $derived(visibleCommitEntries.map((entry) => entry.graphCommit));
|
||||||
|
let visibleBranchCount = $derived(localBranchNames.filter(branchIsVisible).length);
|
||||||
|
let graph = $derived(computeGraph(graphCommits, branchMembership));
|
||||||
let graphRows = $derived(graph.rows);
|
let graphRows = $derived(graph.rows);
|
||||||
let graphWidth = $derived(Math.max(graph.columns, 1) * GRAPH_LANE);
|
let graphWidth = $derived(Math.max(Math.max(graph.columns, 1) * GRAPH_LANE + 18, 42));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleBranchDialogKeydown} />
|
||||||
|
|
||||||
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
|
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">History</span>
|
<span class="eyebrow">History</span>
|
||||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Commits</h2>
|
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Commits</h2>
|
||||||
</div>
|
</div>
|
||||||
<span class="pill pill-count">{commits.length}</span>
|
<div class="section-head-actions">
|
||||||
|
{#if localBranchNames.length > 0}
|
||||||
|
<button
|
||||||
|
class="graph-branch-dialog-button"
|
||||||
|
type="button"
|
||||||
|
onclick={openBranchDialog}
|
||||||
|
title="Select branches shown in the graph"
|
||||||
|
>
|
||||||
|
<GitBranch size={13} aria-hidden="true" />
|
||||||
|
Branches
|
||||||
|
<span>{visibleBranchCount}/{localBranchNames.length}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<span class="pill pill-count" title={visibleCommits.length === commits.length ? "Commits" : `${visibleCommits.length} of ${commits.length} commits shown`}>
|
||||||
|
{visibleCommits.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !hasRepository}
|
{#if !hasRepository}
|
||||||
<div class="blank-state">No repository loaded.</div>
|
<div class="blank-state">No repository loaded.</div>
|
||||||
{:else if commits.length === 0}
|
{:else if commits.length === 0}
|
||||||
<div class="blank-state">No commits returned.</div>
|
<div class="blank-state">No commits returned.</div>
|
||||||
|
{:else if visibleCommits.length === 0}
|
||||||
|
<div class="blank-state">No commits match the selected branches.</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="history-list graph-list overflow-auto">
|
<div class="history-list graph-list overflow-auto">
|
||||||
{#each commits as item, rowIndex (item.hash)}
|
{#each visibleCommitEntries as entry, rowIndex (entry.commit.hash)}
|
||||||
|
{@const item = entry.commit}
|
||||||
{@const row = graphRows[rowIndex]}
|
{@const row = graphRows[rowIndex]}
|
||||||
<article class="commit-row graph-row" class:merge-row={item.parents.length > 1} class:root-row={item.parents.length === 0}>
|
{@const hoverBranchRefs = visibleBranchLabels(row?.branchLabels ?? [])}
|
||||||
|
{@const otherRefs = visibleRefs(item)}
|
||||||
|
<article class="commit-row graph-row" class:merge-row={item.parents.length > 1} class:root-row={item.parents.length === 0} class:tip-row={item.refs.length > 0}>
|
||||||
<div class="graph-gutter" style={`width:${graphWidth}px`} aria-hidden="true">
|
<div class="graph-gutter" style={`width:${graphWidth}px`} aria-hidden="true">
|
||||||
{#if row}
|
{#if row}
|
||||||
<svg class="graph-svg" viewBox={`0 0 ${graphWidth} 100`} preserveAspectRatio="none">
|
<svg class="graph-svg" viewBox={`0 0 ${graphWidth} 100`} preserveAspectRatio="none">
|
||||||
{#each row.top as seg}
|
{#each row.top as seg}
|
||||||
<path
|
<path
|
||||||
|
class:hidden-branch={!segmentIsVisible(seg)}
|
||||||
d={graphPath(seg, 0, 50)}
|
d={graphPath(seg, 0, 50)}
|
||||||
stroke={seg.color}
|
stroke={seg.color}
|
||||||
stroke-width="2"
|
stroke-width="2.2"
|
||||||
vector-effect="non-scaling-stroke"
|
vector-effect="non-scaling-stroke"
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{#each row.bottom as seg}
|
{#each row.bottom as seg}
|
||||||
<path
|
<path
|
||||||
|
class:hidden-branch={!segmentIsVisible(seg)}
|
||||||
d={graphPath(seg, 50, 100)}
|
d={graphPath(seg, 50, 100)}
|
||||||
stroke={seg.color}
|
stroke={seg.color}
|
||||||
stroke-width="2"
|
stroke-width="2.2"
|
||||||
vector-effect="non-scaling-stroke"
|
vector-effect="non-scaling-stroke"
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -214,8 +465,21 @@
|
|||||||
<span
|
<span
|
||||||
class="graph-dot"
|
class="graph-dot"
|
||||||
class:merge={item.parents.length > 1}
|
class:merge={item.parents.length > 1}
|
||||||
|
class:tip={item.refs.length > 0}
|
||||||
|
class:hidden-branch={!rowGraphIsVisible(row)}
|
||||||
|
title={hoverBranchRefs.length > 0 ? `Contained in: ${hoverBranchRefs.join(", ")}` : item.short_hash}
|
||||||
style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`}
|
style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`}
|
||||||
></span>
|
></span>
|
||||||
|
{#if hoverBranchRefs.length > 0}
|
||||||
|
<div class="graph-hover-branches" style={`left:${graphColX(row.dotCol) + 13}px`}>
|
||||||
|
{#each hoverBranchRefs as branch}
|
||||||
|
<span title={branch}>
|
||||||
|
<GitBranch size={10} aria-hidden="true" />
|
||||||
|
{branch}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -241,9 +505,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if item.refs.length > 0}
|
{#if otherRefs.length > 0}
|
||||||
<div class="ref-list" aria-label="Commit refs">
|
<div class="ref-list" aria-label="Commit refs">
|
||||||
{#each item.refs as ref}
|
{#each otherRefs as ref}
|
||||||
<span class={`ref-chip ${refClass(ref)}`}>{refLabel(ref)}</span>
|
<span class={`ref-chip ${refClass(ref)}`}>{refLabel(ref)}</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -303,3 +567,46 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#if branchDialogOpen}
|
||||||
|
<div class="branch-filter-backdrop" role="presentation" onclick={handleBranchDialogBackdropClick}>
|
||||||
|
<div
|
||||||
|
class="branch-filter-dialog"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Select visible branches"
|
||||||
|
>
|
||||||
|
<header class="branch-filter-dialog-head">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Git graph</span>
|
||||||
|
<h3>Visible branches</h3>
|
||||||
|
</div>
|
||||||
|
<button class="dialog-icon-button" type="button" onclick={closeBranchDialog} aria-label="Close branch selection">
|
||||||
|
<X size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="branch-filter-summary">
|
||||||
|
<span>{visibleBranchCount} of {localBranchNames.length} branches selected</span>
|
||||||
|
<div class="branch-filter-actions">
|
||||||
|
<button type="button" onclick={showAllGraphBranches} disabled={visibleBranchCount === localBranchNames.length}>Show all</button>
|
||||||
|
<button type="button" onclick={hideAllGraphBranches} disabled={visibleBranchCount === 0}>Hide all</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="branch-filter-dialog-list">
|
||||||
|
{#each localBranchNames as branch}
|
||||||
|
<label class:muted={!branchIsVisible(branch)} class="branch-filter-option" title={branch}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={branchIsVisible(branch)}
|
||||||
|
onchange={() => toggleGraphBranch(branch)}
|
||||||
|
/>
|
||||||
|
<GitBranch size={14} aria-hidden="true" />
|
||||||
|
<span>{branch}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user