Opt/app layout #18
+25
-2
@@ -1639,6 +1639,7 @@
|
|||||||
.branch-actions .btn-sm { min-height: 24px; padding: 0 6px; font-size: 11px; }
|
.branch-actions .btn-sm { min-height: 24px; padding: 0 6px; font-size: 11px; }
|
||||||
|
|
||||||
.branch-context-menu,
|
.branch-context-menu,
|
||||||
|
.history-context-menu,
|
||||||
.explorer-context-menu {
|
.explorer-context-menu {
|
||||||
z-index: 120;
|
z-index: 120;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1651,10 +1652,12 @@
|
|||||||
box-shadow: 0 18px 50px rgba(0,0,0,0.5);
|
box-shadow: 0 18px 50px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-context-menu { position: absolute; }
|
.branch-context-menu,
|
||||||
|
.history-context-menu { position: absolute; }
|
||||||
.explorer-context-menu { position: fixed; }
|
.explorer-context-menu { position: fixed; }
|
||||||
|
|
||||||
.branch-context-menu button,
|
.branch-context-menu button,
|
||||||
|
.history-context-menu button,
|
||||||
.explorer-context-menu button {
|
.explorer-context-menu button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1673,13 +1676,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.branch-context-menu button:hover:not(:disabled),
|
.branch-context-menu button:hover:not(:disabled),
|
||||||
|
.history-context-menu button:hover:not(:disabled),
|
||||||
.explorer-context-menu button:hover:not(:disabled) {
|
.explorer-context-menu button:hover:not(:disabled) {
|
||||||
border-color: var(--color-border-subtle);
|
border-color: var(--color-border-subtle);
|
||||||
background: rgba(255,255,255,0.06);
|
background: rgba(255,255,255,0.06);
|
||||||
color: var(--color-ink);
|
color: var(--color-ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-context-menu .menu-separator {
|
.branch-context-menu .menu-separator,
|
||||||
|
.history-context-menu .menu-separator {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: 4px 3px;
|
margin: 4px 3px;
|
||||||
background: var(--color-border-subtle);
|
background: var(--color-border-subtle);
|
||||||
@@ -1696,6 +1701,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.branch-context-menu button:disabled,
|
.branch-context-menu button:disabled,
|
||||||
|
.history-context-menu button:disabled,
|
||||||
.explorer-context-menu button:disabled {
|
.explorer-context-menu button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.48;
|
opacity: 0.48;
|
||||||
@@ -1817,6 +1823,7 @@
|
|||||||
|
|
||||||
/* --- Commit history --- */
|
/* --- Commit history --- */
|
||||||
|
|
||||||
|
.history-panel { position: relative; }
|
||||||
.history-list { min-width: 0; padding: 6px; overflow: auto; }
|
.history-list { min-width: 0; padding: 6px; overflow: auto; }
|
||||||
|
|
||||||
.commit-row { display: grid; min-width: 0; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); transition: border-color 120ms; }
|
.commit-row { display: grid; min-width: 0; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); transition: border-color 120ms; }
|
||||||
@@ -2017,6 +2024,22 @@
|
|||||||
}
|
}
|
||||||
.commit-action-buttons { display: flex; flex: 0 0 auto; align-items: center; gap: 5px; }
|
.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; }
|
.commit-action-buttons button { flex: 0 0 auto; white-space: nowrap; }
|
||||||
|
.commit-menu-button {
|
||||||
|
width: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0;
|
||||||
|
border-color: rgba(94,110,156,0.16);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: rgba(255,255,255,0.035);
|
||||||
|
color: var(--color-ink-dim);
|
||||||
|
}
|
||||||
|
.commit-menu-button:hover:not(:disabled),
|
||||||
|
.commit-menu-button[aria-expanded="true"] {
|
||||||
|
border-color: rgba(65,209,255,0.28);
|
||||||
|
background: rgba(65,209,255,0.08);
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
.file-history-head { align-items: flex-start; }
|
.file-history-head { align-items: flex-start; }
|
||||||
.file-history-heading { min-width: 0; flex: 1 1 auto; overflow: hidden; }
|
.file-history-heading { min-width: 0; flex: 1 1 auto; overflow: hidden; }
|
||||||
.section-head .file-history-name {
|
.section-head .file-history-name {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ChevronDown, ChevronRight, GitBranch, GitMerge, RotateCcw, X } from "@lucide/svelte";
|
import { ChevronDown, ChevronRight, EllipsisVertical, 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 {
|
||||||
@@ -60,6 +60,10 @@
|
|||||||
let branchDialogOpen = $state(false);
|
let branchDialogOpen = $state(false);
|
||||||
let userAdjustedBranchFilter = $state(false);
|
let userAdjustedBranchFilter = $state(false);
|
||||||
let lastDefaultFilterKey = $state("");
|
let lastDefaultFilterKey = $state("");
|
||||||
|
let panelElement = $state<HTMLElement | null>(null);
|
||||||
|
let contextCommit = $state<GitCommit | null>(null);
|
||||||
|
let contextMenuX = $state(0);
|
||||||
|
let contextMenuY = $state(0);
|
||||||
|
|
||||||
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];
|
||||||
@@ -316,6 +320,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCommitActionMenu(event: MouseEvent, commit: GitCommit) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (isBusy) return;
|
||||||
|
|
||||||
|
if (contextCommit?.hash === commit.hash) {
|
||||||
|
closeCommitContextMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelRect = panelElement?.getBoundingClientRect();
|
||||||
|
const buttonRect = event.currentTarget instanceof HTMLElement
|
||||||
|
? event.currentTarget.getBoundingClientRect()
|
||||||
|
: null;
|
||||||
|
const rawX = panelRect && buttonRect ? buttonRect.right - panelRect.left - 184 : event.offsetX;
|
||||||
|
const rawY = panelRect && buttonRect ? buttonRect.bottom - panelRect.top + 4 : event.offsetY;
|
||||||
|
const maxX = Math.max(8, (panelRect?.width ?? window.innerWidth) - 192);
|
||||||
|
const maxY = Math.max(8, (panelRect?.height ?? window.innerHeight) - 96);
|
||||||
|
|
||||||
|
contextCommit = commit;
|
||||||
|
contextMenuX = Math.max(8, Math.min(rawX, maxX));
|
||||||
|
contextMenuY = Math.max(8, Math.min(rawY, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCommitContextMenu() {
|
||||||
|
contextCommit = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createBranchFromContextCommit() {
|
||||||
|
const commit = contextCommit;
|
||||||
|
if (!commit || isBusy) return;
|
||||||
|
closeCommitContextMenu();
|
||||||
|
await onCreateBranchFromCommit(commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreContextCommit() {
|
||||||
|
const commit = contextCommit;
|
||||||
|
if (!commit || isBusy) return;
|
||||||
|
closeCommitContextMenu();
|
||||||
|
await onRestoreCommit(commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWindowKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== "Escape") return;
|
||||||
|
closeCommitContextMenu();
|
||||||
|
handleBranchDialogKeydown(event);
|
||||||
|
}
|
||||||
|
|
||||||
function localBranchRefs(commit: GitCommit): string[] {
|
function localBranchRefs(commit: GitCommit): string[] {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
@@ -393,9 +445,9 @@
|
|||||||
let graphWidth = $derived(Math.max(Math.max(graph.columns, 1) * GRAPH_LANE + 18, 42));
|
let graphWidth = $derived(Math.max(Math.max(graph.columns, 1) * GRAPH_LANE + 18, 42));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onkeydown={handleBranchDialogKeydown} />
|
<svelte:window onclick={closeCommitContextMenu} onkeydown={handleWindowKeydown} on:contextmenu|capture={closeCommitContextMenu} />
|
||||||
|
|
||||||
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
|
<section bind:this={panelElement} class="panel history-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>
|
||||||
@@ -433,7 +485,12 @@
|
|||||||
{@const row = graphRows[rowIndex]}
|
{@const row = graphRows[rowIndex]}
|
||||||
{@const hoverBranchRefs = visibleBranchLabels(row?.branchLabels ?? [])}
|
{@const hoverBranchRefs = visibleBranchLabels(row?.branchLabels ?? [])}
|
||||||
{@const otherRefs = visibleRefs(item)}
|
{@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}>
|
<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">
|
||||||
@@ -545,13 +602,17 @@
|
|||||||
<div class="commit-actions">
|
<div class="commit-actions">
|
||||||
<time class="commit-time" datetime={item.date}>{formatCommitDate(item.date)}</time>
|
<time class="commit-time" datetime={item.date}>{formatCommitDate(item.date)}</time>
|
||||||
<div class="commit-action-buttons">
|
<div class="commit-action-buttons">
|
||||||
<button class="btn-sm" type="button" onclick={() => onCreateBranchFromCommit(item)} disabled={isBusy} title="Create a new branch from this commit">
|
<button
|
||||||
<GitBranch size={15} aria-hidden="true" />
|
class="commit-menu-button"
|
||||||
Branch
|
type="button"
|
||||||
</button>
|
onclick={(event) => openCommitActionMenu(event, item)}
|
||||||
<button class="btn-sm" type="button" onclick={() => onRestoreCommit(item)} disabled={isBusy} title="Bring this commit's files into your working tree as unstaged changes (no history is changed)">
|
disabled={isBusy}
|
||||||
<RotateCcw size={15} aria-hidden="true" />
|
title="Commit actions"
|
||||||
Restore
|
aria-label={`Actions for ${item.short_hash}`}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={contextCommit?.hash === item.hash}
|
||||||
|
>
|
||||||
|
<EllipsisVertical size={15} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -560,6 +621,25 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if contextCommit}
|
||||||
|
<div
|
||||||
|
class="history-context-menu"
|
||||||
|
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
|
||||||
|
role="menu"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label={`Actions for ${contextCommit.short_hash}`}
|
||||||
|
>
|
||||||
|
<button type="button" role="menuitem" onclick={createBranchFromContextCommit} disabled={isBusy}>
|
||||||
|
<GitBranch size={14} aria-hidden="true" />
|
||||||
|
Branch
|
||||||
|
</button>
|
||||||
|
<button type="button" role="menuitem" onclick={restoreContextCommit} disabled={isBusy}>
|
||||||
|
<RotateCcw size={14} aria-hidden="true" />
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#if branchDialogOpen}
|
{#if branchDialogOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user