Add submodule management and collapsible Worktree/Tags panels #45
+189
-30
@@ -24,6 +24,8 @@
|
|||||||
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
|
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
|
||||||
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
|
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
|
||||||
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
||||||
|
import TagsPanel from "./lib/components/TagsPanel.svelte";
|
||||||
|
import WorktreePanel from "./lib/components/WorktreePanel.svelte";
|
||||||
import CloneRepositoryDialog from "./lib/components/CloneRepositoryDialog.svelte";
|
import CloneRepositoryDialog from "./lib/components/CloneRepositoryDialog.svelte";
|
||||||
import CommandPalette from "./lib/components/CommandPalette.svelte";
|
import CommandPalette from "./lib/components/CommandPalette.svelte";
|
||||||
import CommitNoteDialog from "./lib/components/CommitNoteDialog.svelte";
|
import CommitNoteDialog from "./lib/components/CommitNoteDialog.svelte";
|
||||||
@@ -293,6 +295,8 @@
|
|||||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||||
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
||||||
const LEFT_BRANCH_PANEL_HEIGHT_KEY = "gitlite.leftBranchPanelHeight.v1";
|
const LEFT_BRANCH_PANEL_HEIGHT_KEY = "gitlite.leftBranchPanelHeight.v1";
|
||||||
|
const LEFT_TAGS_PANEL_HEIGHT_KEY = "gitlite.leftTagsPanelHeight.v1";
|
||||||
|
const LEFT_WORKTREE_PANEL_HEIGHT_KEY = "gitlite.leftWorktreePanelHeight.v1";
|
||||||
const LEFT_STASH_PANEL_HEIGHT_KEY = "gitlite.leftStashPanelHeight.v1";
|
const LEFT_STASH_PANEL_HEIGHT_KEY = "gitlite.leftStashPanelHeight.v1";
|
||||||
const BRANCH_PANEL_COLLAPSED_KEY = "gitlite.branchPanelCollapsed.v1";
|
const BRANCH_PANEL_COLLAPSED_KEY = "gitlite.branchPanelCollapsed.v1";
|
||||||
const STASH_PANEL_COLLAPSED_KEY = "gitlite.stashPanelCollapsed.v2";
|
const STASH_PANEL_COLLAPSED_KEY = "gitlite.stashPanelCollapsed.v2";
|
||||||
@@ -445,6 +449,8 @@
|
|||||||
let worktreeDialogOpen = false;
|
let worktreeDialogOpen = false;
|
||||||
let worktreeInitialBranch = "";
|
let worktreeInitialBranch = "";
|
||||||
let worktrees: GitWorktree[] = [];
|
let worktrees: GitWorktree[] = [];
|
||||||
|
let worktreeLoadId = 0;
|
||||||
|
let worktreePanelCollapsed = loadStoredBoolean("gitlite.worktreePanelCollapsed.v1", false);
|
||||||
let worktreesLoading = false;
|
let worktreesLoading = false;
|
||||||
let worktreeError = "";
|
let worktreeError = "";
|
||||||
let gitLfsDialogOpen = false;
|
let gitLfsDialogOpen = false;
|
||||||
@@ -544,12 +550,21 @@
|
|||||||
let resizingLeftBranchPanel = false;
|
let resizingLeftBranchPanel = false;
|
||||||
let leftBranchResizeStartY = 0;
|
let leftBranchResizeStartY = 0;
|
||||||
let leftBranchResizeStartHeight = 0;
|
let leftBranchResizeStartHeight = 0;
|
||||||
|
let leftWorktreePanelHeight = loadLeftWorktreePanelHeight();
|
||||||
|
let resizingLeftWorktreePanel = false;
|
||||||
|
let leftWorktreeResizeStartY = 0;
|
||||||
|
let leftWorktreeResizeStartHeight = 0;
|
||||||
|
let leftTagsPanelHeight = loadLeftTagsPanelHeight();
|
||||||
|
let resizingLeftTagsPanel = false;
|
||||||
|
let leftTagsResizeStartY = 0;
|
||||||
|
let leftTagsResizeStartHeight = 0;
|
||||||
let leftStashPanelHeight = loadLeftStashPanelHeight();
|
let leftStashPanelHeight = loadLeftStashPanelHeight();
|
||||||
let resizingLeftStashPanel = false;
|
let resizingLeftStashPanel = false;
|
||||||
let leftStashResizeStartY = 0;
|
let leftStashResizeStartY = 0;
|
||||||
let leftStashResizeStartHeight = 0;
|
let leftStashResizeStartHeight = 0;
|
||||||
|
let tagsPanelCollapsed = loadStoredBoolean("gitlite.tagsPanelCollapsed.v1", true);
|
||||||
let branchPanelCollapsed = loadStoredBoolean(BRANCH_PANEL_COLLAPSED_KEY, false);
|
let branchPanelCollapsed = loadStoredBoolean(BRANCH_PANEL_COLLAPSED_KEY, false);
|
||||||
let stashPanelCollapsed = loadStoredBoolean(STASH_PANEL_COLLAPSED_KEY, false);
|
let stashPanelCollapsed = loadStoredBoolean(STASH_PANEL_COLLAPSED_KEY, true);
|
||||||
let explorerPanelCollapsed = loadStoredBoolean(EXPLORER_PANEL_COLLAPSED_KEY, false);
|
let explorerPanelCollapsed = loadStoredBoolean(EXPLORER_PANEL_COLLAPSED_KEY, false);
|
||||||
let historyAsideWidth = loadHistoryAsideWidth();
|
let historyAsideWidth = loadHistoryAsideWidth();
|
||||||
let resizingHistoryAside = false;
|
let resizingHistoryAside = false;
|
||||||
@@ -602,8 +617,8 @@
|
|||||||
) as Record<string, string>;
|
) as Record<string, string>;
|
||||||
$: remoteBranches = branches.filter((b) => b.remote);
|
$: remoteBranches = branches.filter((b) => b.remote);
|
||||||
$: currentBranchIsLocalOnly = Boolean(status?.current_branch) && !status?.upstream;
|
$: currentBranchIsLocalOnly = Boolean(status?.current_branch) && !status?.upstream;
|
||||||
$: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed);
|
$: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed);
|
||||||
$: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
|
$: allLeftPanelsCollapsed = branchPanelCollapsed && worktreePanelCollapsed && tagsPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
|
||||||
$: editorToolName = externalToolDisplayName("editor", externalToolsSettings.editor, detectedExternalTools);
|
$: editorToolName = externalToolDisplayName("editor", externalToolsSettings.editor, detectedExternalTools);
|
||||||
$: diffToolName = externalToolDisplayName("diff", externalToolsSettings.diff, detectedExternalTools);
|
$: diffToolName = externalToolDisplayName("diff", externalToolsSettings.diff, detectedExternalTools);
|
||||||
$: mergeToolName = externalToolDisplayName("merge", externalToolsSettings.merge, detectedExternalTools);
|
$: mergeToolName = externalToolDisplayName("merge", externalToolsSettings.merge, detectedExternalTools);
|
||||||
@@ -2001,18 +2016,108 @@
|
|||||||
persistLeftSidebarWidth(leftSidebarWidth);
|
persistLeftSidebarWidth(leftSidebarWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLeftSidebarRows(branchCollapsed: boolean, stashCollapsed: boolean, explorerCollapsed: boolean): string {
|
function buildLeftSidebarRows(branchCollapsed: boolean, stashCollapsed: boolean, explorerCollapsed: boolean, worktreeCollapsed: boolean, tagsCollapsed: boolean): string {
|
||||||
const branchRow = branchCollapsed
|
const branchRow = branchCollapsed
|
||||||
? "auto"
|
? "auto"
|
||||||
: `minmax(${LEFT_BRANCH_PANEL_MIN_HEIGHT}px, var(--branch-panel-height, ${LEFT_BRANCH_PANEL_DEFAULT_HEIGHT}px))`;
|
: `minmax(${LEFT_BRANCH_PANEL_MIN_HEIGHT}px, var(--branch-panel-height, ${LEFT_BRANCH_PANEL_DEFAULT_HEIGHT}px))`;
|
||||||
const branchHandleRow = branchCollapsed ? "0" : "14px";
|
const branchHandleRow = branchCollapsed ? "0" : "6px";
|
||||||
const stashRow = stashCollapsed
|
const stashRow = stashCollapsed
|
||||||
? "auto"
|
? "auto"
|
||||||
: `minmax(${LEFT_STASH_PANEL_MIN_HEIGHT}px, var(--stash-panel-height, ${LEFT_STASH_PANEL_DEFAULT_HEIGHT}px))`;
|
: `minmax(${LEFT_STASH_PANEL_MIN_HEIGHT}px, var(--stash-panel-height, ${LEFT_STASH_PANEL_DEFAULT_HEIGHT}px))`;
|
||||||
const stashHandleRow = explorerCollapsed || (stashCollapsed && branchCollapsed) ? "0" : "14px";
|
const stashHandleRow = explorerCollapsed || stashCollapsed ? "0" : "6px";
|
||||||
const explorerRow = explorerCollapsed ? "auto" : `minmax(${LEFT_EXPLORER_PANEL_MIN_HEIGHT}px, 1fr)`;
|
const explorerRow = explorerCollapsed ? "auto" : `minmax(${LEFT_EXPLORER_PANEL_MIN_HEIGHT}px, 1fr)`;
|
||||||
|
|
||||||
return `${branchRow} ${branchHandleRow} ${stashRow} ${stashHandleRow} ${explorerRow}`;
|
return `${branchRow} ${branchHandleRow} ${worktreeCollapsed ? "42px 0" : "var(--worktree-panel-height, 140px) 6px"} ${tagsCollapsed ? "42px 0" : "var(--tags-panel-height, 140px) 6px"} ${stashRow} ${stashHandleRow} ${explorerRow}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampLeftWorktreePanelHeight(value: number) {
|
||||||
|
return Math.min(420, Math.max(80, Math.round(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLeftWorktreePanelHeight() {
|
||||||
|
try {
|
||||||
|
const stored = Number(localStorage.getItem(LEFT_WORKTREE_PANEL_HEIGHT_KEY));
|
||||||
|
if (Number.isFinite(stored) && stored > 0) return clampLeftWorktreePanelHeight(stored);
|
||||||
|
} catch { /* Preferences are optional. */ }
|
||||||
|
return 140;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistLeftWorktreePanelHeight() {
|
||||||
|
try { localStorage.setItem(LEFT_WORKTREE_PANEL_HEIGHT_KEY, String(leftWorktreePanelHeight)); }
|
||||||
|
catch { /* Preferences are optional. */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLeftWorktreePanelResize(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
resizingLeftWorktreePanel = true;
|
||||||
|
leftWorktreeResizeStartY = event.clientY;
|
||||||
|
leftWorktreeResizeStartHeight = leftWorktreePanelHeight;
|
||||||
|
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeftWorktreePanelResizeMove(event: PointerEvent) {
|
||||||
|
if (!resizingLeftWorktreePanel) return;
|
||||||
|
leftWorktreePanelHeight = clampLeftWorktreePanelHeight(leftWorktreeResizeStartHeight + event.clientY - leftWorktreeResizeStartY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endLeftWorktreePanelResize(event: PointerEvent) {
|
||||||
|
if (!resizingLeftWorktreePanel) return;
|
||||||
|
resizingLeftWorktreePanel = false;
|
||||||
|
persistLeftWorktreePanelHeight();
|
||||||
|
const target = event.currentTarget as HTMLElement;
|
||||||
|
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeftWorktreePanelResizeKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
||||||
|
event.preventDefault();
|
||||||
|
leftWorktreePanelHeight = clampLeftWorktreePanelHeight(leftWorktreePanelHeight + (event.key === "ArrowDown" ? 20 : -20));
|
||||||
|
persistLeftWorktreePanelHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampLeftTagsPanelHeight(value: number) {
|
||||||
|
return Math.min(420, Math.max(80, Math.round(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLeftTagsPanelHeight() {
|
||||||
|
try {
|
||||||
|
const stored = Number(localStorage.getItem(LEFT_TAGS_PANEL_HEIGHT_KEY));
|
||||||
|
if (Number.isFinite(stored) && stored > 0) return clampLeftTagsPanelHeight(stored);
|
||||||
|
} catch { /* Preferences are optional. */ }
|
||||||
|
return 140;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistLeftTagsPanelHeight() {
|
||||||
|
try { localStorage.setItem(LEFT_TAGS_PANEL_HEIGHT_KEY, String(leftTagsPanelHeight)); }
|
||||||
|
catch { /* Preferences are optional. */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLeftTagsPanelResize(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
resizingLeftTagsPanel = true;
|
||||||
|
leftTagsResizeStartY = event.clientY;
|
||||||
|
leftTagsResizeStartHeight = leftTagsPanelHeight;
|
||||||
|
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeftTagsPanelResizeMove(event: PointerEvent) {
|
||||||
|
if (!resizingLeftTagsPanel) return;
|
||||||
|
leftTagsPanelHeight = clampLeftTagsPanelHeight(leftTagsResizeStartHeight + event.clientY - leftTagsResizeStartY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endLeftTagsPanelResize(event: PointerEvent) {
|
||||||
|
if (!resizingLeftTagsPanel) return;
|
||||||
|
resizingLeftTagsPanel = false;
|
||||||
|
persistLeftTagsPanelHeight();
|
||||||
|
const target = event.currentTarget as HTMLElement;
|
||||||
|
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeftTagsPanelResizeKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
||||||
|
event.preventDefault();
|
||||||
|
leftTagsPanelHeight = clampLeftTagsPanelHeight(leftTagsPanelHeight + (event.key === "ArrowDown" ? 20 : -20));
|
||||||
|
persistLeftTagsPanelHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
function startLeftBranchPanelResize(event: PointerEvent) {
|
function startLeftBranchPanelResize(event: PointerEvent) {
|
||||||
@@ -2248,6 +2353,8 @@
|
|||||||
worktreeDialogOpen = false;
|
worktreeDialogOpen = false;
|
||||||
worktreeInitialBranch = "";
|
worktreeInitialBranch = "";
|
||||||
worktrees = [];
|
worktrees = [];
|
||||||
|
worktreeLoadId++;
|
||||||
|
worktreesLoading = false;
|
||||||
submoduleDialogOpen = false;
|
submoduleDialogOpen = false;
|
||||||
submodules = [];
|
submodules = [];
|
||||||
uninitializedSubmoduleCount = 0;
|
uninitializedSubmoduleCount = 0;
|
||||||
@@ -2454,6 +2561,7 @@
|
|||||||
refreshStashes(resolvedPath, bundle.stashes),
|
refreshStashes(resolvedPath, bundle.stashes),
|
||||||
refreshCommitHistory(resolvedPath, bundle.commits),
|
refreshCommitHistory(resolvedPath, bundle.commits),
|
||||||
refreshExplorerFiles(resolvedPath, bundle.files),
|
refreshExplorerFiles(resolvedPath, bundle.files),
|
||||||
|
refreshWorktrees(resolvedPath),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (forceFileHistory || lastFileHistoryHeadHash !== previousHeadHash) {
|
if (forceFileHistory || lastFileHistoryHeadHash !== previousHeadHash) {
|
||||||
@@ -3373,16 +3481,8 @@
|
|||||||
if (!activeRepoPath || isBusy) return;
|
if (!activeRepoPath || isBusy) return;
|
||||||
worktreeInitialBranch = branch;
|
worktreeInitialBranch = branch;
|
||||||
worktreeDialogOpen = true;
|
worktreeDialogOpen = true;
|
||||||
worktreeError = "";
|
await refreshWorktrees();
|
||||||
worktreesLoading = true;
|
|
||||||
try {
|
|
||||||
worktrees = await listWorktrees(activeRepoPath);
|
|
||||||
trackEvent("worktree_dialog_opened", { linked_worktrees: Math.max(0, worktrees.length - 1) });
|
trackEvent("worktree_dialog_opened", { linked_worktrees: Math.max(0, worktrees.length - 1) });
|
||||||
} catch (error) {
|
|
||||||
worktreeError = errorToMessage(error);
|
|
||||||
} finally {
|
|
||||||
worktreesLoading = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBranchInWorktree(branch: GitBranchInfo) {
|
function openBranchInWorktree(branch: GitBranchInfo) {
|
||||||
@@ -3390,16 +3490,18 @@
|
|||||||
void openWorktreeDialog(branch.name);
|
void openWorktreeDialog(branch.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshWorktrees() {
|
async function refreshWorktrees(path = activeRepoPath) {
|
||||||
if (!activeRepoPath || worktreesLoading) return;
|
if (!path) return;
|
||||||
|
const request = ++worktreeLoadId;
|
||||||
worktreesLoading = true;
|
worktreesLoading = true;
|
||||||
worktreeError = "";
|
worktreeError = "";
|
||||||
try {
|
try {
|
||||||
worktrees = await listWorktrees(activeRepoPath);
|
const loaded = await listWorktrees(path);
|
||||||
|
if (request === worktreeLoadId && sameRepoPath(path, activeRepoPath)) worktrees = loaded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
worktreeError = errorToMessage(error);
|
if (request === worktreeLoadId && sameRepoPath(path, activeRepoPath)) worktreeError = errorToMessage(error);
|
||||||
} finally {
|
} finally {
|
||||||
worktreesLoading = false;
|
if (request === worktreeLoadId) worktreesLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5838,7 +5940,7 @@
|
|||||||
style="--left-sidebar-width: {leftSidebarWidth}px; --history-aside-min-width: {HISTORY_ASIDE_MIN_WIDTH}px; --history-aside-width: {historyAsideWidth}px;"
|
style="--left-sidebar-width: {leftSidebarWidth}px; --history-aside-min-width: {HISTORY_ASIDE_MIN_WIDTH}px; --history-aside-width: {historyAsideWidth}px;"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Left sidebar: branches + explorer -->
|
<!-- Compact repository navigation: branches, worktrees, tags, stashes, files -->
|
||||||
<aside
|
<aside
|
||||||
class="left-sidebar"
|
class="left-sidebar"
|
||||||
class:branch-collapsed={branchPanelCollapsed}
|
class:branch-collapsed={branchPanelCollapsed}
|
||||||
@@ -5846,13 +5948,12 @@
|
|||||||
class:explorer-collapsed={explorerPanelCollapsed}
|
class:explorer-collapsed={explorerPanelCollapsed}
|
||||||
class:all-collapsed={allLeftPanelsCollapsed}
|
class:all-collapsed={allLeftPanelsCollapsed}
|
||||||
aria-label="Repository navigation"
|
aria-label="Repository navigation"
|
||||||
style="--branch-panel-height: {leftBranchPanelHeight}px; --stash-panel-height: {leftStashPanelHeight}px; grid-template-rows: {leftSidebarRows};"
|
style="--tags-panel-height: {leftTagsPanelHeight}px; --worktree-panel-height: {leftWorktreePanelHeight}px; --branch-panel-height: {leftBranchPanelHeight}px; --stash-panel-height: {leftStashPanelHeight}px; grid-template-rows: {leftSidebarRows};"
|
||||||
>
|
>
|
||||||
<BranchPanel
|
<BranchPanel
|
||||||
{branches}
|
{branches}
|
||||||
{localBranches}
|
{localBranches}
|
||||||
{remoteBranches}
|
{remoteBranches}
|
||||||
{tags}
|
|
||||||
{hasRepository}
|
{hasRepository}
|
||||||
{isBusy}
|
{isBusy}
|
||||||
onCheckout={checkout}
|
onCheckout={checkout}
|
||||||
@@ -5864,10 +5965,6 @@
|
|||||||
onDeleteBranch={deleteLocalBranch}
|
onDeleteBranch={deleteLocalBranch}
|
||||||
onDeleteRemoteBranch={deleteTrackedRemoteBranch}
|
onDeleteRemoteBranch={deleteTrackedRemoteBranch}
|
||||||
onDeleteBranchFolder={deleteBranchFolder}
|
onDeleteBranchFolder={deleteBranchFolder}
|
||||||
onCreateTag={createNewTag}
|
|
||||||
onDeleteTag={deleteLocalTag}
|
|
||||||
onPushTag={pushLocalTag}
|
|
||||||
onManageWorktrees={() => { void openWorktreeDialog(); }}
|
|
||||||
onCreateWorktree={openBranchInWorktree}
|
onCreateWorktree={openBranchInWorktree}
|
||||||
collapsed={branchPanelCollapsed}
|
collapsed={branchPanelCollapsed}
|
||||||
onToggleCollapsed={toggleBranchPanelCollapsed}
|
onToggleCollapsed={toggleBranchPanelCollapsed}
|
||||||
@@ -5894,6 +5991,68 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
|
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
|
||||||
{/if}
|
{/if}
|
||||||
|
<WorktreePanel
|
||||||
|
{worktrees}
|
||||||
|
{hasRepository}
|
||||||
|
{isBusy}
|
||||||
|
loading={worktreesLoading}
|
||||||
|
error={worktreeError}
|
||||||
|
collapsed={worktreePanelCollapsed}
|
||||||
|
onToggleCollapsed={() => {
|
||||||
|
worktreePanelCollapsed = !worktreePanelCollapsed;
|
||||||
|
persistStoredBoolean("gitlite.worktreePanelCollapsed.v1", worktreePanelCollapsed);
|
||||||
|
}}
|
||||||
|
onOpen={openWorktreeTab}
|
||||||
|
onManage={() => { void openWorktreeDialog(); }}
|
||||||
|
onRefresh={() => { void refreshWorktrees(); }}
|
||||||
|
/>
|
||||||
|
{#if !worktreePanelCollapsed}
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="panel-resize-handle left-panel-resize-handle worktree-panel-resize-handle"
|
||||||
|
class:resizing={resizingLeftWorktreePanel}
|
||||||
|
role="separator" aria-orientation="horizontal" aria-label="Resize worktrees panel height"
|
||||||
|
aria-valuenow={leftWorktreePanelHeight} aria-valuemin={80} aria-valuemax={420}
|
||||||
|
tabindex="0"
|
||||||
|
onpointerdown={startLeftWorktreePanelResize}
|
||||||
|
onpointermove={onLeftWorktreePanelResizeMove}
|
||||||
|
onpointerup={endLeftWorktreePanelResize}
|
||||||
|
onpointercancel={endLeftWorktreePanelResize}
|
||||||
|
onkeydown={onLeftWorktreePanelResizeKeydown}
|
||||||
|
></div>
|
||||||
|
{:else}
|
||||||
|
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
|
||||||
|
{/if}
|
||||||
|
<TagsPanel
|
||||||
|
{tags} {hasRepository} {isBusy}
|
||||||
|
collapsed={tagsPanelCollapsed}
|
||||||
|
onToggleCollapsed={() => {
|
||||||
|
tagsPanelCollapsed = !tagsPanelCollapsed;
|
||||||
|
persistStoredBoolean("gitlite.tagsPanelCollapsed.v1", tagsPanelCollapsed);
|
||||||
|
}}
|
||||||
|
onCreateTag={createNewTag}
|
||||||
|
onDeleteTag={deleteLocalTag}
|
||||||
|
onPushTag={pushLocalTag}
|
||||||
|
/>
|
||||||
|
{#if !tagsPanelCollapsed}
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="panel-resize-handle left-panel-resize-handle tags-panel-resize-handle"
|
||||||
|
class:resizing={resizingLeftTagsPanel}
|
||||||
|
role="separator" aria-orientation="horizontal" aria-label="Resize tags panel height"
|
||||||
|
aria-valuenow={leftTagsPanelHeight} aria-valuemin={80} aria-valuemax={420}
|
||||||
|
tabindex="0"
|
||||||
|
onpointerdown={startLeftTagsPanelResize}
|
||||||
|
onpointermove={onLeftTagsPanelResizeMove}
|
||||||
|
onpointerup={endLeftTagsPanelResize}
|
||||||
|
onpointercancel={endLeftTagsPanelResize}
|
||||||
|
onkeydown={onLeftTagsPanelResizeKeydown}
|
||||||
|
></div>
|
||||||
|
{:else}
|
||||||
|
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
|
||||||
|
{/if}
|
||||||
<StashPanel
|
<StashPanel
|
||||||
{stashes}
|
{stashes}
|
||||||
changedCount={changedFiles.length}
|
changedCount={changedFiles.length}
|
||||||
@@ -5906,7 +6065,7 @@
|
|||||||
collapsed={stashPanelCollapsed}
|
collapsed={stashPanelCollapsed}
|
||||||
onToggleCollapsed={toggleStashPanelCollapsed}
|
onToggleCollapsed={toggleStashPanelCollapsed}
|
||||||
/>
|
/>
|
||||||
{#if !explorerPanelCollapsed && (!stashPanelCollapsed || !branchPanelCollapsed)}
|
{#if !explorerPanelCollapsed && !stashPanelCollapsed}
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<div
|
<div
|
||||||
@@ -6328,7 +6487,7 @@
|
|||||||
isLoading={worktreesLoading}
|
isLoading={worktreesLoading}
|
||||||
{isBusy}
|
{isBusy}
|
||||||
error={worktreeError}
|
error={worktreeError}
|
||||||
onRefresh={refreshWorktrees}
|
onRefresh={() => refreshWorktrees()}
|
||||||
onOpen={openWorktreeTab}
|
onOpen={openWorktreeTab}
|
||||||
onAdd={createWorktree}
|
onAdd={createWorktree}
|
||||||
onRemove={removeSelectedWorktree}
|
onRemove={removeSelectedWorktree}
|
||||||
|
|||||||
+80
@@ -9173,3 +9173,83 @@ section > header.page-header.page-header {
|
|||||||
border-color: var(--dialog-close-hover-ink);
|
border-color: var(--dialog-close-hover-ink);
|
||||||
background: color-mix(in srgb, var(--dialog-close-hover-ink) 12%, var(--app-dialog-chrome));
|
background: color-mix(in srgb, var(--dialog-close-hover-ink) 12%, var(--app-dialog-chrome));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact accordion navigation (layout A). */
|
||||||
|
.left-sidebar { overflow-y: auto; overflow-x: hidden; }
|
||||||
|
.left-sidebar > .panel { min-width: 0; }
|
||||||
|
.left-sidebar .section-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 6px;
|
||||||
|
min-height: 42px; padding: 6px 10px;
|
||||||
|
background: var(--color-surface-solid);
|
||||||
|
}
|
||||||
|
.left-sidebar .sidebar-section-title {
|
||||||
|
display: flex; align-items: center; gap: 9px; min-width: 0; margin: 0;
|
||||||
|
color: var(--color-ink-muted); font-size: 11px; font-weight: 750;
|
||||||
|
line-height: 20px; letter-spacing: .055em; text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.left-sidebar .sidebar-section-title svg { flex-shrink: 0; }
|
||||||
|
.left-sidebar .sidebar-section-title svg { width: 16px; height: 16px; stroke-width: 1.75; color: var(--color-ink-muted); }
|
||||||
|
.left-sidebar :is(.branch-head-actions, .stash-head-actions, .explorer-head-actions) { gap: 3px; }
|
||||||
|
.left-sidebar :is(.branch-create-toggle, .stash-toggle, .explorer-bulk-button) {
|
||||||
|
width: 24px; min-width: 24px; height: 24px; min-height: 24px; flex-basis: 24px;
|
||||||
|
border-color: transparent; background: transparent; border-radius: 3px;
|
||||||
|
}
|
||||||
|
.left-sidebar :is(.branch-create-toggle, .stash-toggle, .explorer-bulk-button):hover:not(:disabled) {
|
||||||
|
background: var(--color-surface-hover); color: var(--color-ink);
|
||||||
|
}
|
||||||
|
.left-sidebar .left-panel-resize-handle { min-height: 6px; }
|
||||||
|
.left-sidebar .branch-list, .left-sidebar .explorer-list { padding: 4px 0; }
|
||||||
|
.left-sidebar .branch-group-toggle {
|
||||||
|
min-height: 30px; padding: 5px 10px; border-radius: 0;
|
||||||
|
font-size: 12px; font-weight: 500; letter-spacing: normal; text-transform: none;
|
||||||
|
}
|
||||||
|
.left-sidebar .branch-row {
|
||||||
|
min-height: 34px; padding: 5px 10px; padding-left: calc(14px + var(--branch-indent, 0px));
|
||||||
|
border-radius: 0; margin-top: 0;
|
||||||
|
}
|
||||||
|
.left-sidebar .branch-row.current { border-color: transparent; box-shadow: inset 3px 0 0 var(--color-primary); }
|
||||||
|
.left-sidebar .branch-info strong { font-size: 12px; font-weight: 550; }
|
||||||
|
.left-sidebar .branch-row.current strong { font-weight: 700; }
|
||||||
|
.left-sidebar .branch-folder-row { min-height: 32px; border-radius: 0; margin-top: 0; }
|
||||||
|
.left-sidebar .stash-panel { display: flex; flex-direction: column; min-height: 0; }
|
||||||
|
.left-sidebar .stash-panel .section-head, .left-sidebar .stash-create { flex-shrink: 0; }
|
||||||
|
.left-sidebar .stash-list { flex: 1; min-height: 0; overflow: auto; }
|
||||||
|
.left-sidebar .stash-empty { flex: 1; padding: 16px 10px; }
|
||||||
|
.left-sidebar .stash-create { grid-template-columns: minmax(0, 1fr) auto; }
|
||||||
|
.left-sidebar .stash-input { grid-column: 1 / -1; }
|
||||||
|
.left-sidebar .explorer-tool-action, .left-sidebar .explorer-action-divider { display: none; }
|
||||||
|
/* File actions remain available through the file context menu. */
|
||||||
|
.left-sidebar .worktree-panel { display: grid; grid-template-rows: 42px minmax(0, 1fr); min-height: 0; overflow: hidden; }
|
||||||
|
.left-sidebar .worktree-panel.collapsed { grid-template-rows: 42px; }
|
||||||
|
.sidebar-worktree-list { min-height: 0; overflow: auto; padding: 4px 0; }
|
||||||
|
.sidebar-worktree-row {
|
||||||
|
display: flex; align-items: center; gap: 8px; width: 100%; min-height: 46px;
|
||||||
|
padding: 6px 10px; border: 0; border-radius: 0; background: transparent;
|
||||||
|
color: var(--color-ink-muted); text-align: left;
|
||||||
|
}
|
||||||
|
.sidebar-worktree-row.current {
|
||||||
|
background: color-mix(in srgb, var(--color-primary) 11%, var(--color-surface-solid));
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-primary);
|
||||||
|
}
|
||||||
|
.sidebar-worktree-row:hover:not(:disabled) { background: var(--color-surface-hover); }
|
||||||
|
.sidebar-worktree-row > svg { flex-shrink: 0; }
|
||||||
|
.sidebar-worktree-info { display: grid; gap: 2px; flex: 1; min-width: 0; }
|
||||||
|
.sidebar-worktree-info strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--color-ink); }
|
||||||
|
.sidebar-worktree-info > span { display: flex; align-items: center; gap: 4px; overflow: hidden; white-space: nowrap; font-size: 10px; color: var(--color-ink-dim); }
|
||||||
|
.sidebar-worktree-info > span svg { flex-shrink: 0; }
|
||||||
|
.sidebar-worktree-error { display: grid; gap: 8px; padding: 10px; font-size: 11px; overflow-wrap: anywhere; color: var(--color-ink-muted); }
|
||||||
|
.sidebar-worktree-error button { justify-self: start; }
|
||||||
|
|
||||||
|
/* All collapsed sections share exactly the same header and icon geometry. */
|
||||||
|
.left-sidebar > .panel { border: 0; }
|
||||||
|
.left-sidebar > .panel + .panel { border-top: 0; }
|
||||||
|
.left-sidebar .section-head { height: 42px; min-height: 42px; box-sizing: border-box; }
|
||||||
|
.left-sidebar > .panel.collapsed { height: 42px; min-height: 42px; }
|
||||||
|
.left-sidebar .section-head button svg { width: 14px; height: 14px; stroke-width: 1.75; }
|
||||||
|
.left-sidebar .section-head .pill-count { min-width: 24px; justify-content: center; }
|
||||||
|
|
||||||
|
.left-sidebar .tags-panel { display: grid; grid-template-rows: 42px minmax(0, 1fr); min-height: 0; overflow: hidden; }
|
||||||
|
.left-sidebar .tags-panel.collapsed { grid-template-rows: 42px; }
|
||||||
|
.sidebar-tags-list { min-height: 0; overflow: auto; padding: 4px 0; }
|
||||||
|
.left-sidebar .tags-panel .tag-create-form { grid-template-columns: auto minmax(0, 1fr) auto auto; margin: 4px 8px; }
|
||||||
|
.left-sidebar .tags-panel .tag-create-form input[aria-label="Tag message"] { grid-column: 2 / -1; grid-row: 2; }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check, ChevronDown, ChevronRight, Folder, FolderOpen, GitBranch, GitCompare, GitMerge, HardDrive, Pencil, Plus, Tag as TagIcon, Trash2, Upload, X } from "@lucide/svelte";
|
import { Check, ChevronDown, ChevronRight, Folder, FolderOpen, GitBranch, GitCompare, GitMerge, HardDrive, Pencil, Plus, Trash2, X } from "@lucide/svelte";
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import type { GitBranch as GitBranchInfo, GitTag } from "../types";
|
import type { GitBranch as GitBranchInfo } from "../types";
|
||||||
|
|
||||||
type BranchTreeNode = BranchFolderNode | BranchLeafNode;
|
type BranchTreeNode = BranchFolderNode | BranchLeafNode;
|
||||||
|
|
||||||
@@ -48,7 +48,6 @@
|
|||||||
branches: GitBranchInfo[];
|
branches: GitBranchInfo[];
|
||||||
localBranches: GitBranchInfo[];
|
localBranches: GitBranchInfo[];
|
||||||
remoteBranches: GitBranchInfo[];
|
remoteBranches: GitBranchInfo[];
|
||||||
tags: GitTag[];
|
|
||||||
hasRepository: boolean;
|
hasRepository: boolean;
|
||||||
isBusy: boolean;
|
isBusy: boolean;
|
||||||
onCheckout: (branch: GitBranchInfo) => void;
|
onCheckout: (branch: GitBranchInfo) => void;
|
||||||
@@ -60,10 +59,6 @@
|
|||||||
onDeleteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
onDeleteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||||
onDeleteRemoteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
onDeleteRemoteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||||
onDeleteBranchFolder: (folderName: string, branches: GitBranchInfo[], depth: number) => void | Promise<void>;
|
onDeleteBranchFolder: (folderName: string, branches: GitBranchInfo[], depth: number) => void | Promise<void>;
|
||||||
onCreateTag: (name: string, message: string) => void | Promise<void>;
|
|
||||||
onDeleteTag: (tag: GitTag) => void | Promise<void>;
|
|
||||||
onPushTag: (tag: GitTag) => void | Promise<void>;
|
|
||||||
onManageWorktrees: () => void;
|
|
||||||
onCreateWorktree: (branch: GitBranchInfo) => void | Promise<void>;
|
onCreateWorktree: (branch: GitBranchInfo) => void | Promise<void>;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
onToggleCollapsed?: () => void;
|
onToggleCollapsed?: () => void;
|
||||||
@@ -73,7 +68,6 @@
|
|||||||
branches = [],
|
branches = [],
|
||||||
localBranches = [],
|
localBranches = [],
|
||||||
remoteBranches = [],
|
remoteBranches = [],
|
||||||
tags = [],
|
|
||||||
hasRepository = false,
|
hasRepository = false,
|
||||||
isBusy = false,
|
isBusy = false,
|
||||||
onCheckout = () => {},
|
onCheckout = () => {},
|
||||||
@@ -85,10 +79,6 @@
|
|||||||
onDeleteBranch = () => {},
|
onDeleteBranch = () => {},
|
||||||
onDeleteRemoteBranch = () => {},
|
onDeleteRemoteBranch = () => {},
|
||||||
onDeleteBranchFolder = () => {},
|
onDeleteBranchFolder = () => {},
|
||||||
onCreateTag = () => {},
|
|
||||||
onDeleteTag = () => {},
|
|
||||||
onPushTag = () => {},
|
|
||||||
onManageWorktrees = () => {},
|
|
||||||
onCreateWorktree = () => {},
|
onCreateWorktree = () => {},
|
||||||
collapsed = false,
|
collapsed = false,
|
||||||
onToggleCollapsed = () => {},
|
onToggleCollapsed = () => {},
|
||||||
@@ -96,23 +86,14 @@
|
|||||||
|
|
||||||
let localOpen = $state(true);
|
let localOpen = $state(true);
|
||||||
let remoteOpen = $state(false);
|
let remoteOpen = $state(false);
|
||||||
let tagsOpen = $state(false);
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let newBranchName = $state("");
|
let newBranchName = $state("");
|
||||||
let createInput = $state<HTMLInputElement | null>(null);
|
let createInput = $state<HTMLInputElement | null>(null);
|
||||||
let tagCreateOpen = $state(false);
|
|
||||||
let newTagName = $state("");
|
|
||||||
let newTagMessage = $state("");
|
|
||||||
let tagCreateInput = $state<HTMLInputElement | null>(null);
|
|
||||||
let contextBranch = $state<GitBranchInfo | null>(null);
|
let contextBranch = $state<GitBranchInfo | null>(null);
|
||||||
let contextFolder = $state<BranchFolderRow | null>(null);
|
let contextFolder = $state<BranchFolderRow | null>(null);
|
||||||
let branchContextMenuElement = $state<HTMLElement | null>(null);
|
let branchContextMenuElement = $state<HTMLElement | null>(null);
|
||||||
let contextMenuX = $state(0);
|
let contextMenuX = $state(0);
|
||||||
let contextMenuY = $state(0);
|
let contextMenuY = $state(0);
|
||||||
let contextTag = $state<GitTag | null>(null);
|
|
||||||
let tagContextMenuElement = $state<HTMLElement | null>(null);
|
|
||||||
let tagContextMenuX = $state(0);
|
|
||||||
let tagContextMenuY = $state(0);
|
|
||||||
let collapsedBranchFolders = $state<Set<string>>(new Set());
|
let collapsedBranchFolders = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
let localBranchRows = $derived(buildBranchRows("local", localBranches, "local"));
|
let localBranchRows = $derived(buildBranchRows("local", localBranches, "local"));
|
||||||
@@ -227,6 +208,7 @@
|
|||||||
|
|
||||||
function openCreateForm() {
|
function openCreateForm() {
|
||||||
if (!hasRepository || isBusy) return;
|
if (!hasRepository || isBusy) return;
|
||||||
|
if (collapsed) onToggleCollapsed();
|
||||||
createOpen = true;
|
createOpen = true;
|
||||||
queueMicrotask(() => createInput?.focus());
|
queueMicrotask(() => createInput?.focus());
|
||||||
}
|
}
|
||||||
@@ -357,66 +339,8 @@
|
|||||||
await onRebase(branch);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openTagContextMenu(event: MouseEvent, tag: GitTag) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
if (isBusy) return;
|
|
||||||
|
|
||||||
contextTag = tag;
|
|
||||||
tagContextMenuX = event.clientX + 2;
|
|
||||||
tagContextMenuY = event.clientY + 2;
|
|
||||||
await tick();
|
|
||||||
if (contextTag !== tag) return;
|
|
||||||
|
|
||||||
const position = fitContextMenuToViewport(tagContextMenuElement, event.clientX, event.clientY);
|
|
||||||
tagContextMenuX = position.x;
|
|
||||||
tagContextMenuY = position.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
function closeAllContextMenus() {
|
||||||
closeBranchContextMenu();
|
closeBranchContextMenu();
|
||||||
closeTagContextMenu();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowKeydown(event: KeyboardEvent) {
|
function handleWindowKeydown(event: KeyboardEvent) {
|
||||||
@@ -428,10 +352,7 @@
|
|||||||
|
|
||||||
<section class="panel branch-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="Branches">
|
<section class="panel branch-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="Branches">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<h2 class="sidebar-section-title"><GitBranch size={16} aria-hidden="true" />Branches</h2>
|
||||||
<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">
|
<div class="branch-head-actions">
|
||||||
<button
|
<button
|
||||||
class="branch-create-toggle"
|
class="branch-create-toggle"
|
||||||
@@ -543,7 +464,6 @@
|
|||||||
<GitBranch size={16} aria-hidden="true" />
|
<GitBranch size={16} aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
<strong>{row.displayName}</strong>
|
<strong>{row.displayName}</strong>
|
||||||
<span>{row.scopeLabel}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if row.branch.current}
|
{#if row.branch.current}
|
||||||
@@ -611,7 +531,6 @@
|
|||||||
<GitBranch size={16} aria-hidden="true" />
|
<GitBranch size={16} aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
<strong>{row.displayName}</strong>
|
<strong>{row.displayName}</strong>
|
||||||
<span>{row.scopeLabel}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if row.branch.current}
|
{#if row.branch.current}
|
||||||
@@ -624,100 +543,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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 class="branch-group">
|
|
||||||
<button
|
|
||||||
class="branch-group-toggle worktree-group-toggle"
|
|
||||||
type="button"
|
|
||||||
onclick={onManageWorktrees}
|
|
||||||
disabled={isBusy}
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
title="Manage repository worktrees"
|
|
||||||
>
|
|
||||||
<HardDrive size={14} aria-hidden="true" />
|
|
||||||
<span>Worktrees</span>
|
|
||||||
<span class="branch-group-count">Manage</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -792,24 +620,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if contextTag}
|
|
||||||
<div
|
|
||||||
bind:this={tagContextMenuElement}
|
|
||||||
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>
|
</section>
|
||||||
|
|||||||
@@ -293,10 +293,7 @@
|
|||||||
|
|
||||||
<section class="panel explorer-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="File explorer">
|
<section class="panel explorer-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="File explorer">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<h2 class="sidebar-section-title"><Folder size={16} aria-hidden="true" />Files</h2>
|
||||||
<span class="eyebrow">Explorer</span>
|
|
||||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Files</h2>
|
|
||||||
</div>
|
|
||||||
<div class="explorer-head-actions">
|
<div class="explorer-head-actions">
|
||||||
<button
|
<button
|
||||||
class="explorer-bulk-button explorer-tool-action"
|
class="explorer-bulk-button explorer-tool-action"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Archive, ChevronDown, ChevronRight, Download, Trash2, Upload } from "@lucide/svelte";
|
import { Archive, ChevronDown, ChevronRight, Download, Plus, Trash2, Upload } from "@lucide/svelte";
|
||||||
import type { GitStash } from "../types";
|
import type { GitStash } from "../types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
onToggleCollapsed = () => {},
|
onToggleCollapsed = () => {},
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
let createOpen = $state(false);
|
||||||
let message = $state("");
|
let message = $state("");
|
||||||
let includeUntracked = $state(true);
|
let includeUntracked = $state(true);
|
||||||
|
|
||||||
@@ -43,11 +44,13 @@
|
|||||||
|
|
||||||
<section class="panel stash-panel overflow-hidden" class:collapsed aria-label="Git stash">
|
<section class="panel stash-panel overflow-hidden" class:collapsed aria-label="Git stash">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<h2 class="sidebar-section-title"><Archive size={16} aria-hidden="true" />Stashes</h2>
|
||||||
<span class="eyebrow">Stash</span>
|
|
||||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Shelved changes</h2>
|
|
||||||
</div>
|
|
||||||
<div class="stash-head-actions">
|
<div class="stash-head-actions">
|
||||||
|
<button class="stash-toggle" type="button" title="Create stash" aria-label="Create stash"
|
||||||
|
disabled={!hasRepository || isBusy || changedCount === 0}
|
||||||
|
onclick={() => { createOpen = !createOpen; if (collapsed) { createOpen = true; onToggleCollapsed(); } }}>
|
||||||
|
<Plus size={14} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
<span class="pill pill-count">{stashes.length}</span>
|
<span class="pill pill-count">{stashes.length}</span>
|
||||||
<button
|
<button
|
||||||
class="stash-toggle panel-collapse-toggle"
|
class="stash-toggle panel-collapse-toggle"
|
||||||
@@ -71,12 +74,14 @@
|
|||||||
{:else if !hasRepository}
|
{:else if !hasRepository}
|
||||||
<div class="blank-state">No repository loaded.</div>
|
<div class="blank-state">No repository loaded.</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
{#if createOpen}
|
||||||
<div class="stash-create">
|
<div class="stash-create">
|
||||||
<input
|
<input
|
||||||
class="stash-input"
|
class="stash-input"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={message}
|
bind:value={message}
|
||||||
placeholder="Optional message"
|
placeholder="Optional message"
|
||||||
|
aria-label="Stash message"
|
||||||
disabled={isBusy || changedCount === 0}
|
disabled={isBusy || changedCount === 0}
|
||||||
onkeydown={(event) => {
|
onkeydown={(event) => {
|
||||||
if (event.key === "Enter" && changedCount > 0 && !isBusy) submitPush();
|
if (event.key === "Enter" && changedCount > 0 && !isBusy) submitPush();
|
||||||
@@ -98,6 +103,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if stashes.length === 0}
|
{#if stashes.length === 0}
|
||||||
<div class="blank-state stash-empty">No stashes saved.</div>
|
<div class="blank-state stash-empty">No stashes saved.</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Check, ChevronDown, ChevronRight, Plus, Tag as TagIcon, Trash2, Upload, X } from "@lucide/svelte";
|
||||||
|
import { tick } from "svelte";
|
||||||
|
import type { GitTag } from "../types";
|
||||||
|
interface Props {
|
||||||
|
tags: GitTag[];
|
||||||
|
hasRepository: boolean;
|
||||||
|
isBusy: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
onToggleCollapsed: () => void;
|
||||||
|
onCreateTag: (name: string, message: string) => void | Promise<void>;
|
||||||
|
onDeleteTag: (tag: GitTag) => void | Promise<void>;
|
||||||
|
onPushTag: (tag: GitTag) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
let { tags, hasRepository, isBusy, collapsed, onToggleCollapsed, onCreateTag, onDeleteTag, onPushTag }: Props = $props();
|
||||||
|
let tagCreateOpen = $state(false);
|
||||||
|
let newTagName = $state("");
|
||||||
|
let newTagMessage = $state("");
|
||||||
|
let tagCreateInput = $state<HTMLInputElement | null>(null);
|
||||||
|
let contextTag = $state<GitTag | null>(null);
|
||||||
|
let tagContextMenuElement = $state<HTMLElement | null>(null);
|
||||||
|
let tagContextMenuX = $state(0);
|
||||||
|
let tagContextMenuY = $state(0);
|
||||||
|
function fitContextMenuToViewport(element: HTMLElement | null, x: number, y: number) {
|
||||||
|
const rect = element?.getBoundingClientRect();
|
||||||
|
const width = rect?.width ?? 184;
|
||||||
|
const height = rect?.height ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.max(8, Math.min(x + 2, window.innerWidth - width - 8)),
|
||||||
|
y: Math.max(8, Math.min(y + 2, window.innerHeight - height - 8)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTagCreateForm() {
|
||||||
|
if (!hasRepository || isBusy) return;
|
||||||
|
if (collapsed) onToggleCollapsed();
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openTagContextMenu(event: MouseEvent, tag: GitTag) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (isBusy) return;
|
||||||
|
|
||||||
|
contextTag = tag;
|
||||||
|
tagContextMenuX = event.clientX + 2;
|
||||||
|
tagContextMenuY = event.clientY + 2;
|
||||||
|
await tick();
|
||||||
|
if (contextTag !== tag) return;
|
||||||
|
|
||||||
|
const position = fitContextMenuToViewport(tagContextMenuElement, event.clientX, event.clientY);
|
||||||
|
tagContextMenuX = position.x;
|
||||||
|
tagContextMenuY = position.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:click={closeTagContextMenu} on:keydown={(event) => { if (event.key === "Escape") closeTagContextMenu(); }} on:contextmenu|capture={closeTagContextMenu} />
|
||||||
|
<section class="panel tags-panel" class:collapsed aria-label="Tags">
|
||||||
|
<div class="section-head">
|
||||||
|
<h2 class="sidebar-section-title"><TagIcon size={16} aria-hidden="true" />Tags</h2>
|
||||||
|
<div class="branch-head-actions">
|
||||||
|
<button class="branch-create-toggle" type="button" onclick={openTagCreateForm} disabled={!hasRepository || isBusy} title="Create new tag" aria-label="Create new tag"><Plus size={14} aria-hidden="true" /></button>
|
||||||
|
<span class="pill pill-count">{tags.length}</span>
|
||||||
|
<button class="branch-create-toggle panel-collapse-toggle" type="button" onclick={onToggleCollapsed}
|
||||||
|
aria-expanded={!collapsed} title={collapsed ? "Expand tags" : "Collapse tags"} aria-label={collapsed ? "Expand tags" : "Collapse tags"}>
|
||||||
|
{#if collapsed}<ChevronRight size={14} aria-hidden="true" />{:else}<ChevronDown size={14} aria-hidden="true" />{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if !collapsed}
|
||||||
|
<div class="sidebar-tags-list">
|
||||||
|
{#if !hasRepository}<p class="branch-empty">Open a repository to list tags.</p>
|
||||||
|
{:else}
|
||||||
|
{#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 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>
|
||||||
|
{/if}
|
||||||
|
{#if contextTag}
|
||||||
|
<div
|
||||||
|
bind:this={tagContextMenuElement}
|
||||||
|
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>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ChevronDown, ChevronRight, GitBranch, HardDrive, Lock, Plus, RefreshCw } from "@lucide/svelte";
|
||||||
|
import type { GitWorktree } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
worktrees: GitWorktree[];
|
||||||
|
hasRepository: boolean;
|
||||||
|
isBusy: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
|
collapsed: boolean;
|
||||||
|
onToggleCollapsed: () => void;
|
||||||
|
onOpen: (worktree: GitWorktree) => void;
|
||||||
|
onManage: () => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}
|
||||||
|
let { worktrees, hasRepository, isBusy, loading, error, collapsed, onToggleCollapsed, onOpen, onManage, onRefresh }: Props = $props();
|
||||||
|
let linkedWorktrees = $derived(worktrees.filter(worktree => !worktree.is_main));
|
||||||
|
const name = (path: string) => path.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || path;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="panel worktree-panel" class:collapsed aria-label="Worktrees" aria-busy={loading}>
|
||||||
|
<div class="section-head">
|
||||||
|
<h2 class="sidebar-section-title"><HardDrive size={16} aria-hidden="true" />Worktrees</h2>
|
||||||
|
<div class="branch-head-actions">
|
||||||
|
<button class="branch-create-toggle" type="button" onclick={onManage} disabled={!hasRepository || isBusy}
|
||||||
|
title="Create or manage worktrees" aria-label="Create or manage worktrees" aria-haspopup="dialog">
|
||||||
|
<Plus size={14} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<span class="pill pill-count">{loading && linkedWorktrees.length === 0 ? "…" : linkedWorktrees.length}</span>
|
||||||
|
<button class="branch-create-toggle panel-collapse-toggle" type="button" onclick={onToggleCollapsed}
|
||||||
|
aria-expanded={!collapsed} title={collapsed ? "Expand worktrees" : "Collapse worktrees"}
|
||||||
|
aria-label={collapsed ? "Expand worktrees" : "Collapse worktrees"}>
|
||||||
|
{#if collapsed}<ChevronRight size={14} aria-hidden="true" />{:else}<ChevronDown size={14} aria-hidden="true" />{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if !collapsed}
|
||||||
|
<div class="sidebar-worktree-list">
|
||||||
|
{#if !hasRepository}
|
||||||
|
<p class="branch-empty">Open a repository to list worktrees.</p>
|
||||||
|
{:else if error}
|
||||||
|
<div class="sidebar-worktree-error" role="status">
|
||||||
|
<span>{error}</span>
|
||||||
|
<button class="btn-sm" type="button" onclick={onRefresh} disabled={loading || isBusy}><RefreshCw size={13} aria-hidden="true" />Retry</button>
|
||||||
|
</div>
|
||||||
|
{:else if loading && linkedWorktrees.length === 0}
|
||||||
|
<p class="branch-empty" role="status">Loading worktrees…</p>
|
||||||
|
{:else if linkedWorktrees.length === 0}
|
||||||
|
<p class="branch-empty">No linked worktrees.</p>
|
||||||
|
{:else}
|
||||||
|
{#each linkedWorktrees as worktree (worktree.path)}
|
||||||
|
<button class="sidebar-worktree-row" class:current={worktree.is_current} type="button"
|
||||||
|
onclick={() => onOpen(worktree)} disabled={isBusy || worktree.missing || worktree.bare}
|
||||||
|
aria-current={worktree.is_current ? "location" : undefined}
|
||||||
|
title={`${worktree.path}${worktree.missing ? " — missing" : worktree.bare ? " — bare repository" : ""}`}>
|
||||||
|
<HardDrive size={15} aria-hidden="true" />
|
||||||
|
<span class="sidebar-worktree-info">
|
||||||
|
<strong>{name(worktree.path)}</strong>
|
||||||
|
<span><GitBranch size={12} aria-hidden="true" />{worktree.branch || (worktree.bare ? "Bare repository" : `Detached · ${worktree.short_head || "HEAD"}`)}</span>
|
||||||
|
</span>
|
||||||
|
{#if worktree.locked}<Lock size={12} aria-label="Locked" />{/if}
|
||||||
|
{#if worktree.missing}<span class="pill">Missing</span>
|
||||||
|
{:else if worktree.is_current}<span class="pill pill-active">Current</span>{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
Reference in New Issue
Block a user