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
+421 -5
View File
@@ -165,13 +165,33 @@
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1"; const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1"; const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1";
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.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 HISTORY_ASIDE_WIDTH_KEY = "gitlite.historyAsideWidth.v1";
const FILE_HISTORY_WIDTH_KEY = "gitlite.fileHistoryWidth.v1";
const COMMIT_PANEL_DEFAULT_HEIGHT = 220; const COMMIT_PANEL_DEFAULT_HEIGHT = 220;
const COMMIT_PANEL_MIN_HEIGHT = COMMIT_PANEL_DEFAULT_HEIGHT; const COMMIT_PANEL_MIN_HEIGHT = COMMIT_PANEL_DEFAULT_HEIGHT;
const COMMIT_PANEL_MAX_HEIGHT = 640; 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_DEFAULT_WIDTH = 620;
const HISTORY_ASIDE_MIN_WIDTH = 560; const HISTORY_ASIDE_MIN_WIDTH = 560;
const HISTORY_ASIDE_MAX_WIDTH = 920; 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; const ERROR_AUTO_HIDE_MS = 6000;
// ── State ────────────────────────────────────────────────────────────────── // ── State ──────────────────────────────────────────────────────────────────
@@ -292,10 +312,30 @@
let resizingCommitPanel = false; let resizingCommitPanel = false;
let resizeStartY = 0; let resizeStartY = 0;
let resizeStartHeight = 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 historyAsideWidth = loadHistoryAsideWidth();
let resizingHistoryAside = false; let resizingHistoryAside = false;
let historyResizeStartX = 0; let historyResizeStartX = 0;
let historyResizeStartWidth = 0; let historyResizeStartWidth = 0;
let fileHistoryWidth = loadFileHistoryWidth();
let resizingFileHistory = false;
let fileHistoryResizeStartX = 0;
let fileHistoryResizeStartWidth = 0;
let fileHistoryCollapsed = true;
// ── Derived ──────────────────────────────────────────────────────────────── // ── Derived ────────────────────────────────────────────────────────────────
@@ -345,6 +385,8 @@
$: favoriteRepoRows = favoriteRepoPaths $: favoriteRepoRows = favoriteRepoPaths
.map(repoRowFromPath) .map(repoRowFromPath)
.filter(repoMatchesSearch); .filter(repoMatchesSearch);
$: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed);
$: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
// ── Lifecycle ────────────────────────────────────────────────────────────── // ── 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 { function clampCommitPanelHeight(value: number): number {
return Math.min(COMMIT_PANEL_MAX_HEIGHT, Math.max(COMMIT_PANEL_MIN_HEIGHT, Math.round(value))); 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 { function clampHistoryAsideWidth(value: number): number {
return Math.min(HISTORY_ASIDE_MAX_WIDTH, Math.max(HISTORY_ASIDE_MIN_WIDTH, Math.round(value))); 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) { function startCommitPanelResize(event: PointerEvent) {
event.preventDefault(); event.preventDefault();
resizingCommitPanel = true; resizingCommitPanel = true;
@@ -1096,6 +1245,131 @@
persistCommitPanelHeight(commitPanelHeight); 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) { function startHistoryAsideResize(event: PointerEvent) {
event.preventDefault(); event.preventDefault();
resizingHistoryAside = true; resizingHistoryAside = true;
@@ -1124,6 +1398,43 @@
persistHistoryAsideWidth(historyAsideWidth); 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) { function rememberRecentRepo(path: string) {
recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40); recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40);
persistRepoLists(); persistRepoLists();
@@ -2608,6 +2919,7 @@
if (!activeRepoPath || (selectedExplorerPath === node.path && selectedExplorerKind === node.kind)) return; if (!activeRepoPath || (selectedExplorerPath === node.path && selectedExplorerKind === node.kind)) return;
selectedExplorerPath = node.path; selectedExplorerPath = node.path;
selectedExplorerKind = node.kind; selectedExplorerKind = node.kind;
revealFileHistory();
void loadSelectedFileHistory(node.path); void loadSelectedFileHistory(node.path);
trackEvent("explorer_node_selected", { trackEvent("explorer_node_selected", {
kind: node.kind, kind: node.kind,
@@ -2620,6 +2932,7 @@
selectedExplorerPath = file.path; selectedExplorerPath = file.path;
selectedExplorerKind = "file"; selectedExplorerKind = "file";
expandedExplorerPaths = new Set([...expandedExplorerPaths, ...explorerParentFolders(file.path)]); expandedExplorerPaths = new Set([...expandedExplorerPaths, ...explorerParentFolders(file.path)]);
revealFileHistory();
void loadSelectedFileHistory(file.path); void loadSelectedFileHistory(file.path);
trackEvent("explorer_file_selected", { trackEvent("explorer_file_selected", {
@@ -2633,6 +2946,7 @@
selectedExplorerPath = file.path; selectedExplorerPath = file.path;
selectedExplorerKind = "file"; selectedExplorerKind = "file";
expandedExplorerPaths = new Set([...expandedExplorerPaths, ...explorerParentFolders(file.path)]); expandedExplorerPaths = new Set([...expandedExplorerPaths, ...explorerParentFolders(file.path)]);
revealFileHistory();
void loadSelectedFileHistory(file.path); void loadSelectedFileHistory(file.path);
trackEvent("explorer_file_selected", { trackEvent("explorer_file_selected", {
@@ -2645,6 +2959,7 @@
if (!activeRepoPath || node.kind !== "file") return; if (!activeRepoPath || node.kind !== "file") return;
selectedExplorerPath = node.path; selectedExplorerPath = node.path;
selectedExplorerKind = "file"; selectedExplorerKind = "file";
revealFileHistory();
void loadSelectedFileHistory(node.path); void loadSelectedFileHistory(node.path);
try { try {
@@ -3231,10 +3546,22 @@
</section> </section>
{:else} {:else}
<!-- Workspace --> <!-- Workspace -->
<section class="workspace" aria-label="Git workspace" style="--history-aside-width: {historyAsideWidth}px;"> <section
class="workspace"
aria-label="Git workspace"
style="--left-sidebar-width: {leftSidebarWidth}px; --history-aside-width: {historyAsideWidth}px; --file-history-width: {fileHistoryWidth}px;"
>
<!-- Left sidebar: branches + explorer --> <!-- Left sidebar: branches + explorer -->
<aside class="left-sidebar" aria-label="Repository navigation"> <aside
class="left-sidebar"
class:branch-collapsed={branchPanelCollapsed}
class:stash-collapsed={stashPanelCollapsed}
class:explorer-collapsed={explorerPanelCollapsed}
class:all-collapsed={allLeftPanelsCollapsed}
aria-label="Repository navigation"
style="--branch-panel-height: {leftBranchPanelHeight}px; --stash-panel-height: {leftStashPanelHeight}px; grid-template-rows: {leftSidebarRows};"
>
<BranchPanel <BranchPanel
{branches} {branches}
{localBranches} {localBranches}
@@ -3251,7 +3578,31 @@
onCreateTag={createNewTag} onCreateTag={createNewTag}
onDeleteTag={deleteLocalTag} onDeleteTag={deleteLocalTag}
onPushTag={pushLocalTag} onPushTag={pushLocalTag}
collapsed={branchPanelCollapsed}
onToggleCollapsed={toggleBranchPanelCollapsed}
/> />
{#if !branchPanelCollapsed}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="panel-resize-handle left-panel-resize-handle branch-panel-resize-handle"
class:resizing={resizingLeftBranchPanel}
role="separator"
aria-orientation="horizontal"
aria-label="Resize branches panel height"
aria-valuenow={leftBranchPanelHeight}
aria-valuemin={LEFT_BRANCH_PANEL_MIN_HEIGHT}
aria-valuemax={LEFT_BRANCH_PANEL_MAX_HEIGHT}
tabindex="0"
onpointerdown={startLeftBranchPanelResize}
onpointermove={onLeftBranchPanelResizeMove}
onpointerup={endLeftBranchPanelResize}
onpointercancel={endLeftBranchPanelResize}
onkeydown={onLeftBranchPanelResizeKeydown}
></div>
{:else}
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
{/if}
<StashPanel <StashPanel
{stashes} {stashes}
changedCount={changedFiles.length} changedCount={changedFiles.length}
@@ -3261,7 +3612,31 @@
onApply={applyStashEntry} onApply={applyStashEntry}
onPop={popStashEntry} onPop={popStashEntry}
onDrop={dropStashEntry} onDrop={dropStashEntry}
collapsed={stashPanelCollapsed}
onToggleCollapsed={toggleStashPanelCollapsed}
/> />
{#if !explorerPanelCollapsed && (!stashPanelCollapsed || !branchPanelCollapsed)}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="panel-resize-handle left-panel-resize-handle stash-panel-resize-handle"
class:resizing={resizingLeftStashPanel}
role="separator"
aria-orientation="horizontal"
aria-label={stashPanelCollapsed ? "Resize files panel space" : "Resize stash and explorer panels"}
aria-valuenow={stashPanelCollapsed ? leftBranchPanelHeight : leftStashPanelHeight}
aria-valuemin={stashPanelCollapsed ? LEFT_BRANCH_PANEL_MIN_HEIGHT : LEFT_STASH_PANEL_MIN_HEIGHT}
aria-valuemax={stashPanelCollapsed ? LEFT_BRANCH_PANEL_MAX_HEIGHT : LEFT_STASH_PANEL_MAX_HEIGHT}
tabindex="0"
onpointerdown={startLeftStashPanelResize}
onpointermove={onLeftStashPanelResizeMove}
onpointerup={endLeftStashPanelResize}
onpointercancel={endLeftStashPanelResize}
onkeydown={onLeftStashPanelResizeKeydown}
></div>
{:else}
<span class="left-panel-resize-placeholder" aria-hidden="true"></span>
{/if}
<ExplorerPanel <ExplorerPanel
{repoFiles} {repoFiles}
{expandedExplorerPaths} {expandedExplorerPaths}
@@ -3275,8 +3650,28 @@
onSelectNode={selectExplorerNode} onSelectNode={selectExplorerNode}
onOpenFile={openFileFromExplorer} onOpenFile={openFileFromExplorer}
onBlame={openBlame} onBlame={openBlame}
collapsed={explorerPanelCollapsed}
onToggleCollapsed={toggleExplorerPanelCollapsed}
/> />
</aside> </aside>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="left-sidebar-resize-handle"
class:resizing={resizingLeftSidebar}
role="separator"
aria-orientation="vertical"
aria-label="Resize navigation sidebar width"
aria-valuenow={leftSidebarWidth}
aria-valuemin={LEFT_SIDEBAR_MIN_WIDTH}
aria-valuemax={LEFT_SIDEBAR_MAX_WIDTH}
tabindex="0"
onpointerdown={startLeftSidebarResize}
onpointermove={onLeftSidebarResizeMove}
onpointerup={endLeftSidebarResize}
onpointercancel={endLeftSidebarResize}
onkeydown={onLeftSidebarResizeKeydown}
></div>
<!-- Center: summary + status + commit --> <!-- Center: summary + status + commit -->
<section class="main-panel" aria-label="Repository status"> <section class="main-panel" aria-label="Repository status">
@@ -3353,8 +3748,6 @@
</div> </div>
</section> </section>
<!-- Right sidebar: commit graph + file history -->
<aside class="history-aside" aria-label="Commit history">
<!-- svelte-ignore a11y_no_noninteractive_tabindex --> <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div <div
@@ -3362,7 +3755,7 @@
class:resizing={resizingHistoryAside} class:resizing={resizingHistoryAside}
role="separator" role="separator"
aria-orientation="vertical" aria-orientation="vertical"
aria-label="Resize history panel width" aria-label="Resize commit history width"
aria-valuenow={historyAsideWidth} aria-valuenow={historyAsideWidth}
aria-valuemin={HISTORY_ASIDE_MIN_WIDTH} aria-valuemin={HISTORY_ASIDE_MIN_WIDTH}
aria-valuemax={HISTORY_ASIDE_MAX_WIDTH} aria-valuemax={HISTORY_ASIDE_MAX_WIDTH}
@@ -3373,6 +3766,9 @@
onpointercancel={endHistoryAsideResize} onpointercancel={endHistoryAsideResize}
onkeydown={onHistoryAsideResizeKeydown} onkeydown={onHistoryAsideResizeKeydown}
></div> ></div>
<!-- Right sidebar: commit graph + file history -->
<aside class="history-aside" class:file-history-collapsed={fileHistoryCollapsed} aria-label="Commit history">
<HistoryPanel <HistoryPanel
{commits} {commits}
{localBranchNames} {localBranchNames}
@@ -3392,6 +3788,24 @@
expandedCommitHashes = next; expandedCommitHashes = next;
}} }}
/> />
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="file-history-resize-handle"
class:resizing={resizingFileHistory}
role="separator"
aria-orientation="vertical"
aria-label="Resize file history width"
aria-valuenow={fileHistoryWidth}
aria-valuemin={FILE_HISTORY_MIN_WIDTH}
aria-valuemax={FILE_HISTORY_MAX_WIDTH}
tabindex={fileHistoryCollapsed ? -1 : 0}
onpointerdown={startFileHistoryResize}
onpointermove={onFileHistoryResizeMove}
onpointerup={endFileHistoryResize}
onpointercancel={endFileHistoryResize}
onkeydown={onFileHistoryResizeKeydown}
></div>
<FileHistoryPanel <FileHistoryPanel
{fileHistory} {fileHistory}
{selectedExplorerPath} {selectedExplorerPath}
@@ -3401,6 +3815,8 @@
isLoading={fileHistoryLoading} isLoading={fileHistoryLoading}
onDiff={diffSelectedFileFromCommit} onDiff={diffSelectedFileFromCommit}
onRestore={restoreSelectedFileFromCommit} onRestore={restoreSelectedFileFromCommit}
collapsed={fileHistoryCollapsed}
onToggleCollapsed={toggleFileHistoryCollapsed}
/> />
</aside> </aside>
</section> </section>
+217 -36
View File
@@ -1096,63 +1096,120 @@
.workspace { .workspace {
display: grid; 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; flex: 1 1 0;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
gap: 8px; column-gap: 0;
row-gap: 8px;
} }
.history-aside { .history-aside {
position: relative; position: relative;
display: grid; 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-width: 0;
min-height: 0; min-height: 0;
gap: 8px; column-gap: 0;
row-gap: 8px;
} }
.history-resize-handle { .history-aside.file-history-collapsed {
position: absolute; grid-template-columns: minmax(0, 1fr) 0 42px;
z-index: 20; }
top: 0;
bottom: 0; .left-sidebar-resize-handle,
left: -7px; .history-resize-handle,
width: 12px; .file-history-resize-handle {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
min-width: 8px;
cursor: col-resize; cursor: col-resize;
touch-action: none; touch-action: none;
} }
.history-resize-handle::before { .left-sidebar-resize-handle::before,
.history-resize-handle::before,
.file-history-resize-handle::before {
content: ""; content: "";
position: absolute; width: 3px;
top: 12px; height: 40px;
bottom: 12px;
left: 5px;
width: 2px;
border-radius: 999px; border-radius: 999px;
background: transparent; background: var(--color-border);
transition: background 120ms ease, box-shadow 120ms ease; 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:hover::before,
.history-resize-handle.resizing::before { .history-resize-handle.resizing::before,
background: rgba(65,209,255,0.62); .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); 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: 2px solid var(--color-primary);
outline-offset: 2px; 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 { .left-sidebar {
display: grid; 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-width: 0;
min-height: 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) { .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 --- */ /* --- Main panel --- */
@@ -1474,6 +1531,9 @@
gap: 6px; gap: 6px;
} }
.branch-panel { position: relative; } .branch-panel { position: relative; }
.branch-panel.collapsed {
grid-template-rows: auto;
}
.branch-create-toggle { .branch-create-toggle {
width: 26px; width: 26px;
@@ -1747,6 +1807,9 @@
/* --- Explorer --- */ /* --- Explorer --- */
.explorer-panel { position: relative; } .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-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; }
.explorer-bulk-button { .explorer-bulk-button {
@@ -2127,8 +2190,75 @@
background: rgba(65,209,255,0.08); background: rgba(65,209,255,0.08);
color: var(--color-ink); 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-head { align-items: flex-start; }
.file-history-heading { min-width: 0; flex: 1 1 auto; overflow: hidden; } .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 { .section-head .file-history-name {
display: -webkit-box; display: -webkit-box;
max-height: 2.4em; max-height: 2.4em;
@@ -4261,27 +4391,52 @@
/* --- Responsive breakpoints --- */ /* --- Responsive breakpoints --- */
@media (min-width: 1800px) { @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) { @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 */ /* Stack CommitPanel below StatusPanel; history panels stay side by side */
@media (max-width: 1100px) { @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); } .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 */ /* Compact: stack history panels vertically, narrow sidebars */
@media (max-width: 960px) { @media (max-width: 960px) {
.workspace { grid-template-columns: 180px minmax(0, 1fr) 300px; gap: 6px; } .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); } .history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1.3fr) minmax(0, 0.7fr); row-gap: 6px; }
.history-resize-handle { display: none; } .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; } .shell-body { gap: 6px; }
.left-sidebar { gap: 6px; grid-template-rows: minmax(150px, 0.7fr) minmax(145px, 0.55fr) minmax(190px, 1fr); } .left-sidebar {
.left-sidebar:has(.stash-panel.collapsed) { grid-template-rows: minmax(150px, 0.8fr) auto minmax(190px, 1.1fr); } 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; } .section-head { min-height: 40px; padding: 6px 10px; }
.repo-summary { height: 40px; padding: 0 10px; } .repo-summary { height: 40px; padding: 0 10px; }
.repo-branch { max-width: 160px; } .repo-branch { max-width: 160px; }
@@ -4297,9 +4452,35 @@
body { overflow: auto; } body { overflow: auto; }
.shell-body { min-height: 100%; gap: 6px; } .shell-body { min-height: 100%; gap: 6px; }
.workspace { grid-template-columns: 1fr; 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; } .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; } .history-aside.file-history-collapsed { grid-template-rows: minmax(200px, 1fr) 42px; }
.left-sidebar:has(.stash-panel.collapsed) { grid-template-rows: minmax(180px, 1fr) auto minmax(220px, 1.1fr); } .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); } .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-form { grid-template-columns: 1fr; }
.repo-tabbar { grid-template-columns: minmax(0, 1fr) auto; } .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); } .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); } .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; } .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-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 { grid-template-columns: auto minmax(0, 1fr); align-items: start; }
.file-search-hit .status-badge, .file-search-hit .status-badge,
+22 -2
View File
@@ -57,6 +57,8 @@
onCreateTag: (name: string, message: string) => void | Promise<void>; onCreateTag: (name: string, message: string) => void | Promise<void>;
onDeleteTag: (tag: GitTag) => void | Promise<void>; onDeleteTag: (tag: GitTag) => void | Promise<void>;
onPushTag: (tag: GitTag) => void | Promise<void>; onPushTag: (tag: GitTag) => void | Promise<void>;
collapsed?: boolean;
onToggleCollapsed?: () => void;
} }
let { let {
@@ -75,6 +77,8 @@
onCreateTag = () => {}, onCreateTag = () => {},
onDeleteTag = () => {}, onDeleteTag = () => {},
onPushTag = () => {}, onPushTag = () => {},
collapsed = false,
onToggleCollapsed = () => {},
}: Props = $props(); }: Props = $props();
let localOpen = $state(true); let localOpen = $state(true);
@@ -355,7 +359,7 @@
<svelte:window on:click={closeAllContextMenus} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeAllContextMenus} /> <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 class="section-head">
<div> <div>
<span class="eyebrow">Branches</span> <span class="eyebrow">Branches</span>
@@ -373,10 +377,26 @@
<Plus size={14} aria-hidden="true" /> <Plus size={14} aria-hidden="true" />
</button> </button>
<span class="pill pill-count">{branches.length}</span> <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>
</div> </div>
{#if !hasRepository} {#if collapsed}
<!-- collapsed -->
{:else if !hasRepository}
<p class="blank-state">Open a repository to list branches.</p> <p class="blank-state">Open a repository to list branches.</p>
{:else if branches.length === 0 && tags.length === 0} {:else if branches.length === 0 && tags.length === 0}
<p class="blank-state">No branches returned.</p> <p class="blank-state">No branches returned.</p>
+22 -2
View File
@@ -38,6 +38,8 @@
onSelectNode: (node: ExplorerNode) => void; onSelectNode: (node: ExplorerNode) => void;
onOpenFile: (node: ExplorerNode) => void; onOpenFile: (node: ExplorerNode) => void;
onBlame: (node: ExplorerNode) => void; onBlame: (node: ExplorerNode) => void;
collapsed?: boolean;
onToggleCollapsed?: () => void;
} }
let { let {
@@ -53,6 +55,8 @@
onSelectNode = () => {}, onSelectNode = () => {},
onOpenFile = () => {}, onOpenFile = () => {},
onBlame = () => {}, onBlame = () => {},
collapsed = false,
onToggleCollapsed = () => {},
}: Props = $props(); }: Props = $props();
let contextNode = $state<ExplorerNode | null>(null); let contextNode = $state<ExplorerNode | null>(null);
@@ -203,7 +207,7 @@
<svelte:window on:click={closeFileContextMenu} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeFileContextMenu} /> <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 class="section-head">
<div> <div>
<span class="eyebrow">Explorer</span> <span class="eyebrow">Explorer</span>
@@ -231,10 +235,26 @@
<Folder size={14} aria-hidden="true" /> <Folder size={14} aria-hidden="true" />
</button> </button>
<span class="pill pill-count">{repoFiles.length}</span> <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>
</div> </div>
{#if !hasRepository} {#if collapsed}
<!-- collapsed -->
{:else if !hasRepository}
<p class="blank-state">Open a repository to browse files.</p> <p class="blank-state">Open a repository to browse files.</p>
{:else if repoFiles.length === 0} {:else if repoFiles.length === 0}
<p class="blank-state">No files returned.</p> <p class="blank-state">No files returned.</p>
+35 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <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 iconUrl from "../../../src-tauri/icons/icon.png";
import type { GitCommit } from "../types"; import type { GitCommit } from "../types";
@@ -12,6 +12,8 @@
isLoading?: boolean; isLoading?: boolean;
onDiff: (commit: GitCommit) => void; onDiff: (commit: GitCommit) => void;
onRestore: (commit: GitCommit) => void; onRestore: (commit: GitCommit) => void;
collapsed?: boolean;
onToggleCollapsed?: () => void;
} }
let { let {
@@ -23,6 +25,8 @@
isLoading = false, isLoading = false,
onDiff = () => {}, onDiff = () => {},
onRestore = () => {}, onRestore = () => {},
collapsed = false,
onToggleCollapsed = () => {},
}: Props = $props(); }: Props = $props();
function formatCommitDate(value: string): string { function formatCommitDate(value: string): string {
@@ -76,7 +80,23 @@
} }
</script> </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="section-head file-history-head">
<div class="file-history-heading"> <div class="file-history-heading">
<span class="eyebrow">{selectedExplorerLabel}</span> <span class="eyebrow">{selectedExplorerLabel}</span>
@@ -85,7 +105,19 @@
use:pathTooltip={selectedExplorerPath} use:pathTooltip={selectedExplorerPath}
>{selectedExplorerPath ? fileName(selectedExplorerPath) : "No file"}</h2> >{selectedExplorerPath ? fileName(selectedExplorerPath) : "No file"}</h2>
</div> </div>
<div class="file-history-head-actions">
<span class="pill pill-count file-history-count">{fileHistory.length}</span> <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> </div>
{#if !hasRepository} {#if !hasRepository}
@@ -136,4 +168,5 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{/if}
</section> </section>
+15 -11
View File
@@ -11,6 +11,8 @@
onApply: (stash: GitStash) => void; onApply: (stash: GitStash) => void;
onPop: (stash: GitStash) => void; onPop: (stash: GitStash) => void;
onDrop: (stash: GitStash) => void; onDrop: (stash: GitStash) => void;
collapsed?: boolean;
onToggleCollapsed?: () => void;
} }
let { let {
@@ -22,11 +24,12 @@
onApply = () => {}, onApply = () => {},
onPop = () => {}, onPop = () => {},
onDrop = () => {}, onDrop = () => {},
collapsed = false,
onToggleCollapsed = () => {},
}: Props = $props(); }: Props = $props();
let message = $state(""); let message = $state("");
let includeUntracked = $state(true); let includeUntracked = $state(true);
let open = $state(false);
function submitPush() { function submitPush() {
onPush(message, includeUntracked); onPush(message, includeUntracked);
@@ -38,31 +41,32 @@
} }
</script> </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 class="section-head">
<div> <div>
<span class="eyebrow">Stash</span> <span class="eyebrow">Stash</span>
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Shelved changes</h2> <h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Shelved changes</h2>
</div> </div>
<div class="stash-head-actions"> <div class="stash-head-actions">
<span class="pill pill-count">{stashes.length}</span>
<button <button
class="stash-toggle" class="stash-toggle panel-collapse-toggle"
type="button" type="button"
onclick={() => { open = !open; }} onclick={onToggleCollapsed}
aria-expanded={open} aria-expanded={!collapsed}
title={open ? "Collapse stash panel" : "Expand stash panel"} title={collapsed ? "Expand stash panel" : "Collapse stash panel"}
aria-label={collapsed ? "Expand stash panel" : "Collapse stash panel"}
> >
{#if open} {#if collapsed}
<ChevronDown size={14} aria-hidden="true" />
{:else}
<ChevronRight size={14} aria-hidden="true" /> <ChevronRight size={14} aria-hidden="true" />
{:else}
<ChevronDown size={14} aria-hidden="true" />
{/if} {/if}
</button> </button>
<span class="pill pill-count">{stashes.length}</span>
</div> </div>
</div> </div>
{#if !open} {#if collapsed}
<!-- collapsed --> <!-- collapsed -->
{:else if !hasRepository} {:else if !hasRepository}
<div class="blank-state">No repository loaded.</div> <div class="blank-state">No repository loaded.</div>