Toolbar.svelte

  1<script lang="ts">
  2	import { viewMode, showCommitDialog, showExportDialog, theme } from '$lib/stores/ui';
  3	import { stats, reviewStatus, submitReview } from '$lib/stores/review';
  4	import { diffMode, diffScope, baseBranch, approvedCount, uncommittedCount, diffStats, vcs, loadAllDiffs, setDiffScope, loadUncommittedFiles } from '$lib/stores/files';
  5	import { changes, currentChange, approveChange } from '$lib/stores/changes';
  6
  7	let showReviewMenu = $state(false);
  8	let toast = $state<string | null>(null);
  9
 10	// In jj, approve is available when the viewed change is a pending descendant
 11	// of main; in git, when there are uncommitted changes.
 12	const canApprove = $derived(
 13		$vcs === 'jj'
 14			? $changes.some((c) => c.changeId === $currentChange)
 15			: $uncommittedCount.total > 0
 16	);
 17
 18	function openReviewMenu() {
 19		showReviewMenu = !showReviewMenu;
 20		if (showReviewMenu) loadUncommittedFiles();
 21	}
 22
 23	function showToast(message: string) {
 24		toast = message;
 25		setTimeout(() => {
 26			toast = null;
 27		}, 3000);
 28	}
 29
 30	async function handleApprove() {
 31		await submitReview('approved');
 32		if ($vcs === 'jj') {
 33			// Move the `main` bookmark forward to the change being reviewed.
 34			try {
 35				await approveChange($currentChange ?? undefined);
 36				showToast('Approved — main updated');
 37			} catch (err) {
 38				showToast(String(err));
 39			}
 40		} else {
 41			$showCommitDialog = true;
 42		}
 43	}
 44
 45	async function handleRequestChanges() {
 46		await submitReview('changes_requested');
 47		$showExportDialog = true;
 48	}
 49
 50	async function setDiffMode(mode: 'full' | 'unstaged') {
 51		$diffMode = mode;
 52		await loadAllDiffs(mode);
 53	}
 54</script>
 55
 56<header class="bg-panel border-b border-border px-4 py-2 flex items-center gap-3">
 57	<h1 class="text-sm font-semibold text-primary mr-4">Local Code Review</h1>
 58
 59	<span
 60		class="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded border {$vcs === 'jj' ? 'border-accent-blue/40 text-accent-blue' : 'border-border-strong text-muted'}"
 61		title={$vcs === 'jj' ? 'Jujutsu repository' : 'Git repository'}
 62	>
 63		{$vcs}
 64	</span>
 65
 66	<!-- View mode toggle -->
 67	<div class="flex bg-surface rounded overflow-hidden border border-border">
 68		<button
 69			class="text-xs px-3 py-1 transition-colors {$viewMode === 'unified' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
 70			onclick={() => ($viewMode = 'unified')}
 71		>
 72			Unified
 73		</button>
 74		<button
 75			class="text-xs px-3 py-1 transition-colors {$viewMode === 'split' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
 76			onclick={() => ($viewMode = 'split')}
 77		>
 78			Split
 79		</button>
 80	</div>
 81
 82	<!-- Diff scope toggle -->
 83	<div class="flex bg-surface rounded overflow-hidden border border-border">
 84		<button
 85			class="text-xs px-3 py-1 transition-colors {$diffScope === 'uncommitted' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
 86			onclick={() => setDiffScope('uncommitted')}
 87		>
 88			Uncommitted
 89		</button>
 90		<button
 91			class="text-xs px-3 py-1 transition-colors {$diffScope === 'worktree' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
 92			onclick={() => setDiffScope('worktree')}
 93			title={$baseBranch ? `Comparing against ${$baseBranch}` : 'Show all branch changes'}
 94		>
 95			Worktree
 96		</button>
 97	</div>
 98
 99	<!-- Diff mode toggle -->
