From 8c99e15dc921e49ba17e42c7ae1772c6ffec0629 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Thu, 17 Sep 2026 22:45:20 +0200 Subject: [PATCH] refactor(sidebar): unify left sidebar panel sizing and resize logic Replace per-panel height constants, state, loaders and resize handlers with a single model for all left sidebar panels. Panel sizes are now stored/read as a JSON object under "gitlite.sidebarPanelHeights.v2" and managed via shared helpers. Key changes and behavior: - Introduce SIDEBAR_PANEL_ORDER, per-panel MIN/DEFAULT heights and a shared MAX/STEP. - Single in-memory map sidebarPanelHeights with clamp/load/persist helpers. - One pointer/keyboard resize flow: startSidebarPanelResize, onSidebarPanelResizeMove, endSidebarPanelResize and onSidebarPanelResizeKeydown. Dragging moves the border between the panel above (grows) and the next expanded panel below (shrinks). The last expanded panel is flexible (1fr) and follows automatically. - Double-click on a handle resets the above/below panels to their defaults. - buildLeftSidebarRows now derives grid rows from the unified state; template uses new handlers and aria attributes. - LocalStorage remains best-effort; persistence failures are ignored as before. Removes many duplicate functions/variables for individual panels and simplifies the UI logic for showing/hiding resize handles and resizing behavior. --- src/App.svelte | 511 +++++++++++++++++++++++-------------------------- src/app.css | 89 +-------- 2 files changed, 249 insertions(+), 351 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index d4c42ab..a20a3fd 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -296,10 +296,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 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 SIDEBAR_PANEL_HEIGHTS_KEY = "gitlite.sidebarPanelHeights.v2"; const BRANCH_PANEL_COLLAPSED_KEY = "gitlite.branchPanelCollapsed.v1"; const STASH_PANEL_COLLAPSED_KEY = "gitlite.stashPanelCollapsed.v2"; const EXPLORER_PANEL_COLLAPSED_KEY = "gitlite.explorerPanelCollapsed.v1"; @@ -310,13 +307,26 @@ const LEFT_SIDEBAR_DEFAULT_WIDTH = 280; const LEFT_SIDEBAR_MIN_WIDTH = 220; const LEFT_SIDEBAR_MAX_WIDTH = 420; - const LEFT_BRANCH_PANEL_DEFAULT_HEIGHT = 260; - const LEFT_BRANCH_PANEL_MIN_HEIGHT = 180; - const LEFT_BRANCH_PANEL_MAX_HEIGHT = 560; - const LEFT_STASH_PANEL_DEFAULT_HEIGHT = 190; - const LEFT_STASH_PANEL_MIN_HEIGHT = 150; - const LEFT_STASH_PANEL_MAX_HEIGHT = 420; - const LEFT_EXPLORER_PANEL_MIN_HEIGHT = 220; + // Sidebar panels, top to bottom. A drag handle moves the border between two + // neighbours: the panel above grows, the next expanded panel below gives way. + const SIDEBAR_PANEL_ORDER = ["branch", "worktree", "tags", "stash", "explorer"] as const; + type SidebarPanelId = (typeof SIDEBAR_PANEL_ORDER)[number]; + const SIDEBAR_PANEL_MIN_HEIGHT: Record = { + branch: 120, + worktree: 96, + tags: 96, + stash: 110, + explorer: 140, + }; + const SIDEBAR_PANEL_DEFAULT_HEIGHT: Record = { + branch: 260, + worktree: 150, + tags: 150, + stash: 190, + explorer: 260, + }; + const SIDEBAR_PANEL_MAX_HEIGHT = 900; + const SIDEBAR_RESIZE_STEP = 24; const HISTORY_ASIDE_DEFAULT_WIDTH = 620; const HISTORY_ASIDE_MIN_WIDTH = 420; const HISTORY_ASIDE_MAX_WIDTH = 920; @@ -553,22 +563,12 @@ let resizingLeftSidebar = false; let leftSidebarResizeStartX = 0; let leftSidebarResizeStartWidth = 0; - let leftBranchPanelHeight = loadLeftBranchPanelHeight(); - let resizingLeftBranchPanel = false; - let leftBranchResizeStartY = 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 resizingLeftStashPanel = false; - let leftStashResizeStartY = 0; - let leftStashResizeStartHeight = 0; + let sidebarPanelHeights: Record = loadSidebarPanelHeights(); + let resizingSidebarPanel: SidebarPanelId | null = null; + let sidebarResizeStartY = 0; + let sidebarResizeAboveStart = 0; + let sidebarResizeBelowStart = 0; + let sidebarResizeBelow: SidebarPanelId | null = null; let tagsPanelCollapsed = loadStoredBoolean("gitlite.tagsPanelCollapsed.v1", true); let branchPanelCollapsed = loadStoredBoolean(BRANCH_PANEL_COLLAPSED_KEY, false); let stashPanelCollapsed = loadStoredBoolean(STASH_PANEL_COLLAPSED_KEY, true); @@ -624,7 +624,7 @@ ) as Record; $: remoteBranches = branches.filter((b) => b.remote); $: currentBranchIsLocalOnly = Boolean(status?.current_branch) && !status?.upstream; - $: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed); + $: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed, sidebarPanelHeights); $: allLeftPanelsCollapsed = branchPanelCollapsed && worktreePanelCollapsed && tagsPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed; $: editorToolName = externalToolDisplayName("editor", externalToolsSettings.editor, detectedExternalTools); $: diffToolName = externalToolDisplayName("diff", externalToolsSettings.diff, detectedExternalTools); @@ -1976,50 +1976,6 @@ } } - function clampLeftBranchPanelHeight(value: number): number { - return Math.min(LEFT_BRANCH_PANEL_MAX_HEIGHT, Math.max(LEFT_BRANCH_PANEL_MIN_HEIGHT, Math.round(value))); - } - - function loadLeftBranchPanelHeight(): number { - try { - const stored = Number(localStorage.getItem(LEFT_BRANCH_PANEL_HEIGHT_KEY)); - if (Number.isFinite(stored) && stored > 0) return clampLeftBranchPanelHeight(stored); - } catch { - // Fall through to the default below. - } - return LEFT_BRANCH_PANEL_DEFAULT_HEIGHT; - } - - function persistLeftBranchPanelHeight(value: number) { - try { - localStorage.setItem(LEFT_BRANCH_PANEL_HEIGHT_KEY, String(value)); - } catch { - // Local storage is best-effort only; resizing must keep working without it. - } - } - - function clampLeftStashPanelHeight(value: number): number { - return Math.min(LEFT_STASH_PANEL_MAX_HEIGHT, Math.max(LEFT_STASH_PANEL_MIN_HEIGHT, Math.round(value))); - } - - function loadLeftStashPanelHeight(): number { - try { - const stored = Number(localStorage.getItem(LEFT_STASH_PANEL_HEIGHT_KEY)); - if (Number.isFinite(stored) && stored > 0) return clampLeftStashPanelHeight(stored); - } catch { - // Fall through to the default below. - } - return LEFT_STASH_PANEL_DEFAULT_HEIGHT; - } - - function persistLeftStashPanelHeight(value: number) { - try { - localStorage.setItem(LEFT_STASH_PANEL_HEIGHT_KEY, String(value)); - } catch { - // Local storage is best-effort only; resizing must keep working without it. - } - } - function clampHistoryAsideWidth(value: number): number { return Math.min(HISTORY_ASIDE_MAX_WIDTH, Math.max(HISTORY_ASIDE_MIN_WIDTH, Math.round(value))); } @@ -2098,176 +2054,172 @@ persistLeftSidebarWidth(leftSidebarWidth); } - function buildLeftSidebarRows(branchCollapsed: boolean, stashCollapsed: boolean, explorerCollapsed: boolean, worktreeCollapsed: boolean, tagsCollapsed: boolean): string { - const branchRow = branchCollapsed - ? "auto" - : `minmax(${LEFT_BRANCH_PANEL_MIN_HEIGHT}px, var(--branch-panel-height, ${LEFT_BRANCH_PANEL_DEFAULT_HEIGHT}px))`; - const branchHandleRow = branchCollapsed ? "0" : "6px"; - const stashRow = stashCollapsed - ? "auto" - : `minmax(${LEFT_STASH_PANEL_MIN_HEIGHT}px, var(--stash-panel-height, ${LEFT_STASH_PANEL_DEFAULT_HEIGHT}px))`; - const stashHandleRow = explorerCollapsed || stashCollapsed ? "0" : "6px"; - const explorerRow = explorerCollapsed ? "auto" : `minmax(${LEFT_EXPLORER_PANEL_MIN_HEIGHT}px, 1fr)`; + // ── Sidebar panel sizing ─────────────────────────────────────────────────── - 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 clampSidebarPanelHeight(panel: SidebarPanelId, value: number): number { + return Math.min(SIDEBAR_PANEL_MAX_HEIGHT, Math.max(SIDEBAR_PANEL_MIN_HEIGHT[panel], Math.round(value))); } - function clampLeftWorktreePanelHeight(value: number) { - return Math.min(420, Math.max(80, Math.round(value))); - } - - function loadLeftWorktreePanelHeight() { + function loadSidebarPanelHeights(): Record { + const heights = { ...SIDEBAR_PANEL_DEFAULT_HEIGHT }; 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; + const stored: unknown = JSON.parse(localStorage.getItem(SIDEBAR_PANEL_HEIGHTS_KEY) ?? "{}"); + if (stored && typeof stored === "object") { + for (const panel of SIDEBAR_PANEL_ORDER) { + const value = Number((stored as Record)[panel]); + if (Number.isFinite(value) && value > 0) heights[panel] = clampSidebarPanelHeight(panel, value); + } + } + } catch { + // Stored sizes are a convenience; the defaults above always work. + } + return heights; } - 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() { + function persistSidebarPanelHeights() { 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) { - if (branchPanelCollapsed) return; - event.preventDefault(); - resizingLeftBranchPanel = true; - leftBranchResizeStartY = event.clientY; - leftBranchResizeStartHeight = leftBranchPanelHeight; - (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); - } - - function onLeftBranchPanelResizeMove(event: PointerEvent) { - if (!resizingLeftBranchPanel) return; - leftBranchPanelHeight = clampLeftBranchPanelHeight(leftBranchResizeStartHeight + (event.clientY - leftBranchResizeStartY)); - } - - function endLeftBranchPanelResize(event: PointerEvent) { - if (!resizingLeftBranchPanel) return; - resizingLeftBranchPanel = false; - persistLeftBranchPanelHeight(leftBranchPanelHeight); - const target = event.currentTarget as HTMLElement; - if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId); - } - - function onLeftBranchPanelResizeKeydown(event: KeyboardEvent) { - if (branchPanelCollapsed || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return; - event.preventDefault(); - leftBranchPanelHeight = clampLeftBranchPanelHeight(leftBranchPanelHeight + (event.key === "ArrowDown" ? 20 : -20)); - persistLeftBranchPanelHeight(leftBranchPanelHeight); - } - - function startLeftStashPanelResize(event: PointerEvent) { - if (stashPanelCollapsed && branchPanelCollapsed) return; - event.preventDefault(); - resizingLeftStashPanel = true; - leftStashResizeStartY = event.clientY; - leftStashResizeStartHeight = stashPanelCollapsed ? leftBranchPanelHeight : leftStashPanelHeight; - (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); - } - - function onLeftStashPanelResizeMove(event: PointerEvent) { - if (!resizingLeftStashPanel) return; - if (stashPanelCollapsed) { - leftBranchPanelHeight = clampLeftBranchPanelHeight(leftStashResizeStartHeight + (event.clientY - leftStashResizeStartY)); - } else { - leftStashPanelHeight = clampLeftStashPanelHeight(leftStashResizeStartHeight + (event.clientY - leftStashResizeStartY)); + localStorage.setItem(SIDEBAR_PANEL_HEIGHTS_KEY, JSON.stringify(sidebarPanelHeights)); + } catch { + // Local storage is best-effort only; resizing must keep working without it. } } - function endLeftStashPanelResize(event: PointerEvent) { - if (!resizingLeftStashPanel) return; - resizingLeftStashPanel = false; - if (stashPanelCollapsed) persistLeftBranchPanelHeight(leftBranchPanelHeight); - else persistLeftStashPanelHeight(leftStashPanelHeight); + function sidebarPanelIsCollapsed(panel: SidebarPanelId): boolean { + if (panel === "branch") return branchPanelCollapsed; + if (panel === "worktree") return worktreePanelCollapsed; + if (panel === "tags") return tagsPanelCollapsed; + if (panel === "stash") return stashPanelCollapsed; + return explorerPanelCollapsed; + } + + /** Expanded panels, top to bottom. The last one always fills the leftover space. */ + function expandedSidebarPanels(): SidebarPanelId[] { + return SIDEBAR_PANEL_ORDER.filter((panel) => !sidebarPanelIsCollapsed(panel)); + } + + /** The first expanded panel below `panel` — the one that gives way while dragging. */ + function nextExpandedSidebarPanel(panel: SidebarPanelId): SidebarPanelId | null { + const expanded = expandedSidebarPanels(); + const index = expanded.indexOf(panel); + return index >= 0 && index < expanded.length - 1 ? expanded[index + 1] : null; + } + + /** A handle only makes sense between two expanded panels. */ + function sidebarHandleVisible(panel: SidebarPanelId, ...markers: boolean[]): boolean { + void markers; + return !sidebarPanelIsCollapsed(panel) && nextExpandedSidebarPanel(panel) !== null; + } + + function buildLeftSidebarRows( + branchCollapsed: boolean, + worktreeCollapsed: boolean, + tagsCollapsed: boolean, + stashCollapsed: boolean, + explorerCollapsed: boolean, + heights: Record, + ): string { + void branchCollapsed; void worktreeCollapsed; void tagsCollapsed; void stashCollapsed; void explorerCollapsed; + + const expanded = expandedSidebarPanels(); + const flexible = expanded[expanded.length - 1]; + const rows: string[] = []; + + for (const panel of SIDEBAR_PANEL_ORDER) { + 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`); + + if (panel !== "explorer") rows.push(sidebarHandleVisible(panel) ? "8px" : "0"); + } + + return rows.join(" "); + } + + function startSidebarPanelResize(event: PointerEvent, panel: SidebarPanelId) { + const below = nextExpandedSidebarPanel(panel); + if (!below) return; + event.preventDefault(); + resizingSidebarPanel = panel; + sidebarResizeBelow = below; + sidebarResizeStartY = event.clientY; + sidebarResizeAboveStart = sidebarPanelHeights[panel]; + sidebarResizeBelowStart = sidebarPanelHeights[below]; + (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); + } + + function onSidebarPanelResizeMove(event: PointerEvent) { + const panel = resizingSidebarPanel; + if (!panel) return; + applySidebarPanelResize(panel, event.clientY - sidebarResizeStartY); + } + + /** + * Move the border by `delta`: the panel above grows, the neighbour below + * shrinks by the same amount. The last expanded panel is sized with 1fr, so + * it follows on its own and only the panel above needs a new height. + */ + function applySidebarPanelResize(panel: SidebarPanelId, delta: number) { + const below = sidebarResizeBelow; + const expanded = expandedSidebarPanels(); + const belowIsFlexible = below !== null && below === expanded[expanded.length - 1]; + + if (below === null) return; + + if (belowIsFlexible) { + sidebarPanelHeights = { + ...sidebarPanelHeights, + [panel]: clampSidebarPanelHeight(panel, sidebarResizeAboveStart + delta), + }; + return; + } + + const maxGrow = sidebarResizeBelowStart - SIDEBAR_PANEL_MIN_HEIGHT[below]; + const maxShrink = sidebarResizeAboveStart - SIDEBAR_PANEL_MIN_HEIGHT[panel]; + const applied = Math.max(-maxShrink, Math.min(maxGrow, Math.round(delta))); + + sidebarPanelHeights = { + ...sidebarPanelHeights, + [panel]: clampSidebarPanelHeight(panel, sidebarResizeAboveStart + applied), + [below]: clampSidebarPanelHeight(below, sidebarResizeBelowStart - applied), + }; + } + + function endSidebarPanelResize(event: PointerEvent) { + if (!resizingSidebarPanel) return; + resizingSidebarPanel = null; + sidebarResizeBelow = null; + persistSidebarPanelHeights(); const target = event.currentTarget as HTMLElement; if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId); } - function onLeftStashPanelResizeKeydown(event: KeyboardEvent) { - if ((stashPanelCollapsed && branchPanelCollapsed) || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return; - event.preventDefault(); - if (stashPanelCollapsed) { - leftBranchPanelHeight = clampLeftBranchPanelHeight(leftBranchPanelHeight + (event.key === "ArrowDown" ? 20 : -20)); - persistLeftBranchPanelHeight(leftBranchPanelHeight); - } else { - leftStashPanelHeight = clampLeftStashPanelHeight(leftStashPanelHeight + (event.key === "ArrowDown" ? 20 : -20)); - persistLeftStashPanelHeight(leftStashPanelHeight); + function onSidebarPanelResizeKeydown(event: KeyboardEvent, panel: SidebarPanelId) { + const below = nextExpandedSidebarPanel(panel); + if (!below) return; + + if (event.key === "Home") { + event.preventDefault(); + resetSidebarPanelHeight(panel); + return; } + + if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return; + event.preventDefault(); + + sidebarResizeBelow = below; + sidebarResizeAboveStart = sidebarPanelHeights[panel]; + sidebarResizeBelowStart = sidebarPanelHeights[below]; + applySidebarPanelResize(panel, event.key === "ArrowDown" ? SIDEBAR_RESIZE_STEP : -SIDEBAR_RESIZE_STEP); + sidebarResizeBelow = null; + persistSidebarPanelHeights(); + } + + /** Double-click on a handle puts the panel above back to its default height. */ + function resetSidebarPanelHeight(panel: SidebarPanelId) { + const below = nextExpandedSidebarPanel(panel); + sidebarPanelHeights = { ...sidebarPanelHeights, [panel]: SIDEBAR_PANEL_DEFAULT_HEIGHT[panel] }; + if (below) sidebarPanelHeights = { ...sidebarPanelHeights, [below]: SIDEBAR_PANEL_DEFAULT_HEIGHT[below] }; + persistSidebarPanelHeights(); } function toggleBranchPanelCollapsed() { @@ -6153,8 +6105,9 @@ class:stash-collapsed={stashPanelCollapsed} class:explorer-collapsed={explorerPanelCollapsed} class:all-collapsed={allLeftPanelsCollapsed} + class:resizing-panels={resizingSidebarPanel !== null} aria-label="Repository navigation" - 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};" + style="grid-template-rows: {leftSidebarRows};" > - {#if !branchPanelCollapsed} + {#if sidebarHandleVisible("branch", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed)} {:else} @@ -6212,20 +6167,26 @@ onManage={() => { void openWorktreeDialog(); }} onRefresh={() => { void refreshWorktrees(); }} /> - {#if !worktreePanelCollapsed} + {#if sidebarHandleVisible("worktree", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed)} {:else} @@ -6241,20 +6202,26 @@ onDeleteTag={deleteLocalTag} onPushTag={pushLocalTag} /> - {#if !tagsPanelCollapsed} + {#if sidebarHandleVisible("tags", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed)} {:else} @@ -6271,24 +6238,26 @@ collapsed={stashPanelCollapsed} onToggleCollapsed={toggleStashPanelCollapsed} /> - {#if !explorerPanelCollapsed && !stashPanelCollapsed} + {#if sidebarHandleVisible("stash", branchPanelCollapsed, worktreePanelCollapsed, tagsPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed)} {:else} diff --git a/src/app.css b/src/app.css index 8d9eb5c..5f4956e 100644 --- a/src/app.css +++ b/src/app.css @@ -1713,16 +1713,18 @@ } .left-sidebar { + /* The row template is built in App.svelte from the panel heights. */ display: grid; - grid-template-rows: minmax(180px, var(--branch-panel-height, 260px)) 14px minmax(150px, var(--stash-panel-height, 190px)) 14px minmax(220px, 1fr); align-content: start; min-width: 0; min-height: 0; gap: 0; + overflow-x: hidden; + overflow-y: auto; } .left-panel-resize-handle { - min-height: 14px; + min-height: 8px; margin: 0; } @@ -1732,34 +1734,12 @@ min-width: 0; } - .left-sidebar.branch-collapsed { - grid-template-rows: auto 0 minmax(150px, var(--stash-panel-height, 190px)) 14px minmax(220px, 1fr); - } - .left-sidebar.explorer-collapsed { - grid-template-rows: minmax(180px, var(--branch-panel-height, 260px)) 14px minmax(150px, var(--stash-panel-height, 190px)) 0 auto; - } - .left-sidebar.branch-collapsed.explorer-collapsed { - grid-template-rows: auto 0 minmax(150px, var(--stash-panel-height, 190px)) 0 auto; - } - .left-sidebar:has(.stash-panel.collapsed) { - grid-template-rows: minmax(180px, var(--branch-panel-height, 260px)) 14px auto 0 minmax(220px, 1fr); - } - .left-sidebar.branch-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: auto 0 auto 0 minmax(220px, 1fr); - } - .left-sidebar.explorer-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: minmax(180px, var(--branch-panel-height, 260px)) 14px auto 0 auto; - } - .left-sidebar.branch-collapsed.explorer-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: auto 0 auto 0 auto; - align-content: start; - } /* --- Main panel --- */ @@ -7508,31 +7488,6 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s .history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr); row-gap: 0; } .history-resize-handle { display: none; } .shell-body { gap: 6px; } - .left-sidebar { - grid-template-rows: minmax(180px, var(--branch-panel-height, 230px)) 14px minmax(150px, var(--stash-panel-height, 170px)) 14px minmax(220px, 1fr); - } - .left-sidebar:has(.stash-panel.collapsed) { - grid-template-rows: minmax(180px, var(--branch-panel-height, 230px)) 14px auto 0 minmax(220px, 1fr); - } - .left-sidebar.branch-collapsed { - grid-template-rows: auto 0 minmax(150px, var(--stash-panel-height, 170px)) 14px minmax(220px, 1fr); - } - .left-sidebar.explorer-collapsed { - grid-template-rows: minmax(180px, var(--branch-panel-height, 230px)) 14px minmax(150px, var(--stash-panel-height, 170px)) 0 auto; - } - .left-sidebar.branch-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: auto 0 auto 0 minmax(220px, 1fr); - } - .left-sidebar.explorer-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: minmax(180px, var(--branch-panel-height, 230px)) 14px auto 0 auto; - } - .left-sidebar.branch-collapsed.explorer-collapsed { - grid-template-rows: auto 0 minmax(150px, var(--stash-panel-height, 170px)) 0 auto; - } - .left-sidebar.branch-collapsed.explorer-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: auto 0 auto 0 auto; - align-content: start; - } .section-head { min-height: 40px; padding: 6px 10px; } .repo-summary { height: 40px; padding: 0 10px; } .repo-branch { max-width: 160px; } @@ -7562,32 +7517,6 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s .file-history-dialog-row time { display: none; } .file-history-dialog-actions { display: grid; } .file-history-dialog-actions button { width: 32px; min-width: 32px; padding: 0; overflow: hidden; color: var(--color-ink-muted); font-size: 0; gap: 0; } - .left-sidebar { - grid-template-rows: minmax(180px, var(--branch-panel-height, 240px)) 14px minmax(150px, var(--stash-panel-height, 180px)) 14px minmax(220px, 1fr); - min-height: 560px; - } - .left-sidebar:has(.stash-panel.collapsed) { - grid-template-rows: minmax(180px, var(--branch-panel-height, 240px)) 14px auto 0 minmax(220px, 1fr); - } - .left-sidebar.branch-collapsed { - grid-template-rows: auto 0 minmax(150px, var(--stash-panel-height, 180px)) 14px minmax(220px, 1fr); - } - .left-sidebar.explorer-collapsed { - grid-template-rows: minmax(120px, var(--branch-panel-height, 240px)) 14px minmax(150px, var(--stash-panel-height, 180px)) 0 auto; - } - .left-sidebar.branch-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: auto 0 auto 0 minmax(0, 1fr); - } - .left-sidebar.explorer-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: minmax(120px, var(--branch-panel-height, 240px)) 14px auto 0 auto; - } - .left-sidebar.branch-collapsed.explorer-collapsed { - grid-template-rows: auto 0 minmax(150px, var(--stash-panel-height, 180px)) 0 auto; - } - .left-sidebar.branch-collapsed.explorer-collapsed:has(.stash-panel.collapsed) { - grid-template-rows: auto 0 auto 0 auto; - align-content: start; - } .top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 14px var(--commit-panel-height, 190px); } .repo-form { grid-template-columns: 1fr; } .repo-tabbar { grid-template-columns: auto minmax(0, 1fr) auto; } @@ -9172,6 +9101,10 @@ section > header.page-header.page-header { /* Compact accordion navigation (layout A). */ .left-sidebar { overflow-y: auto; overflow-x: hidden; } +/* While a border is being dragged, keep the cursor and stop text selection. */ +.left-sidebar.resizing-panels { cursor: row-resize; user-select: none; } +.left-sidebar.resizing-panels * { pointer-events: none; } +.left-sidebar.resizing-panels .panel-resize-handle { pointer-events: auto; } .left-sidebar > .panel { min-width: 0; } .left-sidebar .section-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; @@ -9193,7 +9126,7 @@ section > header.page-header.page-header { .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 .left-panel-resize-handle { min-height: 8px; } .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; @@ -9215,8 +9148,6 @@ section > header.page-header.page-header { .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; @@ -9244,8 +9175,6 @@ section > header.page-header.page-header { .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; }