ExportDialog.svelte
1<script lang="ts">
2 import { showExportDialog } from '$lib/stores/ui';
3 import { sendReviewToClaude } from '$lib/stores/terminal';
4
5 let exporting = $state(false);
6 let result = $state<{ success: boolean; path?: string; message?: string; text?: string } | null>(null);
7 let clipboardState = $state<'idle' | 'loading' | 'ready' | 'copied'>('idle');
8 let pendingClipboardText = $state<string | null>(null);
9
10 async function exportAs(format: string) {
11 if (format === 'clipboard') {
12 clipboardState = 'loading';
13 pendingClipboardText = null;
14 try {
15 const res = await fetch('/api/export', {
16 method: 'POST',
17 headers: { 'Content-Type': 'application/json' },
18 body: JSON.stringify({ format })
19 });
20 const data = await res.json();
21 if (data?.text) {
22 pendingClipboardText = data.text;
23 // Ensure "Preparing..." is visible long enough for user to notice
24 await new Promise((r) => setTimeout(r, 800));
25 clipboardState = 'ready';
26 } else {
27 result = { success: false, message: 'No content to copy.' };
28 clipboardState = 'idle';
29 }
30 } catch (err) {
31 result = { success: false, message: String(err) };
32 clipboardState = 'idle';
33 }
34 return;
35 }
36
37 exporting = true;
38 result = null;
39 try {
40 if (format === 'claude') {
41 const res = await sendReviewToClaude();
42 if (res.success) {
43 $showExportDialog = false;
44 return;
45 }
46 result = res;
47 return;
48 }
49
50 const res = await fetch('/api/export', {
51 method: 'POST',
52 headers: { 'Content-Type': 'application/json' },
53 body: JSON.stringify({ format })
54 });
55 result = await res.json();
56 } catch (err) {
57 result = { success: false, message: String(err) };
58 } finally {
59 exporting = false;
60 }
61 }
62
63 function copyNow() {
64 if (!pendingClipboardText) return;
65 navigator.clipboard.writeText(pendingClipboardText).then(() => {
66 clipboardState = 'copied';
67 pendingClipboardText = null;
68 setTimeout(() => { clipboardState = 'idle'; }, 2000);
69 }).catch(() => {
70 const textarea = document.createElement('textarea');
71 textarea.value = pendingClipboardText!;
72 textarea.style.position = 'fixed';
73 textarea.style.left = '-9999px';
74 document.body.appendChild(textarea);
75 textarea.focus();
76 textarea.select();
77 document.execCommand('copy');
78 document.body.removeChild(textarea);
79 clipboardState = 'copied';
80 pendingClipboardText = null;
81 setTimeout(() => { clipboardState = 'idle'; }, 2000);
82 });
83 }
84
85 function close() {
86 $showExportDialog = false;
87 result = null;
88 }
89</script>
90
91{#if $showExportDialog}
92 <!-- svelte-ignore a11y_click_events_have_key_events -->
93 <!-- svelte-ignore a11y_no_static_element_interactions -->
94 <div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick={close}>
95 <!-- svelte-ignore a11y_click_events_have_key_events -->
96 <!-- svelte-ignore a11y_no_static_element_interactions -->
97 <div class="bg-panel border border-border-strong rounded-xl shadow-2xl w-full max-w-md mx-4" onclick={(e) => e.stopPropagation()}>
98 <div class="px-5 py-4 border-b border-border">
99 <h2 class="text-lg font-semibold text-primary">Export Review</h2>
100 <p class="text-sm text-tertiary mt-1">Send your review comments to AI or save for reference</p>
101 </div>
102
103 <div class="p-5 space-y-2">
104 <button
105 class="w-full text-left px-4 py-3 rounded-lg border border-border-strong hover:bg-hover/50 transition-colors group"
106 onclick={() => exportAs('claude')}
107 disabled={exporting}
108 >
109 <div class="text-sm font-medium text-primary group-hover:opacity-80">Send to Claude Code</div>
110 <div class="text-xs text-tertiary mt-0.5">Stream review to Claude with live output in terminal</div>
111 </button>
112
113 <button
114 class="w-full text-left px-4 py-3 rounded-lg border transition-all {clipboardState === 'ready' ? 'border-accent-blue bg-accent-blue/10 ring-1 ring-accent-blue/30' : clipboardState === 'copied' ? 'border-accent-green bg-accent-green/10' : 'border-border-strong hover:bg-hover/50'} group"
115 onclick={() => clipboardState === 'ready' ? copyNow() : exportAs('clipboard')}
116 disabled={exporting || clipboardState === 'loading'}
117 >
118 {#if clipboardState === 'loading'}
119 <div class="text-sm font-medium text-primary flex items-center gap-2">
120 <span class="animate-spin">⏳</span> Preparing content...
121 </div>
122 <div class="text-xs text-tertiary mt-0.5">Generating review text</div>
123 {:else if clipboardState === 'ready'}
124 <div class="text-sm font-medium text-accent-blue flex items-center gap-2">
125 📋 Content ready — click to copy
126 </div>
127 <div class="text-xs text-accent-blue/70 mt-0.5">Click now to copy to clipboard</div>
128 {:else if clipboardState === 'copied'}
129 <div class="text-sm font-medium text-accent-green flex items-center gap-2">
130 ✓ Copied to clipboard!
131 </div>
132 {:else}
133 <div class="text-sm font-medium text-primary group-hover:opacity-80">Copy to Clipboard</div>
134 <div class="text-xs text-tertiary mt-0.5">Copy structured review text for any AI tool</div>
135 {/if}
136 </button>
137
138 <button
139 class="w-full text-left px-4 py-3 rounded-lg border border-border-strong hover:bg-hover/50 transition-colors group"
140 onclick={() => exportAs('json')}
141 disabled={exporting}
142 >
143 <div class="text-sm font-medium text-primary group-hover:opacity-80">Export as JSON</div>
144 <div class="text-xs text-tertiary mt-0.5">Save .lcr-review.json in project root</div>
145 </button>
146
147 <button
148 class="w-full text-left px-4 py-3 rounded-lg border border-border-strong hover:bg-hover/50 transition-colors group"
149 onclick={() => exportAs('markdown')}
150 disabled={exporting}
151 >
152 <div class="text-sm font-medium text-primary group-hover:opacity-80">Export as Markdown</div>
153 <div class="text-xs text-tertiary mt-0.5">Save .lcr-review.md in project root</div>
154 </button>
155 </div>
156
157 {#if result}
158 <div class="px-5 pb-3">
159 <div class="text-sm rounded-lg px-3 py-2 {result.success ? 'bg-accent-green/10 text-accent-green' : 'bg-accent-red/10 text-accent-red'}">
160 {result.message || result.path || 'Done!'}
161 </div>
162 </div>
163 {/if}
164
165 <div class="px-5 py-3 border-t border-border flex justify-end">
166 <button
167 class="text-sm px-4 py-2 rounded text-tertiary hover:text-primary hover:bg-hover transition-colors"
168 onclick={close}
169 >
170 Close
171 </button>
172 </div>
173 </div>
174 </div>
175{/if}