add new Context Menu for file Explorer

fix blocking UI
when select the file in Status also select in the file History
This commit is contained in:
Christoph Brandau
2026-07-02 16:24:38 +02:00
parent 794680a696
commit e3df75cc38
11 changed files with 399 additions and 75 deletions
+1 -1
View File
@@ -252,7 +252,7 @@
}
</script>
<svelte:window on:click={closeBranchContextMenu} on:keydown={handleWindowKeydown} />
<svelte:window on:click={closeBranchContextMenu} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeBranchContextMenu} />
<section bind:this={panelElement} class="panel branch-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Branches">
<div class="section-head">
+58 -1
View File
@@ -17,6 +17,7 @@
FileVideo,
Folder,
FolderOpen,
ExternalLink,
Terminal,
} from "@lucide/svelte";
import { languageIconForPath } from "../languageIcons";
@@ -34,6 +35,7 @@
onExpandAllFolders: () => void;
onCollapseAllFolders: () => void;
onSelectNode: (node: ExplorerNode) => void;
onOpenFile: (node: ExplorerNode) => void;
}
let {
@@ -47,8 +49,13 @@
onExpandAllFolders = () => {},
onCollapseAllFolders = () => {},
onSelectNode = () => {},
onOpenFile = () => {},
}: Props = $props();
let contextNode = $state<ExplorerNode | null>(null);
let contextMenuX = $state(0);
let contextMenuY = $state(0);
function mergeExplorerStatus(current: FileStatusKind | null, next: FileStatusKind | null): FileStatusKind | null {
if (!next) return current;
if (!current) return next;
@@ -154,12 +161,39 @@
return "text";
}
function openFileContextMenu(event: MouseEvent, node: ExplorerNode) {
if (node.kind !== "file") return;
event.preventDefault();
event.stopPropagation();
contextNode = node;
contextMenuX = Math.max(8, Math.min(event.clientX + 2, window.innerWidth - 192));
contextMenuY = Math.max(8, Math.min(event.clientY + 2, window.innerHeight - 56));
}
function closeFileContextMenu() {
contextNode = null;
}
function openContextFile() {
const node = contextNode;
if (!node || node.kind !== "file") return;
closeFileContextMenu();
onOpenFile(node);
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key === "Escape") closeFileContextMenu();
}
let explorerTree = $derived(buildExplorerTree(repoFiles));
let visibleNodes = $derived(flattenExplorerTree(explorerTree, expandedExplorerPaths));
let hasFolders = $derived(explorerTree.some((node) => node.kind === "folder"));
</script>
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="File explorer">
<svelte:window on:click={closeFileContextMenu} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeFileContextMenu} />
<section class="panel explorer-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="File explorer">
<div class="section-head">
<div>
<span class="eyebrow">Explorer</span>
@@ -264,6 +298,7 @@
class="explorer-select"
type="button"
onclick={() => onSelectNode(node)}
oncontextmenu={(event) => openFileContextMenu(event, node)}
disabled={isBusy}
title={`Show history for ${node.path}`}
>
@@ -279,4 +314,26 @@
{/each}
</div>
{/if}
</section>
{#if contextNode}
<div
class="explorer-context-menu"
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
role="menu"
tabindex="-1"
aria-label={`Actions for ${contextNode.path}`}
>
<button
type="button"
role="menuitem"
onclick={openContextFile}
disabled={contextNode.status === "deleted"}
title={contextNode.status === "deleted" ? "Deleted files cannot be revealed in Explorer" : "Reveal this file in Explorer"}
>
<ExternalLink size={14} aria-hidden="true" />
Open in Explorer
</button>
</div>
{/if}
+16 -2
View File
@@ -9,6 +9,8 @@
hasRepository: boolean;
isBusy: boolean;
status: GitStatus | null;
selectedFilePath: string;
onSelectFile: (file: GitFileStatus) => void;
onStage: (file: GitFileStatus) => void;
onUnstage: (file: GitFileStatus) => void;
onDiscard: (file: GitFileStatus, staged: boolean) => void;
@@ -24,6 +26,8 @@
hasRepository = false,
isBusy = false,
status = null,
selectedFilePath = "",
onSelectFile = () => {},
onStage = () => {},
onUnstage = () => {},
onDiscard = () => {},
@@ -102,9 +106,19 @@
{:else}
<div class="overflow-auto p-2">
{#each changedFiles as file (`${file.old_path ?? ""}:${file.path}`)}
<article class="file-row">
<article
class="file-row"
class:selected={selectedFilePath === file.path}
>
<div class="file-title">
<strong title={displayPath(file)}>{fileName(file)}</strong>
<button
class="file-title-button"
type="button"
onclick={() => onSelectFile(file)}
title={`Select ${displayPath(file)} in Explorer`}
>
<strong>{fileName(file)}</strong>
</button>
</div>
<div class="change-lanes">