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:
+71
-9
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user