100	<div class="flex bg-surface rounded overflow-hidden border border-border">
101		<button
102			class="text-xs px-3 py-1 transition-colors {$diffMode === 'full' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
103			onclick={() => setDiffMode('full')}
104		>
105			Full Diff
106		</button>
107		<button
108			class="text-xs px-3 py-1 transition-colors {$diffMode === 'unstaged' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
109			onclick={() => setDiffMode('unstaged')}
110		>
111			Needs Review
112		</button>
113	</div>
114
115	<div class="flex-1"></div>
116
117	<!-- Diff stats -->
118	{#if $approvedCount.total > 0}
119		<span class="text-xs text-tertiary flex items-center gap-2">
120			<span><span class="text-accent-green">{$approvedCount.approved}</span>/{$approvedCount.total} approved</span>
121			<span class="text-faint">|</span>
122			<span class="text-accent-green">+{$diffStats.additions}</span>
123			<span class="text-accent-red">-{$diffStats.deletions}</span>
124			{#if $diffStats.unapprovedAdditions > 0 || $diffStats.unapprovedDeletions > 0}
125				<span class="text-faint">|</span>
126				<span class="text-accent-yellow">
127					{$diffStats.unapprovedAdditions + $diffStats.unapprovedDeletions} lines to review
128				</span>
129			{/if}
130		</span>
131	{/if}
132
133	<!-- Stats -->
134	{#if $stats.totalComments > 0}
135		<span class="text-xs text-tertiary">
136			{$stats.totalComments} comment{$stats.totalComments !== 1 ? 's' : ''}
137			{#if $stats.unresolvedThreads > 0}
138				<span class="text-accent-yellow">({$stats.unresolvedThreads} unresolved)</span>
139			{/if}
140		</span>
141	{/if}
142
143	<!-- Status badge -->
144	{#if $reviewStatus !== 'pending'}
145		<span
146			class="text-xs px-2 py-0.5 rounded-full {$reviewStatus === 'approved' ? 'bg-accent-green/20 text-accent-green' : 'bg-accent-yellow/20 text-accent-yellow'}"
147		>
148			{$reviewStatus === 'approved' ? 'Approved' : 'Changes Requested'}
149		</span>
150	{/if}
151
152	<!-- Theme toggle -->
153	<button
154		class="text-xs px-2 py-1.5 rounded border border-border-strong text-tertiary hover:text-primary hover:bg-hover transition-colors"
155		onclick={() => theme.toggle()}
156		title="Toggle theme"
157	>
158		{$theme === 'dark' ? '☀️' : '🌙'}
159	</button>
160
161	<!-- Export button -->
162	<button
163		class="text-xs px-3 py-1.5 rounded border border-border-strong text-secondary hover:bg-hover transition-colors"
164		onclick={() => ($showExportDialog = true)}
165	>
166		Export
167	</button>
168
169	<!-- Review actions -->
170	<div class="relative">
171		<button
172			class="text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-500 transition-colors"
173			onclick={openReviewMenu}
174		>
175			Review ▾
176		</button>
177
178		{#if showReviewMenu}
179			<!-- svelte-ignore a11y_click_events_have_key_events -->
180			<!-- svelte-ignore a11y_no_static_element_interactions -->
181			<div
182				class="fixed inset-0 z-10"
183				onclick={() => (showReviewMenu = false)}
184			></div>
185			<div class="absolute right-0 top-full mt-1 bg-panel border border-border-strong rounded-lg shadow-xl z-20 py-1 min-w-[220px]">
186				<button
187					class="w-full text-left px-3 py-2 text-sm text-secondary hover:bg-hover transition-colors"
188					onclick={() => { handleRequestChanges(); showReviewMenu = false; }}
189				>
190					<span class="text-accent-yellow mr-2">↻</span> Request Changes (send to AI)
191				</button>
192				<button
193					class="w-full text-left px-3 py-2 text-sm transition-colors {!canApprove ? 'text-muted cursor-not-allowed' : 'text-secondary hover:bg-hover'}"
194					disabled={!canApprove}
195					onclick={() => { handleApprove(); showReviewMenu = false; }}
196				>
197					<span class="text-accent-green mr-2">✓</span> {$vcs === 'jj' ? 'Approve' : 'Approve & Commit'}
198					{#if !canApprove}
199						<span class="text-xs text-muted ml-1">(nothing pending)</span>
200					{/if}
201				</button>
202			</div>
203		{/if}
204	</div>
205
206	{#if toast}
207		<div class="fixed bottom-4 right-4 z-50 bg-panel border border-accent-green/40 text-primary text-sm px-4 py-2 rounded-lg shadow-xl">
208			{toast}
209		</div>
210	{/if}
211</header>