From 8627012d93f429c5de6fc80ef7f2605331974f4b Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Thu, 9 Jul 2026 08:33:09 +0200 Subject: [PATCH] feat(ui): enhance panel resizing and state persistence This update introduces functionality for resizing various panels in the UI, including the left sidebar, branch panel, stash panel, and file history. It also adds state persistence for panel sizes and collapsed states using local storage, improving user experience by maintaining preferences across sessions. - Implemented resizing functionality for multiple UI panels - Added local storage support for panel dimensions and collapsed states - Enhanced accessibility features for panel controls --- src/App.svelte | 458 ++++++++++++++++++++- src/app.css | 253 ++++++++++-- src/lib/components/BranchPanel.svelte | 24 +- src/lib/components/ExplorerPanel.svelte | 24 +- src/lib/components/FileHistoryPanel.svelte | 39 +- src/lib/components/StashPanel.svelte | 26 +- 6 files changed, 749 insertions(+), 75 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index c78f283..257cf4b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -165,13 +165,33 @@ const AI_SETTINGS_KEY = "gitlite.aiSettings.v1"; const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.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_STASH_PANEL_HEIGHT_KEY = "gitlite.leftStashPanelHeight.v1"; + const BRANCH_PANEL_COLLAPSED_KEY = "gitlite.branchPanelCollapsed.v1"; + const STASH_PANEL_COLLAPSED_KEY = "gitlite.stashPanelCollapsed.v2"; + const EXPLORER_PANEL_COLLAPSED_KEY = "gitlite.explorerPanelCollapsed.v1"; const HISTORY_ASIDE_WIDTH_KEY = "gitlite.historyAsideWidth.v1"; + const FILE_HISTORY_WIDTH_KEY = "gitlite.fileHistoryWidth.v1"; const COMMIT_PANEL_DEFAULT_HEIGHT = 220; const COMMIT_PANEL_MIN_HEIGHT = COMMIT_PANEL_DEFAULT_HEIGHT; const COMMIT_PANEL_MAX_HEIGHT = 640; + 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; const HISTORY_ASIDE_DEFAULT_WIDTH = 620; const HISTORY_ASIDE_MIN_WIDTH = 560; const HISTORY_ASIDE_MAX_WIDTH = 920; + const FILE_HISTORY_DEFAULT_WIDTH = 300; + const FILE_HISTORY_MIN_WIDTH = 240; + const FILE_HISTORY_MAX_WIDTH = 520; const ERROR_AUTO_HIDE_MS = 6000; // ── State ────────────────────────────────────────────────────────────────── @@ -292,10 +312,30 @@ let resizingCommitPanel = false; let resizeStartY = 0; let resizeStartHeight = 0; + let leftSidebarWidth = loadLeftSidebarWidth(); + let resizingLeftSidebar = false; + let leftSidebarResizeStartX = 0; + let leftSidebarResizeStartWidth = 0; + let leftBranchPanelHeight = loadLeftBranchPanelHeight(); + let resizingLeftBranchPanel = false; + let leftBranchResizeStartY = 0; + let leftBranchResizeStartHeight = 0; + let leftStashPanelHeight = loadLeftStashPanelHeight(); + let resizingLeftStashPanel = false; + let leftStashResizeStartY = 0; + let leftStashResizeStartHeight = 0; + let branchPanelCollapsed = loadStoredBoolean(BRANCH_PANEL_COLLAPSED_KEY, false); + let stashPanelCollapsed = loadStoredBoolean(STASH_PANEL_COLLAPSED_KEY, false); + let explorerPanelCollapsed = loadStoredBoolean(EXPLORER_PANEL_COLLAPSED_KEY, false); let historyAsideWidth = loadHistoryAsideWidth(); let resizingHistoryAside = false; let historyResizeStartX = 0; let historyResizeStartWidth = 0; + let fileHistoryWidth = loadFileHistoryWidth(); + let resizingFileHistory = false; + let fileHistoryResizeStartX = 0; + let fileHistoryResizeStartWidth = 0; + let fileHistoryCollapsed = true; // ── Derived ──────────────────────────────────────────────────────────────── @@ -345,6 +385,8 @@ $: favoriteRepoRows = favoriteRepoPaths .map(repoRowFromPath) .filter(repoMatchesSearch); + $: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed); + $: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed; // ── Lifecycle ────────────────────────────────────────────────────────────── @@ -1024,6 +1066,25 @@ } } + function loadStoredBoolean(key: string, fallback: boolean): boolean { + try { + const stored = localStorage.getItem(key); + if (stored === "true") return true; + if (stored === "false") return false; + } catch { + // Local storage is best-effort only; panel defaults are enough. + } + return fallback; + } + + function persistStoredBoolean(key: string, value: boolean) { + try { + localStorage.setItem(key, String(value)); + } catch { + // Local storage is best-effort only; toggles must keep working without it. + } + } + function clampCommitPanelHeight(value: number): number { return Math.min(COMMIT_PANEL_MAX_HEIGHT, Math.max(COMMIT_PANEL_MIN_HEIGHT, Math.round(value))); } @@ -1046,6 +1107,72 @@ } } + function clampLeftSidebarWidth(value: number): number { + return Math.min(LEFT_SIDEBAR_MAX_WIDTH, Math.max(LEFT_SIDEBAR_MIN_WIDTH, Math.round(value))); + } + + function loadLeftSidebarWidth(): number { + try { + const stored = Number(localStorage.getItem(LEFT_SIDEBAR_WIDTH_KEY)); + if (Number.isFinite(stored) && stored > 0) return clampLeftSidebarWidth(stored); + } catch { + // Fall through to the default below. + } + return LEFT_SIDEBAR_DEFAULT_WIDTH; + } + + function persistLeftSidebarWidth(value: number) { + try { + localStorage.setItem(LEFT_SIDEBAR_WIDTH_KEY, String(value)); + } catch { + // Local storage is best-effort only; resizing must keep working without it. + } + } + + 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))); } @@ -1068,6 +1195,28 @@ } } + function clampFileHistoryWidth(value: number): number { + return Math.min(FILE_HISTORY_MAX_WIDTH, Math.max(FILE_HISTORY_MIN_WIDTH, Math.round(value))); + } + + function loadFileHistoryWidth(): number { + try { + const stored = Number(localStorage.getItem(FILE_HISTORY_WIDTH_KEY)); + if (Number.isFinite(stored) && stored > 0) return clampFileHistoryWidth(stored); + } catch { + // Fall through to the default below. + } + return FILE_HISTORY_DEFAULT_WIDTH; + } + + function persistFileHistoryWidth(value: number) { + try { + localStorage.setItem(FILE_HISTORY_WIDTH_KEY, String(value)); + } catch { + // Local storage is best-effort only; resizing must keep working without it. + } + } + function startCommitPanelResize(event: PointerEvent) { event.preventDefault(); resizingCommitPanel = true; @@ -1096,6 +1245,131 @@ persistCommitPanelHeight(commitPanelHeight); } + function startLeftSidebarResize(event: PointerEvent) { + event.preventDefault(); + resizingLeftSidebar = true; + leftSidebarResizeStartX = event.clientX; + leftSidebarResizeStartWidth = leftSidebarWidth; + (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); + } + + function onLeftSidebarResizeMove(event: PointerEvent) { + if (!resizingLeftSidebar) return; + leftSidebarWidth = clampLeftSidebarWidth(leftSidebarResizeStartWidth + (event.clientX - leftSidebarResizeStartX)); + } + + function endLeftSidebarResize(event: PointerEvent) { + if (!resizingLeftSidebar) return; + resizingLeftSidebar = false; + persistLeftSidebarWidth(leftSidebarWidth); + const target = event.currentTarget as HTMLElement; + if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId); + } + + function onLeftSidebarResizeKeydown(event: KeyboardEvent) { + if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; + event.preventDefault(); + leftSidebarWidth = clampLeftSidebarWidth(leftSidebarWidth + (event.key === "ArrowRight" ? 20 : -20)); + persistLeftSidebarWidth(leftSidebarWidth); + } + + function buildLeftSidebarRows(branchCollapsed: boolean, stashCollapsed: boolean, explorerCollapsed: 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" : "14px"; + 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 && branchCollapsed) ? "0" : "14px"; + const explorerRow = explorerCollapsed ? "auto" : `minmax(${LEFT_EXPLORER_PANEL_MIN_HEIGHT}px, 1fr)`; + + return `${branchRow} ${branchHandleRow} ${stashRow} ${stashHandleRow} ${explorerRow}`; + } + + 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)); + } + } + + function endLeftStashPanelResize(event: PointerEvent) { + if (!resizingLeftStashPanel) return; + resizingLeftStashPanel = false; + if (stashPanelCollapsed) persistLeftBranchPanelHeight(leftBranchPanelHeight); + else persistLeftStashPanelHeight(leftStashPanelHeight); + 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 toggleBranchPanelCollapsed() { + branchPanelCollapsed = !branchPanelCollapsed; + persistStoredBoolean(BRANCH_PANEL_COLLAPSED_KEY, branchPanelCollapsed); + } + + function toggleStashPanelCollapsed() { + stashPanelCollapsed = !stashPanelCollapsed; + persistStoredBoolean(STASH_PANEL_COLLAPSED_KEY, stashPanelCollapsed); + } + + function toggleExplorerPanelCollapsed() { + explorerPanelCollapsed = !explorerPanelCollapsed; + persistStoredBoolean(EXPLORER_PANEL_COLLAPSED_KEY, explorerPanelCollapsed); + } + function startHistoryAsideResize(event: PointerEvent) { event.preventDefault(); resizingHistoryAside = true; @@ -1124,6 +1398,43 @@ persistHistoryAsideWidth(historyAsideWidth); } + function startFileHistoryResize(event: PointerEvent) { + if (fileHistoryCollapsed) return; + event.preventDefault(); + resizingFileHistory = true; + fileHistoryResizeStartX = event.clientX; + fileHistoryResizeStartWidth = fileHistoryWidth; + (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); + } + + function onFileHistoryResizeMove(event: PointerEvent) { + if (!resizingFileHistory) return; + fileHistoryWidth = clampFileHistoryWidth(fileHistoryResizeStartWidth + (fileHistoryResizeStartX - event.clientX)); + } + + function endFileHistoryResize(event: PointerEvent) { + if (!resizingFileHistory) return; + resizingFileHistory = false; + persistFileHistoryWidth(fileHistoryWidth); + const target = event.currentTarget as HTMLElement; + if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId); + } + + function onFileHistoryResizeKeydown(event: KeyboardEvent) { + if (fileHistoryCollapsed || (event.key !== "ArrowLeft" && event.key !== "ArrowRight")) return; + event.preventDefault(); + fileHistoryWidth = clampFileHistoryWidth(fileHistoryWidth + (event.key === "ArrowLeft" ? 20 : -20)); + persistFileHistoryWidth(fileHistoryWidth); + } + + function toggleFileHistoryCollapsed() { + fileHistoryCollapsed = !fileHistoryCollapsed; + } + + function revealFileHistory() { + fileHistoryCollapsed = false; + } + function rememberRecentRepo(path: string) { recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40); persistRepoLists(); @@ -2608,6 +2919,7 @@ if (!activeRepoPath || (selectedExplorerPath === node.path && selectedExplorerKind === node.kind)) return; selectedExplorerPath = node.path; selectedExplorerKind = node.kind; + revealFileHistory(); void loadSelectedFileHistory(node.path); trackEvent("explorer_node_selected", { kind: node.kind, @@ -2620,6 +2932,7 @@ selectedExplorerPath = file.path; selectedExplorerKind = "file"; expandedExplorerPaths = new Set([...expandedExplorerPaths, ...explorerParentFolders(file.path)]); + revealFileHistory(); void loadSelectedFileHistory(file.path); trackEvent("explorer_file_selected", { @@ -2633,6 +2946,7 @@ selectedExplorerPath = file.path; selectedExplorerKind = "file"; expandedExplorerPaths = new Set([...expandedExplorerPaths, ...explorerParentFolders(file.path)]); + revealFileHistory(); void loadSelectedFileHistory(file.path); trackEvent("explorer_file_selected", { @@ -2645,6 +2959,7 @@ if (!activeRepoPath || node.kind !== "file") return; selectedExplorerPath = node.path; selectedExplorerKind = "file"; + revealFileHistory(); void loadSelectedFileHistory(node.path); try { @@ -3231,10 +3546,22 @@ {:else} -
+
-
diff --git a/src/app.css b/src/app.css index f59a20d..1b31446 100644 --- a/src/app.css +++ b/src/app.css @@ -1096,63 +1096,120 @@ .workspace { display: grid; - grid-template-columns: clamp(220px, 18vw, 280px) minmax(0, 1fr) minmax(560px, var(--history-aside-width, 620px)); + grid-template-columns: minmax(220px, var(--left-sidebar-width, 280px)) 8px minmax(0, 1fr) 8px minmax(560px, var(--history-aside-width, 620px)); flex: 1 1 0; min-width: 0; min-height: 0; - gap: 8px; + column-gap: 0; + row-gap: 8px; } .history-aside { position: relative; display: grid; - grid-template-columns: minmax(0, 1fr) 300px; + grid-template-columns: minmax(0, 1fr) 8px minmax(240px, var(--file-history-width, 300px)); min-width: 0; min-height: 0; - gap: 8px; + column-gap: 0; + row-gap: 8px; } - .history-resize-handle { - position: absolute; - z-index: 20; - top: 0; - bottom: 0; - left: -7px; - width: 12px; + .history-aside.file-history-collapsed { + grid-template-columns: minmax(0, 1fr) 0 42px; + } + + .left-sidebar-resize-handle, + .history-resize-handle, + .file-history-resize-handle { + position: relative; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + min-width: 8px; cursor: col-resize; touch-action: none; } - .history-resize-handle::before { + .left-sidebar-resize-handle::before, + .history-resize-handle::before, + .file-history-resize-handle::before { content: ""; - position: absolute; - top: 12px; - bottom: 12px; - left: 5px; - width: 2px; + width: 3px; + height: 40px; border-radius: 999px; - background: transparent; - transition: background 120ms ease, box-shadow 120ms ease; + background: var(--color-border); + transition: background-color 0.15s ease, box-shadow 0.15s ease; } + .left-sidebar-resize-handle:hover::before, + .left-sidebar-resize-handle.resizing::before, .history-resize-handle:hover::before, - .history-resize-handle.resizing::before { - background: rgba(65,209,255,0.62); + .history-resize-handle.resizing::before, + .file-history-resize-handle:hover::before, + .file-history-resize-handle.resizing::before { + background: var(--color-accent); box-shadow: 0 0 14px rgba(65,209,255,0.3); } - .history-resize-handle:focus-visible { + .left-sidebar-resize-handle:focus-visible, + .history-resize-handle:focus-visible, + .file-history-resize-handle:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } + .history-aside.file-history-collapsed .file-history-resize-handle { + pointer-events: none; + } + .history-aside.file-history-collapsed .file-history-resize-handle::before { + display: none; + } + .left-sidebar { display: grid; - grid-template-rows: minmax(170px, 0.75fr) minmax(150px, 0.55fr) minmax(220px, 1fr); + 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: 8px; + gap: 0; + } + + .left-panel-resize-handle { + min-height: 14px; + margin: 0; + } + + .left-panel-resize-placeholder { + display: block; + min-height: 0; + 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(170px, 0.85fr) auto minmax(220px, 1.15fr); + 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 --- */ @@ -1474,6 +1531,9 @@ gap: 6px; } .branch-panel { position: relative; } + .branch-panel.collapsed { + grid-template-rows: auto; + } .branch-create-toggle { width: 26px; @@ -1747,6 +1807,9 @@ /* --- Explorer --- */ .explorer-panel { position: relative; } + .explorer-panel.collapsed { + grid-template-rows: auto; + } .explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; } .explorer-bulk-button { @@ -2127,8 +2190,75 @@ background: rgba(65,209,255,0.08); color: var(--color-ink); } + .file-history-panel.collapsed { + display: grid; + grid-template-rows: 1fr; + overflow: hidden; + } + .file-history-rail-toggle { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + justify-items: center; + gap: 8px; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + padding: 12px 0; + border: 0; + border-radius: 0; + color: var(--color-ink-dim); + background: linear-gradient(180deg, rgba(100,108,255,0.12), rgba(65,209,255,0.04)); + } + .file-history-rail-toggle:hover:not(:disabled) { + color: var(--color-ink); + background: linear-gradient(180deg, rgba(100,108,255,0.18), rgba(65,209,255,0.08)); + } + .file-history-rail-toggle span { + writing-mode: vertical-rl; + text-orientation: mixed; + overflow: hidden; + color: currentColor; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; + white-space: nowrap; + } + .file-history-rail-toggle small { + display: inline-grid; + place-items: center; + min-width: 22px; + min-height: 22px; + border-radius: 999px; + color: var(--color-ink); + background: rgba(94,110,156,0.18); + font-size: 11px; + font-weight: 800; + } .file-history-head { align-items: flex-start; } .file-history-heading { min-width: 0; flex: 1 1 auto; overflow: hidden; } + .file-history-head-actions { + display: flex; + align-items: center; + flex: 0 0 auto; + gap: 6px; + } + .file-history-toggle { + width: 26px; + min-width: 26px; + min-height: 26px; + padding: 0; + border-color: rgba(65,209,255,0.2); + border-radius: 7px; + color: var(--color-ink-dim); + background: rgba(65,209,255,0.06); + } + .file-history-toggle:hover:not(:disabled) { + border-color: rgba(65,209,255,0.45); + color: #ffffff; + background: rgba(65,209,255,0.13); + } .section-head .file-history-name { display: -webkit-box; max-height: 2.4em; @@ -4261,27 +4391,52 @@ /* --- Responsive breakpoints --- */ @media (min-width: 1800px) { - .workspace { grid-template-columns: 320px minmax(0, 1fr) minmax(560px, var(--history-aside-width, 680px)); } + .workspace { grid-template-columns: minmax(220px, var(--left-sidebar-width, 320px)) 8px minmax(0, 1fr) 8px minmax(560px, var(--history-aside-width, 680px)); } } @media (max-width: 1400px) { - .workspace { grid-template-columns: clamp(200px, 17vw, 265px) minmax(0, 1fr) minmax(540px, var(--history-aside-width, 580px)); } + .workspace { grid-template-columns: minmax(200px, var(--left-sidebar-width, 265px)) 8px minmax(0, 1fr) 8px minmax(540px, var(--history-aside-width, 580px)); } } /* Stack CommitPanel below StatusPanel; history panels stay side by side */ @media (max-width: 1100px) { - .workspace { grid-template-columns: clamp(185px, 16vw, 220px) minmax(0, 1fr) minmax(500px, var(--history-aside-width, 560px)); } + .workspace { grid-template-columns: minmax(185px, var(--left-sidebar-width, 220px)) 8px minmax(0, 1fr) 8px minmax(500px, var(--history-aside-width, 560px)); } .top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 14px var(--commit-panel-height, 190px); } } /* Compact: stack history panels vertically, narrow sidebars */ @media (max-width: 960px) { - .workspace { grid-template-columns: 180px minmax(0, 1fr) 300px; gap: 6px; } - .history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1.3fr) minmax(0, 0.7fr); } - .history-resize-handle { display: none; } + .workspace { grid-template-columns: 180px 8px minmax(0, 1fr) 0 300px; row-gap: 6px; } + .history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1.3fr) minmax(0, 0.7fr); row-gap: 6px; } + .history-aside.file-history-collapsed { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) 42px; } + .history-resize-handle, + .file-history-resize-handle { display: none; } .shell-body { gap: 6px; } - .left-sidebar { gap: 6px; grid-template-rows: minmax(150px, 0.7fr) minmax(145px, 0.55fr) minmax(190px, 1fr); } - .left-sidebar:has(.stash-panel.collapsed) { grid-template-rows: minmax(150px, 0.8fr) auto minmax(190px, 1.1fr); } + .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; } @@ -4297,9 +4452,35 @@ body { overflow: auto; } .shell-body { min-height: 100%; gap: 6px; } .workspace { grid-template-columns: 1fr; gap: 6px; } + .left-sidebar-resize-handle { display: none; } .history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(200px, 1fr) minmax(150px, 0.5fr); min-height: 380px; } - .left-sidebar { grid-template-rows: minmax(180px, 0.9fr) minmax(150px, 0.55fr) minmax(220px, 1fr); min-height: 560px; } - .left-sidebar:has(.stash-panel.collapsed) { grid-template-rows: minmax(180px, 1fr) auto minmax(220px, 1.1fr); } + .history-aside.file-history-collapsed { grid-template-rows: minmax(200px, 1fr) 42px; } + .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: minmax(0, 1fr) auto; } @@ -4322,7 +4503,7 @@ .dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); } .compare-dialog .dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); } .global-search-options { grid-template-columns: 1fr; } - .file-search-split { grid-template-columns: 1fr; grid-template-rows: minmax(150px, 0.9fr) minmax(220px, 1fr); } + .file-search-split { grid-template-columns: 1fr; grid-template-rows: minmax(150px, 0.9fr) minmax(0, 1fr); } .file-search-column { border-right: none; border-bottom: 1px solid var(--color-border-subtle); } .file-search-hit { grid-template-columns: auto minmax(0, 1fr); align-items: start; } .file-search-hit .status-badge, diff --git a/src/lib/components/BranchPanel.svelte b/src/lib/components/BranchPanel.svelte index dcec2f0..c57db06 100644 --- a/src/lib/components/BranchPanel.svelte +++ b/src/lib/components/BranchPanel.svelte @@ -57,6 +57,8 @@ onCreateTag: (name: string, message: string) => void | Promise; onDeleteTag: (tag: GitTag) => void | Promise; onPushTag: (tag: GitTag) => void | Promise; + collapsed?: boolean; + onToggleCollapsed?: () => void; } let { @@ -75,6 +77,8 @@ onCreateTag = () => {}, onDeleteTag = () => {}, onPushTag = () => {}, + collapsed = false, + onToggleCollapsed = () => {}, }: Props = $props(); let localOpen = $state(true); @@ -355,7 +359,7 @@ -
+
Branches @@ -373,10 +377,26 @@
- {#if !hasRepository} + {#if collapsed} + + {:else if !hasRepository}

Open a repository to list branches.

{:else if branches.length === 0 && tags.length === 0}

No branches returned.

diff --git a/src/lib/components/ExplorerPanel.svelte b/src/lib/components/ExplorerPanel.svelte index 5c6f9bc..377c5a6 100644 --- a/src/lib/components/ExplorerPanel.svelte +++ b/src/lib/components/ExplorerPanel.svelte @@ -38,6 +38,8 @@ onSelectNode: (node: ExplorerNode) => void; onOpenFile: (node: ExplorerNode) => void; onBlame: (node: ExplorerNode) => void; + collapsed?: boolean; + onToggleCollapsed?: () => void; } let { @@ -53,6 +55,8 @@ onSelectNode = () => {}, onOpenFile = () => {}, onBlame = () => {}, + collapsed = false, + onToggleCollapsed = () => {}, }: Props = $props(); let contextNode = $state(null); @@ -203,7 +207,7 @@ -
+
Explorer @@ -231,10 +235,26 @@
- {#if !hasRepository} + {#if collapsed} + + {:else if !hasRepository}

Open a repository to browse files.

{:else if repoFiles.length === 0}

No files returned.

diff --git a/src/lib/components/FileHistoryPanel.svelte b/src/lib/components/FileHistoryPanel.svelte index dc4c8d4..5cf3b1d 100644 --- a/src/lib/components/FileHistoryPanel.svelte +++ b/src/lib/components/FileHistoryPanel.svelte @@ -1,5 +1,5 @@ -
+
+ {#if collapsed} + + {:else}
{selectedExplorerLabel} @@ -85,7 +105,19 @@ use:pathTooltip={selectedExplorerPath} >{selectedExplorerPath ? fileName(selectedExplorerPath) : "No file"}
- {fileHistory.length} +
+ {fileHistory.length} + +
{#if !hasRepository} @@ -136,4 +168,5 @@ {/each} {/if} + {/if}
diff --git a/src/lib/components/StashPanel.svelte b/src/lib/components/StashPanel.svelte index de3f690..a61c827 100644 --- a/src/lib/components/StashPanel.svelte +++ b/src/lib/components/StashPanel.svelte @@ -11,6 +11,8 @@ onApply: (stash: GitStash) => void; onPop: (stash: GitStash) => void; onDrop: (stash: GitStash) => void; + collapsed?: boolean; + onToggleCollapsed?: () => void; } let { @@ -22,11 +24,12 @@ onApply = () => {}, onPop = () => {}, onDrop = () => {}, + collapsed = false, + onToggleCollapsed = () => {}, }: Props = $props(); let message = $state(""); let includeUntracked = $state(true); - let open = $state(false); function submitPush() { onPush(message, includeUntracked); @@ -38,31 +41,32 @@ } -
+
Stash

Shelved changes

+ {stashes.length} - {stashes.length}
- {#if !open} + {#if collapsed} {:else if !hasRepository}
No repository loaded.