feat(sidebar): add section menu to toggle left-sidebar panels

Add a floating SidebarSectionMenu component and wiring in App.svelte to let users show/hide individual left-sidebar panels (worktrees, tags, stashes, files). Visibility is tracked in new sidebarVisibility state and persisted to localStorage under SIDEBAR_VISIBILITY_KEY. The menu opens via contextmenu on the left sidebar and returns focus to the invoking element when closed.

- Panels and resize handles now respect visibility (buildLeftSidebarRows, sidebarHandleVisible, expandedSidebarPanels).
- Branch panel remains always visible and cannot be toggled; stored preferences only apply to the other panels.
- Safe fallbacks: localStorage errors are ignored so the feature still works without persistence; menu includes keyboard navigation and appropriate ARIA roles.
This commit is contained in:
2026-09-18 20:05:19 +02:00
parent 985e812209
commit e6797bdb81
2 changed files with 150 additions and 9 deletions
+71 -9
View File
@@ -18,6 +18,7 @@
import IssueCenter from "./lib/components/IssueCenter.svelte";
import { readWorkspaces, WORKSPACES_KEY, type Workspace, type WorkspaceState } from "./lib/workspaces";
import RepositoryDashboard from "./lib/components/RepositoryDashboard.svelte";
import SidebarSectionMenu from "./lib/components/SidebarSectionMenu.svelte";
import ReviewCenter from "./lib/components/ReviewCenter.svelte";
import RepoTabs from "./lib/RepoTabs.svelte";
import AiReviewDialog from "./lib/components/AiReviewDialog.svelte";
@@ -296,6 +297,7 @@
const AUTO_REFRESH_ENABLED_KEY = "gitlite.autoRefreshEnabled.v1";
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
const SIDEBAR_VISIBILITY_KEY = "gitlite.sidebarVisibility.v1";
const SIDEBAR_PANEL_HEIGHTS_KEY = "gitlite.sidebarPanelHeights.v2";
const BRANCH_PANEL_COLLAPSED_KEY = "gitlite.branchPanelCollapsed.v1";
const STASH_PANEL_COLLAPSED_KEY = "gitlite.stashPanelCollapsed.v2";
@@ -564,6 +566,9 @@
let resizingLeftSidebar = false;
let leftSidebarResizeStartX = 0;
let leftSidebarResizeStartWidth = 0;
let sidebarVisibility = loadSidebarVisibility();
let sidebarSectionMenu: { x: number; y: number } | null = null;
let sidebarMenuReturnFocus: HTMLElement | null = null;
let sidebarPanelHeights: Record<SidebarPanelId, number> = loadSidebarPanelHeights();
let resizingSidebarPanel: SidebarPanelId | null = null;
let sidebarResizeStartY = 0;
@@ -585,6 +590,7 @@
$: isBusy = operation.length > 0;
$: hasRepository = activeRepoPath.length > 0 && status !== null;
$: if (activeView !== "repository") sidebarSectionMenu = null;
$: workspaceActive = activeView === "repository" && hasRepository;
$: openingRepo = operation === "Opening repository";
$: cloningRepo = operation === "Cloning repository";
@@ -625,8 +631,8 @@
) as Record<string, string>;
$: remoteBranches = branches.filter((b) => b.remote);
$: currentBranchIsLocalOnly = Boolean(status?.current_branch) && !status?.upstream;
$: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, sidebarPanelHeights);
$: allLeftPanelsCollapsed = branchPanelCollapsed && worktreePanelCollapsed && tagsPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
$: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, sidebarPanelHeights, sidebarVisibility);
$: allLeftPanelsCollapsed = branchPanelCollapsed && (!sidebarVisibility.worktree || worktreePanelCollapsed) && (!sidebarVisibility.tags || tagsPanelCollapsed) && (!sidebarVisibility.stash || stashPanelCollapsed) && (!sidebarVisibility.explorer || explorerPanelCollapsed);
$: editorToolName = externalToolDisplayName("editor", externalToolsSettings.editor, detectedExternalTools);
$: diffToolName = externalToolDisplayName("diff", externalToolsSettings.diff, detectedExternalTools);
$: mergeToolName = externalToolDisplayName("merge", externalToolsSettings.merge, detectedExternalTools);
@@ -2062,6 +2068,38 @@
// ── Sidebar panel sizing ───────────────────────────────────────────────────
function loadSidebarVisibility(): Record<SidebarPanelId, boolean> {
const visible = { branch: true, worktree: true, tags: true, stash: true, explorer: true };
try {
const stored = JSON.parse(localStorage.getItem(SIDEBAR_VISIBILITY_KEY) ?? "{}");
for (const panel of SIDEBAR_PANEL_ORDER) {
if (panel !== "branch" && typeof stored?.[panel] === "boolean") visible[panel] = stored[panel];
}
} catch { /* Keep all sections visible if stored preferences are unavailable. */ }
return visible;
}
function toggleSidebarVisibility(id: string) {
if (id === "branch" || !SIDEBAR_PANEL_ORDER.includes(id as SidebarPanelId)) return;
const panel = id as SidebarPanelId;
sidebarVisibility = { ...sidebarVisibility, [panel]: !sidebarVisibility[panel] };
try { localStorage.setItem(SIDEBAR_VISIBILITY_KEY, JSON.stringify(sidebarVisibility)); }
catch { /* Visibility changes still work without persistent storage. */ }
}
function closeSidebarSectionMenu() {
sidebarSectionMenu = null;
if (sidebarMenuReturnFocus?.isConnected) sidebarMenuReturnFocus.focus({ preventScroll: true });
}
function openSidebarSectionMenu(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
closeRepoTabContextMenu();
sidebarMenuReturnFocus = event.currentTarget as HTMLElement;
sidebarSectionMenu = { x: event.clientX, y: event.clientY };
}
function clampSidebarPanelHeight(panel: SidebarPanelId, value: number): number {
return Math.min(SIDEBAR_PANEL_MAX_HEIGHT, Math.max(SIDEBAR_PANEL_MIN_HEIGHT[panel], Math.round(value)));
}
@@ -2100,7 +2138,7 @@
/** Expanded panels, top to bottom. The last one always fills the leftover space. */
function expandedSidebarPanels(): SidebarPanelId[] {
return SIDEBAR_PANEL_ORDER.filter((panel) => !sidebarPanelIsCollapsed(panel));
return SIDEBAR_PANEL_ORDER.filter((panel) => sidebarVisibility[panel] && !sidebarPanelIsCollapsed(panel));
}
/** The first expanded panel below `panel` — the one that gives way while dragging. */
@@ -2111,9 +2149,9 @@
}
/** A handle only makes sense between two expanded panels. */
function sidebarHandleVisible(panel: SidebarPanelId, ...markers: boolean[]): boolean {
function sidebarHandleVisible(panel: SidebarPanelId, ...markers: unknown[]): boolean {
void markers;
return !sidebarPanelIsCollapsed(panel) && nextExpandedSidebarPanel(panel) !== null;
return sidebarVisibility[panel] && !sidebarPanelIsCollapsed(panel) && nextExpandedSidebarPanel(panel) !== null;
}
function buildLeftSidebarRows(
@@ -2123,6 +2161,7 @@
stashCollapsed: boolean,
explorerCollapsed: boolean,
heights: Record<SidebarPanelId, number>,
visibility: Record<SidebarPanelId, boolean>,
): string {
void branchCollapsed; void worktreeCollapsed; void tagsCollapsed; void stashCollapsed; void explorerCollapsed;
@@ -2131,6 +2170,7 @@
const rows: string[] = [];
for (const panel of SIDEBAR_PANEL_ORDER) {
if (!visibility[panel]) continue;
if (sidebarPanelIsCollapsed(panel)) rows.push("auto");
else if (panel === flexible) rows.push(`minmax(${SIDEBAR_PANEL_MIN_HEIGHT[panel]}px, 1fr)`);
else rows.push(`${heights[panel]}px`);
@@ -5902,6 +5942,17 @@
<svelte:window on:click={handleWindowClick} on:keydown={handleWindowKeydown} on:contextmenu|capture={handleWindowContextMenu} />
{#if sidebarSectionMenu && activeView === "repository"}
<SidebarSectionMenu x={sidebarSectionMenu.x} y={sidebarSectionMenu.y} language={appLanguage}
sections={[
{ id: "worktree", label: "Worktrees", visible: sidebarVisibility.worktree },
{ id: "tags", label: "Tags", visible: sidebarVisibility.tags },
{ id: "stash", label: "Stashes", visible: sidebarVisibility.stash },
{ id: "explorer", label: appLanguage === "de" ? "Dateien" : "Files", visible: sidebarVisibility.explorer },
]}
onToggle={toggleSidebarVisibility} onClose={closeSidebarSectionMenu} />
{/if}
<main class="shell">
<TitleBar
onOpenSettings={openAppSettings}
@@ -6140,8 +6191,11 @@
>
<!-- Compact repository navigation: branches, worktrees, tags, stashes, files -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<aside
class="left-sidebar"
tabindex="0"
oncontextmenu={openSidebarSectionMenu}
class:branch-collapsed={branchPanelCollapsed}
class:stash-collapsed={stashPanelCollapsed}
class:explorer-collapsed={explorerPanelCollapsed}
@@ -6169,7 +6223,7 @@
collapsed={branchPanelCollapsed}
onToggleCollapsed={toggleBranchPanelCollapsed}
/>
{#if sidebarHandleVisible("branch", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed)}
{#if sidebarHandleVisible("branch", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, sidebarVisibility)}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
@@ -6193,6 +6247,7 @@
{:else}
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
{/if}
{#if sidebarVisibility.worktree}
<WorktreePanel
{worktrees}
{hasRepository}
@@ -6208,7 +6263,7 @@
onManage={() => { void openWorktreeDialog(); }}
onRefresh={() => { void refreshWorktrees(); }}
/>
{#if sidebarHandleVisible("worktree", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed)}
{#if sidebarHandleVisible("worktree", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, sidebarVisibility)}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
@@ -6232,6 +6287,8 @@
{:else}
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
{/if}
{/if}
{#if sidebarVisibility.tags}
<TagsPanel
{tags} {hasRepository} {isBusy}
collapsed={tagsPanelCollapsed}
@@ -6243,7 +6300,7 @@
onDeleteTag={deleteLocalTag}
onPushTag={pushLocalTag}
/>
{#if sidebarHandleVisible("tags", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed)}
{#if sidebarHandleVisible("tags", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, sidebarVisibility)}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
@@ -6267,6 +6324,8 @@
{:else}
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
{/if}
{/if}
{#if sidebarVisibility.stash}
<StashPanel
{stashes}
changedCount={changedFiles.length}
@@ -6279,7 +6338,7 @@
collapsed={stashPanelCollapsed}
onToggleCollapsed={toggleStashPanelCollapsed}
/>
{#if sidebarHandleVisible("stash", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed)}
{#if sidebarHandleVisible("stash", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, sidebarVisibility)}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
@@ -6303,6 +6362,8 @@
{:else}
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
{/if}
{/if}
{#if sidebarVisibility.explorer}
<ExplorerPanel
{repoFiles}
{expandedExplorerPaths}
@@ -6327,6 +6388,7 @@
collapsed={explorerPanelCollapsed}
onToggleCollapsed={toggleExplorerPanelCollapsed}
/>
{/if}
</aside>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->