Files
GitLite/src/lib/components/ReflogDialog.svelte
T
Christoph d26d2ac982 refactor(dialogs): add unified dialog header chrome and opt-in markup
Introduce a shared dialog header style (.unified-dialog-header) in app.css and opt dialog components into the new chrome by updating their header markup. Headers now use unified-dialog-icon and unified-dialog-text elements (and import the matching Lucide icons where needed), which standardizes icon placement, title/eyebrow layout, close button styling and responsive behavior. The Command Palette layout was adjusted to include the new header and its grid rows.

The CSS is explicitly opt-in (so page/section headers remain unchanged) and includes hover/focus styles and a theme-sensitive close color variable. This commit is a UI refactor only — no API or behavior logic changes.
2026-09-11 22:50:31 +02:00

82 lines
4.8 KiB
Svelte

<script lang="ts">
import { GitBranch, History, LoaderCircle, Search, ShieldCheck, X } from "@lucide/svelte";
import type { ReflogEntry } from "../types";
interface Props {
entries: ReflogEntry[];
currentHash: string;
isLoading: boolean;
isBusy: boolean;
operation: string;
error: string;
onPreview: (entry: ReflogEntry) => void;
onRestore: (entry: ReflogEntry, branch: string) => void;
onClose: () => void;
}
let { entries = [], currentHash = "", isLoading = false, isBusy = false, operation = "", error = "", onPreview = () => {}, onRestore = () => {}, onClose = () => {} }: Props = $props();
let query = $state("");
let selectedHash = $state("");
let recoveryBranch = $state("");
let filteredEntries = $derived(entries.filter((entry) => `${entry.selector} ${entry.action} ${entry.short_hash} ${entry.author_name}`.toLowerCase().includes(query.trim().toLowerCase())));
let selected = $derived(entries.find((entry) => entry.hash === selectedHash) ?? filteredEntries[0] ?? null);
$effect(() => {
if (!selectedHash && entries.length > 0) select(entries[0]);
});
function select(entry: ReflogEntry) {
selectedHash = entry.hash;
recoveryBranch = `recovery/${entry.short_hash}`;
}
</script>
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
<div class="dialog reflog-dialog" role="dialog" aria-modal="true" aria-label="Reflog" tabindex="-1">
<header class="dialog-header unified-dialog-header">
<span class="unified-dialog-icon" aria-hidden="true"><History size={18} /></span>
<div class="unified-dialog-text"><span class="eyebrow">Recovery history</span><h2 class="dialog-title">Reflog</h2></div>
<button class="dialog-close" type="button" onclick={onClose} disabled={isBusy} title="Close"><X size={18} aria-hidden="true" /></button>
</header>
<div class="reflog-body">
<aside class="reflog-list-pane">
<label class="reflog-search"><Search size={15} aria-hidden="true" /><input bind:value={query} placeholder="Search actions, hashes or authors" aria-label="Search reflog" /></label>
{#if isLoading}
<div class="blank-state"><LoaderCircle class="spin" size={18} aria-hidden="true" /> Loading reflog…</div>
{:else if filteredEntries.length === 0}
<div class="blank-state">No reflog entries match this search.</div>
{:else}
<div class="reflog-list" role="listbox" aria-label="Reflog entries">
{#each filteredEntries as entry (`${entry.selector}:${entry.hash}`)}
<button class:active={selected?.selector === entry.selector} type="button" role="option" aria-selected={selected?.selector === entry.selector} onclick={() => select(entry)}>
<span class="reflog-row-top"><code>{entry.selector}</code><span>{new Date(entry.date).toLocaleString()}</span></span>
<strong>{entry.action}</strong>
<span class="reflog-row-bottom"><code>{entry.short_hash}</code><span>{entry.author_name}</span></span>
</button>
{/each}
</div>
{/if}
</aside>
<section class="reflog-detail">
{#if error}<div class="rebase-warning error">{error}</div>{/if}
{#if selected}
<div class="reflog-detail-head"><History size={20} aria-hidden="true" /><div><span class="eyebrow">{selected.selector}</span><h3>{selected.action}</h3></div></div>
<dl><div><dt>Commit</dt><dd><code>{selected.hash}</code></dd></div><div><dt>Author</dt><dd>{selected.author_name}</dd></div><div><dt>Date</dt><dd>{new Date(selected.date).toLocaleString()}</dd></div></dl>
<button class="btn-secondary reflog-preview" type="button" onclick={() => onPreview(selected)} disabled={isBusy || selected.hash === currentHash}><History size={15} aria-hidden="true" /> Preview changes to current HEAD</button>
<div class="reflog-recovery-card">
<div class="reflog-recovery-title"><ShieldCheck size={18} aria-hidden="true" /><div><strong>Safe recovery</strong><span>Create a new branch here. The current branch is not reset or deleted.</span></div></div>
<label><span>Recovery branch</span><div><GitBranch size={15} aria-hidden="true" /><input bind:value={recoveryBranch} disabled={isBusy} spellcheck="false" /></div></label>
<button class="btn-primary" type="button" onclick={() => onRestore(selected, recoveryBranch.trim())} disabled={isBusy || !recoveryBranch.trim()}>
{#if operation === "Restoring reflog entry"}<LoaderCircle class="spin" size={16} aria-hidden="true" />{:else}<ShieldCheck size={16} aria-hidden="true" />{/if}
Create and checkout recovery branch
</button>
</div>
{:else}
<div class="blank-state">Select a reflog entry to inspect or recover it.</div>
{/if}
</section>
</div>
</div>
</div>