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
This commit is contained in:
Christoph Brandau
2026-07-09 08:33:09 +02:00
parent adfb5f4158
commit 8627012d93
6 changed files with 749 additions and 75 deletions
+22 -2
View File
@@ -57,6 +57,8 @@
onCreateTag: (name: string, message: string) => void | Promise<void>;
onDeleteTag: (tag: GitTag) => void | Promise<void>;
onPushTag: (tag: GitTag) => void | Promise<void>;
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 @@
<svelte:window on:click={closeAllContextMenus} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeAllContextMenus} />
<section bind:this={panelElement} class="panel branch-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Branches">
<section bind:this={panelElement} class="panel branch-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="Branches">
<div class="section-head">
<div>
<span class="eyebrow">Branches</span>
@@ -373,10 +377,26 @@
<Plus size={14} aria-hidden="true" />
</button>
<span class="pill pill-count">{branches.length}</span>
<button
class="branch-create-toggle panel-collapse-toggle"
type="button"
onclick={onToggleCollapsed}
aria-expanded={!collapsed}
title={collapsed ? "Expand branches" : "Collapse branches"}
aria-label={collapsed ? "Expand branches panel" : "Collapse branches panel"}
>
{#if collapsed}
<ChevronRight size={14} aria-hidden="true" />
{:else}
<ChevronDown size={14} aria-hidden="true" />
{/if}
</button>
</div>
</div>
{#if !hasRepository}
{#if collapsed}
<!-- collapsed -->
{:else if !hasRepository}
<p class="blank-state">Open a repository to list branches.</p>
{:else if branches.length === 0 && tags.length === 0}
<p class="blank-state">No branches returned.</p>
+22 -2
View File
@@ -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<ExplorerNode | null>(null);
@@ -203,7 +207,7 @@
<svelte:window on:click={closeFileContextMenu} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeFileContextMenu} />
<section class="panel explorer-panel grid grid-rows-[auto_1fr] overflow-hidden" 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>
<span class="eyebrow">Explorer</span>
@@ -231,10 +235,26 @@
<Folder size={14} aria-hidden="true" />
</button>
<span class="pill pill-count">{repoFiles.length}</span>
<button
class="explorer-bulk-button panel-collapse-toggle"
type="button"
onclick={onToggleCollapsed}
aria-expanded={!collapsed}
title={collapsed ? "Expand file explorer" : "Collapse file explorer"}
aria-label={collapsed ? "Expand file explorer panel" : "Collapse file explorer panel"}
>
{#if collapsed}
<ChevronRight size={14} aria-hidden="true" />
{:else}
<ChevronDown size={14} aria-hidden="true" />
{/if}
</button>
</div>
</div>
{#if !hasRepository}
{#if collapsed}
<!-- collapsed -->
{:else if !hasRepository}
<p class="blank-state">Open a repository to browse files.</p>
{:else if repoFiles.length === 0}
<p class="blank-state">No files returned.</p>
+36 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { GitCompare, History, RotateCcw } from "@lucide/svelte";
import { ChevronLeft, ChevronRight, GitCompare, History, RotateCcw } from "@lucide/svelte";
import iconUrl from "../../../src-tauri/icons/icon.png";
import type { GitCommit } from "../types";
@@ -12,6 +12,8 @@
isLoading?: boolean;
onDiff: (commit: GitCommit) => void;
onRestore: (commit: GitCommit) => void;
collapsed?: boolean;
onToggleCollapsed?: () => void;
}
let {
@@ -23,6 +25,8 @@
isLoading = false,
onDiff = () => {},
onRestore = () => {},
collapsed = false,
onToggleCollapsed = () => {},
}: Props = $props();
function formatCommitDate(value: string): string {
@@ -76,7 +80,23 @@
}
</script>
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Selected file history">
<section class="panel file-history-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="Selected file history">
{#if collapsed}
<button
class="file-history-rail-toggle"
type="button"
onclick={onToggleCollapsed}
aria-expanded={!collapsed}
title="Expand file history"
aria-label="Expand file history panel"
>
<ChevronLeft size={16} aria-hidden="true" />
<span>File history</span>
{#if selectedExplorerPath}
<small>{fileHistory.length}</small>
{/if}
</button>
{:else}
<div class="section-head file-history-head">
<div class="file-history-heading">
<span class="eyebrow">{selectedExplorerLabel}</span>
@@ -85,7 +105,19 @@
use:pathTooltip={selectedExplorerPath}
>{selectedExplorerPath ? fileName(selectedExplorerPath) : "No file"}</h2>
</div>
<span class="pill pill-count file-history-count">{fileHistory.length}</span>
<div class="file-history-head-actions">
<span class="pill pill-count file-history-count">{fileHistory.length}</span>
<button
class="file-history-toggle panel-collapse-toggle"
type="button"
onclick={onToggleCollapsed}
aria-expanded={!collapsed}
title="Collapse file history"
aria-label="Collapse file history panel"
>
<ChevronRight size={14} aria-hidden="true" />
</button>
</div>
</div>
{#if !hasRepository}
@@ -136,4 +168,5 @@
{/each}
</div>
{/if}
{/if}
</section>
+15 -11
View File
@@ -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 @@
}
</script>
<section class="panel stash-panel overflow-hidden" class:collapsed={!open} aria-label="Git stash">
<section class="panel stash-panel overflow-hidden" class:collapsed aria-label="Git stash">
<div class="section-head">
<div>
<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">
<span class="pill pill-count">{stashes.length}</span>
<button
class="stash-toggle"
class="stash-toggle panel-collapse-toggle"
type="button"
onclick={() => { open = !open; }}
aria-expanded={open}
title={open ? "Collapse stash panel" : "Expand stash panel"}
onclick={onToggleCollapsed}
aria-expanded={!collapsed}
title={collapsed ? "Expand stash panel" : "Collapse stash panel"}
aria-label={collapsed ? "Expand stash panel" : "Collapse stash panel"}
>
{#if open}
<ChevronDown size={14} aria-hidden="true" />
{:else}
{#if collapsed}
<ChevronRight size={14} aria-hidden="true" />
{:else}
<ChevronDown size={14} aria-hidden="true" />
{/if}
</button>
<span class="pill pill-count">{stashes.length}</span>
</div>
</div>
{#if !open}
{#if collapsed}
<!-- collapsed -->
{:else if !hasRepository}
<div class="blank-state">No repository loaded.</div>