This update introduces functionality for resizing various panels in the UI, including the left sidebar, branch panel, stash panel, and file history. It also adds state persistence for panel sizes and collapsed states using local storage, improving user experience by maintaining preferences across sessions. - Implemented resizing functionality for multiple UI panels - Added local storage support for panel dimensions and collapsed states - Enhanced accessibility features for panel controls
701 lines
23 KiB
Svelte
701 lines
23 KiB
Svelte
<script lang="ts">
|
|
import { Check, ChevronDown, ChevronRight, Folder, FolderOpen, GitBranch, GitMerge, Pencil, Plus, Tag as TagIcon, Trash2, Upload, X } from "@lucide/svelte";
|
|
import type { GitBranch as GitBranchInfo, GitTag } from "../types";
|
|
|
|
type BranchTreeNode = BranchFolderNode | BranchLeafNode;
|
|
|
|
interface BranchFolderNode {
|
|
kind: "folder";
|
|
id: string;
|
|
name: string;
|
|
children: BranchTreeNode[];
|
|
branchCount: number;
|
|
current: boolean;
|
|
folders: Map<string, BranchFolderNode>;
|
|
}
|
|
|
|
interface BranchLeafNode {
|
|
kind: "branch";
|
|
id: string;
|
|
branch: GitBranchInfo;
|
|
displayName: string;
|
|
}
|
|
|
|
type BranchRow = BranchFolderRow | BranchLeafRow;
|
|
|
|
interface BranchFolderRow {
|
|
kind: "folder";
|
|
id: string;
|
|
name: string;
|
|
depth: number;
|
|
branchCount: number;
|
|
current: boolean;
|
|
}
|
|
|
|
interface BranchLeafRow {
|
|
kind: "branch";
|
|
id: string;
|
|
branch: GitBranchInfo;
|
|
displayName: string;
|
|
depth: number;
|
|
scopeLabel: string;
|
|
}
|
|
|
|
interface Props {
|
|
branches: GitBranchInfo[];
|
|
localBranches: GitBranchInfo[];
|
|
remoteBranches: GitBranchInfo[];
|
|
tags: GitTag[];
|
|
hasRepository: boolean;
|
|
isBusy: boolean;
|
|
onCheckout: (branch: GitBranchInfo) => void;
|
|
onMerge: (branch: GitBranchInfo) => void;
|
|
onRebase: (branch: GitBranchInfo) => void;
|
|
onCreateBranch: (branchName: string) => void | Promise<void>;
|
|
onRenameBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
|
onDeleteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
|
onCreateTag: (name: string, message: string) => void | Promise<void>;
|
|
onDeleteTag: (tag: GitTag) => void | Promise<void>;
|
|
onPushTag: (tag: GitTag) => void | Promise<void>;
|
|
collapsed?: boolean;
|
|
onToggleCollapsed?: () => void;
|
|
}
|
|
|
|
let {
|
|
branches = [],
|
|
localBranches = [],
|
|
remoteBranches = [],
|
|
tags = [],
|
|
hasRepository = false,
|
|
isBusy = false,
|
|
onCheckout = () => {},
|
|
onMerge = () => {},
|
|
onRebase = () => {},
|
|
onCreateBranch = () => {},
|
|
onRenameBranch = () => {},
|
|
onDeleteBranch = () => {},
|
|
onCreateTag = () => {},
|
|
onDeleteTag = () => {},
|
|
onPushTag = () => {},
|
|
collapsed = false,
|
|
onToggleCollapsed = () => {},
|
|
}: Props = $props();
|
|
|
|
let localOpen = $state(true);
|
|
let remoteOpen = $state(false);
|
|
let tagsOpen = $state(false);
|
|
let createOpen = $state(false);
|
|
let newBranchName = $state("");
|
|
let createInput = $state<HTMLInputElement | null>(null);
|
|
let tagCreateOpen = $state(false);
|
|
let newTagName = $state("");
|
|
let newTagMessage = $state("");
|
|
let tagCreateInput = $state<HTMLInputElement | null>(null);
|
|
let panelElement = $state<HTMLElement | null>(null);
|
|
let contextBranch = $state<GitBranchInfo | null>(null);
|
|
let contextMenuX = $state(0);
|
|
let contextMenuY = $state(0);
|
|
let contextTag = $state<GitTag | null>(null);
|
|
let tagContextMenuX = $state(0);
|
|
let tagContextMenuY = $state(0);
|
|
let collapsedBranchFolders = $state<Set<string>>(new Set());
|
|
|
|
let localBranchRows = $derived(buildBranchRows("local", localBranches, "local"));
|
|
let remoteBranchRows = $derived(buildBranchRows("remote", remoteBranches, "remote"));
|
|
|
|
function createFolder(id: string, name: string): BranchFolderNode {
|
|
return {
|
|
kind: "folder",
|
|
id,
|
|
name,
|
|
children: [],
|
|
branchCount: 0,
|
|
current: false,
|
|
folders: new Map(),
|
|
};
|
|
}
|
|
|
|
function buildBranchRows(scope: string, branchList: GitBranchInfo[], scopeLabel: string): BranchRow[] {
|
|
const root = createFolder(`${scope}:root`, "");
|
|
|
|
for (const branch of branchList) {
|
|
const parts = branch.name.split("/").filter(Boolean);
|
|
const displayName = parts.length > 0 ? parts[parts.length - 1] : branch.name;
|
|
const folderParts = parts.slice(0, -1);
|
|
let parent = root;
|
|
|
|
for (let index = 0; index < folderParts.length; index += 1) {
|
|
const folderName = folderParts[index];
|
|
const folderPath = folderParts.slice(0, index + 1).join("/");
|
|
let folder = parent.folders.get(folderName);
|
|
|
|
if (!folder) {
|
|
folder = createFolder(`${scope}:folder:${folderPath}`, folderName);
|
|
parent.folders.set(folderName, folder);
|
|
parent.children.push(folder);
|
|
}
|
|
|
|
folder.branchCount += 1;
|
|
folder.current ||= branch.current;
|
|
parent = folder;
|
|
}
|
|
|
|
parent.children.push({
|
|
kind: "branch",
|
|
id: `${scope}:branch:${branch.name}`,
|
|
branch,
|
|
displayName,
|
|
});
|
|
}
|
|
|
|
sortBranchNodes(root.children);
|
|
|
|
const rows: BranchRow[] = [];
|
|
flattenBranchNodes(root.children, rows, 0, scopeLabel);
|
|
return rows;
|
|
}
|
|
|
|
function sortBranchNodes(nodes: BranchTreeNode[]) {
|
|
nodes.sort((left, right) => {
|
|
if (left.kind !== right.kind) return left.kind === "folder" ? -1 : 1;
|
|
const leftName = left.kind === "folder" ? left.name : left.displayName;
|
|
const rightName = right.kind === "folder" ? right.name : right.displayName;
|
|
return leftName.localeCompare(rightName, undefined, { sensitivity: "base" });
|
|
});
|
|
|
|
for (const node of nodes) {
|
|
if (node.kind === "folder") sortBranchNodes(node.children);
|
|
}
|
|
}
|
|
|
|
function flattenBranchNodes(nodes: BranchTreeNode[], rows: BranchRow[], depth: number, scopeLabel: string) {
|
|
for (const node of nodes) {
|
|
if (node.kind === "folder") {
|
|
rows.push({
|
|
kind: "folder",
|
|
id: node.id,
|
|
name: node.name,
|
|
depth,
|
|
branchCount: node.branchCount,
|
|
current: node.current,
|
|
});
|
|
|
|
if (isBranchFolderOpen(node.id)) {
|
|
flattenBranchNodes(node.children, rows, depth + 1, scopeLabel);
|
|
}
|
|
} else {
|
|
rows.push({
|
|
kind: "branch",
|
|
id: node.id,
|
|
branch: node.branch,
|
|
displayName: node.displayName,
|
|
depth,
|
|
scopeLabel,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function isBranchFolderOpen(id: string) {
|
|
return !collapsedBranchFolders.has(id);
|
|
}
|
|
|
|
function toggleBranchFolder(id: string) {
|
|
const next = new Set(collapsedBranchFolders);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
collapsedBranchFolders = next;
|
|
}
|
|
|
|
function openCreateForm() {
|
|
if (!hasRepository || isBusy) return;
|
|
createOpen = true;
|
|
queueMicrotask(() => createInput?.focus());
|
|
}
|
|
|
|
function closeCreateForm() {
|
|
createOpen = false;
|
|
newBranchName = "";
|
|
}
|
|
|
|
async function submitCreate(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
const value = newBranchName.trim();
|
|
if (!value || !hasRepository || isBusy) return;
|
|
await onCreateBranch(value);
|
|
newBranchName = "";
|
|
createOpen = false;
|
|
localOpen = true;
|
|
}
|
|
|
|
function checkoutOnDoubleClick(event: MouseEvent, branch: GitBranchInfo) {
|
|
if (branch.current || isBusy) return;
|
|
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
if (target?.closest("button")) return;
|
|
onCheckout(branch);
|
|
}
|
|
|
|
function openBranchContextMenu(event: MouseEvent, branch: GitBranchInfo) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (isBusy) return;
|
|
|
|
const rect = panelElement?.getBoundingClientRect();
|
|
const rawX = rect ? event.clientX - rect.left : event.offsetX;
|
|
const rawY = rect ? event.clientY - rect.top : event.offsetY;
|
|
const maxX = Math.max(8, (rect?.width ?? window.innerWidth) - 192);
|
|
const maxY = Math.max(8, (rect?.height ?? window.innerHeight) - 190);
|
|
|
|
contextBranch = branch;
|
|
contextMenuX = Math.max(8, Math.min(rawX, maxX));
|
|
contextMenuY = Math.max(8, Math.min(rawY, maxY));
|
|
}
|
|
|
|
function closeBranchContextMenu() {
|
|
contextBranch = null;
|
|
}
|
|
|
|
async function renameContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onRenameBranch(branch);
|
|
}
|
|
|
|
async function deleteContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || branch.current || branch.remote || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onDeleteBranch(branch);
|
|
}
|
|
|
|
async function checkoutContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || branch.current || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onCheckout(branch);
|
|
}
|
|
|
|
async function mergeContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || branch.current || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onMerge(branch);
|
|
}
|
|
|
|
async function rebaseContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || branch.current || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onRebase(branch);
|
|
}
|
|
|
|
function openTagCreateForm() {
|
|
if (!hasRepository || isBusy) return;
|
|
tagCreateOpen = true;
|
|
queueMicrotask(() => tagCreateInput?.focus());
|
|
}
|
|
|
|
function closeTagCreateForm() {
|
|
tagCreateOpen = false;
|
|
newTagName = "";
|
|
newTagMessage = "";
|
|
}
|
|
|
|
async function submitCreateTag(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
const name = newTagName.trim();
|
|
if (!name || !hasRepository || isBusy) return;
|
|
await onCreateTag(name, newTagMessage.trim());
|
|
newTagName = "";
|
|
newTagMessage = "";
|
|
tagCreateOpen = false;
|
|
tagsOpen = true;
|
|
}
|
|
|
|
function openTagContextMenu(event: MouseEvent, tag: GitTag) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (isBusy) return;
|
|
|
|
const rect = panelElement?.getBoundingClientRect();
|
|
const rawX = rect ? event.clientX - rect.left : event.offsetX;
|
|
const rawY = rect ? event.clientY - rect.top : event.offsetY;
|
|
const maxX = Math.max(8, (rect?.width ?? window.innerWidth) - 192);
|
|
const maxY = Math.max(8, (rect?.height ?? window.innerHeight) - 130);
|
|
|
|
contextTag = tag;
|
|
tagContextMenuX = Math.max(8, Math.min(rawX, maxX));
|
|
tagContextMenuY = Math.max(8, Math.min(rawY, maxY));
|
|
}
|
|
|
|
function closeTagContextMenu() {
|
|
contextTag = null;
|
|
}
|
|
|
|
async function pushContextTag() {
|
|
const tag = contextTag;
|
|
if (!tag || isBusy) return;
|
|
closeTagContextMenu();
|
|
await onPushTag(tag);
|
|
}
|
|
|
|
async function deleteContextTag() {
|
|
const tag = contextTag;
|
|
if (!tag || isBusy) return;
|
|
closeTagContextMenu();
|
|
await onDeleteTag(tag);
|
|
}
|
|
|
|
function closeAllContextMenus() {
|
|
closeBranchContextMenu();
|
|
closeTagContextMenu();
|
|
}
|
|
|
|
function handleWindowKeydown(event: KeyboardEvent) {
|
|
if (event.key === "Escape") closeAllContextMenus();
|
|
}
|
|
</script>
|
|
|
|
<svelte:window on:click={closeAllContextMenus} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeAllContextMenus} />
|
|
|
|
<section bind:this={panelElement} class="panel branch-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="Branches">
|
|
<div class="section-head">
|
|
<div>
|
|
<span class="eyebrow">Branches</span>
|
|
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Refs</h2>
|
|
</div>
|
|
<div class="branch-head-actions">
|
|
<button
|
|
class="branch-create-toggle"
|
|
type="button"
|
|
onclick={openCreateForm}
|
|
disabled={!hasRepository || isBusy}
|
|
title="Create new branch"
|
|
aria-label="Create new branch"
|
|
>
|
|
<Plus size={14} aria-hidden="true" />
|
|
</button>
|
|
<span class="pill pill-count">{branches.length}</span>
|
|
<button
|
|
class="branch-create-toggle panel-collapse-toggle"
|
|
type="button"
|
|
onclick={onToggleCollapsed}
|
|
aria-expanded={!collapsed}
|
|
title={collapsed ? "Expand branches" : "Collapse branches"}
|
|
aria-label={collapsed ? "Expand branches panel" : "Collapse branches panel"}
|
|
>
|
|
{#if collapsed}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{#if collapsed}
|
|
<!-- collapsed -->
|
|
{:else if !hasRepository}
|
|
<p class="blank-state">Open a repository to list branches.</p>
|
|
{:else if branches.length === 0 && tags.length === 0}
|
|
<p class="blank-state">No branches returned.</p>
|
|
{:else}
|
|
<div class="branch-list overflow-auto p-2 flex flex-col gap-0">
|
|
{#if createOpen}
|
|
<form class="branch-create-form" onsubmit={submitCreate}>
|
|
<GitBranch size={15} aria-hidden="true" />
|
|
<input
|
|
bind:this={createInput}
|
|
bind:value={newBranchName}
|
|
disabled={isBusy}
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
placeholder="new-branch-name"
|
|
aria-label="New branch name"
|
|
/>
|
|
<button class="branch-create-action confirm" type="submit" disabled={isBusy || newBranchName.trim().length === 0} title="Create branch">
|
|
<Check size={14} aria-hidden="true" />
|
|
</button>
|
|
<button class="branch-create-action" type="button" onclick={closeCreateForm} disabled={isBusy} title="Cancel">
|
|
<X size={14} aria-hidden="true" />
|
|
</button>
|
|
</form>
|
|
{/if}
|
|
|
|
<div class="branch-group">
|
|
<button
|
|
class="branch-group-toggle"
|
|
type="button"
|
|
onclick={() => { localOpen = !localOpen; }}
|
|
aria-expanded={localOpen}
|
|
>
|
|
{#if localOpen}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{/if}
|
|
<span>Local</span>
|
|
<span class="branch-group-count">{localBranches.length}</span>
|
|
</button>
|
|
|
|
{#if localOpen}
|
|
{#if localBranches.length === 0}
|
|
<div class="branch-empty">No local branches.</div>
|
|
{:else}
|
|
{#each localBranchRows as row (row.id)}
|
|
{#if row.kind === "folder"}
|
|
<button
|
|
class="branch-folder-row"
|
|
class:current={row.current}
|
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
|
type="button"
|
|
onclick={() => toggleBranchFolder(row.id)}
|
|
aria-expanded={isBranchFolderOpen(row.id)}
|
|
title={`${row.name} (${row.branchCount})`}
|
|
>
|
|
{#if isBranchFolderOpen(row.id)}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
<FolderOpen size={15} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
<Folder size={15} aria-hidden="true" />
|
|
{/if}
|
|
<span class="branch-folder-name">{row.name}</span>
|
|
<span class="branch-folder-count">{row.branchCount}</span>
|
|
</button>
|
|
{:else}
|
|
<article
|
|
class="branch-row"
|
|
class:current={row.branch.current}
|
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
|
ondblclick={(event) => checkoutOnDoubleClick(event, row.branch)}
|
|
oncontextmenu={(event) => openBranchContextMenu(event, row.branch)}
|
|
title={row.branch.current ? "Current branch" : row.branch.name}
|
|
>
|
|
<div class="branch-info">
|
|
<GitBranch size={16} aria-hidden="true" />
|
|
<div>
|
|
<strong>{row.displayName}</strong>
|
|
<span>{row.scopeLabel}</span>
|
|
</div>
|
|
</div>
|
|
{#if row.branch.current}
|
|
<span class="pill pill-active">Current</span>
|
|
{/if}
|
|
</article>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="branch-group">
|
|
<button
|
|
class="branch-group-toggle"
|
|
type="button"
|
|
onclick={() => { remoteOpen = !remoteOpen; }}
|
|
aria-expanded={remoteOpen}
|
|
>
|
|
{#if remoteOpen}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{/if}
|
|
<span>Remote</span>
|
|
<span class="branch-group-count">{remoteBranches.length}</span>
|
|
</button>
|
|
|
|
{#if remoteOpen}
|
|
{#if remoteBranches.length === 0}
|
|
<div class="branch-empty">No remote branches.</div>
|
|
{:else}
|
|
{#each remoteBranchRows as row (row.id)}
|
|
{#if row.kind === "folder"}
|
|
<button
|
|
class="branch-folder-row"
|
|
class:current={row.current}
|
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
|
type="button"
|
|
onclick={() => toggleBranchFolder(row.id)}
|
|
aria-expanded={isBranchFolderOpen(row.id)}
|
|
title={`${row.name} (${row.branchCount})`}
|
|
>
|
|
{#if isBranchFolderOpen(row.id)}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
<FolderOpen size={15} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
<Folder size={15} aria-hidden="true" />
|
|
{/if}
|
|
<span class="branch-folder-name">{row.name}</span>
|
|
<span class="branch-folder-count">{row.branchCount}</span>
|
|
</button>
|
|
{:else}
|
|
<article
|
|
class="branch-row"
|
|
class:current={row.branch.current}
|
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
|
ondblclick={(event) => checkoutOnDoubleClick(event, row.branch)}
|
|
oncontextmenu={(event) => openBranchContextMenu(event, row.branch)}
|
|
title={row.branch.current ? "Current branch" : row.branch.name}
|
|
>
|
|
<div class="branch-info">
|
|
<GitBranch size={16} aria-hidden="true" />
|
|
<div>
|
|
<strong>{row.displayName}</strong>
|
|
<span>{row.scopeLabel}</span>
|
|
</div>
|
|
</div>
|
|
{#if row.branch.current}
|
|
<span class="pill pill-active">Current</span>
|
|
{/if}
|
|
</article>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="branch-group">
|
|
<div class="tag-group-head">
|
|
<button
|
|
class="branch-group-toggle"
|
|
type="button"
|
|
onclick={() => { tagsOpen = !tagsOpen; }}
|
|
aria-expanded={tagsOpen}
|
|
>
|
|
{#if tagsOpen}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{/if}
|
|
<span>Tags</span>
|
|
<span class="branch-group-count">{tags.length}</span>
|
|
</button>
|
|
<button
|
|
class="branch-create-toggle"
|
|
type="button"
|
|
onclick={openTagCreateForm}
|
|
disabled={!hasRepository || isBusy}
|
|
title="Create new tag"
|
|
aria-label="Create new tag"
|
|
>
|
|
<Plus size={13} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
|
|
{#if tagCreateOpen}
|
|
<form class="branch-create-form tag-create-form" onsubmit={submitCreateTag}>
|
|
<TagIcon size={15} aria-hidden="true" />
|
|
<input
|
|
bind:this={tagCreateInput}
|
|
bind:value={newTagName}
|
|
disabled={isBusy}
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
placeholder="v1.0.0"
|
|
aria-label="New tag name"
|
|
/>
|
|
<input
|
|
bind:value={newTagMessage}
|
|
disabled={isBusy}
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
placeholder="Message (optional)"
|
|
aria-label="Tag message"
|
|
/>
|
|
<button class="branch-create-action confirm" type="submit" disabled={isBusy || newTagName.trim().length === 0} title="Create tag">
|
|
<Check size={14} aria-hidden="true" />
|
|
</button>
|
|
<button class="branch-create-action" type="button" onclick={closeTagCreateForm} disabled={isBusy} title="Cancel">
|
|
<X size={14} aria-hidden="true" />
|
|
</button>
|
|
</form>
|
|
{/if}
|
|
|
|
{#if tagsOpen}
|
|
{#if tags.length === 0}
|
|
<div class="branch-empty">No tags.</div>
|
|
{:else}
|
|
{#each tags as tag (tag.name)}
|
|
<article
|
|
class="branch-row"
|
|
oncontextmenu={(event) => openTagContextMenu(event, tag)}
|
|
title={tag.message ?? tag.name}
|
|
>
|
|
<div class="branch-info">
|
|
<TagIcon size={16} aria-hidden="true" />
|
|
<div>
|
|
<strong>{tag.name}</strong>
|
|
<span>{tag.short_hash}</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if contextBranch}
|
|
<div
|
|
class="branch-context-menu"
|
|
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
|
|
role="menu"
|
|
tabindex="-1"
|
|
aria-label={`Actions for ${contextBranch.name}`}
|
|
>
|
|
<button type="button" role="menuitem" onclick={checkoutContextBranch} disabled={isBusy || contextBranch.current}>
|
|
<GitBranch size={14} aria-hidden="true" />
|
|
Checkout
|
|
</button>
|
|
<button type="button" role="menuitem" onclick={mergeContextBranch} disabled={isBusy || contextBranch.current}>
|
|
<GitMerge size={14} aria-hidden="true" />
|
|
Merge into current
|
|
</button>
|
|
<button type="button" role="menuitem" onclick={rebaseContextBranch} disabled={isBusy || contextBranch.current}>
|
|
<GitBranch size={14} aria-hidden="true" />
|
|
Rebase current onto this
|
|
</button>
|
|
<div class="menu-separator" role="separator"></div>
|
|
<button type="button" role="menuitem" onclick={renameContextBranch} disabled={isBusy || contextBranch.remote}>
|
|
<Pencil size={14} aria-hidden="true" />
|
|
Rename
|
|
</button>
|
|
<button
|
|
class="danger"
|
|
type="button"
|
|
role="menuitem"
|
|
onclick={deleteContextBranch}
|
|
disabled={isBusy || contextBranch.current || contextBranch.remote}
|
|
title={contextBranch.current ? "Current branch cannot be deleted" : contextBranch.remote ? "Remote branch cannot be deleted here" : "Delete local branch"}
|
|
>
|
|
<Trash2 size={14} aria-hidden="true" />
|
|
Delete
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if contextTag}
|
|
<div
|
|
class="branch-context-menu"
|
|
style={`left: ${tagContextMenuX}px; top: ${tagContextMenuY}px;`}
|
|
role="menu"
|
|
tabindex="-1"
|
|
aria-label={`Actions for ${contextTag.name}`}
|
|
>
|
|
<button type="button" role="menuitem" onclick={pushContextTag} disabled={isBusy}>
|
|
<Upload size={14} aria-hidden="true" />
|
|
Push to remote
|
|
</button>
|
|
<div class="menu-separator" role="separator"></div>
|
|
<button class="danger" type="button" role="menuitem" onclick={deleteContextTag} disabled={isBusy} title="Delete local tag">
|
|
<Trash2 size={14} aria-hidden="true" />
|
|
Delete
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</section>
|