Add two new sidebar components (WorktreePanel, TagsPanel) and integrate them into the compact left navigation layout: - Wire up imports and rendering in App.svelte, including collapse toggles and resize handles for both panels. Persisted heights (localStorage) and keyboard resizing are supported; heights are clamped between 80 and 420px with a 140px default. - Extend buildLeftSidebarRows and left-sidebar grid/template styles to include the new panels and reduce panel-handle thickness. Add extensive CSS for the compact accordion navigation, worktree and tags lists. - Move tag and worktree management UI out of BranchPanel (remove tag props), and add dedicated handlers in App.svelte for the new panels. - Refactor worktree loading: introduce a worktreeLoadId to guard async refreshWorktrees() calls and avoid race conditions. refreshWorktrees(path?) now takes an optional path and updates worktree state only when the request is still relevant. - Small behavioral tweaks: stash panel default collapsed state changed to true and the explorer resize-handle visibility condition adjusted. This commit only adds the UI/UX integration and local persistence for the new panels and their resizing/refresh behavior.
146 lines
4.7 KiB
Svelte
146 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import { Archive, ChevronDown, ChevronRight, Download, Plus, Trash2, Upload } from "@lucide/svelte";
|
|
import type { GitStash } from "../types";
|
|
|
|
interface Props {
|
|
stashes: GitStash[];
|
|
changedCount: number;
|
|
hasRepository: boolean;
|
|
isBusy: boolean;
|
|
onPush: (message: string, includeUntracked: boolean) => void;
|
|
onApply: (stash: GitStash) => void;
|
|
onPop: (stash: GitStash) => void;
|
|
onDrop: (stash: GitStash) => void;
|
|
collapsed?: boolean;
|
|
onToggleCollapsed?: () => void;
|
|
}
|
|
|
|
let {
|
|
stashes = [],
|
|
changedCount = 0,
|
|
hasRepository = false,
|
|
isBusy = false,
|
|
onPush = () => {},
|
|
onApply = () => {},
|
|
onPop = () => {},
|
|
onDrop = () => {},
|
|
collapsed = false,
|
|
onToggleCollapsed = () => {},
|
|
}: Props = $props();
|
|
|
|
let createOpen = $state(false);
|
|
let message = $state("");
|
|
let includeUntracked = $state(true);
|
|
|
|
function submitPush() {
|
|
onPush(message, includeUntracked);
|
|
message = "";
|
|
}
|
|
|
|
function stashTitle(stash: GitStash): string {
|
|
return stash.message || stash.selector;
|
|
}
|
|
</script>
|
|
|
|
<section class="panel stash-panel overflow-hidden" class:collapsed aria-label="Git stash">
|
|
<div class="section-head">
|
|
<h2 class="sidebar-section-title"><Archive size={16} aria-hidden="true" />Stashes</h2>
|
|
<div class="stash-head-actions">
|
|
<button class="stash-toggle" type="button" title="Create stash" aria-label="Create stash"
|
|
disabled={!hasRepository || isBusy || changedCount === 0}
|
|
onclick={() => { createOpen = !createOpen; if (collapsed) { createOpen = true; onToggleCollapsed(); } }}>
|
|
<Plus size={14} aria-hidden="true" />
|
|
</button>
|
|
<span class="pill pill-count">{stashes.length}</span>
|
|
<button
|
|
class="stash-toggle panel-collapse-toggle"
|
|
type="button"
|
|
onclick={onToggleCollapsed}
|
|
aria-expanded={!collapsed}
|
|
title={collapsed ? "Expand stash panel" : "Collapse stash panel"}
|
|
aria-label={collapsed ? "Expand stash panel" : "Collapse stash panel"}
|
|
>
|
|
{#if collapsed}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{#if collapsed}
|
|
<!-- collapsed -->
|
|
{:else if !hasRepository}
|
|
<div class="blank-state">No repository loaded.</div>
|
|
{:else}
|
|
{#if createOpen}
|
|
<div class="stash-create">
|
|
<input
|
|
class="stash-input"
|
|
type="text"
|
|
bind:value={message}
|
|
placeholder="Optional message"
|
|
aria-label="Stash message"
|
|
disabled={isBusy || changedCount === 0}
|
|
onkeydown={(event) => {
|
|
if (event.key === "Enter" && changedCount > 0 && !isBusy) submitPush();
|
|
}}
|
|
/>
|
|
<label class="stash-check">
|
|
<input type="checkbox" bind:checked={includeUntracked} disabled={isBusy || changedCount === 0} />
|
|
Untracked
|
|
</label>
|
|
<button
|
|
class="btn-sm stash-save-button"
|
|
type="button"
|
|
onclick={submitPush}
|
|
disabled={isBusy || changedCount === 0}
|
|
title="Save current working tree changes to a stash"
|
|
>
|
|
<Archive size={14} aria-hidden="true" />
|
|
Stash
|
|
</button>
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
{#if stashes.length === 0}
|
|
<div class="blank-state stash-empty">No stashes saved.</div>
|
|
{:else}
|
|
<div class="stash-list">
|
|
{#each stashes as stash (stash.selector)}
|
|
<article class="stash-row">
|
|
<div class="stash-row-main">
|
|
<strong title={stashTitle(stash)}>{stashTitle(stash)}</strong>
|
|
<span>
|
|
{stash.selector}
|
|
{#if stash.branch}
|
|
on {stash.branch}
|
|
{/if}
|
|
{#if stash.date}
|
|
- {stash.date}
|
|
{/if}
|
|
</span>
|
|
</div>
|
|
<div class="stash-actions">
|
|
<button class="btn-sm" type="button" onclick={() => onApply(stash)} disabled={isBusy} title="Apply stash and keep it">
|
|
<Download size={13} aria-hidden="true" />
|
|
Apply
|
|
</button>
|
|
<button class="btn-sm" type="button" onclick={() => onPop(stash)} disabled={isBusy} title="Apply stash and remove it if successful">
|
|
<Upload size={13} aria-hidden="true" />
|
|
Pop
|
|
</button>
|
|
<button class="btn-sm danger" type="button" onclick={() => onDrop(stash)} disabled={isBusy} title="Delete stash">
|
|
<Trash2 size={13} aria-hidden="true" />
|
|
Drop
|
|
</button>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</section>
|